diff --git a/build.gradle b/build.gradle index c0fd2fb..c1d148a 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' + + 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/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..f6a5377 --- /dev/null +++ b/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java @@ -0,0 +1,222 @@ +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 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; +import java.util.Locale; + +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 Field convertersField; + 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"); + convertersField = Converters.class.getDeclaredField("CONVERTERS"); + + // 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); + convertersField.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> getConverters() throws Exception { + return (List>) convertersField.get(null); + } + + @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); + 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); + 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 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(customClassInfo); + try { + resortClassInfos(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + // 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(from); + if (middle == null) + return null; + return castingConverter.convert(middle); + }, converterInfo.getFlags()); + }); + } catch (Exception e) { + throw new RuntimeException(e); + } + + disableRegistrations(); + return customClassInfo; + } + + // Remove from all three collections + public static void removeClassInfo(ClassInfo classInfo) { + try { + 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); + } 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..1b08da6 --- /dev/null +++ b/src/main/java/com/sovdee/oopsk/core/generation/TemporaryClassManager.java @@ -0,0 +1,43 @@ +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; +import java.util.HashMap; +import java.util.Map; + +public class TemporaryClassManager { + private ClassLoader disposableClassLoader; + + private Map> createdClasses = new HashMap<>(); + + 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) { + 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; + } + +} 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..26a1ebb 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 @@ -138,9 +138,12 @@ 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 new Struct[] {Oopsk.getStructManager().createStruct(template, event, parsedFieldValues)}; + return CollectionUtils.array(); + } + Struct struct = Oopsk.getStructManager().createStruct(template, event, parsedFieldValues); + return CollectionUtils.array(struct); } @Override @@ -149,7 +152,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..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 { @@ -12,16 +21,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/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 1e453c7..67131e7 100644 --- a/src/main/java/com/sovdee/oopsk/elements/structures/StructStructTemplate.java +++ b/src/main/java/com/sovdee/oopsk/elements/structures/StructStructTemplate.java @@ -18,6 +18,7 @@ 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.events.DynamicFieldEvalEvent; import org.bukkit.event.Event; @@ -35,6 +36,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 +84,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 +95,71 @@ 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); + } + + public Class customClass; + public ClassInfo customClassInfo; + + // this is a crime against humanity + private void registerCustomType() { + var classManager = Oopsk.getClassManager(); + + // Create a dynamic subclass + //noinspection unchecked + customClass = (Class) classManager.createTemporarySubclass("Struct_"+ name.replaceAll("[^a-zA-Z0-9_]", "_")); + assert customClass != null; + + // hack open the Classes class to allow re-registration + addClassInfo(customClass, name); + } + + private void unregisterCustomType() { + if (customClassInfo != null) { + enableRegistrations(); + removeLanguageNode("types." + customClassInfo.getCodeName()); + removeClassInfo(customClassInfo); + disableRegistrations(); + customClassInfo = null; + customClass = 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 +167,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 +227,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"