Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Registration API #6246

Open
wants to merge 149 commits into
base: dev/feature
Choose a base branch
from
Open

Conversation

APickledWalrus
Copy link
Member

@APickledWalrus APickledWalrus commented Dec 22, 2023

Description

This is built from the work done in #5331 (thanks @kiip1)

This PR is the initial effort to build a modern API for Skript. All new API has been marked as experimental. Backwards compatibility has been kept. The focus of this PR is to introduce a new addon registration process and syntax registration process. It also implements many new utility interfaces and classes for usage across Skript.

Breaking Changes

  • The SyntaxElementInfo getters in the Skript class have been updated to return unmodifiable lists.

New Skript Core

The new Skript interface represents the core of the language. At this time, implementations require methods for registering an addon and obtaining a collection of all addons. The Skript interface is an extension of the SkriptAddon interface, which is further described below.

A default implementation is available, which is what the plugin uses. Below is an example of creating a Skript instance using the default implementation.

Skript skript = Skript.of(getClass(), "Skript"); // parameter is the name for the addon representing Skript

New Addon API

The SkriptAddon interface is the center of the new Addon API. At this time, every SkriptAddon has a name, syntax registry, and localizer (the last two are described in further detail below)

An addon can be registered as follows:

Skript skript = ...
SkriptAddon myAddon = skript.registerAddon(getClass(), "myAddon");

Addon Modules

Also included in this new API are AddonModules, which enables Skript and its addons to be broken up into categories. For example, a Skript implementation not dependent on Minecraft might have a default module containing something like arithmetic.

Modules are functional interfaces with one method, load. The load method has a SkriptAddon parameter that can be used for accessing the syntax registry, localizer, and other things when they are added!

Loading a module is rather simple:

SkriptAddon myAddon = ...
myAddon.loadModules(new MathModule())

