diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java
index e38c523eb62..c24e1fad200 100644
--- a/src/main/java/ch/njol/skript/Skript.java
+++ b/src/main/java/ch/njol/skript/Skript.java
@@ -90,15 +90,13 @@
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
+import org.skriptlang.skript.bukkit.BukkitModule;
import org.skriptlang.skript.bukkit.SkriptMetrics;
import org.skriptlang.skript.bukkit.breeding.BreedingModule;
-import org.skriptlang.skript.bukkit.brewing.BrewingModule;
-import org.skriptlang.skript.bukkit.damagesource.DamageSourceModule;
import org.skriptlang.skript.bukkit.displays.DisplayModule;
import org.skriptlang.skript.bukkit.fishing.FishingModule;
import org.skriptlang.skript.bukkit.furnace.FurnaceModule;
import org.skriptlang.skript.bukkit.input.InputModule;
-import org.skriptlang.skript.bukkit.itemcomponents.ItemComponentModule;
import org.skriptlang.skript.bukkit.log.runtime.BukkitRuntimeErrorConsumer;
import org.skriptlang.skript.bukkit.loottables.LootTableModule;
import org.skriptlang.skript.bukkit.registration.BukkitRegistryKeys;
@@ -122,11 +120,13 @@
import org.skriptlang.skript.registration.SyntaxOrigin;
import org.skriptlang.skript.registration.SyntaxRegistry;
import org.skriptlang.skript.util.ClassLoader;
+import org.skriptlang.skript.util.ReflectUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.Thread.UncaughtExceptionHandler;
+import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
@@ -187,6 +187,7 @@ public final class Skript extends JavaPlugin implements Listener {
private static org.skriptlang.skript.@UnknownNullability Skript skript = null;
private static org.skriptlang.skript.@UnknownNullability Skript unmodifiableSkript = null;
+ private static final ReflectUtils REFLECT_UTILS = new ReflectUtils();
private static boolean disabled = false;
private static boolean partDisabled = false;
@@ -596,10 +597,8 @@ public void onEnable() {
FurnaceModule.load();
LootTableModule.load();
skript.loadModules(
- new DamageSourceModule(),
- new ItemComponentModule(),
- new BrewingModule(),
- new CommonModule()
+ new CommonModule(),
+ new BukkitModule()
);
} catch (final Exception e) {
exception(e, "Could not load required .class files: " + e.getLocalizedMessage());
@@ -1125,80 +1124,6 @@ public static boolean isRunningMinecraft(final Version v) {
return minecraftVersion.compareTo(v) >= 0;
}
- /**
- * Tests whether a given class exists in the classpath.
- *
- * @param className The {@link Class#getCanonicalName() canonical name} of the class
- * @return Whether the given class exists.
- */
- public static boolean classExists(final String className) {
- try {
- Class.forName(className);
- return true;
- } catch (final ClassNotFoundException e) {
- return false;
- }
- }
-
- /**
- * Tests whether a method exists in the given class.
- *
- * @param c The class
- * @param methodName The name of the method
- * @param parameterTypes The parameter types of the method
- * @return Whether the given method exists.
- */
- public static boolean methodExists(final Class> c, final String methodName, final Class>... parameterTypes) {
- try {
- c.getDeclaredMethod(methodName, parameterTypes);
- return true;
- } catch (final NoSuchMethodException e) {
- return false;
- } catch (final SecurityException e) {
- return false;
- }
- }
-
- /**
- * Tests whether a method exists in the given class, and whether the return type matches the expected one.
- *
- * Note that this method doesn't work properly if multiple methods with the same name and parameters exist but have different return types.
- *
- * @param c The class
- * @param methodName The name of the method
- * @param parameterTypes The parameter types of the method
- * @param returnType The expected return type
- * @return Whether the given method exists.
- */
- public static boolean methodExists(final Class> c, final String methodName, final Class>[] parameterTypes, final Class> returnType) {
- try {
- final Method m = c.getDeclaredMethod(methodName, parameterTypes);
- return m.getReturnType() == returnType;
- } catch (final NoSuchMethodException e) {
- return false;
- } catch (final SecurityException e) {
- return false;
- }
- }
-
- /**
- * Tests whether a field exists in the given class.
- *
- * @param c The class
- * @param fieldName The name of the field
- * @return Whether the given field exists.
- */
- public static boolean fieldExists(final Class> c, final String fieldName) {
- try {
- c.getDeclaredField(fieldName);
- return true;
- } catch (final NoSuchFieldException e) {
- return false;
- } catch (final SecurityException e) {
- return false;
- }
- }
-
@Nullable
static Metrics metrics;
@@ -2138,4 +2063,135 @@ public SkriptUpdater getUpdater() {
return updater;
}
+ //
+ /**
+ * Tests whether a given class exists in the classpath.
+ *
+ * @param className The {@link Class#getCanonicalName() canonical name} of the class
+ * @return Whether the given class exists.
+ */
+ public static boolean classExists(String className) {
+ return REFLECT_UTILS.classExists(className);
+ }
+
+ /**
+ * @param className The full package and class name.
+ * @return The resulting {@link Class} if found, otherwise {@code null}.
+ */
+ public static @Nullable Class> getClass(String className) {
+ return REFLECT_UTILS.getClass(className);
+ }
+
+ /**
+ * Tests whether a method exists in the given class.
+ *
+ * @param c The class
+ * @param methodName The name of the method
+ * @param parameterTypes The parameter types of the method
+ * @return Whether the given method exists.
+ */
+ public static boolean methodExists(Class> c, String methodName, Class> @Nullable ... parameterTypes) {
+ return REFLECT_UTILS.methodExists(c, methodName, parameterTypes);
+ }
+
+ /**
+ * Tests whether a method exists in the given class, and whether the return type matches the expected one.
+ *
+ * Note that this method doesn't work properly if multiple methods with the same name and parameters exist but have different return types.
+ *
+ * @param c The class
+ * @param methodName The name of the method
+ * @param parameterTypes The parameter types of the method
+ * @param returnType The expected return type
+ * @return Whether the given method exists.
+ */
+ public static boolean methodExists(Class> c, String methodName, Class> @Nullable [] parameterTypes, Class> returnType) {
+ return REFLECT_UTILS.methodExists(c, methodName, parameterTypes, returnType);
+ }
+
+ /**
+ * @param c The {@link Class} to get the method from.
+ * @param methodName The name of the method.
+ * @param params The {@link Class}es used as parameters for the desired method.
+ * @return The resulting {@link Method} if it exists, otherwise {@code null}.
+ */
+ public static @Nullable Method getMethod(Class> c, String methodName, Class> @Nullable ... params) {
+ return REFLECT_UTILS.getMethod(c, methodName, params);
+ }
+
+ /**
+ * @param c The {@link Class} to get the method from.
+ * @param methodName The name of the method.
+ * @param params The {@link Class}es used as parameters for the desired method.
+ * @param returnType The return type of the desired method.
+ * @return The resulting {@link Method} if it exists, otherwise {@code null}.
+ */
+ public static @Nullable Method getMethod(Class> c, String methodName, Class> @Nullable [] params, @Nullable Class> returnType) {
+ return REFLECT_UTILS.getMethod(c, methodName, params, returnType);
+ }
+
+ /**
+ * Tests whether a field exists in the given class.
+ *
+ * @param c The class
+ * @param fieldName The name of the field
+ * @return Whether the given field exists.
+ */
+ public static boolean fieldExists(Class> c, String fieldName) {
+ return REFLECT_UTILS.fieldExists(c, fieldName);
+ }
+
+ /**
+ * @param c The {@link Class} to get the field from.
+ * @param fieldName The name of the field.
+ * @return The resulting {@link Field} if it exists, otherwise {@code null}.
+ */
+ public static @Nullable Field getField(Class> c, String fieldName) {
+ return REFLECT_UTILS.getField(c, fieldName);
+ }
+
+ /**
+ * Invoke a static {@link Method}.
+ * @param method The {@link Method} to invoke.
+ * @return The result of the invocation if successful, otherwise {@code null}.
+ * @param The expected return type from the invocation.
+ */
+ public static @Nullable Type invokeMethod(Method method) {
+ return REFLECT_UTILS.invokeMethod(method);
+ }
+
+ /**
+ * Invoke a {@link Method}.
+ * @param method The {@link Method} to invoke.
+ * @param holder The holder object to invoke for.
+ * @param params The parameters to pass into the invocation.
+ * @return The result of the invocation if successful, otherwise {@code null}.
+ * @param The expected return type from the invocation.
+ */
+ public static @Nullable Type invokeMethod(Method method, @Nullable Object holder, Object @Nullable ... params) {
+ return REFLECT_UTILS.invokeMethod(method, holder, params);
+ }
+
+ /**
+ * Gets the values of a static {@link Field}.
+ * @param field The {@link Field} to get from.
+ * @return The value of the {@link Field}.
+ * @param The expected return type.
+ */
+ public static @Nullable Type getFieldValue(Field field) {
+ return REFLECT_UTILS.getFieldValue(field);
+ }
+
+ /**
+ * Gets the values of a {@link Field}.
+ * @param field The {@link Field} to get from.
+ * @param holder The holder object to get the field for.
+ * @return The value of the {@link Field}.
+ * @param The expected return type.
+ */
+ public static @Nullable Type getFieldValue(Field field, @Nullable Object holder) {
+ return REFLECT_UTILS.getFieldValue(field, holder);
+ }
+ //
+
}
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 9794bfc2e22..a78702ecfec 100644
--- a/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java
+++ b/src/main/java/ch/njol/skript/classes/data/BukkitClasses.java
@@ -4,12 +4,17 @@
import ch.njol.skript.bukkitutil.BukkitUtils;
import ch.njol.skript.bukkitutil.EntityUtils;
import ch.njol.skript.bukkitutil.SkriptTeleportFlag;
-import ch.njol.skript.classes.*;
+import ch.njol.skript.classes.ClassInfo;
+import ch.njol.skript.classes.EnumClassInfo;
+import ch.njol.skript.classes.Parser;
+import ch.njol.skript.classes.PatternedParser;
+import ch.njol.skript.classes.Serializer;
import ch.njol.skript.classes.registry.RegistryClassInfo;
import ch.njol.skript.expressions.ExprDamageCause;
import ch.njol.skript.expressions.base.EventValueExpression;
import ch.njol.skript.lang.ParseContext;
import ch.njol.skript.lang.util.SimpleLiteral;
+import org.skriptlang.skript.bukkit.paperutil.CopperState;
import ch.njol.skript.registrations.Classes;
import ch.njol.skript.util.BlockUtils;
import ch.njol.skript.util.PotionEffectUtils;
@@ -23,10 +28,16 @@
import org.bukkit.block.DoubleChest;
import org.bukkit.block.banner.PatternType;
import org.bukkit.block.data.BlockData;
+import org.bukkit.block.data.type.CopperGolemStatue;
import org.bukkit.command.CommandSender;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.enchantments.EnchantmentOffer;
-import org.bukkit.entity.*;
+import org.bukkit.entity.EntitySnapshot;
+import org.bukkit.entity.Item;
+import org.bukkit.entity.LivingEntity;
+import org.bukkit.entity.Projectile;
+import org.bukkit.entity.Vehicle;
+import org.bukkit.entity.Villager;
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
import org.bukkit.event.entity.EntityDamageEvent.DamageCause;
import org.bukkit.event.entity.EntityPotionEffectEvent;
@@ -52,8 +63,14 @@
import org.bukkit.util.CachedServerIcon;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.Nullable;
-import org.skriptlang.skript.bukkit.base.types.*;
+import org.skriptlang.skript.bukkit.base.types.BlockClassInfo;
+import org.skriptlang.skript.bukkit.base.types.EntityClassInfo;
import org.skriptlang.skript.bukkit.base.types.EntityClassInfo.EntityChanger;
+import org.skriptlang.skript.bukkit.base.types.InventoryClassInfo;
+import org.skriptlang.skript.bukkit.base.types.ItemStackClassInfo;
+import org.skriptlang.skript.bukkit.base.types.NameableClassInfo;
+import org.skriptlang.skript.bukkit.base.types.OfflinePlayerClassInfo;
+import org.skriptlang.skript.bukkit.base.types.PlayerClassInfo;
import org.skriptlang.skript.lang.properties.Property;
import org.skriptlang.skript.lang.properties.PropertyHandler.ExpressionPropertyHandler;
@@ -1140,5 +1157,23 @@ public String toVariableNameString(WorldBorder border) {
.since("2.12")
);
+ //noinspection unchecked,rawtypes
+ Classes.registerClass(new EnumClassInfo<>((Class) CopperState.getStateClass(), "weatheringcopperstate", "weathering copper states")
+ .user("(weathering ?)?copper ?states?")
+ .name("Weathering Copper State")
+ .description("The weathering state of a copper golem or copper block.")
+ .since("INSERT VERSION")
+ );
+
+ if (Skript.classExists("org.bukkit.block.data.type.CopperGolemStatue$Pose")) {
+ Classes.registerClass(new EnumClassInfo<>(CopperGolemStatue.Pose.class, "coppergolempose", "copper golem poses")
+ .user("copper ?golem ?(statue ?)?poses?")
+ .name("Copper Golem Pose")
+ .description("The pose of a copper golem statue.")
+ .requiredPlugins("Minecraft 1.21.9+")
+ .since("INSERT VERSION")
+ );
+ }
+
}
}
diff --git a/src/main/java/ch/njol/skript/entity/CopperGolemData.java b/src/main/java/ch/njol/skript/entity/CopperGolemData.java
new file mode 100644
index 00000000000..31cd225bb0f
--- /dev/null
+++ b/src/main/java/ch/njol/skript/entity/CopperGolemData.java
@@ -0,0 +1,116 @@
+package ch.njol.skript.entity;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.lang.Literal;
+import ch.njol.skript.lang.SkriptParser.ParseResult;
+import ch.njol.skript.localization.Language;
+import ch.njol.util.Kleenean;
+import io.papermc.paper.world.WeatheringCopperState;
+import org.bukkit.entity.CopperGolem;
+import org.bukkit.entity.CopperGolem.Oxidizing.Waxed;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Objects;
+
+public class CopperGolemData extends EntityData {
+
+ static {
+ if (Skript.classExists("org.bukkit.entity.CopperGolem"))
+ register(CopperGolemData.class, "copper golem", CopperGolem.class, 0, "copper golem");
+ }
+
+ private Kleenean waxed = Kleenean.UNKNOWN;
+ private @Nullable WeatheringCopperState state;
+
+ public CopperGolemData() {}
+
+ public CopperGolemData(@Nullable Kleenean waxed, @Nullable WeatheringCopperState state) {
+ this.waxed = waxed == null ? Kleenean.UNKNOWN : waxed;
+ this.state = state;
+ }
+
+ @Override
+ protected boolean init(Literal>[] exprs, int matchedCodeName, int matchedPattern, ParseResult parseResult) {
+ //noinspection unchecked
+ Literal expr = (Literal) exprs[0];
+ if (expr != null)
+ state = expr.getSingle();
+ if (matchedPattern == 1) {
+ waxed = Kleenean.TRUE;
+ } else if (matchedPattern == 2) {
+ waxed = Kleenean.FALSE;
+ }
+ return true;
+ }
+
+ @Override
+ protected boolean init(@Nullable Class extends CopperGolem> entityClass, @Nullable CopperGolem golem) {
+ if (golem != null) {
+ state = golem.getWeatheringState();
+ waxed = Kleenean.get(golem.getOxidizing() instanceof Waxed);
+ }
+ return true;
+ }
+
+ @Override
+ public void set(CopperGolem golem) {
+ if (state != null)
+ golem.setWeatheringState(state);
+ if (waxed.isTrue())
+ golem.setOxidizing(CopperGolem.Oxidizing.waxed());
+ }
+
+ @Override
+ protected boolean match(CopperGolem golem) {
+ if (!dataMatch(state, golem.getWeatheringState()))
+ return false;
+ return kleeneanMatch(waxed, Kleenean.get(golem.getOxidizing() instanceof Waxed));
+ }
+
+ @Override
+ public Class extends CopperGolem> getType() {
+ return CopperGolem.class;
+ }
+
+ @Override
+ public @NotNull EntityData> getSuperType() {
+ return new CopperGolemData();
+ }
+
+ @Override
+ protected int hashCode_i() {
+ return Objects.hashCode(state) + waxed.hashCode();
+ }
+
+ @Override
+ protected boolean equals_i(EntityData> entityData) {
+ if (!(entityData instanceof CopperGolemData other))
+ return false;
+ return state == other.state && waxed == other.waxed;
+ }
+
+ @Override
+ public boolean isSupertypeOf(EntityData> entityData) {
+ if (!(entityData instanceof CopperGolemData other))
+ return false;
+ return dataMatch(state, other.state) && kleeneanMatch(waxed, other.waxed);
+ }
+
+ @Override
+ public String toString(int flags) {
+ StringBuilder builder = new StringBuilder();
+ if (waxed.isTrue()) {
+ builder.append("waxed ");
+ } else if (waxed.isFalse()) {
+ builder.append("unwaxed ");
+ }
+ if (state != null)
+ builder.append(Language.getList("weathering copper states." + state.name())[0] + " ");
+ builder.append("copper golem");
+ if (isPlural().isTrue())
+ builder.append("s");
+ return builder.toString();
+ }
+
+}
diff --git a/src/main/java/ch/njol/skript/entity/SimpleEntityData.java b/src/main/java/ch/njol/skript/entity/SimpleEntityData.java
index d723321dca0..ed0292a3391 100644
--- a/src/main/java/ch/njol/skript/entity/SimpleEntityData.java
+++ b/src/main/java/ch/njol/skript/entity/SimpleEntityData.java
@@ -241,7 +241,6 @@ private static void addSuperEntity(String codeName, Class extends Entity> enti
addSimpleEntity("happy ghast", HappyGhast.class);
if (Skript.isRunningMinecraft(1, 21, 9)) {
- addSimpleEntity("copper golem", CopperGolem.class);
addSimpleEntity("mannequin", Mannequin.class);
}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/BukkitModule.java b/src/main/java/org/skriptlang/skript/bukkit/BukkitModule.java
new file mode 100644
index 00000000000..b20f85256dd
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/BukkitModule.java
@@ -0,0 +1,47 @@
+package org.skriptlang.skript.bukkit;
+
+import ch.njol.skript.Skript;
+import org.skriptlang.skript.addon.AddonModule;
+import org.skriptlang.skript.addon.SkriptAddon;
+import org.skriptlang.skript.bukkit.brewing.BrewingModule;
+import org.skriptlang.skript.bukkit.damagesource.DamageSourceModule;
+import org.skriptlang.skript.bukkit.elements.conditions.CondIsWaxed;
+import org.skriptlang.skript.bukkit.elements.effects.EffWax;
+import org.skriptlang.skript.bukkit.elements.expressions.ExprCopperGolemOxidationTime;
+import org.skriptlang.skript.bukkit.elements.expressions.ExprCopperGolemPose;
+import org.skriptlang.skript.bukkit.elements.expressions.ExprCopperState;
+import org.skriptlang.skript.bukkit.itemcomponents.ItemComponentModule;
+import org.skriptlang.skript.registration.SyntaxRegistry;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Consumer;
+
+public class BukkitModule implements AddonModule {
+
+ @Override
+ public void load(SkriptAddon addon) {
+ addon.loadModules(
+ new DamageSourceModule(),
+ new ItemComponentModule(),
+ new BrewingModule()
+ );
+
+ Set> elementsToLoad = new HashSet<>();
+
+ elementsToLoad.addAll(Set.of(
+ CondIsWaxed::register,
+ EffWax::register,
+ ExprCopperState::register
+ ));
+
+ if (Skript.classExists("org.bukkit.entity.CopperGolem"))
+ elementsToLoad.add(ExprCopperGolemOxidationTime::register);
+ if (Skript.classExists("org.bukkit.block.data.type.CopperGolemStatue"))
+ elementsToLoad.add(ExprCopperGolemPose::register);
+
+ SyntaxRegistry registry = addon.syntaxRegistry();
+ elementsToLoad.forEach(consumer -> consumer.accept(registry));
+ }
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/elements/conditions/CondIsWaxed.java b/src/main/java/org/skriptlang/skript/bukkit/elements/conditions/CondIsWaxed.java
new file mode 100644
index 00000000000..2cec6d85289
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/elements/conditions/CondIsWaxed.java
@@ -0,0 +1,59 @@
+package org.skriptlang.skript.bukkit.elements.conditions;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.conditions.base.PropertyCondition;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Example;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.RequiredPlugins;
+import ch.njol.skript.doc.Since;
+import com.destroystokyo.paper.MaterialTags;
+import org.bukkit.block.Block;
+import org.bukkit.entity.CopperGolem;
+import org.bukkit.entity.CopperGolem.Oxidizing.Waxed;
+import org.skriptlang.skript.registration.SyntaxRegistry;
+
+@Name("Is Waxed")
+@Description("Whether a copper golem or copper block is waxed.")
+@Example("""
+ if last spawned copper golem is not waxed:
+ wax last spawned copper golem
+ """)
+@Example("""
+ if {_block} is waxed:
+ unwax {_block}
+ """)
+@RequiredPlugins("Minecraft 1.21.9+ (copper golems)")
+@Since("INSERT VERSION")
+public class CondIsWaxed extends PropertyCondition {
+
+ private static final boolean COPPER_GOLEM_EXISTS = Skript.classExists("org.bukkit.entity.CopperGolem");
+
+ public static void register(SyntaxRegistry registry) {
+ String type = "blocks";
+ if (Skript.classExists("org.bukkit.entity.CopperGolem"))
+ type = "entities/blocks";
+ registry.register(
+ SyntaxRegistry.CONDITION,
+ infoBuilder(CondIsWaxed.class, PropertyType.BE, "waxed", type)
+ .supplier(CondIsWaxed::new)
+ .build()
+ );
+ }
+
+ @Override
+ public boolean check(Object object) {
+ if (COPPER_GOLEM_EXISTS && object instanceof CopperGolem golem) {
+ return golem.getOxidizing() instanceof Waxed;
+ } else if (object instanceof Block block) {
+ return MaterialTags.WAXED_COPPER_BLOCKS.isTagged(block);
+ }
+ return false;
+ }
+
+ @Override
+ protected String getPropertyName() {
+ return "waxed";
+ }
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/elements/effects/EffWax.java b/src/main/java/org/skriptlang/skript/bukkit/elements/effects/EffWax.java
new file mode 100644
index 00000000000..5f0dfa32c83
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/elements/effects/EffWax.java
@@ -0,0 +1,114 @@
+package org.skriptlang.skript.bukkit.elements.effects;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Example;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.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.skript.lang.SyntaxStringBuilder;
+import ch.njol.util.Kleenean;
+import com.destroystokyo.paper.MaterialTags;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import org.bukkit.Material;
+import org.bukkit.block.Block;
+import org.bukkit.entity.CopperGolem;
+import org.bukkit.entity.CopperGolem.Oxidizing;
+import org.bukkit.entity.CopperGolem.Oxidizing.Waxed;
+import org.bukkit.event.Event;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.registration.SyntaxInfo;
+import org.skriptlang.skript.registration.SyntaxRegistry;
+
+@Name("Wax")
+@Description("""
+ Wax or unwax a copper golem or copper block.
+ This does not change the weathering copper state of entities and blocks.
+ """)
+@Example("""
+ if last spawned copper golem is not waxed:
+ wax last spawned copper golem
+ """)
+@Example("""
+ if {_block} is waxed:
+ unwax {_block}
+ """)
+@RequiredPlugins("Minecraft 1.21.9+ (copper golems)")
+@Since("INSERT VERSION")
+public class EffWax extends Effect {
+
+ private static final BiMap WAX_CONVERSION;
+ private static final BiMap UNWAX_CONVERSION = HashBiMap.create();
+ private static final boolean COPPER_GOLEM_EXISTS = Skript.classExists("org.bukkit.entity.CopperGolem");
+
+ static {
+ for (Material waxed : MaterialTags.WAXED_COPPER_BLOCKS.getValues()) {
+ Material unwaxed = Material.valueOf(waxed.name().replaceAll("WAXED_", ""));
+ UNWAX_CONVERSION.put(waxed, unwaxed);
+ }
+ WAX_CONVERSION = UNWAX_CONVERSION.inverse();
+ }
+
+ public static void register(SyntaxRegistry registry) {
+ String type = "%blocks%";
+ if (COPPER_GOLEM_EXISTS)
+ type = "%entities/blocks%";
+
+ registry.register(
+ SyntaxRegistry.EFFECT,
+ SyntaxInfo.builder(EffWax.class)
+ .addPatterns("[:un]wax " + type)
+ .supplier(EffWax::new)
+ .build()
+ );
+ }
+
+ private boolean wax;
+ private Expression> objects;
+
+ @Override
+ public boolean init(Expression>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
+ objects = exprs[0];
+ wax = !parseResult.hasTag("un");
+ return true;
+ }
+
+ @Override
+ protected void execute(Event event) {
+ for (Object object : objects.getArray(event)) {
+ if (COPPER_GOLEM_EXISTS && object instanceof CopperGolem golem) {
+ boolean isWaxed = golem.getOxidizing() instanceof Waxed;
+ if (wax == isWaxed)
+ continue;
+ CopperGolem.Oxidizing oxidizing = wax ? Oxidizing.waxed() : Oxidizing.unset();
+ golem.setOxidizing(oxidizing);
+ } else if (object instanceof Block block) {
+ if (!MaterialTags.COPPER_BLOCKS.isTagged(block))
+ continue;
+ BiMap conversion = wax ? WAX_CONVERSION : UNWAX_CONVERSION;
+ if (conversion.containsKey(block.getType())) {
+ Material material = conversion.get(block.getType());
+ assert material != null;
+ block.setType(material);
+ }
+ }
+ }
+ }
+
+ @Override
+ public String toString(@Nullable Event event, boolean debug) {
+ SyntaxStringBuilder builder = new SyntaxStringBuilder(event, debug);
+ if (wax) {
+ builder.append("wax");
+ } else {
+ builder.append("unwax");
+ }
+ builder.append(objects);
+ return builder.toString();
+ }
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/elements/expressions/ExprCopperGolemOxidationTime.java b/src/main/java/org/skriptlang/skript/bukkit/elements/expressions/ExprCopperGolemOxidationTime.java
new file mode 100644
index 00000000000..4112ce593d6
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/elements/expressions/ExprCopperGolemOxidationTime.java
@@ -0,0 +1,99 @@
+package org.skriptlang.skript.bukkit.elements.expressions;
+
+import ch.njol.skript.classes.Changer.ChangeMode;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Example;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.RequiredPlugins;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.expressions.base.SimplePropertyExpression;
+import ch.njol.skript.util.Timespan;
+import ch.njol.skript.util.Timespan.TimePeriod;
+import ch.njol.util.coll.CollectionUtils;
+import org.bukkit.entity.CopperGolem;
+import org.bukkit.entity.CopperGolem.Oxidizing;
+import org.bukkit.entity.CopperGolem.Oxidizing.AtTime;
+import org.bukkit.entity.Entity;
+import org.bukkit.event.Event;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.registration.SyntaxRegistry;
+
+@Name("Time Until Oxidation")
+@Description("""
+ The time until a copper golem oxidizes to its next state. (Normal -> Exposed -> Weathered -> Oxidized).
+ Copper golems that are waxed do not go through oxidation.
+ Setting or resetting the time until oxidation on a waxed copper golem will remove the waxed state.
+ Resetting the time until oxidation uses vanilla behavior of generating a random time between 7 hours and 7 hours 40 minutes.
+ """)
+@Example("set {_time} to the time until oxidation of last spawned copper golem")
+@Example("set the time until oxidation of last spawned copper golem to 10 seconds")
+@Example("clear the time until oxidation of last spawned copper golem")
+@RequiredPlugins("Minecraft 1.21.9+")
+@Since("INSERT VERSION")
+public class ExprCopperGolemOxidationTime extends SimplePropertyExpression {
+
+ public static void register(SyntaxRegistry registry) {
+ registry.register(
+ SyntaxRegistry.EXPRESSION,
+ infoBuilder(
+ ExprCopperGolemOxidationTime.class,
+ Timespan.class,
+ "time until oxidation",
+ "entities",
+ false
+ ).supplier(ExprCopperGolemOxidationTime::new)
+ .build()
+ );
+ }
+
+ @Override
+ public @Nullable Timespan convert(Entity entity) {
+ if (!(entity instanceof CopperGolem golem))
+ return null;
+ if (!(golem.getOxidizing() instanceof AtTime atTime))
+ return null;
+ long worldTime = golem.getWorld().getGameTime();
+ long oxidationTime = atTime.time();
+ if (worldTime > oxidationTime)
+ return null;
+ return new Timespan(TimePeriod.TICK, oxidationTime - worldTime);
+ }
+
+ @Override
+ public Class> @Nullable [] acceptChange(ChangeMode mode) {
+ if (mode == ChangeMode.SET || mode == ChangeMode.RESET)
+ return CollectionUtils.array(Timespan.class);
+ return null;
+ }
+
+ @Override
+ public void change(Event event, Object @Nullable [] delta, ChangeMode mode) {
+ if (mode == ChangeMode.SET) {
+ assert delta != null;
+ long ticks = ((Timespan) delta[0]).getAs(TimePeriod.TICK);
+ for (Entity entity : getExpr().getArray(event)) {
+ if (!(entity instanceof CopperGolem golem))
+ continue;
+ long worldTime = golem.getWorld().getGameTime();
+ golem.setOxidizing(Oxidizing.atTime(worldTime + ticks));
+ }
+ } else if (mode == ChangeMode.RESET) {
+ for (Entity entity : getExpr().getArray(event)) {
+ if (!(entity instanceof CopperGolem golem))
+ continue;
+ golem.setOxidizing(Oxidizing.unset());
+ }
+ }
+ }
+
+ @Override
+ public Class extends Timespan> getReturnType() {
+ return Timespan.class;
+ }
+
+ @Override
+ protected String getPropertyName() {
+ return "time until oxidation";
+ }
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/elements/expressions/ExprCopperGolemPose.java b/src/main/java/org/skriptlang/skript/bukkit/elements/expressions/ExprCopperGolemPose.java
new file mode 100644
index 00000000000..0cb02a25632
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/elements/expressions/ExprCopperGolemPose.java
@@ -0,0 +1,77 @@
+package org.skriptlang.skript.bukkit.elements.expressions;
+
+import ch.njol.skript.classes.Changer.ChangeMode;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Example;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.RequiredPlugins;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.expressions.base.SimplePropertyExpression;
+import ch.njol.util.coll.CollectionUtils;
+import org.bukkit.block.Block;
+import org.bukkit.block.data.type.CopperGolemStatue;
+import org.bukkit.block.data.type.CopperGolemStatue.Pose;
+import org.bukkit.event.Event;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.registration.SyntaxRegistry;
+
+@Name("Copper Golem Statue Pose")
+@Description("The pose of a copper golem statue.")
+@Example("set {_pose} to the copper golem statue pose of {_statue}")
+@Example("set the copper golem pose of {_statue} to running")
+@RequiredPlugins("Minecraft 1.21.9+")
+@Since("INSERT VERSION")
+public class ExprCopperGolemPose extends SimplePropertyExpression {
+
+ public static void register(SyntaxRegistry registry) {
+ registry.register(
+ SyntaxRegistry.EXPRESSION,
+ infoBuilder(
+ ExprCopperGolemPose.class,
+ Pose.class,
+ "copper golem [statue] pose[s]",
+ "blocks",
+ false
+ ).supplier(ExprCopperGolemPose::new)
+ .build()
+ );
+ }
+
+ @Override
+ public @Nullable Pose convert(Block block) {
+ if (block.getBlockData() instanceof CopperGolemStatue statue)
+ return statue.getCopperGolemPose();
+ return null;
+ }
+
+ @Override
+ public Class> @Nullable [] acceptChange(ChangeMode mode) {
+ if (mode == ChangeMode.SET)
+ return CollectionUtils.array(Pose.class);
+ return null;
+ }
+
+ @Override
+ public void change(Event event, Object @Nullable [] delta, ChangeMode mode) {
+ assert delta != null;
+ Pose pose = (Pose) delta[0];
+
+ for (Block block : getExpr().getArray(event)) {
+ if (block.getBlockData() instanceof CopperGolemStatue statue) {
+ statue.setCopperGolemPose(pose);
+ block.setBlockData(statue, true);
+ }
+ }
+ }
+
+ @Override
+ public Class extends Pose> getReturnType() {
+ return Pose.class;
+ }
+
+ @Override
+ protected String getPropertyName() {
+ return "copper golem pose";
+ }
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/elements/expressions/ExprCopperState.java b/src/main/java/org/skriptlang/skript/bukkit/elements/expressions/ExprCopperState.java
new file mode 100644
index 00000000000..73b26569794
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/elements/expressions/ExprCopperState.java
@@ -0,0 +1,193 @@
+package org.skriptlang.skript.bukkit.elements.expressions;
+
+import ch.njol.skript.Skript;
+import ch.njol.skript.classes.Changer.ChangeMode;
+import ch.njol.skript.doc.Description;
+import ch.njol.skript.doc.Example;
+import ch.njol.skript.doc.Name;
+import ch.njol.skript.doc.RequiredPlugins;
+import ch.njol.skript.doc.Since;
+import ch.njol.skript.expressions.base.SimplePropertyExpression;
+import ch.njol.util.coll.CollectionUtils;
+import com.destroystokyo.paper.MaterialTags;
+import io.papermc.paper.world.WeatheringCopperState;
+import org.bukkit.Material;
+import org.bukkit.block.Block;
+import org.bukkit.entity.CopperGolem;
+import org.bukkit.event.Event;
+import org.jetbrains.annotations.Nullable;
+import org.skriptlang.skript.bukkit.paperutil.CopperState;
+import org.skriptlang.skript.registration.SyntaxRegistry;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+@Name("Weathering Copper State")
+@Description("""
+ The weathering copper state of a copper golem or copper block.
+ Changing the copper state does not change the waxed state.
+ """)
+@Example("""
+ if the copper state of last spawned copper golem is weathered:
+ set the copper state of last spawned golem to normal
+ """)
+@Example("""
+ if the weathering copper state of {_block} is not oxidized:
+ set the weathering copper state of {_block} to oxidized
+ """)
+@RequiredPlugins("Minecraft 1.21.9+ (copper golems)")
+@Since("INSERT VERSION")
+public class ExprCopperState extends SimplePropertyExpression {
+
+ // TODO: Remove 'CopperState' and change all instances of 'Enum>' to 'WeatheringCopperState' when 1.21.9+ is minimum version.
+
+ private static final CopperStateMaterialMap STATE_MATERIALS = new CopperStateMaterialMap();
+ private static final List STATE_REPLACEMENTS = new ArrayList<>();
+ private static final boolean COPPER_GOLEM_EXISTS = Skript.classExists("org.bukkit.entity.CopperGolem");
+
+ static {
+ for (Enum> state : CopperState.getValues()) {
+ if (state.name().equals("UNAFFECTED"))
+ continue;
+ STATE_REPLACEMENTS.add(state.name() + "_");
+ }
+
+ for (Material material : MaterialTags.COPPER_BLOCKS.getValues()) {
+ Enum> state = getMaterialCopperState(material);
+ assert state != null;
+ String blockType = getBlockType(material);
+ STATE_MATERIALS.putMaterial(blockType, state, material);
+ }
+ }
+
+ public static void register(SyntaxRegistry registry) {
+ String type = "blocks";
+ if (Skript.classExists("org.bukkit.entity.CopperGolem"))
+ type = "entities/blocks";
+
+ registry.register(
+ SyntaxRegistry.EXPRESSION,
+ infoBuilder(
+ ExprCopperState.class,
+ Object.class,
+ "[weathering] copper state[s]",
+ type,
+ false
+ ).supplier(ExprCopperState::new)
+ .build()
+ );
+ }
+
+ /**
+ * Gets the string of the block type {@code material} relates to.
+ * i.e. stair, slab, door, trapdoor, etc.
+ * @param material The {@link Material} to get the block type from.
+ * @return The resulting string block type.
+ */
+ public static String getBlockType(Material material) {
+ String type = material.name();
+ for (String replace : STATE_REPLACEMENTS)
+ type = type.replaceAll(replace, "");
+ if (type.equals("COPPER_BLOCK")) {
+ type = "COPPER";
+ } else if (type.equals("WAXED_COPPER_BLOCK")) {
+ type = "WAXED_COPPER";
+ }
+ return type;
+ }
+
+ /**
+ * Gets the {@link CopperState} or 'WeatheringCopperState' a {@link Material} belongs to.
+ * @param material The {@link Material} to check.
+ * @return The resulting {@link CopperState} or 'WeatheringCopperState' if found, otherwise {@code null}.
+ */
+ public static @Nullable Enum> getMaterialCopperState(Material material) {
+ if (!MaterialTags.COPPER_BLOCKS.isTagged(material))
+ return null;
+ if (MaterialTags.EXPOSED_COPPER_BLOCKS.isTagged(material)) {
+ return CopperState.get(CopperState.EXPOSED);
+ } else if (MaterialTags.WEATHERED_COPPER_BLOCKS.isTagged(material)) {
+ return CopperState.get(CopperState.WEATHERED);
+ } else if (MaterialTags.OXIDIZED_COPPER_BLOCKS.isTagged(material)) {
+ return CopperState.get(CopperState.OXIDIZED);
+ }
+ return CopperState.get(CopperState.UNAFFECTED);
+ }
+
+ @Override
+ public @Nullable Object convert(Object object) {
+ if (COPPER_GOLEM_EXISTS && object instanceof CopperGolem golem) {
+ return golem.getWeatheringState();
+ } else if (object instanceof Block block) {
+ return getMaterialCopperState(block.getType());
+ }
+ return null;
+ }
+
+ @Override
+ public Class> @Nullable [] acceptChange(ChangeMode mode) {
+ if (mode == ChangeMode.SET || mode == ChangeMode.RESET)
+ return CollectionUtils.array(CopperState.getStateClass());
+ return null;
+ }
+
+ @Override
+ public void change(Event event, Object @Nullable [] delta, ChangeMode mode) {
+ Enum> state = CopperState.get(CopperState.UNAFFECTED);
+ if (delta != null)
+ state = (Enum>) delta[0];
+
+ for (Object object : getExpr().getArray(event)) {
+ if (COPPER_GOLEM_EXISTS && object instanceof CopperGolem golem) {
+ golem.setWeatheringState((WeatheringCopperState) state);
+ } else if (object instanceof Block block) {
+ Material material = getConvertedCopperMaterial(block.getType(), state);
+ if (material != null)
+ block.setType(material);
+ }
+ }
+ }
+
+ /**
+ * Gets the {@link Material} that is the same block type as {@code material} and in the {@code state}.
+ * @param material The current {@link Material} to convert from.
+ * @param state The {@link CopperState} or 'WeatheringCopperState' to get the same block type as {@code material}.
+ * @return The resulting {@link Material} if found, otherwise {@code null}.
+ */
+ public static @Nullable Material getConvertedCopperMaterial(Material material, Enum> state) {
+ if (!MaterialTags.COPPER_BLOCKS.isTagged(material))
+ return null;
+ String type = getBlockType(material);
+ return STATE_MATERIALS.getMaterial(type, state);
+ }
+
+ @Override
+ public Class> getReturnType() {
+ return CopperState.getStateClass();
+ }
+
+ @Override
+ protected String getPropertyName() {
+ return "weathering copper state";
+ }
+
+ /**
+ * Map for storing a block type, a {@link CopperState} or 'WeatheringCopperState' and the {@link Material} that is the block type and state.
+ */
+ private static class CopperStateMaterialMap extends HashMap {
+
+ public void putMaterial(String key, Enum> state, Material material) {
+ computeIfAbsent(key, array -> new Material[CopperState.getValues().length])[state.ordinal()] = material;
+ }
+
+ public @Nullable Material getMaterial(String key, Enum> state) {
+ if (!containsKey(key))
+ return null;
+ Material[] materials = get(key);
+ return materials[state.ordinal()];
+ }
+
+ }
+
+}
diff --git a/src/main/java/org/skriptlang/skript/bukkit/paperutil/CopperState.java b/src/main/java/org/skriptlang/skript/bukkit/paperutil/CopperState.java
new file mode 100644
index 00000000000..247158f427b
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/bukkit/paperutil/CopperState.java
@@ -0,0 +1,60 @@
+package org.skriptlang.skript.bukkit.paperutil;
+
+import ch.njol.skript.Skript;
+import org.jetbrains.annotations.ApiStatus.Internal;
+import org.skriptlang.skript.bukkit.elements.expressions.ExprCopperState;
+
+import java.lang.reflect.Method;
+
+/**
+ * Temporary enum to mimic `WeatheringCopperState`, which represents copper states, used in elements such as for {@link ExprCopperState}.
+ * Can be removed when 1.21.9+ is the minimum supported version.
+ */
+@Internal
+public enum CopperState {
+
+ EXPOSED, OXIDIZED, UNAFFECTED, WEATHERED;
+
+ private static final Class> WEATHERING_CLASS;
+ private static final Enum>[] WEATHERING_VALUES;
+ private static final Method WEATHERING_VALUE_METHOD;
+
+ static {
+ WEATHERING_CLASS = Skript.getClass("io.papermc.paper.world.WeatheringCopperState");
+ if (WEATHERING_CLASS != null) {
+ Method valuesMethod = Skript.getMethod(WEATHERING_CLASS, "values");
+ WEATHERING_VALUES = Skript.invokeMethod(valuesMethod);
+ WEATHERING_VALUE_METHOD = Skript.getMethod(WEATHERING_CLASS, "valueOf", String.class);
+ } else {
+ WEATHERING_VALUES = null;
+ WEATHERING_VALUE_METHOD = null;
+ }
+ }
+
+ /**
+ * @return {@link CopperState} class or 'WeatheringCopperState' if it exists.
+ */
+ public static Class> getStateClass() {
+ return WEATHERING_CLASS != null ? WEATHERING_CLASS : CopperState.class;
+ }
+
+ /**
+ * @return The enum values for {@link CopperState} or 'WeatheringCopperState' if it exists.
+ */
+ public static Enum>[] getValues() {
+ if (WEATHERING_CLASS != null)
+ return WEATHERING_VALUES;
+ return CopperState.values();
+ }
+
+ /**
+ * @param state The {@link CopperState} enum value.
+ * @return {@code state} or the state in 'WeatheringCopperState' if it exists.
+ */
+ public static Enum> get(CopperState state) {
+ if (WEATHERING_CLASS != null)
+ return Skript.invokeMethod(WEATHERING_VALUE_METHOD, null, state.name());
+ return state;
+ }
+
+}
diff --git a/src/main/java/org/skriptlang/skript/util/ClassLoader.java b/src/main/java/org/skriptlang/skript/util/ClassLoader.java
index d6f8ed44da4..cb48dcbcdfe 100644
--- a/src/main/java/org/skriptlang/skript/util/ClassLoader.java
+++ b/src/main/java/org/skriptlang/skript/util/ClassLoader.java
@@ -192,7 +192,7 @@ public void loadClasses(Class> source, @Nullable JarFile jar) {
if (this.forEachClass != null) {
this.forEachClass.accept(clazz);
}
- } catch (ClassNotFoundException ex) {
+ } catch (ClassNotFoundException | NoClassDefFoundError ex) {
throw new RuntimeException("Failed to load class: " + className, ex);
} catch (ExceptionInInitializerError err) {
throw new RuntimeException(className + " generated an exception while loading", err.getCause());
diff --git a/src/main/java/org/skriptlang/skript/util/ReflectUtils.java b/src/main/java/org/skriptlang/skript/util/ReflectUtils.java
new file mode 100644
index 00000000000..f2556a6658f
--- /dev/null
+++ b/src/main/java/org/skriptlang/skript/util/ReflectUtils.java
@@ -0,0 +1,252 @@
+package org.skriptlang.skript.util;
+
+import org.jetbrains.annotations.Nullable;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Utility class for reflection.
+ */
+public class ReflectUtils {
+
+ public ReflectUtils() {}
+
+ /**
+ * Cache for classes.
+ */
+ private final Map> classes = new HashMap<>();
+
+ /**
+ * Cache for methods of classes.
+ */
+ private final MethodMap methods = new MethodMap();
+
+ /**
+ * Cache for fields of classes.
+ */
+ private final FieldMap fields = new FieldMap();
+
+ /**
+ * @param className The full package and class name.
+ * @return Whether the class exists.
+ */
+ public boolean classExists(String className) {
+ return getClass(className) != null;
+ }
+
+ /**
+ * @param className The full package and class name.
+ * @return The resulting {@link Class} if found, otherwise {@code null}.
+ */
+ public @Nullable Class> getClass(String className) {
+ if (classes.containsKey(className))
+ return classes.get(className);
+ Class> c = null;
+ try {
+ c = Class.forName(className);
+ } catch (ClassNotFoundException ignored) {}
+ classes.put(className, c);
+ return c;
+ }
+
+ /**
+ * @param c The {@link Class} to check the method for.
+ * @param methodName The name of the method.
+ * @param params The {@link Class}es used as parameters for the desired method.
+ * @return Whether the method exists.
+ */
+ public boolean methodExists(Class> c, String methodName, Class> @Nullable ... params) {
+ return methodExists(c, methodName, params, null);
+ }
+
+ /**
+ * @param c The {@link Class} to check the method for.
+ * @param methodName The name of the method.
+ * @param params The {@link Class}es used as parameters for the desired method.
+ * @param returnTpe The return type of the desired method.
+ * @return Whether the method exists.
+ */
+ public boolean methodExists(Class> c, String methodName, Class> @Nullable [] params, @Nullable Class> returnTpe) {
+ return getMethod(c, methodName, params, returnTpe) != null;
+ }
+
+ /**
+ * @param c The {@link Class} to get the method from.
+ * @param methodName The name of the method.
+ * @param params The {@link Class}es used as parameters for the desired method.
+ * @return The resulting {@link Method} if it exists, otherwise {@code null}.
+ */
+ public @Nullable Method getMethod(Class> c, String methodName, Class> @Nullable ... params) {
+ return getMethod(c, methodName, params, null);
+ }
+
+ /**
+ * @param c The {@link Class} to get the method from.
+ * @param methodName The name of the method.
+ * @param params The {@link Class}es used as parameters for the desired method.
+ * @param returnType The return type of the desired method.
+ * @return The resulting {@link Method} if it exists, otherwise {@code null}.
+ */
+ public @Nullable Method getMethod(Class> c, String methodName, Class> @Nullable [] params, @Nullable Class> returnType) {
+ MethodID methodID = new MethodID(c, methodName, params);
+ if (methods.contains(c, methodID)) {
+ Method method = methods.get(c, methodID);
+ if (method != null && returnType != null) {
+ Class> methodType = method.getReturnType();
+ if (!returnType.isAssignableFrom(methodType) && !methodType.isAssignableFrom(returnType))
+ return null;
+ }
+ return methods.get(c, methodID);
+ }
+ Method method = null;
+ try {
+ method = c.getDeclaredMethod(methodName, params);
+ } catch (NoSuchMethodException ignored) {}
+
+ methods.put(c, methodID, method);
+ if (method != null && returnType != null) {
+ Class> methodType = method.getReturnType();
+ if (!returnType.isAssignableFrom(methodType) && !methodType.isAssignableFrom(returnType))
+ return null;
+ }
+ return method;
+ }
+
+ /**
+ * @param c The {@link Class} to check the field for.
+ * @param fieldName The name of the field.
+ * @return Whether the field exists.
+ */
+ public boolean fieldExists(Class> c, String fieldName) {
+ return getField(c, fieldName) != null;
+ }
+
+ /**
+ * @param c The {@link Class} to get the field from.
+ * @param fieldName The name of the field.
+ * @return The resulting {@link Field} if it exists, otherwise {@code null}.
+ */
+ public @Nullable Field getField(Class> c, String fieldName) {
+ if (fields.contains(c, fieldName))
+ return fields.get(c, fieldName);
+ Field field = null;
+ try {
+ field = c.getDeclaredField(fieldName);
+ } catch (NoSuchFieldException ignored) {}
+
+ fields.put(c, fieldName, field);
+ return field;
+ }
+
+ /**
+ * Invoke a static {@link Method}.
+ * @param method The {@link Method} to invoke.
+ * @return The result of the invocation if successful, otherwise {@code null}.
+ * @param The expected return type from the invocation.
+ */
+ public @Nullable Type invokeMethod(Method method) {
+ return invokeMethod(method, null);
+ }
+
+ /**
+ * Invoke a {@link Method}.
+ * @param method The {@link Method} to invoke.
+ * @param holder The holder object to invoke for.
+ * @param params The parameters to pass into the invocation.
+ * @return The result of the invocation if successful, otherwise {@code null}.
+ * @param The expected return type from the invocation.
+ */
+ public @Nullable Type invokeMethod(Method method, @Nullable Object holder, Object @Nullable ... params) {
+ method.setAccessible(true);
+ try {
+ //noinspection unchecked
+ return (Type) method.invoke(holder, params);
+ } catch (IllegalAccessException | InvocationTargetException ignored) {}
+ return null;
+ }
+
+ /**
+ * Gets the values of a static {@link Field}.
+ * @param field The {@link Field} to get from.
+ * @return The value of the {@link Field}.
+ * @param The expected return type.
+ */
+ public @Nullable Type getFieldValue(Field field) {
+ return getFieldValue(field, null);
+ }
+
+ /**
+ * Gets the values of a {@link Field}.
+ * @param field The {@link Field} to get from.
+ * @param holder The holder object to get the field for.
+ * @return The value of the {@link Field}.
+ * @param The expected return type.
+ */
+ public @Nullable Type getFieldValue(Field field, @Nullable Object holder) {
+ field.setAccessible(true);
+ try {
+ //noinspection unchecked
+ return (Type) field.get(holder);
+ } catch (IllegalAccessException ignored) {}
+ return null;
+ }
+
+ /**
+ * Record for caching data for a method.
+ * @param c The {@link Class} the method belongs to.
+ * @param methodName The name of the method.
+ * @param params The types of parameters for the method.
+ */
+ private record MethodID(Class> c, String methodName, Class> @Nullable [] params) {}
+
+ /**
+ * Custom map for correlating a {@link Class} to a {@link Method}.
+ */
+ private static class MethodMap extends HashMap, Map> {
+
+ public boolean contains(Class> c, MethodID methodID) {
+ if (!containsKey(c))
+ return false;
+ return get(c).containsKey(methodID);
+ }
+
+ public void put(Class> c, MethodID methodID, Method method) {
+ computeIfAbsent(c, map -> new HashMap<>()).put(methodID, method);
+ }
+
+ public @Nullable Method get(Class> c, MethodID methodID) {
+ if (!contains(c, methodID))
+ return null;
+ return get(c).get(methodID);
+ }
+
+ }
+
+ /**
+ * Custom map for correlating a {@link Class} to a {@link Field}.
+ */
+ private static class FieldMap extends HashMap, Map> {
+
+ public boolean contains(Class> c, String fieldName) {
+ if (!containsKey(c))
+ return false;
+ return get(c).containsKey(fieldName);
+ }
+
+ public void put(Class> c, String fieldName, Field field) {
+ computeIfAbsent(c, map -> new HashMap<>()).put(fieldName, field);
+ }
+
+ public @Nullable Field get(Class> c, String fieldName) {
+ if (!contains(c, fieldName))
+ return null;
+ return get(c).get(fieldName);
+ }
+
+ }
+
+}
diff --git a/src/main/resources/lang/default.lang b/src/main/resources/lang/default.lang
index e9853b2c01a..9e5d938754e 100644
--- a/src/main/resources/lang/default.lang
+++ b/src/main/resources/lang/default.lang
@@ -640,6 +640,14 @@ entities:
0: [%-cowvariant%] cow[plural:s]
1: baby:[%-cowvariant%] cal(f|plural:ves)
+ # CopperGolemData
+ copper golem:
+ name: copper golem¦s
+ patterns:
+ 0: [%-weatheringcopperstate%] copper golem[plural:s]
+ 1: waxed [%-weatheringcopperstate%] copper golem[plural:s]
+ 2: unwaxed [%-weatheringcopperstate%] copper golem[plural:s]
+
# CreeperData
creeper:
name: creeper¦s
@@ -1590,9 +1598,6 @@ entities:
1: baby:[happy] ghastling[plural:s]
# 1.21.9 Entities
- copper golem:
- name: copper golem¦s
- pattern: copper golem[plural:s]
mannequin:
name: mannequin¦s
pattern: mannequin[plural:s]
@@ -3013,6 +3018,20 @@ damage types:
wither: wither
wither_skull: wither skull
+# -- Weathering Copper States --
+weathering copper states:
+ exposed: exposed, exposed state, exposed copper, exposed copper state
+ oxidized: oxidized, oxidized state, oxidized copper, oxidized copper state
+ unaffected: normal state, normal copper state, unaffected, unaffected state, unaffected copper, unaffected copper state
+ weathered: weathered, weathered state, weathered copper, weathered copper state
+
+# -- Copper Golem Poses --
+copper golem poses:
+ running: running, running pose, running statue pose, statue running pose
+ sitting: sitting, sitting pose, sitting statue pose, statue sitting pose
+ standing: standing, standing pose, standing statue pose, statue standing pose
+ star: star, star pose, star statue pose, statue star pose
+
# -- Boolean --
boolean:
true:
@@ -3133,6 +3152,8 @@ types:
frogvariant: frog variant¦s @a
itemcomponent: item component¦s @an
equippablecomponent: equippable component¦s @an
+ weatheringcopperstate: weathering copper state¦s @a
+ coppergolempose: copper golem pose¦s @a
# Skript
weathertype: weather type¦s @a
diff --git a/src/test/skript/tests/misc/EntityData.sk b/src/test/skript/tests/misc/EntityData.sk
index b1e02b2826e..39adec02f3e 100644
--- a/src/test/skript/tests/misc/EntityData.sk
+++ b/src/test/skript/tests/misc/EntityData.sk
@@ -233,6 +233,50 @@ test "chicken data":
testData(temperate chick)
testData(cold chick)
+test "copper golem data" when running minecraft "1.21.9":
+ setSuperTypes(copper golem)
+ testData(copper golem)
+ testData(waxed copper golem)
+ testData(unwaxed copper golem)
+ testData(unaffected copper golem)
+ testData(exposed copper golem)
+ testData(weathered copper golem)
+ testData(oxidized copper golem)
+
+ setSuperTypes(waxed copper golem)
+ testData(waxed copper golem)
+ testData(waxed unaffected copper golem)
+ testData(waxed exposed copper golem)
+ testData(waxed weathered copper golem)
+ testData(waxed oxidized copper golem)
+
+ setSuperTypes(unwaxed copper golem)
+ testData(unwaxed copper golem)
+ testData(unwaxed unaffected copper golem)
+ testData(unwaxed exposed copper golem)
+ testData(unwaxed weathered copper golem)
+ testData(unwaxed oxidized copper golem)
+
+ setSuperTypes(unaffected copper golem)
+ testData(unaffected copper golem)
+ testData(waxed unaffected copper golem)
+ testData(unwaxed unaffected copper golem)
+
+ setSuperTypes(exposed copper golem)
+ testData(exposed copper golem)
+ testData(waxed exposed copper golem)
+ testData(unwaxed exposed copper golem)
+
+ setSuperTypes(weathered copper golem)
+ testData(weathered copper golem)
+ testData(waxed weathered copper golem)
+ testData(unwaxed weathered copper golem)
+
+ setSuperTypes(oxidized copper golem)
+ testData(oxidized copper golem)
+ testData(waxed oxidized copper golem)
+ testData(unwaxed oxidized copper golem)
+
test "cow data":
setSuperTypes(cow)
testData(cow)
diff --git a/src/test/skript/tests/syntaxes/effects/EffWax.sk b/src/test/skript/tests/syntaxes/effects/EffWax.sk
new file mode 100644
index 00000000000..a84a461c8a5
--- /dev/null
+++ b/src/test/skript/tests/syntaxes/effects/EffWax.sk
@@ -0,0 +1,26 @@
+test "wax copper golem" when running minecraft "1.21.9":
+ spawn a copper golem at test-location:
+ set {_entity} to entity
+
+ assert {_entity} is not waxed with "Copper golem should not be waxed after spawn"
+ wax {_entity}
+ assert {_entity} is waxed with "Copper golem should be waxed"
+ unwax {_entity}
+ assert {_entity} is not waxed with "Copper golem should be unwaxed"
+
+ clear entity within {_entity}
+
+test "wax copper blocks":
+ set {_old} to the block data of test-block
+
+ set {_waxTag} to paper tag "waxed_copper_blocks"
+ set {_unwaxTag} to paper tag "unwaxed_copper_blocks"
+ set {_blocks::*} to the tag contents of {_waxTag}
+ loop {_blocks::*}:
+ set test-block to loop-value
+ unwax test-block
+ assert test-block is tagged with {_unwaxTag} with "%loop-value% should be unwaxed, but was %type of test-block%"
+ wax test-block
+ assert test-block is tagged with {_waxTag} with "%loop-value% should be waxed, but was %type of test-block%"
+
+ set the block data of test-block to {_old}
diff --git a/src/test/skript/tests/syntaxes/expressions/ExprCopperGolemOxidationTime.sk b/src/test/skript/tests/syntaxes/expressions/ExprCopperGolemOxidationTime.sk
new file mode 100644
index 00000000000..0a5d225f9a1
--- /dev/null
+++ b/src/test/skript/tests/syntaxes/expressions/ExprCopperGolemOxidationTime.sk
@@ -0,0 +1,10 @@
+test "copper golem oxidation" when running minecraft "1.21.9":
+ spawn a copper golem at test-location:
+ set {_entity} to entity
+
+ set the time until oxidation of {_entity} to 10 seconds
+ assert the time until oxidation of {_entity} is 10 seconds with "Copper golem oxidation time did not change"
+ reset the time until oxidation of {_entity}
+ assert the time until oxidation of {_entity} is not set with "Copper golem oxidation reset takes a tick to be scheduled again"
+
+ clear the entity within {_entity}
diff --git a/src/test/skript/tests/syntaxes/expressions/ExprCopperGolemPose.sk b/src/test/skript/tests/syntaxes/expressions/ExprCopperGolemPose.sk
new file mode 100644
index 00000000000..3b3377d2e14
--- /dev/null
+++ b/src/test/skript/tests/syntaxes/expressions/ExprCopperGolemPose.sk
@@ -0,0 +1,9 @@
+test "copper golem pose" when running minecraft "1.21.9":
+ set {_old} to the block data of test-block
+
+ set test-block to a copper golem statue
+ loop all copper golem poses:
+ set the copper golem pose of test-block to loop-value
+ assert the copper golem pose of test-block is loop-value with "Copper golem pose should be %loop-value%"
+
+ set the block data of test-block to {_old}
diff --git a/src/test/skript/tests/syntaxes/expressions/ExprCopperState.sk b/src/test/skript/tests/syntaxes/expressions/ExprCopperState.sk
new file mode 100644
index 00000000000..4014c33c891
--- /dev/null
+++ b/src/test/skript/tests/syntaxes/expressions/ExprCopperState.sk
@@ -0,0 +1,39 @@
+test "copper state of copper golem" when running minecraft "1.21.9":
+ spawn a copper golem at test-location:
+ set {_entity} to entity
+
+ loop all weathering copper states:
+ set the copper state of {_entity} to loop-value
+ assert the copper state of {_entity} is loop-value with "Copper state of copper golem did not change"
+
+ clear entity within {_entity}
+
+test "copper state of copper blocks":
+ set {_old} to the block data of test-block
+
+ set {_exposedTag} to paper tag "exposed_copper_blocks"
+ set {_weatheredTag} to paper tag "weathered_copper_blocks"
+ set {_oxidizedTag} to paper tag "oxidized_copper_blocks"
+ set {_blocks::*} to the tag contents of paper tag "copper_blocks"
+ filter {_blocks::*} to match:
+ input is not tagged with {_exposedTag}
+ input is not tagged with {_weatheredTag}
+ input is not tagged with {_oxidizedTag}
+
+ parse if running minecraft "1.21.9":
+ filter {_blocks::*} to match:
+ input is not a copper torch
+ input is not a copper wall torch
+
+ loop {_blocks::*}:
+ set test-block to loop-value
+
+ assert the copper state of test-block is normal copper state with "Copper block should be normal copper state, but was %type of test-block%"
+ set the copper state of test-block to exposed copper state
+ assert the copper state of test-block is exposed copper state with "Copper block should be exposed copper state, but was %type of test-block%"
+ set the copper state of test-block to weathered copper state
+ assert the copper state of test-block is weathered copper state with "Copper block should be weathered copper state, but was %type of test-block%"
+ set the copper state of test-block to oxidized copper state
+ assert the copper state of test-block is oxidized copper state with "Copper block should be oxidized copper state, but was %type of test-block%"
+
+ set the block data of test-block to {_old}
diff --git a/src/test/skript/tests/syntaxes/sections/EffSecSpawn.sk b/src/test/skript/tests/syntaxes/sections/EffSecSpawn.sk
index 265bf06bed7..a242afc1d89 100644
--- a/src/test/skript/tests/syntaxes/sections/EffSecSpawn.sk
+++ b/src/test/skript/tests/syntaxes/sections/EffSecSpawn.sk
@@ -268,3 +268,47 @@ test "spawn baby cow by variant" when running minecraft "1.21.5":
assert size of all calves is 5 with "Only temperate calves should have been deleted"
delete all warm calves
assert size of all warm calves is 0 with "Warm calves were not deleted"
+
+test "spawn copper golems" when running minecraft "1.21.9":
+ clear all copper golems
+ spawn 2 copper golems at test-location
+ spawn 2 waxed copper golems at test-location
+ spawn 2 unwaxed copper golems at test-location
+
+ spawn 2 unaffected copper golems at test-location
+ spawn 2 waxed unaffected copper golems at test-location
+ spawn 2 unwaxed unaffected copper golems at test-location
+
+ spawn 2 exposed copper golems at test-location
+ spawn 2 waxed exposed copper golems at test-location
+ spawn 2 unwaxed exposed copper golems at test-location
+
+ spawn 2 weathered copper golems at test-location
+ spawn 2 waxed weathered copper golems at test-location
+ spawn 2 unwaxed weathered copper golems at test-location
+
+ spawn 2 oxidized copper golems at test-location
+ spawn 2 waxed oxidized copper golems at test-location
+ spawn 2 unwaxed oxidized copper golems at test-location
+
+ assert size of all copper golems is 30 with "Size of all copper golems is not 30"
+ assert size of all waxed copper golems is 10 with "Size of all waxed copper golems is not 10"
+ assert size of all unwaxed copper golems is 20 with "Size of all unwaxed copper golems is not 20"
+
+ assert size of all unaffected copper golems is 12 with "Size of all unaffected copper golems is not 12"
+ assert size of all waxed unaffected copper golems is 4 with "Size of all waxed unaffected copper golems is not 4"
+ assert size of all unwaxed unaffected copper golems is 8 with "Size of all unwaxed unaffected copper golems is not 8"
+
+ assert size of all exposed copper golems is 6 with "Size of all exposed copper golems is not 6"
+ assert size of all waxed exposed copper golems is 2 with "Size of all waxed exposed copper golems is not 2"
+ assert size of all unwaxed exposed copper golems is 4 with "Size of all unwaxed exposed copper golems is not 4"
+
+ assert size of all weathered copper golems is 6 with "Size of all weathered copper golems is not 6"
+ assert size of all waxed weathered copper golems is 2 with "Size of all waxed weathered copper golems is not 2"
+ assert size of all unwaxed weathered copper golems is 4 with "Size of all unwaxed weathered copper golems is not 4"
+
+ assert size of all oxidized copper golems is 6 with "Size of all oxidized copper golems is not 6"
+ assert size of all waxed oxidized copper golems is 2 with "Size of all waxed oxidized copper golems is not 2"
+ assert size of all unwaxed oxidized copper golems is 4 with "Size of all unwaxed oxidized copper golems is not 4"
+
+ clear all copper golems