From cbbb71c5673b20cdfbe2eea458405dfb4bc020be Mon Sep 17 00:00:00 2001 From: Xeiji Date: Wed, 6 May 2026 02:08:54 +0800 Subject: [PATCH 01/19] Modernize rgb() function with DefaultFunction usage, range checks (#8608) --- .../skript/classes/data/DefaultFunctions.java | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java b/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java index 551107f9a..ff604c3e9 100644 --- a/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java +++ b/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java @@ -602,28 +602,25 @@ public Long[] executeSimple(Object[][] params) { }.description("Calculates the total amount of experience needed to achieve given level from scratch in Minecraft.") .since("2.2-dev32")); - Functions.registerFunction(new SimpleJavaFunction("rgb", new Parameter[] { - new Parameter<>("red", DefaultClasses.LONG, true, null), - new Parameter<>("green", DefaultClasses.LONG, true, null), - new Parameter<>("blue", DefaultClasses.LONG, true, null), - new Parameter<>("alpha", DefaultClasses.LONG, true, new SimpleLiteral<>(255L,true)) - }, DefaultClasses.COLOR, true) { - @Override - public ColorRGB[] executeSimple(Object[][] params) { - Long red = (Long) params[0][0]; - Long green = (Long) params[1][0]; - Long blue = (Long) params[2][0]; - Long alpha = (Long) params[3][0]; - - return CollectionUtils.array(ColorRGB.fromRGBA(red.intValue(), green.intValue(), blue.intValue(), alpha.intValue())); - } - }).description("Returns a RGB color from the given red, green and blue parameters. Alpha values can be added optionally, " + - "but these only take affect in certain situations, like text display backgrounds.") + Functions.register(DefaultFunction.builder(skript, "rgb", Color.class) + .description(""" + Returns a RGB color from the given red, green and blue parameters. + Alpha values can be added optionally but these only take affect in certain situations, like text display backgrounds.""") .examples( "dye player's leggings rgb(120, 30, 45)", "set the colour of a text display to rgb(10, 50, 100, 50)" ) - .since("2.5, 2.10 (alpha)"); + .since("2.5, 2.10 (alpha)") + .parameter("red", Long.class, Modifier.ranged(0, 255)) + .parameter("green", Long.class, Modifier.ranged(0, 255)) + .parameter("blue", Long.class, Modifier.ranged(0, 255)) + .parameter("alpha", Long.class, Modifier.ranged(0, 255), Modifier.OPTIONAL) + .build(args -> ColorRGB.fromRGBA( + args.get("red").intValue(), + args.get("green").intValue(), + args.get("blue").intValue(), + args.getOrDefault("alpha", 255L).intValue() + ))); Functions.register(DefaultFunction.builder(skript, "player", Player.class) .description( From 54fb6c68820bc8a7b7247b6af2fb0a602a032106 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Tue, 5 May 2026 11:24:33 -0700 Subject: [PATCH 02/19] Clean up some docs mistakes in loot tables (#8596) --- .../loottables/elements/expressions/ExprLoot.java | 8 +++----- .../elements/expressions/ExprLootItems.java | 12 ++++++------ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/bukkit/loottables/elements/expressions/ExprLoot.java b/src/main/java/org/skriptlang/skript/bukkit/loottables/elements/expressions/ExprLoot.java index e8559171d..2e6858596 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/loottables/elements/expressions/ExprLoot.java +++ b/src/main/java/org/skriptlang/skript/bukkit/loottables/elements/expressions/ExprLoot.java @@ -5,16 +5,15 @@ import ch.njol.skript.doc.Description; import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; -import ch.njol.skript.doc.RequiredPlugins; import ch.njol.skript.doc.Since; -import ch.njol.util.coll.CollectionUtils; -import org.bukkit.inventory.ItemStack; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; +import ch.njol.util.coll.CollectionUtils; import org.bukkit.event.Event; import org.bukkit.event.world.LootGenerateEvent; +import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.registration.SyntaxInfo; import org.skriptlang.skript.registration.SyntaxRegistry; @@ -26,12 +25,11 @@ @Description("The loot that will be generated in a 'loot generate' event.") @Example(""" on loot generate: - chance of %10 + chance of 10% add 64 diamonds to loot send "You hit the jackpot!!" """) @Since("2.7") -@RequiredPlugins("MC 1.16+") public class ExprLoot extends SimpleExpression { public static void register(SyntaxRegistry registry) { diff --git a/src/main/java/org/skriptlang/skript/bukkit/loottables/elements/expressions/ExprLootItems.java b/src/main/java/org/skriptlang/skript/bukkit/loottables/elements/expressions/ExprLootItems.java index 58de9d7a7..0020de9fc 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/loottables/elements/expressions/ExprLootItems.java +++ b/src/main/java/org/skriptlang/skript/bukkit/loottables/elements/expressions/ExprLootItems.java @@ -30,11 +30,11 @@ + "Not specifying a loot context will use a loot context with a location at the world's origin." ) @Example(""" - set {_items::*} to loot items of the loot table "minecraft:chests/simple_dungeon" with loot context {_context} + set {_items::*} to loot of the loot table "minecraft:chests/simple_dungeon" with loot context {_context} # this will set {_items::*} to the items that would be dropped from the simple dungeon loot table with the given loot context """) @Example(""" - give player loot items of entity's loot table with loot context {_context} + give player loot of entity's loot table with loot context {_context} # this will give the player the items that the entity would drop with the given loot context """) @Since("2.10") @@ -45,8 +45,8 @@ public static void register(SyntaxRegistry registry) { SyntaxRegistry.EXPRESSION, SyntaxInfo.Expression.builder(ExprLootItems.class, ItemStack.class) .addPatterns( - "[the] loot of %loottables% [(with|using) %-lootcontext%]", - "%loottables%'[s] loot [(with|using) %-lootcontext%]" + "[the] loot of %loottables% [(with|using) [[loot] context] %-lootcontext%]", + "%loottables%'[s] loot [(with|using) [[loot] context] %-lootcontext%]" ) .supplier(ExprLootItems::new) .build() @@ -72,7 +72,7 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye if (context == null) return new ItemStack[0]; } else { - context = new LootContextWrapper(Bukkit.getWorlds().get(0).getSpawnLocation()).getContext(); + context = new LootContextWrapper(Bukkit.getWorlds().getFirst().getSpawnLocation()).getContext(); } List items = new ArrayList<>(); @@ -104,7 +104,7 @@ public String toString(@Nullable Event event, boolean debug) { builder.append("the loot of", lootTables); if (context != null) - builder.append("with", context); + builder.append("with loot context ", context); return builder.toString(); } From 6dd84a380524fa9480d797318edacfecfa9501df Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Tue, 5 May 2026 11:36:09 -0700 Subject: [PATCH 03/19] Remove mentions of SQL variable storage from config.sk (#8598) --- src/main/resources/config.sk | 60 ++---------------------------------- 1 file changed, 3 insertions(+), 57 deletions(-) diff --git a/src/main/resources/config.sk b/src/main/resources/config.sk index 6e99f846d..ab9bb6966 100644 --- a/src/main/resources/config.sk +++ b/src/main/resources/config.sk @@ -304,9 +304,6 @@ databases: # # You can define as many databases as you want, just make sure to choose a distinct name for each one, and don't forget to set all options correctly. # - # To be able to use a database you'll need to download the plugin 'SQLibrary' from https://dev.bukkit.org/projects/sqlibrary/files - # and install it in your server's plugin directory like other plugins. - # # Please note that '/skript reload' will not reload this section, i.e. you'll have to restart Skript for changes to take effect. # Each database definition must be in a separate section. You can choose any name for the sections, as long as it's not already used. @@ -314,8 +311,8 @@ databases: # An example database to describe all possible options. type: disabled - # The type of this database. Allowed values are 'CSV', 'SQLite', 'MySQL' and 'disabled'. - # CSV uses a text file to store the variables, while SQLite and MySQL use databases, and 'disabled' makes Skript ignore the database as if it wasn't defined at all. + # The type of this database. Allowed values are 'CSV' and 'disabled'. + # CSV uses a text file to store the variables and 'disabled' makes Skript ignore the database as if it wasn't defined at all. pattern: .* # Defines which variables to save in this database. @@ -330,24 +327,11 @@ databases: # If 'monitor changes' is set to true, variables will repeatedly be checked for updates in the database (in intervals set in 'monitor interval'). # ! Please note that you should set 'pattern', 'monitor changes' and 'monitor interval' to the same values on all servers that access the same database! - # == MySQL configuration == - host: localhost # Where the database server is located at, e.g. 'example.com', 'localhost', or '192.168.1.100' - port: 3306 # 3306 is MySQL's default port, i.e. you likely won't need to change this value - user: root - password: pass - database: skript # The database to use, the table will be created in this database. - table: variables21 # The name of the table to create. 'variables21' is the default name, if this was to be omitted. - # (If the table exists but is defined differently that how Skript expects it to be you'll get errors and no variables will be saved and/or loaded) - # == SQLite/CSV configuration == + # == CSV configuration == file: ./plugins/Skript/variables.db # Where to save the variables to. For a CSV file, the file extension '.csv' is recommended, but not required, but SQLite database files must end in '.db' (SQLibrary forces this). # The file path can either be absolute (e.g. 'C:\whatever\...' [Windows] or '/usr/whatever/...' [Unix]), or relative to the server directory (e.g. './plugins/Skript/...'). - #table: variables21 - # The name of the table to create. 'variables21' is the default name, if this was to be omitted. - # (If the table exists but is defined differently that how Skript expects it to be you'll get errors and no variables will be saved and/or loaded) - # This is generally not required as the the .db file will only be used by Skript, unless you want to split different variables into different tables - backup interval: 2 hours # Creates a backup of the file every so often. This can be useful if you ever want to revert variables to an older state. # Variables are saved constantly no matter what is set here, thus a server crash will never make you lose any variables. @@ -359,44 +343,6 @@ databases: # If disabled (set to -1), no backup files will be deleted. # WARNING: Setting to 0 will delete all files located in the backup directory upon plugin start/reload - - MySQL example: - # A MySQL database example, with options unrelated to MySQL removed. - - type: disabled # change to line below to enable this database - # type: MySQL - - pattern: synced_.* # this pattern will save all variables that start with 'synced_' in this MySQL database. - - host: localhost - port: 3306 - user: root - password: pass - database: skript - table: variables21 - - monitor changes: true - monitor interval: 20 seconds - - SQLite example: - # An SQLite database example. - - type: disabled # change to line below to enable this database - # type: SQLite - - pattern: db_.* # this pattern will save all variables that start with 'db_' in this SQLite database. - - file: ./plugins/Skript/variables.db - # SQLite databases must end in '.db' - #table: variables21 - # Usually not required, if omitted defaults to variables21 (see above for more details) - - backup interval: 0 # 0 = don't create backups - monitor changes: false - monitor interval: 20 seconds - - backups to keep: -1 - default: # The default "database" is a simple text file, with each variable on a separate line and the variable's name, type, and value separated by commas. # This is the last database in this list to catch all variables that have not been saved anywhere else. From 53e24a8cfc73f63eb8c45c8e75673c19f2064b80 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Wed, 20 May 2026 07:55:51 -0700 Subject: [PATCH 04/19] Fix command usage message ignoring color tags (#8623) --- .../java/ch/njol/skript/command/CommandUsage.java | 13 +++++++++++++ .../java/ch/njol/skript/command/ScriptCommand.java | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/njol/skript/command/CommandUsage.java b/src/main/java/ch/njol/skript/command/CommandUsage.java index 448801df3..d69125760 100644 --- a/src/main/java/ch/njol/skript/command/CommandUsage.java +++ b/src/main/java/ch/njol/skript/command/CommandUsage.java @@ -2,8 +2,10 @@ import ch.njol.skript.lang.VariableString; import ch.njol.skript.util.Utils; +import net.kyori.adventure.text.Component; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.text.TextComponentParser; /** * Holds info about the usage of a command. @@ -63,6 +65,17 @@ public String getUsage(@Nullable Event event) { return defaultUsage; } + /** + * @param event The event used to evaluate the usage message. + * @return The evaluated usage message as an Adventure {@link Component} with + * Skript color tags ({@code }, {@code &c}, etc.) parsed into real + * chat formatting via {@link TextComponentParser#parse(Object)}. Command + * usage strings are server-controlled, so unsafe tags are appropriate. + */ + public Component getUsageComponent(@Nullable Event event) { + return TextComponentParser.instance().parse(getUsage(event)); + } + @Override public String toString() { return getUsage(); diff --git a/src/main/java/ch/njol/skript/command/ScriptCommand.java b/src/main/java/ch/njol/skript/command/ScriptCommand.java index 41f5f47cf..61c3ba78c 100644 --- a/src/main/java/ch/njol/skript/command/ScriptCommand.java +++ b/src/main/java/ch/njol/skript/command/ScriptCommand.java @@ -337,7 +337,7 @@ boolean execute2(final ScriptCommandEvent event, final CommandSender sender, fin final LogEntry e = log.getError(); if (e != null) sender.sendMessage(ChatColor.DARK_RED + e.toString()); - sender.sendMessage(usage.getUsage(event)); + sender.sendMessage(usage.getUsageComponent(event)); log.clear(); return false; } From a0e644d584934587c61694d49cd05f86b380cbbf Mon Sep 17 00:00:00 2001 From: Kayera Date: Wed, 20 May 2026 18:08:07 +0300 Subject: [PATCH 05/19] Fix Turkish language translations for commands and errors (#8650) --- src/main/resources/lang/turkish.lang | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/resources/lang/turkish.lang b/src/main/resources/lang/turkish.lang index 62329b5c6..b2814ddf5 100644 --- a/src/main/resources/lang/turkish.lang +++ b/src/main/resources/lang/turkish.lang @@ -142,15 +142,15 @@ commands: executable by console: Bu komut yalnızca konsol tarafından kullanılabilir. correct usage: Doğru kullanım: invalid argument: Yanlış argüman '%s'. İzin verilenler: - too many arguments: Bu komut yanlızca bir tane %2$s kabul eder. + too many arguments: Bu komut yalnızca bir tane %2$s kabul eder. internal error: Bu komut gerçekleştirilmeye çalışılırken dahili bir hata oluştu. no player starts with: Adı '%s' ile başlayan oyuncu bulunamadı. multiple players start with: Adı '%s' ile başlayan birden fazla oyuncu var. # -- Hooks -- hooks: - hooked: %s'e başarıyla bağlanıldı. - error: %1$s'a bağlanılamadı. Skript %1$s'in yüklü sürümünü desteklemiyorsa bu durum meydana gelebilir + hooked: %s adlı modüle başarıyla bağlanıldı. + error: %1$s adlı modüle bağlanılamadı. Skript %1$s adlı modülün yüklü sürümünü desteklemiyorsa bu durum meydana gelebilir # -- Aliases -- aliases: @@ -184,5 +184,5 @@ time: # -- IO Exceptions -- io exceptions: - unknownhostexception: %s'ya bağlanılamadı. + unknownhostexception: %s sunucusuna bağlanılamadı. accessdeniedexception: %s için izin reddedildi. From cf70374a2e3bc3a238c9dcdbf8e9bf2b8822786a Mon Sep 17 00:00:00 2001 From: _tud <98935832+UnderscoreTud@users.noreply.github.com> Date: Wed, 20 May 2026 18:13:24 +0300 Subject: [PATCH 06/19] Improve event value cache (#8647) --- .../lang/eventvalue/ConvertedEventValue.java | 5 + .../bukkit/lang/eventvalue/EventValue.java | 27 ++ .../lang/eventvalue/EventValueImpl.java | 14 + .../eventvalue/EventValueRegistryImpl.java | 55 ++-- .../bukkit/lang/eventvalue/Resolver.java | 38 ++- .../eventvalue/EventValueRegistryTest.java | 262 ++++++++++++++++++ 6 files changed, 372 insertions(+), 29 deletions(-) create mode 100644 src/test/java/org/skriptlang/skript/bukkit/lang/eventvalue/EventValueRegistryTest.java diff --git a/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/ConvertedEventValue.java b/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/ConvertedEventValue.java index 67da84029..050dc4823 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/ConvertedEventValue.java +++ b/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/ConvertedEventValue.java @@ -191,6 +191,11 @@ public Time time() { return source.excludedErrorMessage(); } + @Override + public boolean contextDependent() { + return source.contextDependent(); + } + @Override public boolean matches(EventValue eventValue) { return matches(eventValue.eventClass(), eventValue.valueClass(), eventValue.patterns()); diff --git a/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/EventValue.java b/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/EventValue.java index 870fc4a05..c8a436256 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/EventValue.java +++ b/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/EventValue.java @@ -155,6 +155,21 @@ static EventValue simple(Class eventClass, Class + * Defaults to {@code false}; only set this when an {@link Builder#eventValidator(Function) + * event validator} is not pure for a given event class. + * + * @return {@code true} if resolutions involving this value should bypass the cache + */ + @Contract(pure = true) + boolean contextDependent(); + /** * Checks whether this event value matches the provided event value in terms of * event class, value class, and identifier patterns. @@ -453,6 +468,18 @@ default Builder excludes(Class event1, Class eve @Contract(value = "_ -> this", mutates = "this") Builder excludedErrorMessage(String excludedErrorMessage); + /** + * Marks this event value as context-dependent. Resolutions that consider this value will + * not be cached, so the {@linkplain #eventValidator(Function) event validator} is + * consulted on every lookup. Use this when the validator's answer for a given event + * class may change at runtime (e.g. because it consults external state). + * + * @return this builder + * @see EventValue#contextDependent() + */ + @Contract(value = " -> this", mutates = "this") + Builder contextDependent(); + /** * Builds the event value. * diff --git a/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/EventValueImpl.java b/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/EventValueImpl.java index b9e91c4a7..f462ff5eb 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/EventValueImpl.java +++ b/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/EventValueImpl.java @@ -36,6 +36,7 @@ final class EventValueImpl implements EventValue { private final Time time; private final Collection> excludedEvents; private final @Nullable String excludedErrorMessage; + private final boolean contextDepenent; private SkriptPattern[] compiledPatterns; @@ -51,6 +52,7 @@ private EventValueImpl(BuilderImpl builder) { this.time = builder.time; this.excludedEvents = builder.excludedEvents; this.excludedErrorMessage = builder.excludedErrorMessage; + this.contextDepenent = builder.contextDependent; } @Override @@ -153,6 +155,11 @@ public Time time() { return excludedErrorMessage; } + @Override + public boolean contextDependent() { + return contextDepenent; + } + @Override public boolean matches(EventValue eventValue) { return matches(eventValue.eventClass(), eventValue.valueClass(), eventValue.patterns()) @@ -204,6 +211,7 @@ static class BuilderImpl implements Builder { private Time time = Time.NOW; private Collection> excludedEvents = Collections.emptyList(); private @Nullable String excludedErrorMessage; + private boolean contextDependent = false; BuilderImpl(Class eventClass, Class valueClass) { this.eventClass = eventClass; @@ -260,6 +268,12 @@ public Builder excludedErrorMessage(String excludedErrorMessage) { return this; } + @Override + public Builder contextDependent() { + this.contextDependent = true; + return this; + } + @Override public EventValue build() { if (patterns == null) { diff --git a/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/EventValueRegistryImpl.java b/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/EventValueRegistryImpl.java index 152dec37b..8e641044c 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/EventValueRegistryImpl.java +++ b/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/EventValueRegistryImpl.java @@ -66,6 +66,12 @@ public boolean isRegistered(Class eventClass, Class valueCla return false; } + private void cache(Input input, Resolution resolution, boolean contextDependent) { + if (contextDependent) + return; + eventValuesCache.put(input, resolution); + } + @Override public Resolution resolve(Class eventClass, String identifier) { return resolve(eventClass, identifier, EventValue.Time.NOW); @@ -96,14 +102,15 @@ public Resolution resolve( return resolution; //noinspection unchecked - resolution = Resolver.builder(eventClass) + var output = Resolver.builder(eventClass) .filter(ev -> ClassUtils.isRelatedTo(ev.eventClass(), eventClass) && ev.matchesInput(identifier)) .comparator(Resolver.EVENT_DISTANCE_COMPARATOR) .mapper(ev -> (EventValue) ev.getConverted(eventClass, ev.valueClass())) .build().resolve(eventValues(time)); + resolution = output.resolution(); - if (resolution.successful()) { - eventValuesCache.put(input, resolution); + if (resolution.successful() || resolution.errored()) { + cache(input, resolution, output.contextDependent()); return resolution; } @@ -111,7 +118,7 @@ public Resolution resolve( return resolve(eventClass, identifier, EventValue.Time.NOW, flags); resolution = Resolution.empty(); - eventValuesCache.put(input, resolution); + cache(input, resolution, output.contextDependent()); return resolution; } @@ -144,31 +151,36 @@ public Resolution resolve( if (resolution != null) return resolution; - resolution = resolveExact(eventClass, valueClass, time) - .anyOptional() - .map(eventValue -> Resolution.of(Collections.singletonList(eventValue))) - .orElse(Resolution.empty()); + var output = Resolver.exact(eventClass, valueClass).resolve(eventValues(time)); + resolution = output.resolution(); + if (resolution.successful() || resolution.errored()) { - eventValuesCache.put(input, resolution); + cache(input, resolution, output.contextDependent()); return resolution; } - resolution = resolveNearest(eventClass, valueClass, time); + output = resolveNearest(eventClass, valueClass, time); + resolution = output.resolution(); + if (resolution.successful() || resolution.errored()) { - eventValuesCache.put(input, resolution); + cache(input, resolution, output.contextDependent()); return resolution; } if (flags.has(Flag.ALLOW_CONVERSION)) { - resolution = resolveWithDowncastConversion(eventClass, valueClass, time); + output = resolveWithDowncastConversion(eventClass, valueClass, time); + resolution = output.resolution(); + if (resolution.successful() || resolution.errored()) { - eventValuesCache.put(input, resolution); + cache(input, resolution, output.contextDependent()); return resolution; } - resolution = resolveWithConversion(eventClass, valueClass, time); + output = resolveWithConversion(eventClass, valueClass, time); + resolution = output.resolution(); + if (resolution.successful() || resolution.errored()) { - eventValuesCache.put(input, resolution); + cache(input, resolution, output.contextDependent()); return resolution; } } @@ -177,7 +189,6 @@ public Resolution resolve( return resolve(eventClass, valueClass, EventValue.Time.NOW, flags); resolution = Resolution.empty(); - eventValuesCache.put(input, resolution); return resolution; } @@ -187,17 +198,13 @@ public Resolution resolveExact( Class valueClass, EventValue.Time time ) { - return Resolver.builder(eventClass, valueClass) - .filter(ev -> ev.eventClass().isAssignableFrom(eventClass) && ev.valueClass().equals(valueClass)) - .comparator(Resolver.EVENT_DISTANCE_COMPARATOR) - .filterMatches() - .build().resolve(eventValues(time)); + return Resolver.exact(eventClass, valueClass).resolve(eventValues(time)).resolution(); } /** * Resolves to the nearest event and value class without conversion. */ - private Resolution resolveNearest( + private Resolver.Output resolveNearest( Class eventClass, Class valueClass, EventValue.Time time @@ -214,7 +221,7 @@ public Resolution resolveExact( * Resolves using downcast conversion when the desired value class is a supertype * of the registered value class. */ - private Resolution resolveWithDowncastConversion( + private Resolver.Output resolveWithDowncastConversion( Class eventClass, Class valueClass, EventValue.Time time @@ -233,7 +240,7 @@ private Resolution resolveWithDowncastConversion( /** * Resolves using {@link Converters} to convert value type when needed. */ - private Resolution resolveWithConversion( + private Resolver.Output resolveWithConversion( Class eventClass, Class valueClass, EventValue.Time time diff --git a/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/Resolver.java b/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/Resolver.java index 1865e6de5..254a4320f 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/Resolver.java +++ b/src/main/java/org/skriptlang/skript/bukkit/lang/eventvalue/Resolver.java @@ -74,21 +74,25 @@ private Resolver( * Resolves the given list of {@link EventValue}s. * * @param eventValues The event values to resolve. - * @return The resolution containing the best candidates. + * @return The resolution containing the best candidates, along with a flag indicating + * whether any candidate considered was {@linkplain EventValue#contextDependent() context-dependent}. */ - public EventValueRegistry.Resolution resolve(List> eventValues) { + public Output resolve(List> eventValues) { List> best = new ArrayList<>(); EventValue bestMatch = null; + boolean contextDependent = false; + for (EventValue eventValue : eventValues) { if (!filter.test(eventValue)) continue; + contextDependent |= eventValue.contextDependent(); switch (eventValue.validate(eventClass)) { case INVALID -> { continue; } case ABORT -> { - return EventValueRegistry.Resolution.error(); + return new Output<>(EventValueRegistry.Resolution.error(), contextDependent); } } @@ -109,9 +113,10 @@ public EventValueRegistry.Resolution resolve(List> eventV best.add(converted); } } + if (valueClass != null && filterMatches) - return EventValueRegistry.Resolution.of(filterEventValues(valueClass, best)); - return EventValueRegistry.Resolution.of(best); + return new Output<>(EventValueRegistry.Resolution.of(filterEventValues(valueClass, best)), contextDependent); + return new Output<>(EventValueRegistry.Resolution.of(best), contextDependent); } /** @@ -173,6 +178,14 @@ static Builder builder(Class eventClass, Class return new Builder<>(eventClass, valueClass); } + static Resolver exact(Class eventClass, Class valueClass) { + return Resolver.builder(eventClass, valueClass) + .filter(ev -> ev.eventClass().isAssignableFrom(eventClass) && ev.valueClass().equals(valueClass)) + .comparator(Resolver.EVENT_DISTANCE_COMPARATOR) + .filterMatches() + .build(); + } + /** * A builder for {@link Resolver}. * @@ -309,4 +322,19 @@ interface EventValueComparatorFactory { } + /** + * The result of a {@link Resolver#resolve(List) resolve} call. + * + * @param resolution The resolution produced. + * @param contextDependent Whether any candidate considered during resolution was + * {@linkplain EventValue#contextDependent() context-dependent}, + * meaning the result must not be cached. + * @param The event type. + * @param The value type. + */ + record Output( + EventValueRegistry.Resolution resolution, + boolean contextDependent + ) {} + } diff --git a/src/test/java/org/skriptlang/skript/bukkit/lang/eventvalue/EventValueRegistryTest.java b/src/test/java/org/skriptlang/skript/bukkit/lang/eventvalue/EventValueRegistryTest.java new file mode 100644 index 000000000..30c80b2eb --- /dev/null +++ b/src/test/java/org/skriptlang/skript/bukkit/lang/eventvalue/EventValueRegistryTest.java @@ -0,0 +1,262 @@ +package org.skriptlang.skript.bukkit.lang.eventvalue; + +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; +import org.junit.Before; +import org.junit.Test; +import org.skriptlang.skript.lang.converter.Converters; + +import static org.junit.Assert.*; + +public class EventValueRegistryTest { + + private EventValueRegistry registry; + + @Before + public void setUp() { + registry = EventValueRegistry.empty(null); + } + + private static class TestEvent extends Event { + @Override + public @NotNull HandlerList getHandlers() { + throw new UnsupportedOperationException(); + } + } + private static class SubTestEvent extends TestEvent {} + private static class OtherEvent extends Event { + @Override + public @NotNull HandlerList getHandlers() { + throw new UnsupportedOperationException(); + } + } + + private static class TestValue {} + private static class SubTestValue extends TestValue {} + private static class OtherValue {} + + @Test + public void testRegistration() { + EventValue ev = EventValue.builder(TestEvent.class, TestValue.class) + .patterns("test") + .getter(e -> new TestValue()) + .build(); + + assertFalse(registry.isRegistered(ev)); + registry.register(ev); + assertTrue(registry.isRegistered(ev)); + assertTrue(registry.isRegistered(TestEvent.class, TestValue.class, EventValue.Time.NOW)); + + assertTrue(registry.elements().contains(ev)); + assertTrue(registry.elements(EventValue.Time.NOW).contains(ev)); + assertTrue(registry.elements(TestEvent.class).contains(ev)); + + assertTrue(registry.unregister(ev)); + assertFalse(registry.isRegistered(ev)); + assertFalse(registry.elements().contains(ev)); + } + + @Test + public void testResolveByIdentifier() { + EventValue ev = EventValue.builder(TestEvent.class, TestValue.class) + .patterns("test pattern") + .getter(e -> new TestValue()) + .build(); + registry.register(ev); + + EventValueRegistry.Resolution res = registry.resolve(TestEvent.class, "test pattern"); + assertTrue(res.successful()); + assertEquals(ev, res.unique()); + + // Test sub-event + EventValueRegistry.Resolution subRes = registry.resolve(SubTestEvent.class, "test pattern"); + assertTrue(subRes.successful()); + // It should be a converted event value + assertEquals(ev.valueClass(), subRes.unique().valueClass()); + assertEquals(TestEvent.class, subRes.unique().eventClass()); + + // Test non-matching identifier + assertFalse(registry.resolve(TestEvent.class, "wrong").successful()); + } + + @Test + public void testResolveByValueClass() { + EventValue ev = EventValue.builder(TestEvent.class, TestValue.class) + .getter(e -> new TestValue()) + .build(); + registry.register(ev); + + EventValueRegistry.Resolution res = registry.resolve(TestEvent.class, TestValue.class); + assertTrue(res.successful()); + assertEquals(ev, res.unique()); + + EventValueRegistry.Resolution subValueRes = registry.resolve(TestEvent.class, SubTestValue.class); + assertTrue(subValueRes.successful()); + } + + @Test + public void testCacheLogic() { + EventValue ev = EventValue.builder(TestEvent.class, TestValue.class) + .patterns("test") + .getter(e -> new TestValue()) + .build(); + registry.register(ev); + + EventValueRegistry.Resolution res1 = registry.resolve(TestEvent.class, "test"); + EventValueRegistry.Resolution res2 = registry.resolve(TestEvent.class, "test"); + + assertSame("Resolutions should be cached and return the same instance", res1, res2); + + // Registering a new value should clear the cache + EventValue ev2 = EventValue.builder(OtherEvent.class, TestValue.class) + .patterns("other") + .getter(e -> new TestValue()) + .build(); + registry.register(ev2); + + EventValueRegistry.Resolution res3 = registry.resolve(TestEvent.class, "test"); + assertNotSame("Cache should have been cleared", res1, res3); + assertEquals(res1, res3); + } + + @Test + public void testContextDependentNotCached() { + EventValue ev = EventValue.builder(TestEvent.class, TestValue.class) + .patterns("test") + .getter(e -> new TestValue()) + .contextDependent() + .build(); + registry.register(ev); + + EventValueRegistry.Resolution res1 = registry.resolve(TestEvent.class, "test"); + EventValueRegistry.Resolution res2 = registry.resolve(TestEvent.class, "test"); + + assertNotSame("Context-dependent resolutions should not be cached", res1, res2); + assertEquals(res1, res2); + } + + @Test + public void testUnmodifiableView() { + EventValue ev = EventValue.builder(TestEvent.class, TestValue.class) + .patterns("test") + .getter(e -> new TestValue()) + .build(); + registry.register(ev); + + EventValueRegistry unmodifiable = registry.unmodifiableView(); + assertTrue(unmodifiable.isRegistered(ev)); + + assertThrows(UnsupportedOperationException.class, () -> unmodifiable.register(ev)); + assertThrows(UnsupportedOperationException.class, () -> unmodifiable.unregister(ev)); + } + + @Test + public void testTimeFallback() { + EventValue evPast = EventValue.builder(TestEvent.class, TestValue.class) + .patterns("test") + .time(EventValue.Time.PAST) + .getter(e -> new TestValue()) + .build(); + registry.register(evPast); + + // Resolve past + assertTrue(registry.resolve(TestEvent.class, "test", EventValue.Time.PAST).successful()); + // Resolve now (should fail) + assertFalse(registry.resolve(TestEvent.class, "test", EventValue.Time.NOW).successful()); + + EventValue evNow = EventValue.builder(TestEvent.class, TestValue.class) + .patterns("test") + .time(EventValue.Time.NOW) + .getter(e -> new TestValue()) + .build(); + registry.register(evNow); + + // Resolve with fallback + EventValueRegistry.Resolution res = registry.resolve( + TestEvent.class, "test", EventValue.Time.FUTURE, EventValueRegistry.Flags.of(EventValueRegistry.Flag.FALLBACK_TO_DEFAULT_TIME_STATE)); + assertTrue(res.successful()); + assertEquals(evNow.valueClass(), res.unique().valueClass()); + assertEquals(EventValue.Time.NOW, res.unique().time()); + } + + @Test + public void testResolveExact() { + EventValue ev = EventValue.builder(TestEvent.class, TestValue.class) + .getter(e -> new TestValue()) + .build(); + registry.register(ev); + + assertTrue(registry.resolveExact(TestEvent.class, TestValue.class, EventValue.Time.NOW).successful()); + assertTrue(registry.resolveExact(SubTestEvent.class, TestValue.class, EventValue.Time.NOW).successful()); + assertFalse(registry.resolveExact(TestEvent.class, SubTestEvent.class, EventValue.Time.NOW).successful()); + } + + @Test + public void testAmbiguousResolution() { + EventValue ev1 = EventValue.builder(TestEvent.class, Integer.class) + .patterns("test") + .getter(e -> 10) + .build(); + EventValue ev2 = EventValue.builder(TestEvent.class, Double.class) + .patterns("test") + .getter(e -> 10.0) + .build(); + registry.register(ev1); + registry.register(ev2); + + EventValueRegistry.Resolution res = registry.resolve(TestEvent.class, "test"); + assertTrue(res.successful()); + assertTrue(res.multiple()); + assertEquals(2, res.size()); + assertNotNull(res.any()); + assertTrue(res.anyOptional().isPresent()); + assertThrows(IllegalStateException.class, res::unique); + } + + @Test + public void testAbortValidation() { + EventValue ev = EventValue.builder(TestEvent.class, TestValue.class) + .patterns("test") + .eventValidator(event -> event.equals(SubTestEvent.class) ? EventValue.Validation.ABORT : EventValue.Validation.VALID) + .getter(e -> new TestValue()) + .build(); + registry.register(ev); + + assertTrue(registry.resolve(TestEvent.class, "test").successful()); + EventValueRegistry.Resolution res = registry.resolve(SubTestEvent.class, "test"); + assertFalse(res.successful()); + assertTrue(res.errored()); + } + + @Test + public void testDowncastConversion() { + EventValue ev = EventValue.builder(TestEvent.class, TestValue.class) + .getter(e -> new TestValue()) + .build(); + registry.register(ev); + + // Resolve for SubTestValue (subtype of TestValue) should work with ALLOW_CONVERSION + EventValueRegistry.Resolution res = registry.resolve(TestEvent.class, SubTestValue.class, EventValue.Time.NOW, + EventValueRegistry.Flags.of(EventValueRegistry.Flag.ALLOW_CONVERSION)); + assertTrue(res.successful()); + assertEquals(SubTestValue.class, res.unique().valueClass()); + } + + @Test + public void testConverter() { + EventValue ev = EventValue.builder(TestEvent.class, TestValue.class) + .getter(e -> new TestValue()) + .build(); + registry.register(ev); + + // Register a converter from TestValue to OtherValue + Converters.registerConverter(TestValue.class, OtherValue.class, from -> new OtherValue()); + + // Resolve for OtherValue should work with ALLOW_CONVERSION + EventValueRegistry.Resolution res = registry.resolve(TestEvent.class, OtherValue.class, EventValue.Time.NOW, + EventValueRegistry.Flags.of(EventValueRegistry.Flag.ALLOW_CONVERSION)); + assertTrue(res.successful()); + assertEquals(OtherValue.class, res.unique().valueClass()); + } +} From 8c76e98185db5bafa601475ec20b90f2e0cb556d Mon Sep 17 00:00:00 2001 From: Efnilite <35348263+Efnilite@users.noreply.github.com> Date: Wed, 20 May 2026 17:24:42 +0200 Subject: [PATCH 07/19] Fix getting element from queue returning whole queue (#8562) --- .../expressions/ExprAffectedEntities.java | 9 +- .../njol/skript/expressions/ExprElement.java | 99 +++++++++---------- .../regressions/8548-first queue element.sk | 7 ++ 3 files changed, 59 insertions(+), 56 deletions(-) create mode 100644 src/test/skript/tests/regressions/8548-first queue element.sk diff --git a/src/main/java/ch/njol/skript/expressions/ExprAffectedEntities.java b/src/main/java/ch/njol/skript/expressions/ExprAffectedEntities.java index ee463e3ea..3666dd45d 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprAffectedEntities.java +++ b/src/main/java/ch/njol/skript/expressions/ExprAffectedEntities.java @@ -74,19 +74,18 @@ public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { if (!(event instanceof AreaEffectCloudApplyEvent areaEvent)) return; - LivingEntity[] entities = (LivingEntity[]) delta; switch (mode) { case REMOVE: - for (LivingEntity entity : entities) { - areaEvent.getAffectedEntities().remove(entity); + for (Object entity : delta) { + areaEvent.getAffectedEntities().remove((LivingEntity) entity); } break; case SET: areaEvent.getAffectedEntities().clear(); // FALLTHROUGH case ADD: - for (LivingEntity entity : entities) { - areaEvent.getAffectedEntities().add(entity); + for (Object entity : delta) { + areaEvent.getAffectedEntities().add((LivingEntity) entity); } break; case RESET, DELETE: diff --git a/src/main/java/ch/njol/skript/expressions/ExprElement.java b/src/main/java/ch/njol/skript/expressions/ExprElement.java index 86b454f88..0d1bb8fdc 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprElement.java +++ b/src/main/java/ch/njol/skript/expressions/ExprElement.java @@ -10,7 +10,6 @@ import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.simplification.SimplifiedLiteral; import ch.njol.skript.lang.util.SimpleExpression; -import ch.njol.skript.registrations.Feature; import ch.njol.skript.util.LiteralUtils; import ch.njol.skript.util.Patterns; import ch.njol.util.Kleenean; @@ -48,17 +47,11 @@ public class ExprElement extends SimpleExpression implements KeyProviderExpression { private static final Patterns PATTERNS = new Patterns<>(new Object[][]{ - {"[the] (first|1:last) element [out] of %objects%", new ElementType[] {ElementType.FIRST_ELEMENT, ElementType.LAST_ELEMENT}}, - {"[the] (first|1:last) %integer% elements [out] of %objects%", new ElementType[] {ElementType.FIRST_X_ELEMENTS, ElementType.LAST_X_ELEMENTS}}, - {"[a] random element [out] of %objects%", new ElementType[] {ElementType.RANDOM}}, - {"[the] %integer%(st|nd|rd|th) [1:[to] last] element [out] of %objects%", new ElementType[] {ElementType.ORDINAL, ElementType.TAIL_END_ORDINAL}}, - {"[the] elements (from|between) %integer% (to|and) %integer% [out] of %objects%", new ElementType[] {ElementType.RANGE}}, - - {"[the] (first|next|1:last) element (of|in) %queue%", new ElementType[] {ElementType.FIRST_ELEMENT, ElementType.LAST_ELEMENT}}, - {"[the] (first|1:last) %integer% elements (of|in) %queue%", new ElementType[] {ElementType.FIRST_X_ELEMENTS, ElementType.LAST_X_ELEMENTS}}, - {"[a] random element (of|in) %queue%", new ElementType[] {ElementType.RANDOM}}, - {"[the] %integer%(st|nd|rd|th) [1:[to] last] element (of|in) %queue%", new ElementType[] {ElementType.ORDINAL, ElementType.TAIL_END_ORDINAL}}, - {"[the] elements (from|between) %integer% (to|and) %integer% (of|in) %queue%", new ElementType[] {ElementType.RANGE}}, + {"[the] (first|1:last) element [out] (of|in) %objects%", new ElementType[]{ElementType.FIRST_ELEMENT, ElementType.LAST_ELEMENT}}, + {"[the] (first|1:last) %integer% elements [out] (of|in) %objects%", new ElementType[]{ElementType.FIRST_X_ELEMENTS, ElementType.LAST_X_ELEMENTS}}, + {"[a] random element [out] (of|in) %objects%", new ElementType[]{ElementType.RANDOM}}, + {"[the] %integer%(st|nd|rd|th) [1:[to] last] element [out] (of|in) %objects%", new ElementType[]{ElementType.ORDINAL, ElementType.TAIL_END_ORDINAL}}, + {"[the] elements (from|between) %integer% (to|and) %integer% [out] (of|in) %objects%", new ElementType[]{ElementType.RANGE}}, }); static { @@ -132,23 +125,16 @@ public Iterator apply(Iterator iterator, int startIndex, int endIndex) private final Map> cache = new WeakHashMap<>(); private Expression expr; - private @Nullable Expression startIndex, endIndex; + private @Nullable Expression startIndex, endIndex; private ElementType type; - private boolean queue; private boolean keyed; @Override @SuppressWarnings("unchecked") public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { ElementType[] types = PATTERNS.getInfo(matchedPattern); - this.queue = matchedPattern > 4; - if (queue && !this.getParser().hasExperiment(Feature.QUEUES)) - return false; - if (queue) { - this.expr = (Expression) expressions[expressions.length - 1]; - } else { - this.expr = LiteralUtils.defendExpression(expressions[expressions.length - 1]); - } + this.expr = LiteralUtils.defendExpression(expressions[expressions.length - 1]); + switch (type = types[parseResult.mark]) { case RANGE: endIndex = (Expression) expressions[1]; @@ -160,13 +146,11 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean is break; } this.keyed = KeyProviderExpression.canReturnKeys(this.expr); - return queue || LiteralUtils.canInitSafely(expr); + return LiteralUtils.canInitSafely(expr); } @Override protected T @Nullable [] get(Event event) { - if (queue) - return this.getFromQueue(event); if (keyed) { KeyedValue.UnzippedKeyValues unzipped = KeyedValue.unzip(keyedIterator(event)); cache.put(event, unzipped.keys()); @@ -174,7 +158,19 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean is T[] empty = (T[]) Array.newInstance(getReturnType(), 0); return unzipped.values().toArray(empty); } - Iterator iterator = iterator(event); + + Iterator iterator; + if (expr.isSingle()) { + T single = expr.getSingle(event); + if (single instanceof SkriptQueue queue) { + return this.getFromQueue(event, queue); + } else { + iterator = transformIterator(event, Iterators.singletonIterator(single)); + } + } else { + iterator = transformIterator(event, expr.iterator(event)); + } + assert iterator != null; //noinspection unchecked return Iterators.toArray(iterator, (Class) getReturnType()); @@ -191,8 +187,13 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean is @Override public @Nullable Iterator iterator(Event event) { - if (queue) - return Optional.ofNullable(getFromQueue(event)).map(Iterators::forArray).orElse(null); + if (expr.isSingle()) { + T single = expr.getSingle(event); + if (single instanceof SkriptQueue queue) { + return Optional.ofNullable(getFromQueue(event, queue)).map(Iterators::forArray).orElse(null); + } + return Iterators.singletonIterator(single); + } Iterator iterator = expr.iterator(event); return transformIterator(event, iterator); } @@ -224,8 +225,7 @@ private Iterator transformIterator(Event event, @Nullable Iterator ite } @SuppressWarnings("unchecked") - private T @Nullable [] getFromQueue(Event event) { - SkriptQueue queue = (SkriptQueue) expr.getSingle(event); + private T @Nullable [] getFromQueue(Event event, SkriptQueue queue) { if (queue == null) return null; Integer startIndex = 0, endIndex = 0; @@ -242,11 +242,13 @@ private Iterator transformIterator(Event event, @Nullable Iterator ite return switch (type) { case FIRST_ELEMENT -> CollectionUtils.array((T) queue.pollFirst()); case LAST_ELEMENT -> CollectionUtils.array((T) queue.pollLast()); - case RANDOM -> CollectionUtils.array((T) queue.removeSafely(ThreadLocalRandom.current().nextInt(0, queue.size()))); + case RANDOM -> + CollectionUtils.array((T) queue.removeSafely(ThreadLocalRandom.current().nextInt(0, queue.size()))); case ORDINAL -> CollectionUtils.array((T) queue.removeSafely(startIndex - 1)); case TAIL_END_ORDINAL -> CollectionUtils.array((T) queue.removeSafely(queue.size() - startIndex)); case FIRST_X_ELEMENTS -> CollectionUtils.array((T[]) queue.removeRangeSafely(0, startIndex)); - case LAST_X_ELEMENTS -> CollectionUtils.array((T[]) queue.removeRangeSafely(queue.size() - startIndex, queue.size())); + case LAST_X_ELEMENTS -> + CollectionUtils.array((T[]) queue.removeRangeSafely(queue.size() - startIndex, queue.size())); case RANGE -> { boolean reverse = startIndex > endIndex; T[] elements = CollectionUtils.array((T[]) queue.removeRangeSafely(Math.min(startIndex, endIndex) - 1, Math.max(startIndex, endIndex))); @@ -284,7 +286,6 @@ public Expression getConvertedExpression(Class... to) { exprElement.startIndex = startIndex; exprElement.endIndex = endIndex; exprElement.type = type; - exprElement.queue = queue; return exprElement; } @@ -295,37 +296,33 @@ public boolean isSingle() { @Override public Class getReturnType() { - if (queue) - return (Class) Object.class; - return expr.getReturnType(); + Class returnType = expr.getReturnType(); + //noinspection unchecked + return returnType == SkriptQueue.class ? (Class) Object.class : returnType; } @Override public Class[] possibleReturnTypes() { - if (!queue) { - return expr.possibleReturnTypes(); - } - return super.possibleReturnTypes(); + Class returnType = expr.getReturnType(); + //noinspection unchecked + return returnType == SkriptQueue.class ? new Class[]{Object.class} : expr.possibleReturnTypes(); } @Override public boolean canReturn(Class returnType) { - if (!queue) { - return expr.canReturn(returnType); - } - return super.canReturn(returnType); + return returnType == SkriptQueue.class || expr.canReturn(returnType); } - - @Override + + @Override public Expression simplify() { - if (!queue && expr instanceof Literal - && type != ElementType.RANDOM - && (startIndex == null || startIndex instanceof Literal) - && (endIndex == null || endIndex instanceof Literal)) { + if (expr instanceof Literal + && type != ElementType.RANDOM + && (startIndex == null || startIndex instanceof Literal) + && (endIndex == null || endIndex instanceof Literal)) { return SimplifiedLiteral.fromExpression(this); } return this; - } + } @Override public String toString(@Nullable Event event, boolean debug) { diff --git a/src/test/skript/tests/regressions/8548-first queue element.sk b/src/test/skript/tests/regressions/8548-first queue element.sk new file mode 100644 index 000000000..c6ac89ac4 --- /dev/null +++ b/src/test/skript/tests/regressions/8548-first queue element.sk @@ -0,0 +1,7 @@ +using queues + +test "8548 first queue element": + set {queue} to a new queue + add "hello" and "there" to {queue} + assert the first element of {queue} is "hello" + assert the first element of {queue} is "there" From 1c240a729d14590f2c9d59a96a243e366ccbe0da Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Wed, 27 May 2026 11:22:53 -0400 Subject: [PATCH 08/19] Use MM formatting for ItemData stringification (#8653) --- src/main/java/ch/njol/skript/aliases/ItemData.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/ch/njol/skript/aliases/ItemData.java b/src/main/java/ch/njol/skript/aliases/ItemData.java index 71ce976fa..e4fe62aae 100644 --- a/src/main/java/ch/njol/skript/aliases/ItemData.java +++ b/src/main/java/ch/njol/skript/aliases/ItemData.java @@ -23,6 +23,7 @@ import org.bukkit.inventory.meta.PotionMeta; import org.bukkit.potion.PotionEffect; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.text.TextComponentParser; import java.io.IOException; import java.io.NotSerializableException; @@ -242,8 +243,10 @@ public String toString(final boolean debug, final boolean plural) { StringBuilder builder = new StringBuilder(Aliases.getMaterialName(this, plural)); ItemMeta meta = stack != null ? stack.getItemMeta() : null; if (meta != null && meta.hasDisplayName()) { - builder.append(" ").append(m_named).append(" "); - builder.append(meta.getDisplayName()); + builder.append(" ") + .append(m_named) + .append(" ") + .append(TextComponentParser.instance().toString(meta.displayName())); } return builder.toString(); } From 9a51b63c7e631dbf0fdc02706ac48936cfc99c58 Mon Sep 17 00:00:00 2001 From: Shane Bee Date: Wed, 27 May 2026 08:34:07 -0700 Subject: [PATCH 09/19] LocationClassInfo - fix null world error (#8664) --- .../bukkit/types/LocationClassInfo.java | 5 ++-- .../skript/tests/bukkit/types/Location.sk | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 src/test/skript/tests/bukkit/types/Location.sk diff --git a/src/main/java/org/skriptlang/skript/bukkit/types/LocationClassInfo.java b/src/main/java/org/skriptlang/skript/bukkit/types/LocationClassInfo.java index 70f42a386..c971f8d22 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/types/LocationClassInfo.java +++ b/src/main/java/org/skriptlang/skript/bukkit/types/LocationClassInfo.java @@ -126,13 +126,14 @@ public boolean canParse(ParseContext context) { @Override public String toString(Location loc, int flags) { - String worldPart = loc.getWorld() == null ? "" : " in '" + loc.getWorld().getName() + "'"; // Safety: getWorld is marked as Nullable by spigot + String worldPart = loc.getWorld() == null ? "" : " in '" + loc.getWorld().getName() + "'"; // Safety: getWorld is marked as Nullable by Paper return "x: " + Skript.toString(loc.getX()) + ", y: " + Skript.toString(loc.getY()) + ", z: " + Skript.toString(loc.getZ()) + ", yaw: " + Skript.toString(loc.getYaw()) + ", pitch: " + Skript.toString(loc.getPitch()) + worldPart; } @Override public String toVariableNameString(Location loc) { - return loc.getWorld().getName() + ":" + loc.getX() + "," + loc.getY() + "," + loc.getZ(); + String worldPart = loc.getWorld() == null ? "" : loc.getWorld().getName() + ":"; // Safety: getWorld is marked as Nullable by Paper + return worldPart + loc.getX() + "," + loc.getY() + "," + loc.getZ(); } @Override diff --git a/src/test/skript/tests/bukkit/types/Location.sk b/src/test/skript/tests/bukkit/types/Location.sk new file mode 100644 index 000000000..5cc31d351 --- /dev/null +++ b/src/test/skript/tests/bukkit/types/Location.sk @@ -0,0 +1,24 @@ +test "Location ClassInfo": + set {_loc} to location(1,1,1, world("world")) + assert {_loc} is set with "Location variable should be set" + + set {_string} to "%{_loc}%" + assert {_string} is set with "Location should be converted to string" + assert {_string} = "x: 1, y: 1, z: 1, yaw: 0, pitch: 0 in 'world'" with "Location should properly be converted to string" + + set {_var::%{_loc}%} to 1 + assert {_var::%{_loc}%} is set with "Location should be able to be used in a variable as an index" + + delete {_loc} + delete {_string} + + set {_loc} to location(1,1,1, {_null_world}) + assert {_loc} is set with "Location variable should be set" + + set {_string} to "%{_loc}%" + assert {_string} is set with "Location should be converted to string" + assert {_string} = "x: 1, y: 1, z: 1, yaw: 0, pitch: 0" with "Location should properly be converted to string" + + set {_var::%{_loc}%} to 1 + # Reference issue 8661 + assert {_var::%{_loc}%} is set with "Location without a world should be able to be used in a variable as an index" From 1911b42e2f6a1b81335b53a4089422f8b4f59938 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Wed, 27 May 2026 16:49:39 -0400 Subject: [PATCH 10/19] Update API to 26.1.2 (#8631) --- .github/workflows/java-25-builds.yml | 2 +- .github/workflows/junit-25-builds.yml | 2 +- build.gradle | 2 +- gradle.properties | 2 +- .../java25/{paper-26.1.1.json => paper-26.1.2.json} | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) rename src/test/skript/environments/java25/{paper-26.1.1.json => paper-26.1.2.json} (85%) diff --git a/.github/workflows/java-25-builds.yml b/.github/workflows/java-25-builds.yml index e7f66faa3..ced274832 100644 --- a/.github/workflows/java-25-builds.yml +++ b/.github/workflows/java-25-builds.yml @@ -13,7 +13,7 @@ jobs: if: "! contains(toJSON(github.event.commits.*.message), '[ci skip]')" uses: ./.github/workflows/parallelize-tests.yml with: - environments: 26.1.1 + environments: 26.1.2 java_version: 25 parallel_jobs: 3 diff --git a/.github/workflows/junit-25-builds.yml b/.github/workflows/junit-25-builds.yml index a50c45b20..33728ac77 100644 --- a/.github/workflows/junit-25-builds.yml +++ b/.github/workflows/junit-25-builds.yml @@ -13,7 +13,7 @@ jobs: if: "! contains(toJSON(github.event.commits.*.message), '[ci skip]')" uses: ./.github/workflows/parallelize-tests.yml with: - environments: 26.1.1 + environments: 26.1.2 java_version: 25 parallel_jobs: 3 diff --git a/build.gradle b/build.gradle index 7dcc91a75..cb6f8c29a 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ dependencies { shadow group: 'io.papermc', name: 'paperlib', version: '1.0.8' shadow group: 'org.bstats', name: 'bstats-bukkit', version: '3.1.0' - implementation group: 'io.papermc.paper', name: 'paper-api', version: '26.1.1.build.+' + implementation group: 'io.papermc.paper', name: 'paper-api', version: '26.1.2.build.+' implementation group: 'com.google.code.findbugs', name: 'findbugs', version: '3.0.1' // bundled with Minecraft 1.19.4+ for display entity transforms diff --git a/gradle.properties b/gradle.properties index e37d84156..450609ece 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,5 +7,5 @@ groupid=ch.njol name=skript version=2.15.2 jarName=Skript.jar -testEnv=java25/paper-26.1.1 +testEnv=java25/paper-26.1.2 testEnvJavaVersion=25 diff --git a/src/test/skript/environments/java25/paper-26.1.1.json b/src/test/skript/environments/java25/paper-26.1.2.json similarity index 85% rename from src/test/skript/environments/java25/paper-26.1.1.json rename to src/test/skript/environments/java25/paper-26.1.2.json index 826fbaab7..45f921d85 100644 --- a/src/test/skript/environments/java25/paper-26.1.1.json +++ b/src/test/skript/environments/java25/paper-26.1.2.json @@ -1,11 +1,11 @@ { - "name": "paper-26.1.1", + "name": "paper-26.1.2", "resources": [ {"source": "server.properties.generic", "target": "server.properties"} ], "paperDownloads": [ { - "version": "26.1.1", + "version": "26.1.2", "target": "paperclip.jar" } ], From 187e57877d9e928a1677168b9c17dacc69b38670 Mon Sep 17 00:00:00 2001 From: Xeiji Date: Thu, 28 May 2026 05:41:01 +0800 Subject: [PATCH 11/19] Fix ExprHash internal error when MD5 is used (#8609) --- src/main/java/ch/njol/skript/expressions/ExprHash.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/ch/njol/skript/expressions/ExprHash.java b/src/main/java/ch/njol/skript/expressions/ExprHash.java index 34aec0aef..cbca80c93 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprHash.java +++ b/src/main/java/ch/njol/skript/expressions/ExprHash.java @@ -7,6 +7,7 @@ import java.util.Locale; import ch.njol.skript.doc.*; +import ch.njol.skript.lang.parser.ParserInstance; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; @@ -51,7 +52,8 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye String algorithm = parseResult.tags.get(0).toUpperCase(Locale.ENGLISH); try { digest = MessageDigest.getInstance(algorithm); - if (algorithm.equals("MD5") && !getParser().getCurrentScript().suppressesWarning(ScriptWarning.DEPRECATED_SYNTAX)) { + ParserInstance parser = getParser(); + if (algorithm.equals("MD5") && parser.isActive() && !parser.getCurrentScript().suppressesWarning(ScriptWarning.DEPRECATED_SYNTAX)) { Skript.warning("MD5 is not secure and shouldn't be used if a cryptographically secure hashing algorithm is required."); } return true; From ae51368f350a732dfc1710d9f3b77832c201d45b Mon Sep 17 00:00:00 2001 From: Eren <67760502+erenkarakal@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:48:54 +0300 Subject: [PATCH 12/19] Fix TestLogHandler Exception (#8662) --- src/main/java/ch/njol/skript/log/TestingLogHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/ch/njol/skript/log/TestingLogHandler.java b/src/main/java/ch/njol/skript/log/TestingLogHandler.java index 08a79c55f..e0974c74a 100644 --- a/src/main/java/ch/njol/skript/log/TestingLogHandler.java +++ b/src/main/java/ch/njol/skript/log/TestingLogHandler.java @@ -32,7 +32,7 @@ public LogResult log(LogEntry entry) { String name = struct instanceof EvtTestCase test ? test.getTestName() : struct != null ? struct.getSyntaxTypeName() : null; TestTracker.parsingStarted(name); - if (node != null) { + if (node != null && parser.isActive()) { TestTracker.testFailed(entry.getMessage(), parser.getCurrentScript(), node.getLine()); } else { TestTracker.testFailed(entry.getMessage()); From 0738890c0a73d925e012d3ec143088d3ecc40737 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 1 Jun 2026 11:05:00 -0400 Subject: [PATCH 13/19] Fix EffKick, EffBan component support (#8628) --- .../java/ch/njol/skript/effects/EffBan.java | 141 ---------------- .../java/ch/njol/skript/effects/EffKick.java | 70 -------- .../bukkit/entity/player/PlayerModule.java | 3 + .../player/elements/effects/EffBan.java | 154 ++++++++++++++++++ .../player/elements/effects/EffKick.java | 80 +++++++++ 5 files changed, 237 insertions(+), 211 deletions(-) delete mode 100644 src/main/java/ch/njol/skript/effects/EffBan.java delete mode 100644 src/main/java/ch/njol/skript/effects/EffKick.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/entity/player/elements/effects/EffBan.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/entity/player/elements/effects/EffKick.java diff --git a/src/main/java/ch/njol/skript/effects/EffBan.java b/src/main/java/ch/njol/skript/effects/EffBan.java deleted file mode 100644 index facf92c2e..000000000 --- a/src/main/java/ch/njol/skript/effects/EffBan.java +++ /dev/null @@ -1,141 +0,0 @@ -package ch.njol.skript.effects; - -import ch.njol.skript.Skript; -import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Example; -import ch.njol.skript.doc.Name; -import ch.njol.skript.doc.Since; -import ch.njol.skript.lang.Effect; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.SyntaxStringBuilder; -import ch.njol.skript.util.Timespan; -import ch.njol.util.Kleenean; -import org.bukkit.BanList; -import org.bukkit.Bukkit; -import org.bukkit.OfflinePlayer; -import org.bukkit.entity.Player; -import org.bukkit.event.Event; -import org.jetbrains.annotations.Nullable; - -import java.net.InetSocketAddress; -import java.util.Date; - -@Name("Ban") -@Description({"Bans or unbans a player or an IP address.", - "If a reason is given, it will be shown to the player when they try to join the server while banned.", - "A length of ban may also be given to apply a temporary ban. If it is absent for any reason, a permanent ban will be used instead.", - "We recommend that you test your scripts so that no accidental permanent bans are applied.", - "", - "Note that banning people does not kick them from the server.", - "You can optionally use 'and kick' or consider using the kick effect after applying a ban."}) -@Example("unban player") -@Example("ban \"127.0.0.1\"") -@Example("IP-ban the player because \"he is an idiot\"") -@Example("ban player due to \"inappropriate language\" for 2 days") -@Example("ban and kick player due to \"inappropriate language\" for 2 days") -@Since("1.4, 2.1.1 (ban reason), 2.5 (timespan), 2.9.0 (kick)") -public class EffBan extends Effect { - - static { - Skript.registerEffect(EffBan.class, - "ban [kick:and kick] %strings/offlineplayers% [(by reason of|because [of]|on account of|due to) %-string%] [for %-timespan%]", - "unban %strings/offlineplayers%", - "ban [kick:and kick] %players% by IP [(by reason of|because [of]|on account of|due to) %-string%] [for %-timespan%]", - "unban %players% by IP", - "IP(-| )ban [kick:and kick] %players% [(by reason of|because [of]|on account of|due to) %-string%] [for %-timespan%]", - "(IP(-| )unban|un[-]IP[-]ban) %players%"); - } - - @SuppressWarnings("null") - private Expression players; - @Nullable - private Expression reason; - @Nullable - private Expression expires; - - private boolean ban; - private boolean ipBan; - private boolean kick; - - @SuppressWarnings({"null", "unchecked"}) - @Override - public boolean init(final Expression[] exprs, final int matchedPattern, final Kleenean isDelayed, final ParseResult parseResult) { - players = exprs[0]; - reason = exprs.length > 1 ? (Expression) exprs[1] : null; - expires = exprs.length > 1 ? (Expression) exprs[2] : null; - ban = matchedPattern % 2 == 0; - ipBan = matchedPattern >= 2; - kick = parseResult.hasTag("kick"); - return true; - } - - @SuppressWarnings("null") - @Override - protected void execute(final Event e) { - final String reason = this.reason != null ? this.reason.getSingle(e) : null; // don't check for null, just ignore an invalid reason - Timespan ts = this.expires != null ? this.expires.getSingle(e) : null; - final Date expires = ts != null ? new Date(System.currentTimeMillis() + ts.getAs(Timespan.TimePeriod.MILLISECOND)) : null; - final String source = "Skript ban effect"; - for (final Object o : players.getArray(e)) { - if (o instanceof Player) { - Player player = (Player) o; - if (ipBan) { - InetSocketAddress addr = player.getAddress(); - if (addr == null) - return; // Can't ban unknown IP - final String ip = addr.getAddress().getHostAddress(); - if (ban) - Bukkit.getBanList(BanList.Type.IP).addBan(ip, reason, expires, source); - else - Bukkit.getBanList(BanList.Type.IP).pardon(ip); - } else { - if (ban) - Bukkit.getBanList(BanList.Type.NAME).addBan(player.getName(), reason, expires, source); // FIXME [UUID] ban UUID - else - Bukkit.getBanList(BanList.Type.NAME).pardon(player.getName()); - } - if (kick) - player.kickPlayer(reason); - } else if (o instanceof OfflinePlayer) { - String name = ((OfflinePlayer) o).getName(); - if (name == null) - return; // Can't ban, name unknown - if (ban) - Bukkit.getBanList(BanList.Type.NAME).addBan(name, reason, expires, source); - else - Bukkit.getBanList(BanList.Type.NAME).pardon(name); - } else if (o instanceof String) { - final String s = (String) o; - if (ban) { - Bukkit.getBanList(BanList.Type.IP).addBan(s, reason, expires, source); - Bukkit.getBanList(BanList.Type.NAME).addBan(s, reason, expires, source); - } else { - Bukkit.getBanList(BanList.Type.IP).pardon(s); - Bukkit.getBanList(BanList.Type.NAME).pardon(s); - } - } else { - assert false; - } - } - } - - @Override - public String toString(@Nullable Event event, boolean debug) { - SyntaxStringBuilder builder = new SyntaxStringBuilder(event, debug); - - if (ipBan) - builder.append("IP"); - builder.append(ban ? "ban" : "unban"); - if (kick) - builder.append("and kick"); - builder.append(players); - if (reason != null) - builder.append("on account of", reason); - if (expires != null) - builder.append("for", expires); - - return builder.toString(); - } - -} diff --git a/src/main/java/ch/njol/skript/effects/EffKick.java b/src/main/java/ch/njol/skript/effects/EffKick.java deleted file mode 100644 index 14d3a7eaa..000000000 --- a/src/main/java/ch/njol/skript/effects/EffKick.java +++ /dev/null @@ -1,70 +0,0 @@ -package ch.njol.skript.effects; - -import org.bukkit.entity.Player; -import org.bukkit.event.Event; -import org.bukkit.event.player.PlayerKickEvent; -import org.bukkit.event.player.PlayerLoginEvent; -import org.bukkit.event.player.PlayerLoginEvent.Result; -import org.jetbrains.annotations.Nullable; - -import ch.njol.skript.Skript; -import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Example; -import ch.njol.skript.doc.Name; -import ch.njol.skript.doc.Since; -import ch.njol.skript.lang.Effect; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.util.Kleenean; - -/** - * @author Peter Güttinger - */ -@Name("Kick") -@Description("Kicks a player from the server.") -@Example(""" - on place of TNT, lava, or obsidian: - kick the player due to "You may not place %block%!" - cancel the event - """) -@Since("1.0") -public class EffKick extends Effect { - static { - Skript.registerEffect(EffKick.class, "kick %players% [(by reason of|because [of]|on account of|due to) %-string%]"); - } - - @SuppressWarnings("null") - private Expression players; - @Nullable - private Expression reason; - - @SuppressWarnings({"unchecked", "null"}) - @Override - public boolean init(final Expression[] exprs, final int matchedPattern, final Kleenean isDelayed, final ParseResult parseResult) { - players = (Expression) exprs[0]; - reason = (Expression) exprs[1]; - return true; - } - - @Override - public String toString(final @Nullable Event e, final boolean debug) { - return "kick " + players.toString(e, debug) + (reason != null ? " on account of " + reason.toString(e, debug) : ""); - } - - @Override - protected void execute(final Event e) { - final String r = reason != null ? reason.getSingle(e) : ""; - if (r == null) - return; - for (final Player p : players.getArray(e)) { - if (e instanceof PlayerLoginEvent && p.equals(((PlayerLoginEvent) e).getPlayer()) && !Delay.isDelayed(e)) { - ((PlayerLoginEvent) e).disallow(Result.KICK_OTHER, r); - } else if (e instanceof PlayerKickEvent && p.equals(((PlayerKickEvent) e).getPlayer()) && !Delay.isDelayed(e)) { - ((PlayerKickEvent) e).setLeaveMessage(r); - } else { - p.kickPlayer(r); - } - } - } - -} diff --git a/src/main/java/org/skriptlang/skript/bukkit/entity/player/PlayerModule.java b/src/main/java/org/skriptlang/skript/bukkit/entity/player/PlayerModule.java index 938a676b5..74bf611cb 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/entity/player/PlayerModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/entity/player/PlayerModule.java @@ -6,6 +6,7 @@ import org.skriptlang.skript.addon.AddonModule; import org.skriptlang.skript.addon.HierarchicalAddonModule; import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.bukkit.entity.player.elements.effects.*; import org.skriptlang.skript.bukkit.entity.player.elements.events.*; import org.skriptlang.skript.bukkit.entity.player.elements.expressions.*; import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos; @@ -20,6 +21,8 @@ public PlayerModule(AddonModule parentModule) { @Override protected void loadSelf(SkriptAddon addon) { register(addon, + EffBan::register, + EffKick::register, ExprChatFormat::register, ExprChatMessage::register, ExprChatRecipients::register, diff --git a/src/main/java/org/skriptlang/skript/bukkit/entity/player/elements/effects/EffBan.java b/src/main/java/org/skriptlang/skript/bukkit/entity/player/elements/effects/EffBan.java new file mode 100644 index 000000000..c9d72e214 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/entity/player/elements/effects/EffBan.java @@ -0,0 +1,154 @@ +package org.skriptlang.skript.bukkit.entity.player.elements.effects; + +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.SyntaxStringBuilder; +import ch.njol.skript.util.Timespan; +import ch.njol.skript.util.Timespan.TimePeriod; +import ch.njol.util.Kleenean; +import net.kyori.adventure.text.Component; +import org.bukkit.BanList.Type; +import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.text.TextComponentParser; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +import java.net.InetSocketAddress; +import java.util.Date; + +@Name("Ban") +@Description({"Bans or unbans a player or an IP address.", + "If a reason is given, it will be shown to the player when they try to join the server while banned.", + "A length of ban may also be given to apply a temporary ban. If it is absent for any reason, a permanent ban will be used instead.", + "We recommend that you test your scripts so that no accidental permanent bans are applied.", + "", + "Note that banning people does not kick them from the server.", + "You can optionally use 'and kick' or consider using the kick effect after applying a ban."}) +@Example("unban player") +@Example("ban \"127.0.0.1\"") +@Example("IP-ban the player because \"he is an idiot\"") +@Example("ban player due to \"inappropriate language\" for 2 days") +@Example("ban and kick player due to \"inappropriate language\" for 2 days") +@Since("1.4, 2.1.1 (ban reason), 2.5 (timespan), 2.9.0 (kick)") +public class EffBan extends Effect { + + private static final String SKRIPT_BAN_SOURCE = "Skript ban effect"; + + public static void register(SyntaxRegistry syntaxRegistry) { + syntaxRegistry.register(SyntaxRegistry.EFFECT, SyntaxInfo.builder(EffBan.class) + .supplier(EffBan::new) + .addPatterns("ban [kick:and kick] %strings/offlineplayers% [(by reason of|because [of]|on account of|due to) %-textcomponent%] [for %-timespan%]", + "unban %strings/offlineplayers%", + "ban [kick:and kick] %players% by IP [(by reason of|because [of]|on account of|due to) %-textcomponent%] [for %-timespan%]", + "unban %players% by IP", + "IP(-| )ban [kick:and kick] %players% [(by reason of|because [of]|on account of|due to) %-textcomponent%] [for %-timespan%]", + "(IP(-| )unban|un[-]IP[-]ban) %players%") + .build()); + } + + private Expression players; + private @Nullable Expression reason; + private @Nullable Expression expires; + + private boolean ban; + private boolean ipBan; + private boolean kick; + + @Override + @SuppressWarnings("unchecked") + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + players = exprs[0]; + reason = exprs.length > 1 ? (Expression) exprs[1] : null; + expires = exprs.length > 1 ? (Expression) exprs[2] : null; + ban = matchedPattern % 2 == 0; + ipBan = matchedPattern >= 2; + kick = parseResult.hasTag("kick"); + return true; + } + + @Override + protected void execute(Event event) { + Component reason = this.reason == null ? null : this.reason.getSingle(event); // don't check for null, just ignore an invalid reason + Timespan duration = this.expires == null ? null : this.expires.getSingle(event); + Date expires = duration == null ? null : new Date(System.currentTimeMillis() + duration.getAs(TimePeriod.MILLISECOND)); + for (Object object : players.getArray(event)) { + switch (object) { + case Player player -> { + if (ipBan) { + InetSocketAddress address = player.getAddress(); + if (address == null) { // Can't ban unknown IP + return; + } + String ip = address.getAddress().getHostAddress(); + var banList = Bukkit.getBanList(Type.IP); + if (ban) { + String legacyReason = TextComponentParser.instance().toLegacyString(reason); + banList.addBan(ip, legacyReason, expires, SKRIPT_BAN_SOURCE); + } else { + banList.pardon(ip); + } + } else { + var banList = Bukkit.getBanList(Type.NAME); + if (ban) { + String legacyReason = TextComponentParser.instance().toLegacyString(reason); + banList.addBan(player.getName(), legacyReason, expires, SKRIPT_BAN_SOURCE); // FIXME [UUID] ban UUID + } else { + banList.pardon(player.getName()); + } + } + if (kick) { + player.kick(reason); + } + } + case OfflinePlayer offlinePlayer -> { + String name = offlinePlayer.getName(); + if (name == null) { // Can't ban, name unknown + return; + } + var banList = Bukkit.getBanList(Type.NAME); + if (ban) { + String legacyReason = TextComponentParser.instance().toLegacyString(reason); + banList.addBan(name, legacyReason, expires, SKRIPT_BAN_SOURCE); + } else { + banList.pardon(name); + } + } + case String ip -> { + var ipBanList = Bukkit.getBanList(Type.IP); + var nameBanList = Bukkit.getBanList(Type.NAME); + if (ban) { + String legacyReason = TextComponentParser.instance().toLegacyString(reason); + ipBanList.addBan(ip, legacyReason, expires, SKRIPT_BAN_SOURCE); + nameBanList.addBan(ip, legacyReason, expires, SKRIPT_BAN_SOURCE); + } else { + ipBanList.pardon(ip); + nameBanList.pardon(ip); + } + } + default -> throw new IllegalStateException("Unexpected value: " + object); + } + } + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return new SyntaxStringBuilder(event, debug) + .appendIf(ipBan, "IP") + .append(ban ? "ban" : "unban") + .appendIf(kick, "and kick") + .append(players) + .appendIf(reason != null, "on account of", reason) + .appendIf(expires != null, "for", expires) + .toString(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/entity/player/elements/effects/EffKick.java b/src/main/java/org/skriptlang/skript/bukkit/entity/player/elements/effects/EffKick.java new file mode 100644 index 000000000..8e85ced94 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/entity/player/elements/effects/EffKick.java @@ -0,0 +1,80 @@ +package org.skriptlang.skript.bukkit.entity.player.elements.effects; + +import ch.njol.skript.effects.Delay; +import ch.njol.skript.lang.SyntaxStringBuilder; +import net.kyori.adventure.text.Component; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.player.PlayerKickEvent; +import org.bukkit.event.player.PlayerLoginEvent; +import org.jetbrains.annotations.Nullable; + +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +@Name("Kick") +@Description("Kicks a player from the server.") +@Example(""" + on place of TNT, lava, or obsidian: + kick the player due to "You may not place %block%!" + cancel the event + """) +@Since("1.0") +public class EffKick extends Effect { + + public static void register(SyntaxRegistry syntaxRegistry) { + syntaxRegistry.register(SyntaxRegistry.EFFECT, SyntaxInfo.builder(EffKick.class) + .supplier(EffKick::new) + .addPattern("kick %players% [(by reason of|because [of]|on account of|due to) %-textcomponent%]") + .build()); + } + + private Expression players; + private @Nullable Expression reason; + + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + //noinspection unchecked + players = (Expression) exprs[0]; + //noinspection unchecked + reason = (Expression) exprs[1]; + return true; + } + + @Override + protected void execute(Event event) { + Component reason = this.reason == null ? Component.empty() : this.reason.getSingle(event); + if (reason == null) { + return; + } + for (Player player : players.getArray(event)) { + if (!Delay.isDelayed(event)) { // handle event specific cases + if (event instanceof PlayerLoginEvent loginEvent && player.equals(loginEvent.getPlayer())) { + loginEvent.disallow(PlayerLoginEvent.Result.KICK_OTHER, reason); + return; + } else if (event instanceof PlayerKickEvent kickEvent && player.equals(kickEvent.getPlayer())) { + kickEvent.leaveMessage(reason); + return; + } + } + player.kick(reason); + } + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return new SyntaxStringBuilder(event, debug) + .append("kick", players) + .appendIf(reason != null, "on account of", reason) + .toString(); + } + +} From db05a2c34e873b224b7bd0af9c415ae5c9ee6d29 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 11:15:54 -0400 Subject: [PATCH 14/19] Bump actions/upload-artifact from 6 to 7 (#8476) --- .github/workflows/checkstyle.yml | 2 +- .github/workflows/java-21-builds.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/checkstyle.yml b/.github/workflows/checkstyle.yml index aaca8c90b..5e6eb1b29 100644 --- a/.github/workflows/checkstyle.yml +++ b/.github/workflows/checkstyle.yml @@ -28,7 +28,7 @@ jobs: - name: Run checkstyle run: ./gradlew clean checkstyleMain - name: Upload checkstyle report - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: success() with: name: checkstyle-report diff --git a/.github/workflows/java-21-builds.yml b/.github/workflows/java-21-builds.yml index 8c4ab3c6d..bba3db361 100644 --- a/.github/workflows/java-21-builds.yml +++ b/.github/workflows/java-21-builds.yml @@ -40,7 +40,7 @@ jobs: - name: Build Skript and run test scripts run: ./gradlew clean customTest -PtestEnvs="${{ matrix.envs }}" -PtestEnvJavaVersion=21 - name: Upload Nightly Build - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: success() && matrix.id == 1 with: name: skript-nightly From e266c13af42c3ea75db6f0307c72958e0b848cd8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 Jun 2026 11:24:00 -0400 Subject: [PATCH 15/19] Bump org.bstats:bstats-bukkit from 3.1.0 to 3.2.1 (#8477) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index cb6f8c29a..0c3c83f6a 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,7 @@ allprojects { dependencies { shadow group: 'io.papermc', name: 'paperlib', version: '1.0.8' - shadow group: 'org.bstats', name: 'bstats-bukkit', version: '3.1.0' + shadow group: 'org.bstats', name: 'bstats-bukkit', version: '3.2.1' implementation group: 'io.papermc.paper', name: 'paper-api', version: '26.1.2.build.+' implementation group: 'com.google.code.findbugs', name: 'findbugs', version: '3.0.1' From 03aa40c43f7422754eb4bbacedf66f97acfe5c69 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 1 Jun 2026 12:10:08 -0400 Subject: [PATCH 16/19] Add missing 26.1.2 lang entries (#8671) --- src/main/resources/lang/default.lang | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/resources/lang/default.lang b/src/main/resources/lang/default.lang index 540fa9cfb..7a9a6cb71 100644 --- a/src/main/resources/lang/default.lang +++ b/src/main/resources/lang/default.lang @@ -2464,10 +2464,12 @@ particle: note: note particle¦s @a ominous_spawning: ominous spawning particle¦s @an pale_oak_leaves: pale oak leaves particle¦s @a + pause_mob_growth: pause mob growth particle¦s @a poof: poof particle¦s @a portal: portal particle¦s @a raid_omen: raid omen particle¦s @a rain: rain particle¦s @a + reset_mob_growth: reset mob growth particle¦s @a reverse_portal: reverse portal particle¦s @a scrape: scrape particle¦s @a sculk_charge: sculk charge particle¦s @a @@ -2521,6 +2523,7 @@ teleport flags: unleash reasons: distance: distance holder_gone: holder (gone|disappeared) + leashed_gone: leashed (gone|disappeared) player_unleash: player unleash, player unleashed, unleashed by player unknown: unknown From 4d5dbf971d2a8fa6d91b7920b16c7dd82e598086 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 1 Jun 2026 14:44:41 -0400 Subject: [PATCH 17/19] Fix FunctionReference Invalidation Process (#8667) --- .../skript/structures/StructFunction.java | 2 +- .../common/function/FunctionReference.java | 63 ++++++++++++------- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/main/java/ch/njol/skript/structures/StructFunction.java b/src/main/java/ch/njol/skript/structures/StructFunction.java index dda314d6d..0e3a3740e 100644 --- a/src/main/java/ch/njol/skript/structures/StructFunction.java +++ b/src/main/java/ch/njol/skript/structures/StructFunction.java @@ -157,7 +157,7 @@ public boolean postLoad() { public void unload() { assert signature != null; Functions.unregisterFunction(signature); - signature.calls().forEach(FunctionReference::invalidateCache); + signature.calls().forEach(FunctionReference::invalidate); VALIDATE_FUNCTIONS.set(true); } diff --git a/src/main/java/org/skriptlang/skript/common/function/FunctionReference.java b/src/main/java/org/skriptlang/skript/common/function/FunctionReference.java index 94514f17a..9bf0e0a0b 100644 --- a/src/main/java/org/skriptlang/skript/common/function/FunctionReference.java +++ b/src/main/java/org/skriptlang/skript/common/function/FunctionReference.java @@ -13,6 +13,7 @@ import com.google.common.base.Preconditions; import org.bukkit.Bukkit; import org.bukkit.event.Event; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.common.function.FunctionReferenceParser.EmptyExpression; @@ -29,10 +30,11 @@ public final class FunctionReference implements Debuggable { private final String namespace; private final String name; - private final Signature signature; private final Argument>[] arguments; - private boolean unloaded = false; + private Signature cachedSignature; + private boolean validSignature = true; + private boolean printedInvalidSignatureWarning; private Function cachedFunction; private LinkedHashMap cachedArguments; @@ -50,31 +52,42 @@ public FunctionReference(@Nullable String namespace, this.namespace = namespace; this.name = name; - this.signature = signature; + this.cachedSignature = signature; this.arguments = arguments; } - /** - * Invalidate the cached function used in this reference. - */ - public void invalidateCache() { - unloaded = true; - } - /** * Validates this function reference. * * @return True if this is a valid function reference, false if not. */ public boolean validate() { - if (signature == null) { - return false; + if (!validSignature) { + Class[] parameters = Arrays.stream(cachedSignature.parameters().all()) + .map(Parameter::type) + .toArray(Class[]::new); + var result = FunctionRegistry.getRegistry().getSignature(namespace, name, parameters); + if (result.result() == RetrievalResult.EXACT) { + //noinspection unchecked + cachedSignature = (Signature) result.retrieved(); + validSignature = true; + printedInvalidSignatureWarning = false; + cachedFunction = null; // need to re-obtain function now since signature changed + } else { + if (!printedInvalidSignatureWarning) { + printedInvalidSignatureWarning = true; + Skript.warning(String.format("The function '%s' from the script '%s' no longer exists." + + " Skript will continue to use the old function until this function is registered again." + + " Function call: %s", + name, namespace, toString(null, false))); + } + } } if (cachedArguments == null) { cachedArguments = new LinkedHashMap<>(); - Parameter[] targets = signature.parameters().all(); + Parameter[] targets = cachedSignature.parameters().all(); for (int i = 0; i < arguments.length; i++) { Argument> argument = arguments[i]; Parameter target = targets[i]; @@ -102,7 +115,7 @@ public boolean validate() { } } - signature.addCall(this); + cachedSignature.addCall(this); return true; } @@ -128,6 +141,11 @@ private boolean validateArgument(Parameter target, Expression original, Ex return true; } + @ApiStatus.Internal + public void invalidate() { + validSignature = false; + } + private String getName(Class clazz, boolean single) { if (single) { return Classes.getSuperClassInfo(clazz).getName().getSingular(); @@ -163,6 +181,10 @@ public T execute(Event event) { }); Function function = function(); + if (function == null) { // probably shouldn't be possible? + Skript.error("Failed to obtain function for call: %s", toString(null, false)); + return null; + } FunctionEvent fnEvent = new FunctionEvent<>(function); if (Functions.callFunctionEvents) @@ -226,8 +248,8 @@ private KeyedValue[] evaluateParameter(Expression argument, Event event) { * @return The function referred to by this reference. */ public Function function() { - if (unloaded || cachedFunction == null) { - Class[] parameters = Arrays.stream(signature.parameters().all()) + if (cachedFunction == null) { + Class[] parameters = Arrays.stream(cachedSignature.parameters().all()) .map(Parameter::type) .toArray(Class[]::new); @@ -236,7 +258,6 @@ public Function function() { if (retrieval.result() == RetrievalResult.EXACT) { //noinspection unchecked cachedFunction = (Function) retrieval.retrieved(); - unloaded = false; } } @@ -247,7 +268,7 @@ public Function function() { * @return The signature belonging to this reference. */ public Signature signature() { - return signature; + return cachedSignature; } /** @@ -275,14 +296,14 @@ public String namespace() { * @return Whether this reference returns a single or multiple values. */ public boolean isSingle() { - if (signature.contract() != null) { + if (cachedSignature.contract() != null) { Expression[] args = Arrays.stream(arguments) .map(it -> it.value) .toArray(Expression[]::new); - return signature.contract().isSingle(args); + return cachedSignature.contract().isSingle(args); } else { - return signature.isSingle(); + return cachedSignature.isSingle(); } } From feeb8145ced3dd54222e212a3a32eabaf872f6a4 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 1 Jun 2026 15:13:08 -0400 Subject: [PATCH 18/19] Fix several additional formatting bugs (#8627) --- .../java/ch/njol/skript/log/LogEntry.java | 4 +- .../bukkit/text/TextComponentParser.java | 20 +++++++--- .../bukkit/text/TextComponentUtils.java | 6 +-- .../skript/bukkit/text/TextModule.java | 2 +- .../text/types/TextComponentClassInfo.java | 38 ++++++++++++------- .../bukkit/text/TextComponentParserTest.java | 6 +++ .../bukkit/text/TextComponentUtilsTest.java | 1 + 7 files changed, 52 insertions(+), 25 deletions(-) diff --git a/src/main/java/ch/njol/skript/log/LogEntry.java b/src/main/java/ch/njol/skript/log/LogEntry.java index 6e068713f..4fa800ee2 100644 --- a/src/main/java/ch/njol/skript/log/LogEntry.java +++ b/src/main/java/ch/njol/skript/log/LogEntry.java @@ -147,8 +147,8 @@ public String toFormattedString() { return String.format(lineInfoMsg, node.getLine(), c.getFileName()) + - String.format(detailsMsg, TextComponentParser.instance().escape(message.replaceAll("§", "&"))) + from + - String.format(lineDetailsMsg, TextComponentParser.instance().escape(node.save().trim().replaceAll("§", "&"))); + String.format(detailsMsg, TextComponentParser.instance().escape(message)) + from + + String.format(lineDetailsMsg, TextComponentParser.instance().escape(node.save().trim())); } private String replaceNewline(String s) { diff --git a/src/main/java/org/skriptlang/skript/bukkit/text/TextComponentParser.java b/src/main/java/org/skriptlang/skript/bukkit/text/TextComponentParser.java index c28f85edb..8877aa382 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/text/TextComponentParser.java +++ b/src/main/java/org/skriptlang/skript/bukkit/text/TextComponentParser.java @@ -1,5 +1,6 @@ package org.skriptlang.skript.bukkit.text; +import ch.njol.skript.Skript; import ch.njol.skript.registrations.Classes; import ch.njol.util.coll.CollectionUtils; import net.kyori.adventure.text.Component; @@ -121,13 +122,13 @@ public TextReplacementConfig textReplacementConfig() { * A pattern for matching double hashtag hex color tags ({@code <##123456>}). * It also matches all preceding backslashes to determine whether the supposed tag is escaped. */ - private static final Pattern LEGACY_DOUBLE_HASHTAG_PATTERN = Pattern.compile("(\\\\*)<(##[a-f0-9]{6})>"); + private static final Pattern LEGACY_DOUBLE_HASHTAG_PATTERN = Pattern.compile("(\\\\*)<(##[a-fA-F0-9]{6})>"); /** * A pattern for matching legacy hex codes ({@code &x&1&2&3&4&5&6}). * It also matches all preceding backslashes to determine whether the supposed tag is escaped. */ - static final Pattern LEGACY_CODE_PATTERN = Pattern.compile("(\\\\*)([&§][a-f0-9klomnr])"); + static final Pattern LEGACY_CODE_PATTERN = Pattern.compile("(\\\\*)([&§][a-fA-F0-9kKlLoOmMnNrR])"); static { INSTANCE = new TextComponentParser(); @@ -436,7 +437,14 @@ private Component parse(Object message, boolean safe) { realMessage = reformatText(realMessage); // parse as component - Component component = safe ? safeParser.deserialize(realMessage) : parser.deserialize(realMessage); + Component component; + try { + component = safe ? safeParser.deserialize(realMessage) : parser.deserialize(realMessage); + } catch (ParsingException e) { + Skript.exception(e, "An error occurred while trying to parse formatting for '" + realMessage + "'." + + " This is likely caused by the presence of legacy formatting characters (such as '§') that Skript could not handle."); + return Component.text(message instanceof String ? (String) message : Classes.toString(message)); + } // replace links based on configuration setting if (linkParseMode != LinkParseMode.DISABLED) { @@ -492,10 +500,12 @@ public String escape(String string) { // legacy compatibility, escape color codes if (string.contains("&") || string.contains("§")) { string = LEGACY_CODE_PATTERN.matcher(string).replaceAll(result -> { + // Even if escaped, MiniMessage will throw an exception for legacy section codes + String group = result.group().replace('§', '&'); if (result.group(1).length() % 2 == 1) { // tag is already escaped - return Matcher.quoteReplacement(result.group()); + return Matcher.quoteReplacement(group); } - return Matcher.quoteReplacement('\\' + result.group()); + return Matcher.quoteReplacement('\\' + group); }); } string = LEGACY_DOUBLE_HASHTAG_PATTERN.matcher(string).replaceAll(result -> { diff --git a/src/main/java/org/skriptlang/skript/bukkit/text/TextComponentUtils.java b/src/main/java/org/skriptlang/skript/bukkit/text/TextComponentUtils.java index 24aa974ea..1d040780b 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/text/TextComponentUtils.java +++ b/src/main/java/org/skriptlang/skript/bukkit/text/TextComponentUtils.java @@ -77,7 +77,7 @@ private static Component appendToLastChild(Component base, Component appendee) { * A pattern for matching standard legacy codes ({@code &1}). * It also matches all preceding backslashes to determine whether the supposed tag is escaped. */ - private static final Pattern LEGACY_HEX_PATTERN = Pattern.compile("[&§]x(?:[&§][a-f0-9]){6}"); + private static final Pattern LEGACY_HEX_PATTERN = Pattern.compile("[&§]x(?:[&§][a-fA-F0-9]){6}"); /** * Replaces all legacy formatting codes in a string with {@link net.kyori.adventure.text.minimessage.MiniMessage} equivalents. @@ -93,7 +93,7 @@ public static String replaceLegacyFormattingCodes(String text) { String hex = result.group(); StringBuilder replacement = new StringBuilder(); replacement.append("<#"); - for (int i = 3; i <= 13; i += 2) { // isolate the specific numbers + for (int i = 3; i <= 13; i += 2) { // isolate the specific numbers/letters replacement.append(hex.charAt(i)); } replacement.append('>'); @@ -108,7 +108,7 @@ public static String replaceLegacyFormattingCodes(String text) { backslashes = backslashes.substring(1); } StringBuilder replacement = new StringBuilder(backslashes); - ChatColor color = ChatColor.getByChar(result.group(2).charAt(1)); + ChatColor color = ChatColor.getByChar(Character.toLowerCase(result.group(2).charAt(1))); assert color != null; replacement.append('<').append(color.asBungee().getName()).append('>'); return Matcher.quoteReplacement(replacement.toString()); diff --git a/src/main/java/org/skriptlang/skript/bukkit/text/TextModule.java b/src/main/java/org/skriptlang/skript/bukkit/text/TextModule.java index cb270e111..ec67d0668 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/text/TextModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/text/TextModule.java @@ -36,7 +36,7 @@ public void initSelf(SkriptAddon addon) { Comparators.registerComparator(Component.class, String.class, (component, string) -> { TextComponentParser parser = TextComponentParser.instance(); String string1 = parser.toString(component); - String string2 = parser.toString(parser.parseSafe(string)); + String string2 = parser.toString(parser.parse(string)); return Comparators.compare(string1, string2); }); Comparators.registerComparator(Component.class, Component.class, (component1, component2) -> { diff --git a/src/main/java/org/skriptlang/skript/bukkit/text/types/TextComponentClassInfo.java b/src/main/java/org/skriptlang/skript/bukkit/text/types/TextComponentClassInfo.java index 21f52c2c3..3645081fb 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/text/types/TextComponentClassInfo.java +++ b/src/main/java/org/skriptlang/skript/bukkit/text/types/TextComponentClassInfo.java @@ -11,7 +11,6 @@ import net.kyori.adventure.text.serializer.json.JSONComponentSerializer; import org.jetbrains.annotations.ApiStatus; import org.skriptlang.skript.addon.SkriptAddon; -import org.skriptlang.skript.bukkit.text.TextComponentParser; import org.skriptlang.skript.lang.properties.Property; import org.skriptlang.skript.lang.properties.handlers.ContainsHandler; @@ -33,19 +32,7 @@ public TextComponentClassInfo(SkriptAddon addon) { .property(Property.CONTAINS, "Components can contain other components.", addon, - new ContainsHandler() { - @Override - public boolean contains(Component container, Component element) { - var parser = org.skriptlang.skript.bukkit.text.TextComponentParser.instance(); - return StringUtils.contains(parser.toString(container), parser.toString(element), SkriptConfig.caseSensitive.value()); - } - - @Override - public Class[] elementTypes() { - //noinspection unchecked - return new Class[]{Component.class}; - } - } + new TextComponentContainsHandler() ); } @@ -96,4 +83,27 @@ protected boolean canBeInstantiated() { } + private static final class TextComponentContainsHandler implements ContainsHandler { + + @Override + public boolean contains(Component container, Object element) { + var parser = org.skriptlang.skript.bukkit.text.TextComponentParser.instance(); + String haystack = parser.toString(container); + String needle; + if (element instanceof Component component) { + needle = parser.toString(component); + } else { // String + needle = parser.toString(parser.parse(element)); + } + return StringUtils.contains(haystack, needle, SkriptConfig.caseSensitive.value()); + } + + @Override + public Class[] elementTypes() { + //noinspection unchecked + return new Class[]{Component.class, String.class}; + } + + } + } diff --git a/src/test/java/org/skriptlang/skript/bukkit/text/TextComponentParserTest.java b/src/test/java/org/skriptlang/skript/bukkit/text/TextComponentParserTest.java index fbae9a5c3..b123ec393 100644 --- a/src/test/java/org/skriptlang/skript/bukkit/text/TextComponentParserTest.java +++ b/src/test/java/org/skriptlang/skript/bukkit/text/TextComponentParserTest.java @@ -65,6 +65,8 @@ public void testLegacy() { for (ChatColor color : ChatColor.values()) { String message = "&" + color.getChar() + "hello"; assertEquals(LegacyComponentSerializer.legacyAmpersand().deserialize(message), parser.parse(message)); + message = "&" + Character.toUpperCase(color.getChar()) + "hello"; + assertEquals(LegacyComponentSerializer.legacyAmpersand().deserialize(message), parser.parse(message)); } } @@ -80,6 +82,10 @@ public void testLegacyDoubleHashtag() { TextComponentParser parser = new TextComponentParser(); assertEquals(parser.parse("<#123456>hello"), parser.parse("<##123456>hello")); assertEquals("\\<##123456>hello", parser.escape("<##123456>hello")); + // test letters in tag + assertEquals(parser.parse("<#ffff11>hello"), parser.parse("<#ffff11>hello")); + assertEquals(parser.parse("<#FFFF11>hello"), parser.parse("<##FFFF11>hello")); + assertEquals(parser.parse("<#ffff11>hello"), parser.parse("<##FFFF11>hello")); } } diff --git a/src/test/java/org/skriptlang/skript/bukkit/text/TextComponentUtilsTest.java b/src/test/java/org/skriptlang/skript/bukkit/text/TextComponentUtilsTest.java index c8bc544c4..73bf34c95 100644 --- a/src/test/java/org/skriptlang/skript/bukkit/text/TextComponentUtilsTest.java +++ b/src/test/java/org/skriptlang/skript/bukkit/text/TextComponentUtilsTest.java @@ -17,6 +17,7 @@ public void testReplaceLegacyFormattingCodes() { assertEquals("\\\\&cHello!", replaceLegacyFormattingCodes("\\\\\\&cHello!")); assertEquals("<#123456>Hello!", replaceLegacyFormattingCodes("&x&1&2&3&4&5&6Hello!")); assertEquals("<#123456>Hello!", replaceLegacyFormattingCodes("&x&1&2&3&4&5&6&cHello!")); + assertEquals("<#FFFF11>Hello!", replaceLegacyFormattingCodes("&x&F&F&F&F&1&1Hello!")); // validate internal metacharacter escaping assertEquals("You have $10", replaceLegacyFormattingCodes("&cYou have $10")); } From 1aed991ad2fcd9c2ac8c95e036e2dde653fc0160 Mon Sep 17 00:00:00 2001 From: Patrick Miller Date: Mon, 1 Jun 2026 15:23:22 -0400 Subject: [PATCH 19/19] Prepare For Release (2.15.3) --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 450609ece..45cda3213 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ org.gradle.parallel=true groupid=ch.njol name=skript -version=2.15.2 +version=2.15.3 jarName=Skript.jar testEnv=java25/paper-26.1.2 testEnvJavaVersion=25