diff --git a/README.md b/README.md index a613e7a..0f1ad56 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,17 @@ struct Message: contents: string sender: offline player const timestamp: date = now + converts to: + string via this->contents ``` A template's name and field names are case-insensitive. The template name follows the same rules as a function name, while field names can only consist of letters, underscores, and spaces. Each field has a name and a type, as well as an optional default value. This default value may be an expression, as it is evaluated when a struct is created, not when the template is registered. Adding `const` or `constant` to the start will prevent the field from being changed after creation. +Structs also support conversion expressions, which allow you to define how a struct can be converted to other types. The syntax is +` via `, where `` is the type to convert to and `` is an expression that evaluates to that type. +The expression may use `this` to refer to the struct being converted. Multiple conversions can be defined by adding more lines under the `converts to:` section. + Creating a struct involves a simple expression: ``` set {_a} to a message struct @@ -31,6 +37,13 @@ set {_a} to a message struct: contents: "hello world" ``` +### Custom Types + +Struct templates automatically create a new Skript type using the template's name + `struct`. +In the above example, `message struct` is now a valid Skript type that can be used in function parameters and other struct fields. +**You should be very careful when reloading templates, as any existing code that used the type may break if the template was modified or removed.** +ALWAYS reload all scripts after modifying templates to ensure all code is properly re-parsed. + ### Type Safety oopsk attempts to ensure type safety at parse time via checking all fields with the given name. This means having unique field names across structs allows oopsk to give you more accurate parse errors, while sharing field names can result in invalid code not causing any errors during parsing. Any type violations not caught during parsing should be caught at runtime via runtime errors that cannot be suppressed. Note that code parsed in one script prior to updates to a struct in another script will not show parse errors until it is reloaded again, though it should properly emit runtime errors. 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 f6a5377..89704e0 100644 --- a/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java +++ b/src/main/java/com/sovdee/oopsk/core/generation/ReflectionUtils.java @@ -4,6 +4,7 @@ import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.localization.Language; import ch.njol.skript.registrations.Classes; +import ch.njol.util.Pair; import com.sovdee.oopsk.core.Struct; import org.skriptlang.skript.lang.converter.Converter; import org.skriptlang.skript.lang.converter.ConverterInfo; @@ -16,6 +17,7 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; public class ReflectionUtils { @@ -26,6 +28,7 @@ public class ReflectionUtils { private static final Field localizedLanguageField; private static final Field classInfosField; private static final Field convertersField; + private static final Field quickAccessConvertersField; private static final Method sortClassInfosMethod; static { @@ -38,6 +41,7 @@ public class ReflectionUtils { localizedLanguageField = Language.class.getDeclaredField("localizedLanguage"); classInfosField = Classes.class.getDeclaredField("classInfos"); convertersField = Converters.class.getDeclaredField("CONVERTERS"); + quickAccessConvertersField = Converters.class.getDeclaredField("QUICK_ACCESS_CONVERTERS"); // Get the method sortClassInfosMethod = Classes.class.getDeclaredMethod("sortClassInfos"); @@ -51,6 +55,7 @@ public class ReflectionUtils { localizedLanguageField.setAccessible(true); classInfosField.setAccessible(true); convertersField.setAccessible(true); + quickAccessConvertersField.setAccessible(true); sortClassInfosMethod.setAccessible(true); } catch (Exception e) { @@ -79,6 +84,11 @@ public static List> getConverters() throws Exception { return (List>) convertersField.get(null); } + public static Map, Class>, ConverterInfo> getQuickAccessConverters() throws Exception { + //noinspection unchecked + return (Map, Class>, ConverterInfo>) quickAccessConvertersField.get(null); + } + @SuppressWarnings("unchecked") public static List> getTempClassInfos() throws Exception { return (List>) tempClassInfosField.get(null); @@ -170,6 +180,12 @@ public static ClassInfo addClassInfo(Class c } // converters + try { + //noinspection SuspiciousMethodCalls,removal + getQuickAccessConverters().remove(new Pair<>(Struct.class, customClass)); + } catch (Exception e) { + throw new RuntimeException(e); + } Converter castingConverter = struct -> { if (customClass.isInstance(struct)) return customClass.cast(struct); @@ -206,10 +222,18 @@ public static void removeClassInfo(ClassInfo classInfo) { List> toRemove = new ArrayList<>(); var converters = getConverters(); for (var converterInfo : converters) { - if (converterInfo.getTo().equals(customClass)) + if (converterInfo.getTo().equals(customClass) || converterInfo.getFrom().equals(customClass)) toRemove.add(converterInfo); } converters.removeAll(toRemove); + // remove all converters from or to customClass + var quickAccessConverters = getQuickAccessConverters(); + // remove all converters starting from customClass + for (var key : new ArrayList<>(quickAccessConverters.keySet())) { + if (key.getKey().equals(customClass) || key.getValue().equals(customClass)) { + quickAccessConverters.remove(key); + } + } getExactClassInfos().remove(classInfo.getC()); getClassInfosByCodeName().remove(classInfo.getCodeName()); 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 67131e7..604fd28 100644 --- a/src/main/java/com/sovdee/oopsk/elements/structures/StructStructTemplate.java +++ b/src/main/java/com/sovdee/oopsk/elements/structures/StructStructTemplate.java @@ -11,10 +11,13 @@ import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Literal; +import ch.njol.skript.lang.ParseContext; import ch.njol.skript.lang.SkriptParser; import ch.njol.skript.lang.function.Functions; import ch.njol.skript.registrations.Classes; +import ch.njol.skript.util.LiteralUtils; import ch.njol.skript.util.Utils; +import ch.njol.util.Pair; import com.sovdee.oopsk.Oopsk; import com.sovdee.oopsk.core.Field; import com.sovdee.oopsk.core.Field.Modifier; @@ -24,22 +27,26 @@ import org.bukkit.event.Event; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.lang.converter.Converter; +import org.skriptlang.skript.lang.converter.Converters; import org.skriptlang.skript.lang.entry.EntryContainer; import org.skriptlang.skript.lang.structure.Structure; import java.util.ArrayList; import java.util.EnumSet; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.regex.MatchResult; 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.getQuickAccessConverters; import static com.sovdee.oopsk.core.generation.ReflectionUtils.removeClassInfo; import static com.sovdee.oopsk.core.generation.ReflectionUtils.removeLanguageNode; @@ -52,7 +59,11 @@ "The default value will be evaluated when the struct is created.", "Fields can be marked as constant by adding 'const' or 'constant' at the beginning of the line. Constant fields cannot be changed after the struct is created.", "Dynamic fields can be made by adding 'dynamic' to the beginning of the line. Dynamic fields require a default value and will always re-evaluate their value each time they are called. " + - "This means they cannot be changed directly, but can rely on the values of other fields or even functions." + "This means they cannot be changed directly, but can rely on the values of other fields or even functions.", + "Converters can be defined in a 'converts to:' section. Each converter is defined in the format ' via %expression%'. " + + "Note that oopsk cannot generate chained converters reliably, so you should expressly define converters for all target types you wish to convert to.", + "Be careful when using converters, as they can cause unexpected behavior in all of your scripts if not used properly." + + "Best practice is to ensure you reload all scripts after defining or modifying struct templates to ensure all converters are registered correctly." }) @Example(""" struct message: @@ -60,6 +71,8 @@ message: string const timestamp: date = now attachments: objects + converts to: + string via this->message """) @Example(""" struct Vector2: @@ -67,6 +80,15 @@ y: number dynamic length: number = sqrt(this->x^2 + this->y^2) """) +@Example(""" + struct CustomPlayer + const player: player + rank: string = "Member" + dynamic isAdmin: boolean = whether this->rank is "Admin" + converts to: + player via this->player + location via this->player's location + """) @Since("1.0") public class StructStructTemplate extends Structure { @@ -77,6 +99,7 @@ public class StructStructTemplate extends Structure { private StructTemplate template; private EntryContainer entryContainer; private String name; + private final Map, Converter> converters = new HashMap<>(); @Override public boolean init(Literal[] args, int matchedPattern, SkriptParser.ParseResult parseResult, @Nullable EntryContainer entryContainer) { @@ -123,6 +146,94 @@ public boolean preLoad() { return templateManager.addTemplate(template); } + private void registerConverters(SectionNode node) { + // find + boolean found = false; + for (Node child : node) { + if (child instanceof SectionNode convertersNode) { + String key = ScriptLoader.replaceOptions(convertersNode.getKey()); + if (key != null && key.trim().equalsIgnoreCase("converts to")) { + if (found) { + Skript.error("Multiple 'converts to' sections found in struct " + name + "."); + return; + } + parseConverters(convertersNode); + found = true; + if (this.converters.isEmpty()) { + Skript.error("No valid converters found in struct " + name + "'s 'converts to' section."); + return; + } + } else { + Skript.error("Unexpected section '" + key + "' found in struct " + name + "."); + return; + } + } + } + + // register + if (found) { + enableRegistrations(); + for (var entry : this.converters.entrySet()) { + Class targetClass = entry.getKey(); + Converter converter = entry.getValue(); + //noinspection unchecked + Converters.registerConverter((Class) customClass, (Class) targetClass, converter); + } + disableRegistrations(); + } + + } + + private static final Pattern CONVERTER_PATTERN = Pattern.compile("([\\w ]+) via (.+)"); + + private void parseConverters(@NotNull SectionNode node) { + for (Node child : node) { + if (child instanceof SimpleNode simpleNode) { + String entry = simpleNode.getKey(); + if (entry == null) + throw new IllegalStateException("Null node found."); + // split into type and converter + Matcher matcher = CONVERTER_PATTERN.matcher(entry); + if (!matcher.matches()) { + Skript.error("Invalid converter entry: " + entry); + continue; + } + String typeString = matcher.group(1).trim(); + String converterString = matcher.group(2).trim(); + + var pair = Utils.getEnglishPlural(typeString); + ClassInfo targetType = Classes.getClassInfoFromUserInput(pair.getKey()); + if (targetType == null) { + Skript.error("Invalid converter target type: " + typeString); + continue; + } + + // parse the converter expression + var converter = new SkriptParser(converterString, SkriptParser.ALL_FLAGS, ParseContext.DEFAULT).parseExpression(targetType.getC()); + if (converter == null || LiteralUtils.hasUnparsedLiteral(converter)) { + Skript.error("Converter expression does not return the declared type of " + Classes.toString(targetType) + ": '" + converterString + "'"); + continue; + } + + // clear quick access converter to ensure there isn't a null entry + try { + //noinspection removal,SuspiciousMethodCalls + getQuickAccessConverters().remove(new Pair<>(customClass, targetType.getC())); + } catch (Exception e) { + throw new RuntimeException(e); + } + + // register the converter + converters.put(targetType.getC(), new Converter<>() { + @Override + public @Nullable Object convert(Struct from) { + return converter.getSingle(new DynamicFieldEvalEvent(from)); + } + }); + } + } + } + public Class customClass; public ClassInfo customClassInfo; @@ -136,7 +247,7 @@ private void registerCustomType() { assert customClass != null; // hack open the Classes class to allow re-registration - addClassInfo(customClass, name); + customClassInfo = addClassInfo(customClass, name); } private void unregisterCustomType() { @@ -150,7 +261,6 @@ private void unregisterCustomType() { } } - @Override public boolean load() { var templateManager = Oopsk.getTemplateManager(); @@ -162,6 +272,8 @@ public boolean load() { unregisterCustomType(); return false; } + SectionNode node = entryContainer.getSource(); + registerConverters(node); getParser().deleteCurrentEvent(); return true;