From 9d9fff892b4ff2fc007ab190ac214901e8e5a3ec Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sat, 4 Oct 2025 12:03:33 -0700 Subject: [PATCH 1/8] get custom types working --- .../java/com/sovdee/oopsk/core/Struct.java | 33 +++- .../com/sovdee/oopsk/core/StructManager.java | 2 +- .../com/sovdee/oopsk/core/StructTemplate.java | 11 +- .../sovdee/oopsk/core/TemplateManager.java | 13 ++ .../core/generation/ReflectionUtils.java | 141 ++++++++++++++++++ .../generation/TemporaryClassManager.java | 42 ++++++ .../elements/expressions/ExprFieldAccess.java | 36 ++++- .../expressions/ExprSecStructInstance.java | 15 +- .../elements/expressions/ExprStructCopy.java | 6 +- .../structures/StructStructTemplate.java | 98 +++++++++--- src/test/scripts/basics.sk | 2 +- src/test/scripts/copies.sk | 2 +- src/test/scripts/customtypes.sk | 28 ++++ 13 files changed, 394 insertions(+), 35 deletions(-) create mode 100644 src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java create mode 100644 src/main/java/com/sovdee/oopsk/core/generation/TemporaryClassManager.java create mode 100644 src/test/scripts/customtypes.sk diff --git a/src/main/java/com/sovdee/oopsk/core/Struct.java b/src/main/java/com/sovdee/oopsk/core/Struct.java index 8523a15..9d7133c 100644 --- a/src/main/java/com/sovdee/oopsk/core/Struct.java +++ b/src/main/java/com/sovdee/oopsk/core/Struct.java @@ -22,6 +22,33 @@ public class Struct { private StructTemplate template; private final Map, Object[]> fieldValues; + public static Struct newInstance(@NotNull StructTemplate template, @Nullable Event event) { + Class structClass = template.getCustomClass(); + try { + return structClass.getDeclaredConstructor(StructTemplate.class, Event.class).newInstance(template, event); + } catch (Exception e) { + throw new RuntimeException("Failed to create new instance of struct class " + structClass.getName(), e); + } + } + + public static Struct newInstance(Struct source) { + Class structClass = source.getTemplate().getCustomClass(); + try { + return structClass.getDeclaredConstructor(Struct.class).newInstance(source); + } catch (Exception e) { + throw new RuntimeException("Failed to create new instance of struct class " + structClass.getName(), e); + } + } + + public static Struct newInstance(@NotNull StructTemplate template, @Nullable Event event, @Nullable Map> initialValues) { + Class structClass = template.getCustomClass(); + try { + return structClass.getDeclaredConstructor(StructTemplate.class, Event.class, Map.class).newInstance(template, event, initialValues); + } catch (Exception e) { + throw new RuntimeException("Failed to create new instance of struct class " + structClass.getName(), e); + } + } + /** * Creates a new struct with the given template and event. * @@ -29,7 +56,7 @@ public class Struct { * @param event The event to evaluate the default values in. * @see StructManager#createStruct(StructTemplate, Event) */ - public Struct(@NotNull StructTemplate template, @Nullable Event event) { + protected Struct(@NotNull StructTemplate template, @Nullable Event event) { this.template = template; fieldValues = new HashMap<>(); for (Field field : template.getFields()) { @@ -44,7 +71,7 @@ public Struct(@NotNull StructTemplate template, @Nullable Event event) { * @param source The struct to copy from. * @see StructManager#createStruct(StructTemplate, Event) */ - public Struct(Struct source) { + protected Struct(Struct source) { this.template = source.template; fieldValues = new HashMap<>(); for (Map.Entry, Object[]> entry : source.fieldValues.entrySet()) { @@ -64,7 +91,7 @@ public Struct(Struct source) { * @param initialValues The initial values to set in the struct. This is a map of field names to expressions. * @see StructManager#createStruct(StructTemplate, Event) */ - Struct(@NotNull StructTemplate template, @Nullable Event event, @Nullable Map> initialValues) { + protected Struct(@NotNull StructTemplate template, @Nullable Event event, @Nullable Map> initialValues) { this.template = template; fieldValues = new HashMap<>(); for (Field field : template.getFields()) { diff --git a/src/main/java/com/sovdee/oopsk/core/StructManager.java b/src/main/java/com/sovdee/oopsk/core/StructManager.java index dbf27e4..8544ee8 100644 --- a/src/main/java/com/sovdee/oopsk/core/StructManager.java +++ b/src/main/java/com/sovdee/oopsk/core/StructManager.java @@ -41,7 +41,7 @@ public Struct createStruct(StructTemplate template, @Nullable Event event) { * @return The created struct. */ public Struct createStruct(StructTemplate template, @Nullable Event event, @Nullable Map> initialValues) { - Struct struct = new Struct(template, event, initialValues); + Struct struct = Struct.newInstance(template, event, initialValues); activeStructs.computeIfAbsent(template, k -> Collections.newSetFromMap(new WeakHashMap<>())).add(struct); return struct; } diff --git a/src/main/java/com/sovdee/oopsk/core/StructTemplate.java b/src/main/java/com/sovdee/oopsk/core/StructTemplate.java index 8fc4e35..ac89a6b 100644 --- a/src/main/java/com/sovdee/oopsk/core/StructTemplate.java +++ b/src/main/java/com/sovdee/oopsk/core/StructTemplate.java @@ -14,6 +14,7 @@ public class StructTemplate { private final String name; private final Map> fields; + private final Class customClass; /** * Creates a new struct template with the given name and fields. @@ -21,8 +22,9 @@ public class StructTemplate { * @param name The name of the template. * @param fields The fields of the template. */ - public StructTemplate(String name, @NotNull List> fields) { + public StructTemplate(String name, @NotNull List> fields, Class customClass) { this.name = name; + this.customClass = customClass; this.fields = new HashMap<>(); for (Field field : fields) { this.fields.put(field.name(), field); @@ -36,6 +38,13 @@ public String getName() { return name; } + /** + * @return The custom class for this struct, or null if none was specified. + */ + public Class getCustomClass() { + return customClass; + } + /** * Parses all the default value expressions for this struct. Prints errors. * @return true if no errors were encountered. False otherwise. diff --git a/src/main/java/com/sovdee/oopsk/core/TemplateManager.java b/src/main/java/com/sovdee/oopsk/core/TemplateManager.java index d86d280..ee97ceb 100644 --- a/src/main/java/com/sovdee/oopsk/core/TemplateManager.java +++ b/src/main/java/com/sovdee/oopsk/core/TemplateManager.java @@ -18,6 +18,7 @@ public class TemplateManager { private final Map templates = new HashMap<>(); + private final Map, StructTemplate> templatesByClass = new HashMap<>(); /** * Adds a new template to the manager. Attempts to reparent all orphaned structs that match this template's name. @@ -29,6 +30,7 @@ public boolean addTemplate(@NotNull StructTemplate template) { if (templates.containsKey(template.getName())) return false; // Template with the same name already exists templates.put(template.getName(), template); + templatesByClass.put(template.getCustomClass(), template); // reparent all orphaned structs of this template Oopsk.getStructManager().reparentStructs(template); return true; // Template added successfully @@ -44,6 +46,16 @@ public StructTemplate getTemplate(String name) { return templates.get(name); } + /** + * Retrieves a template by class. + * + * @param structClass The name of the template to retrieve. + * @return The template, or null if it does not exist. + */ + public StructTemplate getTemplate(Class structClass) { + return templatesByClass.get(structClass); + } + /** * Removes a template from the manager. This will orphan all structs of this template. * @@ -54,6 +66,7 @@ public void removeTemplate(@NotNull StructTemplate template) { if (!templates.containsKey(name)) return; // Template with the given name does not exist templates.remove(name); + templatesByClass.remove(template.getCustomClass()); // mark all structs of this template as orphaned Oopsk.getStructManager().orphanStructs(template); } diff --git a/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java b/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java new file mode 100644 index 0000000..96796d0 --- /dev/null +++ b/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java @@ -0,0 +1,141 @@ +package com.sovdee.oopsk.core.generation; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.localization.Language; +import ch.njol.skript.registrations.Classes; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +public class ReflectionUtils { + + private static final Field tempClassInfosField; + private static final Field exactClassInfosField; + private static final Field classInfosByCodeNameField; + private static final Field acceptRegistrationsField; + private static final Field localizedLanguageField; + private static final Field classInfosField; + private static final Method sortClassInfosMethod; + + static { + try { + // Get the fields once during class initialization + tempClassInfosField = Classes.class.getDeclaredField("tempClassInfos"); + exactClassInfosField = Classes.class.getDeclaredField("exactClassInfos"); + classInfosByCodeNameField = Classes.class.getDeclaredField("classInfosByCodeName"); + acceptRegistrationsField = Skript.class.getDeclaredField("acceptRegistrations"); + localizedLanguageField = Language.class.getDeclaredField("localizedLanguage"); + classInfosField = Classes.class.getDeclaredField("classInfos"); + + // Get the method + sortClassInfosMethod = Classes.class.getDeclaredMethod("sortClassInfos"); + + + // Make them accessible + tempClassInfosField.setAccessible(true); + exactClassInfosField.setAccessible(true); + classInfosByCodeNameField.setAccessible(true); + acceptRegistrationsField.setAccessible(true); + localizedLanguageField.setAccessible(true); + classInfosField.setAccessible(true); + sortClassInfosMethod.setAccessible(true); + + } catch (Exception e) { + throw new RuntimeException("Failed to access fields", e); + } + } + + public static void enableRegistrations() { + setFieldValue(true); + } + + public static void disableRegistrations() { + setFieldValue(false); + } + + private static void setFieldValue(boolean value) { + try { + acceptRegistrationsField.set(null, value); + } catch (Exception e) { + throw new RuntimeException("Failed to modify acceptRegistrations field", e); + } + } + + @SuppressWarnings("unchecked") + public static List> getTempClassInfos() throws Exception { + return (List>) tempClassInfosField.get(null); + } + + @SuppressWarnings("unchecked") + public static HashMap, ClassInfo> getExactClassInfos() throws Exception { + return (HashMap, ClassInfo>) exactClassInfosField.get(null); + } + + @SuppressWarnings("unchecked") + public static HashMap> getClassInfosByCodeName() throws Exception { + return (HashMap>) classInfosByCodeNameField.get(null); + } + + public static void addLanguageNode(String key, String value) { + try { + @SuppressWarnings("unchecked") + HashMap langMap = (HashMap) localizedLanguageField.get(null); + System.out.println("Adding language node: " + key + " -> " + value); + langMap.put(key, value); + } catch (Exception e) { + throw new RuntimeException("Failed to add language node.", e); + } + } + + public static void removeLanguageNode(String key) { + try { + @SuppressWarnings("unchecked") + HashMap langMap = (HashMap) localizedLanguageField.get(null); + System.out.println("Removing language node: " + key + " (was " + langMap.get(key) + ")"); + langMap.remove(key); + } catch (Exception e) { + throw new RuntimeException("Failed to add language node.", e); + } + } + + public static void resortClassInfos(ClassInfo... exclude) throws Exception { + var tempClassInfos = getTempClassInfos(); + Collections.addAll(tempClassInfos, (ClassInfo[]) classInfosField.get(null)); + if (exclude != null && exclude.length > 0) + tempClassInfos.removeAll(List.of(exclude)); + classInfosField.set(null, null); + sortClassInfos(); + } + + public static void sortClassInfos() throws Exception { + sortClassInfosMethod.invoke(null); + } + + public static void addClassInfo(ClassInfo classInfo) { + enableRegistrations(); + Classes.registerClass(classInfo); + try { + resortClassInfos(); + } catch (Exception e) { + throw new RuntimeException(e); + } + System.out.println("Registered custom struct class: " + classInfo.getName()); + disableRegistrations(); + } + + // Remove from all three collections + public static void removeClassInfo(ClassInfo classInfo) { + try { + getExactClassInfos().remove(classInfo.getC()); + getClassInfosByCodeName().remove(classInfo.getCodeName()); + resortClassInfos(classInfo); + } catch (Exception e) { + throw new RuntimeException("Failed to remove classinfo.", e); + } + } + +} diff --git a/src/main/java/com/sovdee/oopsk/core/generation/TemporaryClassManager.java b/src/main/java/com/sovdee/oopsk/core/generation/TemporaryClassManager.java new file mode 100644 index 0000000..bde4339 --- /dev/null +++ b/src/main/java/com/sovdee/oopsk/core/generation/TemporaryClassManager.java @@ -0,0 +1,42 @@ +package com.sovdee.oopsk.core.generation; + +import com.sovdee.oopsk.core.Struct; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; + +import java.net.URL; +import java.net.URLClassLoader; + +public class TemporaryClassManager { + private ClassLoader disposableClassLoader; + + public TemporaryClassManager() { + resetClassLoader(); + } + + private void resetClassLoader() { + // Create a new child ClassLoader + this.disposableClassLoader = new URLClassLoader( + new URL[0], + Struct.class.getClassLoader() + ); + } + + public Class createTemporarySubclass(String name) { + return new ByteBuddy() + .subclass(Struct.class) + .name(name) + .make() + .load(disposableClassLoader, ClassLoadingStrategy.Default.WRAPPER) + .getLoaded(); + } + + public void unloadAll() { + // Drop the ClassLoader and all its classes + disposableClassLoader = null; + System.gc(); // Suggest garbage collection + + // Create a fresh ClassLoader for new classes + resetClassLoader(); + } +} diff --git a/src/main/java/com/sovdee/oopsk/elements/expressions/ExprFieldAccess.java b/src/main/java/com/sovdee/oopsk/elements/expressions/ExprFieldAccess.java index 4569f4e..a62f6e0 100644 --- a/src/main/java/com/sovdee/oopsk/elements/expressions/ExprFieldAccess.java +++ b/src/main/java/com/sovdee/oopsk/elements/expressions/ExprFieldAccess.java @@ -34,6 +34,7 @@ import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -80,7 +81,6 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean is } fieldName = fieldName.trim().toLowerCase(Locale.ENGLISH); if (!updateFieldGuesses()) { - Skript.error("No field with name '" + fieldName + "' found."); return false; } return true; @@ -91,21 +91,43 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean is * @return True if a field was found, false otherwise. */ private boolean updateFieldGuesses() { + var templateManager = Oopsk.getTemplateManager(); + + // get all possible struct templates this could be + Set templates = new HashSet<>(); + Class[] possibleReturnTypes = getExpr().possibleReturnTypes(); + for (Class type : possibleReturnTypes) { + if (type != Struct.class && Struct.class.isAssignableFrom(type)) { + templates.add(templateManager.getTemplate(type.asSubclass(Struct.class))); + } else if (type == Object.class || type == Struct.class) { + // if Object or Struct, we have to assume all templates + templates.clear(); + break; + } + } + // get all possible fields that this could be accessing - var fieldSetMap = Oopsk.getTemplateManager() - .getFieldsMatching((field -> field.name().equalsIgnoreCase(fieldName))); + var fieldSetMap = templateManager.getFieldsMatching( + field -> field.name().equalsIgnoreCase(fieldName)); if (fieldSetMap.isEmpty()) { + noFieldFoundError(templates); return false; } // collapse setMap to a 1:1 map of template -> field // since our predicate is based on name, there should never be a template with multiple fields that match. possibleFields = new WeakHashMap<>(); for (Map.Entry>> entry : fieldSetMap.entrySet()) { + if (!templates.isEmpty() && !templates.contains(entry.getKey())) + continue; // if we have a limited set of templates, skip ones that aren't in it StructTemplate template = entry.getKey(); Set> fields = entry.getValue(); // if there are multiple fields, pick the first one possibleFields.put(template, fields.stream().findFirst().orElse(null)); } + if (possibleFields.isEmpty()) { + noFieldFoundError(templates); + return false; + } // use super type of all possible fields returnTypes = possibleFields.values().stream() .map((field -> field.type().getC())) @@ -127,6 +149,14 @@ private boolean updateFieldGuesses() { return true; } + private void noFieldFoundError(Set templates) { + if (!templates.isEmpty()) { + Skript.error("No field with name '" + fieldName + "' found in the structs " + Classes.toString(templates.stream().map(StructTemplate::getName).toArray(), false) + "."); + } else { + Skript.error("No field with name '" + fieldName + "' found in any struct."); + } + } + @Override protected Object[] get(Event event, Struct[] source) { if (source.length == 0) diff --git a/src/main/java/com/sovdee/oopsk/elements/expressions/ExprSecStructInstance.java b/src/main/java/com/sovdee/oopsk/elements/expressions/ExprSecStructInstance.java index 5209cda..c7a2f97 100644 --- a/src/main/java/com/sovdee/oopsk/elements/expressions/ExprSecStructInstance.java +++ b/src/main/java/com/sovdee/oopsk/elements/expressions/ExprSecStructInstance.java @@ -17,6 +17,7 @@ import ch.njol.skript.lang.TriggerItem; import ch.njol.skript.util.LiteralUtils; import ch.njol.util.Kleenean; +import ch.njol.util.coll.CollectionUtils; import com.sovdee.oopsk.Oopsk; import com.sovdee.oopsk.core.Field; import com.sovdee.oopsk.core.Struct; @@ -48,7 +49,7 @@ public class ExprSecStructInstance extends SectionExpression implements static { Skript.registerExpression(ExprSecStructInstance.class, Struct.class, ExpressionType.SIMPLE, - "[a[n]] <([\\w ]+)> struct [instance] [with [the] [initial] values [of]]"); + "[a[n]] <([\\w ]+)> struct instance [with [the] [initial] values [of]]"); } private String name; @@ -118,11 +119,10 @@ public boolean parseInitialValues(@NotNull SectionNode node, StructTemplate temp } // parse the value - //noinspection unchecked Expression expr = new SkriptParser(value, SkriptParser.ALL_FLAGS, ParseContext.DEFAULT).parseExpression(field.type().getC()); expr = LiteralUtils.defendExpression(expr); if (expr == null || !LiteralUtils.canInitSafely(expr)) { - Skript.error("Invalid value for field '" + fieldName + "' in struct '" + name + "'."); +// Skript.error("Invalid value for field '" + fieldName + "' in struct '" + name + "'."); return false; } // store in map @@ -140,7 +140,8 @@ public boolean parseInitialValues(@NotNull SectionNode node, StructTemplate temp StructTemplate template = Oopsk.getTemplateManager().getTemplate(name); if (template == null) error("A struct by the name of '" + name + "' does not exist."); - return new Struct[] {Oopsk.getStructManager().createStruct(template, event, parsedFieldValues)}; + Struct struct = Oopsk.getStructManager().createStruct(template, event, parsedFieldValues); + return CollectionUtils.array(struct); } @Override @@ -149,7 +150,11 @@ public boolean isSingle() { } @Override - public Class getReturnType() { + public Class getReturnType() { + var template = Oopsk.getTemplateManager().getTemplate(name); + if (template != null && template.getCustomClass() != null) { + return template.getCustomClass(); + } return Struct.class; } diff --git a/src/main/java/com/sovdee/oopsk/elements/expressions/ExprStructCopy.java b/src/main/java/com/sovdee/oopsk/elements/expressions/ExprStructCopy.java index fe3a833..3e33c92 100644 --- a/src/main/java/com/sovdee/oopsk/elements/expressions/ExprStructCopy.java +++ b/src/main/java/com/sovdee/oopsk/elements/expressions/ExprStructCopy.java @@ -12,16 +12,16 @@ public class ExprStructCopy extends SimplePropertyExpression { @Override public @Nullable Struct convert(Struct struct) { - return new Struct(struct); + return Struct.newInstance(struct); } @Override public Class getReturnType() { - return Struct.class; + return getExpr().getReturnType(); } @Override protected String getPropertyName() { - return "copy"; + return "struct copy"; } } diff --git a/src/main/java/com/sovdee/oopsk/elements/structures/StructStructTemplate.java b/src/main/java/com/sovdee/oopsk/elements/structures/StructStructTemplate.java index 1e453c7..12fc072 100644 --- a/src/main/java/com/sovdee/oopsk/elements/structures/StructStructTemplate.java +++ b/src/main/java/com/sovdee/oopsk/elements/structures/StructStructTemplate.java @@ -18,7 +18,9 @@ import com.sovdee.oopsk.Oopsk; import com.sovdee.oopsk.core.Field; import com.sovdee.oopsk.core.Field.Modifier; +import com.sovdee.oopsk.core.Struct; import com.sovdee.oopsk.core.StructTemplate; +import com.sovdee.oopsk.core.generation.TemporaryClassManager; import com.sovdee.oopsk.events.DynamicFieldEvalEvent; import org.bukkit.event.Event; import org.jetbrains.annotations.NotNull; @@ -35,6 +37,13 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import static com.sovdee.oopsk.core.generation.ReflectionUtils.addClassInfo; +import static com.sovdee.oopsk.core.generation.ReflectionUtils.addLanguageNode; +import static com.sovdee.oopsk.core.generation.ReflectionUtils.disableRegistrations; +import static com.sovdee.oopsk.core.generation.ReflectionUtils.enableRegistrations; +import static com.sovdee.oopsk.core.generation.ReflectionUtils.removeClassInfo; +import static com.sovdee.oopsk.core.generation.ReflectionUtils.removeLanguageNode; + @Name("Struct Template") @Description({ "Creates a struct template. The template name is case insensitive and has the same restrictions as function names.", @@ -76,11 +85,10 @@ public boolean init(Literal[] args, int matchedPattern, SkriptParser.ParseRes name = regex.group(1).trim().toLowerCase(Locale.ENGLISH); this.entryContainer = entryContainer; - return entryContainer != null; - } + if (entryContainer == null) { + return false; + } - @Override - public boolean preLoad() { SectionNode node = entryContainer.getSource(); if (node.isEmpty()) { @@ -88,25 +96,86 @@ public boolean preLoad() { return false; } - List> fields = getFields(node); - - if (fields == null) - return false; - var templateManager = Oopsk.getTemplateManager(); if (templateManager.getTemplate(name) != null) { Skript.error("Struct by the name of " + name + " already exists."); return false; } - template = new StructTemplate(name, fields); - if (!templateManager.addTemplate(template)) + + registerCustomType(); // TODO: safety against name clashes + + return true; + } + + @Override + public boolean preLoad() { + SectionNode node = entryContainer.getSource(); + var templateManager = Oopsk.getTemplateManager(); + + List> fields = getFields(node); + + if (fields == null) { + unregisterCustomType(); return false; + } + + template = new StructTemplate(name, fields, customClass); + return templateManager.addTemplate(template); + } + + private TemporaryClassManager classManager; + public Class customClass; + public ClassInfo customClassInfo; + + // this is a crime against humanity + private void registerCustomType() { + + assert classManager == null; + classManager = new TemporaryClassManager(); + + // Create a dynamic subclass + //noinspection unchecked + customClass = (Class) classManager.createTemporarySubclass("Struct_"+ name.replaceAll("[^a-zA-Z0-9_]", "_")); + assert customClass != null; + // Register the class if it doesn't exist + String codeName = name.toLowerCase(Locale.ENGLISH) + "struct"; + codeName = codeName.replaceAll("_", "underscore"); + addLanguageNode("types." + codeName , name + " struct"); + customClassInfo = new ClassInfo<>(customClass, codeName) + .user(name + " structs?( types?)?"); + + // hack open the Classes class to allow re-registration + addClassInfo(customClassInfo); + System.out.println("By class: " + Classes.getExactClassInfo(customClass)); + System.out.println("By user input: " + Classes.getClassInfoFromUserInput(name + " struct")); + } + + private void unregisterCustomType() { + if (customClassInfo != null) { + enableRegistrations(); + removeLanguageNode("types." + customClassInfo.getCodeName()); + removeClassInfo(customClassInfo); + disableRegistrations(); + customClassInfo = null; + customClass = null; + } + if (classManager != null) { + classManager.unloadAll(); + classManager = null; + } + } + + + @Override + public boolean load() { + var templateManager = Oopsk.getTemplateManager(); // delayed parse so all fields are present getParser().setCurrentEvent("parse template", DynamicFieldEvalEvent.class); if (!template.parseFields()) { templateManager.removeTemplate(template); + unregisterCustomType(); return false; } getParser().deleteCurrentEvent(); @@ -114,7 +183,6 @@ public boolean preLoad() { return true; } - private static final Pattern fieldPattern = Pattern.compile("(?const(?:ant)? )?(?dynamic)?(?[\\w ]+): (?[\\w ]+?)(?: ?= ?(?.+))?"); private List> getFields(@NotNull SectionNode node) { @@ -175,15 +243,11 @@ private List> getFields(@NotNull SectionNode node) { } return fields; } - @Override - public boolean load() { - - return true; - } @Override public void unload() { Oopsk.getTemplateManager().removeTemplate(template); + unregisterCustomType(); } @Override diff --git a/src/test/scripts/basics.sk b/src/test/scripts/basics.sk index 68d4e8c..ed37d82 100644 --- a/src/test/scripts/basics.sk +++ b/src/test/scripts/basics.sk @@ -8,7 +8,7 @@ struct basic: d{@a}: number = {@a} test "basic struct behavior": - set {_struct} to a basic struct + set {_struct} to a basic struct instance assert {_struct}->a is not set with "struct field a was somehow set" assert {_struct}->b is not set with "struct field b was somehow set" assert {_struct}->c is a cow with "struct field c was not set to cow" diff --git a/src/test/scripts/copies.sk b/src/test/scripts/copies.sk index 44dcddb..4521044 100644 --- a/src/test/scripts/copies.sk +++ b/src/test/scripts/copies.sk @@ -3,7 +3,7 @@ struct copyable: copy_vectors: vectors test "copy structs": - set {_A} to a copyable struct: + set {_A} to a copyable struct instance: copy_num: 1 copy_vectors: vector(1, 2, 3) and vector(4, 5, 6) diff --git a/src/test/scripts/customtypes.sk b/src/test/scripts/customtypes.sk new file mode 100644 index 0000000..fad1959 --- /dev/null +++ b/src/test/scripts/customtypes.sk @@ -0,0 +1,28 @@ +struct custom: + a: int = 0 + b: strings + c: custom struct + +struct standard: + a: int = 1 + +local function test(a: custom struct) returns custom struct: + return {_a} + +test "custom struct behavior": + set {_a} to a custom struct instance + set {_b} to a custom struct instance: + a: 5 + b: "hello" + c: {_a} + assert {_b}->a is 5 with "struct field a was not set to 5" + assert {_b}->b is "hello" with "struct field b was not set to hello" + assert {_b}->c is {_a} with "struct field c was not set to {_a}" + assert {_b}->c->a is 0 with "nested struct field a was not set to 0" + + assert test({_b})->c is {_a} with "function did not return the correct struct" + assert test({_b})->c is a custom struct with "function returned struct field b was somehow set" + + parse: + test(a standard struct instance) + assert last parse logs contain "Can't understand this condition/effect" with "function did not error on wrong struct type" From 08c8bc4ebc1dff76cc854f85bbdc8381069cbff1 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sat, 4 Oct 2025 12:03:55 -0700 Subject: [PATCH 2/8] shadow + tests --- build.gradle | 28 ++++++++++++++++++++++++---- settings.gradle | 9 +++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index c0fd2fb..a3a6285 100644 --- a/build.gradle +++ b/build.gradle @@ -2,6 +2,8 @@ import org.apache.tools.ant.filters.ReplaceTokens plugins { id 'java' + id 'skript-test' version('1.0.1') + id 'com.gradleup.shadow' version('9.2.2') } group = 'com.sovdee' @@ -12,6 +14,7 @@ test { repositories { mavenCentral() + mavenLocal() maven { url 'https://repo.papermc.io/repository/maven-public/' } @@ -21,14 +24,31 @@ repositories { } dependencies { - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' - compileOnly 'org.spigotmc:spigot-api:1.21.4-R0.1-SNAPSHOT' - compileOnly "com.github.SkriptLang:Skript:2.11.0" + compileOnly "com.github.SkriptLang:Skript:2.12.2" compileOnly 'org.jetbrains:annotations:26.0.1' + implementation 'net.bytebuddy:byte-buddy:1.14.18' // for runtime code generation } processResources { filter ReplaceTokens, tokens: ["version": project.property("version")] } + +shadowJar { + relocate 'net.bytebuddy', 'com.sovdee.oopsk.bytebuddy' + minimize() + archiveClassifier.set('') +} + +build { + dependsOn shadowJar +} + + +skriptTest { + dependsOn build + testScriptDirectory = file("src/test/scripts") + extraPluginsDirectory = new File("./build/libs") + skriptRepoRef = "dev/patch" + runVanillaTests = false +} diff --git a/settings.gradle b/settings.gradle index 075b72a..83541dd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,10 @@ +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + } +} + rootProject.name = 'oopsk' + + From d76a802b00713c3ae0e308b17f3723c0cb9e8a9a Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sat, 4 Oct 2025 12:33:56 -0700 Subject: [PATCH 3/8] polish --- src/main/java/com/sovdee/oopsk/Oopsk.java | 6 ++++++ .../core/generation/ReflectionUtils.java | 3 --- .../elements/expressions/ExprThisStruct.java | 21 +++++++++++++++---- .../structures/StructStructTemplate.java | 12 +---------- 4 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/sovdee/oopsk/Oopsk.java b/src/main/java/com/sovdee/oopsk/Oopsk.java index 83a86dd..e4cfa02 100644 --- a/src/main/java/com/sovdee/oopsk/Oopsk.java +++ b/src/main/java/com/sovdee/oopsk/Oopsk.java @@ -5,6 +5,7 @@ import ch.njol.skript.bstats.bukkit.Metrics; import com.sovdee.oopsk.core.StructManager; import com.sovdee.oopsk.core.TemplateManager; +import com.sovdee.oopsk.core.generation.TemporaryClassManager; import org.bukkit.plugin.java.JavaPlugin; import java.io.IOException; @@ -16,6 +17,7 @@ public final class Oopsk extends JavaPlugin { private static SkriptAddon addon; private static StructManager structManager; private static TemplateManager templateManager; + private static TemporaryClassManager classManager = new TemporaryClassManager(); private static Logger logger; public static Oopsk getInstance() { @@ -34,6 +36,10 @@ public static TemplateManager getTemplateManager() { return templateManager; } + public static TemporaryClassManager getClassManager() { + return classManager; + } + public static void info(String message) { logger.info(message); } diff --git a/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java b/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java index 96796d0..40cffac 100644 --- a/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java +++ b/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java @@ -84,7 +84,6 @@ public static void addLanguageNode(String key, String value) { try { @SuppressWarnings("unchecked") HashMap langMap = (HashMap) localizedLanguageField.get(null); - System.out.println("Adding language node: " + key + " -> " + value); langMap.put(key, value); } catch (Exception e) { throw new RuntimeException("Failed to add language node.", e); @@ -95,7 +94,6 @@ public static void removeLanguageNode(String key) { try { @SuppressWarnings("unchecked") HashMap langMap = (HashMap) localizedLanguageField.get(null); - System.out.println("Removing language node: " + key + " (was " + langMap.get(key) + ")"); langMap.remove(key); } catch (Exception e) { throw new RuntimeException("Failed to add language node.", e); @@ -123,7 +121,6 @@ public static void addClassInfo(ClassInfo classInfo) { } catch (Exception e) { throw new RuntimeException(e); } - System.out.println("Registered custom struct class: " + classInfo.getName()); disableRegistrations(); } diff --git a/src/main/java/com/sovdee/oopsk/elements/expressions/ExprThisStruct.java b/src/main/java/com/sovdee/oopsk/elements/expressions/ExprThisStruct.java index d820975..cdf0152 100644 --- a/src/main/java/com/sovdee/oopsk/elements/expressions/ExprThisStruct.java +++ b/src/main/java/com/sovdee/oopsk/elements/expressions/ExprThisStruct.java @@ -10,11 +10,15 @@ import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.util.Kleenean; +import ch.njol.util.coll.CollectionUtils; import com.sovdee.oopsk.core.Struct; +import com.sovdee.oopsk.elements.structures.StructStructTemplate; import com.sovdee.oopsk.events.DynamicFieldEvalEvent; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; +import java.lang.reflect.Array; + @Name("This Struct") @Description("Usable only in dynamic field expressions, this refers to whatever struct is evaluating this field.") @Example(""" @@ -30,6 +34,8 @@ public class ExprThisStruct extends SimpleExpression { Skript.registerExpression(ExprThisStruct.class, Struct.class, ExpressionType.SIMPLE, "this [struct]"); } + private Class structClass; + @Override public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { // check for right event @@ -37,14 +43,20 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean is Skript.error("The 'this struct' expression can only be used in a struct template definition."); return false; } + var structure = getParser().getCurrentStructure(); + if (!(structure instanceof StructStructTemplate templateStructure)) { + Skript.error("The 'this struct' expression can only be used in a struct template definition."); + return false; + } + structClass = templateStructure.customClass; return true; } @Override protected Struct @Nullable [] get(Event event) { if (!(event instanceof DynamicFieldEvalEvent evalEvent)) - return new Struct[0]; - return new Struct[]{evalEvent.getStruct()}; + return (Struct[]) Array.newInstance(structClass, 0); + return CollectionUtils.array(evalEvent.getStruct()); } @Override @@ -53,12 +65,13 @@ public boolean isSingle() { } @Override - public Class getReturnType() { - return Struct.class; + public Class getReturnType() { + return structClass; } @Override public String toString(@Nullable Event event, boolean debug) { return "this struct"; } + } diff --git a/src/main/java/com/sovdee/oopsk/elements/structures/StructStructTemplate.java b/src/main/java/com/sovdee/oopsk/elements/structures/StructStructTemplate.java index 12fc072..f8cacc8 100644 --- a/src/main/java/com/sovdee/oopsk/elements/structures/StructStructTemplate.java +++ b/src/main/java/com/sovdee/oopsk/elements/structures/StructStructTemplate.java @@ -20,7 +20,6 @@ import com.sovdee.oopsk.core.Field.Modifier; import com.sovdee.oopsk.core.Struct; import com.sovdee.oopsk.core.StructTemplate; -import com.sovdee.oopsk.core.generation.TemporaryClassManager; import com.sovdee.oopsk.events.DynamicFieldEvalEvent; import org.bukkit.event.Event; import org.jetbrains.annotations.NotNull; @@ -124,15 +123,12 @@ public boolean preLoad() { return templateManager.addTemplate(template); } - private TemporaryClassManager classManager; public Class customClass; public ClassInfo customClassInfo; // this is a crime against humanity private void registerCustomType() { - - assert classManager == null; - classManager = new TemporaryClassManager(); + var classManager = Oopsk.getClassManager(); // Create a dynamic subclass //noinspection unchecked @@ -147,8 +143,6 @@ private void registerCustomType() { // hack open the Classes class to allow re-registration addClassInfo(customClassInfo); - System.out.println("By class: " + Classes.getExactClassInfo(customClass)); - System.out.println("By user input: " + Classes.getClassInfoFromUserInput(name + " struct")); } private void unregisterCustomType() { @@ -160,10 +154,6 @@ private void unregisterCustomType() { customClassInfo = null; customClass = null; } - if (classManager != null) { - classManager.unloadAll(); - classManager = null; - } } From 31595ec15e1a46da1b30426256fabc64d444542a Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sat, 4 Oct 2025 12:53:23 -0700 Subject: [PATCH 4/8] return empty array when creating a struct whose template doesn't exist --- .../oopsk/elements/expressions/ExprSecStructInstance.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/sovdee/oopsk/elements/expressions/ExprSecStructInstance.java b/src/main/java/com/sovdee/oopsk/elements/expressions/ExprSecStructInstance.java index c7a2f97..26a1ebb 100644 --- a/src/main/java/com/sovdee/oopsk/elements/expressions/ExprSecStructInstance.java +++ b/src/main/java/com/sovdee/oopsk/elements/expressions/ExprSecStructInstance.java @@ -138,8 +138,10 @@ public boolean parseInitialValues(@NotNull SectionNode node, StructTemplate temp @Override protected Struct @Nullable [] get(Event event) { StructTemplate template = Oopsk.getTemplateManager().getTemplate(name); - if (template == null) + if (template == null) { error("A struct by the name of '" + name + "' does not exist."); + return CollectionUtils.array(); + } Struct struct = Oopsk.getStructManager().createStruct(template, event, parsedFieldValues); return CollectionUtils.array(struct); } From a8b5889832cc0ec9e3e91de2dbd7d0e529df2ca7 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sat, 4 Oct 2025 12:55:02 -0700 Subject: [PATCH 5/8] comment out local test --- build.gradle | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index a3a6285..c1d148a 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ import org.apache.tools.ant.filters.ReplaceTokens plugins { id 'java' - id 'skript-test' version('1.0.1') +// id 'skript-test' version('1.0.1') id 'com.gradleup.shadow' version('9.2.2') } @@ -45,10 +45,10 @@ build { } -skriptTest { - dependsOn build - testScriptDirectory = file("src/test/scripts") - extraPluginsDirectory = new File("./build/libs") - skriptRepoRef = "dev/patch" - runVanillaTests = false -} +//skriptTest { +// dependsOn build +// testScriptDirectory = file("src/test/scripts") +// extraPluginsDirectory = new File("./build/libs") +// skriptRepoRef = "dev/patch" +// runVanillaTests = false +//} From 5207c7520fd355c619a3b612fc3a34d88a52ff5b Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sat, 4 Oct 2025 13:53:57 -0700 Subject: [PATCH 6/8] keep classes/classinfos around and re-use to avoid stale references, add converters from Struct to subclasses. --- .../core/generation/ReflectionUtils.java | 59 ++++++++++++++++++- .../generation/TemporaryClassManager.java | 19 +++--- .../structures/StructStructTemplate.java | 8 +-- 3 files changed, 68 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java b/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java index 40cffac..cc82a3f 100644 --- a/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java +++ b/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java @@ -4,12 +4,16 @@ import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.localization.Language; import ch.njol.skript.registrations.Classes; +import com.sovdee.oopsk.core.Struct; +import org.skriptlang.skript.lang.converter.ConverterInfo; +import org.skriptlang.skript.lang.converter.Converters; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; public class ReflectionUtils { @@ -19,6 +23,7 @@ public class ReflectionUtils { private static final Field acceptRegistrationsField; private static final Field localizedLanguageField; private static final Field classInfosField; + private static final Field convertersField; private static final Method sortClassInfosMethod; static { @@ -30,6 +35,7 @@ public class ReflectionUtils { acceptRegistrationsField = Skript.class.getDeclaredField("acceptRegistrations"); localizedLanguageField = Language.class.getDeclaredField("localizedLanguage"); classInfosField = Classes.class.getDeclaredField("classInfos"); + convertersField = Converters.class.getDeclaredField("CONVERTERS"); // Get the method sortClassInfosMethod = Classes.class.getDeclaredMethod("sortClassInfos"); @@ -42,6 +48,7 @@ public class ReflectionUtils { acceptRegistrationsField.setAccessible(true); localizedLanguageField.setAccessible(true); classInfosField.setAccessible(true); + convertersField.setAccessible(true); sortClassInfosMethod.setAccessible(true); } catch (Exception e) { @@ -65,6 +72,11 @@ private static void setFieldValue(boolean value) { } } + @SuppressWarnings("unchecked") + public static List> getConverters() throws Exception { + return (List>) convertersField.get(null); + } + @SuppressWarnings("unchecked") public static List> getTempClassInfos() throws Exception { return (List>) tempClassInfosField.get(null); @@ -113,20 +125,63 @@ public static void sortClassInfos() throws Exception { sortClassInfosMethod.invoke(null); } - public static void addClassInfo(ClassInfo classInfo) { + public static ClassInfo addClassInfo(Class customClass, String name) { + + // get the classinfo if it exists + String codeName = name.toLowerCase(Locale.ENGLISH) + "struct"; + codeName = codeName.replaceAll("_", "underscore"); + + //noinspection unchecked + ClassInfo customClassInfo = (ClassInfo) Classes.getClassInfoNoError(codeName); + if (customClassInfo != null) { + if (!customClassInfo.getC().equals(customClass)) { + // conflict, remove the old one and try again + removeClassInfo(customClassInfo); + return addClassInfo(customClass, name); + } + // already exists, adopt it. + return customClassInfo; + } + + // get by class + customClassInfo = Classes.getExactClassInfo(customClass); + if (customClassInfo != null) { + if (!customClassInfo.getCodeName().equals(codeName)) { + // conflict, remove the old one and try again + removeClassInfo(customClassInfo); + return addClassInfo(customClass, name); + } + // already exists, adopt it. + return customClassInfo; + } + + addLanguageNode("types." + codeName , name + " struct"); + customClassInfo = new ClassInfo<>(customClass, codeName) + .user(name + " structs?( types?)?"); + enableRegistrations(); - Classes.registerClass(classInfo); + Classes.registerClass(customClassInfo); try { resortClassInfos(); } catch (Exception e) { throw new RuntimeException(e); } + //noinspection unchecked + Converters.registerConverter(Struct.class, (Class) customClass, struct -> { + if (customClass.isInstance(struct)) + return customClass.cast(struct); + return null; + }); disableRegistrations(); + return customClassInfo; } // Remove from all three collections public static void removeClassInfo(ClassInfo classInfo) { try { + var converter = Converters.getConverterInfo(Struct.class, classInfo.getC()); + if (converter != null) + getConverters().remove(converter); getExactClassInfos().remove(classInfo.getC()); getClassInfosByCodeName().remove(classInfo.getCodeName()); resortClassInfos(classInfo); diff --git a/src/main/java/com/sovdee/oopsk/core/generation/TemporaryClassManager.java b/src/main/java/com/sovdee/oopsk/core/generation/TemporaryClassManager.java index bde4339..1b08da6 100644 --- a/src/main/java/com/sovdee/oopsk/core/generation/TemporaryClassManager.java +++ b/src/main/java/com/sovdee/oopsk/core/generation/TemporaryClassManager.java @@ -6,10 +6,14 @@ import java.net.URL; import java.net.URLClassLoader; +import java.util.HashMap; +import java.util.Map; public class TemporaryClassManager { private ClassLoader disposableClassLoader; + private Map> createdClasses = new HashMap<>(); + public TemporaryClassManager() { resetClassLoader(); } @@ -23,20 +27,17 @@ private void resetClassLoader() { } public Class createTemporarySubclass(String name) { - return new ByteBuddy() + if (createdClasses.containsKey(name)) { + return createdClasses.get(name); + } + var c = new ByteBuddy() .subclass(Struct.class) .name(name) .make() .load(disposableClassLoader, ClassLoadingStrategy.Default.WRAPPER) .getLoaded(); + createdClasses.put(name, c); + return c; } - public void unloadAll() { - // Drop the ClassLoader and all its classes - disposableClassLoader = null; - System.gc(); // Suggest garbage collection - - // Create a fresh ClassLoader for new classes - resetClassLoader(); - } } diff --git a/src/main/java/com/sovdee/oopsk/elements/structures/StructStructTemplate.java b/src/main/java/com/sovdee/oopsk/elements/structures/StructStructTemplate.java index f8cacc8..67131e7 100644 --- a/src/main/java/com/sovdee/oopsk/elements/structures/StructStructTemplate.java +++ b/src/main/java/com/sovdee/oopsk/elements/structures/StructStructTemplate.java @@ -134,15 +134,9 @@ private void registerCustomType() { //noinspection unchecked customClass = (Class) classManager.createTemporarySubclass("Struct_"+ name.replaceAll("[^a-zA-Z0-9_]", "_")); assert customClass != null; - // Register the class if it doesn't exist - String codeName = name.toLowerCase(Locale.ENGLISH) + "struct"; - codeName = codeName.replaceAll("_", "underscore"); - addLanguageNode("types." + codeName , name + " struct"); - customClassInfo = new ClassInfo<>(customClass, codeName) - .user(name + " structs?( types?)?"); // hack open the Classes class to allow re-registration - addClassInfo(customClassInfo); + addClassInfo(customClass, name); } private void unregisterCustomType() { From 71034e9d98aff1e9226dcf2d089c2a02f2f444f4 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sat, 4 Oct 2025 14:13:47 -0700 Subject: [PATCH 7/8] handle chained converters --- .../core/generation/ReflectionUtils.java | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java b/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java index cc82a3f..60d76de 100644 --- a/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java +++ b/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java @@ -5,11 +5,13 @@ import ch.njol.skript.localization.Language; import ch.njol.skript.registrations.Classes; import com.sovdee.oopsk.core.Struct; +import org.skriptlang.skript.lang.converter.Converter; import org.skriptlang.skript.lang.converter.ConverterInfo; import org.skriptlang.skript.lang.converter.Converters; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -154,7 +156,7 @@ public static ClassInfo addClassInfo(Class c // already exists, adopt it. return customClassInfo; } - + addLanguageNode("types." + codeName , name + " struct"); customClassInfo = new ClassInfo<>(customClass, codeName) .user(name + " structs?( types?)?"); @@ -166,12 +168,33 @@ public static ClassInfo addClassInfo(Class c } catch (Exception e) { throw new RuntimeException(e); } - //noinspection unchecked - Converters.registerConverter(Struct.class, (Class) customClass, struct -> { + + // converters + Converter castingConverter = struct -> { if (customClass.isInstance(struct)) return customClass.cast(struct); return null; - }); + }; + //noinspection unchecked + Converters.registerConverter(Struct.class, (Class) customClass, castingConverter); + + // chained converters + try { + getConverters().forEach(converterInfo -> { + if (converterInfo.getTo().equals(Struct.class)) + //noinspection unchecked + Converters.registerConverter(converterInfo.getFrom(), (Class) customClass, from -> { + //noinspection unchecked + Struct middle = ((Converter) converterInfo.getConverter()).convert((Object) from); + if (middle == null) + return null; + return castingConverter.convert(middle); + }, converterInfo.getFlags()); + }); + } catch (Exception e) { + throw new RuntimeException(e); + } + disableRegistrations(); return customClassInfo; } @@ -179,9 +202,15 @@ public static ClassInfo addClassInfo(Class c // Remove from all three collections public static void removeClassInfo(ClassInfo classInfo) { try { - var converter = Converters.getConverterInfo(Struct.class, classInfo.getC()); - if (converter != null) - getConverters().remove(converter); + Class customClass = classInfo.getC(); + List> toRemove = new ArrayList<>(); + var converters = getConverters(); + for (var converterInfo : converters) { + if (converterInfo.getTo().equals(customClass)) + toRemove.add(converterInfo); + } + converters.removeAll(toRemove); + getExactClassInfos().remove(classInfo.getC()); getClassInfosByCodeName().remove(classInfo.getCodeName()); resortClassInfos(classInfo); From 5e9bfeb22032f2d152cc2e0c34c9bf6a798f381a Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sun, 12 Oct 2025 21:21:05 -0700 Subject: [PATCH 8/8] struct copy docs --- .../sovdee/oopsk/core/generation/ReflectionUtils.java | 2 +- .../oopsk/elements/expressions/ExprStructCopy.java | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java b/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java index 60d76de..f6a5377 100644 --- a/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java +++ b/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java @@ -185,7 +185,7 @@ public static ClassInfo addClassInfo(Class c //noinspection unchecked Converters.registerConverter(converterInfo.getFrom(), (Class) customClass, from -> { //noinspection unchecked - Struct middle = ((Converter) converterInfo.getConverter()).convert((Object) from); + Struct middle = ((Converter) converterInfo.getConverter()).convert(from); if (middle == null) return null; return castingConverter.convert(middle); diff --git a/src/main/java/com/sovdee/oopsk/elements/expressions/ExprStructCopy.java b/src/main/java/com/sovdee/oopsk/elements/expressions/ExprStructCopy.java index 3e33c92..dc33dcf 100644 --- a/src/main/java/com/sovdee/oopsk/elements/expressions/ExprStructCopy.java +++ b/src/main/java/com/sovdee/oopsk/elements/expressions/ExprStructCopy.java @@ -1,9 +1,18 @@ package com.sovdee.oopsk.elements.expressions; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; import ch.njol.skript.expressions.base.SimplePropertyExpression; import com.sovdee.oopsk.core.Struct; import org.jetbrains.annotations.Nullable; +@Name("Struct Copy") +@Description("Makes a copy of a struct. The field contents may or may not be copies, depending on their types. " + + "Entities, for example, cannot be copied.") +@Example("set {_a} to a struct copy of {_b}->playerdata") +@Since("1.0") public class ExprStructCopy extends SimplePropertyExpression { static {