From bec86cba0ec7699f5a5bdf2d67e4b2cb72f0b628 Mon Sep 17 00:00:00 2001 From: Lukas Jost Date: Tue, 8 Sep 2026 21:52:14 +0200 Subject: [PATCH] feat(editor): author NPC actions in game --- .../editor/catalog/SceneCatalogBinding.kt | 10 +++ .../editor/mutation/SceneMutationResult.kt | 2 + .../scene/editor/mutation/SceneMutations.kt | 63 +++++++++++++++ .../editor/catalog/SceneCatalogBindingTest.kt | 44 +++++++++++ .../editor/mutation/SceneMutationsTest.kt | 79 +++++++++++++++++++ .../editor/paper/command/SceneCommand.kt | 20 ++++- .../paper/command/SceneCommandAuthorizer.kt | 5 +- .../paper/command/SceneCommandDispatcher.kt | 4 + .../editor/paper/command/SceneTabCompleter.kt | 26 +++++- paper/src/main/resources/plugin.yml | 2 + .../paper/command/SceneCommandAdapterTest.kt | 41 +++++++++- .../command/SceneCommandAuthorizerTest.kt | 21 +++++ .../command/SceneCommandDispatcherTest.kt | 6 ++ .../paper/command/SceneTabCompleterTest.kt | 22 ++++++ 14 files changed, 338 insertions(+), 7 deletions(-) diff --git a/common/src/main/kotlin/gg/grounds/scene/editor/catalog/SceneCatalogBinding.kt b/common/src/main/kotlin/gg/grounds/scene/editor/catalog/SceneCatalogBinding.kt index 5f9a246..24955b6 100644 --- a/common/src/main/kotlin/gg/grounds/scene/editor/catalog/SceneCatalogBinding.kt +++ b/common/src/main/kotlin/gg/grounds/scene/editor/catalog/SceneCatalogBinding.kt @@ -75,6 +75,16 @@ private constructor( matches.all { (path, candidate) -> actionVerified(document, path, candidate) } } + /** Returns an action definition only from the action catalog pinned by [document]. */ + fun actionDefinition(document: SceneDocument, key: gg.grounds.scene.format.ActionKey) = + actionCatalogFor(document)?.actions?.get(key) + + /** Actions that this editor slice can construct without collecting arguments. */ + fun parameterlessActionsFor( + document: SceneDocument + ): Collection = + actionCatalogFor(document)?.actions?.values?.filter { it.parameters.isEmpty() }.orEmpty() + private fun actionVerified( document: SceneDocument, path: String, diff --git a/common/src/main/kotlin/gg/grounds/scene/editor/mutation/SceneMutationResult.kt b/common/src/main/kotlin/gg/grounds/scene/editor/mutation/SceneMutationResult.kt index e7d1cb1..7ce06ac 100644 --- a/common/src/main/kotlin/gg/grounds/scene/editor/mutation/SceneMutationResult.kt +++ b/common/src/main/kotlin/gg/grounds/scene/editor/mutation/SceneMutationResult.kt @@ -10,6 +10,8 @@ enum class SceneMutationRejection { WRONG_ASSET_KIND, MISSING_NPC_BOUNDS, INVALID_SCALE, + UNKNOWN_APPLICATION_ACTION, + ACTION_REQUIRES_PARAMETERS, INTRINSIC_INVALID, READ_ONLY_APPLICATION_ACTION, SELECTION_REQUIRED, diff --git a/common/src/main/kotlin/gg/grounds/scene/editor/mutation/SceneMutations.kt b/common/src/main/kotlin/gg/grounds/scene/editor/mutation/SceneMutations.kt index 515a55a..83d4bb8 100644 --- a/common/src/main/kotlin/gg/grounds/scene/editor/mutation/SceneMutations.kt +++ b/common/src/main/kotlin/gg/grounds/scene/editor/mutation/SceneMutations.kt @@ -1,6 +1,7 @@ package gg.grounds.scene.editor.mutation import gg.grounds.scene.editor.catalog.SceneCatalogBinding +import gg.grounds.scene.format.ActionKey import gg.grounds.scene.format.ActivationPolicy import gg.grounds.scene.format.ApplicationAction import gg.grounds.scene.format.AssetKey @@ -12,8 +13,10 @@ import gg.grounds.scene.format.Npc import gg.grounds.scene.format.Prop import gg.grounds.scene.format.SceneDocument import gg.grounds.scene.format.SceneElement +import gg.grounds.scene.format.SceneTrigger import gg.grounds.scene.format.SceneValidation import gg.grounds.scene.format.Transform +import gg.grounds.scene.format.TriggerBinding import gg.grounds.scene.format.Vec3 import java.util.UUID import net.kyori.adventure.text.Component @@ -103,6 +106,13 @@ object SceneMutations { fun setLabel(actor: UUID, target: LocalId, label: Component?): SceneMutation = LabelEdit(actor, target, label) + fun setApplicationAction( + actor: UUID, + target: LocalId, + trigger: SceneTrigger, + action: ActionKey, + ): SceneMutation = ApplicationActionEdit(actor, target, trigger, action) + fun remove(actor: UUID, target: LocalId): SceneMutation = Remove(actor, target) private data class Create( @@ -312,6 +322,59 @@ object SceneMutations { } } + private data class ApplicationActionEdit( + override val actor: UUID, + override val target: LocalId, + private val trigger: SceneTrigger, + private val action: ActionKey, + ) : SceneMutation { + override val name = "npc.action.set" + + override fun apply( + document: SceneDocument, + catalogs: SceneCatalogBinding, + ): SceneMutationResult { + val definition = + catalogs.actionDefinition(document, action) + ?: return rejected(document, SceneMutationRejection.UNKNOWN_APPLICATION_ACTION) + if (definition.parameters.isNotEmpty()) + return rejected(document, SceneMutationRejection.ACTION_REQUIRES_PARAMETERS) + return changeElement(document, target) { element -> + val npc = element as? Npc ?: return@changeElement null + val preserved = + npc.bindings.filterNot { binding -> + binding.trigger == trigger && + binding.conditions.isEmpty() && + binding.cooldownMillis == 0L && + binding.debounceMillis == 0L && + binding.actions.all { it is ApplicationAction } + } + Npc( + npc.id, + npc.group, + npc.transform, + npc.visible, + npc.activation, + npc.body, + npc.label, + npc.labelOffset, + npc.look, + npc.initialAnimation, + npc.interactionBounds, + npc.proximity, + preserved + + TriggerBinding( + trigger, + emptyList(), + 0, + 0, + listOf(ApplicationAction(action, emptyMap())), + ), + ) + } + } + } + private data class Remove(override val actor: UUID, override val target: LocalId) : SceneMutation { override val name = "element.remove" diff --git a/common/src/test/kotlin/gg/grounds/scene/editor/catalog/SceneCatalogBindingTest.kt b/common/src/test/kotlin/gg/grounds/scene/editor/catalog/SceneCatalogBindingTest.kt index f9b30bf..ec916bf 100644 --- a/common/src/test/kotlin/gg/grounds/scene/editor/catalog/SceneCatalogBindingTest.kt +++ b/common/src/test/kotlin/gg/grounds/scene/editor/catalog/SceneCatalogBindingTest.kt @@ -1,11 +1,17 @@ package gg.grounds.scene.editor.catalog import gg.grounds.lobby.scene.LobbySceneCatalogs +import gg.grounds.resourcepacks.catalog.GroundsAssetCatalog +import gg.grounds.scene.format.ActionCatalog +import gg.grounds.scene.format.ActionDefinition import gg.grounds.scene.format.ActionKey +import gg.grounds.scene.format.ActionParameter +import gg.grounds.scene.format.ActionParameterType import gg.grounds.scene.format.ApplicationAction import gg.grounds.scene.format.CatalogId import gg.grounds.scene.format.CatalogReference import gg.grounds.scene.format.LocalId +import gg.grounds.scene.format.NoConstraints import gg.grounds.scene.format.Npc import gg.grounds.scene.format.SceneCatalogReferences import gg.grounds.scene.format.SceneDocument @@ -68,6 +74,44 @@ class SceneCatalogBindingTest { ) } + @Test + fun `parameterless actions exclude catalog entries that require authored arguments`() { + val navigator = ActionKey("grounds:lobby/open_navigator") + val parameterized = ActionKey("grounds:lobby/teleport") + val binding = + SceneCatalogBinding( + GroundsAssetCatalog.catalog, + ActionCatalog( + CatalogId("grounds:actions"), + "1", + mapOf( + navigator to ActionDefinition(navigator, "Navigator", "Open", emptyMap()), + parameterized to + ActionDefinition( + parameterized, + "Teleport", + "Teleport", + mapOf( + LocalId("target") to + ActionParameter( + LocalId("target"), + ActionParameterType.STRING, + true, + null, + NoConstraints, + ) + ), + ), + ), + ), + ) + + assertEquals( + listOf(navigator), + binding.parameterlessActionsFor(binding.newDocument("grounds:test")).map { it.key }, + ) + } + private fun document( source: SceneDocument, catalogs: SceneCatalogReferences = source.catalogs, diff --git a/common/src/test/kotlin/gg/grounds/scene/editor/mutation/SceneMutationsTest.kt b/common/src/test/kotlin/gg/grounds/scene/editor/mutation/SceneMutationsTest.kt index 2209f75..5ee2d72 100644 --- a/common/src/test/kotlin/gg/grounds/scene/editor/mutation/SceneMutationsTest.kt +++ b/common/src/test/kotlin/gg/grounds/scene/editor/mutation/SceneMutationsTest.kt @@ -2,6 +2,7 @@ package gg.grounds.scene.editor.mutation import gg.grounds.scene.editor.catalog.SceneCatalogBinding import gg.grounds.scene.format.ActionCatalog +import gg.grounds.scene.format.ActionDefinition import gg.grounds.scene.format.ActionKey import gg.grounds.scene.format.ApplicationAction import gg.grounds.scene.format.AssetCatalog @@ -12,13 +13,16 @@ import gg.grounds.scene.format.CatalogId import gg.grounds.scene.format.CatalogReference import gg.grounds.scene.format.CatalogVersionRange import gg.grounds.scene.format.EulerRotation +import gg.grounds.scene.format.HandCondition import gg.grounds.scene.format.LocalBounds import gg.grounds.scene.format.LocalId import gg.grounds.scene.format.LookBehavior import gg.grounds.scene.format.Npc import gg.grounds.scene.format.Prop import gg.grounds.scene.format.SceneDocument +import gg.grounds.scene.format.SceneHand import gg.grounds.scene.format.SceneTrigger +import gg.grounds.scene.format.SendMessageAction import gg.grounds.scene.format.Transform import gg.grounds.scene.format.TriggerBinding import gg.grounds.scene.format.Vec3 @@ -82,6 +86,81 @@ class SceneMutationsTest { assertEquals(LocalBounds(Vec3(0.0, 0.9, 0.0), Vec3(0.6, 1.8, 0.6)), npc.interactionBounds) } + @Test + fun `sets a catalogued parameterless npc action without disturbing conditional bindings`() { + val action = ActionKey("grounds:lobby/open_navigator") + val catalogs = + SceneCatalogBinding( + testAssets, + ActionCatalog( + CatalogId("grounds:actions"), + "1", + mapOf(action to ActionDefinition(action, "Navigator", "Open it", emptyMap())), + ), + ) + val preserved = + TriggerBinding( + SceneTrigger.RIGHT_CLICK, + listOf(HandCondition(SceneHand.MAIN)), + 250, + 50, + listOf(SendMessageAction(Component.text("Existing action"))), + ) + val replaceable = + TriggerBinding( + SceneTrigger.RIGHT_CLICK, + emptyList(), + 0, + 0, + listOf(ApplicationAction(action, emptyMap())), + ) + val original = + catalogs.newDocument( + "grounds:test", + elements = + listOf( + Npc( + LocalId("guide"), + null, + Transform( + placement.position, + EulerRotation(0.0, 0.0, 0.0), + Vec3(1.0, 1.0, 1.0), + ), + body = AssetKey("grounds:editor/guide"), + label = null, + labelOffset = Vec3(0.0, 2.25, 0.0), + look = LookBehavior.Fixed, + initialAnimation = null, + interactionBounds = + LocalBounds(Vec3(0.0, 0.9, 0.0), Vec3(0.6, 1.8, 0.6)), + proximity = null, + bindings = listOf(preserved, replaceable), + ) + ), + ) + + val edited = + SceneMutations.setApplicationAction( + actor, + LocalId("guide"), + SceneTrigger.RIGHT_CLICK, + action, + ) + .apply(original, catalogs) + .documentOrThrow() + val npc = edited.elements.single() as Npc + + assertEquals(preserved, npc.bindings.first()) + assertEquals(2, npc.bindings.size) + val actionBinding = npc.bindings.last() + assertEquals(SceneTrigger.RIGHT_CLICK, actionBinding.trigger) + assertTrue(actionBinding.conditions.isEmpty()) + assertEquals(0, actionBinding.cooldownMillis) + assertEquals(0, actionBinding.debounceMillis) + assertEquals(ApplicationAction(action, emptyMap()), actionBinding.actions.single()) + } + @Test fun `creation rejects wrong asset kind missing npc bounds and duplicate ids without changing original`() { val document = diff --git a/paper/src/main/kotlin/gg/grounds/scene/editor/paper/command/SceneCommand.kt b/paper/src/main/kotlin/gg/grounds/scene/editor/paper/command/SceneCommand.kt index 812d706..2ec8e53 100644 --- a/paper/src/main/kotlin/gg/grounds/scene/editor/paper/command/SceneCommand.kt +++ b/paper/src/main/kotlin/gg/grounds/scene/editor/paper/command/SceneCommand.kt @@ -255,7 +255,7 @@ class SceneCommand( val actions = applicationActions(session.document).ifEmpty { "none" } feedback.info( player, - "Scene ${session.document.id.value}: ${session.document.elements.size} elements; preserved application actions (read-only): $actions; dirty=${sessions.hasUnsavedChanges(worldId)}.", + "Scene ${session.document.id.value}: ${session.document.elements.size} elements; application actions: $actions (parameterless catalog actions can be set on NPC triggers); dirty=${sessions.hasUnsavedChanges(worldId)}.", ) } @@ -399,6 +399,17 @@ class SceneCommand( Component.text(args.drop(4).joinToString(" ")), ) else null + "action" -> + if (kind == "npc" && args.getOrNull(3)?.equals("set", true) == true) + SceneMutations.setApplicationAction( + player.uniqueId, + id, + gg.grounds.scene.format.SceneTrigger.valueOf( + args.getOrNull(4)?.uppercase(Locale.ROOT) ?: return null + ), + gg.grounds.scene.format.ActionKey(args.getOrNull(5) ?: return null), + ) + else null "position" -> transformPosition(player, id, args) "rotation" -> transformRotation(player, id, args) else -> null @@ -698,6 +709,13 @@ class SceneCommand( .map { it.key.value } .sorted() + internal fun catalogActions(sender: CommandSender?): List { + val player = sender as? Player ?: return emptyList() + val worldId = resolver.resolve(player)?.worldId ?: return emptyList() + val document = sessions.session(worldId)?.document ?: return emptyList() + return catalogs.parameterlessActionsFor(document).map { it.key.value }.sorted() + } + internal fun elementIds(sender: CommandSender?, npc: Boolean): List { val player = sender as? Player ?: return emptyList() val worldId = resolver.resolve(player)?.worldId ?: return emptyList() diff --git a/paper/src/main/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandAuthorizer.kt b/paper/src/main/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandAuthorizer.kt index 8af7809..0cd316f 100644 --- a/paper/src/main/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandAuthorizer.kt +++ b/paper/src/main/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandAuthorizer.kt @@ -57,7 +57,7 @@ class SceneCommandAuthorizer(private val hasPermission: (String) -> Boolean) { val operation = if (path.getOrNull(1) == "list") "list" else path.getOrNull(2) val allowed = setOf("list", "create", "select", "position", "rotation", "scale", "clone", "remove") + - if (kind == "npc") setOf("label") else emptySet() + if (kind == "npc") setOf("label", "action") else emptySet() return operation ?.takeIf { it in allowed } ?.let { allowedOperation -> @@ -67,7 +67,8 @@ class SceneCommandAuthorizer(private val hasPermission: (String) -> Boolean) { path.getOrNull(3)?.let(::listOf) ?: listOf("set", "here", "add") "rotation" -> path.getOrNull(3)?.let(::listOf) ?: listOf("set", "add") "scale", - "label" -> path.getOrNull(3)?.let(::listOf) ?: listOf("set") + "label", + "action" -> path.getOrNull(3)?.let(::listOf) ?: listOf("set") else -> listOf(null) } modes.map { mode -> diff --git a/paper/src/main/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandDispatcher.kt b/paper/src/main/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandDispatcher.kt index b537964..1624bf9 100644 --- a/paper/src/main/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandDispatcher.kt +++ b/paper/src/main/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandDispatcher.kt @@ -72,6 +72,10 @@ object SceneCommandDispatcher { kind == "npc" && args.getOrNull(3)?.lowercase(Locale.ROOT) == "set" && args.size >= 5 + "action" -> + kind == "npc" && + args.getOrNull(3)?.lowercase(Locale.ROOT) == "set" && + args.size == 6 else -> false } return if (valid) Route.Element(kind, operation) else Route.Invalid diff --git a/paper/src/main/kotlin/gg/grounds/scene/editor/paper/command/SceneTabCompleter.kt b/paper/src/main/kotlin/gg/grounds/scene/editor/paper/command/SceneTabCompleter.kt index 122f7be..cf8776d 100644 --- a/paper/src/main/kotlin/gg/grounds/scene/editor/paper/command/SceneTabCompleter.kt +++ b/paper/src/main/kotlin/gg/grounds/scene/editor/paper/command/SceneTabCompleter.kt @@ -8,10 +8,13 @@ import org.bukkit.command.TabCompleter class SceneTabCompleter( private val assets: (Boolean) -> List, private val elementIds: (CommandSender?, Boolean) -> List, + private val actions: (CommandSender?) -> List = { emptyList() }, ) : TabCompleter { constructor(assets: (Boolean) -> List) : this(assets, { _, _ -> emptyList() }) - constructor(scene: SceneCommand) : this(scene::catalogAssets, scene::elementIds) + constructor( + scene: SceneCommand + ) : this(scene::catalogAssets, scene::elementIds, scene::catalogActions) override fun onTabComplete( sender: CommandSender, @@ -66,7 +69,7 @@ class SceneTabCompleter( "clone", "remove", ) + - if (args[0].equals("npc", true)) listOf("label") + if (args[0].equals("npc", true)) listOf("label", "action") else emptyList() else -> emptyList() } @@ -80,8 +83,27 @@ class SceneTabCompleter( args[2].equals("rotation", true) -> listOf("set", "add") args[2].equals("scale", true) -> listOf("set") args[2].equals("label", true) -> listOf("set") + args[2].equals("action", true) -> listOf("set") else -> emptyList() } + 5 -> + if ( + args[0].equals("npc", true) && + args[2].equals("action", true) && + args[3].equals("set", true) + ) + gg.grounds.scene.format.SceneTrigger.entries.map { + it.name.lowercase(Locale.ROOT) + } + else emptyList() + 6 -> + if ( + args[0].equals("npc", true) && + args[2].equals("action", true) && + args[3].equals("set", true) + ) + actions(sender) + else emptyList() else -> emptyList() } val needle = args.lastOrNull().orEmpty().lowercase(Locale.ROOT) diff --git a/paper/src/main/resources/plugin.yml b/paper/src/main/resources/plugin.yml index 965ef31..13c7623 100644 --- a/paper/src/main/resources/plugin.yml +++ b/paper/src/main/resources/plugin.yml @@ -97,6 +97,8 @@ permissions: description: Scale NPCs grounds.scene.npc.label.set: description: Change NPC labels + grounds.scene.npc.action.set: + description: Set parameterless catalog actions on NPC triggers grounds.scene.npc.clone: description: Clone NPCs grounds.scene.npc.remove: diff --git a/paper/src/test/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandAdapterTest.kt b/paper/src/test/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandAdapterTest.kt index aa901c2..50336cb 100644 --- a/paper/src/test/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandAdapterTest.kt +++ b/paper/src/test/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandAdapterTest.kt @@ -7,6 +7,7 @@ import gg.grounds.scene.editor.repository.WorldSceneRepository import gg.grounds.scene.editor.session.EditorSessionService import gg.grounds.scene.editor.session.ReloadPreparationResult import gg.grounds.scene.format.ActionCatalog +import gg.grounds.scene.format.ActionDefinition import gg.grounds.scene.format.ActionKey import gg.grounds.scene.format.ApplicationAction import gg.grounds.scene.format.AssetCatalog @@ -213,6 +214,30 @@ class SceneCommandAdapterTest { assertTrue(fixture.document().elements.any { it.id.value == "guide-copy" }) } + @Test + fun `npc action command enters the normal undoable session pipeline`() { + val fixture = fixture() + fixture.command.execute(fixture.player, "create", "grounds:editing", "Editing") + fixture.command.execute(fixture.player, "npc", "guide", "create", "grounds:editor/guide") + fixture.command.execute(fixture.player, "npc", "guide", "select") + fixture.command.execute( + fixture.player, + "npc", + "guide", + "action", + "set", + "right_click", + "grounds:lobby/open_navigator", + ) + + assertEquals( + ApplicationAction(ActionKey("grounds:lobby/open_navigator"), emptyMap()), + (fixture.document().elements.single() as Npc).bindings.single().actions.single(), + ) + fixture.command.execute(fixture.player, "undo") + assertTrue((fixture.document().elements.single() as Npc).bindings.isEmpty()) + } + @Test fun `mixed case prop recovery and lease actions execute identically`() { val fixture = fixtureWithProp() @@ -279,7 +304,7 @@ class SceneCommandAdapterTest { actionFixture.sessions.open(actionFixture.worldId, actionDocument(actionFixture.catalogs)) actionFixture.command.execute(actionFixture.player, "info") assertTrue(actionFixture.feedback.infos.last().contains("grounds:award")) - assertTrue(actionFixture.feedback.infos.last().contains("read-only")) + assertTrue(actionFixture.feedback.infos.last().contains("parameterless catalog actions")) } @Test @@ -419,7 +444,19 @@ class SceneCommandAdapterTest { ), ), ), - ActionCatalog(CatalogId("grounds:actions"), "1", emptyMap()), + ActionCatalog( + CatalogId("grounds:actions"), + "1", + mapOf( + ActionKey("grounds:lobby/open_navigator") to + ActionDefinition( + ActionKey("grounds:lobby/open_navigator"), + "Navigator", + "Open navigator", + emptyMap(), + ) + ), + ), ) private fun actionDocument(catalogs: SceneCatalogBinding): SceneDocument { diff --git a/paper/src/test/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandAuthorizerTest.kt b/paper/src/test/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandAuthorizerTest.kt index 37bc39d..699d79d 100644 --- a/paper/src/test/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandAuthorizerTest.kt +++ b/paper/src/test/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandAuthorizerTest.kt @@ -34,6 +34,27 @@ class SceneCommandAuthorizerTest { assertFalse(authorizer.isAllowed(listOf("prop", "marker", "position", "set"))) } + @Test + fun `npc action mutation requires its dedicated permission`() { + val authorizer = SceneCommandAuthorizer { it == "grounds.scene.npc.action.set" } + + assertTrue( + authorizer.isAllowed( + listOf( + "npc", + "guide", + "action", + "set", + "right_click", + "grounds:lobby/open_navigator", + ) + ) + ) + assertFalse( + authorizer.isAllowed(listOf("prop", "marker", "action", "set", "right_click", "x:y")) + ) + } + @Test fun `override administrators may reach explicit lease release`() { val authorizer = SceneCommandAuthorizer { it == "grounds.scene.lease.override" } diff --git a/paper/src/test/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandDispatcherTest.kt b/paper/src/test/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandDispatcherTest.kt index a8039d5..7a5c816 100644 --- a/paper/src/test/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandDispatcherTest.kt +++ b/paper/src/test/kotlin/gg/grounds/scene/editor/paper/command/SceneCommandDispatcherTest.kt @@ -77,5 +77,11 @@ class SceneCommandDispatcherTest { SceneCommandDispatcher.Route.Element("npc", "label"), SceneCommandDispatcher.route(listOf("npc", "id", "label", "set", "Guide")), ) + assertEquals( + SceneCommandDispatcher.Route.Element("npc", "action"), + SceneCommandDispatcher.route( + listOf("npc", "id", "action", "set", "right_click", "grounds:lobby/open_navigator") + ), + ) } } diff --git a/paper/src/test/kotlin/gg/grounds/scene/editor/paper/command/SceneTabCompleterTest.kt b/paper/src/test/kotlin/gg/grounds/scene/editor/paper/command/SceneTabCompleterTest.kt index 30d2b55..c62ab99 100644 --- a/paper/src/test/kotlin/gg/grounds/scene/editor/paper/command/SceneTabCompleterTest.kt +++ b/paper/src/test/kotlin/gg/grounds/scene/editor/paper/command/SceneTabCompleterTest.kt @@ -37,6 +37,28 @@ class SceneTabCompleterTest { ) } + @Test + fun `completes npc action triggers and catalogued action keys`() { + val completer = + SceneTabCompleter( + { emptyList() }, + { _, _ -> emptyList() }, + { _ -> listOf("grounds:lobby/open_navigator") }, + ) + + assertEquals( + listOf("right_click"), + completer.complete(null, arrayOf("npc", "guide", "action", "set", "right_")), + ) + assertEquals( + listOf("grounds:lobby/open_navigator"), + completer.complete( + null, + arrayOf("npc", "guide", "action", "set", "right_click", "grounds:"), + ), + ) + } + @Test fun `filters mixed case paths using normalized leaf permissions`() { val sender = mock(CommandSender::class.java)