Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
`<type> via <expression>`, where `<type>` is the type to convert to and `<expression>` 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
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -16,6 +17,7 @@
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

public class ReflectionUtils {

Expand All @@ -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 {
Expand All @@ -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");
Expand All @@ -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) {
Expand Down Expand Up @@ -79,6 +84,11 @@ public static List<ConverterInfo<?,?>> getConverters() throws Exception {
return (List<ConverterInfo<?, ?>>) convertersField.get(null);
}

public static Map<Pair<Class<?>, Class<?>>, ConverterInfo<?, ?>> getQuickAccessConverters() throws Exception {
//noinspection unchecked
return (Map<Pair<Class<?>, Class<?>>, ConverterInfo<?, ?>>) quickAccessConvertersField.get(null);
}

@SuppressWarnings("unchecked")
public static List<ClassInfo<?>> getTempClassInfos() throws Exception {
return (List<ClassInfo<?>>) tempClassInfosField.get(null);
Expand Down Expand Up @@ -170,6 +180,12 @@ public static ClassInfo<? extends Struct> addClassInfo(Class<? extends Struct> c
}

// converters
try {
//noinspection SuspiciousMethodCalls,removal
getQuickAccessConverters().remove(new Pair<>(Struct.class, customClass));
} catch (Exception e) {
throw new RuntimeException(e);
}
Converter<Struct, Struct> castingConverter = struct -> {
if (customClass.isInstance(struct))
return customClass.cast(struct);
Expand Down Expand Up @@ -206,10 +222,18 @@ public static void removeClassInfo(ClassInfo<?> classInfo) {
List<ConverterInfo<?,?>> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -52,21 +59,36 @@
"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 '<target type> 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:
sender: player
message: string
const timestamp: date = now
attachments: objects
converts to:
string via this->message
""")
@Example("""
struct Vector2:
x: number
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 {

Expand All @@ -77,6 +99,7 @@ public class StructStructTemplate extends Structure {
private StructTemplate template;
private EntryContainer entryContainer;
private String name;
private final Map<Class<?>, Converter<Struct, Object>> converters = new HashMap<>();

@Override
public boolean init(Literal<?>[] args, int matchedPattern, SkriptParser.ParseResult parseResult, @Nullable EntryContainer entryContainer) {
Expand Down Expand Up @@ -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<Struct, Object> converter = entry.getValue();
//noinspection unchecked
Converters.registerConverter((Class<Struct>) customClass, (Class<Object>) 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<? extends Struct> customClass;
public ClassInfo<?> customClassInfo;

Expand All @@ -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() {
Expand All @@ -150,7 +261,6 @@ private void unregisterCustomType() {
}
}


@Override
public boolean load() {
var templateManager = Oopsk.getTemplateManager();
Expand All @@ -162,6 +272,8 @@ public boolean load() {
unregisterCustomType();
return false;
}
SectionNode node = entryContainer.getSource();
registerConverters(node);
getParser().deleteCurrentEvent();

return true;
Expand Down