Note: loadModules() is simply a helper method for `new MathModule().load(myAddon)

New Syntax Registration API

The syntax registration API is the biggest component of this PR. The registration process has been completely overhauled to provide greater control over the registration process.

Syntax Infos

The SyntaxInfo interface is a replacement for the SyntaxElementInfo class.

Every SyntaxInfo has the following properties:

  • An origin describing where it is from
  • SyntaxElement class providing the implementation
  • A method for obtaining a new instance of the SyntaxElement class
  • A collection of uncompiled patterns (string form)
  • A priority that dictates its position in the registry

Two default extensions exist:

  • SyntaxInfo.Expression
    • Has an additional property for the return type of the expression
  • SyntaxInfo.Structure
    • Has an additional property for the EntryValidator of the structure

Additionally, one Bukkit-specific implementation exists for Bukkit events:

  • BukkitInfos.Event which has the following properties:
    • Documentation methods to be used for something like SimpleEvent
    • A collection of event classes for the Bukkit events the SkriptEvent represents

Builders

Four builders exist for creating a SyntaxInfo, SyntaxInfo.Expression, SyntaxInfo.Structure, and BukkitInfos.Event. These builders are implemented using the new Builder/Buildable interfaces, described below.

The following is an example of building a SyntaxInfo for LitPi with the builder for SyntaxInfo.Expression:

SyntaxInfo.Expression.builder(LitPi.class) // Syntax class
	.returnType(Double.class) // Expression return supertype
	.origin(SyntaxOrigin.of(Skript.instance())) // (optional) This syntax is from Skript itself
	.supplier(LitPi::new) // (optional) Provides Skript with a way to create the object rather than using reflection
	.priority(SyntaxInfo.SIMPLE) // (optional) Equivalent to ExpressionType.SIMPLE
	.addPattern("(pi|π)")
	.build();

SyntaxOrigin

A SyntaxOrigin describes where a syntax has come from. By default, it only has a name (String) which describes the origin. When specified, this would likely be the name of an addon (see AddonOrigin). When not specified, it is the fully qualified name of the SyntaxElement class.

Priorities

The priority system is an evolution of ExpressionType. Unlike ExpressionType, which is only for expressions, the priority system applies to all SyntaxInfos. By default, Skript has three priorities:

  • SIMPLE (for patterns that are simple text)
  • COMBINED (for patterns containing at least one expression) 
  • PATTERN_MATCHES_EVERYTHING (for patterns, like ExprArithmetic, that are likely to (partially) match most user inputs)

ExpressionType#EVENT and ExpressionType#PROPERTY have been replaced with the constants EventValueExpression#DEFAULT_PRIORITY and PropertyExpression#DEFAULT_PRIORITY respectively. The former lies between SIMPLE/COMBINED while the latter lies between COMBINED/PATTERN_MATCHES_EVERYTHING
Additionally, PropertyCondition#DEFAULT_PRIORITY has been added for conditions. It also lies between COMBINED/PATTERN_MATCHES_EVERYTHING.

Unlike Structure priorities, this new Priority system is purely relational. There are no "magic" numbers that determine position. The definition for the default three may be useful for visualizing this:

Priority SIMPLE = Priority.base(); // A base priority is one that has no relation to another.
Priority COMBINED = Priority.after(SIMPLE); // That is, it comes after SIMPLE
Priority PATTERN_MATCHES_EVERYTHING = Priority.after(COMBINED); // That is, it comes after COMBINED, which comes after SIMPLE

Additionally, a Priority may be created that comes before some other Priority. If one wanted to create a Priority for syntax that should be parsed really early, they might do something like:

Priority REALLY_EARLY = Priority.before(SIMPLE);

SyntaxRegistry

The SyntaxRegistry is the home for an addon's syntax infos. A SyntaxRegistry has three important methods:

  • syntaxes(Key) which returns all SyntaxInfos registered under a Key (e.g. all expressions, effects, etc.)
  • register(Key, SyntaxInfo) which registers a SyntaxInfo under a Key
  • unregister(Key, SyntaxInfo) which unregisters a SyntaxInfo that is stored under a Key

A default implementation of a SyntaxRegistry may be obtained through:

SyntaxRegistry myRegistry = SyntaxRegistry.empty();

Keys

Keys are used for storing SyntaxInfos of a certain type. Skript has six built in keys:

  • STRUCTURE for structures
  • SECTION for sections
  • STATEMENT for effects and conditions (Statement classes)
  • EFFECT for effects
  • CONDITION for conditions
  • EXPRESSION for expressions

Creating a Key constant is simple:

Key<SyntaxInfo<? extends Statement>> STATEMENT = Key.of("statement");

Now, if we wanted to access the Statements in a registry:

SyntaxRegistry registry = ...
registry.syntaxes(STATEMENT);

Child Keys

Child Keys are the same as Keys, but they also have a parent Key. Thus, when a SyntaxInfo is registered under a Child Key, it is also registered under the parent Key. This is how the register for Statements works, as the Effect and Condition Keys are Child Keys of the Statement key. We can use this example to see how to build Child Keys:

Key<SyntaxInfo<? extends Statement>> STATEMENT = Key.of("statement");
// Anything registered under EFFECT will also be registered under STATEMENT
Key<SyntaxInfo<? extends Effect>> EFFECT = ChildKey.of(STATEMENT, "effect");

Experimental Localization API

This PR includes an experimental (and likely subject to change) API for Localization. At this point in time, it exists for modern addons to register their language files. I avoided creating too much API as that will be for a separate PR reworking localization. The key idea here is loading language files, which can be done as follows:

SkriptAddon addon = ...
addon.localizer().setSourceDirectories("lang", null);

setSourceDirectories takes in two parameters:

  • languageFileDirectory which is the path to the language file directory on the jar.
  • dataFileDirectory (optional) which is the path to the language file directory on the disk (e.g. user customizable lang files).

New Utilities

Builder/Buildable Interfaces

I have introduced two new interfaces for building objects along with converting objects into builders. The primary interface, builder, has two methods:

  • build() which is the terminal operation for a builder that returns an instance of the type being built.
  • applyTo(Builder) which enables applying (copying) the values of one builder onto another.

Another interface, Buildable, enables converting an object back into a builder. It has one method:

  • builder() which returns a builder representing the object.

For example, one might want to add a new pattern to a SyntaxInfo. This is now trivial:

SyntaxInfo info = ...;
info.builder()
	.addPattern("my new pattern")
	.build();

ClassLoader API

I have introduced a ClassLoader utility class which acts as a replacement for the SkriptAddon#loadClasses method. It takes inspiration from the changes I had made in #4573.

A simple utility method functioning the same as SkriptAddon#loadClasses exists too:

public class MyAddon extends JavaPlugin {

  public void onEnable() {
    // getFile() returns a File object representing the plugin's jar file
    ClassLoader.loadClasses(MyAddon.class, getFile(), "my.addon.skript", "expressions", "effects");
  }

}

However, a builder exists for creating custom ClassLoaders with different behavior:

public class MyAddon extends JavaPlugin {

  public void onEnable() {
    // getFile() returns a File object representing the plugin's jar file
    ClassLoader.builder()
        .basePackage("my.addon.skript")
        .addSubPackages("expressions", "effects")
        .initialize(true) // sets whether classes should be initialized
        .deep(true) // sets whether subpackages of packages should be searched
        .forEachClass(clazz -> /*do something*/) // sets a consumer that is run on each initialized class.
        .build()
        .loadClasses(MyAddon.class, getFile());
  }

}

It is possible to load classes without passing getFile() (or a jar), though it is not preferred due to reliability reasons. It worked fine during my testing, but the documentation for the utility used (ClassPath from Guava) notes some potential issues.

ViewProvider Interface

An interface has been added to represent objects that can have unmodifiable views of themselves created. An unmodifiable view allows read access into an object but prevents making any changes to its values. For example, an unmodifiable SkriptAddon would allow you to obtain properties such as its name, but it would prevent changes such as storing a new registry or loading a new module.


Target Minecraft Versions: any
Requirements: none
Related Issues:

@APickledWalrus APickledWalrus marked this pull request as ready for review June 30, 2024 20:51
@sovdeeth sovdeeth removed the 2.9 Targeting a 2.9.X version release label Jul 1, 2024
src/main/java/ch/njol/skript/Skript.java Outdated Show resolved Hide resolved
public static Collection<SkriptAddon> getAddons() {
return Collections.unmodifiableCollection(addons.values());
Set<SkriptAddon> addons = new HashSet<>(Skript.addons);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could use a name->addon map here rather than a set, instead of whatever you're doing with streams below

src/main/java/ch/njol/skript/Skript.java Outdated Show resolved Hide resolved
src/main/java/ch/njol/skript/Skript.java Outdated Show resolved Hide resolved
src/main/java/ch/njol/skript/Skript.java Outdated Show resolved Hide resolved
src/main/java/ch/njol/skript/Skript.java Outdated Show resolved Hide resolved
src/main/java/ch/njol/skript/SkriptAddon.java Outdated Show resolved Hide resolved
public static void register(SyntaxRegistry registry, Class<? extends Condition> condition, PropertyType propertyType, String property, String type) {
if (type.contains("%"))
throw new SkriptAPIException("The type argument must not contain any '%'s");
SyntaxInfo.Builder<?, ? extends Condition> builder = SyntaxInfo.builder(condition).priority(DEFAULT_PRIORITY);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think there should be another method for letting the user specify their own priority for the element?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now, the methods return the SyntaxInfo that was registered. It would be possible to construct a builder from that SyntaxInfo, change the priority, and then register it again. Of course, having to unregister and then register again is not ideal, but needing to change the priority is not exactly common.

An alternative approach could be to revamp these methods to just return a SyntaxInfo (and then you have to register it yourself), but I'm not sure if that's ideal (and it might conflict with the existing method). What do you think?

src/main/java/org/skriptlang/skript/SkriptImpl.java Outdated Show resolved Hide resolved
@APickledWalrus APickledWalrus added the breaking changes Pull or feature requests that contain breaking changes (API, syntax, etc.) label Sep 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking changes Pull or feature requests that contain breaking changes (API, syntax, etc.) enhancement Feature request, an issue about something that could be improved, or a PR improving something. feature Pull request adding a new feature. needs testing Needs testing to determine current status or issue validity, or for WIP feature pulls.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants