Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 34 additions & 1 deletion src/main/java/net/tfminecraft/tfmccore/tfmc/TfmcCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.brigadier.suggestion.SuggestionProvider;
import com.mojang.brigadier.tree.CommandNode;
import com.mojang.brigadier.tree.LiteralCommandNode;

import io.papermc.paper.command.brigadier.CommandSourceStack;
Expand Down Expand Up @@ -148,7 +149,39 @@ public LiteralCommandNode<CommandSourceStack> build() {
return player(p -> poster(p, name)).run(context);
})));

return root.build();
LiteralCommandNode<CommandSourceStack> tree = root.build();
// ConditionalEvents receives PlayerCommandPreprocessEvent before Brigadier executes.
// These paths make its commands visible to the client without running them twice.
for (String path : config.lines("conditional-events.commands")) {
if (!path.matches("[a-z0-9_+-]+(?: [a-z0-9_+-]+)*(?: <args>)?")) {
LOGGER.warning("Invalid conditional-events command path: " + path);
continue;
}
CommandNode<CommandSourceStack> parent = tree;
String[] parts = path.split(" ");
for (int i = 0; i < parts.length; i++) {
CommandNode<CommandSourceStack> child = parent.getChild(parts[i]);
if (child == null) {
if (parts[i].equals("<args>")) {
parent.addChild(Commands.argument("args", StringArgumentType.greedyString())
.executes(context -> Command.SINGLE_SUCCESS).build());
break;
}
var node = Commands.literal(parts[i]);
if (i == parts.length - 1) {
node.executes(context -> Command.SINGLE_SUCCESS);
}
child = node.build();
parent.addChild(child);
} else if (i == parts.length - 1 && child.getCommand() == null) {
// Merge only the endpoint; retain existing permission requirements.
parent.addChild(Commands.literal(parts[i])
.executes(context -> Command.SINGLE_SUCCESS).build());
}
parent = child;
}
}
return tree;
}

private void tips(Player player, boolean disable) {
Expand Down
8 changes: 8 additions & 0 deletions src/main/resources/tfmc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ messages:
cooldown: "<red>You need to wait <yellow><time></yellow> before using this again."
failed: "<red>Something went wrong. Please try again or open a Ticket."

# Literal paths after /tfmc handled by ConditionalEvents on this server, e.g.
# ["calendar", "kit iron", "kit iron remove"]. Restart after changing
# this list. CE still owns execution, permissions, conditions and cooldowns.
# Existing TFMCCore command handlers and permission gates are preserved.
# A final <args> accepts free-form arguments, e.g. "roll <args>".
conditional-events:
commands: []

tips:
# Players holding this permission (in the current server context) do not receive chat tips.
permission: tips.off
Expand Down
42 changes: 42 additions & 0 deletions src/test/java/net/tfminecraft/tfmccore/tfmc/TfmcCommandTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,48 @@ public int nextInt(int bound) {
dispatcher.getRoot().addChild(command.build());
}

@Test
void conditionalEventsPathsParseAndCompleteWithoutRunningCoreActions() throws Exception {
String yaml = Files.readString(Path.of("src/main/resources/tfmc.yml"));
config.loadFromString(yaml.replace("commands: []", "commands: [kit iron remove, kit iron, archaeology clear, masks clear, ban, pack, promote, roll strength, 'roll <args>']"));
dispatcher = new CommandDispatcher<>();
dispatcher.getRoot().addChild(command.build());

TestPlayer steve = player("Steve");
assertTrue(visible(steve).contains("kit"));
assertFalse(visible(steve).contains("ban"));
assertFalse(visible(steve).contains("promote"));
assertEquals(List.of("iron"), visible(steve, "kit"));
assertEquals(List.of("remove"), visible(steve, "kit", "iron"));
run(steve, "tfmc kit iron");
run(steve, "tfmc kit iron remove");
run(steve, "tfmc archaeology clear");
run(steve, "tfmc masks clear");
run(steve, "tfmc roll 20 +3");
run(steve, "tfmc roll strength");
assertTrue(console.isEmpty());
assertTrue(given.isEmpty());
assertTrue(steve.messages.isEmpty());
run(steve, "tfmc pack");
assertEquals(1, packsSent);
assertThrows(CommandSyntaxException.class, () -> run(steve, "tfmc kit gold"));
}

@Test
void invalidConditionalEventsPathsAreSkipped() throws Exception {
String yaml = Files.readString(Path.of("src/main/resources/tfmc.yml"));
config.loadFromString(yaml.replace("commands: []", "commands: ['Kit Iron', 'kit iron', '<args>', calendar]"));
dispatcher = new CommandDispatcher<>();
dispatcher.getRoot().addChild(command.build());

TestPlayer steve = player("Steve");
assertTrue(visible(steve).contains("calendar"));
assertFalse(visible(steve).contains("kit"));
assertFalse(visible(steve).contains("args"));
run(steve, "tfmc calendar");
assertTrue(steve.messages.isEmpty());
}

@Test
void playersSeeOnlyPublicSubcommands() {
assertEquals(List.of("tips", "pack", "booster", "drinks", "patreon", "statues", "patterns", "masks", "map", "date", "poster"),
Expand Down
Loading