diff --git a/.github/workflows/archive-docs.yml b/.github/workflows/archive-docs.yml
index e183f748d78..c31092b63b0 100644
--- a/.github/workflows/archive-docs.yml
+++ b/.github/workflows/archive-docs.yml
@@ -8,7 +8,6 @@ on:
jobs:
archive-docs:
if: "! contains(toJSON(github.event.commits.*.message), '[ci skip]')"
- needs: release-docs
runs-on: ubuntu-latest
steps:
- name: Configure workflow
diff --git a/.github/workflows/java-11-builds.yml b/.github/workflows/java-11-builds.yml
deleted file mode 100644
index 443aa925d0c..00000000000
--- a/.github/workflows/java-11-builds.yml
+++ /dev/null
@@ -1,35 +0,0 @@
-name: Java 11 CI (MC 1.13-1.16)
-
-on:
- push:
- branches:
- - master
- - 'dev/**'
- pull_request:
-
-jobs:
- build:
- if: "! contains(toJSON(github.event.commits.*.message), '[ci skip]')"
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- with:
- submodules: recursive
- - name: validate gradle wrapper
- uses: gradle/wrapper-validation-action@v2
- - name: Set up JDK 21
- uses: actions/setup-java@v4
- with:
- java-version: '21'
- distribution: 'adopt'
- cache: gradle
- - name: Grant execute permission for gradlew
- run: chmod +x gradlew
- - name: Build Skript and run test scripts
- run: ./gradlew clean skriptTestJava11
- - name: Upload Nightly Build
- uses: actions/upload-artifact@v4
- if: success()
- with:
- name: skript-nightly
- path: build/libs/*
diff --git a/.github/workflows/java-17-builds.yml b/.github/workflows/java-17-builds.yml
index 2ca6d6bde61..43d6458c946 100644
--- a/.github/workflows/java-17-builds.yml
+++ b/.github/workflows/java-17-builds.yml
@@ -1,4 +1,4 @@
-name: Java 17 CI (MC 1.17-1.20.4)
+name: Java 17 CI (MC 1.19.4-1.20.4)
on:
push:
diff --git a/.github/workflows/junit-11-builds.yml b/.github/workflows/junit-11-builds.yml
deleted file mode 100644
index cdf75e4c43a..00000000000
--- a/.github/workflows/junit-11-builds.yml
+++ /dev/null
@@ -1,29 +0,0 @@
-name: JUnit (MC 1.13-1.16)
-
-on:
- push:
- branches:
- - master
- - 'dev/**'
- pull_request:
-
-jobs:
- build:
- if: "! contains(toJSON(github.event.commits.*.message), '[ci skip]')"
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- with:
- submodules: recursive
- - name: validate gradle wrapper
- uses: gradle/wrapper-validation-action@v2
- - name: Set up JDK 21
- uses: actions/setup-java@v4
- with:
- java-version: '21'
- distribution: 'adopt'
- cache: gradle
- - name: Grant execute permission for gradlew
- run: chmod +x gradlew
- - name: Build Skript and run JUnit
- run: ./gradlew clean JUnitJava11
diff --git a/.github/workflows/junit-17-builds.yml b/.github/workflows/junit-17-builds.yml
index 238bd9b132e..7bbe8952eb3 100644
--- a/.github/workflows/junit-17-builds.yml
+++ b/.github/workflows/junit-17-builds.yml
@@ -1,4 +1,4 @@
-name: JUnit (MC 1.17-1.20.4)
+name: JUnit (MC 1.19.4-1.20.4)
on:
push:
diff --git a/README.md b/README.md
index b4ab6c4e48f..bd1e1375345 100644
--- a/README.md
+++ b/README.md
@@ -16,8 +16,8 @@ Skript requires **Spigot** to work. You heard it right, **CraftBukkit** does *no
**Paper**, which is a fork of Spigot, is recommended; it is required for some
parts of Skript to be available.
-Skript supports only the **latest** patch versions of Minecraft 1.13+.
-For example, this means that 1.16.5 is supported, but 1.16.4 is *not*.
+Skript supports only the **latest** patch versions of Minecraft 1.19 and newer.
+For example, this means that 1.19.4 is supported, but 1.19.3 is *not*.
Testing with all old patch versions is not feasible for us.
Minecraft 1.12 and earlier are not, and will not be supported. New Minecraft
@@ -77,15 +77,14 @@ Skript has some tests written in Skript. Running them requires a Minecraft
server, but our build script will create one for you. Running the tests is easy:
```
-./gradlew (quickTest|skriptTest|skriptTestJava11|skriptTestJava17|skriptTestJava21)
+./gradlew (quickTest|skriptTest|skriptTestJava17|skriptTestJava21)
```
quickTest runs the test suite on newest supported server version.
skriptTestJava21 (1.20.6+) runs the tests on Java 21 supported versions.
-skriptTestJava17 (1.17-1.20.4) runs the tests on Java 17 supported versions.
-skriptTestJava11 (1.13-1.16) runs the tests on Java 11 supported versions.
+skriptTestJava17 (1.19.4-1.20.4) runs the tests on Java 17 supported versions.
skriptTest runs the tests on all versions.
-That is, it runs skriptTestJava11, skriptTestJava17, and skriptTestJava21.
+That is, it runs skriptTestJava17, and skriptTestJava21.
By running the tests, you agree to Mojang's End User License Agreement.
diff --git a/build.gradle b/build.gradle
index d6a60fa9e37..415ed5b5709 100644
--- a/build.gradle
+++ b/build.gradle
@@ -29,7 +29,7 @@ dependencies {
shadow group: 'org.bstats', name: 'bstats-bukkit', version: '3.0.2'
shadow group: 'net.kyori', name: 'adventure-text-serializer-bungeecord', version: '4.3.2'
- implementation group: 'io.papermc.paper', name: 'paper-api', version: '1.20.6-R0.1-SNAPSHOT'
+ implementation group: 'io.papermc.paper', name: 'paper-api', version: '1.21-R0.1-SNAPSHOT'
implementation group: 'org.eclipse.jdt', name: 'org.eclipse.jdt.annotation', version: '2.2.700'
implementation group: 'com.google.code.findbugs', name: 'findbugs', version: '3.0.1'
implementation group: 'com.sk89q.worldguard', name: 'worldguard-legacy', version: '7.0.0-SNAPSHOT'
@@ -70,7 +70,7 @@ task build(overwrite: true, type: ShadowJar) {
from sourceSets.main.output
}
-// Excludes the tests for the build task. Should be using junit, junitJava17, junitJava11, skriptTest, quickTest.
+// Excludes the tests for the build task. Should be using junit, junitJava17, skriptTest, quickTest.
// We do not want tests to run for building. That's time consuming and annoying. Especially in development.
test {
exclude '**/*'
@@ -233,11 +233,10 @@ void createTestTask(String name, String desc, String environments, int javaVersi
def java21 = 21
def java17 = 17
-def java11 = 11
-def latestEnv = 'java21/paper-1.20.6.json'
+def latestEnv = 'java21/paper-1.21.0.json'
def latestJava = java21
-def oldestJava = java11
+def oldestJava = java17
def latestJUnitEnv = 'java17/paper-1.20.4.json'
def latestJUnitJava = java17
@@ -260,14 +259,13 @@ int envJava = project.property('testEnvJavaVersion') == null ? latestJava : Inte
createTestTask('quickTest', 'Runs tests on one environment being the latest supported Java and Minecraft.', environments + latestEnv, latestJava, 0)
createTestTask('skriptTestJava21', 'Runs tests on all Java 21 environments.', environments + 'java21', java21, 0)
createTestTask('skriptTestJava17', 'Runs tests on all Java 17 environments.', environments + 'java17', java17, 0)
-createTestTask('skriptTestJava11', 'Runs tests on all Java 11 environments.', environments + 'java11', java11, 0)
createTestTask('skriptTestDev', 'Runs testing server and uses \'system.in\' for command input, stop server to finish.', environments + env, envJava, 0, Modifiers.DEV_MODE, Modifiers.DEBUG)
createTestTask('skriptProfile', 'Starts the testing server with JProfiler support.', environments + latestEnv, latestJava, -1, Modifiers.PROFILE)
createTestTask('genNightlyDocs', 'Generates the Skript documentation website html files.', environments + env, envJava, 0, Modifiers.GEN_NIGHTLY_DOCS)
createTestTask('genReleaseDocs', 'Generates the Skript documentation website html files for a release.', environments + env, envJava, 0, Modifiers.GEN_RELEASE_DOCS)
tasks.register('skriptTest') {
description = 'Runs tests on all environments.'
- dependsOn skriptTestJava11, skriptTestJava17, skriptTestJava21
+ dependsOn skriptTestJava17, skriptTestJava21
}
createTestTask('JUnitQuick', 'Runs JUnit tests on one environment being the latest supported Java and Minecraft.', environments + latestJUnitEnv, latestJUnitJava, 0, Modifiers.JUNIT)
@@ -275,10 +273,9 @@ createTestTask('JUnitQuick', 'Runs JUnit tests on one environment being the late
// However, we are currently using 5.0.1 (see https://github.com/SkriptLang/Skript/pull/6204#discussion_r1405302009)
//createTestTask('JUnitJava21', 'Runs JUnit tests on all Java 21 environments.', environments + 'java21', java21, 0, Modifiers.JUNIT)
createTestTask('JUnitJava17', 'Runs JUnit tests on all Java 17 environments.', environments + 'java17', java17, 0, Modifiers.JUNIT)
-createTestTask('JUnitJava11', 'Runs JUnit tests on all Java 11 environments.', environments + 'java11', java11, 0, Modifiers.JUNIT)
tasks.register('JUnit') {
description = 'Runs JUnit tests on all environments.'
- dependsOn JUnitJava11, JUnitJava17//, JUnitJava21
+ dependsOn JUnitJava17//, JUnitJava21
}
// Build flavor configurations
diff --git a/code-conventions.md b/code-conventions.md
index 3bcf17c6c9f..c9b70f8d5a4 100644
--- a/code-conventions.md
+++ b/code-conventions.md
@@ -194,9 +194,8 @@ Your comments should look something like these:
## Language Features
### Compatibility
-[//]: # (To be updated for 2.10 for Java 17)
-* Contributions should maintain Java 11 source/binary compatibility, even though compiling Skript requires Java 21
- - Users must not need JRE newer than version 11
+* Contributions should maintain Java 17 source/binary compatibility, even though compiling Skript requires Java 21
+ - Users must not need JRE newer than version 17
* Versions up to and including Java 21 should work too
- Please avoid using unsafe reflection
* It is recommended to make fields final, if they are effectively final
diff --git a/gradle.properties b/gradle.properties
index f78ae1766eb..befe897fdfc 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -5,7 +5,7 @@ org.gradle.parallel=true
groupid=ch.njol
name=skript
-version=2.8.7
+version=2.9.1
jarName=Skript.jar
-testEnv=java21/paper-1.20.6
+testEnv=java21/paper-1.21.0
testEnvJavaVersion=21
diff --git a/skript-aliases b/skript-aliases
index 9ea857f6b7d..16949c28e0d 160000
--- a/skript-aliases
+++ b/skript-aliases
@@ -1 +1 @@
-Subproject commit 9ea857f6b7dd1e4fc4a35a88149b9e463b537b06
+Subproject commit 16949c28e0d7bb25ea7c3479c3d6754ff3b5a3e6
diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java
index d98a51a7763..3755525edb1 100644
--- a/src/main/java/ch/njol/skript/Skript.java
+++ b/src/main/java/ch/njol/skript/Skript.java
@@ -752,6 +752,8 @@ protected void afterErrors() {
TestTracker.JUnitTestFailed(test, message);
Skript.exception(failure.getException(), "JUnit test '" + failure.getTestHeader() + " failed.");
});
+ if (SkriptJUnitTest.class.isAssignableFrom(clazz))
+ ((SkriptJUnitTest) clazz.getConstructor().newInstance()).cleanup();
SkriptJUnitTest.clearJUnitTest();
}
} catch (IOException e) {
@@ -759,6 +761,8 @@ protected void afterErrors() {
} catch (ClassNotFoundException e) {
// Should be the Skript test jar gradle task.
assert false : "Class 'ch.njol.skript.variables.FlatFileStorageTest' was not found.";
+ } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) {
+ Skript.exception(e, "Failed to initalize test JUnit classes.");
}
if (ignored > 0)
Skript.warning("There were " + ignored + " ignored test cases! This can mean they are not properly setup in order in that class!");
diff --git a/src/main/java/ch/njol/skript/SkriptConfig.java b/src/main/java/ch/njol/skript/SkriptConfig.java
index 5a3bb954cd4..ee715ce038e 100644
--- a/src/main/java/ch/njol/skript/SkriptConfig.java
+++ b/src/main/java/ch/njol/skript/SkriptConfig.java
@@ -170,6 +170,12 @@ public static String formatDate(final long timestamp) {
}
});
+ /**
+ * Determines whether `on <event>` will be triggered by cancelled events or not.
+ */
+ public static final Option listenCancelledByDefault = new Option<>("listen to cancelled events by default", false)
+ .optional(true);
+
/**
* Maximum number of digits to display after the period for floats and doubles
diff --git a/src/main/java/ch/njol/skript/aliases/Aliases.java b/src/main/java/ch/njol/skript/aliases/Aliases.java
index 56bffc24f9a..111ffc5adcd 100644
--- a/src/main/java/ch/njol/skript/aliases/Aliases.java
+++ b/src/main/java/ch/njol/skript/aliases/Aliases.java
@@ -25,6 +25,7 @@
import ch.njol.skript.config.Node;
import ch.njol.skript.config.SectionNode;
import ch.njol.skript.entity.EntityData;
+import org.bukkit.entity.EntityType;
import org.skriptlang.skript.lang.script.Script;
import ch.njol.skript.lang.parser.ParserInstance;
import ch.njol.skript.localization.ArgsMessage;
@@ -267,7 +268,7 @@ public static ItemType parseItemType(String s) {
}
String lc = s.toLowerCase(Locale.ENGLISH);
- String of = Language.getSpaced("enchantments.of").toLowerCase();
+ String of = Language.getSpaced("of").toLowerCase();
int c = -1;
outer: while ((c = lc.indexOf(of, c + 1)) != -1) {
ItemType t2 = t.clone();
@@ -431,7 +432,6 @@ private static void loadInternal() throws IOException {
Path aliasesPath = zipFs.getPath("/", "aliases-english");
assert aliasesPath != null;
loadDirectory(aliasesPath);
- loadMissingAliases();
}
} catch (URISyntaxException e) {
assert false;
@@ -445,6 +445,9 @@ private static void loadInternal() throws IOException {
assert aliasesFolder != null;
loadDirectory(aliasesFolder);
}
+
+ // generate aliases from item names for any missing items
+ loadMissingAliases();
// Update tracked item types
for (Map.Entry entry : trackedTypes.entrySet()) {
@@ -554,10 +557,16 @@ public static EntityData> getRelatedEntity(ItemData data) {
*
Item types provided by this method are updated when aliases are
* reloaded. However, this also means they are tracked by aliases system
* and NOT necessarily garbage-collected.
+ *
+ *
Relying on this method to create item types is not safe,
+ * as users can change aliases at any point. ItemTypes should instead be created
+ * via {@link Material}s, {@link org.bukkit.Tag}s, or any other manual method.
+ *
* @param name Name of item to search from aliases.
* @return An item.
* @throws IllegalArgumentException When item is not found.
*/
+ @Deprecated(forRemoval = true, since = "2.9.0")
public static ItemType javaItemType(String name) {
ItemType type = parseItemType(name);
if (type == null) {
diff --git a/src/main/java/ch/njol/skript/aliases/AliasesProvider.java b/src/main/java/ch/njol/skript/aliases/AliasesProvider.java
index a3b8b215b75..cce78c4ef71 100644
--- a/src/main/java/ch/njol/skript/aliases/AliasesProvider.java
+++ b/src/main/java/ch/njol/skript/aliases/AliasesProvider.java
@@ -304,16 +304,19 @@ public void addAlias(AliasName name, String id, @Nullable Map ta
}
// Apply (NBT) tags to item stack
- ItemStack stack = new ItemStack(material);
+ ItemStack stack = null;
int itemFlags = 0;
- if (tags != null) {
- itemFlags = applyTags(stack, new HashMap<>(tags));
+ if (material.isItem()) {
+ stack = new ItemStack(material);
+ if (tags != null) {
+ itemFlags = applyTags(stack, new HashMap<>(tags));
+ }
}
// Parse block state to block values
BlockValues blockValues = BlockCompat.INSTANCE.createBlockValues(material, blockStates, stack, itemFlags);
- ItemData data = new ItemData(stack, blockValues);
+ ItemData data = stack != null ? new ItemData(stack, blockValues) : new ItemData(material, blockValues);
data.isAlias = true;
data.itemFlags = itemFlags;
@@ -413,6 +416,7 @@ public EntityData> getRelatedEntity(ItemData item) {
public void clearAliases() {
aliases.clear();
+ materials.clear();
variations.clear();
aliasesMap.clear();
}
diff --git a/src/main/java/ch/njol/skript/aliases/ItemData.java b/src/main/java/ch/njol/skript/aliases/ItemData.java
index 32323c57e6f..f18812aabb8 100644
--- a/src/main/java/ch/njol/skript/aliases/ItemData.java
+++ b/src/main/java/ch/njol/skript/aliases/ItemData.java
@@ -37,7 +37,7 @@
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
-import org.eclipse.jdt.annotation.Nullable;
+import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.NotSerializableException;
@@ -91,11 +91,6 @@ public static class OldItemData {
@Deprecated
public static final boolean itemDataValues = false;
- /**
- * ItemStack, which is used for everything but serialization.
- */
- transient ItemStack stack;
-
/**
* Type of the item as Bukkit material. Serialized manually.
*/
@@ -105,14 +100,18 @@ public static class OldItemData {
* If this represents all possible items.
*/
boolean isAnything;
-
+
+ /**
+ * ItemStack, which is used for everything but serialization.
+ */
+ transient @Nullable ItemStack stack;
+
/**
* When this ItemData represents a block, this contains information to
* allow comparing it against other blocks.
*/
- @Nullable
- BlockValues blockValues;
-
+ @Nullable BlockValues blockValues;
+
/**
* Whether this represents an item (that definitely cannot have
* block states) or a block, which might have them.
@@ -140,9 +139,10 @@ public static class OldItemData {
public ItemData(Material type, @Nullable String tags) {
this.type = type;
-
- this.stack = new ItemStack(type);
- this.blockValues = BlockCompat.INSTANCE.getBlockValues(stack);
+
+ if (type.isItem())
+ this.stack = new ItemStack(type);
+ this.blockValues = BlockCompat.INSTANCE.getBlockValues(type);
if (tags != null) {
applyTags(tags);
}
@@ -150,8 +150,9 @@ public ItemData(Material type, @Nullable String tags) {
public ItemData(Material type, int amount) {
this.type = type;
- this.stack = new ItemStack(type, Math.abs(amount));
- this.blockValues = BlockCompat.INSTANCE.getBlockValues(stack);
+ if (type.isItem())
+ this.stack = new ItemStack(type, Math.abs(amount));
+ this.blockValues = BlockCompat.INSTANCE.getBlockValues(type);
}
public ItemData(Material type) {
@@ -159,13 +160,18 @@ public ItemData(Material type) {
}
public ItemData(ItemData data) {
- this.stack = data.stack.clone();
+ this.stack = data.stack != null ? data.stack.clone() : null;
this.type = data.type;
this.blockValues = data.blockValues;
this.isAlias = data.isAlias;
this.plain = data.plain;
this.itemFlags = data.itemFlags;
}
+
+ public ItemData(Material material, @Nullable BlockValues values) {
+ this.type = material;
+ this.blockValues = values;
+ }
public ItemData(ItemStack stack, @Nullable BlockValues values) {
this.stack = stack;
@@ -200,7 +206,8 @@ public ItemData(BlockState blockState) {
public ItemData(BlockData blockData) {
this.type = blockData.getMaterial();
- this.stack = new ItemStack(type);
+ if (type.isItem())
+ this.stack = new ItemStack(type);
this.blockValues = BlockCompat.INSTANCE.getBlockValues(blockData);
}
@@ -227,13 +234,12 @@ public boolean isOfType(@Nullable ItemStack item) {
if (type != item.getType())
return false; // Obvious mismatch
- if (itemFlags != 0) { // Either stack has tags (or durability)
+ if (stack != null && itemFlags != 0) { // Either stack has tags (or durability)
if (ItemUtils.getDamage(stack) != ItemUtils.getDamage(item))
return false; // On 1.12 and below, damage is not in meta
if (stack.hasItemMeta() == item.hasItemMeta()) // Compare ItemMeta as in isSimilar() of ItemStack
- return stack.hasItemMeta() ? itemFactory.equals(stack.getItemMeta(), item.getItemMeta()) : true;
- else
- return false;
+ return !stack.hasItemMeta() || itemFactory.equals(stack.getItemMeta(), item.getItemMeta());
+ return false;
}
return true;
}
@@ -249,7 +255,7 @@ public String toString() {
public String toString(final boolean debug, final boolean plural) {
StringBuilder builder = new StringBuilder(Aliases.getMaterialName(this, plural));
- ItemMeta meta = stack.getItemMeta();
+ ItemMeta meta = stack != null ? stack.getItemMeta() : null;
if (meta != null && meta.hasDisplayName()) {
builder.append(" ").append(m_named).append(" ");
builder.append(meta.getDisplayName());
@@ -272,7 +278,7 @@ public boolean equals(final @Nullable Object obj) {
return false;
ItemData other = (ItemData) obj;
- if (isAlias) { // This is alias, other item might not be
+ if (isAlias()) { // This is alias, other item might not be
return other.matchAlias(this).isAtLeast(MatchQuality.SAME_ITEM);
} else { // This is not alias, but other might be
return matchAlias(other).isAtLeast(MatchQuality.SAME_ITEM);
@@ -282,7 +288,7 @@ public boolean equals(final @Nullable Object obj) {
@Override
public int hashCode() {
int hash = type.hashCode(); // Has collisions, but probably not too many of them
- if (blockValues == null || (blockValues != null && blockValues.isDefault())) {
+ if (blockValues == null || blockValues.isDefault()) {
hash = hash * 37 + 1;
}
return hash;
@@ -351,7 +357,7 @@ public MatchQuality matchAlias(ItemData item) {
}
// See if we need to compare item metas (excluding durability)
- if (quality.isAtLeast(MatchQuality.SAME_ITEM) && stack.hasItemMeta() || item.stack.hasItemMeta()) { // Item meta checks could lower this
+ if (quality.isAtLeast(MatchQuality.SAME_ITEM) && this.hasItemMeta() || item.hasItemMeta()) { // Item meta checks could lower this
MatchQuality metaQuality = compareItemMetas(getItemMeta(), item.getItemMeta());
// If given item doesn't care about meta, promote to SAME_ITEM
@@ -489,9 +495,13 @@ public ItemData intersection(final ItemData other) {
* It is not a copy, so please be careful.
* @return Item stack.
*/
- public ItemStack getStack() {
+ public @Nullable ItemStack getStack() {
return stack;
}
+
+ private boolean hasItemMeta() {
+ return stack != null && stack.hasItemMeta();
+ }
@Override
public ItemData clone() {
@@ -508,7 +518,7 @@ public BlockValues getBlockValues() {
}
public ItemMeta getItemMeta() {
- ItemMeta meta = stack.getItemMeta();
+ ItemMeta meta = stack != null ? stack.getItemMeta() : null;
if (meta == null) { // AIR has null item meta!
meta = itemFactory.getItemMeta(Material.STONE);
}
@@ -517,6 +527,8 @@ public ItemMeta getItemMeta() {
}
public void setItemMeta(ItemMeta meta) {
+ if (stack == null)
+ return;
stack.setItemMeta(meta);
isAlias = false; // This is no longer exact alias
plain = false; // This is no longer a plain item
@@ -524,10 +536,14 @@ public void setItemMeta(ItemMeta meta) {
}
public int getDurability() {
+ if (stack == null)
+ return 0; // no damage?
return ItemUtils.getDamage(stack);
}
public void setDurability(int durability) {
+ if (stack == null)
+ return;
ItemUtils.setDamage(stack, durability);
isAlias = false; // Change happened
plain = false; // This is no longer a plain item
@@ -567,7 +583,7 @@ public boolean matchPlain(ItemData other) {
public Fields serialize() throws NotSerializableException {
Fields fields = new Fields(this); // ItemStack is transient, will be ignored
fields.putPrimitive("id", type.ordinal());
- fields.putObject("meta", stack.getItemMeta());
+ fields.putObject("meta", stack != null ? stack.getItemMeta() : null);
return fields;
}
@@ -579,8 +595,10 @@ public void deserialize(Fields fields) throws StreamCorruptedException, NotSeria
ItemMeta meta = fields.getAndRemoveObject("meta", ItemMeta.class);
// Initialize ItemStack
- this.stack = new ItemStack(type);
- stack.setItemMeta(meta); // Just set meta to it
+ if (meta != null && type.isItem()) {
+ this.stack = new ItemStack(type);
+ stack.setItemMeta(meta); // Just set meta to it
+ }
fields.setFields(this); // Everything but ItemStack and Material
}
@@ -598,17 +616,17 @@ public void deserialize(Fields fields) throws StreamCorruptedException, NotSeria
*/
public ItemData aliasCopy() {
ItemData data = new ItemData();
- data.stack = new ItemStack(type, 1);
-
- if (stack.hasItemMeta()) {
- ItemMeta meta = stack.getItemMeta(); // Creates a copy
- meta.setDisplayName(null); // Clear display name
- if (!itemFactory.getItemMeta(type).equals(meta)) // there may be different tags (e.g. potions)
- data.itemFlags |= ItemFlags.CHANGED_TAGS;
- data.stack.setItemMeta(meta);
+ if (stack != null) {
+ data.stack = new ItemStack(type, 1);
+ if (stack.hasItemMeta()) {
+ ItemMeta meta = stack.getItemMeta(); // Creates a copy
+ meta.setDisplayName(null); // Clear display name
+ if (!itemFactory.getItemMeta(type).equals(meta)) // there may be different tags (e.g. potions)
+ data.itemFlags |= ItemFlags.CHANGED_TAGS;
+ data.stack.setItemMeta(meta);
+ }
+ ItemUtils.setDamage(data.stack, 0); // Set to undamaged
}
- ItemUtils.setDamage(data.stack, 0); // Set to undamaged
-
data.type = type;
data.blockValues = blockValues;
data.itemForm = itemForm;
@@ -620,6 +638,8 @@ public ItemData aliasCopy() {
* @param tags Tags in Mojang's JSON format.
*/
public void applyTags(String tags) {
+ if (stack == null)
+ return;
BukkitUnsafe.modifyItemStack(stack, tags);
itemFlags |= ItemFlags.CHANGED_TAGS;
}
diff --git a/src/main/java/ch/njol/skript/aliases/ItemType.java b/src/main/java/ch/njol/skript/aliases/ItemType.java
index 7341667fa91..d2a6fc85421 100644
--- a/src/main/java/ch/njol/skript/aliases/ItemType.java
+++ b/src/main/java/ch/njol/skript/aliases/ItemType.java
@@ -40,6 +40,7 @@
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.OfflinePlayer;
+import org.bukkit.Tag;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.block.Skull;
@@ -68,6 +69,7 @@
import java.util.Random;
import java.util.RandomAccess;
import java.util.Set;
+import java.util.stream.Collectors;
@ContainerType(ItemStack.class)
public class ItemType implements Unit, Iterable, Container, YggdrasilExtendedSerializable {
@@ -172,6 +174,18 @@ public ItemType(Material id) {
add_(new ItemData(id));
}
+ public ItemType(Material... ids) {
+ for (Material id : ids) {
+ add_(new ItemData(id));
+ }
+ }
+
+ public ItemType(Tag tag) {
+ for (Material id : tag.getValues()) {
+ add_(new ItemData(id));
+ }
+ }
+
public ItemType(Material id, String tags) {
add_(new ItemData(id, tags));
}
@@ -315,7 +329,7 @@ public boolean isOfType(Material id, @Nullable String tags) {
public boolean isOfType(Material id) {
// TODO avoid object creation
- return isOfType(new ItemData(id, null));
+ return isOfType(new ItemData(id, (String) null));
}
/**
@@ -343,7 +357,7 @@ public ItemType getBlock() {
*/
public boolean hasItem() {
for (ItemData d : types) {
- if (!d.type.isBlock())
+ if (d.type.isItem())
return true;
}
return false;
@@ -487,9 +501,13 @@ public boolean hasNext() {
@Override
public ItemStack next() {
- if (!hasNext())
- throw new NoSuchElementException();
- ItemStack is = iter.next().getStack().clone();
+ ItemStack is = null;
+ while (is == null) {
+ if (!hasNext())
+ throw new NoSuchElementException();
+ is = iter.next().getStack();
+ }
+ is = is.clone();
is.setAmount(getAmount());
return is;
}
@@ -588,14 +606,33 @@ public ItemType clone() {
* @see #removeFrom(ItemStack)
* @see #removeFrom(List...)
*/
- public ItemStack getRandom() {
- int numItems = types.size();
- int index = random.nextInt(numItems);
- ItemStack is = types.get(index).getStack().clone();
+ public @Nullable ItemStack getRandom() {
+ List datas = types.stream()
+ .filter(data -> data.stack != null)
+ .collect(Collectors.toList());
+ if (datas.isEmpty())
+ return null;
+ ItemStack is = datas.get(random.nextInt(datas.size())).getStack();
+ assert is != null; // verified above
+ is = is.clone();
is.setAmount(getAmount());
return is;
}
+ /**
+ * @return One random ItemStack or Material that this ItemType represents.
+ * A Material may only be returned for ItemStacks containing a Material where {@link Material#isItem()} is false.
+ */
+ public Object getRandomStackOrMaterial() {
+ ItemData randomData = types.get(random.nextInt(types.size()));
+ ItemStack stack = randomData.getStack();
+ if (stack == null)
+ return randomData.getType();
+ stack = stack.clone();
+ stack.setAmount(getAmount());
+ return stack;
+ }
+
/**
* Test whether this ItemType can be put into the given inventory completely.
*
@@ -869,7 +906,9 @@ public final boolean removeFrom(boolean replaceWithNull, List... list
*/
public void addTo(final List list) {
if (!isAll()) {
- list.add(getItem().getRandom());
+ ItemStack random = getItem().getRandom();
+ if (random != null)
+ list.add(getItem().getRandom());
return;
}
for (final ItemStack is : getItem().getAll())
@@ -936,7 +975,9 @@ private static boolean addTo(@Nullable ItemStack is, ItemStack[] buf) {
public boolean addTo(final ItemStack[] buf) {
if (!isAll()) {
- return addTo(getItem().getRandom(), buf);
+ ItemStack random = getItem().getRandom();
+ if (random != null)
+ return addTo(getItem().getRandom(), buf);
}
boolean ok = true;
for (ItemStack is : getItem().getAll()) {
@@ -1367,8 +1408,11 @@ public void clearItemMeta() {
globalMeta = null;
}
+ /**
+ * @return A random Material this ItemType represents.
+ */
public Material getMaterial() {
- ItemData data = types.get(0);
+ ItemData data = types.get(random.nextInt(types.size()));
if (data == null)
throw new IllegalStateException("material not found");
return data.getType();
diff --git a/src/main/java/ch/njol/skript/bukkitutil/AdventureSoundReceiver.java b/src/main/java/ch/njol/skript/bukkitutil/AdventureSoundReceiver.java
new file mode 100644
index 00000000000..fd596a324a4
--- /dev/null
+++ b/src/main/java/ch/njol/skript/bukkitutil/AdventureSoundReceiver.java
@@ -0,0 +1,108 @@
+/**
+ * This file is part of Skript.
+ *
+ * Skript is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Skript is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Skript. If not, see .
+ *
+ * Copyright Peter Güttinger, SkriptLang team and contributors
+ */
+package ch.njol.skript.bukkitutil;
+
+import java.util.Locale;
+import java.util.OptionalLong;
+
+import org.bukkit.Location;
+import org.bukkit.NamespacedKey;
+import org.bukkit.Sound;
+import org.bukkit.SoundCategory;
+import org.bukkit.entity.Entity;
+import org.jetbrains.annotations.NotNull;
+
+import ch.njol.skript.effects.EffPlaySound;
+
+/**
+ * A utility interface to access the Player::playSound while also providing the same arguments to World::playSound
+ * Used in EffPlaySound. Separated due to static versioning.
+ */
+@FunctionalInterface
+public interface AdventureSoundReceiver {
+
+ void play(
+ @NotNull T receiver, @NotNull E emitter, @NotNull String sound,
+ @NotNull SoundCategory category, float volume, float pitch
+ );
+
+ static void play(
+ @NotNull AdventureEmitterSoundReceiver adventureLocationReceiver,
+ @NotNull AdventureEntitySoundReceiver adventureEmitterReceiver,
+ @NotNull T receiver, @NotNull E emitter, @NotNull String[] sounds,
+ @NotNull SoundCategory category, float volume, float pitch, OptionalLong seed
+ ) {
+ for (String sound : sounds) {
+ NamespacedKey key = null;
+ try {
+ Sound enumSound = Sound.valueOf(sound.toUpperCase(Locale.ENGLISH));
+ key = enumSound.getKey();
+ } catch (IllegalArgumentException alternative) {
+ sound = sound.toLowerCase(Locale.ENGLISH);
+ if (!EffPlaySound.KEY_PATTERN.matcher(sound).matches())
+ continue;
+ try {
+ key = NamespacedKey.fromString(sound);
+ } catch (IllegalArgumentException argument) {
+ // The user input invalid characters
+ }
+ }
+
+ if (key == null)
+ continue;
+ net.kyori.adventure.sound.Sound adventureSound = net.kyori.adventure.sound.Sound.sound()
+ .source(category)
+ .volume(volume)
+ .pitch(pitch)
+ .seed(seed)
+ .type(key)
+ .build();
+ AdventureEmitterSoundReceiver.play(adventureLocationReceiver, adventureEmitterReceiver, receiver, adventureSound, emitter);
+ }
+ }
+
+ @FunctionalInterface
+ public interface AdventureEmitterSoundReceiver {
+ void play(
+ @NotNull T receiver, @NotNull net.kyori.adventure.sound.Sound sound, double x, double y, double z
+ );
+
+ static void play(
+ @NotNull AdventureEmitterSoundReceiver locationReceiver,
+ @NotNull AdventureEntitySoundReceiver emitterReceiver,
+ @NotNull T receiver, @NotNull net.kyori.adventure.sound.Sound sound, @NotNull E emitter
+ ) {
+ if (emitter instanceof Location) {
+ Location location = (Location) emitter;
+ locationReceiver.play(receiver, sound, location.getX(), location.getY(), location.getZ());
+ } else if (emitter instanceof Entity) {
+ Entity entity = (Entity) emitter;
+ emitterReceiver.play(receiver, sound, entity);
+ }
+ }
+ }
+
+ @FunctionalInterface
+ public interface AdventureEntitySoundReceiver {
+ void play(
+ @NotNull T receiver, @NotNull net.kyori.adventure.sound.Sound sound, net.kyori.adventure.sound.Sound.Emitter emitter
+ );
+ }
+
+}
diff --git a/src/main/java/ch/njol/skript/bukkitutil/BukkitUtils.java b/src/main/java/ch/njol/skript/bukkitutil/BukkitUtils.java
new file mode 100644
index 00000000000..df551c268f5
--- /dev/null
+++ b/src/main/java/ch/njol/skript/bukkitutil/BukkitUtils.java
@@ -0,0 +1,21 @@
+package ch.njol.skript.bukkitutil;
+
+import ch.njol.skript.Skript;
+import org.bukkit.Registry;
+
+/**
+ * Utility class with methods pertaining to Bukkit API
+ */
+public class BukkitUtils {
+
+ /**
+ * Check if a registry exists
+ *
+ * @param registry Registry to check for (Fully qualified name of registry)
+ * @return True if registry exists else false
+ */
+ public static boolean registryExists(String registry) {
+ return Skript.classExists("org.bukkit.Registry") && Skript.fieldExists(Registry.class, registry);
+ }
+
+}
diff --git a/src/main/java/ch/njol/skript/bukkitutil/EnchantmentUtils.java b/src/main/java/ch/njol/skript/bukkitutil/EnchantmentUtils.java
index fcebff78917..7247bba5d37 100644
--- a/src/main/java/ch/njol/skript/bukkitutil/EnchantmentUtils.java
+++ b/src/main/java/ch/njol/skript/bukkitutil/EnchantmentUtils.java
@@ -18,26 +18,165 @@
*/
package ch.njol.skript.bukkitutil;
+import ch.njol.skript.classes.ClassInfo;
+import ch.njol.skript.classes.Parser;
+import ch.njol.skript.classes.Serializer;
+import ch.njol.skript.lang.ParseContext;
+import ch.njol.skript.localization.Language;
+import ch.njol.util.StringUtils;
+import ch.njol.yggdrasil.Fields;
import org.bukkit.NamespacedKey;
+import org.bukkit.Registry;
import org.bukkit.enchantments.Enchantment;
-import org.eclipse.jdt.annotation.Nullable;
+import org.jetbrains.annotations.Nullable;
-import com.google.common.collect.BiMap;
-import com.google.common.collect.HashBiMap;
-import ch.njol.skript.Skript;
+import java.io.StreamCorruptedException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
/**
* Maps enchantments to their keys.
*/
public class EnchantmentUtils {
+ private static final Map NAMES = new HashMap<>();
+ private static final Map PATTERNS = new HashMap<>();
+ private static final boolean HAS_REGISTRY = BukkitUtils.registryExists("ENCHANTMENT");
+
+ static {
+ if (!HAS_REGISTRY) {
+ Language.addListener(() -> {
+ NAMES.clear();
+ PATTERNS.clear();
+ for (Enchantment enchantment : Enchantment.values()) {
+ NamespacedKey key = enchantment.getKey();
+ final String[] names = Language.getList("enchantments." + key.getKey());
+
+ if (!names[0].startsWith("enchantments.")) {
+ NAMES.put(enchantment, names[0]);
+ // Add lang file names
+ for (String name : names)
+ PATTERNS.put(name.toLowerCase(Locale.ENGLISH), enchantment);
+ }
+ // If Minecraft provided, add key without namespace and underscores (ex: "fire aspect")
+ if (key.getNamespace().equalsIgnoreCase(NamespacedKey.MINECRAFT))
+ PATTERNS.put(key.getKey().replace("_", " "), enchantment);
+ // Add full namespaced key as pattern (ex: "minecraft:fire_aspect", "custom:floopy_floopy")
+ PATTERNS.put(key.toString(), enchantment);
+ }
+ });
+ }
+ }
+
public static String getKey(Enchantment enchantment) {
- return enchantment.getKey().getKey();
+ return enchantment.getKey().toString();
}
@Nullable
public static Enchantment getByKey(String key) {
- return Enchantment.getByKey(NamespacedKey.minecraft(key));
+ if (!key.contains(":")) {
+ // Old method for old variables
+ return Enchantment.getByKey(NamespacedKey.minecraft(key));
+ } else {
+ NamespacedKey namespacedKey = NamespacedKey.fromString(key);
+ if (namespacedKey == null)
+ return null;
+
+ if (HAS_REGISTRY) {
+ return Registry.ENCHANTMENT.get(namespacedKey);
+ } else {
+ return Enchantment.getByKey(namespacedKey);
+ }
+ }
+ }
+
+ @Nullable
+ public static Enchantment parseEnchantment(String s) {
+ return PATTERNS.get(s);
+ }
+
+ @SuppressWarnings("null")
+ public static Collection getNames() {
+ return NAMES.values();
+ }
+
+ @SuppressWarnings("null")
+ public static String toString(final Enchantment enchantment) {
+ // If we have a name in the lang file, return that first
+ if (NAMES.containsKey(enchantment))
+ return NAMES.get(enchantment);
+
+ // If no name is available, return the namespaced key
+ return enchantment.getKey().toString();
+ }
+
+ // REMIND flags?
+ @SuppressWarnings("null")
+ public static String toString(final Enchantment enchantment, final int flags) {
+ return toString(enchantment);
+ }
+
+ public static ClassInfo createClassInfo() {
+ return new ClassInfo<>(Enchantment.class, "enchantment")
+ .parser(new Parser<>() {
+ @Override
+ @Nullable
+ public Enchantment parse(final String s, final ParseContext context) {
+ return EnchantmentUtils.parseEnchantment(s);
+ }
+
+ @Override
+ public String toString(final Enchantment e, final int flags) {
+ return EnchantmentUtils.toString(e, flags);
+ }
+
+ @Override
+ public String toVariableNameString(final Enchantment e) {
+ return "" + EnchantmentUtils.getKey(e);
+ }
+ }).serializer(new Serializer<>() {
+ @Override
+ public Fields serialize(final Enchantment ench) {
+ final Fields f = new Fields();
+ f.putObject("key", EnchantmentUtils.getKey(ench));
+ return f;
+ }
+
+ @Override
+ public boolean canBeInstantiated() {
+ return false;
+ }
+
+ @Override
+ public void deserialize(final Enchantment o, final Fields f) {
+ assert false;
+ }
+
+ @Override
+ protected Enchantment deserialize(final Fields fields) throws StreamCorruptedException {
+ final String key = fields.getObject("key", String.class);
+ assert key != null; // If a key happens to be null, something went really wrong...
+ final Enchantment e = EnchantmentUtils.getByKey(key);
+ if (e == null)
+ throw new StreamCorruptedException("Invalid enchantment " + key);
+ return e;
+ }
+
+ @Override
+ @Nullable
+ public Enchantment deserialize(String s) {
+ return Enchantment.getByName(s);
+ }
+
+ @Override
+ public boolean mustSyncDeserialization() {
+ return false;
+ }
+ })
+ .usage(StringUtils.join(EnchantmentUtils.getNames(), ", "))
+ .supplier(Enchantment.values());
}
}
diff --git a/src/main/java/ch/njol/skript/bukkitutil/HealthUtils.java b/src/main/java/ch/njol/skript/bukkitutil/HealthUtils.java
index 20b5120a0f1..be8675ccabb 100644
--- a/src/main/java/ch/njol/skript/bukkitutil/HealthUtils.java
+++ b/src/main/java/ch/njol/skript/bukkitutil/HealthUtils.java
@@ -26,13 +26,13 @@
import org.bukkit.damage.DamageSource;
import org.bukkit.damage.DamageType;
import org.bukkit.entity.Damageable;
+import org.bukkit.entity.LivingEntity;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityDamageEvent.DamageCause;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
public class HealthUtils {
@@ -112,8 +112,11 @@ public static double getFinalDamage(EntityDamageEvent e) {
return e.getFinalDamage() / 2;
}
- public static void setDamage(EntityDamageEvent e, double damage) {
- e.setDamage(damage * 2);
+ public static void setDamage(EntityDamageEvent event, double damage) {
+ event.setDamage(damage * 2);
+ // Set last damage manually as Bukkit doesn't appear to do that
+ if (event.getEntity() instanceof LivingEntity)
+ ((LivingEntity) event.getEntity()).setLastDamage(damage * 2);
}
@Nullable
diff --git a/src/main/java/ch/njol/skript/bukkitutil/InventoryUtils.java b/src/main/java/ch/njol/skript/bukkitutil/InventoryUtils.java
new file mode 100644
index 00000000000..e04228edc3a
--- /dev/null
+++ b/src/main/java/ch/njol/skript/bukkitutil/InventoryUtils.java
@@ -0,0 +1,111 @@
+package ch.njol.skript.bukkitutil;
+
+import ch.njol.skript.Skript;
+import org.bukkit.inventory.Inventory;
+import org.bukkit.inventory.InventoryView;
+import org.eclipse.jdt.annotation.Nullable;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+
+/**
+ * Utilities for inventories.
+ * In newer versions (1.21+), InventoryView is an interface instead of an abstract class
+ * Directing calling InventoryView#getTitle on 1.20.6 and below results in an IncompatibleClassChangeError
+ * as an interface, not an abstract class, is expected.
+ */
+public class InventoryUtils {
+
+ private static final @Nullable MethodHandle GET_TITLE;
+ private static final @Nullable MethodHandle GET_INVENTORY;
+ private static final @Nullable MethodHandle CONVERT_SLOT;
+ private static final @Nullable MethodHandle GET_TOP_INVENTORY;
+ private static final @Nullable MethodHandle GET_BOTTOM_INVENTORY;
+
+ static {
+ MethodHandle getTitle = null;
+ MethodHandle getInventory = null;
+ MethodHandle convertSlot = null;
+ MethodHandle getTopInventory = null;
+ MethodHandle getBottomInventory = null;
+ if (!InventoryView.class.isInterface()) { // initialize legacy support as it's likely an abstract class
+ try {
+ MethodHandles.Lookup lookup = MethodHandles.lookup();
+ getTitle = lookup.findVirtual(InventoryView.class, "getTitle", MethodType.methodType(String.class));
+ getInventory = lookup.findVirtual(InventoryView.class, "getInventory", MethodType.methodType(Inventory.class, int.class));
+ convertSlot = lookup.findVirtual(InventoryView.class, "convertSlot", MethodType.methodType(int.class, int.class));
+ getTopInventory = lookup.findVirtual(InventoryView.class, "getTopInventory", MethodType.methodType(Inventory.class));
+ getBottomInventory = lookup.findVirtual(InventoryView.class, "getBottomInventory", MethodType.methodType(Inventory.class));
+ } catch (NoSuchMethodException | IllegalAccessException e) {
+ Skript.exception(e, "Failed to load old inventory view support.");
+ }
+ }
+ GET_TITLE = getTitle;
+ GET_INVENTORY = getInventory;
+ CONVERT_SLOT = convertSlot;
+ GET_TOP_INVENTORY = getTopInventory;
+ GET_BOTTOM_INVENTORY = getBottomInventory;
+ }
+
+ /**
+ * @see InventoryView#getTitle()
+ */
+ public static @Nullable String getTitle(InventoryView inventoryView) {
+ if (GET_TITLE == null)
+ return inventoryView.getTitle();
+ try {
+ return (String) GET_TITLE.invoke(inventoryView);
+ } catch (Throwable ignored) { }
+ return null;
+ }
+
+ /**
+ * @see InventoryView#getInventory(int)
+ */
+ public static @Nullable Inventory getInventory(InventoryView inventoryView, int rawSlot) {
+ if (GET_INVENTORY == null)
+ return inventoryView.getInventory(rawSlot);
+ try {
+ return (Inventory) GET_INVENTORY.invoke(inventoryView, rawSlot);
+ } catch (Throwable ignored) { }
+ return null;
+ }
+
+ /**
+ * @see InventoryView#convertSlot(int)
+ */
+ public static @Nullable Integer convertSlot(InventoryView inventoryView, int rawSlot) {
+ if (CONVERT_SLOT == null)
+ return inventoryView.convertSlot(rawSlot);
+ try {
+ return (Integer) CONVERT_SLOT.invoke(inventoryView, rawSlot);
+ } catch (Throwable ignored) { }
+ return null;
+ }
+
+ /**
+ * @see InventoryView#getTopInventory()
+ */
+ public static @Nullable Inventory getTopInventory(InventoryView inventoryView) {
+ if (GET_TOP_INVENTORY == null)
+ return inventoryView.getTopInventory();
+ try {
+ return (Inventory) GET_TOP_INVENTORY.invoke(inventoryView);
+ } catch (Throwable ignored) { }
+ return null;
+ }
+
+ /**
+ * @see InventoryView#getBottomInventory()
+ */
+ public static @Nullable Inventory getBottomInventory(InventoryView inventoryView) {
+ if (GET_BOTTOM_INVENTORY == null)
+ return inventoryView.getBottomInventory();
+ try {
+ return (Inventory) GET_BOTTOM_INVENTORY.invoke(inventoryView);
+ } catch (Throwable ignored) { }
+ return null;
+ }
+
+}
diff --git a/src/main/java/ch/njol/skript/bukkitutil/ItemUtils.java b/src/main/java/ch/njol/skript/bukkitutil/ItemUtils.java
index 31bf99c2d56..b216f645127 100644
--- a/src/main/java/ch/njol/skript/bukkitutil/ItemUtils.java
+++ b/src/main/java/ch/njol/skript/bukkitutil/ItemUtils.java
@@ -20,8 +20,15 @@
import ch.njol.skript.Skript;
import ch.njol.skript.aliases.ItemType;
+import ch.njol.skript.util.slot.Slot;
import org.bukkit.Material;
+import org.bukkit.Tag;
import org.bukkit.TreeType;
+import org.bukkit.block.Block;
+import org.bukkit.block.data.BlockData;
+import org.bukkit.block.data.type.Fence;
+import org.bukkit.block.data.type.Gate;
+import org.bukkit.block.data.type.Wall;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.Damageable;
import org.bukkit.inventory.meta.ItemMeta;
@@ -35,6 +42,8 @@
public class ItemUtils {
public static final boolean HAS_MAX_DAMAGE = Skript.methodExists(Damageable.class, "getMaxDamage");
+ // Introduced in Paper 1.21
+ public static final boolean HAS_RESET = Skript.methodExists(Damageable.class, "resetDamage");
/**
* Gets damage/durability of an item, or 0 if it does not have damage.
@@ -42,9 +51,17 @@ public class ItemUtils {
* @return Damage.
*/
public static int getDamage(ItemStack itemStack) {
- ItemMeta meta = itemStack.getItemMeta();
- if (meta instanceof Damageable)
- return ((Damageable) meta).getDamage();
+ return getDamage(itemStack.getItemMeta());
+ }
+
+ /**
+ * Gets damage/durability of an itemmeta, or 0 if it does not have damage.
+ * @param itemMeta ItemMeta.
+ * @return Damage.
+ */
+ public static int getDamage(ItemMeta itemMeta) {
+ if (itemMeta instanceof Damageable)
+ return ((Damageable) itemMeta).getDamage();
return 0; // Non damageable item
}
@@ -60,6 +77,25 @@ public static int getMaxDamage(ItemStack itemStack) {
return itemStack.getType().getMaxDurability();
}
+ /**
+ * Set the max damage/durability of an item
+ *
+ * @param itemStack ItemStack to set max damage
+ * @param maxDamage Amount of new max damage
+ */
+ public static void setMaxDamage(ItemStack itemStack, int maxDamage) {
+ ItemMeta meta = itemStack.getItemMeta();
+ if (HAS_MAX_DAMAGE && meta instanceof Damageable) {
+ Damageable damageable = (Damageable) meta;
+ if (HAS_RESET && maxDamage < 1) {
+ damageable.resetDamage();
+ } else {
+ damageable.setMaxDamage(Math.max(1, maxDamage));
+ }
+ itemStack.setItemMeta(damageable);
+ }
+ }
+
/**
* Sets damage/durability of an item if possible.
* @param itemStack Item to modify.
@@ -69,7 +105,7 @@ public static int getMaxDamage(ItemStack itemStack) {
public static void setDamage(ItemStack itemStack, int damage) {
ItemMeta meta = itemStack.getItemMeta();
if (meta instanceof Damageable) {
- ((Damageable) meta).setDamage(damage);
+ ((Damageable) meta).setDamage(Math.max(0, damage));
itemStack.setItemMeta(meta);
}
}
@@ -139,19 +175,44 @@ public static Material asItem(Material type) {
// Assume (naively) that all types are valid items
return type;
}
+
+ /**
+ * Convert an ItemType/Slot to ItemStack
+ * Will also accept an ItemStack that will return itself
+ *
+ * @param object Object to convert
+ * @return ItemStack from slot/itemtype
+ */
+ @Nullable
+ public static ItemStack asItemStack(Object object) {
+ if (object instanceof ItemType)
+ return ((ItemType) object).getRandom();
+ else if (object instanceof Slot)
+ return ((Slot) object).getItem();
+ else if (object instanceof ItemStack)
+ return ((ItemStack) object);
+ return null;
+ }
/**
* Tests whether two item stacks are of the same type, i.e. it ignores the amounts.
*
- * @param is1
- * @param is2
+ * @param itemStack1
+ * @param itemStack2
* @return Whether the item stacks are of the same type
*/
- public static boolean itemStacksEqual(final @Nullable ItemStack is1, final @Nullable ItemStack is2) {
- if (is1 == null || is2 == null)
- return is1 == is2;
- return is1.getType() == is2.getType() && ItemUtils.getDamage(is1) == ItemUtils.getDamage(is2)
- && is1.getItemMeta().equals(is2.getItemMeta());
+ public static boolean itemStacksEqual(@Nullable ItemStack itemStack1, @Nullable ItemStack itemStack2) {
+ if (itemStack1 == null || itemStack2 == null)
+ return itemStack1 == itemStack2;
+ if (itemStack1.getType() != itemStack2.getType())
+ return false;
+
+ ItemMeta itemMeta1 = itemStack1.getItemMeta();
+ ItemMeta itemMeta2 = itemStack2.getItemMeta();
+ if (itemMeta1 == null || itemMeta2 == null)
+ return itemMeta1 == itemMeta2;
+
+ return itemStack1.getItemMeta().equals(itemStack2.getItemMeta());
}
// Only 1.15 and versions after have Material#isAir method
@@ -219,5 +280,56 @@ public static boolean isAir(Material type) {
public static Material getTreeSapling(TreeType treeType) {
return TREE_TO_SAPLING_MAP.get(treeType);
}
-
+
+
+ private static final boolean HAS_FENCE_TAGS = !Skript.isRunningMinecraft(1, 14);
+
+ /**
+ * Whether the block is a fence or a wall.
+ * @param block the block to check.
+ * @return whether the block is a fence/wall.
+ */
+ public static boolean isFence(Block block) {
+ // TODO: 1.13 only, so remove in 2.10
+ if (!HAS_FENCE_TAGS) {
+ BlockData data = block.getBlockData();
+ return data instanceof Fence
+ || data instanceof Wall
+ || data instanceof Gate;
+ }
+
+ Material type = block.getType();
+ return Tag.FENCES.isTagged(type)
+ || Tag.FENCE_GATES.isTagged(type)
+ || Tag.WALLS.isTagged(type);
+ }
+
+ /**
+ * @param material The material to check
+ * @return whether the material is a full glass block
+ */
+ public static boolean isGlass(Material material) {
+ switch (material) {
+ case GLASS:
+ case RED_STAINED_GLASS:
+ case ORANGE_STAINED_GLASS:
+ case YELLOW_STAINED_GLASS:
+ case LIGHT_BLUE_STAINED_GLASS:
+ case BLUE_STAINED_GLASS:
+ case CYAN_STAINED_GLASS:
+ case LIME_STAINED_GLASS:
+ case GREEN_STAINED_GLASS:
+ case MAGENTA_STAINED_GLASS:
+ case PURPLE_STAINED_GLASS:
+ case PINK_STAINED_GLASS:
+ case WHITE_STAINED_GLASS:
+ case LIGHT_GRAY_STAINED_GLASS:
+ case GRAY_STAINED_GLASS:
+ case BLACK_STAINED_GLASS:
+ case BROWN_STAINED_GLASS:
+ return true;
+ default:
+ return false;
+ }
+ }
}
diff --git a/src/main/java/ch/njol/skript/bukkitutil/block/BlockCompat.java b/src/main/java/ch/njol/skript/bukkitutil/block/BlockCompat.java
index eef0866a01b..87bc45a6ec8 100644
--- a/src/main/java/ch/njol/skript/bukkitutil/block/BlockCompat.java
+++ b/src/main/java/ch/njol/skript/bukkitutil/block/BlockCompat.java
@@ -65,6 +65,9 @@ default BlockValues getBlockValues(Block block) {
return getBlockValues(block.getBlockData());
}
+ @Nullable
+ BlockValues getBlockValues(Material material);
+
@Nullable
BlockValues getBlockValues(BlockData blockData);
diff --git a/src/main/java/ch/njol/skript/bukkitutil/block/NewBlockCompat.java b/src/main/java/ch/njol/skript/bukkitutil/block/NewBlockCompat.java
index 2a0ecd36da8..a1413a8ad35 100644
--- a/src/main/java/ch/njol/skript/bukkitutil/block/NewBlockCompat.java
+++ b/src/main/java/ch/njol/skript/bukkitutil/block/NewBlockCompat.java
@@ -22,6 +22,7 @@
import ch.njol.skript.aliases.Aliases;
import ch.njol.skript.aliases.ItemType;
import ch.njol.skript.aliases.MatchQuality;
+import ch.njol.skript.bukkitutil.ItemUtils;
import ch.njol.skript.variables.Variables;
import ch.njol.yggdrasil.Fields;
import org.bukkit.Bukkit;
@@ -31,10 +32,12 @@
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.BlockState;
+import org.bukkit.block.BlockSupport;
import org.bukkit.block.data.Bisected;
import org.bukkit.block.data.BlockData;
import org.bukkit.block.data.Directional;
import org.bukkit.block.data.type.Bed;
+import org.bukkit.block.data.type.Snow;
import org.bukkit.entity.FallingBlock;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
@@ -141,15 +144,6 @@ public void deserialize(@NonNull Fields fields) throws StreamCorruptedException
}
private static class NewBlockSetter implements BlockSetter {
-
- private ItemType floorTorch;
- private ItemType wallTorch;
-
- private ItemType specialTorchSides;
- private ItemType specialTorchFloors;
-
- private boolean typesLoaded = false;
-
private static final BlockFace[] CARDINAL_FACES =
new BlockFace[] {BlockFace.NORTH, BlockFace.EAST, BlockFace.SOUTH, BlockFace.WEST};
@@ -158,14 +152,11 @@ public NewBlockSetter() {}
@Override
public void setBlock(Block block, Material type, @Nullable BlockValues values, int flags) {
- if (!typesLoaded)
- loadTypes();
-
- boolean rotate = (flags | ROTATE) != 0;
- boolean rotateForce = (flags | ROTATE_FORCE) != 0;
- boolean rotateFixType = (flags | ROTATE_FIX_TYPE) != 0;
- boolean multipart = (flags | MULTIPART) != 0;
- boolean applyPhysics = (flags | APPLY_PHYSICS) != 0;
+ boolean rotate = (flags & ROTATE) != 0;
+ boolean rotateForce = (flags & ROTATE_FORCE) != 0;
+ boolean rotateFixType = (flags & ROTATE_FIX_TYPE) != 0;
+ boolean multipart = (flags & MULTIPART) != 0;
+ boolean applyPhysics = (flags & APPLY_PHYSICS) != 0;
NewBlockValues ourValues = null;
if (values != null)
ourValues = (NewBlockValues) values;
@@ -178,31 +169,23 @@ public void setBlock(Block block, Material type, @Nullable BlockValues values, i
*/
boolean placed = false;
if (rotate) {
- if (floorTorch.isOfType(type) || (rotateFixType && wallTorch.isOfType(type))) {
+ if (type == Material.TORCH || (rotateFixType && type == Material.WALL_TORCH)) {
// If floor torch cannot be placed, try a wall torch
Block under = block.getRelative(0, -1, 0);
- boolean canPlace = true;
- if (!under.getType().isOccluding()) { // Usually cannot be placed, but there are exceptions
- // TODO check for stairs and slabs, currently complicated since there is no 'any' alias
- if (specialTorchFloors.isOfType(under)) {
- canPlace = true;
- } else {
- canPlace = false;
- }
- }
+ boolean canPlace = canSupportFloorTorch(under);
// Can't really place a floor torch, try wall one instead
if (!canPlace) {
BlockFace face = findWallTorchSide(block);
if (face != null) { // Found better torch spot
- block.setType(wallTorch.getMaterial());
+ block.setType(Material.WALL_TORCH);
Directional data = (Directional) block.getBlockData();
data.setFacing(face);
block.setBlockData(data, applyPhysics);
placed = true;
}
}
- } else if (wallTorch.isOfType(type)) {
+ } else if (type == Material.WALL_TORCH) {
Directional data;
if (ourValues != null)
data = (Directional) ourValues.data;
@@ -210,7 +193,7 @@ public void setBlock(Block block, Material type, @Nullable BlockValues values, i
data = (Directional) Bukkit.createBlockData(type);
Block relative = block.getRelative(data.getFacing());
- if ((!relative.getType().isOccluding() && !specialTorchSides.isOfType(relative)) || rotateForce) {
+ if ((!canSupportWallTorch(relative, data.getFacing().getOppositeFace())) || rotateForce) {
// Attempt to figure out a better rotation
BlockFace face = findWallTorchSide(block);
if (face != null) { // Found better torch spot
@@ -294,22 +277,49 @@ public void setBlock(Block block, Material type, @Nullable BlockValues values, i
block.setBlockData(ourValues.data, applyPhysics);
}
}
-
- private void loadTypes() {
- floorTorch = Aliases.javaItemType("floor torch");
- wallTorch = Aliases.javaItemType("wall torch");
-
- specialTorchSides = Aliases.javaItemType("special torch sides");
- specialTorchFloors = Aliases.javaItemType("special torch floors");
-
- typesLoaded = true;
+
+ // 1.19+
+ // TODO: remove in 2.10
+ private static final boolean HAS_BLOCK_SUPPORT = Skript.classExists("org.bukkit.block.BlockSupport");
+
+ /**
+ * Returns whether this block can support a floor torch.
+ * @param block The block the torch will be placed on
+ * @return whether the block can support the torch.
+ */
+ private static boolean canSupportFloorTorch(Block block) {
+ if (HAS_BLOCK_SUPPORT)
+ return block.getBlockData().isFaceSturdy(BlockFace.UP, BlockSupport.CENTER);
+
+ Material material = block.getType();
+ return (material.isOccluding()
+ || canSupportWallTorch(block, null)
+ || ItemUtils.isFence(block)
+ || ItemUtils.isGlass(material)
+ || material == Material.HOPPER
+ || (material == Material.SNOW && (((Snow) block.getBlockData()).getLayers() == 8))
+ );
+ }
+
+ /**
+ * Returns whether this block can support a wall torch. In 1.19+, a face can be specified.
+ * @param block The block the torch will be placed on
+ * @param face The face the torch will be placed on (only considered in 1.19+)
+ * @return whether the block face can support the torch.
+ */
+ private static boolean canSupportWallTorch(Block block, @Nullable BlockFace face) {
+ if (HAS_BLOCK_SUPPORT && face != null)
+ return block.getBlockData().isFaceSturdy(face, BlockSupport.FULL);
+
+ Material material = block.getType();
+ return material.isOccluding() || material == Material.SOUL_SAND || material == Material.SPAWNER;
}
@Nullable
private BlockFace findWallTorchSide(Block block) {
for (BlockFace face : CARDINAL_FACES) {
Block relative = block.getRelative(face);
- if (relative.getType().isOccluding() || specialTorchSides.isOfType(relative))
+ if (relative.getType().isOccluding() || canSupportWallTorch(relative, face.getOppositeFace()))
return face.getOppositeFace(); // Torch can be rotated towards from this face
}
@@ -336,6 +346,13 @@ public BlockValues getBlockValues(BlockState blockState) {
return getBlockValues(blockState.getBlockData());
}
+ @Override
+ public @Nullable BlockValues getBlockValues(Material material) {
+ if (material.isBlock())
+ return new NewBlockValues(material, Bukkit.createBlockData(material), true);
+ return null;
+ }
+
@Nullable
@Override
public BlockValues getBlockValues(BlockData blockData) {
diff --git a/src/main/java/ch/njol/skript/classes/Changer.java b/src/main/java/ch/njol/skript/classes/Changer.java
index d3b413531fe..a73f40b439a 100644
--- a/src/main/java/ch/njol/skript/classes/Changer.java
+++ b/src/main/java/ch/njol/skript/classes/Changer.java
@@ -29,14 +29,13 @@
* isn't overridden.
*
* Some useful Changers can be found in {@link DefaultChangers}
- *
- * @author Peter Güttinger
+ *
* @see DefaultChangers
* @see Expression
*/
public interface Changer {
- public static enum ChangeMode {
+ enum ChangeMode {
ADD, SET, REMOVE, REMOVE_ALL, DELETE, RESET;
}
@@ -45,45 +44,44 @@ public static enum ChangeMode {
*
* Unlike {@link Expression#acceptChange(ChangeMode)} this method must not print errors.
*
- * @param mode
+ * @param mode The {@link ChangeMode} to test.
* @return An array of types that {@link #change(Object[], Object[], ChangeMode)} accepts as its delta parameter (which can be arrays to denote that multiple of
* that type are accepted), or null if the given mode is not supported. For {@link ChangeMode#DELETE} and {@link ChangeMode#RESET} this can return any non-null array to
* mark them as supported.
*/
- @Nullable
- public abstract Class>[] acceptChange(ChangeMode mode);
+ Class> @Nullable [] acceptChange(ChangeMode mode);
/**
* @param what The objects to change
* @param delta An array with one or more instances of one or more of the the classes returned by {@link #acceptChange(ChangeMode)} for the given change mode (null for
* {@link ChangeMode#DELETE} and {@link ChangeMode#RESET}). This can be a Object[], thus casting is not allowed.
- * @param mode
+ * @param mode The {@link ChangeMode} to test.
* @throws UnsupportedOperationException (optional) if this method was called on an unsupported ChangeMode.
*/
- public abstract void change(T[] what, @Nullable Object[] delta, ChangeMode mode);
+ void change(T[] what, Object @Nullable [] delta, ChangeMode mode);
- public static abstract class ChangerUtils {
-
- @SuppressWarnings("unchecked")
- public static void change(final Changer changer, final Object[] what, final @Nullable Object[] delta, final ChangeMode mode) {
+ abstract class ChangerUtils {
+
+ public static void change(Changer changer, Object[] what, Object @Nullable [] delta, ChangeMode mode) {
+ //noinspection unchecked
changer.change((T[]) what, delta, mode);
}
/**
* Tests whether an expression accepts changes of a certain type. If multiple types are given it test for whether any of the types is accepted.
*
- * @param e The expression to test
+ * @param expression The expression to test
* @param mode The ChangeMode to use in the test
* @param types The types to test for
- * @return Whether e.{@link Expression#change(Event, Object[], ChangeMode) change}(event, type[], mode) can be used or not.
+ * @return Whether expression.{@link Expression#change(Event, Object[], ChangeMode) change}(event, type[], mode) can be used or not.
*/
- public static boolean acceptsChange(final Expression> e, final ChangeMode mode, final Class>... types) {
- final Class>[] cs = e.acceptChange(mode);
- if (cs == null)
+ public static boolean acceptsChange(final Expression> expression, final ChangeMode mode, final Class>... types) {
+ final Class>[] validTypes = expression.acceptChange(mode);
+ if (validTypes == null)
return false;
for (final Class> type : types) {
- for (final Class> c : cs) {
- if (c.isArray() ? c.getComponentType().isAssignableFrom(type) : c.isAssignableFrom(type))
+ for (final Class> validType : validTypes) {
+ if (validType.isArray() ? validType.getComponentType().isAssignableFrom(type) : validType.isAssignableFrom(type))
return true;
}
}
diff --git a/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java b/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java
index ba1a419aad0..ba0a66a1f41 100644
--- a/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java
+++ b/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java
@@ -29,6 +29,7 @@
import java.util.regex.Pattern;
import java.util.stream.Collectors;
+import ch.njol.skript.bukkitutil.BukkitUtils;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Difficulty;
@@ -99,7 +100,6 @@
import ch.njol.skript.localization.Language;
import ch.njol.skript.registrations.Classes;
import ch.njol.skript.util.BlockUtils;
-import ch.njol.skript.util.EnchantmentType;
import ch.njol.skript.util.PotionEffectUtils;
import ch.njol.skript.util.StringMode;
import ch.njol.util.StringUtils;
@@ -355,7 +355,7 @@ public boolean mustSyncDeserialization() {
protected boolean canBeInstantiated() {
return false;
}
- }));
+ }).cloner(BlockData::clone));
Classes.registerClass(new ClassInfo<>(Location.class, "location")
.user("locations?")
@@ -932,6 +932,7 @@ public String toVariableNameString(InventoryHolder holder) {
.since("1.0")
.after("number")
.supplier(() -> Arrays.stream(Material.values())
+ .filter(Material::isItem)
.map(ItemStack::new)
.iterator())
.parser(new Parser() {
@@ -948,7 +949,10 @@ public ItemStack parse(final String s, final ParseContext context) {
}
final ItemStack i = t.getRandom();
- assert i != null;
+ if (i == null) {
+ Skript.error("'" + s + "' cannot represent an item");
+ return null;
+ }
return i;
}
@@ -980,7 +984,7 @@ public String toVariableNameString(final ItemStack i) {
.changer(DefaultChangers.itemChanger));
ClassInfo> biomeClassInfo;
- if (Skript.classExists("org.bukkit.Registry") && Skript.fieldExists(Registry.class, "BIOME")) {
+ if (BukkitUtils.registryExists("BIOME")) {
biomeClassInfo = new RegistryClassInfo<>(Biome.class, Registry.BIOME, "biome", "biomes");
} else {
biomeClassInfo = new EnumClassInfo<>(Biome.class, "biome", "biomes");
@@ -988,7 +992,8 @@ public String toVariableNameString(final ItemStack i) {
Classes.registerClass(biomeClassInfo
.user("biomes?")
.name("Biome")
- .description("All possible biomes Minecraft uses to generate a world.")
+ .description("All possible biomes Minecraft uses to generate a world.",
+ "NOTE: Minecraft namespaces are supported, ex: 'minecraft:basalt_deltas'.")
.examples("biome at the player is desert")
.since("1.4.4")
.after("damagecause"));
@@ -1075,12 +1080,12 @@ protected boolean canBeInstantiated() {
public PotionEffectType parse(final String s, final ParseContext context) {
return PotionEffectUtils.parseType(s);
}
-
+
@Override
public String toString(final PotionEffectType p, final int flags) {
return PotionEffectUtils.toString(p, flags);
}
-
+
@Override
public String toVariableNameString(final PotionEffectType p) {
return "" + p.getName();
@@ -1220,74 +1225,23 @@ public boolean mustSyncDeserialization() {
return true;
}
}));
-
- Classes.registerClass(new ClassInfo<>(Enchantment.class, "enchantment")
+
+ ClassInfo enchantmentClassInfo;
+ if (BukkitUtils.registryExists("ENCHANTMENT")) {
+ enchantmentClassInfo = new RegistryClassInfo<>(Enchantment.class, Registry.ENCHANTMENT, "enchantment", "enchantments");
+ } else {
+ enchantmentClassInfo = EnchantmentUtils.createClassInfo();
+ }
+ Classes.registerClass(enchantmentClassInfo
.user("enchantments?")
.name("Enchantment")
.description("An enchantment, e.g. 'sharpness' or 'fortune'. Unlike enchantment type " +
- "this type has no level, but you usually don't need to use this type anyway.")
- .usage(StringUtils.join(EnchantmentType.getNames(), ", "))
+ "this type has no level, but you usually don't need to use this type anyway.",
+ "NOTE: Minecraft namespaces are supported, ex: 'minecraft:basalt_deltas'.",
+ "As of Minecraft 1.21 this will also support custom enchantments using namespaces, ex: 'myenchants:explosive'.")
.examples("")
.since("1.4.6")
- .before("enchantmenttype")
- .supplier(Enchantment.values())
- .parser(new Parser() {
- @Override
- @Nullable
- public Enchantment parse(final String s, final ParseContext context) {
- return EnchantmentType.parseEnchantment(s);
- }
-
- @Override
- public String toString(final Enchantment e, final int flags) {
- return EnchantmentType.toString(e, flags);
- }
-
- @Override
- public String toVariableNameString(final Enchantment e) {
- return "" + EnchantmentUtils.getKey(e);
- }
- })
- .serializer(new Serializer() {
- @Override
- public Fields serialize(final Enchantment ench) {
- final Fields f = new Fields();
- f.putObject("key", EnchantmentUtils.getKey(ench));
- return f;
- }
-
- @Override
- public boolean canBeInstantiated() {
- return false;
- }
-
- @Override
- public void deserialize(final Enchantment o, final Fields f) {
- assert false;
- }
-
- @Override
- protected Enchantment deserialize(final Fields fields) throws StreamCorruptedException {
- final String key = fields.getObject("key", String.class);
- assert key != null; // If a key happens to be null, something went really wrong...
- final Enchantment e = EnchantmentUtils.getByKey(key);
- if (e == null)
- throw new StreamCorruptedException("Invalid enchantment " + key);
- return e;
- }
-
- // return "" + e.getId();
- @Override
- @Nullable
- public Enchantment deserialize(String s) {
- return Enchantment.getByName(s);
- }
-
- @Override
- public boolean mustSyncDeserialization() {
- return false;
- }
- }));
+ .before("enchantmenttype"));
Material[] allMaterials = Material.values();
Classes.registerClass(new ClassInfo<>(Material.class, "material")
@@ -1446,17 +1400,26 @@ public String toVariableNameString(FireworkEffect effect) {
.since("2.4")
.requiredPlugins("Minecraft 1.14 or newer"));
}
+
Classes.registerClass(new EnumClassInfo<>(RegainReason.class, "healreason", "heal reasons")
- .user("(regen|heal) (reason|cause)")
- .name("Heal Reason")
- .description("The heal reason in a heal event.")
- .examples("")
- .since("2.5"));
+ .user("(regen|heal) (reason|cause)")
+ .name("Heal Reason")
+ .description("The health regain reason in a heal event.")
+ .since("2.5"));
+
if (Skript.classExists("org.bukkit.entity.Cat$Type")) {
- Classes.registerClass(new EnumClassInfo<>(Cat.Type.class, "cattype", "cat types")
+ ClassInfo catTypeClassInfo;
+ if (BukkitUtils.registryExists("CAT_VARIANT")) {
+ catTypeClassInfo = new RegistryClassInfo<>(Cat.Type.class, Registry.CAT_VARIANT, "cattype", "cat types");
+ } else {
+ //noinspection unchecked, rawtypes - it is an enum on other versions
+ catTypeClassInfo = new EnumClassInfo<>((Class) Cat.Type.class, "cattype", "cat types");
+ }
+ Classes.registerClass(catTypeClassInfo
.user("cat ?(type|race)s?")
.name("Cat Type")
- .description("Represents the race/type of a cat entity.")
+ .description("Represents the race/type of a cat entity.",
+ "NOTE: Minecraft namespaces are supported, ex: 'minecraft:british_shorthair'.")
.since("2.4")
.requiredPlugins("Minecraft 1.14 or newer")
.documentationId("CatType"));
@@ -1505,20 +1468,27 @@ public boolean canParse(ParseContext context) {
@Override
public String toString(EnchantmentOffer eo, int flags) {
- return EnchantmentType.toString(eo.getEnchantment(), flags) + " " + eo.getEnchantmentLevel();
+ return EnchantmentUtils.toString(eo.getEnchantment(), flags) + " " + eo.getEnchantmentLevel();
}
@Override
public String toVariableNameString(EnchantmentOffer eo) {
- return "offer:" + EnchantmentType.toString(eo.getEnchantment()) + "=" + eo.getEnchantmentLevel();
+ return "offer:" + EnchantmentUtils.toString(eo.getEnchantment()) + "=" + eo.getEnchantmentLevel();
}
}));
- Classes.registerClass(new EnumClassInfo<>(Attribute.class, "attributetype", "attribute types")
+ ClassInfo attributeClassInfo;
+ if (BukkitUtils.registryExists("ATTRIBUTE")) {
+ attributeClassInfo = new RegistryClassInfo<>(Attribute.class, Registry.ATTRIBUTE, "attributetype", "attribute types");
+ } else {
+ attributeClassInfo = new EnumClassInfo<>(Attribute.class, "attributetype", "attribute types");
+ }
+ Classes.registerClass(attributeClassInfo
.user("attribute ?types?")
.name("Attribute Type")
.description("Represents the type of an attribute. Note that this type does not contain any numerical values."
- + "See attribute types for more info.")
+ + "See attribute types for more info.",
+ "NOTE: Minecraft namespaces are supported, ex: 'minecraft:generic.attack_damage'.")
.since("2.5"));
Classes.registerClass(new EnumClassInfo<>(Environment.class, "environment", "environments")
diff --git a/src/main/java/ch/njol/skript/classes/data/BukkitEventValues.java b/src/main/java/ch/njol/skript/classes/data/BukkitEventValues.java
index 76e098cb602..e430dd472f6 100644
--- a/src/main/java/ch/njol/skript/classes/data/BukkitEventValues.java
+++ b/src/main/java/ch/njol/skript/classes/data/BukkitEventValues.java
@@ -26,6 +26,7 @@
import ch.njol.skript.Skript;
import ch.njol.skript.aliases.Aliases;
import ch.njol.skript.aliases.ItemType;
+import ch.njol.skript.bukkitutil.InventoryUtils;
import ch.njol.skript.command.CommandEvent;
import ch.njol.skript.events.bukkit.ScriptEvent;
import ch.njol.skript.events.bukkit.SkriptStartEvent;
@@ -101,8 +102,11 @@
import org.bukkit.event.entity.EntityDropItemEvent;
import org.bukkit.event.entity.EntityEvent;
import org.bukkit.event.entity.EntityPickupItemEvent;
+import org.bukkit.event.entity.EntityRegainHealthEvent;
+import org.bukkit.event.entity.EntityRegainHealthEvent.RegainReason;
import org.bukkit.event.entity.EntityResurrectEvent;
import org.bukkit.event.entity.EntityTameEvent;
+import org.bukkit.event.entity.EntityTeleportEvent;
import org.bukkit.event.entity.EntityTransformEvent;
import org.bukkit.event.entity.EntityTransformEvent.TransformReason;
import org.bukkit.event.entity.FireworkExplodeEvent;
@@ -390,12 +394,11 @@ public Block get(final BlockBreakEvent e) {
return new DelayedChangeBlock(e.getBlock());
}
}, 0);
- ItemType stationaryWater = Aliases.javaItemType("stationary water");
EventValues.registerEventValue(BlockBreakEvent.class, Block.class, new Getter() {
@Override
public Block get(final BlockBreakEvent e) {
final BlockState s = e.getBlock().getState();
- s.setType(s.getType() == Material.ICE ? stationaryWater.getMaterial() : Material.AIR);
+ s.setType(s.getType() == Material.ICE ? Material.WATER : Material.AIR);
s.setRawData((byte) 0);
return new BlockStateBlock(s, true);
}
@@ -623,6 +626,20 @@ public Entity get(final EntityTameEvent e) {
}
}, 0);
+ // EntityTeleportEvent
+ EventValues.registerEventValue(EntityTeleportEvent.class, Location.class, new Getter() {
+ @Override
+ public @Nullable Location get(EntityTeleportEvent event) {
+ return event.getFrom();
+ }
+ }, EventValues.TIME_PAST);
+ EventValues.registerEventValue(EntityTeleportEvent.class, Location.class, new Getter() {
+ @Override
+ public @Nullable Location get(EntityTeleportEvent event) {
+ return event.getTo();
+ }
+ }, EventValues.TIME_NOW);
+
// EntityChangeBlockEvent
EventValues.registerEventValue(EntityChangeBlockEvent.class, Block.class, new Getter() {
@Override
@@ -758,12 +775,11 @@ public Block get(final PlayerBucketEmptyEvent e) {
return e.getBlockClicked().getRelative(e.getBlockFace());
}
}, -1);
- ItemType stationaryLava = Aliases.javaItemType("stationary lava");
EventValues.registerEventValue(PlayerBucketEmptyEvent.class, Block.class, new Getter() {
@Override
public Block get(final PlayerBucketEmptyEvent e) {
final BlockState s = e.getBlockClicked().getRelative(e.getBlockFace()).getState();
- s.setType(e.getBucket() == Material.WATER_BUCKET ? stationaryWater.getMaterial() : stationaryLava.getMaterial());
+ s.setType(e.getBucket() == Material.WATER_BUCKET ? Material.WATER : Material.LAVA);
s.setRawData((byte) 0);
return new BlockStateBlock(s, true);
}
@@ -1237,15 +1253,15 @@ public Slot[] get(InventoryDragEvent event) {
List slots = new ArrayList<>(event.getRawSlots().size());
InventoryView view = event.getView();
for (Integer rawSlot : event.getRawSlots()) {
- Inventory inventory = view.getInventory(rawSlot);
- int slot = view.convertSlot(rawSlot);
- if (inventory == null)
+ Inventory inventory = InventoryUtils.getInventory(view, rawSlot);
+ Integer slot = InventoryUtils.convertSlot(view, rawSlot);
+ if (inventory == null || slot == null)
continue;
// Not all indices point to inventory slots. Equipment, for example
if (inventory instanceof PlayerInventory && slot >= 36) {
slots.add(new ch.njol.skript.util.slot.EquipmentSlot(((PlayerInventory) view.getBottomInventory()).getHolder(), slot));
} else {
- slots.add(new InventorySlot(inventory, view.convertSlot(rawSlot)));
+ slots.add(new InventorySlot(inventory, slot));
}
}
return slots.toArray(new Slot[0]);
@@ -1265,7 +1281,9 @@ public Inventory[] get(InventoryDragEvent event) {
Set inventories = new HashSet<>();
InventoryView view = event.getView();
for (Integer rawSlot : event.getRawSlots()) {
- inventories.add(view.getInventory(rawSlot));
+ Inventory inventory = InventoryUtils.getInventory(view, rawSlot);
+ if (inventory != null)
+ inventories.add(inventory);
}
return inventories.toArray(new Inventory[0]);
}
@@ -1908,5 +1926,14 @@ public ItemStack get(InventoryMoveItemEvent event) {
return event.getItem();
}
}, EventValues.TIME_NOW);
+
+ // EntityRegainHealthEvent
+ EventValues.registerEventValue(EntityRegainHealthEvent.class, RegainReason.class, new Getter() {
+ @Override
+ @Nullable
+ public RegainReason get(EntityRegainHealthEvent event) {
+ return event.getRegainReason();
+ }
+ }, EventValues.TIME_NOW);
}
}
diff --git a/src/main/java/ch/njol/skript/classes/data/DefaultComparators.java b/src/main/java/ch/njol/skript/classes/data/DefaultComparators.java
index e59a5678403..e5e42bf1423 100644
--- a/src/main/java/ch/njol/skript/classes/data/DefaultComparators.java
+++ b/src/main/java/ch/njol/skript/classes/data/DefaultComparators.java
@@ -60,6 +60,7 @@
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
+import org.bukkit.potion.PotionEffectType;
import org.skriptlang.skript.lang.comparator.Comparator;
import org.skriptlang.skript.lang.comparator.Comparators;
import org.skriptlang.skript.lang.comparator.Relation;
@@ -495,7 +496,6 @@ public boolean supportsOrdering() {
});
// DamageCause - ItemType
- ItemType lava = Aliases.javaItemType("lava");
Comparators.registerComparator(DamageCause.class, ItemType.class, new Comparator() {
@Override
public Relation compare(DamageCause dc, ItemType t) {
@@ -503,7 +503,7 @@ public Relation compare(DamageCause dc, ItemType t) {
case FIRE:
return Relation.get(t.isOfType(Material.FIRE));
case LAVA:
- return Relation.get(t.equals(lava));
+ return Relation.get(t.getMaterial() == Material.LAVA);
case MAGIC:
return Relation.get(t.isOfType(Material.POTION));
case HOT_FLOOR:
@@ -653,6 +653,9 @@ public boolean supportsOrdering() {
return false;
}
});
+
+ // Potion Effect Type
+ Comparators.registerComparator(PotionEffectType.class, PotionEffectType.class, (one, two) -> Relation.get(one.equals(two)));
}
}
diff --git a/src/main/java/ch/njol/skript/classes/data/DefaultConverters.java b/src/main/java/ch/njol/skript/classes/data/DefaultConverters.java
index 00f419f4901..a90de6957d3 100644
--- a/src/main/java/ch/njol/skript/classes/data/DefaultConverters.java
+++ b/src/main/java/ch/njol/skript/classes/data/DefaultConverters.java
@@ -176,6 +176,14 @@ public DefaultConverters() {}
return ((Block) holder).getLocation();
if (holder instanceof BlockState)
return BlockUtils.getLocation(((BlockState) holder).getBlock());
+ if (holder instanceof DoubleChest) {
+ DoubleChest doubleChest = (DoubleChest) holder;
+ if (doubleChest.getLeftSide() != null) {
+ return BlockUtils.getLocation(((BlockState) doubleChest.getLeftSide()).getBlock());
+ } else if (((DoubleChest) holder).getRightSide() != null) {
+ return BlockUtils.getLocation(((BlockState) doubleChest.getRightSide()).getBlock());
+ }
+ }
return null;
});
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 afc46cba346..10baab6fb8c 100644
--- a/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java
+++ b/src/main/java/ch/njol/skript/classes/data/DefaultFunctions.java
@@ -18,6 +18,7 @@
*/
package ch.njol.skript.classes.data;
+import ch.njol.skript.Skript;
import ch.njol.skript.expressions.base.EventValueExpression;
import ch.njol.skript.lang.Expression;
import ch.njol.skript.lang.function.FunctionEvent;
@@ -45,7 +46,9 @@
import java.math.BigDecimal;
import java.math.RoundingMode;
+import java.util.ArrayList;
import java.util.Calendar;
+import java.util.List;
import java.util.UUID;
public class DefaultFunctions {
@@ -376,7 +379,7 @@ public World[] executeSimple(Object[][] params) {
}, DefaultClasses.LOCATION, true) {
@Override
@Nullable
- public Location[] execute(FunctionEvent> e, Object[][] params) {
+ public Location[] execute(FunctionEvent> event, Object[][] params) {
for (int i : new int[] {0, 1, 2, 4, 5}) {
if (params[i] == null || params[i].length == 0 || params[i][0] == null)
return null;
@@ -556,23 +559,54 @@ public Player[] executeSimple(Object[][] params) {
.examples("set {_p} to player(\"Notch\") # will return an online player whose name is or starts with 'Notch'", "set {_p} to player(\"Notch\", true) # will return the only online player whose name is 'Notch'", "set {_p} to player(\"069a79f4-44e9-4726-a5be-fca90e38aaf5\") # if player is offline")
.since("2.8.0");
- Functions.registerFunction(new SimpleJavaFunction("offlineplayer", new Parameter[] {
- new Parameter<>("nameOrUUID", DefaultClasses.STRING, true, null)
- }, DefaultClasses.OFFLINE_PLAYER, true) {
- @Override
- public OfflinePlayer[] executeSimple(Object[][] params) {
- String name = (String) params[0][0];
- UUID uuid = null;
- if (name.length() > 16 || name.contains("-")) { // shortcut
- try {
- uuid = UUID.fromString(name);
- } catch (IllegalArgumentException ignored) {}
+ { // offline player function
+ boolean hasIfCached = Skript.methodExists(Bukkit.class, "getOfflinePlayerIfCached", String.class);
+
+ List> params = new ArrayList<>();
+ params.add(new Parameter<>("nameOrUUID", DefaultClasses.STRING, true, null));
+ if (hasIfCached)
+ params.add(new Parameter<>("allowLookups", DefaultClasses.BOOLEAN, true, new SimpleLiteral<>(true, true)));
+
+ Functions.registerFunction(new SimpleJavaFunction("offlineplayer", params.toArray(new Parameter[0]),
+ DefaultClasses.OFFLINE_PLAYER, true) {
+ @Override
+ public OfflinePlayer[] executeSimple(Object[][] params) {
+ String name = (String) params[0][0];
+ UUID uuid = null;
+ if (name.length() > 16 || name.contains("-")) { // shortcut
+ try {
+ uuid = UUID.fromString(name);
+ } catch (IllegalArgumentException ignored) {
+ }
+ }
+ OfflinePlayer result;
+
+ if (uuid != null) {
+ result = Bukkit.getOfflinePlayer(uuid); // doesn't do lookups
+ } else if (hasIfCached && !((Boolean) params[1][0])) {
+ result = Bukkit.getOfflinePlayerIfCached(name);
+ if (result == null)
+ return new OfflinePlayer[0];
+ } else {
+ result = Bukkit.getOfflinePlayer(name);
+ }
+
+ return CollectionUtils.array(result);
}
- return CollectionUtils.array(uuid != null ? Bukkit.getOfflinePlayer(uuid) : Bukkit.getOfflinePlayer(name));
- }
- }).description("Returns a offline player from their name or UUID. This function will still return the player if they're online.")
- .examples("set {_p} to offlineplayer(\"Notch\")", "set {_p} to offlineplayer(\"069a79f4-44e9-4726-a5be-fca90e38aaf5\")")
- .since("2.8.0");
+
+ }).description(
+ "Returns a offline player from their name or UUID. This function will still return the player if they're online. " +
+ "If Paper 1.16.5+ is used, the 'allowLookup' parameter can be set to false to prevent this function from doing a " +
+ "web lookup for players who have not joined before. Lookups can cause lag spikes of up to multiple seconds, so " +
+ "use offline players with caution."
+ )
+ .examples(
+ "set {_p} to offlineplayer(\"Notch\")",
+ "set {_p} to offlineplayer(\"069a79f4-44e9-4726-a5be-fca90e38aaf5\")",
+ "set {_p} to offlineplayer(\"Notch\", false)"
+ )
+ .since("2.8.0, 2.9.0 (prevent lookups)");
+ } // end offline player function
Functions.registerFunction(new SimpleJavaFunction("isNaN", numberParam, DefaultClasses.BOOLEAN, true) {
@Override
@@ -598,7 +632,7 @@ public String[] executeSimple(Object[][] params) {
.examples(
"concat(\"hello \", \"there\") # hello there",
"concat(\"foo \", 100, \" bar\") # foo 100 bar"
- ).since("INSERT VERSION");
+ ).since("2.9.0");
}
diff --git a/src/main/java/ch/njol/skript/classes/registry/RegistryParser.java b/src/main/java/ch/njol/skript/classes/registry/RegistryParser.java
index 51cb4637434..dc9bf9f00b7 100644
--- a/src/main/java/ch/njol/skript/classes/registry/RegistryParser.java
+++ b/src/main/java/ch/njol/skript/classes/registry/RegistryParser.java
@@ -30,12 +30,11 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.stream.Collectors;
/**
* A parser based on a {@link Registry} used to parse data from a string or turn data into a string.
@@ -147,8 +146,7 @@ private void refresh() {
* Note that some entries may represent the same registry object.
*/
public String getAllNames() {
- List strings = new ArrayList<>(parseMap.keySet());
- Collections.sort(strings);
+ List strings = parseMap.keySet().stream().filter(s -> !s.startsWith("minecraft:")).sorted().collect(Collectors.toList());
return StringUtils.join(strings, ", ");
}
diff --git a/src/main/java/ch/njol/skript/classes/registry/RegistrySerializer.java b/src/main/java/ch/njol/skript/classes/registry/RegistrySerializer.java
index ddec6c3225f..33f6645fe52 100644
--- a/src/main/java/ch/njol/skript/classes/registry/RegistrySerializer.java
+++ b/src/main/java/ch/njol/skript/classes/registry/RegistrySerializer.java
@@ -23,6 +23,7 @@
import org.bukkit.Keyed;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
+import org.jetbrains.annotations.NotNull;
import java.io.StreamCorruptedException;
@@ -40,29 +41,29 @@ public RegistrySerializer(Registry registry) {
}
@Override
- public Fields serialize(R o) {
+ public @NotNull Fields serialize(R o) {
Fields fields = new Fields();
- fields.putPrimitive("name", o.getKey().toString());
- return null;
+ fields.putObject("name", o.getKey().toString());
+ return fields;
}
@Override
- protected R deserialize(Fields fields) {
- try {
- String name = fields.getAndRemovePrimitive("name", String.class);
- NamespacedKey namespacedKey;
- if (!name.contains(":")) {
- // Old variables
- namespacedKey = NamespacedKey.minecraft(name);
- } else {
- namespacedKey = NamespacedKey.fromString(name);
- }
- if (namespacedKey == null)
- return null;
- return registry.get(namespacedKey);
- } catch (StreamCorruptedException e) {
- return null;
+ protected R deserialize(Fields fields) throws StreamCorruptedException {
+ String name = fields.getAndRemoveObject("name", String.class);
+ assert name != null;
+ NamespacedKey namespacedKey;
+ if (!name.contains(":")) {
+ // Old variables
+ namespacedKey = NamespacedKey.minecraft(name);
+ } else {
+ namespacedKey = NamespacedKey.fromString(name);
}
+ if (namespacedKey == null)
+ throw new StreamCorruptedException("Invalid namespacedkey: " + name);
+ R object = registry.get(namespacedKey);
+ if (object == null)
+ throw new StreamCorruptedException("Invalid object from registry: " + namespacedKey);
+ return object;
}
@Override
diff --git a/src/main/java/ch/njol/skript/command/Commands.java b/src/main/java/ch/njol/skript/command/Commands.java
index cff08f617ae..398bdbdb12e 100644
--- a/src/main/java/ch/njol/skript/command/Commands.java
+++ b/src/main/java/ch/njol/skript/command/Commands.java
@@ -184,7 +184,7 @@ public void onServerCommand(ServerCommandEvent event) {
};
static boolean handleEffectCommand(CommandSender sender, String command) {
- if (!(sender instanceof ConsoleCommandSender || sender.hasPermission("skript.effectcommands") || SkriptConfig.allowOpsToUseEffectCommands.value() && sender.isOp()))
+ if (!(Skript.testing() || sender instanceof ConsoleCommandSender || sender.hasPermission("skript.effectcommands") || SkriptConfig.allowOpsToUseEffectCommands.value() && sender.isOp()))
return false;
try {
command = "" + command.substring(SkriptConfig.effectCommandToken.value().length()).trim();
@@ -300,7 +300,7 @@ public static void registerListeners() {
Bukkit.getPluginManager().registerEvents(new Listener() {
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
public void onPlayerChat(AsyncPlayerChatEvent event) {
- if (!SkriptConfig.enableEffectCommands.value() || !event.getMessage().startsWith(SkriptConfig.effectCommandToken.value()))
+ if ((!SkriptConfig.enableEffectCommands.value() && !Skript.testing()) || !event.getMessage().startsWith(SkriptConfig.effectCommandToken.value()))
return;
if (!event.isAsynchronous()) {
if (handleEffectCommand(event.getPlayer(), event.getMessage()))
diff --git a/src/main/java/ch/njol/skript/conditions/CondIsFireResistant.java b/src/main/java/ch/njol/skript/conditions/CondIsFireResistant.java
index 924fbd8a710..e52d8f08992 100644
--- a/src/main/java/ch/njol/skript/conditions/CondIsFireResistant.java
+++ b/src/main/java/ch/njol/skript/conditions/CondIsFireResistant.java
@@ -35,7 +35,7 @@
"if {_items::*} aren't resistant to fire:"
})
@RequiredPlugins("Spigot 1.20.5+")
-@Since("INSERT VERSION")
+@Since("2.9.0")
public class CondIsFireResistant extends PropertyCondition {
static {
diff --git a/src/main/java/ch/njol/skript/conditions/CondIsPathfinding.java b/src/main/java/ch/njol/skript/conditions/CondIsPathfinding.java
index 3fb40549421..111d06baebc 100644
--- a/src/main/java/ch/njol/skript/conditions/CondIsPathfinding.java
+++ b/src/main/java/ch/njol/skript/conditions/CondIsPathfinding.java
@@ -54,7 +54,7 @@
"clear entity within {_entity}"
})
@RequiredPlugins("Paper")
-@Since("INSERT VERSION")
+@Since("2.9.0")
public class CondIsPathfinding extends Condition {
static {
diff --git a/src/main/java/ch/njol/skript/conditions/CondIsPreferredTool.java b/src/main/java/ch/njol/skript/conditions/CondIsPreferredTool.java
index 195e8240d93..f69a183adfd 100644
--- a/src/main/java/ch/njol/skript/conditions/CondIsPreferredTool.java
+++ b/src/main/java/ch/njol/skript/conditions/CondIsPreferredTool.java
@@ -80,13 +80,14 @@ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelaye
public boolean check(Event event) {
return blocks.check(event, block ->
items.check(event, item -> {
- if (block instanceof Block) {
- return ((Block) block).isPreferredTool(item.getRandom());
- } else if (block instanceof BlockData) {
- return ((BlockData) block).isPreferredTool(item.getRandom());
- } else {
- return false;
+ ItemStack stack = item.getRandom();
+ if (stack != null) {
+ if (block instanceof Block)
+ return ((Block) block).isPreferredTool(stack);
+ if (block instanceof BlockData)
+ return ((BlockData) block).isPreferredTool(stack);
}
+ return false;
}), isNegated());
}
diff --git a/src/main/java/ch/njol/skript/conditions/CondIsResonating.java b/src/main/java/ch/njol/skript/conditions/CondIsResonating.java
index c2614aaead1..0e255b87e16 100644
--- a/src/main/java/ch/njol/skript/conditions/CondIsResonating.java
+++ b/src/main/java/ch/njol/skript/conditions/CondIsResonating.java
@@ -36,7 +36,7 @@
})
@Examples("target block is resonating")
@RequiredPlugins("Spigot 1.19.4+")
-@Since("INSERT VERSION")
+@Since("2.9.0")
public class CondIsResonating extends PropertyCondition {
static {
diff --git a/src/main/java/ch/njol/skript/conditions/CondIsRinging.java b/src/main/java/ch/njol/skript/conditions/CondIsRinging.java
index c7ddca50b1a..49bda5d6672 100644
--- a/src/main/java/ch/njol/skript/conditions/CondIsRinging.java
+++ b/src/main/java/ch/njol/skript/conditions/CondIsRinging.java
@@ -33,7 +33,7 @@
@Description("Checks to see if a bell is currently ringing. A bell typically rings for 50 game ticks.")
@Examples("target block is ringing")
@RequiredPlugins("Spigot 1.19.4+")
-@Since("INSERT VERSION")
+@Since("2.9.0")
public class CondIsRinging extends PropertyCondition {
static {
diff --git a/src/main/java/ch/njol/skript/conditions/CondIsTamed.java b/src/main/java/ch/njol/skript/conditions/CondIsTamed.java
new file mode 100644
index 00000000000..a3e342deef6
--- /dev/null
+++ b/src/main/java/ch/njol/skript/conditions/CondIsTamed.java
@@ -0,0 +1,34 @@
+package ch.njol.skript.conditions;
+
+import ch.njol.skript.conditions.base.PropertyCondition;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import org.bukkit.entity.Entity;
+import org.bukkit.entity.Tameable;
+
+@Name("Is Tamed")
+@Description("Check if a tameable entity is tamed (horse, parrot, cat, etc.).")
+@Examples({
+ "send true if {_horse} is tamed",
+ "tame {_horse} if {_horse} is untamed"
+})
+@Since("INSERT VERSION")
+public class CondIsTamed extends PropertyCondition {
+
+ static {
+ register(CondIsTamed.class, "(tamed|domesticated)", "entities");
+ }
+
+ @Override
+ public boolean check(Entity entity) {
+ return (entity instanceof Tameable) && ((Tameable) entity).isTamed();
+ }
+
+ @Override
+ protected String getPropertyName() {
+ return "tamed";
+ }
+
+}
diff --git a/src/main/java/ch/njol/skript/conditions/CondIsUnbreakable.java b/src/main/java/ch/njol/skript/conditions/CondIsUnbreakable.java
index b4886661517..5a0a7d4cd58 100644
--- a/src/main/java/ch/njol/skript/conditions/CondIsUnbreakable.java
+++ b/src/main/java/ch/njol/skript/conditions/CondIsUnbreakable.java
@@ -37,7 +37,7 @@
"if tool of {_p} is breakable:",
"\tsend \"Your tool is breakable!\" to {_p}"
})
-@Since("2.5.1, INSERT VERSION (breakable)")
+@Since("2.5.1, 2.9.0 (breakable)")
public class CondIsUnbreakable extends PropertyCondition {
static {
diff --git a/src/main/java/ch/njol/skript/conditions/CondIsUsingFeature.java b/src/main/java/ch/njol/skript/conditions/CondIsUsingFeature.java
index 4ce1c0ef0bd..39a23b740ab 100644
--- a/src/main/java/ch/njol/skript/conditions/CondIsUsingFeature.java
+++ b/src/main/java/ch/njol/skript/conditions/CondIsUsingFeature.java
@@ -39,7 +39,7 @@
"on load:",
"\tif the script is using \"example feature\":",
"\t\tbroadcast \"You're using an experimental feature!\""})
-@Since("INSERT VERSION")
+@Since("2.9.0")
public class CondIsUsingFeature extends Condition {
static {
diff --git a/src/main/java/ch/njol/skript/conditions/CondIsWhitelisted.java b/src/main/java/ch/njol/skript/conditions/CondIsWhitelisted.java
index e0d50f25c33..2f771590ff9 100644
--- a/src/main/java/ch/njol/skript/conditions/CondIsWhitelisted.java
+++ b/src/main/java/ch/njol/skript/conditions/CondIsWhitelisted.java
@@ -40,7 +40,7 @@
"if the server is whitelisted:",
"if the server whitelist is enforced:"
})
-@Since("2.5.2, INSERT VERSION (enforce, offline players)")
+@Since("2.5.2, 2.9.0 (enforce, offline players)")
@RequiredPlugins("MC 1.17+ (enforce)")
public class CondIsWhitelisted extends Condition {
diff --git a/src/main/java/ch/njol/skript/conditions/CondTooltip.java b/src/main/java/ch/njol/skript/conditions/CondTooltip.java
new file mode 100644
index 00000000000..fd1ce0515cb
--- /dev/null
+++ b/src/main/java/ch/njol/skript/conditions/CondTooltip.java
@@ -0,0 +1,87 @@
+/**
+ * This file is part of Skript.
+ *
+ * Skript is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Skript is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Skript. If not, see .
+ *
+ * Copyright Peter Güttinger, SkriptLang team and contributors
+ */
+package ch.njol.skript.conditions;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.aliases.ItemType;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.RequiredPlugins;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.lang.Condition;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.util.Kleenean;
+import org.bukkit.event.Event;
+import org.bukkit.inventory.ItemFlag;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.eclipse.jdt.annotation.Nullable;
+
+@Name("Has Item Tooltips")
+@Description({
+ "Whether the entire or additional tooltip of an item is shown or hidden.",
+ "The 'entire tooltip' is what shows to the player when they hover an item (i.e. name, lore, etc.).",
+ "The 'additional tooltip' hides certain information from certain items (potions, maps, books, fireworks, and banners)."
+})
+@Examples({
+ "send true if entire tooltip of player's tool is shown",
+ "if additional tooltip of {_item} is hidden:"
+})
+@RequiredPlugins("Spigot 1.20.5+")
+@Since("2.9.0")
+public class CondTooltip extends Condition {
+
+ static {
+ if (Skript.methodExists(ItemMeta.class, "isHideTooltip")) {// this method was added in the same version as the additional tooltip item flag
+ Skript.registerCondition(CondTooltip.class,
+ "[the] [entire|:additional] tool[ ]tip[s] of %itemtypes% (is|are) (:shown|hidden)",
+ "[the] [entire|:additional] tool[ ]tip[s] of %itemtypes% (isn't|is not|aren't|are not) (:shown|hidden)",
+ "%itemtypes%'[s] [entire|:additional] tool[ ]tip[s] (is|are) (:shown|hidden)",
+ "%itemtypes%'[s] [entire|:additional] tool[ ]tip[s] (isn't|is not|aren't|are not) (:shown|hidden)"
+ );
+ }
+ }
+
+ @SuppressWarnings("NotNullFieldNotInitialized")
+ private Expression items;
+ private boolean entire;
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+ items = (Expression) exprs[0];
+ entire = !parseResult.hasTag("additional");
+ setNegated(parseResult.hasTag("shown") ^ (matchedPattern == 1 || matchedPattern == 3));
+ return true;
+ }
+
+ @Override
+ public boolean check(Event event) {
+ if (entire)
+ return items.check(event, item -> item.getItemMeta().isHideTooltip(), isNegated());
+ return items.check(event, item -> item.getItemMeta().hasItemFlag(ItemFlag.HIDE_ADDITIONAL_TOOLTIP), isNegated());
+ }
+
+ @Override
+ public String toString(@Nullable Event event, boolean debug) {
+ return "the " + (entire ? "entire" : "additional") + " tooltip of " + items.toString(event, debug) + " is " + (isNegated() ? "hidden" : "shown");
+ }
+
+}
diff --git a/src/main/java/ch/njol/skript/config/Node.java b/src/main/java/ch/njol/skript/config/Node.java
index f59c7210f5c..cff13bda618 100644
--- a/src/main/java/ch/njol/skript/config/Node.java
+++ b/src/main/java/ch/njol/skript/config/Node.java
@@ -23,6 +23,7 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import ch.njol.skript.SkriptConfig;
import org.eclipse.jdt.annotation.Nullable;
import ch.njol.skript.Skript;
@@ -114,17 +115,27 @@ public void move(final SectionNode newParent) {
p.remove(this);
newParent.add(this);
}
-
- @SuppressWarnings("null")
- private final static Pattern linePattern = Pattern.compile("^((?:[^#]|##)*)(\\s*#(?!#).*)$");
-
+
/**
* Splits a line into value and comment.
*
* Whitespace is preserved (whitespace in front of the comment is added to the value), and any ## in the value are replaced by a single #. The comment is returned with a
* leading #, except if there is no comment in which case it will be the empty string.
+ *
+ * @param line the line to split
+ * @return A pair (value, comment).
+ */
+ public static NonNullPair splitLine(String line) {
+ return splitLine(line, new AtomicBoolean(false));
+ }
+
+ /**
+ * Splits a line into value and comment.
+ *
+ * Whitespace is preserved (whitespace in front of the comment is added to the value), and any ## not in quoted strings in the value are replaced by a single #. The comment is returned with a
+ * leading #, except if there is no comment in which case it will be the empty string.
*
- * @param line
+ * @param line the line to split
* @param inBlockComment Whether we are currently inside a block comment
* @return A pair (value, comment).
*/
@@ -138,34 +149,99 @@ public static NonNullPair splitLine(String line, AtomicBoolean i
} else if (inBlockComment.get()) { // we're inside a comment, all text is a comment
return new NonNullPair<>("", line);
}
- final Matcher m = linePattern.matcher(line);
- boolean matches = false;
- try {
- matches = line.contains("#") && m.matches();
- } catch (StackOverflowError e) { // Probably a very long line
- handleNodeStackOverflow(e, line);
+
+ // idea: find first # that is not within a string or variable name. Use state machine to determine whether a # is a comment or not.
+ int length = line.length();
+ StringBuilder finalLine = new StringBuilder(line);
+ int numRemoved = 0;
+ SplitLineState state = SplitLineState.CODE;
+ SplitLineState previousState = SplitLineState.CODE; // stores the state prior to entering %, so it can be re-set when leaving
+ // find next " or %
+ for (int i = 0; i < length; i++) {
+ char c = line.charAt(i);
+ // check for things that can be escaped by doubling
+ if (c == '%' || c == '"' || c == '#') {
+ // skip if doubled (only skip ## outside of strings)
+ if ((c != '#' || state != SplitLineState.STRING) && i + 1 < length && line.charAt(i + 1) == c) {
+ if (c == '#') { // remove duplicate #
+ finalLine.deleteCharAt(i - numRemoved);
+ numRemoved++;
+ }
+ i++;
+ continue;
+ }
+ SplitLineState tmp = state;
+ state = SplitLineState.update(c, state, previousState);
+ if (state == SplitLineState.HALT)
+ return new NonNullPair<>(finalLine.substring(0, i - numRemoved), line.substring(i));
+ // only update previous state when we go from !CODE -> CODE due to %
+ if (c == '%' && state == SplitLineState.CODE)
+ previousState = tmp;
+ }
}
- if (matches)
- return new NonNullPair<>("" + m.group(1).replace("##", "#"), "" + m.group(2));
- return new NonNullPair<>("" + line.replace("##", "#"), "");
+ return new NonNullPair<>(finalLine.toString(), "");
}
/**
- * Splits a line into value and comment.
- *
- * Whitespace is preserved (whitespace in front of the comment is added to the value), and any ## in the value are replaced by a single #. The comment is returned with a
- * leading #, except if there is no comment in which case it will be the empty string.
- *
- * @param line
- * @return A pair (value, comment).
+ * state machine:
+ * ": CODE -> STRING, STRING -> CODE, VARIABLE -> VARIABLE
+ * %: CODE -> PREVIOUS_STATE, STRING -> CODE, VARIABLE -> CODE
+ * {: CODE -> VARIABLE, STRING -> STRING, VARIABLE -> VARIABLE
+ * }: CODE -> CODE, STRING -> STRING, VARIABLE -> CODE
+ * #: CODE -> HALT, STRING -> STRING, VARIABLE -> HALT
+ * invalid characters simply return given state.
*/
- public static NonNullPair splitLine(String line) {
- return splitLine(line, new AtomicBoolean(false));
+ private enum SplitLineState {
+ HALT,
+ CODE,
+ STRING,
+ VARIABLE;
+
+ /**
+ * Updates the state given a character input.
+ * @param c character input. '"', '%', '{', '}', and '#' are valid.
+ * @param state the current state of the machine
+ * @param previousState the state of the machine when it last entered a % CODE % section
+ * @return the new state of the machine
+ */
+ private static SplitLineState update(char c, SplitLineState state, SplitLineState previousState) {
+ if (state == HALT)
+ return HALT;
+
+ switch (c) {
+ case '%':
+ if (state == CODE)
+ return previousState;
+ return CODE;
+ case '"':
+ switch (state) {
+ case CODE:
+ return STRING;
+ case STRING:
+ return CODE;
+ default:
+ return state;
+ }
+ case '{':
+ if (state == STRING)
+ return STRING;
+ return VARIABLE;
+ case '}':
+ if (state == STRING)
+ return STRING;
+ return CODE;
+ case '#':
+ if (state == STRING)
+ return STRING;
+ return HALT;
+ }
+ return state;
+ }
}
static void handleNodeStackOverflow(StackOverflowError e, String line) {
Node n = SkriptLogger.getNode();
- SkriptLogger.setNode(null); // Avoid duplicating the which node error occurred in paranthesis on every error message
+ SkriptLogger.setNode(null); // Avoid duplicating the which node error occurred in parentheses on every error message
Skript.error("There was a StackOverFlowError occurred when loading a node. This maybe from your scripts, aliases or Skript configuration.");
Skript.error("Please make your script lines shorter! Do NOT report this to SkriptLang unless it occurs with a short script line or built-in aliases!");
@@ -214,12 +290,43 @@ protected String getIndentation() {
abstract String save_i();
public final String save() {
- return getIndentation() + save_i().replace("#", "##") + comment;
+ return getIndentation() + escapeUnquotedHashtags(save_i()) + comment;
}
public void save(final PrintWriter w) {
w.println(save());
}
+
+ private static String escapeUnquotedHashtags(String input) {
+ int length = input.length();
+ StringBuilder output = new StringBuilder(input);
+ int numAdded = 0;
+ SplitLineState state = SplitLineState.CODE;
+ SplitLineState previousState = SplitLineState.CODE;
+ // find next " or %
+ for (int i = 0; i < length; i++) {
+ char c = input.charAt(i);
+ // check for things that can be escaped by doubling
+ if (c == '%' || c == '"' || c == '#') {
+ // escaped #s outside of strings
+ if (c == '#' && state != SplitLineState.STRING) {
+ output.insert(i + numAdded, "#");
+ numAdded++;
+ continue;
+ }
+ // skip if doubled (not #s)
+ if (i + 1 < length && input.charAt(i + 1) == c) {
+ i++;
+ continue;
+ }
+ SplitLineState tmp = state;
+ state = SplitLineState.update(c, state, previousState);
+ previousState = tmp;
+ }
+ }
+ return output.toString();
+ }
+
@Nullable
public SectionNode getParent() {
diff --git a/src/main/java/ch/njol/skript/doc/HTMLGenerator.java b/src/main/java/ch/njol/skript/doc/HTMLGenerator.java
index e882057cebd..d79b1e65320 100644
--- a/src/main/java/ch/njol/skript/doc/HTMLGenerator.java
+++ b/src/main/java/ch/njol/skript/doc/HTMLGenerator.java
@@ -36,6 +36,9 @@
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import org.apache.commons.lang.StringUtils;
+import org.bukkit.event.Cancellable;
+import org.bukkit.event.Event;
+import org.bukkit.event.block.BlockCanBuildEvent;
import org.eclipse.jdt.annotation.Nullable;
import org.skriptlang.skript.lang.entry.EntryData;
import org.skriptlang.skript.lang.entry.EntryValidator;
@@ -481,6 +484,9 @@ private String generateAnnotated(String descTemp, SyntaxElementInfo> info, @Nu
}
desc = desc.replace("${element.id}", ID);
+ // Cancellable
+ desc = handleIf(desc, "${if cancellable}", false);
+
// Events
Events events = c.getAnnotation(Events.class);
desc = handleIf(desc, "${if events}", events != null);
@@ -603,6 +609,17 @@ private String generateEvent(String descTemp, SkriptEventInfo> info, @Nullable
String[] keywords = info.getKeywords();
desc = desc.replace("${element.keywords}", keywords == null ? "" : Joiner.on(", ").join(keywords));
+ // Cancellable
+ boolean cancellable = false;
+ for (Class extends Event> event : info.events) {
+ if (Cancellable.class.isAssignableFrom(event) || BlockCanBuildEvent.class.isAssignableFrom(event)) {
+ cancellable = true; // let's assume all are cancellable otherwise EffCancelEvent would do the rest in action
+ break;
+ }
+ }
+ desc = handleIf(desc, "${if cancellable}", cancellable);
+ desc = desc.replace("${element.cancellable}", cancellable ? "Yes" : ""); // if not cancellable the section is hidden
+
// Documentation ID
String ID = info.getDocumentationID() != null ? info.getDocumentationID() : info.getId();
// Fix duplicated IDs
@@ -719,6 +736,9 @@ private String generateClass(String descTemp, ClassInfo> info, @Nullable Strin
}
desc = desc.replace("${element.id}", ID);
+ // Cancellable
+ desc = handleIf(desc, "${if cancellable}", false);
+
// Events
Events events = c.getAnnotation(Events.class);
desc = handleIf(desc, "${if events}", events != null);
@@ -821,6 +841,9 @@ private String generateFunction(String descTemp, JavaFunction> info) {
// Documentation ID
desc = desc.replace("${element.id}", info.getName());
+ // Cancellable
+ desc = handleIf(desc, "${if cancellable}", false);
+
// Events
desc = handleIf(desc, "${if events}", false); // Functions do not require events nor plugins (at time writing this)
diff --git a/src/main/java/ch/njol/skript/effects/EffBan.java b/src/main/java/ch/njol/skript/effects/EffBan.java
index 1e674e23ded..88f815c96df 100644
--- a/src/main/java/ch/njol/skript/effects/EffBan.java
+++ b/src/main/java/ch/njol/skript/effects/EffBan.java
@@ -26,7 +26,6 @@
import org.bukkit.OfflinePlayer;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
-import org.eclipse.jdt.annotation.Nullable;
import ch.njol.skript.Skript;
import ch.njol.skript.doc.Description;
@@ -38,6 +37,7 @@
import ch.njol.skript.lang.SkriptParser.ParseResult;
import ch.njol.skript.util.Timespan;
import ch.njol.util.Kleenean;
+import org.jetbrains.annotations.Nullable;
@Name("Ban")
@Description({"Bans or unbans a player or an IP address.",
@@ -46,19 +46,23 @@
"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.",
- "Consider using the kick effect after applying a ban."})
+ "You can optionally use 'and kick' or consider using the kick effect after applying a ban."})
@Examples({"unban player",
"ban \"127.0.0.1\"",
"IP-ban the player because \"he is an idiot\"",
- "ban player due to \"inappropriate language\" for 2 days"})
-@Since("1.4, 2.1.1 (ban reason), 2.5 (timespan)")
+ "ban player due to \"inappropriate language\" for 2 days",
+ "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 %strings/offlineplayers% [(by reason of|because [of]|on account of|due to) %-string%] [for %-timespan%]", "unban %strings/offlineplayers%",
- "ban %players% by IP [(by reason of|because [of]|on account of|due to) %-string%] [for %-timespan%]", "unban %players% by IP",
- "IP(-| )ban %players% [(by reason of|because [of]|on account of|due to) %-string%] [for %-timespan%]", "(IP(-| )unban|un[-]IP[-]ban) %players%");
+ "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")
@@ -70,6 +74,7 @@ public class EffBan extends Effect {
private boolean ban;
private boolean ipBan;
+ private boolean kick;
@SuppressWarnings({"null", "unchecked"})
@Override
@@ -79,6 +84,7 @@ public boolean init(final Expression>[] exprs, final int matchedPattern, final
expires = exprs.length > 1 ? (Expression) exprs[2] : null;
ban = matchedPattern % 2 == 0;
ipBan = matchedPattern >= 2;
+ kick = parseResult.hasTag("kick");
return true;
}
@@ -91,8 +97,9 @@ protected void execute(final Event e) {
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) o).getAddress();
+ InetSocketAddress addr = player.getAddress();
if (addr == null)
return; // Can't ban unknown IP
final String ip = addr.getAddress().getHostAddress();
@@ -102,10 +109,12 @@ protected void execute(final Event e) {
Bukkit.getBanList(BanList.Type.IP).pardon(ip);
} else {
if (ban)
- Bukkit.getBanList(BanList.Type.NAME).addBan(((Player) o).getName(), reason, expires, source); // FIXME [UUID] ban UUID
+ Bukkit.getBanList(BanList.Type.NAME).addBan(player.getName(), reason, expires, source); // FIXME [UUID] ban UUID
else
- Bukkit.getBanList(BanList.Type.NAME).pardon(((Player) o).getName());
+ 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)
@@ -130,9 +139,13 @@ protected void execute(final Event e) {
}
@Override
- public String toString(final @Nullable Event e, final boolean debug) {
- return (ipBan ? "IP-" : "") + (ban ? "" : "un") + "ban " + players.toString(e, debug) +
- (reason != null ? " on account of " + reason.toString(e, debug) : "") + (expires != null ? " for " + expires.toString(e, debug) : "");
+ public String toString(final @Nullable Event event, final boolean debug) {
+ return (ipBan ? "IP-" : "") +
+ (this.ban ? "ban " : "unban ") +
+ (kick ? "and kick " : "") +
+ this.players.toString(event, debug) +
+ (this.reason != null ? " on account of " + this.reason.toString(event, debug) : "") +
+ (expires != null ? " for " + expires.toString(event, debug) : "");
}
}
diff --git a/src/main/java/ch/njol/skript/effects/EffBroadcast.java b/src/main/java/ch/njol/skript/effects/EffBroadcast.java
index 5e2b8fbdefb..373565bfe6d 100644
--- a/src/main/java/ch/njol/skript/effects/EffBroadcast.java
+++ b/src/main/java/ch/njol/skript/effects/EffBroadcast.java
@@ -18,6 +18,13 @@
*/
package ch.njol.skript.effects;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Pattern;
+
import ch.njol.skript.Skript;
import ch.njol.skript.doc.Description;
import ch.njol.skript.doc.Examples;
@@ -31,19 +38,20 @@
import ch.njol.skript.lang.VariableString;
import ch.njol.skript.registrations.Classes;
import ch.njol.skript.util.LiteralUtils;
+import ch.njol.skript.util.SkriptColor;
+import ch.njol.skript.util.Utils;
import ch.njol.skript.util.chat.BungeeConverter;
import ch.njol.skript.util.chat.ChatMessages;
import ch.njol.util.Kleenean;
+import ch.njol.util.StringUtils;
import ch.njol.util.coll.CollectionUtils;
import net.md_5.bungee.api.chat.BaseComponent;
import org.bukkit.Bukkit;
import org.bukkit.World;
import org.bukkit.command.CommandSender;
import org.bukkit.event.Event;
-import org.eclipse.jdt.annotation.Nullable;
-
-import java.util.ArrayList;
-import java.util.List;
+import org.bukkit.event.server.BroadcastMessageEvent;
+import org.jetbrains.annotations.Nullable;
@Name("Broadcast")
@Description("Broadcasts a message to the server.")
@@ -54,6 +62,8 @@
@Since("1.0, 2.6 (broadcasting objects), 2.6.1 (using advanced formatting)")
public class EffBroadcast extends Effect {
+ private static final Pattern HEX_PATTERN = Pattern.compile("(?i)&x((?:&\\p{XDigit}){6})");
+
static {
Skript.registerEffect(EffBroadcast.class, "broadcast %objects% [(to|in) %-worlds%]");
}
@@ -65,8 +75,8 @@ public class EffBroadcast extends Effect {
@Nullable
private Expression worlds;
- @SuppressWarnings("unchecked")
@Override
+ @SuppressWarnings("unchecked")
public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
messageExpr = LiteralUtils.defendExpression(exprs[0]);
messages = messageExpr instanceof ExpressionList ?
@@ -74,37 +84,51 @@ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelaye
worlds = (Expression) exprs[1];
return LiteralUtils.canInitSafely(messageExpr);
}
-
+
+ /**
+ * This effect will call {@link BroadcastMessageEvent} as of 2.9.0.
+ */
@Override
@SuppressWarnings("deprecation")
- public void execute(Event e) {
+ public void execute(Event event) {
List receivers = new ArrayList<>();
if (worlds == null) {
receivers.addAll(Bukkit.getOnlinePlayers());
receivers.add(Bukkit.getConsoleSender());
} else {
- for (World world : worlds.getArray(e))
+ for (World world : worlds.getArray(event))
receivers.addAll(world.getPlayers());
}
for (Expression> message : getMessages()) {
if (message instanceof VariableString) {
- BaseComponent[] components = BungeeConverter.convert(((VariableString) message).getMessageComponents(e));
+ if (!dispatchEvent(getRawString(event, (VariableString) message), receivers))
+ continue;
+ BaseComponent[] components = BungeeConverter.convert(((VariableString) message).getMessageComponents(event));
receivers.forEach(receiver -> receiver.spigot().sendMessage(components));
} else if (message instanceof ExprColoured && ((ExprColoured) message).isUnsafeFormat()) { // Manually marked as trusted
- for (Object realMessage : message.getArray(e)) {
+ for (Object realMessage : message.getArray(event)) {
+ if (!dispatchEvent(Utils.replaceChatStyles((String) realMessage), receivers))
+ continue;
BaseComponent[] components = BungeeConverter.convert(ChatMessages.parse((String) realMessage));
receivers.forEach(receiver -> receiver.spigot().sendMessage(components));
}
} else {
- for (Object messageObject : message.getArray(e)) {
+ for (Object messageObject : message.getArray(event)) {
String realMessage = messageObject instanceof String ? (String) messageObject : Classes.toString(messageObject);
+ if (!dispatchEvent(Utils.replaceChatStyles(realMessage), receivers))
+ continue;
receivers.forEach(receiver -> receiver.sendMessage(realMessage));
}
}
}
}
+ @Override
+ public String toString(@Nullable Event event, boolean debug) {
+ return "broadcast " + messageExpr.toString(event, debug) + (worlds == null ? "" : " to " + worlds.toString(event, debug));
+ }
+
private Expression>[] getMessages() {
if (messageExpr instanceof ExpressionList && !messageExpr.getAnd()) {
return new Expression[] {CollectionUtils.getRandom(messages)};
@@ -112,9 +136,35 @@ private Expression>[] getMessages() {
return messages;
}
- @Override
- public String toString(@Nullable Event e, boolean debug) {
- return "broadcast " + messageExpr.toString(e, debug) + (worlds == null ? "" : " to " + worlds.toString(e, debug));
+ /**
+ * Manually calls a {@link BroadcastMessageEvent}.
+ * @param message the message
+ * @return true if the dispatched event does not get cancelled
+ */
+ @SuppressWarnings("deprecation")
+ private static boolean dispatchEvent(String message, List receivers) {
+ Set recipients = Collections.unmodifiableSet(new HashSet<>(receivers));
+ BroadcastMessageEvent broadcastEvent;
+ if (Skript.isRunningMinecraft(1, 14)) {
+ broadcastEvent = new BroadcastMessageEvent(!Bukkit.isPrimaryThread(), message, recipients);
+ } else {
+ broadcastEvent = new BroadcastMessageEvent(message, recipients);
+ }
+ Bukkit.getPluginManager().callEvent(broadcastEvent);
+ return !broadcastEvent.isCancelled();
}
-
+
+ @Nullable
+ private static String getRawString(Event event, Expression extends String> string) {
+ if (string instanceof VariableString)
+ return ((VariableString) string).toUnformattedString(event);
+ String rawString = string.getSingle(event);
+ rawString = SkriptColor.replaceColorChar(rawString);
+ if (rawString.toLowerCase().contains("&x")) {
+ rawString = StringUtils.replaceAll(rawString, HEX_PATTERN, matchResult ->
+ "<#" + matchResult.group(1).replace("&", "") + '>');
+ }
+ return rawString;
+ }
+
}
diff --git a/src/main/java/ch/njol/skript/effects/EffEnforceWhitelist.java b/src/main/java/ch/njol/skript/effects/EffEnforceWhitelist.java
index cce18cb3cd9..86b37d2c88e 100644
--- a/src/main/java/ch/njol/skript/effects/EffEnforceWhitelist.java
+++ b/src/main/java/ch/njol/skript/effects/EffEnforceWhitelist.java
@@ -48,7 +48,7 @@
"enforce the whitelist",
"unenforce the whitelist"
})
-@Since("INSERT VERSION")
+@Since("2.9.0")
@RequiredPlugins("MC 1.17+")
public class EffEnforceWhitelist extends Effect {
diff --git a/src/main/java/ch/njol/skript/effects/EffEquip.java b/src/main/java/ch/njol/skript/effects/EffEquip.java
index c66a5a1d2fd..baa7334b160 100644
--- a/src/main/java/ch/njol/skript/effects/EffEquip.java
+++ b/src/main/java/ch/njol/skript/effects/EffEquip.java
@@ -18,7 +18,9 @@
*/
package ch.njol.skript.effects;
+import ch.njol.skript.aliases.ItemData;
import org.bukkit.Material;
+import org.bukkit.Tag;
import org.bukkit.entity.AbstractHorse;
import org.bukkit.entity.ChestedHorse;
import org.bukkit.entity.LivingEntity;
@@ -93,13 +95,58 @@ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelaye
private static final boolean SUPPORTS_STEERABLE = Skript.classExists("org.bukkit.entity.Steerable");
- private static final ItemType CHESTPLATE = Aliases.javaItemType("chestplate");
- private static final ItemType LEGGINGS = Aliases.javaItemType("leggings");
- private static final ItemType BOOTS = Aliases.javaItemType("boots");
- private static final ItemType HORSE_ARMOR = Aliases.javaItemType("horse armor");
- private static final ItemType SADDLE = Aliases.javaItemType("saddle");
- private static final ItemType CHEST = Aliases.javaItemType("chest");
- private static final ItemType CARPET = Aliases.javaItemType("carpet");
+ private static ItemType CHESTPLATE;
+ private static ItemType LEGGINGS;
+ private static ItemType BOOTS;
+ private static ItemType CARPET;
+ private static final ItemType HORSE_ARMOR = new ItemType(Material.IRON_HORSE_ARMOR, Material.GOLDEN_HORSE_ARMOR, Material.DIAMOND_HORSE_ARMOR);
+ private static final ItemType SADDLE = new ItemType(Material.SADDLE);
+ private static final ItemType CHEST = new ItemType(Material.CHEST);
+
+ static {
+ boolean usesWoolCarpetTag = Skript.fieldExists(Tag.class, "WOOL_CARPET");
+ CARPET = new ItemType(usesWoolCarpetTag ? Tag.WOOL_CARPETS : Tag.CARPETS);
+ // added in 1.20.6
+ if (Skript.fieldExists(Tag.class, "ITEM_CHEST_ARMOR")) {
+ CHESTPLATE = new ItemType(Tag.ITEMS_CHEST_ARMOR);
+ LEGGINGS = new ItemType(Tag.ITEMS_LEG_ARMOR);
+ BOOTS = new ItemType(Tag.ITEMS_FOOT_ARMOR);
+ } else {
+ CHESTPLATE = new ItemType(
+ Material.LEATHER_CHESTPLATE,
+ Material.CHAINMAIL_CHESTPLATE,
+ Material.GOLDEN_CHESTPLATE,
+ Material.IRON_CHESTPLATE,
+ Material.DIAMOND_CHESTPLATE,
+ Material.ELYTRA
+ );
+
+ LEGGINGS = new ItemType(
+ Material.LEATHER_LEGGINGS,
+ Material.CHAINMAIL_LEGGINGS,
+ Material.GOLDEN_LEGGINGS,
+ Material.IRON_LEGGINGS,
+ Material.DIAMOND_LEGGINGS
+ );
+
+ BOOTS = new ItemType(
+ Material.LEATHER_BOOTS,
+ Material.CHAINMAIL_BOOTS,
+ Material.GOLDEN_BOOTS,
+ Material.IRON_BOOTS,
+ Material.DIAMOND_BOOTS
+ );
+
+ // netherite
+ if (Skript.isRunningMinecraft(1,16)) {
+ CHESTPLATE.add(new ItemData(Material.NETHERITE_CHESTPLATE));
+ LEGGINGS.add(new ItemData(Material.NETHERITE_LEGGINGS));
+ BOOTS.add(new ItemData(Material.NETHERITE_BOOTS));
+ }
+ }
+ }
+
+
private static final ItemType[] ALL_EQUIPMENT = new ItemType[] {CHESTPLATE, LEGGINGS, BOOTS, HORSE_ARMOR, SADDLE, CHEST, CARPET};
diff --git a/src/main/java/ch/njol/skript/effects/EffFireResistant.java b/src/main/java/ch/njol/skript/effects/EffFireResistant.java
index 6a7bfb79f89..c5044e376fe 100644
--- a/src/main/java/ch/njol/skript/effects/EffFireResistant.java
+++ b/src/main/java/ch/njol/skript/effects/EffFireResistant.java
@@ -40,7 +40,7 @@
"make {_items::*} not resistant to fire"
})
@RequiredPlugins("Spigot 1.20.5+")
-@Since("INSERT VERSION")
+@Since("2.9.0")
public class EffFireResistant extends Effect {
static {
diff --git a/src/main/java/ch/njol/skript/effects/EffLog.java b/src/main/java/ch/njol/skript/effects/EffLog.java
index bce9d3bae76..aaa787d130f 100644
--- a/src/main/java/ch/njol/skript/effects/EffLog.java
+++ b/src/main/java/ch/njol/skript/effects/EffLog.java
@@ -46,28 +46,31 @@
import ch.njol.util.Closeable;
import ch.njol.util.Kleenean;
-/**
- * @author Peter Güttinger
- */
@Name("Log")
@Description({"Writes text into a .log file. Skript will write these files to /plugins/Skript/logs.",
"NB: Using 'server.log' as the log file will write to the default server log. Omitting the log file altogether will log the message as '[Skript] [<script>.sk] <message>' in the server log."})
-@Examples({"on place of TNT:",
- " log \"%player% placed TNT in %world% at %location of block%\" to \"tnt/placement.log\""})
-@Since("2.0")
+@Examples({
+ "on join:",
+ "\tlog \"%player% has just joined the server!\"",
+ "on world change:",
+ "\tlog \"Someone just went to %event-world%!\" to file \"worldlog/worlds.log\"",
+ "on command:",
+ "\tlog \"%player% just executed %full command%!\" to file \"server/commands.log\" with a severity of warning"
+})
+@Since("2.0, 2.9.0 (severities)")
public class EffLog extends Effect {
static {
- Skript.registerEffect(EffLog.class, "log %strings% [(to|in) [file[s]] %-strings%]");
+ Skript.registerEffect(EffLog.class, "log %strings% [(to|in) [file[s]] %-strings%] [with [the|a] severity [of] (1:warning|2:severe)]");
}
- private final static File logsFolder = new File(Skript.getInstance().getDataFolder(), "logs");
+ private static final File logsFolder = new File(Skript.getInstance().getDataFolder(), "logs");
final static HashMap writers = new HashMap<>();
static {
Skript.closeOnDisable(new Closeable() {
@Override
public void close() {
- for (final PrintWriter pw : writers.values())
+ for (PrintWriter pw : writers.values())
pw.close();
}
});
@@ -77,42 +80,55 @@ public void close() {
private Expression messages;
@Nullable
private Expression files;
-
+
+ private Level logLevel = Level.INFO;
+ private static String getLogPrefix(Level logLevel) {
+ String timestamp = SkriptConfig.formatDate(System.currentTimeMillis());
+ if (logLevel == Level.INFO)
+ return "[" + timestamp + "]";
+ return "[" + timestamp + " " + logLevel + "]";
+ }
+
@SuppressWarnings({"unchecked", "null"})
@Override
- public boolean init(final Expression>[] exprs, final int matchedPattern, final Kleenean isDelayed, final ParseResult parser) {
+ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parser) {
messages = (Expression) exprs[0];
files = (Expression) exprs[1];
+ if (parser.mark == 1) {
+ logLevel = Level.WARNING;
+ } else if (parser.mark == 2) {
+ logLevel = Level.SEVERE;
+ }
return true;
}
@SuppressWarnings("resource")
@Override
- protected void execute(final Event e) {
- for (final String message : messages.getArray(e)) {
+ protected void execute(Event event) {
+ for (String message : messages.getArray(event)) {
if (files != null) {
- for (String s : files.getArray(e)) {
- s = s.toLowerCase(Locale.ENGLISH);
- if (!s.endsWith(".log"))
- s += ".log";
- if (s.equals("server.log")) {
- SkriptLogger.LOGGER.log(Level.INFO, message);
+ for (String logFile : files.getArray(event)) {
+ logFile = logFile.toLowerCase(Locale.ENGLISH);
+ if (!logFile.endsWith(".log"))
+ logFile += ".log";
+ if (logFile.equals("server.log")) {
+ SkriptLogger.LOGGER.log(logLevel, message);
continue;
}
- PrintWriter w = writers.get(s);
- if (w == null) {
- final File f = new File(logsFolder, s); // REMIND what if s contains '..'?
+ PrintWriter logWriter = writers.get(logFile);
+ if (logWriter == null) {
+ File logFolder = new File(logsFolder, logFile); // REMIND what if logFile contains '..'?
try {
- f.getParentFile().mkdirs();
- w = new PrintWriter(new BufferedWriter(new FileWriter(f, true)));
- writers.put(s, w);
- } catch (final IOException ex) {
- Skript.error("Cannot write to log file '" + s + "' (" + f.getPath() + "): " + ExceptionUtils.toString(ex));
+ logFolder.getParentFile().mkdirs();
+ logWriter = new PrintWriter(new BufferedWriter(new FileWriter(logFolder, true)));
+ writers.put(logFile, logWriter);
+ } catch (IOException ex) {
+ Skript.error("Cannot write to log file '" + logFile + "' (" + logFolder.getPath() + "): " + ExceptionUtils.toString(ex));
return;
}
}
- w.println("[" + SkriptConfig.formatDate(System.currentTimeMillis()) + "] " + message);
- w.flush();
+ logWriter.println(getLogPrefix(logLevel) + " " + message);
+ logWriter.flush();
}
} else {
Trigger t = getTrigger();
@@ -122,13 +138,15 @@ protected void execute(final Event e) {
if (script != null)
scriptName = script.getConfig().getFileName();
}
- Skript.info("[" + scriptName + "] " + message);
+ SkriptLogger.LOGGER.log(logLevel, "[" + scriptName + "] " + message);
}
}
}
-
+
@Override
- public String toString(final @Nullable Event e, final boolean debug) {
- return "log " + messages.toString(e, debug) + (files != null ? " to " + files.toString(e, debug) : "");
+ public String toString(@Nullable Event event, boolean debug) {
+ return "log " + messages.toString(event, debug)
+ + (files != null ? " to " + files.toString(event, debug) : "")
+ + (logLevel != Level.INFO ? "with severity " + logLevel.toString().toLowerCase(Locale.ENGLISH) : "");
}
}
diff --git a/src/main/java/ch/njol/skript/effects/EffOpenBook.java b/src/main/java/ch/njol/skript/effects/EffOpenBook.java
index 5b96114818b..95d82caf27e 100644
--- a/src/main/java/ch/njol/skript/effects/EffOpenBook.java
+++ b/src/main/java/ch/njol/skript/effects/EffOpenBook.java
@@ -18,6 +18,7 @@
*/
package ch.njol.skript.effects;
+import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.inventory.ItemStack;
@@ -49,8 +50,6 @@ public class EffOpenBook extends Effect {
}
}
- private static final ItemType bookItemType = Aliases.javaItemType("written book");
-
@SuppressWarnings("null")
private Expression book;
@SuppressWarnings("null")
@@ -69,7 +68,7 @@ protected void execute(final Event e) {
ItemType itemType = book.getSingle(e);
if (itemType != null) {
ItemStack itemStack = itemType.getRandom();
- if (itemStack != null && bookItemType.isOfType(itemStack)) {
+ if (itemStack != null && itemStack.getType() == Material.WRITTEN_BOOK) {
for (Player player : players.getArray(e)) {
player.openBook(itemStack);
}
diff --git a/src/main/java/ch/njol/skript/effects/EffPlaySound.java b/src/main/java/ch/njol/skript/effects/EffPlaySound.java
index 2d0d5cc0e5b..686f7334671 100644
--- a/src/main/java/ch/njol/skript/effects/EffPlaySound.java
+++ b/src/main/java/ch/njol/skript/effects/EffPlaySound.java
@@ -19,112 +19,170 @@
package ch.njol.skript.effects;
import ch.njol.skript.Skript;
+import ch.njol.skript.bukkitutil.AdventureSoundReceiver;
+import ch.njol.skript.bukkitutil.AdventureSoundReceiver.AdventureEmitterSoundReceiver;
+import ch.njol.skript.bukkitutil.AdventureSoundReceiver.AdventureEntitySoundReceiver;
import ch.njol.skript.doc.Description;
import ch.njol.skript.doc.Examples;
import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.RequiredPlugins;
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.bukkit.Location;
+import org.bukkit.NamespacedKey;
import org.bukkit.Sound;
import org.bukkit.SoundCategory;
import org.bukkit.World;
+import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Locale;
+import java.util.OptionalLong;
+import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Name("Play Sound")
-@Description({"Plays a sound at given location for everyone or just for given players, or plays a sound to specified players. " +
- "Both Minecraft sound names and " +
- "Spigot sound names " +
- "are supported. Playing resource pack sounds are supported too. The sound category is 'master' by default. ",
- "",
- "Please note that sound names can get changed in any Minecraft or Spigot version, or even removed from Minecraft itself."})
+@Description({
+ "Plays a sound at given location for everyone or just for given players, or plays a sound to specified players. " +
+ "Both Minecraft sound names and " +
+ "Spigot sound names " +
+ "are supported. Playing resource pack sounds are supported too. The sound category is 'master' by default. ",
+ "",
+ "When running 1.18+, playing a sound from an entity directly will result in the sound coming from said entity, even while moving.",
+ "If the sound is custom, a location emitter will follow the entity. Do note that pitch and volume ",
+ "are reflected based on the entity, and Minecraft may not use the values from this syntax.",
+ "",
+ "If using Paper 1.19.4+ or Adventure API 4.12.0+ you can utilize sound seeds. Minecraft sometimes have a set of sounds under one sound ID ",
+ "that will randomly play, to counter this, you can directly state which seed to use.",
+ "",
+ "Please note that sound names can get changed in any Minecraft or Spigot version, or even removed from Minecraft itself.",
+})
@Examples({
"play sound \"block.note_block.pling\" # It is block.note.pling in 1.12.2",
"play sound \"entity.experience_orb.pickup\" with volume 0.5 to the player",
- "play sound \"custom.music.1\" in jukebox category at {speakerBlock}"
+ "play sound \"custom.music.1\" in jukebox category at {speakerBlock}",
+ "play sound \"BLOCK_AMETHYST_BLOCK_RESONATE\" with seed 1 on target entity for the player #1.20.1+"
})
-@Since("2.2-dev28, 2.4 (sound categories)")
+@RequiredPlugins("Minecraft 1.18.1+ (entity emitters), Paper 1.19.4+ or Adventure API 4.12.0+ (sound seed)")
+@Since("2.2-dev28, 2.4 (sound categories), 2.9.0 (sound seed & entity emitter)")
public class EffPlaySound extends Effect {
- private static final Pattern KEY_PATTERN = Pattern.compile("([a-z0-9._-]+:)?[a-z0-9/._-]+");
-
+ private static final boolean ADVENTURE_API = Skript.classExists("net.kyori.adventure.sound.Sound$Builder");
+ private static final boolean PLAYER_ENTITY_EMITTER = Skript.methodExists(Player.class, "playSound", Entity.class, Sound.class, SoundCategory.class, float.class, float.class);
+ private static final boolean WORLD_ENTITY_EMITTER = Skript.methodExists(World.class, "playSound", Entity.class, String.class, SoundCategory.class, float.class, float.class);
+ public static final Pattern KEY_PATTERN = Pattern.compile("([a-z0-9._-]+:)?([a-z0-9/._-]+)");
+
static {
+ String additional = "";
+ if (ADVENTURE_API)
+ additional = "[[with] seed %-number%] ";
+ String emitterTypes = "locations";
+ if (PLAYER_ENTITY_EMITTER)
+ emitterTypes += "/entities";
Skript.registerEffect(EffPlaySound.class,
- "play sound[s] %strings% [(in|from) %-soundcategory%] " +
- "[(at|with) volume %-number%] [(and|at|with) pitch %-number%] at %locations% [(to|for) %-players%]",
- "play sound[s] %strings% [(in|from) %-soundcategory%] " +
- "[(at|with) volume %-number%] [(and|at|with) pitch %-number%] [(to|for) %players%] [(at|from) %-locations%]"
+ "play sound[s] %strings% " + additional + "[(in|from) %-soundcategory%] " +
+ "[(at|with) volume %-number%] [(and|at|with) pitch %-number%] (at|on|from) %" + emitterTypes + "% [(to|for) %-players%]",
+ "play sound[s] %strings% " + additional + "[(in|from) %-soundcategory%] " +
+ "[(at|with) volume %-number%] [(and|at|with) pitch %-number%] [(to|for) %players%] [(at|on|from) %-" + emitterTypes + "%]"
);
}
@SuppressWarnings("NotNullFieldNotInitialized")
private Expression sounds;
+
@Nullable
private Expression category;
+
+ @Nullable
+ private Expression players;
+
@Nullable
private Expression volume;
+
@Nullable
private Expression pitch;
+
@Nullable
- private Expression locations;
+ private Expression seed;
+
@Nullable
- private Expression players;
+ private Expression> emitters;
@Override
@SuppressWarnings("unchecked")
public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
sounds = (Expression) exprs[0];
- category = (Expression) exprs[1];
- volume = (Expression) exprs[2];
- pitch = (Expression) exprs[3];
+ int index = 1;
+ if (ADVENTURE_API)
+ seed = (Expression) exprs[index++];
+ category = (Expression) exprs[index++];
+ volume = (Expression) exprs[index++];
+ pitch = (Expression) exprs[index++];
if (matchedPattern == 0) {
- locations = (Expression) exprs[4];
- players = (Expression) exprs[5];
+ emitters = exprs[index++];
+ players = (Expression) exprs[index];
} else {
- players = (Expression) exprs[4];
- locations = (Expression) exprs[5];
+ players = (Expression) exprs[index++];
+ emitters = exprs[index];
}
-
return true;
}
@Override
protected void execute(Event event) {
+ OptionalLong seed = OptionalLong.empty();
+ if (this.seed != null) {
+ Number number = this.seed.getSingle(event);
+ if (number != null)
+ seed = OptionalLong.of(number.longValue());
+ }
SoundCategory category = this.category == null ? SoundCategory.MASTER : this.category.getOptionalSingle(event)
- .orElse(SoundCategory.MASTER);
+ .orElse(SoundCategory.MASTER);
float volume = this.volume == null ? 1 : this.volume.getOptionalSingle(event)
- .orElse(1)
- .floatValue();
+ .orElse(1)
+ .floatValue();
float pitch = this.pitch == null ? 1 : this.pitch.getOptionalSingle(event)
- .orElse(1)
- .floatValue();
-
+ .orElse(1)
+ .floatValue();
+
if (players != null) {
- if (locations == null) {
+ if (emitters == null) {
for (Player player : players.getArray(event)) {
- SoundReceiver.play(Player::playSound, Player::playSound, player,
- player.getLocation(), sounds.getArray(event), category, volume, pitch);
+ play(PLAYER_ENTITY_EMITTER ? Player::playSound : null, Player::playSound, ADVENTURE_API ? Player::playSound : null, ADVENTURE_API ? Player::playSound : null,
+ player, player.getLocation(), sounds.getArray(event), category, volume, pitch, seed);
}
} else {
for (Player player : players.getArray(event)) {
- for (Location location : locations.getArray(event)) {
- SoundReceiver.play(Player::playSound, Player::playSound, player,
- location, sounds.getArray(event), category, volume, pitch);
+ for (Object emitter : emitters.getArray(event)) {
+ if (emitter instanceof Entity && PLAYER_ENTITY_EMITTER) {
+ Entity entity = (Entity) emitter;
+ play(Player::playSound, Player::playSound, ADVENTURE_API ? Player::playSound : null, ADVENTURE_API ? Player::playSound : null,
+ player, entity, sounds.getArray(event), category, volume, pitch, seed);
+ } else if (emitter instanceof Location) {
+ Location location = (Location) emitter;
+ play(PLAYER_ENTITY_EMITTER ? Player::playSound : null, Player::playSound, ADVENTURE_API ? Player::playSound : null, ADVENTURE_API ? Player::playSound : null,
+ player, location, sounds.getArray(event), category, volume, pitch, seed);
+ }
}
}
}
- } else if (locations != null) {
- for (Location location : locations.getArray(event)) {
- SoundReceiver.play(World::playSound, World::playSound, location.getWorld(),
- location, sounds.getArray(event), category, volume, pitch);
+ } else if (emitters != null) {
+ for (Object emitter : emitters.getArray(event)) {
+ if (emitter instanceof Entity && WORLD_ENTITY_EMITTER) {
+ Entity entity = (Entity) emitter;
+ play(World::playSound, World::playSound, ADVENTURE_API ? World::playSound : null, ADVENTURE_API ? World::playSound : null,
+ entity.getWorld(), entity, sounds.getArray(event), category, volume, pitch, seed);
+ } else if (emitter instanceof Location) {
+ Location location = (Location) emitter;
+ play(WORLD_ENTITY_EMITTER ? World::playSound : null, World::playSound, ADVENTURE_API ? World::playSound : null, ADVENTURE_API ? World::playSound : null,
+ location.getWorld(), location, sounds.getArray(event), category, volume, pitch, seed);
+ }
}
}
}
@@ -132,53 +190,85 @@ protected void execute(Event event) {
@Override
public String toString(@Nullable Event event, boolean debug) {
StringBuilder builder = new StringBuilder()
- .append("play sound ")
- .append(sounds.toString(event, debug));
-
+ .append("play sound ")
+ .append(sounds.toString(event, debug));
+
+ if (seed != null)
+ builder.append(" with seed ").append(seed.toString(event, debug));
if (category != null)
builder.append(" in ").append(category.toString(event, debug));
-
if (volume != null)
builder.append(" with volume ").append(volume.toString(event, debug));
-
if (pitch != null)
builder.append(" with pitch ").append(pitch.toString(event, debug));
-
- if (locations != null)
- builder.append(" at ").append(locations.toString(event, debug));
-
+ if (emitters != null)
+ builder.append(" from ").append(emitters.toString(event, debug));
if (players != null)
builder.append(" to ").append(players.toString(event, debug));
return builder.toString();
}
-
+
+ private void play(@Nullable SoundReceiver entityReceiver,
+ @NotNull SoundReceiver locationReceiver,
+ @Nullable AdventureEmitterSoundReceiver adventureLocationReceiver,
+ @Nullable AdventureEntitySoundReceiver adventureEmitterReceiver,
+ @NotNull T receiver, @NotNull E emitter, @NotNull String[] sounds,
+ @NotNull SoundCategory category, float volume, float pitch, OptionalLong seed) {
+ if (!ADVENTURE_API || adventureLocationReceiver == null || adventureEmitterReceiver == null) {
+ SoundReceiver.play(entityReceiver, locationReceiver, receiver, emitter, sounds, category, volume, pitch, seed);
+ return;
+ }
+ AdventureSoundReceiver.play(adventureLocationReceiver, adventureEmitterReceiver, receiver, emitter, sounds, category, volume, pitch, seed);
+ }
+
@FunctionalInterface
- private interface SoundReceiver {
+ private interface SoundReceiver {
void play(
- @NotNull T receiver, @NotNull Location location, @NotNull S sound,
+ @NotNull T receiver, @NotNull E emitter, @NotNull String sound,
@NotNull SoundCategory category, float volume, float pitch
);
-
- static void play(
- @NotNull SoundReceiver stringReceiver,
- @NotNull SoundReceiver soundReceiver,
- @NotNull T receiver, @NotNull Location location, @NotNull String[] sounds,
- @NotNull SoundCategory category, float volume, float pitch
- ) {
+
+ static void play(
+ @Nullable SoundReceiver entityReceiver,
+ @NotNull SoundReceiver locationReceiver,
+ @NotNull T receiver, @NotNull E emitter, @NotNull String[] sounds,
+ @NotNull SoundCategory category, float volume, float pitch, OptionalLong seed
+ ) {
for (String sound : sounds) {
+ NamespacedKey key = null;
try {
Sound enumSound = Sound.valueOf(sound.toUpperCase(Locale.ENGLISH));
- soundReceiver.play(receiver, location, enumSound, category, volume, pitch);
- continue;
- } catch (IllegalArgumentException ignored) {}
-
- sound = sound.toLowerCase(Locale.ENGLISH);
- if (!KEY_PATTERN.matcher(sound).matches())
+ key = enumSound.getKey();
+ } catch (IllegalArgumentException alternative) {
+ sound = sound.toLowerCase(Locale.ENGLISH);
+ Matcher keyMatcher = KEY_PATTERN.matcher(sound);
+ if (!keyMatcher.matches())
+ continue;
+ try {
+ String namespace = keyMatcher.group(1);
+ String keyValue = keyMatcher.group(2);
+ if (namespace == null) {
+ key = NamespacedKey.minecraft(keyValue);
+ } else {
+ namespace = namespace.substring(0, namespace.length() - 1);
+ key = new NamespacedKey(namespace, keyValue);
+ }
+ } catch (IllegalArgumentException argument) {
+ // The user input invalid characters
+ }
+ }
+
+ if (key == null)
continue;
-
- stringReceiver.play(receiver, location, sound, category, volume, pitch);
+ if (emitter instanceof Location) {
+ locationReceiver.play(receiver, (Location) emitter, key.getKey(), category, volume, pitch);
+ } else if (emitter instanceof Entity && entityReceiver != null) {
+ entityReceiver.play(receiver, (Entity) emitter, key.getKey(), category, volume, pitch);
+ }
+ return;
}
}
}
+
}
diff --git a/src/main/java/ch/njol/skript/effects/EffReplace.java b/src/main/java/ch/njol/skript/effects/EffReplace.java
index f6a1fd5bad8..bf163106851 100644
--- a/src/main/java/ch/njol/skript/effects/EffReplace.java
+++ b/src/main/java/ch/njol/skript/effects/EffReplace.java
@@ -129,9 +129,10 @@ private void replace(Event event, Object[] needles, Expression> haystackExpr)
if (new ItemType(itemStack).isSimilar(needle)) {
ItemStack newItemStack = ((ItemType) replacement).getRandom();
- newItemStack.setAmount(itemStack.getAmount());
-
- inv.setItem(slot, newItemStack);
+ if (newItemStack != null) {
+ newItemStack.setAmount(itemStack.getAmount());
+ inv.setItem(slot, newItemStack);
+ }
}
}
}
diff --git a/src/main/java/ch/njol/skript/effects/EffReturn.java b/src/main/java/ch/njol/skript/effects/EffReturn.java
index 21ce3d4d475..7c4025b5d7a 100644
--- a/src/main/java/ch/njol/skript/effects/EffReturn.java
+++ b/src/main/java/ch/njol/skript/effects/EffReturn.java
@@ -19,7 +19,6 @@
package ch.njol.skript.effects;
import ch.njol.skript.Skript;
-import ch.njol.skript.classes.ClassInfo;
import ch.njol.skript.doc.Description;
import ch.njol.skript.doc.Examples;
import ch.njol.skript.doc.Name;
@@ -110,7 +109,7 @@ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelaye
protected TriggerItem walk(Event event) {
debug(event, false);
//noinspection rawtypes,unchecked
- ((ReturnHandler) handler).returnValues(value.getArray(event));
+ ((ReturnHandler) handler).returnValues(event, value);
TriggerSection parent = getParent();
while (parent != null && parent != handler) {
diff --git a/src/main/java/ch/njol/skript/effects/EffRing.java b/src/main/java/ch/njol/skript/effects/EffRing.java
index 8b66e1ce488..54ec99a97da 100644
--- a/src/main/java/ch/njol/skript/effects/EffRing.java
+++ b/src/main/java/ch/njol/skript/effects/EffRing.java
@@ -46,7 +46,7 @@
})
@Examples({"make player ring target-block"})
@RequiredPlugins("Spigot 1.19.4+")
-@Since("INSERT VERSION")
+@Since("2.9.0")
public class EffRing extends Effect {
static {
diff --git a/src/main/java/ch/njol/skript/effects/EffSort.java b/src/main/java/ch/njol/skript/effects/EffSort.java
new file mode 100644
index 00000000000..19de3a87362
--- /dev/null
+++ b/src/main/java/ch/njol/skript/effects/EffSort.java
@@ -0,0 +1,167 @@
+/**
+ * This file is part of Skript.
+ *
+ * Skript is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Skript is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Skript. If not, see .
+ *
+ * Copyright Peter Güttinger, SkriptLang team and contributors
+ */
+package ch.njol.skript.effects;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.classes.Changer.ChangeMode;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Examples;
+import ch.njol.skript.doc.Keywords;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.expressions.ExprInput;
+import ch.njol.skript.expressions.ExprSortedList;
+import ch.njol.skript.lang.Effect;
+import ch.njol.skript.lang.Expression;
+import ch.njol.skript.lang.InputSource;
+import ch.njol.skript.lang.ParseContext;
+import ch.njol.skript.lang.SkriptParser;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.skript.lang.Variable;
+import ch.njol.skript.lang.parser.ParserInstance;
+import ch.njol.util.Kleenean;
+import ch.njol.util.Pair;
+import org.bukkit.event.Event;
+import org.eclipse.jdt.annotation.Nullable;
+import org.jetbrains.annotations.UnknownNullability;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+@Name("Sort")
+@Description({
+ "Sorts a list variable using either the natural ordering of the contents or the results of the given expression.",
+ "Be warned, this will overwrite the indices of the list variable."
+})
+@Examples({
+ "set {_words::*} to \"pineapple\", \"banana\", \"yoghurt\", and \"apple\"",
+ "sort {_words::*} # alphabetical sort",
+ "sort {_words::*} by length of input # shortest to longest",
+ "sort {_words::*} based on {tastiness::%input%} # sort based on custom value"
+})
+@Since("2.9.0")
+@Keywords("input")
+public class EffSort extends Effect implements InputSource {
+
+ static {
+ Skript.registerEffect(EffSort.class, "sort %~objects% [(by|based on) <.+>]");
+ if (!ParserInstance.isRegistered(InputData.class))
+ ParserInstance.registerData(InputData.class, InputData::new);
+ }
+
+ @Nullable
+ private Expression> mappingExpr;
+ @Nullable
+ private String unparsedExpression;
+ private Variable> unsortedObjects;
+
+ private Set> dependentInputs = new HashSet<>();
+
+ @Nullable
+ private Object currentValue;
+ @UnknownNullability
+ private String currentIndex;
+
+ @Override
+ public boolean init(Expression>[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+ if (expressions[0].isSingle() || !(expressions[0] instanceof Variable)) {
+ Skript.error("You can only sort list variables!");
+ return false;
+ }
+ unsortedObjects = (Variable>) expressions[0];
+
+ if (!parseResult.regexes.isEmpty()) {
+ unparsedExpression = parseResult.regexes.get(0).group();
+ assert unparsedExpression != null;
+ InputData inputData = getParser().getData(InputData.class);
+ InputSource originalSource = inputData.getSource();
+ inputData.setSource(this);
+ mappingExpr = new SkriptParser(unparsedExpression, SkriptParser.PARSE_EXPRESSIONS, ParseContext.DEFAULT)
+ .parseExpression(Object.class);
+ inputData.setSource(originalSource);
+ return mappingExpr != null && mappingExpr.isSingle();
+ }
+ return true;
+ }
+
+ @Override
+ protected void execute(Event event) {
+ Object[] sorted;
+ if (mappingExpr == null) {
+ try {
+ sorted = unsortedObjects.stream(event)
+ .sorted(ExprSortedList::compare)
+ .toArray();
+ } catch (IllegalArgumentException | ClassCastException e) {
+ return;
+ }
+ } else {
+ Map