From d4c9326490c9a4f73c1a1a20788d9fcceefc2d5b Mon Sep 17 00:00:00 2001 From: Pesek Date: Tue, 15 Jul 2025 22:45:55 +0200 Subject: [PATCH 01/14] added support for single entry data to be included multiple times within a section node --- .../skript/lang/entry/EntryContainer.java | 134 +++++++++++------- .../skript/lang/entry/EntryData.java | 13 ++ .../skript/lang/entry/EntryValidator.java | 73 ++++++---- .../skript/lang/entry/KeyValueEntryData.java | 9 +- .../skript/lang/entry/SectionEntryData.java | 7 +- 5 files changed, 155 insertions(+), 81 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/lang/entry/EntryContainer.java b/src/main/java/org/skriptlang/skript/lang/entry/EntryContainer.java index 6c2dfb59d2d..1382edf3a2c 100644 --- a/src/main/java/org/skriptlang/skript/lang/entry/EntryContainer.java +++ b/src/main/java/org/skriptlang/skript/lang/entry/EntryContainer.java @@ -6,9 +6,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; /** * An EntryContainer is a data container for obtaining the values of the entries of a {@link SectionNode}. @@ -19,11 +17,12 @@ public class EntryContainer { @Nullable private final EntryValidator entryValidator; @Nullable - private final Map handledNodes; + private final Map> handledNodes; private final List unhandledNodes; EntryContainer( - SectionNode source, @Nullable EntryValidator entryValidator, @Nullable Map handledNodes, List unhandledNodes + SectionNode source, @Nullable EntryValidator entryValidator, + @Nullable Map> handledNodes, List unhandledNodes ) { this.source = source; this.entryValidator = entryValidator; @@ -60,11 +59,69 @@ public List getUnhandledNodes() { return unhandledNodes; } + /** + * A method for obtaining a typed entry values. + * @param key The key associated with the entry. + * @param expectedType The class representing the expected type of the entry's values. + * @param useDefaultValue Whether the default value should be used if parsing failed. + * @return The entry's values. May be empty list if the entry is missing or a parsing error occurred. + * @throws RuntimeException If the entry's value is not of the expected type. + */ + @SuppressWarnings("unchecked") + public List getAll(String key, Class expectedType, boolean useDefaultValue) { + List parsed = getAll(key, useDefaultValue); + for (Object object : parsed) { + if (!expectedType.isInstance(object)) + throw new RuntimeException("Expected entry with key '" + key + "' to be '" + + expectedType + "', but got '" + object.getClass() + "'"); + } + return (List) parsed; + } + + /** + * A method for obtaining an entry values with an unknown type. + * @param key The key associated with the entry. + * @param useDefaultValue Whether the default value should be used if parsing failed. + * @return The entry's values. May be empty list if the entry is missing or a parsing error occurred. + */ + public List getAll(String key, boolean useDefaultValue) { + if (entryValidator == null || handledNodes == null) + return Collections.emptyList(); + + EntryData entryData = entryValidator.getEntryData().stream() + .filter(data -> data.getKey().equals(key)) + .findFirst() + .orElse(null); + if (entryData == null) return Collections.emptyList(); + + Collection nodes = handledNodes.get(key); + if (nodes == null || nodes.isEmpty()) { + Object defaultValue = entryData.getDefaultValue(); + return defaultValue != null + ? Collections.singletonList(defaultValue) + : Collections.emptyList(); + } + + List values = new LinkedList<>(); + for (Node node : nodes) { + ParserInstance parser = ParserInstance.get(); + Node oldNode = parser.getNode(); + parser.setNode(node); + Object value = entryData.getValue(node); + if (value == null && useDefaultValue) value = entryData.getDefaultValue(); + parser.setNode(oldNode); + if (value != null) values.add(value); + } + + return values; + } + /** * A method for obtaining a non-null, typed entry value. * This method should ONLY be called if there is no way the entry could return null. - * In general, this means that the entry has a default value (and 'useDefaultValue' is true). This is because even - * though an entry may be required, parsing errors may occur that mean no value can be returned. + * In general, this means that the entry has a default value (and 'useDefaultValue' is true). + * This is because even though an entry may be required, parsing errors may occur that + * mean no value can be returned. * It can also mean that the entry data is simple enough such that it will never return a null value. * @param key The key associated with the entry. * @param expectedType The class representing the expected type of the entry's value. @@ -73,17 +130,18 @@ public List getUnhandledNodes() { * @throws RuntimeException If the entry's value is null, or if it is not of the expected type. */ public R get(String key, Class expectedType, boolean useDefaultValue) { - R value = getOptional(key, expectedType, useDefaultValue); - if (value == null) + List all = getAll(key, expectedType, useDefaultValue); + if (all.isEmpty()) throw new RuntimeException("Null value for asserted non-null value"); - return value; + return all.get(0); // always present } /** * A method for obtaining a non-null entry value with an unknown type. * This method should ONLY be called if there is no way the entry could return null. - * In general, this means that the entry has a default value (and 'useDefaultValue' is true). This is because even - * though an entry may be required, parsing errors may occur that mean no value can be returned. + * In general, this means that the entry has a default value (and 'useDefaultValue' is true). + * This is because even though an entry may be required, parsing errors may occur that + * mean no value can be returned. * It can also mean that the entry data is simple enough such that it will never return a null value. * @param key The key associated with the entry. * @param useDefaultValue Whether the default value should be used if parsing failed. @@ -91,10 +149,10 @@ public R get(String key, Class expectedType, boolean useDefa * @throws RuntimeException If the entry's value is null. */ public Object get(String key, boolean useDefaultValue) { - Object parsed = getOptional(key, useDefaultValue); - if (parsed == null) + List all = getAll(key, useDefaultValue); + if (all.isEmpty()) throw new RuntimeException("Null value for asserted non-null value"); - return parsed; + return all.get(0); // always present } /** @@ -105,15 +163,9 @@ public Object get(String key, boolean useDefaultValue) { * @return The entry's value. May be null if the entry is missing or a parsing error occurred. * @throws RuntimeException If the entry's value is not of the expected type. */ - @Nullable - @SuppressWarnings("unchecked") - public R getOptional(String key, Class expectedType, boolean useDefaultValue) { - Object parsed = getOptional(key, useDefaultValue); - if (parsed == null) - return null; - if (!expectedType.isInstance(parsed)) - throw new RuntimeException("Expected entry with key '" + key + "' to be '" + expectedType + "', but got '" + parsed.getClass() + "'"); - return (R) parsed; + public @Nullable R getOptional(String key, Class expectedType, boolean useDefaultValue) { + List all = getAll(key, expectedType, useDefaultValue); + return all.isEmpty() ? null : all.get(0); } /** @@ -122,35 +174,9 @@ public R getOptional(String key, Class expectedType, boolean * @param useDefaultValue Whether the default value should be used if parsing failed. * @return The entry's value. May be null if the entry is missing or a parsing error occurred. */ - @Nullable - public Object getOptional(String key, boolean useDefaultValue) { - if (entryValidator == null || handledNodes == null) - return null; - - EntryData entryData = null; - for (EntryData data : entryValidator.getEntryData()) { - if (data.getKey().equals(key)) { - entryData = data; - break; - } - } - if (entryData == null) - return null; - - Node node = handledNodes.get(key); - if (node == null) - return entryData.getDefaultValue(); - - // Update ParserInstance node for parsing - ParserInstance parser = ParserInstance.get(); - Node oldNode = parser.getNode(); - parser.setNode(node); - Object value = entryData.getValue(node); - if (value == null && useDefaultValue) - value = entryData.getDefaultValue(); - parser.setNode(oldNode); - - return value; + public @Nullable Object getOptional(String key, boolean useDefaultValue) { + List all = getAll(key, useDefaultValue); + return all.isEmpty() ? null : all.get(0); } /** @@ -159,7 +185,7 @@ public Object getOptional(String key, boolean useDefaultValue) { * @return true if an entry data with the matching key was used. */ public boolean hasEntry(@NotNull String key) { - return handledNodes.containsKey(key); + return handledNodes != null && handledNodes.containsKey(key); } } diff --git a/src/main/java/org/skriptlang/skript/lang/entry/EntryData.java b/src/main/java/org/skriptlang/skript/lang/entry/EntryData.java index 2be30f7e5cb..4a0ed9a5232 100644 --- a/src/main/java/org/skriptlang/skript/lang/entry/EntryData.java +++ b/src/main/java/org/skriptlang/skript/lang/entry/EntryData.java @@ -30,11 +30,17 @@ public abstract class EntryData { private final String key; private final @Nullable T defaultValue; private final boolean optional; + private final boolean multiple; public EntryData(String key, @Nullable T defaultValue, boolean optional) { + this(key, defaultValue, optional, false); + } + + public EntryData(String key, @Nullable T defaultValue, boolean optional, boolean multiple) { this.key = key; this.defaultValue = defaultValue; this.optional = optional; + this.multiple = multiple; } /** @@ -59,6 +65,13 @@ public boolean isOptional() { return optional; } + /** + * @return Whether this entry data can be included repeatedly within a {@link SectionNode}. + */ + public boolean supportsMultiple() { + return multiple; + } + /** * Obtains a value from the provided node using the methods of this entry data. * @param node The node to obtain a value from. diff --git a/src/main/java/org/skriptlang/skript/lang/entry/EntryValidator.java b/src/main/java/org/skriptlang/skript/lang/entry/EntryValidator.java index e3fc1054153..fcd1f154f56 100644 --- a/src/main/java/org/skriptlang/skript/lang/entry/EntryValidator.java +++ b/src/main/java/org/skriptlang/skript/lang/entry/EntryValidator.java @@ -6,12 +6,7 @@ import ch.njol.skript.config.SectionNode; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.Function; import java.util.function.Predicate; @@ -50,10 +45,12 @@ private EntryValidator( ) { this.entryData = entryData; this.unexpectedNodeTester = unexpectedNodeTester; - this.unexpectedEntryMessage = - unexpectedEntryMessage != null ? unexpectedEntryMessage : DEFAULT_UNEXPECTED_ENTRY_MESSAGE; - this.missingRequiredEntryMessage = - missingRequiredEntryMessage != null ? missingRequiredEntryMessage : DEFAULT_MISSING_REQUIRED_ENTRY_MESSAGE; + this.unexpectedEntryMessage = unexpectedEntryMessage != null + ? unexpectedEntryMessage + : DEFAULT_UNEXPECTED_ENTRY_MESSAGE; + this.missingRequiredEntryMessage = missingRequiredEntryMessage != null + ? missingRequiredEntryMessage + : DEFAULT_MISSING_REQUIRED_ENTRY_MESSAGE; } /** @@ -66,13 +63,15 @@ public List> getEntryData() { /** * Validates a node using this entry validator. * @param sectionNode The node to validate. - * @return A pair containing a map of handled nodes and a list of unhandled nodes (if this validator permits unhandled nodes) - * The returned map uses the matched entry data's key as a key and uses a pair containing the entry data and matching node + * @return A pair containing a map of handled nodes and a list of unhandled nodes + * (if this validator permits unhandled nodes) + * The returned map uses the matched entry data's key as a key and + * uses a pair containing the entry data and matching node * Will return null if the provided node couldn't be validated. */ public @Nullable EntryContainer validate(SectionNode sectionNode) { List> entries = new ArrayList<>(entryData); - Map handledNodes = new HashMap<>(); + Map> handledNodes = new HashMap<>(); List unhandledNodes = new ArrayList<>(); boolean ok = true; @@ -85,8 +84,12 @@ public List> getEntryData() { while (iterator.hasNext()) { EntryData data = iterator.next(); if (data.canCreateWith(node)) { // Determine if it's a match - handledNodes.put(data.getKey(), node); // This is a known node, mark it as such - iterator.remove(); + Collection nodes = handledNodes.computeIfAbsent( + data.getKey(), k -> new LinkedList<>() + ); + nodes.add(node); + // we do not expect this entry data anymore + if (!data.supportsMultiple()) iterator.remove(); continue nodeLoop; } } @@ -102,6 +105,11 @@ public List> getEntryData() { // Now we're going to check for missing entries that are *required* for (EntryData entryData : entries) { + // entries that can be included multiple times are not removed, + // so we skip them if they occurred at least once + if (entryData.supportsMultiple() && handledNodes.containsKey(entryData.getKey())) { + continue; + } if (!entryData.isOptional()) { Skript.error(missingRequiredEntryMessage.apply(entryData.getKey())); ok = false; @@ -115,7 +123,8 @@ public List> getEntryData() { } /** - * A utility builder for creating an entry validator that can be used to parse and validate a {@link SectionNode}. + * A utility builder for creating an entry validator that can be used to parse + * and validate a {@link SectionNode}. * @see EntryValidator#builder() */ public static class EntryValidatorBuilder { @@ -173,23 +182,31 @@ public EntryValidatorBuilder unexpectedEntryMessage(Function une * A function to be applied when a required Node is missing during validation. * A String representing the key of the missing entry goes in, * and an error message to output comes out. - * @param missingRequiredEntryMessage The function to use. + * @param message The function to use. * @return The builder instance. */ - public EntryValidatorBuilder missingRequiredEntryMessage(Function missingRequiredEntryMessage) { - this.missingRequiredEntryMessage = missingRequiredEntryMessage; + public EntryValidatorBuilder missingRequiredEntryMessage(Function message) { + this.missingRequiredEntryMessage = message; return this; } + public EntryValidatorBuilder addEntry(String key, @Nullable String defaultValue, boolean optional) { + return addEntry(key, defaultValue, optional, false); + } + /** * Adds a new {@link KeyValueEntryData} to this validator that returns the raw, unhandled String value. * The added entry is optional and will use the provided default value as a backup. * @param key The key of the entry. * @param defaultValue The default value of this entry to use if the user does not include this entry. + * @param optional Whether the entry is optional + * @param multiple Whether the entry can be included multiple times within a single section node * @return The builder instance. */ - public EntryValidatorBuilder addEntry(String key, @Nullable String defaultValue, boolean optional) { - entryData.add(new KeyValueEntryData(key, defaultValue, optional) { + public EntryValidatorBuilder addEntry( + String key, @Nullable String defaultValue, boolean optional, boolean multiple + ) { + entryData.add(new KeyValueEntryData<>(key, defaultValue, optional, multiple) { @Override protected String getValue(String value) { return value; @@ -203,20 +220,26 @@ public String getSeparator() { return this; } + public EntryValidatorBuilder addSection(String key, boolean optional) { + return addSection(key, optional, false); + } + /** * Adds a new, potentially optional {@link SectionEntryData} to this validator. * @param key The key of the section entry. - * @param optional Whether this section entry should be optional. + * @param optional Whether the entry is optional + * @param multiple Whether the entry can be included multiple times within a single section node * @return The builder instance. */ - public EntryValidatorBuilder addSection(String key, boolean optional) { - entryData.add(new SectionEntryData(key, null, optional)); + public EntryValidatorBuilder addSection(String key, boolean optional, boolean multiple) { + entryData.add(new SectionEntryData(key, null, optional, multiple)); return this; } /** * A method to add custom {@link EntryData} to a validator. - * Custom entry data should be preferred when the default methods included in this builder are not expansive enough. + * Custom entry data should be preferred when the default methods included in + * this builder are not expansive enough. * Please note that for custom {@link KeyValueEntryData} implementations, the default entry separator * value of this builder will not be used. Instead, {@link #DEFAULT_ENTRY_SEPARATOR} will be used. * @param entryData The custom entry data to include in this validator. diff --git a/src/main/java/org/skriptlang/skript/lang/entry/KeyValueEntryData.java b/src/main/java/org/skriptlang/skript/lang/entry/KeyValueEntryData.java index 98fcf2863a2..f5bd68b9b92 100644 --- a/src/main/java/org/skriptlang/skript/lang/entry/KeyValueEntryData.java +++ b/src/main/java/org/skriptlang/skript/lang/entry/KeyValueEntryData.java @@ -18,6 +18,10 @@ public KeyValueEntryData(String key, @Nullable T defaultValue, boolean optional) super(key, defaultValue, optional); } + public KeyValueEntryData(String key, @Nullable T defaultValue, boolean optional, boolean multiple) { + super(key, defaultValue, optional, multiple); + } + /** * Used to obtain and parse the value of a {@link SimpleNode}. This method accepts * any type of node, but assumes the input to be a {@link SimpleNode}. Before calling this method, @@ -31,7 +35,10 @@ public KeyValueEntryData(String key, @Nullable T defaultValue, boolean optional) String key = node.getKey(); if (key == null) throw new IllegalArgumentException("EntryData#getValue() called with invalid node."); - return getValue(ScriptLoader.replaceOptions(key).substring(getKey().length() + getSeparator().length())); + return getValue( + ScriptLoader.replaceOptions(key) + .substring(getKey().length() + getSeparator().length()) + ); } /** diff --git a/src/main/java/org/skriptlang/skript/lang/entry/SectionEntryData.java b/src/main/java/org/skriptlang/skript/lang/entry/SectionEntryData.java index 21fc561d250..1400c194f46 100644 --- a/src/main/java/org/skriptlang/skript/lang/entry/SectionEntryData.java +++ b/src/main/java/org/skriptlang/skript/lang/entry/SectionEntryData.java @@ -15,8 +15,13 @@ public SectionEntryData(String key, @Nullable SectionNode defaultValue, boolean super(key, defaultValue, optional); } + public SectionEntryData(String key, @Nullable SectionNode defaultValue, boolean optional, boolean multiple) { + super(key, defaultValue, optional, multiple); + } + /** - * Because this entry data is for {@link SectionNode}s, no specific handling needs to be done to obtain the "value". + * Because this entry data is for {@link SectionNode}s, + * no specific handling needs to be done to obtain the "value". * This method just asserts that the provided node is actually a {@link SectionNode}. * @param node A {@link SimpleNode} to obtain (and possibly convert) the value of. * @return The value obtained from the provided {@link SimpleNode}. From f5c7e1bce0fa0bcdc32b97b9b22af0375aeb55de Mon Sep 17 00:00:00 2001 From: Pesek Date: Wed, 16 Jul 2025 11:30:58 +0200 Subject: [PATCH 02/14] added test for entry data being included multiple times within a single entry container --- .../test/runner/StructTestEntryContainer.java | 17 +++++++++++++++-- .../skript/lang/entry/EntryContainer.java | 15 ++++++++------- .../skript/lang/entry/EntryValidator.java | 19 ++++++++++++++++++- src/test/skript/junit/EntryContainerTest.sk | 11 ++++++++++- 4 files changed, 51 insertions(+), 11 deletions(-) diff --git a/src/main/java/ch/njol/skript/test/runner/StructTestEntryContainer.java b/src/main/java/ch/njol/skript/test/runner/StructTestEntryContainer.java index 8780e67fb4b..987efb1b162 100644 --- a/src/main/java/ch/njol/skript/test/runner/StructTestEntryContainer.java +++ b/src/main/java/ch/njol/skript/test/runner/StructTestEntryContainer.java @@ -23,6 +23,7 @@ public class StructTestEntryContainer extends Structure { Skript.registerStructure(StructTestEntryContainer.class, EntryValidator.builder() .addSection("has entry", true) + .addSection("has multiple entry", true, true) .build(), "test entry container"); } @@ -30,10 +31,12 @@ public class StructTestEntryContainer extends Structure { private EntryContainer entryContainer; @Override - public boolean init(Literal[] args, int matchedPattern, ParseResult parseResult, @Nullable EntryContainer entryContainer) { + public boolean init( + Literal[] args, int matchedPattern, ParseResult parseResult, @Nullable EntryContainer entryContainer + ) { assert entryContainer != null; this.entryContainer = entryContainer; - if (entryContainer.hasEntry("has entry")) { + if (entryContainer.hasEntry("has entry") && entryContainer.hasEntry("has multiple entry")) { return true; } assert false; @@ -47,6 +50,16 @@ public boolean load() { Script script = getParser().getCurrentScript(); Trigger trigger = new Trigger(script, "entry container test", null, triggerItems); trigger.execute(new SkriptTestEvent()); + + List multipleSections = entryContainer.getAll( + "has multiple entry", SectionNode.class, false + ); + for (SectionNode multipleSection : multipleSections) { + triggerItems = ScriptLoader.loadItems(multipleSection); + trigger = new Trigger(script, "entry container test", null, triggerItems); + trigger.execute(new SkriptTestEvent()); + } + return true; } diff --git a/src/main/java/org/skriptlang/skript/lang/entry/EntryContainer.java b/src/main/java/org/skriptlang/skript/lang/entry/EntryContainer.java index 1382edf3a2c..ffe3911c3be 100644 --- a/src/main/java/org/skriptlang/skript/lang/entry/EntryContainer.java +++ b/src/main/java/org/skriptlang/skript/lang/entry/EntryContainer.java @@ -14,10 +14,8 @@ public class EntryContainer { private final SectionNode source; - @Nullable - private final EntryValidator entryValidator; - @Nullable - private final Map> handledNodes; + private final @Nullable EntryValidator entryValidator; + private final @Nullable Map> handledNodes; private final List unhandledNodes; EntryContainer( @@ -92,7 +90,8 @@ public List getAll(String key, boolean useDefaultValue) { .filter(data -> data.getKey().equals(key)) .findFirst() .orElse(null); - if (entryData == null) return Collections.emptyList(); + if (entryData == null) + return Collections.emptyList(); Collection nodes = handledNodes.get(key); if (nodes == null || nodes.isEmpty()) { @@ -108,9 +107,11 @@ public List getAll(String key, boolean useDefaultValue) { Node oldNode = parser.getNode(); parser.setNode(node); Object value = entryData.getValue(node); - if (value == null && useDefaultValue) value = entryData.getDefaultValue(); + if (value == null && useDefaultValue) + value = entryData.getDefaultValue(); parser.setNode(oldNode); - if (value != null) values.add(value); + if (value != null) + values.add(value); } return values; diff --git a/src/main/java/org/skriptlang/skript/lang/entry/EntryValidator.java b/src/main/java/org/skriptlang/skript/lang/entry/EntryValidator.java index fcd1f154f56..85da3f2ea21 100644 --- a/src/main/java/org/skriptlang/skript/lang/entry/EntryValidator.java +++ b/src/main/java/org/skriptlang/skript/lang/entry/EntryValidator.java @@ -89,7 +89,8 @@ public List> getEntryData() { ); nodes.add(node); // we do not expect this entry data anymore - if (!data.supportsMultiple()) iterator.remove(); + if (!data.supportsMultiple()) + iterator.remove(); continue nodeLoop; } } @@ -190,6 +191,15 @@ public EntryValidatorBuilder missingRequiredEntryMessage(Function Date: Thu, 17 Jul 2025 10:47:31 +0200 Subject: [PATCH 03/14] added missing constructors --- .../skript/lang/entry/ContainerEntryData.java | 15 +++++++++++++-- .../skript/lang/entry/util/TriggerEntryData.java | 6 ++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/lang/entry/ContainerEntryData.java b/src/main/java/org/skriptlang/skript/lang/entry/ContainerEntryData.java index 342be3bd9fe..059caf259d5 100644 --- a/src/main/java/org/skriptlang/skript/lang/entry/ContainerEntryData.java +++ b/src/main/java/org/skriptlang/skript/lang/entry/ContainerEntryData.java @@ -25,6 +25,18 @@ public ContainerEntryData(String key, boolean optional, EntryValidatorBuilder va this.entryValidator = validatorBuilder.build(); } + public ContainerEntryData(String key, boolean optional, boolean multiple, EntryValidator entryValidator) { + super(key, null, optional, multiple); + this.entryValidator = entryValidator; + } + + public ContainerEntryData( + String key, boolean optional, boolean multiple, EntryValidatorBuilder validatorBuilder + ) { + super(key, null, optional, multiple); + this.entryValidator = validatorBuilder.build(); + } + public EntryValidator getEntryValidator() { return entryValidator; } @@ -44,8 +56,7 @@ public boolean canCreateWith(Node node) { key = ScriptLoader.replaceOptions(key); if (!getKey().equalsIgnoreCase(key)) return false; - EntryContainer container = entryValidator.validate(sectionNode); - entryContainer = container; + entryContainer = entryValidator.validate(sectionNode); return true; } diff --git a/src/main/java/org/skriptlang/skript/lang/entry/util/TriggerEntryData.java b/src/main/java/org/skriptlang/skript/lang/entry/util/TriggerEntryData.java index 7cc093e9473..09d23938587 100644 --- a/src/main/java/org/skriptlang/skript/lang/entry/util/TriggerEntryData.java +++ b/src/main/java/org/skriptlang/skript/lang/entry/util/TriggerEntryData.java @@ -8,8 +8,6 @@ import org.skriptlang.skript.lang.entry.EntryData; import org.skriptlang.skript.lang.entry.SectionEntryData; import ch.njol.skript.lang.util.SimpleEvent; -import ch.njol.util.Kleenean; -import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; /** @@ -23,6 +21,10 @@ public TriggerEntryData(String key, @Nullable Trigger defaultValue, boolean opti super(key, defaultValue, optional); } + public TriggerEntryData(String key, @Nullable Trigger defaultValue, boolean optional, boolean multiple) { + super(key, defaultValue, optional, multiple); + } + @Nullable @Override public Trigger getValue(Node node) { From 5aca0a29ac0b21b5f5e49efcfb5f4d1e7e2667da Mon Sep 17 00:00:00 2001 From: Pesek Date: Fri, 18 Jul 2025 15:21:41 +0200 Subject: [PATCH 04/14] made list returned by EntryContainer#getAll unmodifiable --- .../skript/lang/entry/EntryContainer.java | 13 +++++++------ .../skript/lang/entry/KeyValueEntryData.java | 6 ++---- src/test/skript/junit/EntryContainerTest.sk | 2 +- 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/lang/entry/EntryContainer.java b/src/main/java/org/skriptlang/skript/lang/entry/EntryContainer.java index ffe3911c3be..1f1740b7d58 100644 --- a/src/main/java/org/skriptlang/skript/lang/entry/EntryContainer.java +++ b/src/main/java/org/skriptlang/skript/lang/entry/EntryContainer.java @@ -5,6 +5,7 @@ import ch.njol.skript.lang.parser.ParserInstance; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; import java.util.*; @@ -66,7 +67,7 @@ public List getUnhandledNodes() { * @throws RuntimeException If the entry's value is not of the expected type. */ @SuppressWarnings("unchecked") - public List getAll(String key, Class expectedType, boolean useDefaultValue) { + public @Unmodifiable List getAll(String key, Class expectedType, boolean useDefaultValue) { List parsed = getAll(key, useDefaultValue); for (Object object : parsed) { if (!expectedType.isInstance(object)) @@ -82,7 +83,7 @@ public List getAll(String key, Class expectedType, boolea * @param useDefaultValue Whether the default value should be used if parsing failed. * @return The entry's values. May be empty list if the entry is missing or a parsing error occurred. */ - public List getAll(String key, boolean useDefaultValue) { + public @Unmodifiable List getAll(String key, boolean useDefaultValue) { if (entryValidator == null || handledNodes == null) return Collections.emptyList(); @@ -102,19 +103,19 @@ public List getAll(String key, boolean useDefaultValue) { } List values = new LinkedList<>(); + ParserInstance parser = ParserInstance.get(); + Node oldNode = parser.getNode(); for (Node node : nodes) { - ParserInstance parser = ParserInstance.get(); - Node oldNode = parser.getNode(); parser.setNode(node); Object value = entryData.getValue(node); if (value == null && useDefaultValue) value = entryData.getDefaultValue(); - parser.setNode(oldNode); if (value != null) values.add(value); } + parser.setNode(oldNode); - return values; + return Collections.unmodifiableList(values); } /** diff --git a/src/main/java/org/skriptlang/skript/lang/entry/KeyValueEntryData.java b/src/main/java/org/skriptlang/skript/lang/entry/KeyValueEntryData.java index f5bd68b9b92..2d56dcd5ba0 100644 --- a/src/main/java/org/skriptlang/skript/lang/entry/KeyValueEntryData.java +++ b/src/main/java/org/skriptlang/skript/lang/entry/KeyValueEntryData.java @@ -35,10 +35,8 @@ public KeyValueEntryData(String key, @Nullable T defaultValue, boolean optional, String key = node.getKey(); if (key == null) throw new IllegalArgumentException("EntryData#getValue() called with invalid node."); - return getValue( - ScriptLoader.replaceOptions(key) - .substring(getKey().length() + getSeparator().length()) - ); + return getValue(ScriptLoader.replaceOptions(key) + .substring(getKey().length() + getSeparator().length())); } /** diff --git a/src/test/skript/junit/EntryContainerTest.sk b/src/test/skript/junit/EntryContainerTest.sk index b670873fb5c..fec52c91937 100644 --- a/src/test/skript/junit/EntryContainerTest.sk +++ b/src/test/skript/junit/EntryContainerTest.sk @@ -15,4 +15,4 @@ test entry container: has multiple entry: add 1 to {entry-container-test::call-count} if {entry-container-test::call-count} is 2: - complete objective "has multiple entry" for {@test} \ No newline at end of file + complete objective "has multiple entry" for {@test} From dd48da857f6248aed8bafe5a0dfd570f038c5145 Mon Sep 17 00:00:00 2001 From: Pesek Date: Mon, 21 Jul 2025 21:52:56 +0200 Subject: [PATCH 05/14] fixed incorrect package name in the test --- src/test/skript/junit/EntryContainerTest.sk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/skript/junit/EntryContainerTest.sk b/src/test/skript/junit/EntryContainerTest.sk index fec52c91937..d8d10e73178 100644 --- a/src/test/skript/junit/EntryContainerTest.sk +++ b/src/test/skript/junit/EntryContainerTest.sk @@ -1,5 +1,5 @@ options: - test: "org.skriptlang.skript.test.tests.syntaxes.events.EntryContainerTest" + test: "org.skriptlang.skript.test.tests.lang.EntryContainerTest" test "EntryContainerTest" when running JUnit: set {_tests::1} to "has entry" From 65a1f0fcd7df0e406b091c1551e32de6afcf8ef8 Mon Sep 17 00:00:00 2001 From: Pesek Date: Sun, 3 Aug 2025 16:29:26 +0200 Subject: [PATCH 06/14] added initial implementation --- src/main/java/ch/njol/skript/Skript.java | 19 ++ .../brigadier/BrigadierCommandEvent.java | 43 +++ .../brigadier/BrigadierCommandHandler.java | 191 ++++++++++++ .../command/brigadier/BrigadierModule.java | 23 ++ .../brigadier/BrigadierSuggestionsEvent.java | 57 ++++ .../brigadier/DefaultArgumentTypes.java | 72 +++++ .../command/brigadier/PaperCommandUtils.java | 90 ++++++ .../brigadier/StructBrigadierCommand.java | 33 ++ .../brigadier/WrappedCommandSourceStack.java | 139 +++++++++ .../brigadier/ArgumentSkriptCommandNode.java | 205 +++++++++++++ .../brigadier/LiteralSkriptCommandNode.java | 166 ++++++++++ .../brigadier/RootSkriptCommandNode.java | 192 ++++++++++++ .../skript/brigadier/SkriptCommandNode.java | 287 ++++++++++++++++++ .../brigadier/SkriptSuggestionProvider.java | 67 ++++ .../lang/command/ArgumentTypeElement.java | 95 ++++++ .../skript/lang/command/CommandArgument.java | 70 +++++ .../lang/command/CommandArgumentParser.java | 97 ++++++ .../skript/lang/command/CommandCooldown.java | 5 + .../skript/lang/command/CommandHandler.java | 70 +++++ .../lang/command/CommandSourceType.java | 50 +++ .../skript/lang/command/CommandUtils.java | 271 +++++++++++++++++ .../lang/command/StructGeneralCommand.java | 155 ++++++++++ .../lang/command/SubCommandEntryData.java | 167 ++++++++++ 23 files changed, 2564 insertions(+) create mode 100644 src/main/java/ch/njol/skript/command/brigadier/BrigadierCommandEvent.java create mode 100644 src/main/java/ch/njol/skript/command/brigadier/BrigadierCommandHandler.java create mode 100644 src/main/java/ch/njol/skript/command/brigadier/BrigadierModule.java create mode 100644 src/main/java/ch/njol/skript/command/brigadier/BrigadierSuggestionsEvent.java create mode 100644 src/main/java/ch/njol/skript/command/brigadier/DefaultArgumentTypes.java create mode 100644 src/main/java/ch/njol/skript/command/brigadier/PaperCommandUtils.java create mode 100644 src/main/java/ch/njol/skript/command/brigadier/StructBrigadierCommand.java create mode 100644 src/main/java/ch/njol/skript/command/brigadier/WrappedCommandSourceStack.java create mode 100644 src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java create mode 100644 src/main/java/org/skriptlang/skript/brigadier/LiteralSkriptCommandNode.java create mode 100644 src/main/java/org/skriptlang/skript/brigadier/RootSkriptCommandNode.java create mode 100644 src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java create mode 100644 src/main/java/org/skriptlang/skript/brigadier/SkriptSuggestionProvider.java create mode 100644 src/main/java/org/skriptlang/skript/lang/command/ArgumentTypeElement.java create mode 100644 src/main/java/org/skriptlang/skript/lang/command/CommandArgument.java create mode 100644 src/main/java/org/skriptlang/skript/lang/command/CommandArgumentParser.java create mode 100644 src/main/java/org/skriptlang/skript/lang/command/CommandCooldown.java create mode 100644 src/main/java/org/skriptlang/skript/lang/command/CommandHandler.java create mode 100644 src/main/java/org/skriptlang/skript/lang/command/CommandSourceType.java create mode 100644 src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java create mode 100644 src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java create mode 100644 src/main/java/org/skriptlang/skript/lang/command/SubCommandEntryData.java diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index d44ee843f79..e1fc8f6f8c2 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -12,6 +12,7 @@ import ch.njol.skript.classes.data.JavaClasses; import ch.njol.skript.classes.data.SkriptClasses; import ch.njol.skript.command.Commands; +import ch.njol.skript.command.brigadier.BrigadierModule; import ch.njol.skript.doc.Documentation; import ch.njol.skript.events.EvtSkript; import ch.njol.skript.hooks.Hook; @@ -101,6 +102,7 @@ import org.skriptlang.skript.bukkit.registration.BukkitRegistryKeys; import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos; import org.skriptlang.skript.bukkit.tags.TagModule; +import org.skriptlang.skript.lang.command.ArgumentTypeElement; import org.skriptlang.skript.lang.comparator.Comparator; import org.skriptlang.skript.lang.comparator.Comparators; import org.skriptlang.skript.lang.converter.Converter; @@ -586,6 +588,7 @@ public void onEnable() { FurnaceModule.load(); LootTableModule.load(); skript.loadModules(new DamageSourceModule()); + skript.loadModules(new BrigadierModule()); } catch (final Exception e) { exception(e, "Could not load required .class files: " + e.getLocalizedMessage()); setEnabled(false); @@ -1735,6 +1738,22 @@ public static boolean dispatchCommand(final CommandSender sender, final String c } } + /** + * Registers a new argument type. + * + * @param argumentClass class of the argument type + * @param patterns patterns + * @param argument type + */ + public static > void registerArgumentType(Class argumentClass, String... patterns) { + checkAcceptRegistrations(); + skript.syntaxRegistry().register(ArgumentTypeElement.REGISTRY_KEY, SyntaxInfo.builder(argumentClass) + .origin(getSyntaxOrigin(argumentClass)) + .addPatterns(patterns) + .build() + ); + } + // ================ LOGGING ================ public static boolean logNormal() { diff --git a/src/main/java/ch/njol/skript/command/brigadier/BrigadierCommandEvent.java b/src/main/java/ch/njol/skript/command/brigadier/BrigadierCommandEvent.java new file mode 100644 index 00000000000..19ec70ddfd1 --- /dev/null +++ b/src/main/java/ch/njol/skript/command/brigadier/BrigadierCommandEvent.java @@ -0,0 +1,43 @@ +package ch.njol.skript.command.brigadier; + +import ch.njol.skript.command.CommandEvent; +import com.mojang.brigadier.context.CommandContext; +import org.bukkit.command.CommandSender; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * Event for executing brigadier commands. + */ +public class BrigadierCommandEvent extends CommandEvent { + + private final CommandContext context; + + public BrigadierCommandEvent(CommandContext context) { + super(context.getSource(), context.getRootNode().getName(), + context.getNodes().stream() + .map(arg -> arg.getRange().get(context.getInput())) + .toArray(String[]::new)); + this.context = context; + } + + /** + * @return command execution context + */ + public CommandContext getContext() { + return context; + } + + // Bukkit stuff + private final static HandlerList handlers = new HandlerList(); + + @Override + public @NotNull HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } + +} diff --git a/src/main/java/ch/njol/skript/command/brigadier/BrigadierCommandHandler.java b/src/main/java/ch/njol/skript/command/brigadier/BrigadierCommandHandler.java new file mode 100644 index 00000000000..afc57b0d3ae --- /dev/null +++ b/src/main/java/ch/njol/skript/command/brigadier/BrigadierCommandHandler.java @@ -0,0 +1,191 @@ +package ch.njol.skript.command.brigadier; + +import ch.njol.skript.Skript; +import com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent; +import com.google.common.base.Preconditions; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +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 com.mojang.brigadier.tree.RootCommandNode; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import org.bukkit.Bukkit; +import org.bukkit.command.BlockCommandSender; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.jetbrains.annotations.UnmodifiableView; +import org.skriptlang.skript.brigadier.RootSkriptCommandNode; +import org.skriptlang.skript.lang.command.CommandHandler; +import org.skriptlang.skript.lang.command.CommandSourceType; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; + +public class BrigadierCommandHandler implements CommandHandler, Listener { + + private final Map> commands = new ConcurrentHashMap<>(); + private @Nullable CommandDispatcher dispatcher; + + @Override + public synchronized boolean registerCommand(RootSkriptCommandNode command) { + if (commands.containsKey(command.getLiteral())) { + Skript.error("Command " + command.getLiteral() + " is already registered"); + return false; + } + commands.put(command.getLiteral(), command); + // TODO proper registration with aliases and help page + Bukkit.getCommandMap().getKnownCommands().put(command.getLiteral(), new WrappedBrigadierCommand(command)); + dispatcher = null; // reset dispatcher + Bukkit.getScheduler().runTaskLater(Skript.getInstance(), PaperCommandUtils::syncCommands, 1); + return true; + } + + @Override + public synchronized boolean unregisterCommand(RootSkriptCommandNode command) { + boolean result = commands.remove(command.getLiteral(), command); + if (!result) return false; + Bukkit.getCommandMap().getKnownCommands().remove(command.getLiteral()); + dispatcher = null; // reset dispatcher + Bukkit.getScheduler().runTaskLater(Skript.getInstance(), PaperCommandUtils::syncCommands, 1); + return true; + } + + @Override + public @Nullable RootSkriptCommandNode getCommand(String label) { + return commands.get(label); + } + + @Override + public @UnmodifiableView Map> getAllCommands() { + return Collections.unmodifiableMap(commands); + } + + @Override + public boolean dispatchCommand(CommandSender source, String input) { + if (dispatcher == null) { + dispatcher = new CommandDispatcher<>(); + commands.values().forEach(cmd -> dispatcher.register(cmd.flatToBuilder())); + } + + // TODO + // if the command does not exist return false + // send error message to source similar to vanilla + try { + dispatcher.execute(input, source); + } catch (CommandSyntaxException exception) { + Skript.getInstance().getSLF4JLogger().info("Command syntax error", exception); + } + return true; + } + + @Override + public @Unmodifiable Set supportedTypes() { + return Set.of( + CommandSourceType.typed(Player.class, "player", "the player", "players", "the players"), + CommandSourceType.typed(ConsoleCommandSender.class, "console", "the console", "server", "the server"), + CommandSourceType.typed(BlockCommandSender.class, "block", "blocks"), + CommandSourceType.typed(Entity.class, "entity", "entities") + ); + } + + private static final @Nullable Field NODE_CHILDREN_FIELD; + + static { + Field nodeChildrenField = null; + try { + nodeChildrenField = CommandNode.class.getDeclaredField("children"); + nodeChildrenField.setAccessible(true); + } catch (Exception exception) { + if (Skript.debug()) + throw Skript.exception(exception, "Failed to access the command node children field"); + } + NODE_CHILDREN_FIELD = nodeChildrenField; + } + + @EventHandler + @SuppressWarnings("UnstableApiUsage") + public void onAsyncPlayerSendCommands(AsyncPlayerSendCommandsEvent<@NotNull CommandSourceStack> event) { + if (!event.isAsynchronous() && event.hasFiredAsync()) + return; + RootCommandNode rootNode = event.getCommandNode(); + Consumer> bukkitNodeRemover = node -> {}; + try { + Preconditions.checkNotNull(NODE_CHILDREN_FIELD); + //noinspection unchecked + Map> children = (Map>) + NODE_CHILDREN_FIELD.get(rootNode); + bukkitNodeRemover = node -> children.remove(node.getName()); + } catch (Exception exception) { + if (Skript.debug()) + throw Skript.exception(exception, "Failed to access the node children field"); + } + + for (RootSkriptCommandNode node : commands.values()) { + CommandNode paperCompatible = convertToClientsidePaper(node.flat()); + bukkitNodeRemover.accept(paperCompatible); + rootNode.addChild(paperCompatible); + } + } + + // TODO suggestions + + /** + * Converts given command node to a node that can be safely sent to the client. + * + * @param node node to convert + * @return node that can be sent to the client + */ + // TODO convert paper argument types to NMS argument types + // TODO filter out commands with requirements player does not meet (mirrors vanilla behaviour) + private CommandNode convertToClientsidePaper(CommandNode node) { + ArgumentBuilder builder; + if (node instanceof LiteralCommandNode lcn) { + builder = LiteralArgumentBuilder.literal(lcn.getLiteral()); + } else if (node instanceof ArgumentCommandNode acn) { + builder = RequiredArgumentBuilder.argument(acn.getName(), acn.getType()); + } else { + throw new IllegalArgumentException("Unsupported node implementation; only native Brigadier nodes " + + "are supported"); + } + if (node.getRequirement() != null) + builder.requires(stack -> node.getRequirement().test(new WrappedCommandSourceStack(stack))); + if (node.getRedirect() != null) + builder.forward(convertToClientsidePaper(node.getRedirect()), ctx -> Collections.emptyList(), node.isFork()); + if (node.getCommand() != null) + builder.executes(ctx -> com.mojang.brigadier.Command.SINGLE_SUCCESS); + node.getChildren().forEach(child -> builder.then(convertToClientsidePaper(child))); + return builder.build(); + } + + private class WrappedBrigadierCommand extends Command { + + protected WrappedBrigadierCommand(RootSkriptCommandNode node) { + super(node.getName(), node.getDescription() != null ? node.getDescription() : "", + "" /* TODO usage */, new ArrayList<>(node.getAliases())); + } + + @Override + public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String @NotNull [] args) { + return BrigadierCommandHandler.this.dispatchCommand(sender, getName() + " " + String.join(" ", args)); + } + + } + +} diff --git a/src/main/java/ch/njol/skript/command/brigadier/BrigadierModule.java b/src/main/java/ch/njol/skript/command/brigadier/BrigadierModule.java new file mode 100644 index 00000000000..f90f889004d --- /dev/null +++ b/src/main/java/ch/njol/skript/command/brigadier/BrigadierModule.java @@ -0,0 +1,23 @@ +package ch.njol.skript.command.brigadier; + +import ch.njol.skript.Skript; +import org.skriptlang.skript.addon.AddonModule; +import org.skriptlang.skript.addon.SkriptAddon; + +import java.io.IOException; + +/** + * Module for Brigadier commands. + */ +public class BrigadierModule implements AddonModule { + + @Override + public void load(SkriptAddon addon) { + try { + Skript.getAddonInstance().loadClasses("ch.njol.skript.command.brigadier"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/ch/njol/skript/command/brigadier/BrigadierSuggestionsEvent.java b/src/main/java/ch/njol/skript/command/brigadier/BrigadierSuggestionsEvent.java new file mode 100644 index 00000000000..8e38bad93d2 --- /dev/null +++ b/src/main/java/ch/njol/skript/command/brigadier/BrigadierSuggestionsEvent.java @@ -0,0 +1,57 @@ +package ch.njol.skript.command.brigadier; + +import com.mojang.brigadier.context.CommandContext; +import org.bukkit.command.CommandSender; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * Event for retrieving command suggestions (tab completions). + */ +public class BrigadierSuggestionsEvent extends BrigadierCommandEvent { + + private final List suggestions = new ArrayList<>(); + + public BrigadierSuggestionsEvent(CommandContext context) { + super(context); + } + + /** + * @return suggestions + */ + public String[] getSuggestions() { + return suggestions.toArray(String[]::new); + } + + /** + * @param suggestions new suggestions + */ + public void setSuggestions(List suggestions) { + this.suggestions.clear(); + if (suggestions != null) + this.suggestions.addAll(suggestions); + } + + /** + * @param suggestions new suggestions + */ + public void setSuggestions(String... suggestions) { + setSuggestions(List.of(suggestions)); + } + + // Bukkit stuff + private final static HandlerList handlers = new HandlerList(); + + @Override + public @NotNull HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } + +} diff --git a/src/main/java/ch/njol/skript/command/brigadier/DefaultArgumentTypes.java b/src/main/java/ch/njol/skript/command/brigadier/DefaultArgumentTypes.java new file mode 100644 index 00000000000..730390ec678 --- /dev/null +++ b/src/main/java/ch/njol/skript/command/brigadier/DefaultArgumentTypes.java @@ -0,0 +1,72 @@ +package ch.njol.skript.command.brigadier; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.command.ArgumentTypeElement; + +/** + * Default Argument Types provided by Skript. + */ +public class DefaultArgumentTypes { + + private DefaultArgumentTypes() { + throw new UnsupportedOperationException(); + } + + /** + * String argument type that provides 3 different implementations. + * 1. single word + * 2. quoted string + * 3. greedy string + */ + public static final class String extends ArgumentTypeElement.Simple { + + static { + Skript.registerArgumentType(DefaultArgumentTypes.String.class, + "[single] (word|string)", "[quotable|quoted] string", "greedy string"); + } + + @Override + protected @Nullable ArgumentType get(Expression[] expressions, int matchedPattern, + SkriptParser.ParseResult parseResult) { + return switch (matchedPattern) { + case 0 -> StringArgumentType.word(); + case 1 -> StringArgumentType.string(); + case 2 -> StringArgumentType.greedyString(); + default -> null; + }; + } + + } + + /** + * Integer argument type. + */ + public static final class Integer extends ArgumentTypeElement.Simple { + + static { + Skript.registerArgumentType(DefaultArgumentTypes.Integer.class, + "integer [from %-integer% [to %-integer%]]"); + } + + @Override + protected @NotNull ArgumentType get(Expression[] expressions, int matchedPattern, + SkriptParser.ParseResult parseResult) { + int min = java.lang.Integer.MIN_VALUE; + int max = java.lang.Integer.MAX_VALUE; + if (expressions[0] != null) + min = (int) expressions[0].getSingle(null); + if (expressions[1] != null) + max = (int) expressions[1].getSingle(null); + return IntegerArgumentType.integer(min, max); + } + + } + +} diff --git a/src/main/java/ch/njol/skript/command/brigadier/PaperCommandUtils.java b/src/main/java/ch/njol/skript/command/brigadier/PaperCommandUtils.java new file mode 100644 index 00000000000..bbb24ef9bb3 --- /dev/null +++ b/src/main/java/ch/njol/skript/command/brigadier/PaperCommandUtils.java @@ -0,0 +1,90 @@ +package ch.njol.skript.command.brigadier; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.VariableString; +import ch.njol.skript.localization.Language; +import ch.njol.skript.localization.Message; +import com.mojang.brigadier.context.CommandContext; +import org.bukkit.Bukkit; +import org.bukkit.Server; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.Nullable; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; + +/** + * Utilities related to Skript commands on Paper platform. + */ +public final class PaperCommandUtils { + + private PaperCommandUtils() { + throw new UnsupportedOperationException(); + } + + /** + * Sends permission message to the command source of given context. + * + * @param context context + * @param permissionMessage permission message to send + * @param command source + */ + @SuppressWarnings("unchecked") + public static void sendPermissionMessage(CommandContext context, + VariableString permissionMessage) { + if (permissionMessage == null) + permissionMessage = VariableString.newInstance(Language.get("commands.no permission message")); + assert permissionMessage != null; + BrigadierCommandEvent event = new BrigadierCommandEvent((CommandContext) context); + String formatted = permissionMessage.getSingle(event); + if (formatted != null) + context.getSource().sendMessage(formatted); + } + + // TODO missing in the lang file + private static final Message INVALID_EXECUTOR_MESSAGE = new Message("commands.invalid executor"); + + /** + * Sends message about unsupported command executor type to the command source of given context. + * + * @param context context + * @param command source + */ + public static void sendInvalidExecutorMessage(CommandContext context) { + context.getSource().sendMessage(INVALID_EXECUTOR_MESSAGE.toString()); + } + + + private static final @Nullable MethodHandle SYNC_COMMANDS_HANDLE; + + static { + MethodHandle handle = null; + try { + Server server = Bukkit.getServer(); + Method methodInstance = server.getClass().getDeclaredMethod("syncCommands"); + methodInstance.setAccessible(true); + handle = MethodHandles.privateLookupIn(server.getClass(), MethodHandles.lookup()) + .unreflect(methodInstance); + handle = handle.bindTo(server); + } catch (Exception exception) { + // Ignore except for debugging. This is not necessary or in any way supported functionality + if (Skript.debug()) + throw Skript.exception(exception, "Failed to access the syncCommands method"); + } + SYNC_COMMANDS_HANDLE = handle; + } + + /** + * Synchronizes the server commands with the client. + */ + public static void syncCommands() { + if (SYNC_COMMANDS_HANDLE == null) return; + try { + SYNC_COMMANDS_HANDLE.invokeExact(); + } catch (Throwable exception) { + throw Skript.exception(exception, "Failed to invoke the syncCommands method"); + } + } + +} diff --git a/src/main/java/ch/njol/skript/command/brigadier/StructBrigadierCommand.java b/src/main/java/ch/njol/skript/command/brigadier/StructBrigadierCommand.java new file mode 100644 index 00000000000..0468ecf71a8 --- /dev/null +++ b/src/main/java/ch/njol/skript/command/brigadier/StructBrigadierCommand.java @@ -0,0 +1,33 @@ +package ch.njol.skript.command.brigadier; + +import ch.njol.skript.Skript; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.command.CommandHandler; +import org.skriptlang.skript.lang.command.StructGeneralCommand; + +/** + * Brigadier command structure implementation. + */ +public class StructBrigadierCommand extends StructGeneralCommand { + + private static final BrigadierCommandHandler HANDLER = new BrigadierCommandHandler(); + + static { + StructGeneralCommand.registerCommandStructure(StructBrigadierCommand.class, "brigadier"); + Bukkit.getPluginManager().registerEvents(HANDLER, Skript.getInstance()); + } + + @Override + public CommandHandler getHandler() { + return HANDLER; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "brigadier command /" + commandNode.getNamespace() + ":" + commandNode.getLiteral(); + } + +} diff --git a/src/main/java/ch/njol/skript/command/brigadier/WrappedCommandSourceStack.java b/src/main/java/ch/njol/skript/command/brigadier/WrappedCommandSourceStack.java new file mode 100644 index 00000000000..46c8b0078a5 --- /dev/null +++ b/src/main/java/ch/njol/skript/command/brigadier/WrappedCommandSourceStack.java @@ -0,0 +1,139 @@ +package ch.njol.skript.command.brigadier; + +import io.papermc.paper.command.brigadier.CommandSourceStack; +import net.kyori.adventure.text.Component; +import org.bukkit.Server; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Entity; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionAttachment; +import org.bukkit.permissions.PermissionAttachmentInfo; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; +import java.util.UUID; + +/** + * CommandSender instance created from a command source stack. + * + * @param sourceStack source stack + */ +// TODO is this needed? probably not and the requirements (Predicate) can have dummy implementation +record WrappedCommandSourceStack(CommandSourceStack sourceStack) implements CommandSender { + + public CommandSender sender() { + Entity executor = sourceStack.getExecutor(); + return executor != null ? executor : sourceStack.getSender(); + } + + @Override + public void sendMessage(@NotNull String message) { + sender().sendMessage(message); + } + + @Override + public void sendMessage(@NotNull String... messages) { + sender().sendMessage(messages); + } + + @Override + @Deprecated + public void sendMessage(@Nullable UUID sender, @NotNull String message) { + sender().sendMessage(sender, message); + } + + @Override + @Deprecated + public void sendMessage(@Nullable UUID sender, @NotNull String... messages) { + sender().sendMessage(sender, messages); + } + + @Override + public @NotNull Server getServer() { + return sender().getServer(); + } + + @Override + public @NotNull String getName() { + return sender().getName(); + } + + @NotNull + @Override + public Spigot spigot() { + return sender().spigot(); + } + + @Override + public @NotNull Component name() { + return sender().name(); + } + + @Override + public boolean isPermissionSet(@NotNull String name) { + return sender().isPermissionSet(name); + } + + @Override + public boolean isPermissionSet(@NotNull Permission perm) { + return sender().isPermissionSet(perm); + } + + @Override + public boolean hasPermission(@NotNull String name) { + return sender().hasPermission(name); + } + + @Override + public boolean hasPermission(@NotNull Permission perm) { + return sender().hasPermission(perm); + } + + @Override + public @NotNull PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value) { + return sender().addAttachment(plugin, name, value); + } + + @Override + public @NotNull PermissionAttachment addAttachment(@NotNull Plugin plugin) { + return sender().addAttachment(plugin); + } + + @Override + public @Nullable PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value, int ticks) { + return sender().addAttachment(plugin, name, value, ticks); + } + + @Override + public @Nullable PermissionAttachment addAttachment(@NotNull Plugin plugin, int ticks) { + return sender().addAttachment(plugin, ticks); + } + + @Override + public void removeAttachment(@NotNull PermissionAttachment attachment) { + sender().removeAttachment(attachment); + } + + @Override + public void recalculatePermissions() { + sender().recalculatePermissions(); + } + + @Override + public @NotNull Set getEffectivePermissions() { + return sender().getEffectivePermissions(); + } + + @Override + public boolean isOp() { + return sender().isOp(); + } + + @Override + public void setOp(boolean value) { + sender().setOp(value); + } + +} diff --git a/src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java b/src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java new file mode 100644 index 00000000000..e6fa81340a9 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java @@ -0,0 +1,205 @@ +package org.skriptlang.skript.brigadier; + +import ch.njol.skript.lang.VariableString; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.RedirectModifier; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.context.CommandContextBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +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 org.bukkit.command.CommandSender; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.command.CommandCooldown; +import org.skriptlang.skript.lang.command.CommandSourceType; + +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; + +/** + * SkriptCommandNode implementation of {@link ArgumentSkriptCommandNode}. + * + * @param command sender type + * @see ArgumentSkriptCommandNode.Builder#argument(String, ArgumentType) + */ +public non-sealed class ArgumentSkriptCommandNode + extends SkriptCommandNode> { + + private final ArgumentCommandNode wrapped; + + public ArgumentSkriptCommandNode(String name, ArgumentType type, @Nullable Command command, + @Nullable Predicate requirement, @Nullable CommandNode redirect, + @Nullable RedirectModifier modifier, boolean forks, @Nullable SuggestionProvider customSuggestions, + @Nullable String permission, @Nullable VariableString permissionMessage, + @Nullable Collection possibleSources, @Nullable CommandCooldown cooldown) { + super(command, requirement, redirect, modifier, forks, permission, permissionMessage, + possibleSources, cooldown); + wrapped = RequiredArgumentBuilder.argument(name, type) + .executes(command) + .requires(requirement) + .forward(redirect, modifier, forks) + .suggests(customSuggestions) + .build(); + } + + public ArgumentType getType() { + return wrapped.getType(); + } + + public @Nullable SuggestionProvider getCustomSuggestions() { + return wrapped.getCustomSuggestions(); + } + + @Override + public ArgumentCommandNode flat() { + RequiredArgumentBuilder builder = RequiredArgumentBuilder.argument(getName(), getType()); + SkriptCommandNode.flat(builder, this); + builder.suggests(getCustomSuggestions()); + return builder.build(); + } + + @Override + @SuppressWarnings("unchecked") + public RequiredArgumentBuilder flatToBuilder() { + return (RequiredArgumentBuilder) super.flatToBuilder(); + } + + @Override + protected boolean isValidInput(String s) { + return wrapped.isValidInput(s); + } + + @Override + public String getName() { + return wrapped.getName(); + } + + @Override + public String getUsageText() { + return wrapped.getUsageText(); + } + + @Override + public void parse(StringReader stringReader, CommandContextBuilder commandContextBuilder) throws CommandSyntaxException { + wrapped.parse(stringReader, commandContextBuilder); + } + + @Override + public CompletableFuture listSuggestions(CommandContext commandContext, + SuggestionsBuilder suggestionsBuilder) throws CommandSyntaxException { + return wrapped.listSuggestions(commandContext, suggestionsBuilder); + } + + @Override + public Builder createBuilder() { + return Builder.argument(getName(), getType()) + .executes(getCommand()) + .requires(getRequirement()) + .forward(getRedirect(), getRedirectModifier(), isFork()) + .suggests(getCustomSuggestions()) + .permission(getPermission()) + .permissionMessage(getPermissionMessage()) + .possibleSources(getPossibleSources()) + .cooldown(getCooldown()); + } + + @Override + protected String getSortedKey() { + return getName(); + } + + @Override + public Collection getExamples() { + return wrapped.getExamples(); + } + + /** + * Builder implementation for {@link ArgumentSkriptCommandNode}. + * + * @param command sender type + * @param argument type + */ + public static non-sealed class Builder + extends SkriptCommandNode.Builder, + ArgumentSkriptCommandNode, Builder> { + + /** + * Creates new builder for argument skript command node with given name and type. + * + * @param name name of the argument + * @param type type of the argument + * @return builder + * @param command sender type + * @param argument type + */ + public static Builder argument(String name, ArgumentType type) { + return new Builder<>(name, type); + } + + private final String name; + private final ArgumentType type; + private @Nullable SuggestionProvider suggestionsProvider; + + protected Builder(String name, ArgumentType type) { + this.name = name; + this.type = type; + } + + /** + * @return argument name + */ + public String getName() { + return this.name; + } + + /** + * @return argument type + */ + public ArgumentType getType() { + return this.type; + } + + /** + * @param provider new suggestion provider for command being built + * @return this + * @see SkriptSuggestionProvider + */ + @Contract("_ -> this") + public Builder suggests(SuggestionProvider provider) { + this.suggestionsProvider = provider; + return getThis(); + } + + /** + * @return suggestion provider for command being built + */ + public @Nullable SuggestionProvider getSuggestionsProvider() { + return suggestionsProvider; + } + + @Override + protected Builder getThis() { + return this; + } + + @Override + public ArgumentSkriptCommandNode build() { + ArgumentSkriptCommandNode result = new ArgumentSkriptCommandNode<>(getName(), getType(), getCommand(), + getRequirement(), getRedirect(), getRedirectModifier(), isFork(), getSuggestionsProvider(), + getPermission(), getPermissionMessage(), getPossibleSources(), getCooldown()); + for (CommandNode argument : getArguments()) + result.addChild(argument); + return result; + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/brigadier/LiteralSkriptCommandNode.java b/src/main/java/org/skriptlang/skript/brigadier/LiteralSkriptCommandNode.java new file mode 100644 index 00000000000..4dd7f92bcfb --- /dev/null +++ b/src/main/java/org/skriptlang/skript/brigadier/LiteralSkriptCommandNode.java @@ -0,0 +1,166 @@ +package org.skriptlang.skript.brigadier; + +import ch.njol.skript.lang.VariableString; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.RedirectModifier; +import com.mojang.brigadier.StringReader; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.context.CommandContextBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.command.CommandCooldown; +import org.skriptlang.skript.lang.command.CommandSourceType; + +import java.util.Collection; +import java.util.concurrent.CompletableFuture; +import java.util.function.Predicate; + +/** + * SkriptCommandNode implementation of {@link LiteralCommandNode}. + * + * @param command sender type + * @see Builder#literal(String) + */ +public non-sealed class LiteralSkriptCommandNode + extends SkriptCommandNode> { + + private final LiteralCommandNode wrapped; + + protected LiteralSkriptCommandNode(String literal, @Nullable Command command, @Nullable Predicate requirement, + @Nullable CommandNode redirect, @Nullable RedirectModifier modifier, boolean forks, + @Nullable String permission, @Nullable VariableString permissionMessage, + @Nullable Collection possibleSources, @Nullable CommandCooldown cooldown) { + super(command, requirement, redirect, modifier, forks, permission, permissionMessage, + possibleSources, cooldown); + this.wrapped = LiteralArgumentBuilder.literal(literal) + .executes(command) + .requires(requirement) + .forward(redirect, modifier, forks) + .build(); + } + + /** + * @return literal of this command node + */ + public String getLiteral() { + return wrapped.getLiteral(); + } + + @Override + public LiteralCommandNode flat() { + LiteralArgumentBuilder builder = LiteralArgumentBuilder.literal(getLiteral()); + SkriptCommandNode.flat(builder, this); + return builder.build(); + } + + @Override + @SuppressWarnings("unchecked") + public LiteralArgumentBuilder flatToBuilder() { + return (LiteralArgumentBuilder) super.flatToBuilder(); + } + + @Override + protected boolean isValidInput(String s) { + return wrapped.isValidInput(s); + } + + @Override + public String getName() { + return wrapped.getName(); + } + + @Override + public String getUsageText() { + return wrapped.getUsageText(); + } + + @Override + public void parse(StringReader stringReader, + CommandContextBuilder commandContextBuilder) throws CommandSyntaxException { + wrapped.parse(stringReader, commandContextBuilder); + } + + @Override + public CompletableFuture listSuggestions(CommandContext commandContext, + SuggestionsBuilder suggestionsBuilder) { + return wrapped.listSuggestions(commandContext, suggestionsBuilder); + } + + @Override + public Builder createBuilder() { + return Builder.literal(getLiteral()) + .executes(getCommand()) + .requires(getRequirement()) + .forward(getRedirect(), getRedirectModifier(), isFork()) + .permission(getPermission()) + .permissionMessage(getPermissionMessage()) + .possibleSources(getPossibleSources()) + .cooldown(getCooldown()); + } + + @Override + protected String getSortedKey() { + return getLiteral(); + } + + @Override + public Collection getExamples() { + return wrapped.getExamples(); + } + + /** + * Builder implementation for {@link LiteralSkriptCommandNode}. + * + * @param command sender type + */ + public static non-sealed class Builder + extends SkriptCommandNode.Builder, LiteralSkriptCommandNode, Builder> { + + /** + * Creates new builder for literal skript command node with given name. + * + * @param name name of the literal + * @return builder + * @param command sender type + */ + public static Builder literal(String name) { + return new Builder<>(name); + } + + private final String literal; + + protected Builder(String literal) { + this.literal = literal; + } + + /** + * @return literal name + */ + public String getLiteral() { + return literal; + } + + @Override + protected Builder getThis() { + return this; + } + + @Override + public LiteralSkriptCommandNode build() { + LiteralSkriptCommandNode result = new LiteralSkriptCommandNode<>(getLiteral(), getCommand(), + getRequirement(), getRedirect(), getRedirectModifier(), isFork(), getPermission(), + getPermissionMessage(), getPossibleSources(), getCooldown()); + for (CommandNode argument : getArguments()) + result.addChild(argument); + return result; + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/brigadier/RootSkriptCommandNode.java b/src/main/java/org/skriptlang/skript/brigadier/RootSkriptCommandNode.java new file mode 100644 index 00000000000..87ed5c5e419 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/brigadier/RootSkriptCommandNode.java @@ -0,0 +1,192 @@ +package org.skriptlang.skript.brigadier; + +import ch.njol.skript.lang.VariableString; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.RedirectModifier; +import com.mojang.brigadier.tree.CommandNode; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.lang.command.CommandCooldown; +import org.skriptlang.skript.lang.command.CommandSourceType; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; +import java.util.function.Predicate; + +/** + * Represents a root node for a Skript command. + *

+ * This is different form {@link com.mojang.brigadier.tree.RootCommandNode}, which is used as a root node + * for a CommandDispatcher. + *

+ * This class just expands on {@link LiteralSkriptCommandNode} with additional properties of a Skript command. + * + * @param command source type + */ +public class RootSkriptCommandNode extends LiteralSkriptCommandNode { + + private final String namespace; + private final @Nullable String description; + private final @Nullable VariableString usage; // TODO this should be ch.njol.skript.command.CommandUsage + private final @Unmodifiable Set aliases = new LinkedHashSet<>(); + + protected RootSkriptCommandNode(String namespace, @Nullable String description, @Nullable VariableString usage, + @Nullable Collection aliases, String literal, @Nullable Command command, + @Nullable Predicate requirement, @Nullable CommandNode redirect, + @Nullable RedirectModifier modifier, boolean forks, @Nullable String permission, + @Nullable VariableString permissionMessage, @Nullable Collection possibleSources, + @Nullable CommandCooldown cooldown) { + super(literal, command, requirement, redirect, modifier, forks, permission, permissionMessage, + possibleSources, cooldown); + this.namespace = namespace; + this.description = description; + this.usage = usage; + if (aliases != null) + this.aliases.addAll(aliases); + } + + /** + * @return namespace + */ + public String getNamespace() { + return namespace; + } + + /** + * @return description + */ + public @Nullable String getDescription() { + return description; + } + + /** + * @return usage + */ + public @Nullable VariableString getUsage() { + return usage; + } + + /** + * @return aliases + */ + public @Unmodifiable Set getAliases() { + return aliases; + } + + public static class Builder extends LiteralSkriptCommandNode.Builder { + + /** + * Creates new builder for root skript command node with given namespace and name. + * + * @param namespace namespace of the root command + * @param name name of the literal + * @return builder + * @param command sender type + */ + public static Builder root(String namespace, + String name) { + return new Builder<>(namespace, name); + } + + private final String namespace; + private @Nullable String description; + private @Nullable VariableString usage; + private @Nullable Set aliases = new LinkedHashSet<>(); + + protected Builder(String namespace, String literal) { + super(literal); + this.namespace = namespace; + } + + /** + * @return literal name + */ + public String getNamespace() { + return namespace; + } + + /** + * @param description new description for command being built + * @return this + */ + public Builder description(String description) { + this.description = description; + return this; + } + + /** + * @return description + */ + public @Nullable String getDescription() { + return description; + } + + /** + * @param usage new usage for command being built + * @return this + */ + public Builder usage(VariableString usage) { + this.usage = usage; + return this; + } + + /** + * @return usage + */ + public @Nullable VariableString getUsage() { + return usage; + } + + /** + * @param aliases new aliases for command being built + * @return this + */ + public Builder aliases(String... aliases) { + if (aliases == null) { + this.aliases = null; + return this; + } + return aliases(Set.of(aliases)); + } + + /** + * @param aliases new aliases for command being built + * @return this + */ + public Builder aliases(@Nullable Collection aliases) { + if (aliases == null) { + this.aliases = null; + return this; + } + this.aliases = new LinkedHashSet<>(aliases); + return this; + } + + /** + * @return aliases + */ + public @Nullable Set getAliases() { + return aliases; + } + + @Override + protected Builder getThis() { + return this; + } + + @Override + public RootSkriptCommandNode build() { + RootSkriptCommandNode result = new RootSkriptCommandNode<>(getNamespace(), + getDescription(), getUsage(), getAliases(), getLiteral(), getCommand(), + getRequirement(), getRedirect(), getRedirectModifier(), isFork(), getPermission(), + getPermissionMessage(), getPossibleSources(), getCooldown()); + for (CommandNode argument : getArguments()) + result.addChild(argument); + return result; + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java b/src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java new file mode 100644 index 00000000000..a578954bf4b --- /dev/null +++ b/src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java @@ -0,0 +1,287 @@ +package org.skriptlang.skript.brigadier; + +import ch.njol.skript.command.brigadier.PaperCommandUtils; +import ch.njol.skript.lang.VariableString; +import com.mojang.brigadier.Command; +import com.mojang.brigadier.RedirectModifier; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.tree.ArgumentCommandNode; +import com.mojang.brigadier.tree.CommandNode; +import com.mojang.brigadier.tree.LiteralCommandNode; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.lang.command.CommandCooldown; +import org.skriptlang.skript.lang.command.CommandSourceType; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Predicate; + +/** + * Command nodes provided by Skript with additional properties. + * + * @param command source + */ +public sealed abstract class SkriptCommandNode> extends CommandNode + permits ArgumentSkriptCommandNode, LiteralSkriptCommandNode { + + private final @Nullable String permission; + private final @Nullable VariableString permissionMessage; + private final Set possibleSources = new HashSet<>(); + private final @Nullable CommandCooldown cooldown; + + protected SkriptCommandNode(@Nullable Command command, @Nullable Predicate requirement, + @Nullable CommandNode redirect, @Nullable RedirectModifier modifier, boolean forks, + @Nullable String permission, @Nullable VariableString permissionMessage, + @Nullable Collection possibleSources, @Nullable CommandCooldown cooldown) { + super(command, requirement, redirect, modifier, forks); + this.permission = permission; + this.permissionMessage = permissionMessage; + if (possibleSources != null) + this.possibleSources.addAll(possibleSources); + this.cooldown = cooldown; + } + + /** + * @return permission required to execute the command + */ + public @Nullable String getPermission() { + return permission; + } + + /** + * @return message if the command source does not have required permissions to execute the command + */ + public @Nullable VariableString getPermissionMessage() { + return permissionMessage; + } + + /** + * @return possible command source types for this command ({@code executable by} entry) + */ + public @Unmodifiable Set getPossibleSources() { + return Collections.unmodifiableSet(possibleSources); + } + + /** + * @return cooldown of this command + */ + public @Nullable CommandCooldown getCooldown() { + return cooldown; + } + + /** + * Creates native brigadier node from this one. + *

+ * Given node must implement the additional properties within its + * executable command or requirements. + *

+ * This is used for transformation of Skript command nodes to regular Brigadier nodes that + * are accepted by platforms such as Paper. + * + * @return native brigadier node + */ + public abstract N flat(); + + /** + * Creates native brigadier node builder from this node. + *

+ * The conversion is done using {@link #flat()}. All the attributes are then copied + * to the newly created builder instance. + *

+ * This differs from {@link CommandNode#createBuilder()} because it also copies arguments. + * + * @return native brigadier builder + */ + @SuppressWarnings("unchecked") + public > ArgumentBuilder flatToBuilder() { + return (ArgumentBuilder) flatToBuilder(flat()); + } + + @SuppressWarnings("unchecked") + private static ArgumentBuilder flatToBuilder(CommandNode node) { + ArgumentBuilder builder; + if (node instanceof LiteralCommandNode lcn) { + builder = LiteralArgumentBuilder.literal(lcn.getLiteral()); + } else if (node instanceof ArgumentCommandNode acn) { + builder = RequiredArgumentBuilder.argument(acn.getName(), acn.getType()); + } else { + throw new IllegalStateException("Unsupported command node type; only native brigadier nodes are supported"); + } + if (node.getRequirement() != null) + builder.requires(node.getRequirement()); + if (node.getRedirect() != null) + builder.forward(node.getRedirect(), node.getRedirectModifier(), node.isFork()); + if (node.getCommand() != null) + builder.executes(node.getCommand()); + for (CommandNode child : node.getChildren()) { + CommandNode nativeNode = child; + if (child instanceof SkriptCommandNode scn) + nativeNode = (CommandNode) scn.flat(); + builder.then(flatToBuilder(nativeNode)); + } + return builder; + } + + /** + * Copies argument node data from given SkriptCommandNode to provided builder and + * provides correct implementation for the data unique to SkriptCommandNodes. + *

+ * This method is used for simplifying the implementation of {@link #flat()}. + * + * @param builder builder to copy the data to + * @param node SkriptCommandNode to copy (as native brigadier node) + * @param command sender type + * @param command node builder + * @param skript command node to flat + */ + @SuppressWarnings("unchecked") + static , + F extends SkriptCommandNode> void flat(B builder, F node) { + builder.requires(sender -> { + if (!node.getRequirement().test(sender)) + return false; + boolean possibleSource = node.getPossibleSources().stream().anyMatch(s -> s.check(null, sender)); + // if there are no specified possible sources, everyone can execute the command + if (!possibleSource && !node.getPossibleSources().isEmpty()) + return false; + String permission = node.getPermission(); + return permission == null || permission.isBlank() || sender.hasPermission(permission); + }); + + CommandNode redirect = node.getRedirect(); + if (redirect instanceof SkriptCommandNode scn) + redirect = (CommandNode) scn.flat(); + builder.forward(redirect, node.getRedirectModifier(), node.isFork()); + + if (node.getCommand() != null) { + builder.executes(context -> { + S sender = context.getSource(); + boolean possibleSource = node.getPossibleSources().stream().anyMatch(s -> s.check(context, sender)); + if (!possibleSource && !node.getPossibleSources().isEmpty()) { + PaperCommandUtils.sendInvalidExecutorMessage(context); + return 0; + } + String permission = node.getPermission(); + if (permission != null && !permission.isBlank() && !sender.hasPermission(permission)) { + PaperCommandUtils.sendPermissionMessage(context, node.getPermissionMessage()); + return 0; + } + if (!node.getRequirement().test(sender)) + return 0; + return node.getCommand().run(context); + }); + } + + for (CommandNode child : node.getChildren()) { + if (child instanceof SkriptCommandNode scn) { + builder.then((CommandNode) scn.flat()); + continue; + } + builder.then(child); + } + } + + /** + * Represents builder of a SkriptCommandNode. + * + * @param command sender type + * @param native brigadier command node of the SkriptCommandNode + * @param the SkriptCommandNode that is being built + * @param this builder + */ + public sealed abstract static class Builder, + Built extends SkriptCommandNode, T extends Builder> + extends ArgumentBuilder permits ArgumentSkriptCommandNode.Builder, LiteralSkriptCommandNode.Builder { + + private @Nullable String permission = null; + private @Nullable VariableString permissionMessage = null; + private final Set possibleSources = new HashSet<>(); + private @Nullable CommandCooldown cooldown = null; + + /** + * @param permission new permission for command being built + * @return this + */ + @Contract("_ -> this") + public T permission(@Nullable String permission) { + this.permission = permission; + return getThis(); + } + + /** + * @return permission for command being built + */ + public @Nullable String getPermission() { + return permission; + } + + /** + * @param permissionMessage new permission message for command being built + * @return this + */ + @Contract("_ -> this") + public T permissionMessage(@Nullable VariableString permissionMessage) { + this.permissionMessage = permissionMessage; + return getThis(); + } + + /** + * @return permission message for command being built + */ + public @Nullable VariableString getPermissionMessage() { + return permissionMessage; + } + + /** + * @param possibleSources new possible sources for command being built ({@code executable by}) + * @return this + */ + @Contract("_ -> this") + public T possibleSources(Collection possibleSources) { + this.possibleSources.clear(); + this.possibleSources.addAll(possibleSources); + return getThis(); + } + + /** + * @return possible sources for command being built ({@code executable by}) + */ + public @Unmodifiable Set getPossibleSources() { + return Set.copyOf(possibleSources); + } + + /** + * @param cooldown new cooldown for command being built + * @return this + */ + @Contract("_ -> this") + public T cooldown(@Nullable CommandCooldown cooldown) { + this.cooldown = cooldown; + return getThis(); + } + + /** + * @return cooldown for command being built + */ + public @Nullable CommandCooldown getCooldown() { + return cooldown; + } + + /** + * Builds the SkriptCommandNode from this builder. + * + * @return new SkriptCommandNode from this builder + */ + @Contract(pure = true) + public abstract Built build(); + + } + +} diff --git a/src/main/java/org/skriptlang/skript/brigadier/SkriptSuggestionProvider.java b/src/main/java/org/skriptlang/skript/brigadier/SkriptSuggestionProvider.java new file mode 100644 index 00000000000..1e6e27470f0 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/brigadier/SkriptSuggestionProvider.java @@ -0,0 +1,67 @@ +package org.skriptlang.skript.brigadier; + +import ch.njol.skript.util.chat.BungeeConverter; +import ch.njol.skript.util.chat.ChatMessages; +import ch.njol.skript.util.chat.MessageComponent; +import com.mojang.brigadier.Message; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import io.papermc.paper.command.brigadier.MessageComponentSerializer; +import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer; +import net.md_5.bungee.api.chat.BaseComponent; +import org.bukkit.command.CommandSender; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * Implementation of {@link SuggestionProvider} that accepts returning Skript MessageComponents + * instead of Brigadier Messages. + *

+ * Tooltips of those MessageComponents are mapped to tooltips of the suggestions if + * they are provided as text. + * + * @param command source + */ +@FunctionalInterface +public interface SkriptSuggestionProvider extends SuggestionProvider { + + @Override + default CompletableFuture getSuggestions(CommandContext commandContext, + SuggestionsBuilder suggestionsBuilder) throws CommandSyntaxException { + return getSuggestions(commandContext) + .thenApply(components -> { + for (MessageComponent component : components) { + Message tooltip; + + if (component.hoverEvent != null + && component.hoverEvent.action == MessageComponent.HoverEvent.Action.show_text) { + //noinspection deprecation + BaseComponent[] baseComponents = BungeeConverter + .convert(ChatMessages.parseToArray(component.hoverEvent.value)); + tooltip = MessageComponentSerializer.message() + .serialize(BungeeComponentSerializer.get().deserialize(baseComponents)); + } else { + tooltip = null; + } + suggestionsBuilder.suggest(component.text, tooltip); + } + return suggestionsBuilder.build(); + }); + } + + /** + * Returns suggestions for given context as Skript MessageComponents. + *

+ * Tooltips of those MessageComponents are mapped to tooltips of the suggestions if + * they are provided as text. + * + * @param commandContext command context + * @return suggestions + */ + CompletableFuture> getSuggestions(CommandContext commandContext) throws CommandSyntaxException; + +} diff --git a/src/main/java/org/skriptlang/skript/lang/command/ArgumentTypeElement.java b/src/main/java/org/skriptlang/skript/lang/command/ArgumentTypeElement.java new file mode 100644 index 00000000000..5babb72545e --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/command/ArgumentTypeElement.java @@ -0,0 +1,95 @@ +package org.skriptlang.skript.lang.command; + +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.skript.lang.SyntaxElement; +import ch.njol.util.Kleenean; +import com.mojang.brigadier.arguments.ArgumentType; +import io.papermc.paper.command.brigadier.argument.CustomArgumentType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +/** + * Represents a custom argument type ranging from simple types to more complex structures + * like {@code integer between 1 and 5}. + *

+ * Implementations of this class are responsible for converting their Skript-specific + * argument definition into a Brigadier {@link ArgumentType}. + * + * @param the type that the Brigadier {@link ArgumentType} will parse the argument into + * + * @see Custom + */ +public abstract class ArgumentTypeElement implements SyntaxElement { + + /** + * Registry key for the argument types. + */ + public static final SyntaxRegistry.Key>> REGISTRY_KEY + = SyntaxRegistry.Key.of("argument type"); + + /** + * Returns the Brigadier {@link ArgumentType} that corresponds to this argument type element. + * + * @apiNote If this argument type element represents a custom type not supported + * by Paper, and is meant to be used within Paper environment, it is required to provide an + * appropriate {@link io.papermc.paper.command.brigadier.argument.CustomArgumentType} + * implementation. + * + * @return a {@link ArgumentType} instance representing this argument type element + */ + public abstract ArgumentType toBrigadier(); + + @Override + public @NotNull String getSyntaxTypeName() { + return "argument type"; + } + + /** + * Custom argument type that also implements {@link CustomArgumentType}. + *

+ * This class serves as a convenient base for implementing custom argument types + * available within Paper environment. + * + * @param the type that the Brigadier {@link ArgumentType} will parse the argument into + * @param the native argument type supported by Paper + */ + public abstract static class Custom extends ArgumentTypeElement implements CustomArgumentType { + + @Override + public ArgumentType toBrigadier() { + return this; + } + + } + + /** + * Convenient implementation of ArgumentTypeElement that makes it simple to wrap around + * existing argument types. + * + * @param the type that the Brigadier {@link ArgumentType} will parse the argument into + */ + public abstract static class Simple extends ArgumentTypeElement { + + private ArgumentType nativeType; + + protected abstract @Nullable ArgumentType get(Expression[] expressions, int matchedPattern, + SkriptParser.ParseResult parseResult); + + @Override + public ArgumentType toBrigadier() { + return nativeType; + } + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, + SkriptParser.ParseResult parseResult) { + nativeType = get(expressions, matchedPattern, parseResult); + return nativeType != null; + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/lang/command/CommandArgument.java b/src/main/java/org/skriptlang/skript/lang/command/CommandArgument.java new file mode 100644 index 00000000000..9d9f9d95cc2 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/command/CommandArgument.java @@ -0,0 +1,70 @@ +package org.skriptlang.skript.lang.command; + +import com.mojang.brigadier.arguments.ArgumentType; +import org.bukkit.command.CommandSender; +import org.skriptlang.skript.brigadier.ArgumentSkriptCommandNode; +import org.skriptlang.skript.brigadier.LiteralSkriptCommandNode; +import org.skriptlang.skript.brigadier.RootSkriptCommandNode; +import org.skriptlang.skript.brigadier.SkriptCommandNode; + +/** + * Represents a parsed command argument (from Skript code). + */ +// TODO optional, possibly plural +public sealed interface CommandArgument { + + /** + * Creates an empty builder from given command argument. + * + * @return empty builder for this command argument + */ + SkriptCommandNode.Builder emptyBuilder(); + + /** + * Represents a literal. + * + * @param literal literal + */ + record Literal(String literal) implements CommandArgument { + @Override + public LiteralSkriptCommandNode.Builder emptyBuilder() { + return LiteralSkriptCommandNode.Builder.literal(literal); + } + } + + /** + * Represents a typed argument parsed from {@link ArgumentTypeElement}. + * + * @param name name of the argument + * @param type argument type + * @param argument type + */ + record Typed(String name, ArgumentType type) implements CommandArgument { + + public Typed(String name, ArgumentTypeElement typeElement) { + this(name, typeElement.toBrigadier()); + } + + @Override + public ArgumentSkriptCommandNode.Builder emptyBuilder() { + return ArgumentSkriptCommandNode.Builder.argument(name, type); + } + + } + + /** + * Represents a root command node. + * + * @param label label + * @see org.skriptlang.skript.brigadier.RootSkriptCommandNode + */ + record Root(String namespace, String label) implements CommandArgument { + + @Override + public RootSkriptCommandNode.Builder emptyBuilder() { + return RootSkriptCommandNode.Builder.root(namespace, label); + } + + } + +} diff --git a/src/main/java/org/skriptlang/skript/lang/command/CommandArgumentParser.java b/src/main/java/org/skriptlang/skript/lang/command/CommandArgumentParser.java new file mode 100644 index 00000000000..022902e64f9 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/command/CommandArgumentParser.java @@ -0,0 +1,97 @@ +package org.skriptlang.skript.lang.command; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.lang.SkriptParser; +import com.google.common.base.Preconditions; +import org.skriptlang.skript.registration.SyntaxInfo; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A utility class responsible for parsing command argument strings. + *

+ * It tokenizes an input string into a list of {@link CommandArgument} objects. + */ +public final class CommandArgumentParser { + + private CommandArgumentParser() { + throw new UnsupportedOperationException(); + } + + private static final Pattern ARGUMENT_TOKENIZER_PATTERN = + Pattern.compile("<[^>]+>|\\S+"); + + private static final Pattern TYPED_ARGUMENT_PARSER_PATTERN = + Pattern.compile("\\s*([^:]+?)\\s*:\\s*(.+?)\\s*"); + + /** + * Parses a raw string of arguments into a list of CommandArgument objects. + * + * @param arguments the raw argument string (e.g. "help ") + * @return a list of parsed command arguments + */ + public static List parse(String arguments) { + if (arguments == null || arguments.isBlank()) + return Collections.emptyList(); + + Matcher matcher = ARGUMENT_TOKENIZER_PATTERN.matcher(arguments); + List argumentList = new LinkedList<>(); + + while (matcher.find()) { + String token = matcher.group(); + try { + argumentList.add(parseToken(token)); + } catch (IllegalArgumentException e) { + Skript.error("Failed to parse command argument '" + token + "': " + e.getMessage()); + // Stop parsing on the first error. + return Collections.emptyList(); + } + } + return argumentList; + } + + /** + * Parses a single argument token into a {@link CommandArgument} object. + * + * @param token a single token (e.g. "" or "help"). + * @return The parsed CommandArgument + */ + private static CommandArgument parseToken(String token) { + if (token.startsWith("<") && token.endsWith(">")) { + return parseTypedArgument(token); + } else { + return new CommandArgument.Literal(token); + } + } + + @SuppressWarnings("unchecked") + private static CommandArgument.Typed parseTypedArgument(String token) { + // remove the outer brackets to get the content + String content = token.substring(1, token.length() - 1); + Matcher typedMatcher = TYPED_ARGUMENT_PARSER_PATTERN.matcher(content); + + Preconditions.checkArgument(typedMatcher.matches(), "Invalid format: must be '', but got '" + token + "'"); + + String name = typedMatcher.group(1); + String typeExpression = typedMatcher.group(2); + + Preconditions.checkArgument(!name.isBlank(), "Argument name cannot be blank in '" + token + "'"); + Preconditions.checkArgument(!typeExpression.isBlank(), "Argument type cannot be blank in '" + token + "'"); + + var argumentTypes = (Collection>>) (Collection) + Skript.instance().syntaxRegistry().syntaxes(ArgumentTypeElement.REGISTRY_KEY); + + ArgumentTypeElement parsedType = SkriptParser.parseStatic(typeExpression, argumentTypes.iterator(), + ParseContext.DEFAULT, "Failed to parse argument type '" + typeExpression + "'"); + + Preconditions.checkArgument(parsedType != null, "Unknown argument type in '" + token + "'"); + return new CommandArgument.Typed<>(name, parsedType); + } + +} diff --git a/src/main/java/org/skriptlang/skript/lang/command/CommandCooldown.java b/src/main/java/org/skriptlang/skript/lang/command/CommandCooldown.java new file mode 100644 index 00000000000..c63d6d75ca6 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/command/CommandCooldown.java @@ -0,0 +1,5 @@ +package org.skriptlang.skript.lang.command; + +// TODO +public class CommandCooldown { +} diff --git a/src/main/java/org/skriptlang/skript/lang/command/CommandHandler.java b/src/main/java/org/skriptlang/skript/lang/command/CommandHandler.java new file mode 100644 index 00000000000..b517ee7824b --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/command/CommandHandler.java @@ -0,0 +1,70 @@ +package org.skriptlang.skript.lang.command; + +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.jetbrains.annotations.UnmodifiableView; +import org.skriptlang.skript.brigadier.RootSkriptCommandNode; + +import java.util.Map; +import java.util.Set; + +/** + * Handles the registration, un-registration and dispatching of commands. + * + * @param command source + */ +public interface CommandHandler { + + /** + * Registers a new command. + * + * @param command command to register + * @return if the command was registered + */ + boolean registerCommand(RootSkriptCommandNode command); + + /** + * Unregisters a command. + * + * @param command command to unregister + * @return if the command was unregistered + */ + boolean unregisterCommand(RootSkriptCommandNode command); + + /** + * Returns command of given label or alias. + * + * @param label command label or alias + * @return command with given label if it exists + */ + @Nullable RootSkriptCommandNode getCommand(String label); + + /** + * Returns all registered commands. + * + * @return all registered commands + */ + @UnmodifiableView Map> getAllCommands(); + + /** + * Dispatches a command from a given source. + * This method is responsible for parsing and executing the command logic. + * + * @param source source initiating the command + * @return true if the command dispatch was handled (not necessarily successful execution, but + * the handler processed it), false otherwise + */ + boolean dispatchCommand(S source, String input); + + /** + * Returns supported command source types. + *

+ * These types are used in the {@code executable by} entry in command structures. + * + * @return supported command source types + */ + @Unmodifiable Set supportedTypes(); + +} + diff --git a/src/main/java/org/skriptlang/skript/lang/command/CommandSourceType.java b/src/main/java/org/skriptlang/skript/lang/command/CommandSourceType.java new file mode 100644 index 00000000000..bf2fc820d7f --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/command/CommandSourceType.java @@ -0,0 +1,50 @@ +package org.skriptlang.skript.lang.command; + +import com.mojang.brigadier.context.CommandContext; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.Set; +import java.util.function.BiFunction; + +/** + * Represents a type of command source, e.g. console. + * + * @param type class representing the command source + * @param names names of the command source, this is used by the {@code executable by} entry + * @param checkFunction function to check if the source type can execute command in given context + */ +public record CommandSourceType(Class type, + @Unmodifiable Set names, + BiFunction<@Nullable CommandContext, CommandSender, Boolean> checkFunction) { + + public CommandSourceType { + names = Set.copyOf(names); + } + + /** + * Simple command source type that just checks if the + * sender is instance of the CommandSourceType type. + * + * @param type lass representing the command source + * @param names names of the command source, this is used by the {@code executable by} entry + * @return command source type instance + */ + public static CommandSourceType typed(Class type, String... names) { + return new CommandSourceType(type, Set.of(names), ((context, sender) -> type.isInstance(sender))); + } + + /** + * Checks whether given sender is instance of this source type. + * + * @param sender sender to check + * @return if the sender is of this type + * @param command sender type + */ + @SuppressWarnings("unchecked") + public boolean check(@Nullable CommandContext context, S sender) { + return checkFunction.apply((CommandContext) context, sender); + } + +} diff --git a/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java b/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java new file mode 100644 index 00000000000..3817561f572 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java @@ -0,0 +1,271 @@ +package org.skriptlang.skript.lang.command; + +import ch.njol.skript.ScriptLoader; +import ch.njol.skript.Skript; +import ch.njol.skript.command.brigadier.BrigadierCommandEvent; +import ch.njol.skript.command.brigadier.BrigadierSuggestionsEvent; +import ch.njol.skript.config.SectionNode; +import ch.njol.skript.lang.*; +import ch.njol.skript.lang.parser.ParserInstance; +import ch.njol.skript.lang.util.SimpleEvent; +import ch.njol.skript.util.chat.ChatMessages; +import com.google.common.base.Preconditions; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.event.Event; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.brigadier.ArgumentSkriptCommandNode; +import org.skriptlang.skript.brigadier.RootSkriptCommandNode; +import org.skriptlang.skript.brigadier.SkriptCommandNode; +import org.skriptlang.skript.brigadier.SkriptSuggestionProvider; +import org.skriptlang.skript.lang.entry.EntryContainer; + +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +public final class CommandUtils { + + private CommandUtils() { + throw new UnsupportedOperationException(); + } + + /** + * Checks whether the entry container can represent a command entry. + * + * @param entryContainer entry container + * @return whether the given container can represent a command entry + */ + public static boolean isValidCommandNode(EntryContainer entryContainer) { + if (!entryContainer.hasEntry(SubCommandEntryData.SUBCOMMAND_KEY) + && !entryContainer.hasEntry(SubCommandEntryData.TRIGGER_KEY)) { + Skript.error("Command node needs to have either a trigger or another sub-command entry."); + return false; + } + return true; + } + + /** + * Creates a command node from a sub command entry data. + *

+ * If parent is provided, the created node is added as its child. + * Parent properties are also copied to the created node. + * + * @param handler command handler + * @param subCommandData sub command entry data + * @param parent parent command node (inherits command properties), this is always present for + * sub command entries (either another sub command entry or the root command) + * @return created skript command node or null if the creation failed + */ + public static @Nullable SkriptCommandNode createCommandNode(CommandHandler handler, + SubCommandEntryData.Parsed subCommandData, SkriptCommandNode.Builder parent) { + Preconditions.checkNotNull(parent, "Sub-command entry data always have a parent command node"); + if (subCommandData.entryContainer() == null || subCommandData.arguments() == null) + return null; + return createCommandNode(handler, subCommandData.entryContainer(), parent, subCommandData.arguments()); + } + + /** + * Creates a command node from an entry container and parsed command arguments. + *

+ * If parent is provided, the created node is added as its child. + * Parent properties are also copied to the created node. + * + * @param handler command handler + * @param entryContainer entry container + * @param parent parent command node (inherits command properties) + * @param arguments arguments + * @return created skript command node or null if the creation failed + */ + public static @Nullable SkriptCommandNode createCommandNode(CommandHandler handler, + EntryContainer entryContainer, @Nullable SkriptCommandNode.Builder parent, + List arguments) { + if (arguments.isEmpty()) { + Skript.error("Command nodes can not be empty; arguments are missing"); + return null; + } + + List> builders = new LinkedList<>(); + for (int i = 0; i < arguments.size(); i++) { + var builder = arguments.get(i).emptyBuilder(); + // first we copy parent properties + copyParentProperties(builder, parent); + // now we override them if they are present in the entry container for this node + if (!setCommandNodeProperties(handler, entryContainer, builder, i == arguments.size() - 1)) + return null; + builders.add(builder); + } + + // now we prepared all builders, it is time to connect them together, from last to first one + for (int i = builders.size() - 1; i > 0; i--) { + var current = builders.get(i); + var previous = builders.get(i - 1); + previous.then(current); + } + + SkriptCommandNode firstNode = builders.get(0).build(); + if (parent != null) + parent.then(firstNode); + return firstNode; + } + + private static void copyParentProperties(SkriptCommandNode.Builder builder, + @Nullable SkriptCommandNode.Builder parent) { + if (parent == null) return; + builder.permission(parent.getPermission()); + builder.permissionMessage(parent.getPermissionMessage()); + builder.possibleSources(parent.getPossibleSources()); + builder.cooldown(parent.getCooldown()); + } + + /** + * Updates the command node builder with command properties of given entry container. + * + * @param handler command handler + * @param entryContainer entry container + * @param builder builder + * @param last whether the node is at the end of the command argument + * definition, e.g. for this command '/foo bar world', 'world' is the last node + * @return false if the provided data are not compatible with given handler, else true + */ + public static boolean setCommandNodeProperties(CommandHandler handler, + EntryContainer entryContainer, SkriptCommandNode.Builder builder, + boolean last) { + + // root command properties + if (builder instanceof RootSkriptCommandNode.Builder rootBuilder) { + if (entryContainer.hasEntry(StructGeneralCommand.DESCRIPTION_KEY)) { + rootBuilder.description(entryContainer.get(StructGeneralCommand.DESCRIPTION_KEY, String.class, + false)); + } + if (entryContainer.hasEntry(StructGeneralCommand.USAGE_KEY)) { + rootBuilder.usage(entryContainer.getOptional(StructGeneralCommand.USAGE_KEY, VariableString.class, + false)); + } + if (entryContainer.hasEntry(StructGeneralCommand.ALIASES_KEY)) { + //noinspection unchecked + rootBuilder.aliases(entryContainer.get(StructGeneralCommand.ALIASES_KEY, List.class, + true)); + } + } + + if (entryContainer.hasEntry(SubCommandEntryData.PERMISSION_KEY)) { + builder.permission(entryContainer.getOptional(SubCommandEntryData.PERMISSION_KEY, + String.class, false)); + } + if (entryContainer.hasEntry(SubCommandEntryData.PERMISSION_MSG_KEY)) { + builder.permissionMessage(entryContainer.getOptional(SubCommandEntryData.PERMISSION_MSG_KEY, + VariableString.class, false)); + } + + if (entryContainer.hasEntry(SubCommandEntryData.EXECUTABLE_KEY)) { + List possibleSources = new LinkedList<>(); + for (Object source : entryContainer.get(SubCommandEntryData.EXECUTABLE_KEY, List.class, true)) { + var found = handler.supportedTypes().stream() + .filter(type -> type.names().contains(source.toString())) + .findFirst(); + if (found.isPresent()) { + possibleSources.add(found.get()); + continue; + } + Skript.error("Invalid command source type: " + source); + return false; + } + builder.possibleSources(possibleSources); + } + + // TODO cooldown + + // suggestions, trigger and children are only considered for the last node + if (!last) return true; + + if (entryContainer.hasEntry(SubCommandEntryData.SUGGESTIONS_KEY)) { + if (!(builder instanceof ArgumentSkriptCommandNode.Builder)) { + Skript.error("Custom suggestions can only be provided for argument nodes"); + return false; + } + SectionNode suggestionsNode = entryContainer.get(SubCommandEntryData.SUGGESTIONS_KEY, + SectionNode.class, false); + + ReturnHandler suggestionsReturnHandler = new ReturnHandler<>() { + @Override + public void returnValues(Event event, Expression value) { + if (!(event instanceof BrigadierSuggestionsEvent bse)) return; + bse.setSuggestions(value.getAll(event)); + } + + @Override + public boolean isSingleReturnValue() { + return false; + } + + @Override + public @NotNull Class returnValueType() { + return String.class; + } + }; + + ParserInstance parser = ParserInstance.get(); + ParserInstance.Backup parserBackup = parser.backup(); + parser.reset(); + parser.setCurrentEvent("command suggestions", BrigadierSuggestionsEvent.class); + Trigger trigger = suggestionsReturnHandler.loadReturnableTrigger(suggestionsNode, + "command suggestions", new SimpleEvent()); + trigger.setLineNumber(suggestionsNode.getLine()); + parser.restoreBackup(parserBackup); + + SkriptSuggestionProvider suggestionProvider = ctx -> { + BrigadierSuggestionsEvent event = new BrigadierSuggestionsEvent(ctx); + runTriggerOnMainThread(trigger, event); + // the chat message parsing can be done async + return CompletableFuture.supplyAsync(() -> Arrays.stream(event.getSuggestions()) + .flatMap(s -> ChatMessages.parse(s).stream()) + .toList()); + }; + + //noinspection unchecked + ((ArgumentSkriptCommandNode.Builder) builder).suggests(suggestionProvider); + } + + if (entryContainer.hasEntry(SubCommandEntryData.TRIGGER_KEY)) { + SectionNode triggerNode = entryContainer.get(SubCommandEntryData.TRIGGER_KEY, SectionNode.class, + false); + ParserInstance parser = ParserInstance.get(); + ParserInstance.Backup parserBackup = parser.backup(); + parser.reset(); + parser.setCurrentEvent("command trigger", BrigadierCommandEvent.class); + Trigger trigger = new Trigger(ParserInstance.get().getCurrentScript(), + "command trigger", new SimpleEvent(), ScriptLoader.loadItems(triggerNode)); + trigger.setLineNumber(triggerNode.getLine()); + parser.restoreBackup(parserBackup); + builder.executes(ctx -> runTriggerOnMainThread(trigger, new BrigadierCommandEvent(ctx)) ? 1 : 0); + } + + if (entryContainer.hasEntry(SubCommandEntryData.SUBCOMMAND_KEY)) { + List subCommands = entryContainer.getAll(SubCommandEntryData.SUBCOMMAND_KEY, + SubCommandEntryData.Parsed.class, false); + for (SubCommandEntryData.Parsed subCommand : subCommands) { + createCommandNode(handler, subCommand, builder); + } + } + + return true; + } + + private static boolean runTriggerOnMainThread(Trigger trigger, Event event) { + if (Bukkit.isPrimaryThread()) { + return trigger.execute(event); + } else { + Executor executor = Bukkit.getScheduler().getMainThreadExecutor(Skript.getInstance()); + try { + return CompletableFuture.supplyAsync(() -> trigger.execute(event), executor).get(); + } catch (Exception exception) { + return false; + } + } + } + +} diff --git a/src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java b/src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java new file mode 100644 index 00000000000..9381f47f87b --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java @@ -0,0 +1,155 @@ +package org.skriptlang.skript.lang.command; + +import ch.njol.skript.ScriptLoader; +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.SkriptParser; +import com.google.common.base.Preconditions; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.brigadier.RootSkriptCommandNode; +import org.skriptlang.skript.lang.entry.EntryContainer; +import org.skriptlang.skript.lang.entry.EntryValidator; +import org.skriptlang.skript.lang.entry.KeyValueEntryData; +import org.skriptlang.skript.lang.entry.util.VariableStringEntryData; +import org.skriptlang.skript.lang.structure.Structure; + +import java.util.*; +import java.util.function.Consumer; +import java.util.regex.MatchResult; +import java.util.regex.Pattern; + +/** + * Represents an abstract base class for command structures within Skript. + */ +public abstract class StructGeneralCommand extends Structure { + + // syntax pattern for the command structure + private static final String COMMAND_PATTERN = "command [/]<^(\\S+)\\s*(.+)?>"; + + public static final String DEFAULT_NAMESPACE = "skript"; + + // additional command entry data keys + public static final String NAMESPACE_KEY = "namespace"; + public static final String DESCRIPTION_KEY = "description"; + public static final String USAGE_KEY = "usage"; + public static final String ALIASES_KEY = "aliases"; + + /** + * Registers a command structure. + *

+ * This method provides easy way to register the command structure with already predefined + * syntax and entry validator that can be further modified with provided consumer. + * + * @param structureClass command structure to be registered + * @param builderConsumer consumer for further modification of the entry validator + * @param prefixPatterns string prefixes that define the start of the command structure's syntax. + * Those can not contain expressions or regex. + * @param command structure type + */ + protected static void registerCommandStructure(Class structureClass, + Consumer builderConsumer, String... prefixPatterns) { + String[] patterns = Arrays.stream(prefixPatterns) + .peek(prefix -> Preconditions.checkArgument(!prefix.contains("%"), + "Command prefix can not contain expressions")) + .peek(prefix -> Preconditions.checkArgument(!prefix.contains("<") || prefix.contains(">"), + "Command prefix can not contain regex")) + .map(p -> p.trim().concat(" " + COMMAND_PATTERN)) + .distinct() + .toArray(String[]::new); + Skript.registerStructure( + structureClass, + SubCommandEntryData.validator(builder -> { + builder + .addEntry(NAMESPACE_KEY, DEFAULT_NAMESPACE, true) + .addEntry(DESCRIPTION_KEY, null, true) + .addEntryData(new VariableStringEntryData(USAGE_KEY, null, true)) + .addEntryData(new KeyValueEntryData>(ALIASES_KEY, new ArrayList<>(), true) { + private final Pattern pattern = Pattern.compile("\\s*,\\s*/?"); + @Override + protected List getValue(String value) { + List aliases = new ArrayList<>(Arrays.asList(pattern.split(value))); + if (aliases.get(0).startsWith("/")) { + aliases.set(0, aliases.get(0).substring(1)); + } else if (aliases.get(0).isEmpty()) { + aliases = new ArrayList<>(0); + } + return aliases; + } + }); + builderConsumer.accept(builder); + }), + patterns + ); + } + + /** + * Registers a command structure. + *

+ * This method provides easy way to register the command structure with already predefined syntax. + * + * @param structureClass command structure to be registered + * @param prefixPatterns string prefixes that define the start of the command structure's syntax. + * Those can not contain expressions or regex. + * @param command structure type + */ + protected static void registerCommandStructure(Class structureClass, + String... prefixPatterns) { + registerCommandStructure(structureClass, builder -> {}, prefixPatterns); + } + + protected EntryContainer entryContainer; + protected RootSkriptCommandNode commandNode; + + /** + * Command handler used for the registration and creation of the commands. + * + * @return command handler + */ + public abstract CommandHandler getHandler(); + + @Override + public boolean init(Literal[] args, int matchedPattern, SkriptParser.ParseResult parseResult, + @Nullable EntryContainer entryContainer) { + assert entryContainer != null; // cannot be null for non-simple structures + this.entryContainer = entryContainer; + MatchResult matchResult = parseResult.regexes.get(0); + + String label = ScriptLoader.replaceOptions(matchResult.group(1)); + String namespace = entryContainer.get(NAMESPACE_KEY, String.class, true); + List arguments = Collections.emptyList(); + String rawArguments = matchResult.group(2); + + if (rawArguments != null) { + arguments = CommandArgumentParser.parse(ScriptLoader.replaceOptions(rawArguments)); + if (arguments.isEmpty() && !rawArguments.isBlank()) { + return false; // parsing failed + } + } + + if (!CommandUtils.isValidCommandNode(entryContainer)) + return false; + + List rootArguments = new ArrayList<>(); + rootArguments.add(new CommandArgument.Root(namespace, label)); + rootArguments.addAll(arguments); + + //noinspection unchecked + commandNode = (RootSkriptCommandNode) CommandUtils.createCommandNode(getHandler(), + entryContainer, null, rootArguments); + return commandNode != null; + } + + @Override + public boolean load() { + if (commandNode == null) return false; + return getHandler().registerCommand(commandNode); + } + + @Override + public void unload() { + if (commandNode == null) return; + getHandler().unregisterCommand(commandNode); + } + +} diff --git a/src/main/java/org/skriptlang/skript/lang/command/SubCommandEntryData.java b/src/main/java/org/skriptlang/skript/lang/command/SubCommandEntryData.java new file mode 100644 index 00000000000..43de79e243b --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/command/SubCommandEntryData.java @@ -0,0 +1,167 @@ +package org.skriptlang.skript.lang.command; + +import ch.njol.skript.ScriptLoader; +import ch.njol.skript.Skript; +import ch.njol.skript.config.Node; +import ch.njol.skript.config.SectionNode; +import ch.njol.skript.util.StringMode; +import ch.njol.skript.util.Timespan; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.skriptlang.skript.lang.entry.EntryContainer; +import org.skriptlang.skript.lang.entry.EntryData; +import org.skriptlang.skript.lang.entry.EntryValidator; +import org.skriptlang.skript.lang.entry.KeyValueEntryData; +import org.skriptlang.skript.lang.entry.util.LiteralEntryData; +import org.skriptlang.skript.lang.entry.util.VariableStringEntryData; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Entry data used for sub-commands. + *

+ * This entry data can be repeatedly used within general command structure and + * also repeatedly within itself. + *

+ * For the sub-command entry to be considered valid, it needs to either + * have a trigger section, + * or have other sub command entry. + */ +public class SubCommandEntryData extends EntryData { + + // pattern used to match the subcommand node + private static final Pattern SUBCOMMAND_PATTERN = Pattern.compile("^sub-?command\\s+(.+)$"); + + // sub-command entry data keys + public static final String PERMISSION_KEY = "permission"; + // TODO permissions messages should be removed completely, + // instead of a message the command is not sent to client + // and dispatcher ignores it + public static final String PERMISSION_MSG_KEY = "permission message"; + public static final String EXECUTABLE_KEY = "executable by"; + public static final String COOLDOWN_KEY = "cooldown"; + public static final String COOLDOWN_MSG_KEY = "cooldown message"; + public static final String COOLDOWN_BYPASS_KEY = "cooldown bypass"; + public static final String COOLDOWN_STORAGE_KEY = "cooldown storage"; + public static final String SUGGESTIONS_KEY = "suggestions"; + public static final String TRIGGER_KEY = "trigger"; + public static final String SUBCOMMAND_KEY = "subcommand"; + + private static final EntryValidator VALIDATOR = validator(); + + /** + * Creates entry validator for a sub command entry. + * + * @return entry validator for sub command entry + */ + public static EntryValidator validator() { + return validator(builder -> {}); + } + + /** + * Creates entry validator for sub command entry which is possible to + * further modify with given consumer. + * + * @param builderConsumer consumer used to further modify the entry validator + * @return entry validator for sub command entry + */ + public static EntryValidator validator(Consumer builderConsumer) { + EntryValidator.EntryValidatorBuilder builder = EntryValidator.builder(); + builder + // permissions + .addEntry(PERMISSION_KEY, null, true) + .addEntryData(new VariableStringEntryData(PERMISSION_MSG_KEY, null, true)) + // executable by + .addEntryData(new KeyValueEntryData>(EXECUTABLE_KEY, new ArrayList<>(), true) { + private final Pattern pattern = Pattern.compile("\\s*,\\s*|\\s+(and|or)\\s+"); + @Override + protected @NotNull List getValue(String value) { + return List.of(pattern.split(value)); + } + }) + // cooldown + .addEntryData(new LiteralEntryData<>(COOLDOWN_KEY, null, true, Timespan.class)) + .addEntryData(new VariableStringEntryData(COOLDOWN_MSG_KEY, null, true)) + .addEntry(COOLDOWN_BYPASS_KEY, null, true) + .addEntryData(new VariableStringEntryData(COOLDOWN_STORAGE_KEY, null, + true, StringMode.VARIABLE_NAME)) + // suggestions (tab completions) + .addSection(SUGGESTIONS_KEY, true) + // trigger + .addSection(TRIGGER_KEY, true) + // subcommands + .addEntryData(new SubCommandEntryData()) + .unexpectedEntryMessage(key -> + "Unexpected entry '" + key + "'. Check that it's spelled correctly, and ensure that you have " + + "put all code into a trigger." + ); + builderConsumer.accept(builder); + return builder.build(); + } + + protected @Nullable SectionNode node; + protected @Nullable Parsed parsed; + + protected SubCommandEntryData() { + super(SUBCOMMAND_KEY, null, true, true); + } + + @Override + public @Nullable Parsed getValue(Node node) { + if (this.node != null && this.node == node) return parsed; + if (!canCreateWith(node)) return null; + return parsed; + } + + @Override + public boolean canCreateWith(Node node) { + if (!(node instanceof SectionNode sectionNode)) + return false; + String key = node.getKey(); + if (key == null) + return false; + key = ScriptLoader.replaceOptions(key); + Matcher subCommandMatcher = SUBCOMMAND_PATTERN.matcher(key); + if (!subCommandMatcher.matches()) + return false; + EntryContainer entryContainer = VALIDATOR.validate(sectionNode); + if (entryContainer == null) + return false; + if (!CommandUtils.isValidCommandNode(entryContainer)) + return false; + this.node = sectionNode; + + String rawArguments = subCommandMatcher.group(1).trim(); + if (rawArguments.isBlank()) { + Skript.error("Sub-commands must have arguments"); + return false; + } + + List arguments = CommandArgumentParser.parse(subCommandMatcher.group(1)); + if (arguments.isEmpty()) + return false; + + parsed = new Parsed(entryContainer, arguments); + return true; + } + + /** + * Represents a parsed sub command entry. + * + * @param entryContainer entry container of the sub command entry + * @param arguments arguments of the sub command + */ + public record Parsed(EntryContainer entryContainer, @Unmodifiable List arguments) { + + public Parsed { + arguments = List.copyOf(arguments); + } + + } + +} From 3cc007edd245872c8a8f13a49433b3dcd565926b Mon Sep 17 00:00:00 2001 From: Pesek Date: Sun, 10 Aug 2025 14:38:34 +0200 Subject: [PATCH 07/14] switch to custom command sender API, moved paper platform impl from ch/njo/skript to org/skriptlang/skript, improved syntax patterns for existing command argument types --- src/main/java/ch/njol/skript/Skript.java | 2 +- .../ch/njol/skript/command/CommandUsage.java | 18 +-- .../brigadier/BrigadierCommandEvent.java | 43 ------ .../brigadier/WrappedCommandSourceStack.java | 139 ------------------ .../brigadier/ArgumentSkriptCommandNode.java | 8 +- .../brigadier/LiteralSkriptCommandNode.java | 8 +- .../brigadier/RootSkriptCommandNode.java | 21 +-- .../skript/brigadier/SkriptCommandNode.java | 12 +- .../brigadier/SkriptSuggestionProvider.java | 4 +- .../bukkit/command/BrigadierCommandEvent.java | 58 ++++++++ .../bukkit/command}/BrigadierModule.java | 4 +- .../command}/BrigadierSuggestionsEvent.java | 6 +- .../bukkit/command/BukkitCommandSender.java | 44 ++++++ .../bukkit/command/PaperCommandHandler.java} | 53 ++++--- .../bukkit/command}/PaperCommandUtils.java | 10 +- .../elements}/DefaultArgumentTypes.java | 33 ++++- .../elements}/StructBrigadierCommand.java | 12 +- .../skript/lang/command/CommandArgument.java | 9 +- .../skript/lang/command/CommandHandler.java | 3 +- .../lang/command/CommandSourceType.java | 31 +++- .../skript/lang/command/CommandUtils.java | 38 ++--- .../lang/command/SkriptCommandSender.java | 41 ++++++ .../lang/command/StructGeneralCommand.java | 7 +- 23 files changed, 304 insertions(+), 300 deletions(-) delete mode 100644 src/main/java/ch/njol/skript/command/brigadier/BrigadierCommandEvent.java delete mode 100644 src/main/java/ch/njol/skript/command/brigadier/WrappedCommandSourceStack.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/BrigadierCommandEvent.java rename src/main/java/{ch/njol/skript/command/brigadier => org/skriptlang/skript/bukkit/command}/BrigadierModule.java (75%) rename src/main/java/{ch/njol/skript/command/brigadier => org/skriptlang/skript/bukkit/command}/BrigadierSuggestionsEvent.java (85%) create mode 100644 src/main/java/org/skriptlang/skript/bukkit/command/BukkitCommandSender.java rename src/main/java/{ch/njol/skript/command/brigadier/BrigadierCommandHandler.java => org/skriptlang/skript/bukkit/command/PaperCommandHandler.java} (74%) rename src/main/java/{ch/njol/skript/command/brigadier => org/skriptlang/skript/bukkit/command}/PaperCommandUtils.java (88%) rename src/main/java/{ch/njol/skript/command/brigadier => org/skriptlang/skript/bukkit/command/elements}/DefaultArgumentTypes.java (66%) rename src/main/java/{ch/njol/skript/command/brigadier => org/skriptlang/skript/bukkit/command/elements}/StructBrigadierCommand.java (63%) create mode 100644 src/main/java/org/skriptlang/skript/lang/command/SkriptCommandSender.java diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index e1fc8f6f8c2..ce06d63d783 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -12,7 +12,7 @@ import ch.njol.skript.classes.data.JavaClasses; import ch.njol.skript.classes.data.SkriptClasses; import ch.njol.skript.command.Commands; -import ch.njol.skript.command.brigadier.BrigadierModule; +import org.skriptlang.skript.bukkit.command.BrigadierModule; import ch.njol.skript.doc.Documentation; import ch.njol.skript.events.EvtSkript; import ch.njol.skript.hooks.Hook; diff --git a/src/main/java/ch/njol/skript/command/CommandUsage.java b/src/main/java/ch/njol/skript/command/CommandUsage.java index 8d27f74a87c..112a1b17700 100644 --- a/src/main/java/ch/njol/skript/command/CommandUsage.java +++ b/src/main/java/ch/njol/skript/command/CommandUsage.java @@ -7,20 +7,12 @@ /** * Holds info about the usage of a command. - * TODO: replace with record when java 17 + * + * @param usage a dynamic usage message that can contain expressions + * @param defaultUsage a fallback usage message that can be used in non-event environments + * like when registering the Bukkit command */ -public class CommandUsage { - - /** - * A dynamic usage message that can contain expressions. - */ - private final VariableString usage; - - /** - * A fallback usage message that can be used in non-event environments, - * like when registering the Bukkit command. - */ - private final String defaultUsage; +public record CommandUsage(VariableString usage, String defaultUsage) { /** * @param usage The dynamic usage message, can contain expressions. diff --git a/src/main/java/ch/njol/skript/command/brigadier/BrigadierCommandEvent.java b/src/main/java/ch/njol/skript/command/brigadier/BrigadierCommandEvent.java deleted file mode 100644 index 19ec70ddfd1..00000000000 --- a/src/main/java/ch/njol/skript/command/brigadier/BrigadierCommandEvent.java +++ /dev/null @@ -1,43 +0,0 @@ -package ch.njol.skript.command.brigadier; - -import ch.njol.skript.command.CommandEvent; -import com.mojang.brigadier.context.CommandContext; -import org.bukkit.command.CommandSender; -import org.bukkit.event.HandlerList; -import org.jetbrains.annotations.NotNull; - -/** - * Event for executing brigadier commands. - */ -public class BrigadierCommandEvent extends CommandEvent { - - private final CommandContext context; - - public BrigadierCommandEvent(CommandContext context) { - super(context.getSource(), context.getRootNode().getName(), - context.getNodes().stream() - .map(arg -> arg.getRange().get(context.getInput())) - .toArray(String[]::new)); - this.context = context; - } - - /** - * @return command execution context - */ - public CommandContext getContext() { - return context; - } - - // Bukkit stuff - private final static HandlerList handlers = new HandlerList(); - - @Override - public @NotNull HandlerList getHandlers() { - return handlers; - } - - public static HandlerList getHandlerList() { - return handlers; - } - -} diff --git a/src/main/java/ch/njol/skript/command/brigadier/WrappedCommandSourceStack.java b/src/main/java/ch/njol/skript/command/brigadier/WrappedCommandSourceStack.java deleted file mode 100644 index 46c8b0078a5..00000000000 --- a/src/main/java/ch/njol/skript/command/brigadier/WrappedCommandSourceStack.java +++ /dev/null @@ -1,139 +0,0 @@ -package ch.njol.skript.command.brigadier; - -import io.papermc.paper.command.brigadier.CommandSourceStack; -import net.kyori.adventure.text.Component; -import org.bukkit.Server; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Entity; -import org.bukkit.permissions.Permission; -import org.bukkit.permissions.PermissionAttachment; -import org.bukkit.permissions.PermissionAttachmentInfo; -import org.bukkit.plugin.Plugin; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Set; -import java.util.UUID; - -/** - * CommandSender instance created from a command source stack. - * - * @param sourceStack source stack - */ -// TODO is this needed? probably not and the requirements (Predicate) can have dummy implementation -record WrappedCommandSourceStack(CommandSourceStack sourceStack) implements CommandSender { - - public CommandSender sender() { - Entity executor = sourceStack.getExecutor(); - return executor != null ? executor : sourceStack.getSender(); - } - - @Override - public void sendMessage(@NotNull String message) { - sender().sendMessage(message); - } - - @Override - public void sendMessage(@NotNull String... messages) { - sender().sendMessage(messages); - } - - @Override - @Deprecated - public void sendMessage(@Nullable UUID sender, @NotNull String message) { - sender().sendMessage(sender, message); - } - - @Override - @Deprecated - public void sendMessage(@Nullable UUID sender, @NotNull String... messages) { - sender().sendMessage(sender, messages); - } - - @Override - public @NotNull Server getServer() { - return sender().getServer(); - } - - @Override - public @NotNull String getName() { - return sender().getName(); - } - - @NotNull - @Override - public Spigot spigot() { - return sender().spigot(); - } - - @Override - public @NotNull Component name() { - return sender().name(); - } - - @Override - public boolean isPermissionSet(@NotNull String name) { - return sender().isPermissionSet(name); - } - - @Override - public boolean isPermissionSet(@NotNull Permission perm) { - return sender().isPermissionSet(perm); - } - - @Override - public boolean hasPermission(@NotNull String name) { - return sender().hasPermission(name); - } - - @Override - public boolean hasPermission(@NotNull Permission perm) { - return sender().hasPermission(perm); - } - - @Override - public @NotNull PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value) { - return sender().addAttachment(plugin, name, value); - } - - @Override - public @NotNull PermissionAttachment addAttachment(@NotNull Plugin plugin) { - return sender().addAttachment(plugin); - } - - @Override - public @Nullable PermissionAttachment addAttachment(@NotNull Plugin plugin, @NotNull String name, boolean value, int ticks) { - return sender().addAttachment(plugin, name, value, ticks); - } - - @Override - public @Nullable PermissionAttachment addAttachment(@NotNull Plugin plugin, int ticks) { - return sender().addAttachment(plugin, ticks); - } - - @Override - public void removeAttachment(@NotNull PermissionAttachment attachment) { - sender().removeAttachment(attachment); - } - - @Override - public void recalculatePermissions() { - sender().recalculatePermissions(); - } - - @Override - public @NotNull Set getEffectivePermissions() { - return sender().getEffectivePermissions(); - } - - @Override - public boolean isOp() { - return sender().isOp(); - } - - @Override - public void setOp(boolean value) { - sender().setOp(value); - } - -} diff --git a/src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java b/src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java index e6fa81340a9..9e27c4e37ca 100644 --- a/src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java +++ b/src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java @@ -14,11 +14,11 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder; import com.mojang.brigadier.tree.ArgumentCommandNode; import com.mojang.brigadier.tree.CommandNode; -import org.bukkit.command.CommandSender; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.lang.command.CommandCooldown; import org.skriptlang.skript.lang.command.CommandSourceType; +import org.skriptlang.skript.lang.command.SkriptCommandSender; import java.util.Collection; import java.util.concurrent.CompletableFuture; @@ -30,7 +30,7 @@ * @param command sender type * @see ArgumentSkriptCommandNode.Builder#argument(String, ArgumentType) */ -public non-sealed class ArgumentSkriptCommandNode +public non-sealed class ArgumentSkriptCommandNode extends SkriptCommandNode> { private final ArgumentCommandNode wrapped; @@ -127,7 +127,7 @@ public Collection getExamples() { * @param command sender type * @param argument type */ - public static non-sealed class Builder + public static non-sealed class Builder extends SkriptCommandNode.Builder, ArgumentSkriptCommandNode, Builder> { @@ -140,7 +140,7 @@ public static non-sealed class Builder * @param command sender type * @param argument type */ - public static Builder argument(String name, ArgumentType type) { + public static Builder argument(String name, ArgumentType type) { return new Builder<>(name, type); } diff --git a/src/main/java/org/skriptlang/skript/brigadier/LiteralSkriptCommandNode.java b/src/main/java/org/skriptlang/skript/brigadier/LiteralSkriptCommandNode.java index 4dd7f92bcfb..e1916b89aff 100644 --- a/src/main/java/org/skriptlang/skript/brigadier/LiteralSkriptCommandNode.java +++ b/src/main/java/org/skriptlang/skript/brigadier/LiteralSkriptCommandNode.java @@ -12,10 +12,10 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder; import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.LiteralCommandNode; -import org.bukkit.command.CommandSender; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.lang.command.CommandCooldown; import org.skriptlang.skript.lang.command.CommandSourceType; +import org.skriptlang.skript.lang.command.SkriptCommandSender; import java.util.Collection; import java.util.concurrent.CompletableFuture; @@ -27,7 +27,7 @@ * @param command sender type * @see Builder#literal(String) */ -public non-sealed class LiteralSkriptCommandNode +public non-sealed class LiteralSkriptCommandNode extends SkriptCommandNode> { private final LiteralCommandNode wrapped; @@ -119,7 +119,7 @@ public Collection getExamples() { * * @param command sender type */ - public static non-sealed class Builder + public static non-sealed class Builder extends SkriptCommandNode.Builder, LiteralSkriptCommandNode, Builder> { /** @@ -129,7 +129,7 @@ public static non-sealed class Builder * @return builder * @param command sender type */ - public static Builder literal(String name) { + public static Builder literal(String name) { return new Builder<>(name); } diff --git a/src/main/java/org/skriptlang/skript/brigadier/RootSkriptCommandNode.java b/src/main/java/org/skriptlang/skript/brigadier/RootSkriptCommandNode.java index 87ed5c5e419..2e1f9464531 100644 --- a/src/main/java/org/skriptlang/skript/brigadier/RootSkriptCommandNode.java +++ b/src/main/java/org/skriptlang/skript/brigadier/RootSkriptCommandNode.java @@ -1,14 +1,15 @@ package org.skriptlang.skript.brigadier; +import ch.njol.skript.command.CommandUsage; import ch.njol.skript.lang.VariableString; import com.mojang.brigadier.Command; import com.mojang.brigadier.RedirectModifier; import com.mojang.brigadier.tree.CommandNode; -import org.bukkit.command.CommandSender; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import org.skriptlang.skript.lang.command.CommandCooldown; import org.skriptlang.skript.lang.command.CommandSourceType; +import org.skriptlang.skript.lang.command.SkriptCommandSender; import java.util.Collection; import java.util.LinkedHashSet; @@ -25,14 +26,14 @@ * * @param command source type */ -public class RootSkriptCommandNode extends LiteralSkriptCommandNode { +public class RootSkriptCommandNode extends LiteralSkriptCommandNode { private final String namespace; private final @Nullable String description; - private final @Nullable VariableString usage; // TODO this should be ch.njol.skript.command.CommandUsage + private final @Nullable CommandUsage usage; private final @Unmodifiable Set aliases = new LinkedHashSet<>(); - protected RootSkriptCommandNode(String namespace, @Nullable String description, @Nullable VariableString usage, + protected RootSkriptCommandNode(String namespace, @Nullable String description, @Nullable CommandUsage usage, @Nullable Collection aliases, String literal, @Nullable Command command, @Nullable Predicate requirement, @Nullable CommandNode redirect, @Nullable RedirectModifier modifier, boolean forks, @Nullable String permission, @@ -64,7 +65,7 @@ public String getNamespace() { /** * @return usage */ - public @Nullable VariableString getUsage() { + public @Nullable CommandUsage getUsage() { return usage; } @@ -75,7 +76,7 @@ public String getNamespace() { return aliases; } - public static class Builder extends LiteralSkriptCommandNode.Builder { + public static class Builder extends LiteralSkriptCommandNode.Builder { /** * Creates new builder for root skript command node with given namespace and name. @@ -85,14 +86,14 @@ public static class Builder extends LiteralSkriptComman * @return builder * @param command sender type */ - public static Builder root(String namespace, + public static Builder root(String namespace, String name) { return new Builder<>(namespace, name); } private final String namespace; private @Nullable String description; - private @Nullable VariableString usage; + private @Nullable CommandUsage usage; private @Nullable Set aliases = new LinkedHashSet<>(); protected Builder(String namespace, String literal) { @@ -127,7 +128,7 @@ public Builder description(String description) { * @param usage new usage for command being built * @return this */ - public Builder usage(VariableString usage) { + public Builder usage(CommandUsage usage) { this.usage = usage; return this; } @@ -135,7 +136,7 @@ public Builder usage(VariableString usage) { /** * @return usage */ - public @Nullable VariableString getUsage() { + public @Nullable CommandUsage getUsage() { return usage; } diff --git a/src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java b/src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java index a578954bf4b..9c5c4bd0515 100644 --- a/src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java +++ b/src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java @@ -1,6 +1,6 @@ package org.skriptlang.skript.brigadier; -import ch.njol.skript.command.brigadier.PaperCommandUtils; +import org.skriptlang.skript.bukkit.command.PaperCommandUtils; import ch.njol.skript.lang.VariableString; import com.mojang.brigadier.Command; import com.mojang.brigadier.RedirectModifier; @@ -10,12 +10,12 @@ import com.mojang.brigadier.tree.ArgumentCommandNode; import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.LiteralCommandNode; -import org.bukkit.command.CommandSender; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import org.skriptlang.skript.lang.command.CommandCooldown; import org.skriptlang.skript.lang.command.CommandSourceType; +import org.skriptlang.skript.lang.command.SkriptCommandSender; import java.util.Collection; import java.util.Collections; @@ -28,7 +28,7 @@ * * @param command source */ -public sealed abstract class SkriptCommandNode> extends CommandNode +public sealed abstract class SkriptCommandNode> extends CommandNode permits ArgumentSkriptCommandNode, LiteralSkriptCommandNode { private final @Nullable String permission; @@ -105,7 +105,7 @@ public > ArgumentBuilder flatToBuilder() { } @SuppressWarnings("unchecked") - private static ArgumentBuilder flatToBuilder(CommandNode node) { + private static ArgumentBuilder flatToBuilder(CommandNode node) { ArgumentBuilder builder; if (node instanceof LiteralCommandNode lcn) { builder = LiteralArgumentBuilder.literal(lcn.getLiteral()); @@ -142,7 +142,7 @@ public > ArgumentBuilder flatToBuilder() { * @param skript command node to flat */ @SuppressWarnings("unchecked") - static , + static , F extends SkriptCommandNode> void flat(B builder, F node) { builder.requires(sender -> { if (!node.getRequirement().test(sender)) @@ -196,7 +196,7 @@ public > ArgumentBuilder flatToBuilder() { * @param the SkriptCommandNode that is being built * @param this builder */ - public sealed abstract static class Builder, + public sealed abstract static class Builder, Built extends SkriptCommandNode, T extends Builder> extends ArgumentBuilder permits ArgumentSkriptCommandNode.Builder, LiteralSkriptCommandNode.Builder { diff --git a/src/main/java/org/skriptlang/skript/brigadier/SkriptSuggestionProvider.java b/src/main/java/org/skriptlang/skript/brigadier/SkriptSuggestionProvider.java index 1e6e27470f0..34923090fdc 100644 --- a/src/main/java/org/skriptlang/skript/brigadier/SkriptSuggestionProvider.java +++ b/src/main/java/org/skriptlang/skript/brigadier/SkriptSuggestionProvider.java @@ -12,7 +12,7 @@ import io.papermc.paper.command.brigadier.MessageComponentSerializer; import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer; import net.md_5.bungee.api.chat.BaseComponent; -import org.bukkit.command.CommandSender; +import org.skriptlang.skript.lang.command.SkriptCommandSender; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -27,7 +27,7 @@ * @param command source */ @FunctionalInterface -public interface SkriptSuggestionProvider extends SuggestionProvider { +public interface SkriptSuggestionProvider extends SuggestionProvider { @Override default CompletableFuture getSuggestions(CommandContext commandContext, diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/BrigadierCommandEvent.java b/src/main/java/org/skriptlang/skript/bukkit/command/BrigadierCommandEvent.java new file mode 100644 index 00000000000..2943a7491af --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/BrigadierCommandEvent.java @@ -0,0 +1,58 @@ +package org.skriptlang.skript.bukkit.command; + +import ch.njol.skript.command.CommandEvent; +import com.mojang.brigadier.context.CommandContext; +import org.bukkit.command.CommandSender; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.command.SkriptCommandSender; + +/** + * Event for executing brigadier commands. + */ +public class BrigadierCommandEvent extends CommandEvent { + + private final CommandContext context; + + private static @Nullable CommandSender getBukkit(SkriptCommandSender skriptCommandSender) { + return skriptCommandSender instanceof BukkitCommandSender bcs ? bcs.wrapped() : null; + } + + public BrigadierCommandEvent(CommandContext context) { + super(getBukkit(context.getSource()), context.getRootNode().getName(), + context.getNodes().stream() + .map(arg -> arg.getRange().get(context.getInput())) + .toArray(String[]::new)); + this.context = context; + } + + /** + * @return command execution context + */ + public CommandContext getContext() { + return context; + } + + /** + * @deprecated for compatibility reasons with the old command systems + */ + @Override + @Deprecated(forRemoval = true) + public @Nullable CommandSender getSender() { + return super.getSender(); + } + + // Bukkit stuff + private final static HandlerList handlers = new HandlerList(); + + @Override + public @NotNull HandlerList getHandlers() { + return handlers; + } + + public static HandlerList getHandlerList() { + return handlers; + } + +} diff --git a/src/main/java/ch/njol/skript/command/brigadier/BrigadierModule.java b/src/main/java/org/skriptlang/skript/bukkit/command/BrigadierModule.java similarity index 75% rename from src/main/java/ch/njol/skript/command/brigadier/BrigadierModule.java rename to src/main/java/org/skriptlang/skript/bukkit/command/BrigadierModule.java index f90f889004d..ffd9f9007f0 100644 --- a/src/main/java/ch/njol/skript/command/brigadier/BrigadierModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/BrigadierModule.java @@ -1,4 +1,4 @@ -package ch.njol.skript.command.brigadier; +package org.skriptlang.skript.bukkit.command; import ch.njol.skript.Skript; import org.skriptlang.skript.addon.AddonModule; @@ -14,7 +14,7 @@ public class BrigadierModule implements AddonModule { @Override public void load(SkriptAddon addon) { try { - Skript.getAddonInstance().loadClasses("ch.njol.skript.command.brigadier"); + Skript.getAddonInstance().loadClasses(BrigadierModule.class.getPackageName()); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/main/java/ch/njol/skript/command/brigadier/BrigadierSuggestionsEvent.java b/src/main/java/org/skriptlang/skript/bukkit/command/BrigadierSuggestionsEvent.java similarity index 85% rename from src/main/java/ch/njol/skript/command/brigadier/BrigadierSuggestionsEvent.java rename to src/main/java/org/skriptlang/skript/bukkit/command/BrigadierSuggestionsEvent.java index 8e38bad93d2..67f5a316e8b 100644 --- a/src/main/java/ch/njol/skript/command/brigadier/BrigadierSuggestionsEvent.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/BrigadierSuggestionsEvent.java @@ -1,9 +1,9 @@ -package ch.njol.skript.command.brigadier; +package org.skriptlang.skript.bukkit.command; import com.mojang.brigadier.context.CommandContext; -import org.bukkit.command.CommandSender; import org.bukkit.event.HandlerList; import org.jetbrains.annotations.NotNull; +import org.skriptlang.skript.lang.command.SkriptCommandSender; import java.util.ArrayList; import java.util.List; @@ -15,7 +15,7 @@ public class BrigadierSuggestionsEvent extends BrigadierCommandEvent { private final List suggestions = new ArrayList<>(); - public BrigadierSuggestionsEvent(CommandContext context) { + public BrigadierSuggestionsEvent(CommandContext context) { super(context); } diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/BukkitCommandSender.java b/src/main/java/org/skriptlang/skript/bukkit/command/BukkitCommandSender.java new file mode 100644 index 00000000000..4f5dbd7ac9e --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/command/BukkitCommandSender.java @@ -0,0 +1,44 @@ +package org.skriptlang.skript.bukkit.command; + +import ch.njol.skript.util.chat.BungeeConverter; +import ch.njol.skript.util.chat.MessageComponent; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Entity; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.command.SkriptCommandSender; + +import java.util.List; +import java.util.UUID; + +/** + * Implementation of {@link SkriptCommandSender} that wraps around + * existing Bukkit {@link CommandSender} instance. + * + * @param wrapped wrapped bukkit command sender + */ +public record BukkitCommandSender(CommandSender wrapped) implements SkriptCommandSender { + + @Override + public void sendMessage(String message) { + wrapped.sendMessage(message); + } + + @Override + @SuppressWarnings("deprecation") + public void sendMessage(List components) { + components.stream().map(BungeeConverter::convert).forEach(wrapped::sendMessage); + } + + @Override + public @Nullable UUID getUniqueID() { + if (wrapped instanceof Entity player) + return player.getUniqueId(); + return null; + } + + @Override + public boolean hasPermission(String permission) { + return wrapped.hasPermission(permission); + } + +} diff --git a/src/main/java/ch/njol/skript/command/brigadier/BrigadierCommandHandler.java b/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandHandler.java similarity index 74% rename from src/main/java/ch/njol/skript/command/brigadier/BrigadierCommandHandler.java rename to src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandHandler.java index afc57b0d3ae..caad84657bd 100644 --- a/src/main/java/ch/njol/skript/command/brigadier/BrigadierCommandHandler.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandHandler.java @@ -1,4 +1,4 @@ -package ch.njol.skript.command.brigadier; +package org.skriptlang.skript.bukkit.command; import ch.njol.skript.Skript; import com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent; @@ -38,13 +38,16 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; -public class BrigadierCommandHandler implements CommandHandler, Listener { +/** + * Command Handler implementation for Paper environment. + */ +public class PaperCommandHandler implements CommandHandler, Listener { - private final Map> commands = new ConcurrentHashMap<>(); - private @Nullable CommandDispatcher dispatcher; + private final Map> commands = new ConcurrentHashMap<>(); + private @Nullable CommandDispatcher dispatcher; @Override - public synchronized boolean registerCommand(RootSkriptCommandNode command) { + public synchronized boolean registerCommand(RootSkriptCommandNode command) { if (commands.containsKey(command.getLiteral())) { Skript.error("Command " + command.getLiteral() + " is already registered"); return false; @@ -53,32 +56,34 @@ public synchronized boolean registerCommand(RootSkriptCommandNode // TODO proper registration with aliases and help page Bukkit.getCommandMap().getKnownCommands().put(command.getLiteral(), new WrappedBrigadierCommand(command)); dispatcher = null; // reset dispatcher - Bukkit.getScheduler().runTaskLater(Skript.getInstance(), PaperCommandUtils::syncCommands, 1); + if (Skript.getInstance().isEnabled()) + Bukkit.getScheduler().runTaskLater(Skript.getInstance(), PaperCommandUtils::syncCommands, 1); return true; } @Override - public synchronized boolean unregisterCommand(RootSkriptCommandNode command) { + public synchronized boolean unregisterCommand(RootSkriptCommandNode command) { boolean result = commands.remove(command.getLiteral(), command); if (!result) return false; Bukkit.getCommandMap().getKnownCommands().remove(command.getLiteral()); dispatcher = null; // reset dispatcher - Bukkit.getScheduler().runTaskLater(Skript.getInstance(), PaperCommandUtils::syncCommands, 1); + if (Skript.getInstance().isEnabled()) + Bukkit.getScheduler().runTaskLater(Skript.getInstance(), PaperCommandUtils::syncCommands, 1); return true; } @Override - public @Nullable RootSkriptCommandNode getCommand(String label) { + public @Nullable RootSkriptCommandNode getCommand(String label) { return commands.get(label); } @Override - public @UnmodifiableView Map> getAllCommands() { + public @UnmodifiableView Map> getAllCommands() { return Collections.unmodifiableMap(commands); } @Override - public boolean dispatchCommand(CommandSender source, String input) { + public boolean dispatchCommand(BukkitCommandSender source, String input) { if (dispatcher == null) { dispatcher = new CommandDispatcher<>(); commands.values().forEach(cmd -> dispatcher.register(cmd.flatToBuilder())); @@ -98,10 +103,15 @@ public boolean dispatchCommand(CommandSender source, String input) { @Override public @Unmodifiable Set supportedTypes() { return Set.of( - CommandSourceType.typed(Player.class, "player", "the player", "players", "the players"), - CommandSourceType.typed(ConsoleCommandSender.class, "console", "the console", "server", "the server"), - CommandSourceType.typed(BlockCommandSender.class, "block", "blocks"), - CommandSourceType.typed(Entity.class, "entity", "entities") + CommandSourceType.simple(BukkitCommandSender.class, sender -> sender.wrapped() instanceof Player, + "player", "the player", "players", "the players"), + CommandSourceType.simple(BukkitCommandSender.class, + sender -> sender.wrapped() instanceof ConsoleCommandSender, + "console", "the console", "server", "the server"), + CommandSourceType.simple(BukkitCommandSender.class, sender -> sender.wrapped() instanceof BlockCommandSender, + "block", "blocks", "command block", "command blocks"), + CommandSourceType.simple(BukkitCommandSender.class, sender -> sender.wrapped() instanceof Entity, + "entity", "entities") ); } @@ -137,7 +147,7 @@ public void onAsyncPlayerSendCommands(AsyncPlayerSendCommandsEvent<@NotNull Comm throw Skript.exception(exception, "Failed to access the node children field"); } - for (RootSkriptCommandNode node : commands.values()) { + for (RootSkriptCommandNode node : commands.values()) { CommandNode paperCompatible = convertToClientsidePaper(node.flat()); bukkitNodeRemover.accept(paperCompatible); rootNode.addChild(paperCompatible); @@ -154,9 +164,9 @@ public void onAsyncPlayerSendCommands(AsyncPlayerSendCommandsEvent<@NotNull Comm */ // TODO convert paper argument types to NMS argument types // TODO filter out commands with requirements player does not meet (mirrors vanilla behaviour) - private CommandNode convertToClientsidePaper(CommandNode node) { + private CommandNode convertToClientsidePaper(CommandNode node) { ArgumentBuilder builder; - if (node instanceof LiteralCommandNode lcn) { + if (node instanceof LiteralCommandNode lcn) { builder = LiteralArgumentBuilder.literal(lcn.getLiteral()); } else if (node instanceof ArgumentCommandNode acn) { builder = RequiredArgumentBuilder.argument(acn.getName(), acn.getType()); @@ -165,7 +175,7 @@ private CommandNode convertToClientsidePaper(CommandNode node.getRequirement().test(new WrappedCommandSourceStack(stack))); + builder.requires(stack -> true); if (node.getRedirect() != null) builder.forward(convertToClientsidePaper(node.getRedirect()), ctx -> Collections.emptyList(), node.isFork()); if (node.getCommand() != null) @@ -176,14 +186,15 @@ private CommandNode convertToClientsidePaper(CommandNode node) { + protected WrappedBrigadierCommand(RootSkriptCommandNode node) { super(node.getName(), node.getDescription() != null ? node.getDescription() : "", "" /* TODO usage */, new ArrayList<>(node.getAliases())); } @Override public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String @NotNull [] args) { - return BrigadierCommandHandler.this.dispatchCommand(sender, getName() + " " + String.join(" ", args)); + return PaperCommandHandler.this.dispatchCommand(new BukkitCommandSender(sender), + getName() + " " + String.join(" ", args)); } } diff --git a/src/main/java/ch/njol/skript/command/brigadier/PaperCommandUtils.java b/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandUtils.java similarity index 88% rename from src/main/java/ch/njol/skript/command/brigadier/PaperCommandUtils.java rename to src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandUtils.java index bbb24ef9bb3..c4a3a24b5fc 100644 --- a/src/main/java/ch/njol/skript/command/brigadier/PaperCommandUtils.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandUtils.java @@ -1,4 +1,4 @@ -package ch.njol.skript.command.brigadier; +package org.skriptlang.skript.bukkit.command; import ch.njol.skript.Skript; import ch.njol.skript.lang.VariableString; @@ -7,8 +7,8 @@ import com.mojang.brigadier.context.CommandContext; import org.bukkit.Bukkit; import org.bukkit.Server; -import org.bukkit.command.CommandSender; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.command.SkriptCommandSender; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -31,12 +31,12 @@ private PaperCommandUtils() { * @param command source */ @SuppressWarnings("unchecked") - public static void sendPermissionMessage(CommandContext context, + public static void sendPermissionMessage(CommandContext context, VariableString permissionMessage) { if (permissionMessage == null) permissionMessage = VariableString.newInstance(Language.get("commands.no permission message")); assert permissionMessage != null; - BrigadierCommandEvent event = new BrigadierCommandEvent((CommandContext) context); + BrigadierCommandEvent event = new BrigadierCommandEvent((CommandContext) context); String formatted = permissionMessage.getSingle(event); if (formatted != null) context.getSource().sendMessage(formatted); @@ -51,7 +51,7 @@ public static void sendPermissionMessage(CommandContex * @param context context * @param command source */ - public static void sendInvalidExecutorMessage(CommandContext context) { + public static void sendInvalidExecutorMessage(CommandContext context) { context.getSource().sendMessage(INVALID_EXECUTOR_MESSAGE.toString()); } diff --git a/src/main/java/ch/njol/skript/command/brigadier/DefaultArgumentTypes.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/DefaultArgumentTypes.java similarity index 66% rename from src/main/java/ch/njol/skript/command/brigadier/DefaultArgumentTypes.java rename to src/main/java/org/skriptlang/skript/bukkit/command/elements/DefaultArgumentTypes.java index 730390ec678..f0861b183c3 100644 --- a/src/main/java/ch/njol/skript/command/brigadier/DefaultArgumentTypes.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/DefaultArgumentTypes.java @@ -1,8 +1,9 @@ -package ch.njol.skript.command.brigadier; +package org.skriptlang.skript.bukkit.command.elements; import ch.njol.skript.Skript; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser; +import ch.njol.skript.lang.util.ContextlessEvent; import com.mojang.brigadier.arguments.ArgumentType; import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.arguments.StringArgumentType; @@ -29,7 +30,7 @@ public static final class String extends ArgumentTypeElement.Simple { + min = (int) expressions[0].getSingle(ContextlessEvent.get()); + if (!parseResult.hasTag("equal")) + min++; + } + case 1 -> { + max = (int) expressions[0].getSingle(ContextlessEvent.get()); + if (!parseResult.hasTag("equal")) + max--; + } + case 2 -> { + int first = (int) expressions[0].getSingle(ContextlessEvent.get()); + int second = (int) expressions[1].getSingle(ContextlessEvent.get()); + min = Math.min(first, second); + max = Math.max(first, second); + } + } return IntegerArgumentType.integer(min, max); } diff --git a/src/main/java/ch/njol/skript/command/brigadier/StructBrigadierCommand.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/StructBrigadierCommand.java similarity index 63% rename from src/main/java/ch/njol/skript/command/brigadier/StructBrigadierCommand.java rename to src/main/java/org/skriptlang/skript/bukkit/command/elements/StructBrigadierCommand.java index 0468ecf71a8..89c09bf7703 100644 --- a/src/main/java/ch/njol/skript/command/brigadier/StructBrigadierCommand.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/StructBrigadierCommand.java @@ -1,11 +1,12 @@ -package ch.njol.skript.command.brigadier; +package org.skriptlang.skript.bukkit.command.elements; import ch.njol.skript.Skript; import org.bukkit.Bukkit; -import org.bukkit.command.CommandSender; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.command.PaperCommandHandler; import org.skriptlang.skript.lang.command.CommandHandler; +import org.skriptlang.skript.lang.command.SkriptCommandSender; import org.skriptlang.skript.lang.command.StructGeneralCommand; /** @@ -13,7 +14,7 @@ */ public class StructBrigadierCommand extends StructGeneralCommand { - private static final BrigadierCommandHandler HANDLER = new BrigadierCommandHandler(); + private static final PaperCommandHandler HANDLER = new PaperCommandHandler(); static { StructGeneralCommand.registerCommandStructure(StructBrigadierCommand.class, "brigadier"); @@ -21,8 +22,9 @@ public class StructBrigadierCommand extends StructGeneralCommand { } @Override - public CommandHandler getHandler() { - return HANDLER; + @SuppressWarnings("unchecked") + public CommandHandler getHandler() { + return (CommandHandler) (CommandHandler) HANDLER; } @Override diff --git a/src/main/java/org/skriptlang/skript/lang/command/CommandArgument.java b/src/main/java/org/skriptlang/skript/lang/command/CommandArgument.java index 9d9f9d95cc2..6771c627600 100644 --- a/src/main/java/org/skriptlang/skript/lang/command/CommandArgument.java +++ b/src/main/java/org/skriptlang/skript/lang/command/CommandArgument.java @@ -1,7 +1,6 @@ package org.skriptlang.skript.lang.command; import com.mojang.brigadier.arguments.ArgumentType; -import org.bukkit.command.CommandSender; import org.skriptlang.skript.brigadier.ArgumentSkriptCommandNode; import org.skriptlang.skript.brigadier.LiteralSkriptCommandNode; import org.skriptlang.skript.brigadier.RootSkriptCommandNode; @@ -18,7 +17,7 @@ public sealed interface CommandArgument { * * @return empty builder for this command argument */ - SkriptCommandNode.Builder emptyBuilder(); + SkriptCommandNode.Builder emptyBuilder(); /** * Represents a literal. @@ -27,7 +26,7 @@ public sealed interface CommandArgument { */ record Literal(String literal) implements CommandArgument { @Override - public LiteralSkriptCommandNode.Builder emptyBuilder() { + public LiteralSkriptCommandNode.Builder emptyBuilder() { return LiteralSkriptCommandNode.Builder.literal(literal); } } @@ -46,7 +45,7 @@ public Typed(String name, ArgumentTypeElement typeElement) { } @Override - public ArgumentSkriptCommandNode.Builder emptyBuilder() { + public ArgumentSkriptCommandNode.Builder emptyBuilder() { return ArgumentSkriptCommandNode.Builder.argument(name, type); } @@ -61,7 +60,7 @@ public ArgumentSkriptCommandNode.Builder emptyBuilder() { record Root(String namespace, String label) implements CommandArgument { @Override - public RootSkriptCommandNode.Builder emptyBuilder() { + public RootSkriptCommandNode.Builder emptyBuilder() { return RootSkriptCommandNode.Builder.root(namespace, label); } diff --git a/src/main/java/org/skriptlang/skript/lang/command/CommandHandler.java b/src/main/java/org/skriptlang/skript/lang/command/CommandHandler.java index b517ee7824b..5a2298db77b 100644 --- a/src/main/java/org/skriptlang/skript/lang/command/CommandHandler.java +++ b/src/main/java/org/skriptlang/skript/lang/command/CommandHandler.java @@ -1,6 +1,5 @@ package org.skriptlang.skript.lang.command; -import org.bukkit.command.CommandSender; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import org.jetbrains.annotations.UnmodifiableView; @@ -14,7 +13,7 @@ * * @param command source */ -public interface CommandHandler { +public interface CommandHandler { /** * Registers a new command. diff --git a/src/main/java/org/skriptlang/skript/lang/command/CommandSourceType.java b/src/main/java/org/skriptlang/skript/lang/command/CommandSourceType.java index bf2fc820d7f..0b7370b9645 100644 --- a/src/main/java/org/skriptlang/skript/lang/command/CommandSourceType.java +++ b/src/main/java/org/skriptlang/skript/lang/command/CommandSourceType.java @@ -1,12 +1,12 @@ package org.skriptlang.skript.lang.command; import com.mojang.brigadier.context.CommandContext; -import org.bukkit.command.CommandSender; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import java.util.Set; import java.util.function.BiFunction; +import java.util.function.Function; /** * Represents a type of command source, e.g. console. @@ -15,9 +15,9 @@ * @param names names of the command source, this is used by the {@code executable by} entry * @param checkFunction function to check if the source type can execute command in given context */ -public record CommandSourceType(Class type, +public record CommandSourceType(Class type, @Unmodifiable Set names, - BiFunction<@Nullable CommandContext, CommandSender, Boolean> checkFunction) { + BiFunction<@Nullable CommandContext, SkriptCommandSender, Boolean> checkFunction) { public CommandSourceType { names = Set.copyOf(names); @@ -31,10 +31,29 @@ public record CommandSourceType(Class type, * @param names names of the command source, this is used by the {@code executable by} entry * @return command source type instance */ - public static CommandSourceType typed(Class type, String... names) { + public static CommandSourceType typed(Class type, String... names) { return new CommandSourceType(type, Set.of(names), ((context, sender) -> type.isInstance(sender))); } + /** + * Simple command source type that applies check function on instance of specific command sender implementation. + * + * @param sourceType expected skript command sender type + * @param checkFunction check function + * @param names names of the command source, this is used by the {@code executable by} entry + * @return command source type instance + * @param expected skript command sender type + */ + public static CommandSourceType simple(Class sourceType, + Function checkFunction, String... names) { + return new CommandSourceType(SkriptCommandSender.class, Set.of(names), + (ctx, sender) -> { + if (!sourceType.isInstance(sender)) return false; + return checkFunction.apply(sourceType.cast(sender)); + } + ); + } + /** * Checks whether given sender is instance of this source type. * @@ -43,8 +62,8 @@ public static CommandSourceType typed(Class type, Strin * @param command sender type */ @SuppressWarnings("unchecked") - public boolean check(@Nullable CommandContext context, S sender) { - return checkFunction.apply((CommandContext) context, sender); + public boolean check(@Nullable CommandContext context, S sender) { + return checkFunction.apply((CommandContext) context, sender); } } diff --git a/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java b/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java index 3817561f572..8568bc60b04 100644 --- a/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java +++ b/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java @@ -2,8 +2,10 @@ import ch.njol.skript.ScriptLoader; import ch.njol.skript.Skript; -import ch.njol.skript.command.brigadier.BrigadierCommandEvent; -import ch.njol.skript.command.brigadier.BrigadierSuggestionsEvent; +import ch.njol.skript.command.CommandUsage; +import org.skriptlang.skript.brigadier.SkriptSuggestionProvider; +import org.skriptlang.skript.bukkit.command.BrigadierCommandEvent; +import org.skriptlang.skript.bukkit.command.BrigadierSuggestionsEvent; import ch.njol.skript.config.SectionNode; import ch.njol.skript.lang.*; import ch.njol.skript.lang.parser.ParserInstance; @@ -11,14 +13,12 @@ import ch.njol.skript.util.chat.ChatMessages; import com.google.common.base.Preconditions; import org.bukkit.Bukkit; -import org.bukkit.command.CommandSender; import org.bukkit.event.Event; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.brigadier.ArgumentSkriptCommandNode; import org.skriptlang.skript.brigadier.RootSkriptCommandNode; import org.skriptlang.skript.brigadier.SkriptCommandNode; -import org.skriptlang.skript.brigadier.SkriptSuggestionProvider; import org.skriptlang.skript.lang.entry.EntryContainer; import java.util.Arrays; @@ -60,8 +60,8 @@ public static boolean isValidCommandNode(EntryContainer entryContainer) { * sub command entries (either another sub command entry or the root command) * @return created skript command node or null if the creation failed */ - public static @Nullable SkriptCommandNode createCommandNode(CommandHandler handler, - SubCommandEntryData.Parsed subCommandData, SkriptCommandNode.Builder parent) { + public static @Nullable SkriptCommandNode createCommandNode(CommandHandler handler, + SubCommandEntryData.Parsed subCommandData, SkriptCommandNode.Builder parent) { Preconditions.checkNotNull(parent, "Sub-command entry data always have a parent command node"); if (subCommandData.entryContainer() == null || subCommandData.arguments() == null) return null; @@ -80,15 +80,15 @@ public static boolean isValidCommandNode(EntryContainer entryContainer) { * @param arguments arguments * @return created skript command node or null if the creation failed */ - public static @Nullable SkriptCommandNode createCommandNode(CommandHandler handler, - EntryContainer entryContainer, @Nullable SkriptCommandNode.Builder parent, + public static @Nullable SkriptCommandNode createCommandNode(CommandHandler handler, + EntryContainer entryContainer, @Nullable SkriptCommandNode.Builder parent, List arguments) { if (arguments.isEmpty()) { Skript.error("Command nodes can not be empty; arguments are missing"); return null; } - List> builders = new LinkedList<>(); + List> builders = new LinkedList<>(); for (int i = 0; i < arguments.size(); i++) { var builder = arguments.get(i).emptyBuilder(); // first we copy parent properties @@ -106,14 +106,14 @@ public static boolean isValidCommandNode(EntryContainer entryContainer) { previous.then(current); } - SkriptCommandNode firstNode = builders.get(0).build(); + SkriptCommandNode firstNode = builders.get(0).build(); if (parent != null) parent.then(firstNode); return firstNode; } - private static void copyParentProperties(SkriptCommandNode.Builder builder, - @Nullable SkriptCommandNode.Builder parent) { + private static void copyParentProperties(SkriptCommandNode.Builder builder, + @Nullable SkriptCommandNode.Builder parent) { if (parent == null) return; builder.permission(parent.getPermission()); builder.permissionMessage(parent.getPermissionMessage()); @@ -131,8 +131,8 @@ private static void copyParentProperties(SkriptCommandNode.Builder handler, - EntryContainer entryContainer, SkriptCommandNode.Builder builder, + public static boolean setCommandNodeProperties(CommandHandler handler, + EntryContainer entryContainer, SkriptCommandNode.Builder builder, boolean last) { // root command properties @@ -142,8 +142,10 @@ public static boolean setCommandNodeProperties(CommandHandler han false)); } if (entryContainer.hasEntry(StructGeneralCommand.USAGE_KEY)) { - rootBuilder.usage(entryContainer.getOptional(StructGeneralCommand.USAGE_KEY, VariableString.class, - false)); + VariableString usage = entryContainer.getOptional(StructGeneralCommand.USAGE_KEY, VariableString.class, + false); + // brigadier commands get too complex to create meaningful usage, default to '/literal' + rootBuilder.usage(new CommandUsage(usage, "/" + rootBuilder.getLiteral())); } if (entryContainer.hasEntry(StructGeneralCommand.ALIASES_KEY)) { //noinspection unchecked @@ -217,7 +219,7 @@ public boolean isSingleReturnValue() { trigger.setLineNumber(suggestionsNode.getLine()); parser.restoreBackup(parserBackup); - SkriptSuggestionProvider suggestionProvider = ctx -> { + SkriptSuggestionProvider suggestionProvider = ctx -> { BrigadierSuggestionsEvent event = new BrigadierSuggestionsEvent(ctx); runTriggerOnMainThread(trigger, event); // the chat message parsing can be done async @@ -227,7 +229,7 @@ public boolean isSingleReturnValue() { }; //noinspection unchecked - ((ArgumentSkriptCommandNode.Builder) builder).suggests(suggestionProvider); + ((ArgumentSkriptCommandNode.Builder) builder).suggests(suggestionProvider); } if (entryContainer.hasEntry(SubCommandEntryData.TRIGGER_KEY)) { diff --git a/src/main/java/org/skriptlang/skript/lang/command/SkriptCommandSender.java b/src/main/java/org/skriptlang/skript/lang/command/SkriptCommandSender.java new file mode 100644 index 00000000000..fc206d7d657 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/lang/command/SkriptCommandSender.java @@ -0,0 +1,41 @@ +package org.skriptlang.skript.lang.command; + +import ch.njol.skript.util.chat.MessageComponent; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.UUID; + +/** + * Executor of a Skript command. + */ +public interface SkriptCommandSender { + + /** + * Sends message to the command sender. + * + * @param message message to send + */ + void sendMessage(String message); + + /** + * Sends message components to the command sender. + * + * @param components components to send + */ + void sendMessage(List components); + + /** + * @return the unique ID of the sender, if one exists. + */ + @Nullable UUID getUniqueID(); + + /** + * Checks whether this sender has the given permission. + * + * @param permission the permission to check + * @return whether the sender has the given permission + */ + boolean hasPermission(String permission); + +} diff --git a/src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java b/src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java index 9381f47f87b..4ba638ad2d1 100644 --- a/src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java +++ b/src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java @@ -5,7 +5,6 @@ import ch.njol.skript.lang.Literal; import ch.njol.skript.lang.SkriptParser; import com.google.common.base.Preconditions; -import org.bukkit.command.CommandSender; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.brigadier.RootSkriptCommandNode; import org.skriptlang.skript.lang.entry.EntryContainer; @@ -99,14 +98,14 @@ protected static void registerCommandStructure( } protected EntryContainer entryContainer; - protected RootSkriptCommandNode commandNode; + protected RootSkriptCommandNode commandNode; /** * Command handler used for the registration and creation of the commands. * * @return command handler */ - public abstract CommandHandler getHandler(); + public abstract CommandHandler getHandler(); @Override public boolean init(Literal[] args, int matchedPattern, SkriptParser.ParseResult parseResult, @@ -135,7 +134,7 @@ public boolean init(Literal[] args, int matchedPattern, SkriptParser.ParseRes rootArguments.addAll(arguments); //noinspection unchecked - commandNode = (RootSkriptCommandNode) CommandUtils.createCommandNode(getHandler(), + commandNode = (RootSkriptCommandNode) CommandUtils.createCommandNode(getHandler(), entryContainer, null, rootArguments); return commandNode != null; } From 8c6a820225fe810bd9bca2b833b53eefefea9da5 Mon Sep 17 00:00:00 2001 From: Pesek Date: Mon, 11 Aug 2025 10:06:08 +0200 Subject: [PATCH 08/14] changed the registration process for argument types --- src/main/java/ch/njol/skript/Skript.java | 16 ---------- .../bukkit/command/BrigadierModule.java | 18 ++++++----- .../elements/DefaultArgumentTypes.java | 31 +++++++++++++------ 3 files changed, 31 insertions(+), 34 deletions(-) diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index ce06d63d783..17a7b23d528 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -1738,22 +1738,6 @@ public static boolean dispatchCommand(final CommandSender sender, final String c } } - /** - * Registers a new argument type. - * - * @param argumentClass class of the argument type - * @param patterns patterns - * @param argument type - */ - public static > void registerArgumentType(Class argumentClass, String... patterns) { - checkAcceptRegistrations(); - skript.syntaxRegistry().register(ArgumentTypeElement.REGISTRY_KEY, SyntaxInfo.builder(argumentClass) - .origin(getSyntaxOrigin(argumentClass)) - .addPatterns(patterns) - .build() - ); - } - // ================ LOGGING ================ public static boolean logNormal() { diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/BrigadierModule.java b/src/main/java/org/skriptlang/skript/bukkit/command/BrigadierModule.java index ffd9f9007f0..17563c15e1b 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/BrigadierModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/BrigadierModule.java @@ -1,10 +1,11 @@ package org.skriptlang.skript.bukkit.command; -import ch.njol.skript.Skript; import org.skriptlang.skript.addon.AddonModule; import org.skriptlang.skript.addon.SkriptAddon; - -import java.io.IOException; +import org.skriptlang.skript.bukkit.command.elements.DefaultArgumentTypes; +import org.skriptlang.skript.bukkit.command.elements.StructBrigadierCommand; +import org.skriptlang.skript.registration.DefaultSyntaxInfos; +import org.skriptlang.skript.registration.SyntaxRegistry; /** * Module for Brigadier commands. @@ -13,11 +14,12 @@ public class BrigadierModule implements AddonModule { @Override public void load(SkriptAddon addon) { - try { - Skript.getAddonInstance().loadClasses(BrigadierModule.class.getPackageName()); - } catch (IOException e) { - throw new RuntimeException(e); - } + addon.syntaxRegistry().register(SyntaxRegistry.STRUCTURE, + DefaultSyntaxInfos.Structure.builder(StructBrigadierCommand.class) + .supplier(StructBrigadierCommand::new) + .build()); + DefaultArgumentTypes.String.load(addon); + DefaultArgumentTypes.Integer.load(addon); } } diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/DefaultArgumentTypes.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/DefaultArgumentTypes.java index f0861b183c3..8b406dfa804 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/DefaultArgumentTypes.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/DefaultArgumentTypes.java @@ -1,6 +1,5 @@ package org.skriptlang.skript.bukkit.command.elements; -import ch.njol.skript.Skript; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser; import ch.njol.skript.lang.util.ContextlessEvent; @@ -9,7 +8,10 @@ import com.mojang.brigadier.arguments.StringArgumentType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.addon.SkriptAddon; import org.skriptlang.skript.lang.command.ArgumentTypeElement; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxOrigin; /** * Default Argument Types provided by Skript. @@ -28,9 +30,15 @@ private DefaultArgumentTypes() { */ public static final class String extends ArgumentTypeElement.Simple { - static { - Skript.registerArgumentType(DefaultArgumentTypes.String.class, - "[single] (word|string)", "(quotable|quoted) string", "greedy string"); + public static void load(SkriptAddon addon) { + addon.syntaxRegistry().register(ArgumentTypeElement.REGISTRY_KEY, + SyntaxInfo.builder(DefaultArgumentTypes.Integer.class) + .addPatterns("[single] (word|string)", + "(quotable|quoted) string", + "greedy string") + .supplier(DefaultArgumentTypes.Integer::new) + .origin(SyntaxOrigin.of(addon)) + .build()); } @Override @@ -51,12 +59,15 @@ public static final class String extends ArgumentTypeElement.Simple { - static { - Skript.registerArgumentType(DefaultArgumentTypes.Integer.class, - "integer greater than [equal:or equal to] %integer%", - "integer less than [equal:or equal to] %integer%", - "integer between %integer% and %integer%" - ); + public static void load(SkriptAddon addon) { + addon.syntaxRegistry().register(ArgumentTypeElement.REGISTRY_KEY, + SyntaxInfo.builder(DefaultArgumentTypes.Integer.class) + .addPatterns("integer greater than [equal:or equal to] %integer%", + "integer less than [equal:or equal to] %integer%", + "integer between %integer% and %integer%") + .supplier(DefaultArgumentTypes.Integer::new) + .origin(SyntaxOrigin.of(addon)) + .build()); } @Override From ab43e0deebb18d01ff176b7c6cc37cb5eed7bd98 Mon Sep 17 00:00:00 2001 From: Pesek Date: Mon, 11 Aug 2025 18:15:42 +0200 Subject: [PATCH 09/14] added support for unnamed arguments, fixed the syntax registration, small changes to argument types --- .../bukkit/command/BrigadierModule.java | 7 +- .../elements/DefaultArgumentTypes.java | 24 +++-- .../elements/StructBrigadierCommand.java | 11 ++- .../lang/command/ArgumentTypeElement.java | 17 ++++ .../lang/command/CommandArgumentParser.java | 44 +++++++--- .../lang/command/StructGeneralCommand.java | 87 +++++++++++-------- 6 files changed, 128 insertions(+), 62 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/BrigadierModule.java b/src/main/java/org/skriptlang/skript/bukkit/command/BrigadierModule.java index 17563c15e1b..2e5420406b1 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/BrigadierModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/BrigadierModule.java @@ -4,8 +4,6 @@ import org.skriptlang.skript.addon.SkriptAddon; import org.skriptlang.skript.bukkit.command.elements.DefaultArgumentTypes; import org.skriptlang.skript.bukkit.command.elements.StructBrigadierCommand; -import org.skriptlang.skript.registration.DefaultSyntaxInfos; -import org.skriptlang.skript.registration.SyntaxRegistry; /** * Module for Brigadier commands. @@ -14,10 +12,7 @@ public class BrigadierModule implements AddonModule { @Override public void load(SkriptAddon addon) { - addon.syntaxRegistry().register(SyntaxRegistry.STRUCTURE, - DefaultSyntaxInfos.Structure.builder(StructBrigadierCommand.class) - .supplier(StructBrigadierCommand::new) - .build()); + StructBrigadierCommand.load(addon); DefaultArgumentTypes.String.load(addon); DefaultArgumentTypes.Integer.load(addon); } diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/DefaultArgumentTypes.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/DefaultArgumentTypes.java index 8b406dfa804..69e0ff3f540 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/DefaultArgumentTypes.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/DefaultArgumentTypes.java @@ -30,13 +30,17 @@ private DefaultArgumentTypes() { */ public static final class String extends ArgumentTypeElement.Simple { + private String() { + super("string"); + } + public static void load(SkriptAddon addon) { addon.syntaxRegistry().register(ArgumentTypeElement.REGISTRY_KEY, - SyntaxInfo.builder(DefaultArgumentTypes.Integer.class) + SyntaxInfo.builder(DefaultArgumentTypes.String.class) .addPatterns("[single] (word|string)", "(quotable|quoted) string", "greedy string") - .supplier(DefaultArgumentTypes.Integer::new) + .supplier(DefaultArgumentTypes.String::new) .origin(SyntaxOrigin.of(addon)) .build()); } @@ -59,10 +63,16 @@ public static void load(SkriptAddon addon) { */ public static final class Integer extends ArgumentTypeElement.Simple { + private Integer() { + super("integer"); + } + public static void load(SkriptAddon addon) { addon.syntaxRegistry().register(ArgumentTypeElement.REGISTRY_KEY, SyntaxInfo.builder(DefaultArgumentTypes.Integer.class) - .addPatterns("integer greater than [equal:or equal to] %integer%", + .addPatterns("integer", + "positive integer", + "integer greater than [equal:or equal to] %integer%", "integer less than [equal:or equal to] %integer%", "integer between %integer% and %integer%") .supplier(DefaultArgumentTypes.Integer::new) @@ -77,17 +87,19 @@ public static void load(SkriptAddon addon) { int max = java.lang.Integer.MAX_VALUE; switch (matchedPattern) { - case 0 -> { + // case 0 is regular integer + case 1 -> min = 0; + case 2 -> { min = (int) expressions[0].getSingle(ContextlessEvent.get()); if (!parseResult.hasTag("equal")) min++; } - case 1 -> { + case 3 -> { max = (int) expressions[0].getSingle(ContextlessEvent.get()); if (!parseResult.hasTag("equal")) max--; } - case 2 -> { + case 4 -> { int first = (int) expressions[0].getSingle(ContextlessEvent.get()); int second = (int) expressions[1].getSingle(ContextlessEvent.get()); min = Math.min(first, second); diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/StructBrigadierCommand.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/StructBrigadierCommand.java index 89c09bf7703..ea81d41688e 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/StructBrigadierCommand.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/StructBrigadierCommand.java @@ -4,6 +4,7 @@ import org.bukkit.Bukkit; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.addon.SkriptAddon; import org.skriptlang.skript.bukkit.command.PaperCommandHandler; import org.skriptlang.skript.lang.command.CommandHandler; import org.skriptlang.skript.lang.command.SkriptCommandSender; @@ -16,8 +17,14 @@ public class StructBrigadierCommand extends StructGeneralCommand { private static final PaperCommandHandler HANDLER = new PaperCommandHandler(); - static { - StructGeneralCommand.registerCommandStructure(StructBrigadierCommand.class, "brigadier"); + /** + * Registers the syntax for the brigadier commands for Paper platform and registers + * the necessary event handlers for its command handler. + * + * @param addon addon to register the structure for + */ + public static void load(SkriptAddon addon) { + StructGeneralCommand.registerCommandStructure(addon, StructBrigadierCommand.class, "brigadier"); Bukkit.getPluginManager().registerEvents(HANDLER, Skript.getInstance()); } diff --git a/src/main/java/org/skriptlang/skript/lang/command/ArgumentTypeElement.java b/src/main/java/org/skriptlang/skript/lang/command/ArgumentTypeElement.java index 5babb72545e..fcfec3e529e 100644 --- a/src/main/java/org/skriptlang/skript/lang/command/ArgumentTypeElement.java +++ b/src/main/java/org/skriptlang/skript/lang/command/ArgumentTypeElement.java @@ -42,6 +42,13 @@ public abstract class ArgumentTypeElement implements SyntaxElement { */ public abstract ArgumentType toBrigadier(); + /** + * Returns a short codename that is used as a placeholder for unnamed arguments of this type. + * + * @return code name for this argument type + */ + public abstract String codeName(); + @Override public @NotNull String getSyntaxTypeName() { return "argument type"; @@ -73,8 +80,13 @@ public ArgumentType toBrigadier() { */ public abstract static class Simple extends ArgumentTypeElement { + private final String codeName; private ArgumentType nativeType; + protected Simple(String codeName) { + this.codeName = codeName; + } + protected abstract @Nullable ArgumentType get(Expression[] expressions, int matchedPattern, SkriptParser.ParseResult parseResult); @@ -90,6 +102,11 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean is return nativeType != null; } + @Override + public String codeName() { + return codeName; + } + } } diff --git a/src/main/java/org/skriptlang/skript/lang/command/CommandArgumentParser.java b/src/main/java/org/skriptlang/skript/lang/command/CommandArgumentParser.java index 022902e64f9..90d2a06fabb 100644 --- a/src/main/java/org/skriptlang/skript/lang/command/CommandArgumentParser.java +++ b/src/main/java/org/skriptlang/skript/lang/command/CommandArgumentParser.java @@ -6,10 +6,8 @@ import com.google.common.base.Preconditions; import org.skriptlang.skript.registration.SyntaxInfo; -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -28,7 +26,18 @@ private CommandArgumentParser() { Pattern.compile("<[^>]+>|\\S+"); private static final Pattern TYPED_ARGUMENT_PARSER_PATTERN = - Pattern.compile("\\s*([^:]+?)\\s*:\\s*(.+?)\\s*"); + Pattern.compile("\\s*(?:([^:]+?):)?\\s*(.+?)\\s*"); + + private static final ThreadLocal> ARG_COUNTERS = ThreadLocal.withInitial(HashMap::new); + + /** + * Resets the argument counter for current command. + *

+ * It is used for auto-generating argument names for unnamed arguments. + */ + static void resetArgumentCounter() { + ARG_COUNTERS.set(new HashMap<>()); + } /** * Parses a raw string of arguments into a list of CommandArgument objects. @@ -75,23 +84,32 @@ private static CommandArgument.Typed parseTypedArgument(String token) { // remove the outer brackets to get the content String content = token.substring(1, token.length() - 1); Matcher typedMatcher = TYPED_ARGUMENT_PARSER_PATTERN.matcher(content); - - Preconditions.checkArgument(typedMatcher.matches(), "Invalid format: must be '', but got '" + token + "'"); - + Preconditions.checkArgument(typedMatcher.matches(), "Invalid format: must be '' or " + + "'', but got '" + token + "'"); String name = typedMatcher.group(1); String typeExpression = typedMatcher.group(2); - Preconditions.checkArgument(!name.isBlank(), "Argument name cannot be blank in '" + token + "'"); - Preconditions.checkArgument(!typeExpression.isBlank(), "Argument type cannot be blank in '" + token + "'"); + if (name != null) { + Preconditions.checkArgument(!name.isBlank(), "Argument name cannot be blank in '" + token + + "'"); + } + Preconditions.checkArgument(!typeExpression.isBlank(), "Argument type cannot be blank in '" + + token + "'"); var argumentTypes = (Collection>>) (Collection) Skript.instance().syntaxRegistry().syntaxes(ArgumentTypeElement.REGISTRY_KEY); - ArgumentTypeElement parsedType = SkriptParser.parseStatic(typeExpression, argumentTypes.iterator(), ParseContext.DEFAULT, "Failed to parse argument type '" + typeExpression + "'"); + Preconditions.checkArgument(parsedType != null, "Unknown argument type in '" + + token + "'"); + + // create placeholder name for unnamed arguments + if (name == null) { + name = parsedType.codeName() + + ARG_COUNTERS.get().computeIfAbsent(parsedType.codeName(), n -> new AtomicInteger()).getAndIncrement(); + } - Preconditions.checkArgument(parsedType != null, "Unknown argument type in '" + token + "'"); return new CommandArgument.Typed<>(name, parsedType); } -} +} \ No newline at end of file diff --git a/src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java b/src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java index 4ba638ad2d1..b11bafa49c9 100644 --- a/src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java +++ b/src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java @@ -1,17 +1,19 @@ package org.skriptlang.skript.lang.command; import ch.njol.skript.ScriptLoader; -import ch.njol.skript.Skript; import ch.njol.skript.lang.Literal; import ch.njol.skript.lang.SkriptParser; import com.google.common.base.Preconditions; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.addon.SkriptAddon; import org.skriptlang.skript.brigadier.RootSkriptCommandNode; import org.skriptlang.skript.lang.entry.EntryContainer; import org.skriptlang.skript.lang.entry.EntryValidator; import org.skriptlang.skript.lang.entry.KeyValueEntryData; import org.skriptlang.skript.lang.entry.util.VariableStringEntryData; import org.skriptlang.skript.lang.structure.Structure; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; import java.util.*; import java.util.function.Consumer; @@ -40,46 +42,59 @@ public abstract class StructGeneralCommand extends Structure { * This method provides easy way to register the command structure with already predefined * syntax and entry validator that can be further modified with provided consumer. * + * @param addon addon to register the command structure for * @param structureClass command structure to be registered - * @param builderConsumer consumer for further modification of the entry validator + * @param builder consumer for further modification of the syntax info + * @param validatorBuilder consumer for further modification of the entry validator * @param prefixPatterns string prefixes that define the start of the command structure's syntax. - * Those can not contain expressions or regex. + * Those can not contain expressions or regex. * @param command structure type */ - protected static void registerCommandStructure(Class structureClass, - Consumer builderConsumer, String... prefixPatterns) { - String[] patterns = Arrays.stream(prefixPatterns) + protected static void registerCommandStructure(SkriptAddon addon, + Class structureClass, + @Nullable Consumer, E>> builder, + @Nullable Consumer validatorBuilder, + String... prefixPatterns) { + // patterns + List patterns = Arrays.stream(prefixPatterns) .peek(prefix -> Preconditions.checkArgument(!prefix.contains("%"), "Command prefix can not contain expressions")) .peek(prefix -> Preconditions.checkArgument(!prefix.contains("<") || prefix.contains(">"), "Command prefix can not contain regex")) .map(p -> p.trim().concat(" " + COMMAND_PATTERN)) - .distinct() - .toArray(String[]::new); - Skript.registerStructure( - structureClass, - SubCommandEntryData.validator(builder -> { - builder - .addEntry(NAMESPACE_KEY, DEFAULT_NAMESPACE, true) - .addEntry(DESCRIPTION_KEY, null, true) - .addEntryData(new VariableStringEntryData(USAGE_KEY, null, true)) - .addEntryData(new KeyValueEntryData>(ALIASES_KEY, new ArrayList<>(), true) { - private final Pattern pattern = Pattern.compile("\\s*,\\s*/?"); - @Override - protected List getValue(String value) { - List aliases = new ArrayList<>(Arrays.asList(pattern.split(value))); - if (aliases.get(0).startsWith("/")) { - aliases.set(0, aliases.get(0).substring(1)); - } else if (aliases.get(0).isEmpty()) { - aliases = new ArrayList<>(0); - } - return aliases; + .distinct().toList(); + // validator + EntryValidator validator = SubCommandEntryData.validator(vb -> { + vb + .addEntry(NAMESPACE_KEY, DEFAULT_NAMESPACE, true) + .addEntry(DESCRIPTION_KEY, null, true) + .addEntryData(new VariableStringEntryData(USAGE_KEY, null, true)) + .addEntryData(new KeyValueEntryData>(ALIASES_KEY, new ArrayList<>(), true) { + private final Pattern pattern = Pattern.compile("\\s*,\\s*/?"); + @Override + protected List getValue(String value) { + List aliases = new ArrayList<>(Arrays.asList(pattern.split(value))); + if (aliases.get(0).startsWith("/")) { + aliases.set(0, aliases.get(0).substring(1)); + } else if (aliases.get(0).isEmpty()) { + aliases = new ArrayList<>(0); } - }); - builderConsumer.accept(builder); - }), - patterns - ); + return aliases; + } + }); + // if provided, accept modifications + if (validatorBuilder != null) + validatorBuilder.accept(vb); + }); + // syntax info + var building = SyntaxInfo.Structure.builder(structureClass) + .addPatterns(patterns) + .entryValidator(validator); + // if provided, accept modifications + if (builder != null) + builder.accept(building); + // register + addon.syntaxRegistry().register(SyntaxRegistry.STRUCTURE, building.build()); } /** @@ -87,14 +102,15 @@ protected List getValue(String value) { *

* This method provides easy way to register the command structure with already predefined syntax. * + * @param addon addon to register the structure for * @param structureClass command structure to be registered * @param prefixPatterns string prefixes that define the start of the command structure's syntax. - * Those can not contain expressions or regex. + * Those can not contain expressions or regex. * @param command structure type */ - protected static void registerCommandStructure(Class structureClass, - String... prefixPatterns) { - registerCommandStructure(structureClass, builder -> {}, prefixPatterns); + protected static void registerCommandStructure(SkriptAddon addon, + Class structureClass, String... prefixPatterns) { + registerCommandStructure(addon, structureClass, null, null, prefixPatterns); } protected EntryContainer entryContainer; @@ -133,6 +149,7 @@ public boolean init(Literal[] args, int matchedPattern, SkriptParser.ParseRes rootArguments.add(new CommandArgument.Root(namespace, label)); rootArguments.addAll(arguments); + CommandArgumentParser.resetArgumentCounter(); //noinspection unchecked commandNode = (RootSkriptCommandNode) CommandUtils.createCommandNode(getHandler(), entryContainer, null, rootArguments); From 47e64669860e7efb339cec385ee31bc0f7a5bc21 Mon Sep 17 00:00:00 2001 From: Pesek Date: Tue, 12 Aug 2025 10:29:37 +0200 Subject: [PATCH 10/14] removed generic param for native brigadier node from skript command nodes --- .../brigadier/ArgumentSkriptCommandNode.java | 6 ++---- .../brigadier/LiteralSkriptCommandNode.java | 5 ++--- .../skript/brigadier/SkriptCommandNode.java | 19 ++++++++----------- .../skript/lang/command/CommandArgument.java | 2 +- .../skript/lang/command/CommandUtils.java | 18 +++++++++--------- 5 files changed, 22 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java b/src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java index 9e27c4e37ca..6e8abb50e19 100644 --- a/src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java +++ b/src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java @@ -30,8 +30,7 @@ * @param command sender type * @see ArgumentSkriptCommandNode.Builder#argument(String, ArgumentType) */ -public non-sealed class ArgumentSkriptCommandNode - extends SkriptCommandNode> { +public non-sealed class ArgumentSkriptCommandNode extends SkriptCommandNode { private final ArgumentCommandNode wrapped; @@ -128,8 +127,7 @@ public Collection getExamples() { * @param argument type */ public static non-sealed class Builder - extends SkriptCommandNode.Builder, - ArgumentSkriptCommandNode, Builder> { + extends SkriptCommandNode.Builder> { /** * Creates new builder for argument skript command node with given name and type. diff --git a/src/main/java/org/skriptlang/skript/brigadier/LiteralSkriptCommandNode.java b/src/main/java/org/skriptlang/skript/brigadier/LiteralSkriptCommandNode.java index e1916b89aff..f4750acb00d 100644 --- a/src/main/java/org/skriptlang/skript/brigadier/LiteralSkriptCommandNode.java +++ b/src/main/java/org/skriptlang/skript/brigadier/LiteralSkriptCommandNode.java @@ -27,8 +27,7 @@ * @param command sender type * @see Builder#literal(String) */ -public non-sealed class LiteralSkriptCommandNode - extends SkriptCommandNode> { +public non-sealed class LiteralSkriptCommandNode extends SkriptCommandNode { private final LiteralCommandNode wrapped; @@ -120,7 +119,7 @@ public Collection getExamples() { * @param command sender type */ public static non-sealed class Builder - extends SkriptCommandNode.Builder, LiteralSkriptCommandNode, Builder> { + extends SkriptCommandNode.Builder> { /** * Creates new builder for literal skript command node with given name. diff --git a/src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java b/src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java index 9c5c4bd0515..ee4a363e24a 100644 --- a/src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java +++ b/src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java @@ -28,7 +28,7 @@ * * @param command source */ -public sealed abstract class SkriptCommandNode> extends CommandNode +public sealed abstract class SkriptCommandNode extends CommandNode permits ArgumentSkriptCommandNode, LiteralSkriptCommandNode { private final @Nullable String permission; @@ -87,7 +87,7 @@ protected SkriptCommandNode(@Nullable Command command, @Nullable Predicate * * @return native brigadier node */ - public abstract N flat(); + public abstract CommandNode flat(); /** * Creates native brigadier node builder from this node. @@ -122,7 +122,7 @@ public > ArgumentBuilder flatToBuilder() { builder.executes(node.getCommand()); for (CommandNode child : node.getChildren()) { CommandNode nativeNode = child; - if (child instanceof SkriptCommandNode scn) + if (child instanceof SkriptCommandNode scn) nativeNode = (CommandNode) scn.flat(); builder.then(flatToBuilder(nativeNode)); } @@ -143,7 +143,7 @@ public > ArgumentBuilder flatToBuilder() { */ @SuppressWarnings("unchecked") static , - F extends SkriptCommandNode> void flat(B builder, F node) { + F extends SkriptCommandNode> void flat(B builder, F node) { builder.requires(sender -> { if (!node.getRequirement().test(sender)) return false; @@ -156,7 +156,7 @@ public > ArgumentBuilder flatToBuilder() { }); CommandNode redirect = node.getRedirect(); - if (redirect instanceof SkriptCommandNode scn) + if (redirect instanceof SkriptCommandNode scn) redirect = (CommandNode) scn.flat(); builder.forward(redirect, node.getRedirectModifier(), node.isFork()); @@ -180,7 +180,7 @@ public > ArgumentBuilder flatToBuilder() { } for (CommandNode child : node.getChildren()) { - if (child instanceof SkriptCommandNode scn) { + if (child instanceof SkriptCommandNode scn) { builder.then((CommandNode) scn.flat()); continue; } @@ -192,12 +192,9 @@ public > ArgumentBuilder flatToBuilder() { * Represents builder of a SkriptCommandNode. * * @param command sender type - * @param native brigadier command node of the SkriptCommandNode - * @param the SkriptCommandNode that is being built * @param this builder */ - public sealed abstract static class Builder, - Built extends SkriptCommandNode, T extends Builder> + public sealed abstract static class Builder> extends ArgumentBuilder permits ArgumentSkriptCommandNode.Builder, LiteralSkriptCommandNode.Builder { private @Nullable String permission = null; @@ -280,7 +277,7 @@ public T cooldown(@Nullable CommandCooldown cooldown) { * @return new SkriptCommandNode from this builder */ @Contract(pure = true) - public abstract Built build(); + public abstract SkriptCommandNode build(); } diff --git a/src/main/java/org/skriptlang/skript/lang/command/CommandArgument.java b/src/main/java/org/skriptlang/skript/lang/command/CommandArgument.java index 6771c627600..bdf520583f6 100644 --- a/src/main/java/org/skriptlang/skript/lang/command/CommandArgument.java +++ b/src/main/java/org/skriptlang/skript/lang/command/CommandArgument.java @@ -17,7 +17,7 @@ public sealed interface CommandArgument { * * @return empty builder for this command argument */ - SkriptCommandNode.Builder emptyBuilder(); + SkriptCommandNode.Builder emptyBuilder(); /** * Represents a literal. diff --git a/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java b/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java index 8568bc60b04..d61e4e6d311 100644 --- a/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java +++ b/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java @@ -60,8 +60,8 @@ public static boolean isValidCommandNode(EntryContainer entryContainer) { * sub command entries (either another sub command entry or the root command) * @return created skript command node or null if the creation failed */ - public static @Nullable SkriptCommandNode createCommandNode(CommandHandler handler, - SubCommandEntryData.Parsed subCommandData, SkriptCommandNode.Builder parent) { + public static @Nullable SkriptCommandNode createCommandNode(CommandHandler handler, + SubCommandEntryData.Parsed subCommandData, SkriptCommandNode.Builder parent) { Preconditions.checkNotNull(parent, "Sub-command entry data always have a parent command node"); if (subCommandData.entryContainer() == null || subCommandData.arguments() == null) return null; @@ -80,15 +80,15 @@ public static boolean isValidCommandNode(EntryContainer entryContainer) { * @param arguments arguments * @return created skript command node or null if the creation failed */ - public static @Nullable SkriptCommandNode createCommandNode(CommandHandler handler, - EntryContainer entryContainer, @Nullable SkriptCommandNode.Builder parent, + public static @Nullable SkriptCommandNode createCommandNode(CommandHandler handler, + EntryContainer entryContainer, @Nullable SkriptCommandNode.Builder parent, List arguments) { if (arguments.isEmpty()) { Skript.error("Command nodes can not be empty; arguments are missing"); return null; } - List> builders = new LinkedList<>(); + List> builders = new LinkedList<>(); for (int i = 0; i < arguments.size(); i++) { var builder = arguments.get(i).emptyBuilder(); // first we copy parent properties @@ -106,14 +106,14 @@ public static boolean isValidCommandNode(EntryContainer entryContainer) { previous.then(current); } - SkriptCommandNode firstNode = builders.get(0).build(); + SkriptCommandNode firstNode = builders.get(0).build(); if (parent != null) parent.then(firstNode); return firstNode; } - private static void copyParentProperties(SkriptCommandNode.Builder builder, - @Nullable SkriptCommandNode.Builder parent) { + private static void copyParentProperties(SkriptCommandNode.Builder builder, + @Nullable SkriptCommandNode.Builder parent) { if (parent == null) return; builder.permission(parent.getPermission()); builder.permissionMessage(parent.getPermissionMessage()); @@ -132,7 +132,7 @@ private static void copyParentProperties(SkriptCommandNode.Builder handler, - EntryContainer entryContainer, SkriptCommandNode.Builder builder, + EntryContainer entryContainer, SkriptCommandNode.Builder builder, boolean last) { // root command properties From a623e9ac27a0182bd6a24d3770da44fa4763b68a Mon Sep 17 00:00:00 2001 From: Pesek Date: Tue, 12 Aug 2025 11:06:42 +0200 Subject: [PATCH 11/14] added support for literal groups as arguments --- .../skript/lang/command/CommandArgument.java | 46 +++++++++---- .../lang/command/CommandArgumentParser.java | 18 ++++- .../skript/lang/command/CommandUtils.java | 69 ++++++++++--------- .../lang/command/StructGeneralCommand.java | 11 ++- 4 files changed, 95 insertions(+), 49 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/lang/command/CommandArgument.java b/src/main/java/org/skriptlang/skript/lang/command/CommandArgument.java index bdf520583f6..f176bcaaa10 100644 --- a/src/main/java/org/skriptlang/skript/lang/command/CommandArgument.java +++ b/src/main/java/org/skriptlang/skript/lang/command/CommandArgument.java @@ -1,34 +1,56 @@ package org.skriptlang.skript.lang.command; import com.mojang.brigadier.arguments.ArgumentType; +import org.jetbrains.annotations.Unmodifiable; import org.skriptlang.skript.brigadier.ArgumentSkriptCommandNode; import org.skriptlang.skript.brigadier.LiteralSkriptCommandNode; import org.skriptlang.skript.brigadier.RootSkriptCommandNode; import org.skriptlang.skript.brigadier.SkriptCommandNode; +import java.util.Collections; +import java.util.List; + /** * Represents a parsed command argument (from Skript code). + *

+ * In case of literal command arguments, single argument can represent multiple command + * nodes, e.g. {@code (req|request)} is a single command argument but must be represented + * by two literal command nodes, 'req' and 'request'. */ // TODO optional, possibly plural public sealed interface CommandArgument { /** - * Creates an empty builder from given command argument. + * Creates an empty builders from given command argument. + *

+ * In case of literal command arguments, single argument can represent multiple command + * nodes, e.g. {@code (req|request)} is a single command argument but must be represented + * by two literal command nodes, 'req' and 'request'. * - * @return empty builder for this command argument + * @return empty builders for this command argument */ - SkriptCommandNode.Builder emptyBuilder(); + List> emptyBuilders(); /** - * Represents a literal. + * Represents a literal or a group of literals. * - * @param literal literal + * @param literals literals */ - record Literal(String literal) implements CommandArgument { + record Literal(@Unmodifiable List literals) implements CommandArgument { + + public Literal(String... literals) { + this(List.of(literals)); + } + + public Literal { + literals = Collections.unmodifiableList(literals); + } + @Override - public LiteralSkriptCommandNode.Builder emptyBuilder() { - return LiteralSkriptCommandNode.Builder.literal(literal); + public List> emptyBuilders() { + return literals.stream().map(LiteralSkriptCommandNode.Builder::literal).toList(); } + } /** @@ -45,8 +67,8 @@ public Typed(String name, ArgumentTypeElement typeElement) { } @Override - public ArgumentSkriptCommandNode.Builder emptyBuilder() { - return ArgumentSkriptCommandNode.Builder.argument(name, type); + public List> emptyBuilders() { + return Collections.singletonList(ArgumentSkriptCommandNode.Builder.argument(name, type)); } } @@ -60,8 +82,8 @@ public ArgumentSkriptCommandNode.Builder emptyBuilder() record Root(String namespace, String label) implements CommandArgument { @Override - public RootSkriptCommandNode.Builder emptyBuilder() { - return RootSkriptCommandNode.Builder.root(namespace, label); + public List> emptyBuilders() { + return Collections.singletonList(RootSkriptCommandNode.Builder.root(namespace, label)); } } diff --git a/src/main/java/org/skriptlang/skript/lang/command/CommandArgumentParser.java b/src/main/java/org/skriptlang/skript/lang/command/CommandArgumentParser.java index 90d2a06fabb..dc3054a75c0 100644 --- a/src/main/java/org/skriptlang/skript/lang/command/CommandArgumentParser.java +++ b/src/main/java/org/skriptlang/skript/lang/command/CommandArgumentParser.java @@ -23,11 +23,14 @@ private CommandArgumentParser() { } private static final Pattern ARGUMENT_TOKENIZER_PATTERN = - Pattern.compile("<[^>]+>|\\S+"); + Pattern.compile("<[^>]+>|\\([^)]+\\)|\\S+"); private static final Pattern TYPED_ARGUMENT_PARSER_PATTERN = Pattern.compile("\\s*(?:([^:]+?):)?\\s*(.+?)\\s*"); + private static final Pattern LITERAL_GROUP_PATTERN = + Pattern.compile("\\(([^)]+)\\)"); + private static final ThreadLocal> ARG_COUNTERS = ThreadLocal.withInitial(HashMap::new); /** @@ -68,12 +71,21 @@ public static List parse(String arguments) { /** * Parses a single argument token into a {@link CommandArgument} object. * - * @param token a single token (e.g. "" or "help"). + * @param token a single token (e.g. "", "help", or "(first|second)"). * @return The parsed CommandArgument */ private static CommandArgument parseToken(String token) { if (token.startsWith("<") && token.endsWith(">")) { return parseTypedArgument(token); + } else if (token.startsWith("(") && token.endsWith(")")) { + Matcher matcher = LITERAL_GROUP_PATTERN.matcher(token); + Preconditions.checkArgument(matcher.matches(), "Invalid literal group format: must be " + + "'(literal1|literal2)', but got '" + token + "'"); + String content = matcher.group(1); + List literals = Arrays.asList(content.split("\\|")); + Preconditions.checkArgument(!literals.isEmpty() && literals.stream().noneMatch(String::isBlank), + "Literal group cannot contain blank or empty literals in '" + token + "'"); + return new CommandArgument.Literal(literals); } else { return new CommandArgument.Literal(token); } @@ -112,4 +124,4 @@ private static CommandArgument.Typed parseTypedArgument(String token) { return new CommandArgument.Typed<>(name, parsedType); } -} \ No newline at end of file +} diff --git a/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java b/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java index d61e4e6d311..18753587506 100644 --- a/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java +++ b/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java @@ -22,6 +22,7 @@ import org.skriptlang.skript.lang.entry.EntryContainer; import java.util.Arrays; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -49,67 +50,72 @@ public static boolean isValidCommandNode(EntryContainer entryContainer) { } /** - * Creates a command node from a sub command entry data. + * Creates a command nodes from a sub command entry data. *

- * If parent is provided, the created node is added as its child. - * Parent properties are also copied to the created node. + * If parent is provided, the created nodes are added as its children. + * Parent properties are also copied to the created nodes. * * @param handler command handler * @param subCommandData sub command entry data * @param parent parent command node (inherits command properties), this is always present for * sub command entries (either another sub command entry or the root command) - * @return created skript command node or null if the creation failed + * @return created skript command nodes or empty list if the creation failed */ - public static @Nullable SkriptCommandNode createCommandNode(CommandHandler handler, - SubCommandEntryData.Parsed subCommandData, SkriptCommandNode.Builder parent) { + public static List> createCommandNode( + CommandHandler handler, SubCommandEntryData.Parsed subCommandData, + SkriptCommandNode.Builder parent) { Preconditions.checkNotNull(parent, "Sub-command entry data always have a parent command node"); if (subCommandData.entryContainer() == null || subCommandData.arguments() == null) - return null; + return Collections.emptyList(); return createCommandNode(handler, subCommandData.entryContainer(), parent, subCommandData.arguments()); } /** - * Creates a command node from an entry container and parsed command arguments. + * Creates a command nodes from an entry container and parsed command arguments. *

- * If parent is provided, the created node is added as its child. - * Parent properties are also copied to the created node. + * If parent is provided, the created nodes are added as its children. + * Parent properties are also copied to the created nodes. * * @param handler command handler * @param entryContainer entry container * @param parent parent command node (inherits command properties) * @param arguments arguments - * @return created skript command node or null if the creation failed + * @return created skript command nodes or empty list if the creation failed */ - public static @Nullable SkriptCommandNode createCommandNode(CommandHandler handler, - EntryContainer entryContainer, @Nullable SkriptCommandNode.Builder parent, - List arguments) { + public static List> createCommandNode( + CommandHandler handler, EntryContainer entryContainer, + @Nullable SkriptCommandNode.Builder parent, List arguments) { if (arguments.isEmpty()) { Skript.error("Command nodes can not be empty; arguments are missing"); - return null; + return Collections.emptyList(); } - List> builders = new LinkedList<>(); + // list of builder groups (lists), each representing nodes of one parsed command argument + List>> builderGroups = new LinkedList<>(); + for (int i = 0; i < arguments.size(); i++) { - var builder = arguments.get(i).emptyBuilder(); - // first we copy parent properties - copyParentProperties(builder, parent); - // now we override them if they are present in the entry container for this node - if (!setCommandNodeProperties(handler, entryContainer, builder, i == arguments.size() - 1)) - return null; - builders.add(builder); + var builders = arguments.get(i).emptyBuilders(); + for (SkriptCommandNode.Builder builder : builders) { + // first we copy parent properties + copyParentProperties(builder, parent); + // now we override them if they are present in the entry container for this node + if (!setCommandNodeProperties(handler, entryContainer, builder, i == arguments.size() - 1)) + return Collections.emptyList(); + } + builderGroups.add(builders); } // now we prepared all builders, it is time to connect them together, from last to first one - for (int i = builders.size() - 1; i > 0; i--) { - var current = builders.get(i); - var previous = builders.get(i - 1); - previous.then(current); + for (int i = builderGroups.size() - 1; i > 0; i--) { + for (SkriptCommandNode.Builder previous : builderGroups.get(i - 1)) { + builderGroups.get(i).forEach(previous::then); + } } - SkriptCommandNode firstNode = builders.get(0).build(); + var firstNodes = builderGroups.get(0).stream().map(SkriptCommandNode.Builder::build).toList(); if (parent != null) - parent.then(firstNode); - return firstNode; + firstNodes.forEach(parent::then); + return firstNodes; } private static void copyParentProperties(SkriptCommandNode.Builder builder, @@ -250,7 +256,8 @@ public boolean isSingleReturnValue() { List subCommands = entryContainer.getAll(SubCommandEntryData.SUBCOMMAND_KEY, SubCommandEntryData.Parsed.class, false); for (SubCommandEntryData.Parsed subCommand : subCommands) { - createCommandNode(handler, subCommand, builder); + if (createCommandNode(handler, subCommand, builder).isEmpty()) + return false; } } diff --git a/src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java b/src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java index b11bafa49c9..cee948f93f9 100644 --- a/src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java +++ b/src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java @@ -7,6 +7,7 @@ import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.addon.SkriptAddon; import org.skriptlang.skript.brigadier.RootSkriptCommandNode; +import org.skriptlang.skript.brigadier.SkriptCommandNode; import org.skriptlang.skript.lang.entry.EntryContainer; import org.skriptlang.skript.lang.entry.EntryValidator; import org.skriptlang.skript.lang.entry.KeyValueEntryData; @@ -150,10 +151,14 @@ public boolean init(Literal[] args, int matchedPattern, SkriptParser.ParseRes rootArguments.addAll(arguments); CommandArgumentParser.resetArgumentCounter(); - //noinspection unchecked - commandNode = (RootSkriptCommandNode) CommandUtils.createCommandNode(getHandler(), + // the created nodes should be a list of length 1 (the root skript command node) or empty + // if the creation failed + List> createdNodes = CommandUtils.createCommandNode(getHandler(), entryContainer, null, rootArguments); - return commandNode != null; + if (createdNodes.isEmpty()) + return false; + commandNode = (RootSkriptCommandNode) createdNodes.get(0); + return true; } @Override From b453668bd7d851502bf4a9ae2a540b6b5a605e1a Mon Sep 17 00:00:00 2001 From: Pesek Date: Tue, 12 Aug 2025 13:30:10 +0200 Subject: [PATCH 12/14] fixed triggers being loaded multiple times for command arguments composed of multiple command nodes --- .../skript/lang/command/CommandUtils.java | 72 +++++++++++++------ .../lang/command/StructGeneralCommand.java | 1 + 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java b/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java index 18753587506..f40baed526d 100644 --- a/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java +++ b/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java @@ -3,6 +3,8 @@ import ch.njol.skript.ScriptLoader; import ch.njol.skript.Skript; import ch.njol.skript.command.CommandUsage; +import com.google.common.collect.HashBasedTable; +import com.google.common.collect.Table; import org.skriptlang.skript.brigadier.SkriptSuggestionProvider; import org.skriptlang.skript.bukkit.command.BrigadierCommandEvent; import org.skriptlang.skript.bukkit.command.BrigadierSuggestionsEvent; @@ -21,19 +23,37 @@ import org.skriptlang.skript.brigadier.SkriptCommandNode; import org.skriptlang.skript.lang.entry.EntryContainer; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; public final class CommandUtils { + /** + * Table mapping entry container and entry keys (suggestions and trigger) to loaded triggers. + *

+ * This is to ensure each trigger is loaded only once for command arguments composed of multiple + * command nodes. + * + * @see CommandArgument + */ + private static final ThreadLocal> LOADED_TRIGGERS + = ThreadLocal.withInitial(HashBasedTable::create); + private CommandUtils() { throw new UnsupportedOperationException(); } + /** + * Clears all loaded triggers. + *

+ * This should be called after command nodes are created to + * prevent storing further unused triggers. + */ + static void clearLoadedTriggers() { + LOADED_TRIGGERS.get().clear(); + } + /** * Checks whether the entry container can represent a command entry. * @@ -216,14 +236,18 @@ public boolean isSingleReturnValue() { } }; - ParserInstance parser = ParserInstance.get(); - ParserInstance.Backup parserBackup = parser.backup(); - parser.reset(); - parser.setCurrentEvent("command suggestions", BrigadierSuggestionsEvent.class); - Trigger trigger = suggestionsReturnHandler.loadReturnableTrigger(suggestionsNode, - "command suggestions", new SimpleEvent()); - trigger.setLineNumber(suggestionsNode.getLine()); - parser.restoreBackup(parserBackup); + Trigger trigger = LOADED_TRIGGERS.get().row(entryContainer).computeIfAbsent( + SubCommandEntryData.SUGGESTIONS_KEY, key -> { + ParserInstance parser = ParserInstance.get(); + ParserInstance.Backup parserBackup = parser.backup(); + parser.reset(); + parser.setCurrentEvent("command suggestions", BrigadierSuggestionsEvent.class); + Trigger loaded = suggestionsReturnHandler.loadReturnableTrigger(suggestionsNode, + "command suggestions", new SimpleEvent()); + loaded.setLineNumber(suggestionsNode.getLine()); + parser.restoreBackup(parserBackup); + return loaded; + }); SkriptSuggestionProvider suggestionProvider = ctx -> { BrigadierSuggestionsEvent event = new BrigadierSuggestionsEvent(ctx); @@ -239,16 +263,20 @@ public boolean isSingleReturnValue() { } if (entryContainer.hasEntry(SubCommandEntryData.TRIGGER_KEY)) { - SectionNode triggerNode = entryContainer.get(SubCommandEntryData.TRIGGER_KEY, SectionNode.class, - false); - ParserInstance parser = ParserInstance.get(); - ParserInstance.Backup parserBackup = parser.backup(); - parser.reset(); - parser.setCurrentEvent("command trigger", BrigadierCommandEvent.class); - Trigger trigger = new Trigger(ParserInstance.get().getCurrentScript(), - "command trigger", new SimpleEvent(), ScriptLoader.loadItems(triggerNode)); - trigger.setLineNumber(triggerNode.getLine()); - parser.restoreBackup(parserBackup); + Trigger trigger = LOADED_TRIGGERS.get().row(entryContainer).computeIfAbsent( + SubCommandEntryData.TRIGGER_KEY, key -> { + SectionNode triggerNode = entryContainer.get(SubCommandEntryData.TRIGGER_KEY, SectionNode.class, + false); + ParserInstance parser = ParserInstance.get(); + ParserInstance.Backup parserBackup = parser.backup(); + parser.reset(); + parser.setCurrentEvent("command trigger", BrigadierCommandEvent.class); + Trigger loaded = new Trigger(ParserInstance.get().getCurrentScript(), + "command trigger", new SimpleEvent(), ScriptLoader.loadItems(triggerNode)); + loaded.setLineNumber(triggerNode.getLine()); + parser.restoreBackup(parserBackup); + return loaded; + }); builder.executes(ctx -> runTriggerOnMainThread(trigger, new BrigadierCommandEvent(ctx)) ? 1 : 0); } diff --git a/src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java b/src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java index cee948f93f9..ad8a2975a61 100644 --- a/src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java +++ b/src/main/java/org/skriptlang/skript/lang/command/StructGeneralCommand.java @@ -155,6 +155,7 @@ public boolean init(Literal[] args, int matchedPattern, SkriptParser.ParseRes // if the creation failed List> createdNodes = CommandUtils.createCommandNode(getHandler(), entryContainer, null, rootArguments); + CommandUtils.clearLoadedTriggers(); if (createdNodes.isEmpty()) return false; commandNode = (RootSkriptCommandNode) createdNodes.get(0); From 279d612761a627f7bc3fc2d2564419a0abce645c Mon Sep 17 00:00:00 2001 From: Pesek Date: Tue, 12 Aug 2025 16:39:51 +0200 Subject: [PATCH 13/14] implemented the initial support for argument suggestions --- .../brigadier/ArgumentSkriptCommandNode.java | 2 +- .../skript/brigadier/SkriptCommandNode.java | 6 +- .../brigadier/SkriptSuggestionProvider.java | 14 +-- .../bukkit/command/PaperCommandHandler.java | 97 ++++++++++++++----- .../bukkit/command/PaperCommandUtils.java | 46 +++++++++ .../skript/lang/command/CommandUtils.java | 3 +- 6 files changed, 133 insertions(+), 35 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java b/src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java index 6e8abb50e19..057dd9ca798 100644 --- a/src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java +++ b/src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java @@ -68,7 +68,7 @@ public ArgumentCommandNode flat() { @Override @SuppressWarnings("unchecked") public RequiredArgumentBuilder flatToBuilder() { - return (RequiredArgumentBuilder) super.flatToBuilder(); + return ((RequiredArgumentBuilder) super.flatToBuilder()).suggests(getCustomSuggestions()); } @Override diff --git a/src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java b/src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java index ee4a363e24a..22e16aa16ce 100644 --- a/src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java +++ b/src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java @@ -109,8 +109,10 @@ public > ArgumentBuilder flatToBuilder() { ArgumentBuilder builder; if (node instanceof LiteralCommandNode lcn) { builder = LiteralArgumentBuilder.literal(lcn.getLiteral()); - } else if (node instanceof ArgumentCommandNode acn) { - builder = RequiredArgumentBuilder.argument(acn.getName(), acn.getType()); + } else if (node instanceof ArgumentCommandNode) { + ArgumentCommandNode acn = (ArgumentCommandNode) node; + builder = RequiredArgumentBuilder.argument(acn.getName(), acn.getType()) + .suggests(acn.getCustomSuggestions()); } else { throw new IllegalStateException("Unsupported command node type; only native brigadier nodes are supported"); } diff --git a/src/main/java/org/skriptlang/skript/brigadier/SkriptSuggestionProvider.java b/src/main/java/org/skriptlang/skript/brigadier/SkriptSuggestionProvider.java index 34923090fdc..9ad690b8a59 100644 --- a/src/main/java/org/skriptlang/skript/brigadier/SkriptSuggestionProvider.java +++ b/src/main/java/org/skriptlang/skript/brigadier/SkriptSuggestionProvider.java @@ -9,9 +9,10 @@ import com.mojang.brigadier.suggestion.SuggestionProvider; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.suggestion.SuggestionsBuilder; -import io.papermc.paper.command.brigadier.MessageComponentSerializer; +import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer; import net.md_5.bungee.api.chat.BaseComponent; +import org.skriptlang.skript.bukkit.command.PaperCommandUtils; import org.skriptlang.skript.lang.command.SkriptCommandSender; import java.util.List; @@ -35,18 +36,19 @@ default CompletableFuture getSuggestions(CommandContext commandC return getSuggestions(commandContext) .thenApply(components -> { for (MessageComponent component : components) { - Message tooltip; + Message tooltip = null; if (component.hoverEvent != null && component.hoverEvent.action == MessageComponent.HoverEvent.Action.show_text) { //noinspection deprecation BaseComponent[] baseComponents = BungeeConverter .convert(ChatMessages.parseToArray(component.hoverEvent.value)); - tooltip = MessageComponentSerializer.message() - .serialize(BungeeComponentSerializer.get().deserialize(baseComponents)); - } else { - tooltip = null; + Component adventure = BungeeComponentSerializer.get().deserialize(baseComponents); + tooltip = PaperCommandUtils.brigadierMessage(adventure); } + // skip any blank components + if (component.text.isBlank()) + continue; suggestionsBuilder.suggest(component.text, tooltip); } return suggestionsBuilder.build(); diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandHandler.java b/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandHandler.java index caad84657bd..22de43b9fda 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandHandler.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandHandler.java @@ -2,12 +2,17 @@ import ch.njol.skript.Skript; import com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent; -import com.google.common.base.Preconditions; +import com.destroystokyo.paper.event.brigadier.AsyncPlayerSendSuggestionsEvent; +import com.destroystokyo.paper.event.server.AsyncTabCompleteEvent; import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.ParseResults; +import com.mojang.brigadier.arguments.ArgumentType; import com.mojang.brigadier.builder.ArgumentBuilder; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.context.StringRange; import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.tree.ArgumentCommandNode; import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.LiteralCommandNode; @@ -21,6 +26,7 @@ import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -30,7 +36,8 @@ import org.skriptlang.skript.lang.command.CommandHandler; import org.skriptlang.skript.lang.command.CommandSourceType; -import java.lang.reflect.Field; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; import java.util.ArrayList; import java.util.Collections; import java.util.Map; @@ -41,13 +48,23 @@ /** * Command Handler implementation for Paper environment. */ +// TODO thread safety public class PaperCommandHandler implements CommandHandler, Listener { private final Map> commands = new ConcurrentHashMap<>(); private @Nullable CommandDispatcher dispatcher; + /** + * Ensures the dispatcher instance is available. + */ + private void ensureDispatcher() { + if (dispatcher != null) return; + dispatcher = new CommandDispatcher<>(); + commands.values().forEach(cmd -> dispatcher.register(cmd.flatToBuilder())); + } + @Override - public synchronized boolean registerCommand(RootSkriptCommandNode command) { + public boolean registerCommand(RootSkriptCommandNode command) { if (commands.containsKey(command.getLiteral())) { Skript.error("Command " + command.getLiteral() + " is already registered"); return false; @@ -62,7 +79,7 @@ public synchronized boolean registerCommand(RootSkriptCommandNode command) { + public boolean unregisterCommand(RootSkriptCommandNode command) { boolean result = commands.remove(command.getLiteral(), command); if (!result) return false; Bukkit.getCommandMap().getKnownCommands().remove(command.getLiteral()); @@ -84,11 +101,8 @@ public synchronized boolean unregisterCommand(RootSkriptCommandNode(); - commands.values().forEach(cmd -> dispatcher.register(cmd.flatToBuilder())); - } - + ensureDispatcher(); + assert dispatcher != null; // TODO // if the command does not exist return false // send error message to source similar to vanilla @@ -115,36 +129,33 @@ public boolean dispatchCommand(BukkitCommandSender source, String input) { ); } - private static final @Nullable Field NODE_CHILDREN_FIELD; + private static final @Nullable MethodHandle NODE_CHILDREN_GETTER; static { - Field nodeChildrenField = null; + MethodHandle nodeChildrenGetter = null; try { - nodeChildrenField = CommandNode.class.getDeclaredField("children"); - nodeChildrenField.setAccessible(true); + nodeChildrenGetter = MethodHandles.privateLookupIn(CommandNode.class, MethodHandles.lookup()) + .unreflectGetter(CommandNode.class.getDeclaredField("children")); } catch (Exception exception) { if (Skript.debug()) throw Skript.exception(exception, "Failed to access the command node children field"); } - NODE_CHILDREN_FIELD = nodeChildrenField; + NODE_CHILDREN_GETTER = nodeChildrenGetter; } @EventHandler @SuppressWarnings("UnstableApiUsage") - public void onAsyncPlayerSendCommands(AsyncPlayerSendCommandsEvent<@NotNull CommandSourceStack> event) { + private void onAsyncPlayerSendCommands(AsyncPlayerSendCommandsEvent<@NotNull CommandSourceStack> event) + throws Throwable { if (!event.isAsynchronous() && event.hasFiredAsync()) return; RootCommandNode rootNode = event.getCommandNode(); Consumer> bukkitNodeRemover = node -> {}; - try { - Preconditions.checkNotNull(NODE_CHILDREN_FIELD); + if (NODE_CHILDREN_GETTER != null) { //noinspection unchecked Map> children = (Map>) - NODE_CHILDREN_FIELD.get(rootNode); + NODE_CHILDREN_GETTER.invokeExact((CommandNode) rootNode); bukkitNodeRemover = node -> children.remove(node.getName()); - } catch (Exception exception) { - if (Skript.debug()) - throw Skript.exception(exception, "Failed to access the node children field"); } for (RootSkriptCommandNode node : commands.values()) { @@ -154,7 +165,35 @@ public void onAsyncPlayerSendCommands(AsyncPlayerSendCommandsEvent<@NotNull Comm } } - // TODO suggestions + @EventHandler + private void onAsyncPlayerSendSuggestions(AsyncPlayerSendSuggestionsEvent event) throws Throwable { + String label = getLabel(event.getBuffer()); + if (getCommand(label) == null) + return; + ensureDispatcher(); + assert dispatcher != null; + ParseResults parseResults = + dispatcher.parse(event.getBuffer().substring(1), new BukkitCommandSender(event.getPlayer())); + Suggestions suggestions = dispatcher.getCompletionSuggestions(parseResults).get(); + // add +1 for '/' + StringRange range = new StringRange(suggestions.getRange().getStart() + 1, suggestions.getRange().getEnd() + 1); + event.setSuggestions(new Suggestions(range, suggestions.getList())); + } + + @EventHandler(priority = EventPriority.MONITOR) + private void onAsyncTabComplete(AsyncTabCompleteEvent event) { + if (getCommand(getLabel(event.getBuffer())) == null) + return; + event.setCompletions(Collections.singletonList("placeholder")); + event.setHandled(true); + } + + private static String getLabel(String buffer) { + String label = buffer.split(" ")[0]; + if (label.startsWith("/")) + return label.substring(1); + return label; + } /** * Converts given command node to a node that can be safely sent to the client. @@ -169,7 +208,13 @@ private CommandNode convertToClientsidePaper(CommandNode lcn) { builder = LiteralArgumentBuilder.literal(lcn.getLiteral()); } else if (node instanceof ArgumentCommandNode acn) { - builder = RequiredArgumentBuilder.argument(acn.getName(), acn.getType()); + //noinspection unchecked + builder = RequiredArgumentBuilder.argument(acn.getName(), + (ArgumentType) acn.getType()) + // We provide dummy suggestions for all argument command nodes. + // Such nodes are then marked as 'Has suggestions type' on the client and + // always asked for tab completions, handled by the listener in this class. + .suggests((ctx, b) -> Suggestions.empty()); } else { throw new IllegalArgumentException("Unsupported node implementation; only native Brigadier nodes " + "are supported"); @@ -177,7 +222,8 @@ private CommandNode convertToClientsidePaper(CommandNode true); if (node.getRedirect() != null) - builder.forward(convertToClientsidePaper(node.getRedirect()), ctx -> Collections.emptyList(), node.isFork()); + builder.forward(convertToClientsidePaper(node.getRedirect()), + ctx -> Collections.emptyList(), node.isFork()); if (node.getCommand() != null) builder.executes(ctx -> com.mojang.brigadier.Command.SINGLE_SUCCESS); node.getChildren().forEach(child -> builder.then(convertToClientsidePaper(child))); @@ -189,12 +235,13 @@ private class WrappedBrigadierCommand extends Command { protected WrappedBrigadierCommand(RootSkriptCommandNode node) { super(node.getName(), node.getDescription() != null ? node.getDescription() : "", "" /* TODO usage */, new ArrayList<>(node.getAliases())); + // TODO permission, permission message } @Override public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String @NotNull [] args) { return PaperCommandHandler.this.dispatchCommand(new BukkitCommandSender(sender), - getName() + " " + String.join(" ", args)); + (getName() + " " + String.join(" ", args)).trim()); } } diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandUtils.java b/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandUtils.java index c4a3a24b5fc..2c03e228ac9 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandUtils.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandUtils.java @@ -5,6 +5,9 @@ import ch.njol.skript.localization.Language; import ch.njol.skript.localization.Message; import com.mojang.brigadier.context.CommandContext; +import io.papermc.paper.command.brigadier.MessageComponentSerializer; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.bukkit.Bukkit; import org.bukkit.Server; import org.jetbrains.annotations.Nullable; @@ -87,4 +90,47 @@ public static void syncCommands() { } } + private static final @Nullable MessageComponentSerializer MESSAGE_COMPONENT_SERIALIZER; + private static final @Nullable MethodHandle TO_MESSAGE_HANDLE; + + static { + MessageComponentSerializer messageComponentSerializer = null; + try { + messageComponentSerializer = MessageComponentSerializer.message(); + } catch (Exception exception) { + // message serializer service is unavailable + try { + messageComponentSerializer = (MessageComponentSerializer) + Class.forName("io.papermc.paper.command.brigadier.MessageComponentSerializerImpl") + .getDeclaredConstructor().newInstance(); + } catch (Exception ignored) { /* paper implementation is missing */ } + } + MESSAGE_COMPONENT_SERIALIZER = messageComponentSerializer; + + MethodHandle toMessageHandle = null; + try { + Method method = Class.forName("io.papermc.paper.adventure.PaperAdventure") + .getDeclaredMethod("asVanilla", Component.class); + toMessageHandle = MethodHandles.lookup().unreflect(method); + } catch (Exception ignored) { /* paper does not have method to convert the component */ } + TO_MESSAGE_HANDLE = toMessageHandle; + } + + /** + * Converts adventure component to brigadier message. + * + * @param adventure adventure component + * @return brigadier message + */ + public static com.mojang.brigadier.Message brigadierMessage(Component adventure) { + if (MESSAGE_COMPONENT_SERIALIZER != null) + return MESSAGE_COMPONENT_SERIALIZER.serialize(adventure); + if (TO_MESSAGE_HANDLE != null) { + try { + return (com.mojang.brigadier.Message) TO_MESSAGE_HANDLE.invokeExact(adventure); + } catch (Throwable ignored) { /* everything failed, rip */ } + } + return () -> PlainTextComponentSerializer.plainText().serialize(adventure); + } + } diff --git a/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java b/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java index f40baed526d..2a0d706d923 100644 --- a/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java +++ b/src/main/java/org/skriptlang/skript/lang/command/CommandUtils.java @@ -208,7 +208,8 @@ public static boolean setCommandNodeProperties(CommandHandler)) { From 58f3cefdc9cbdc8bb1f8a2d068c7e57fda931aa9 Mon Sep 17 00:00:00 2001 From: Pesek Date: Sun, 17 Aug 2025 13:41:04 +0200 Subject: [PATCH 14/14] finished the implementation of suggestions, small improvements --- .../brigadier/ArgumentSkriptCommandNode.java | 6 -- .../brigadier/LiteralSkriptCommandNode.java | 6 -- .../skript/brigadier/SkriptCommandNode.java | 14 ++- .../brigadier/SkriptSuggestionProvider.java | 10 +- .../bukkit/command/PaperCommandHandler.java | 98 ++++++++++++++----- .../bukkit/command/PaperCommandUtils.java | 46 --------- .../elements/DefaultArgumentTypes.java | 6 +- 7 files changed, 92 insertions(+), 94 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java b/src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java index 057dd9ca798..cfb81f593ea 100644 --- a/src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java +++ b/src/main/java/org/skriptlang/skript/brigadier/ArgumentSkriptCommandNode.java @@ -65,12 +65,6 @@ public ArgumentCommandNode flat() { return builder.build(); } - @Override - @SuppressWarnings("unchecked") - public RequiredArgumentBuilder flatToBuilder() { - return ((RequiredArgumentBuilder) super.flatToBuilder()).suggests(getCustomSuggestions()); - } - @Override protected boolean isValidInput(String s) { return wrapped.isValidInput(s); diff --git a/src/main/java/org/skriptlang/skript/brigadier/LiteralSkriptCommandNode.java b/src/main/java/org/skriptlang/skript/brigadier/LiteralSkriptCommandNode.java index f4750acb00d..aaa2b38b4cf 100644 --- a/src/main/java/org/skriptlang/skript/brigadier/LiteralSkriptCommandNode.java +++ b/src/main/java/org/skriptlang/skript/brigadier/LiteralSkriptCommandNode.java @@ -58,12 +58,6 @@ public LiteralCommandNode flat() { return builder.build(); } - @Override - @SuppressWarnings("unchecked") - public LiteralArgumentBuilder flatToBuilder() { - return (LiteralArgumentBuilder) super.flatToBuilder(); - } - @Override protected boolean isValidInput(String s) { return wrapped.isValidInput(s); diff --git a/src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java b/src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java index 22e16aa16ce..06a03206138 100644 --- a/src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java +++ b/src/main/java/org/skriptlang/skript/brigadier/SkriptCommandNode.java @@ -100,7 +100,7 @@ protected SkriptCommandNode(@Nullable Command command, @Nullable Predicate * @return native brigadier builder */ @SuppressWarnings("unchecked") - public > ArgumentBuilder flatToBuilder() { + public final > ArgumentBuilder flatToBuilder() { return (ArgumentBuilder) flatToBuilder(flat()); } @@ -140,12 +140,9 @@ public > ArgumentBuilder flatToBuilder() { * @param builder builder to copy the data to * @param node SkriptCommandNode to copy (as native brigadier node) * @param command sender type - * @param command node builder - * @param skript command node to flat */ @SuppressWarnings("unchecked") - static , - F extends SkriptCommandNode> void flat(B builder, F node) { + static void flat(ArgumentBuilder builder, SkriptCommandNode node) { builder.requires(sender -> { if (!node.getRequirement().test(sender)) return false; @@ -181,6 +178,13 @@ F extends SkriptCommandNode> void flat(B builder, F node) { }); } + if (builder instanceof RequiredArgumentBuilder && node instanceof ArgumentSkriptCommandNode) { + RequiredArgumentBuilder rab = (RequiredArgumentBuilder) builder; + ArgumentSkriptCommandNode argumentNode = (ArgumentSkriptCommandNode) node; + if (argumentNode.getCustomSuggestions() != null) + rab.suggests(argumentNode.getCustomSuggestions()); + } + for (CommandNode child : node.getChildren()) { if (child instanceof SkriptCommandNode scn) { builder.then((CommandNode) scn.flat()); diff --git a/src/main/java/org/skriptlang/skript/brigadier/SkriptSuggestionProvider.java b/src/main/java/org/skriptlang/skript/brigadier/SkriptSuggestionProvider.java index 9ad690b8a59..6029f1803ba 100644 --- a/src/main/java/org/skriptlang/skript/brigadier/SkriptSuggestionProvider.java +++ b/src/main/java/org/skriptlang/skript/brigadier/SkriptSuggestionProvider.java @@ -11,8 +11,8 @@ import com.mojang.brigadier.suggestion.SuggestionsBuilder; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.serializer.bungeecord.BungeeComponentSerializer; +import net.kyori.adventure.text.serializer.json.JSONComponentSerializer; import net.md_5.bungee.api.chat.BaseComponent; -import org.skriptlang.skript.bukkit.command.PaperCommandUtils; import org.skriptlang.skript.lang.command.SkriptCommandSender; import java.util.List; @@ -22,8 +22,7 @@ * Implementation of {@link SuggestionProvider} that accepts returning Skript MessageComponents * instead of Brigadier Messages. *

- * Tooltips of those MessageComponents are mapped to tooltips of the suggestions if - * they are provided as text. + * Tooltips of those MessageComponents are serialized as JSON and used as tooltips of Brigadier Messages. * * @param command source */ @@ -44,7 +43,7 @@ default CompletableFuture getSuggestions(CommandContext commandC BaseComponent[] baseComponents = BungeeConverter .convert(ChatMessages.parseToArray(component.hoverEvent.value)); Component adventure = BungeeComponentSerializer.get().deserialize(baseComponents); - tooltip = PaperCommandUtils.brigadierMessage(adventure); + tooltip = () -> JSONComponentSerializer.json().serialize(adventure); } // skip any blank components if (component.text.isBlank()) @@ -64,6 +63,7 @@ default CompletableFuture getSuggestions(CommandContext commandC * @param commandContext command context * @return suggestions */ - CompletableFuture> getSuggestions(CommandContext commandContext) throws CommandSyntaxException; + CompletableFuture> getSuggestions(CommandContext commandContext) + throws CommandSyntaxException; } diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandHandler.java b/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandHandler.java index 22de43b9fda..c6e1002a63c 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandHandler.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandHandler.java @@ -2,7 +2,6 @@ import ch.njol.skript.Skript; import com.destroystokyo.paper.event.brigadier.AsyncPlayerSendCommandsEvent; -import com.destroystokyo.paper.event.brigadier.AsyncPlayerSendSuggestionsEvent; import com.destroystokyo.paper.event.server.AsyncTabCompleteEvent; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.ParseResults; @@ -12,12 +11,15 @@ import com.mojang.brigadier.builder.RequiredArgumentBuilder; import com.mojang.brigadier.context.StringRange; import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestion; import com.mojang.brigadier.suggestion.Suggestions; import com.mojang.brigadier.tree.ArgumentCommandNode; import com.mojang.brigadier.tree.CommandNode; import com.mojang.brigadier.tree.LiteralCommandNode; import com.mojang.brigadier.tree.RootCommandNode; import io.papermc.paper.command.brigadier.CommandSourceStack; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.json.JSONComponentSerializer; import org.bukkit.Bukkit; import org.bukkit.command.BlockCommandSender; import org.bukkit.command.Command; @@ -38,10 +40,7 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; @@ -52,15 +51,27 @@ public class PaperCommandHandler implements CommandHandler, Listener { private final Map> commands = new ConcurrentHashMap<>(); + + // It is impossible to properly modify command dispatchers. + // When a new command is (un)registered, the dispatcher tied to this handler + // is set to null and recreated again lazily once it is needed. (when command is executed) private @Nullable CommandDispatcher dispatcher; /** * Ensures the dispatcher instance is available. + *

+ * If no dispatcher instance is available, a new one is created. */ private void ensureDispatcher() { if (dispatcher != null) return; dispatcher = new CommandDispatcher<>(); - commands.values().forEach(cmd -> dispatcher.register(cmd.flatToBuilder())); + commands.values().forEach(cmd -> { + ArgumentBuilder node = cmd.flatToBuilder(); + if (!(node instanceof LiteralArgumentBuilder)) // should not happen + return; + //noinspection unchecked + dispatcher.register((LiteralArgumentBuilder) node); + }); } @Override @@ -73,7 +84,8 @@ public boolean registerCommand(RootSkriptCommandNode comman // TODO proper registration with aliases and help page Bukkit.getCommandMap().getKnownCommands().put(command.getLiteral(), new WrappedBrigadierCommand(command)); dispatcher = null; // reset dispatcher - if (Skript.getInstance().isEnabled()) + if (Skript.getInstance().isEnabled()) // prevents scheduling task on server shutdown + // command synchronization has to be delayed at least one tick to prevent concurrent modification exception Bukkit.getScheduler().runTaskLater(Skript.getInstance(), PaperCommandUtils::syncCommands, 1); return true; } @@ -81,10 +93,12 @@ public boolean registerCommand(RootSkriptCommandNode comman @Override public boolean unregisterCommand(RootSkriptCommandNode command) { boolean result = commands.remove(command.getLiteral(), command); - if (!result) return false; + if (!result) + return false; Bukkit.getCommandMap().getKnownCommands().remove(command.getLiteral()); dispatcher = null; // reset dispatcher - if (Skript.getInstance().isEnabled()) + if (Skript.getInstance().isEnabled()) // prevents scheduling task on server shutdown + // command synchronization has to be delayed at least one tick to prevent concurrent modification exception Bukkit.getScheduler().runTaskLater(Skript.getInstance(), PaperCommandUtils::syncCommands, 1); return true; } @@ -122,13 +136,17 @@ public boolean dispatchCommand(BukkitCommandSender source, String input) { CommandSourceType.simple(BukkitCommandSender.class, sender -> sender.wrapped() instanceof ConsoleCommandSender, "console", "the console", "server", "the server"), - CommandSourceType.simple(BukkitCommandSender.class, sender -> sender.wrapped() instanceof BlockCommandSender, + CommandSourceType.simple(BukkitCommandSender.class, + sender -> sender.wrapped() instanceof BlockCommandSender, "block", "blocks", "command block", "command blocks"), CommandSourceType.simple(BukkitCommandSender.class, sender -> sender.wrapped() instanceof Entity, "entity", "entities") ); } + // We access the node children here to remove the greedy string node added by Bukkit. + // This results only in a visual change on the client - the Bukkit greedy string argument node + // for all Bukkit commands is not sent to the client. private static final @Nullable MethodHandle NODE_CHILDREN_GETTER; static { @@ -147,10 +165,13 @@ public boolean dispatchCommand(BukkitCommandSender source, String input) { @SuppressWarnings("UnstableApiUsage") private void onAsyncPlayerSendCommands(AsyncPlayerSendCommandsEvent<@NotNull CommandSourceStack> event) throws Throwable { + // This event will potentially (and most likely) fire twice. Once for async, and once again for sync. if (!event.isAsynchronous() && event.hasFiredAsync()) return; RootCommandNode rootNode = event.getCommandNode(); Consumer> bukkitNodeRemover = node -> {}; + + // if the handle is available, we remove the node added by Bukkit command system. if (NODE_CHILDREN_GETTER != null) { //noinspection unchecked Map> children = (Map>) @@ -158,6 +179,7 @@ private void onAsyncPlayerSendCommands(AsyncPlayerSendCommandsEvent<@NotNull Com bukkitNodeRemover = node -> children.remove(node.getName()); } + // and we add our nodes with the correct command structure for (RootSkriptCommandNode node : commands.values()) { CommandNode paperCompatible = convertToClientsidePaper(node.flat()); bukkitNodeRemover.accept(paperCompatible); @@ -165,26 +187,56 @@ private void onAsyncPlayerSendCommands(AsyncPlayerSendCommandsEvent<@NotNull Com } } - @EventHandler - private void onAsyncPlayerSendSuggestions(AsyncPlayerSendSuggestionsEvent event) throws Throwable { - String label = getLabel(event.getBuffer()); + @EventHandler(priority = EventPriority.MONITOR) + private void onAsyncTabComplete(AsyncTabCompleteEvent event) throws Throwable { + String buffer = event.getBuffer(); + String label = getLabel(buffer); if (getCommand(label) == null) return; + ensureDispatcher(); assert dispatcher != null; + ParseResults parseResults = - dispatcher.parse(event.getBuffer().substring(1), new BukkitCommandSender(event.getPlayer())); + dispatcher.parse(buffer.substring(1), new BukkitCommandSender(event.getSender())); Suggestions suggestions = dispatcher.getCompletionSuggestions(parseResults).get(); - // add +1 for '/' - StringRange range = new StringRange(suggestions.getRange().getStart() + 1, suggestions.getRange().getEnd() + 1); - event.setSuggestions(new Suggestions(range, suggestions.getList())); - } + StringRange range = suggestions.getRange(); - @EventHandler(priority = EventPriority.MONITOR) - private void onAsyncTabComplete(AsyncTabCompleteEvent event) { - if (getCommand(getLabel(event.getBuffer())) == null) - return; - event.setCompletions(Collections.singletonList("placeholder")); + List completions = new LinkedList<>(); + for (Suggestion suggestion : suggestions.getList()) { + @Nullable Component tooltip = null; + if (suggestion.getTooltip() != null) { + try { + // we expect JSON component format from SkriptSuggestionProvider + tooltip = JSONComponentSerializer.json().deserialize(suggestion.getTooltip().getString()); + } catch (Exception exception) { + // unexpected format, we still provide the tooltip as raw string + tooltip = Component.text(suggestion.getTooltip().getString()); + } + } + String text = suggestion.getText(); + + // On console side, the command is still a Bukkit command - has only a single greedy string node. + // We need to adjust the completions to that; we also suggest the previous arguments from the buffer. + if (event.getSender() instanceof ConsoleCommandSender) { + // There is already a text present for the argument we suggest for + if (range.getLength() != 0) { + String[] args = buffer.split(" "); + // We filter out suggestions that do not start with the last argument in the buffer. + // We can not properly use brigadier string range for console so the suggestions get appended + // to the buffer. + if (!text.startsWith(args[args.length - 1])) + continue; + } + // the suggestion also contains all previous arguments in the buffer + text = buffer.substring(1 /* slash */ + label.length() + 1 /* space */) + // we cut off the start of the suggestion already in the buffer if present + + text.substring(range.getLength()); + } + + completions.add(AsyncTabCompleteEvent.Completion.completion(text, tooltip)); + } + event.completions(completions); event.setHandled(true); } diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandUtils.java b/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandUtils.java index 2c03e228ac9..c4a3a24b5fc 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandUtils.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/PaperCommandUtils.java @@ -5,9 +5,6 @@ import ch.njol.skript.localization.Language; import ch.njol.skript.localization.Message; import com.mojang.brigadier.context.CommandContext; -import io.papermc.paper.command.brigadier.MessageComponentSerializer; -import net.kyori.adventure.text.Component; -import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import org.bukkit.Bukkit; import org.bukkit.Server; import org.jetbrains.annotations.Nullable; @@ -90,47 +87,4 @@ public static void syncCommands() { } } - private static final @Nullable MessageComponentSerializer MESSAGE_COMPONENT_SERIALIZER; - private static final @Nullable MethodHandle TO_MESSAGE_HANDLE; - - static { - MessageComponentSerializer messageComponentSerializer = null; - try { - messageComponentSerializer = MessageComponentSerializer.message(); - } catch (Exception exception) { - // message serializer service is unavailable - try { - messageComponentSerializer = (MessageComponentSerializer) - Class.forName("io.papermc.paper.command.brigadier.MessageComponentSerializerImpl") - .getDeclaredConstructor().newInstance(); - } catch (Exception ignored) { /* paper implementation is missing */ } - } - MESSAGE_COMPONENT_SERIALIZER = messageComponentSerializer; - - MethodHandle toMessageHandle = null; - try { - Method method = Class.forName("io.papermc.paper.adventure.PaperAdventure") - .getDeclaredMethod("asVanilla", Component.class); - toMessageHandle = MethodHandles.lookup().unreflect(method); - } catch (Exception ignored) { /* paper does not have method to convert the component */ } - TO_MESSAGE_HANDLE = toMessageHandle; - } - - /** - * Converts adventure component to brigadier message. - * - * @param adventure adventure component - * @return brigadier message - */ - public static com.mojang.brigadier.Message brigadierMessage(Component adventure) { - if (MESSAGE_COMPONENT_SERIALIZER != null) - return MESSAGE_COMPONENT_SERIALIZER.serialize(adventure); - if (TO_MESSAGE_HANDLE != null) { - try { - return (com.mojang.brigadier.Message) TO_MESSAGE_HANDLE.invokeExact(adventure); - } catch (Throwable ignored) { /* everything failed, rip */ } - } - return () -> PlainTextComponentSerializer.plainText().serialize(adventure); - } - } diff --git a/src/main/java/org/skriptlang/skript/bukkit/command/elements/DefaultArgumentTypes.java b/src/main/java/org/skriptlang/skript/bukkit/command/elements/DefaultArgumentTypes.java index 69e0ff3f540..3212f14a8bd 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/command/elements/DefaultArgumentTypes.java +++ b/src/main/java/org/skriptlang/skript/bukkit/command/elements/DefaultArgumentTypes.java @@ -37,9 +37,9 @@ private String() { public static void load(SkriptAddon addon) { addon.syntaxRegistry().register(ArgumentTypeElement.REGISTRY_KEY, SyntaxInfo.builder(DefaultArgumentTypes.String.class) - .addPatterns("[single] (word|string)", - "(quotable|quoted) string", - "greedy string") + .addPatterns("[single] (word|string|text)", + "(quotable|quoted) (string|text)", + "greedy (string|text)") .supplier(DefaultArgumentTypes.String::new) .origin(SyntaxOrigin.of(addon)) .build());