Skip to content

New Registration API#6246

Merged
sovdeeth merged 157 commits intodev/featurefrom
feature/registration-rework
Dec 29, 2024
Merged

New Registration API#6246
sovdeeth merged 157 commits intodev/featurefrom
feature/registration-rework

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

NOTE: The class parameter represents the main class of the application registering the addon.

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 have two loading phases: init and load. init is intended for registering components that might be used by other modules (e.g. class infos). load is intended for registering components that are more specific to the module, such as syntax.
When loading multiple modules together, their init phases will first all be executed, and then their load phases will all be executed.

Loading a module is rather simple:

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

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 two additional properties for the EntryValidator and NodeType 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, Double.class) // Syntax class, Return Type(s) superclass
	.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 added the breaking changes Pull or feature requests that contain breaking changes (API, syntax, etc.) label Sep 29, 2024
@sovdeeth sovdeeth added the 2.10 label Dec 17, 2024
@JakeGBLP
Copy link
Contributor

JakeGBLP commented Dec 20, 2024

Should there be the possibility of modifying already existing elements? Some expressions like the "name" expression are very generic and being able to add to them as an addon developer would be awesome, some use cases would be skbee merging its merge component expression with skript's join string expression, adding xyz coordinates of non standard objects to the coordinate expression; this could open up a world of fixes for conflicts and it would be very cool to see it implemented, and if it is i will surely use it.

This would in a sense act like "patches".

@APickledWalrus
Copy link
Member Author

Should there be the possibility of modifying already existing elements? Some expressions like the "name" expression are very generic and being able to add to them as an addon developer would be awesome, some use cases would be skbee merging its merge component expression with skript's join string expression, adding xyz coordinates of non standard objects to the coordinate expression; this could open up a world of fixes for conflicts and it would be very cool to see it implemented, and if it is i will surely use it.

This would in a sense act like "patches".

The ability to unregister, modify, and then re-register infos exists. What you want is covered in #6728

Copy link
Member

@Efnilite Efnilite left a comment

Choose a reason for hiding this comment

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

merge this !!!

@sovdeeth sovdeeth merged commit a0f3a5b into dev/feature Dec 29, 2024
7 checks passed
@APickledWalrus APickledWalrus deleted the feature/registration-rework branch December 30, 2024 16:16
@APickledWalrus APickledWalrus mentioned this pull request Jan 1, 2025
11 tasks
erenkarakal pushed a commit to erenkarakal/Skript that referenced this pull request Nov 26, 2025
* First api design

* Rework and implement for expression

* Implement for all syntax elements

* Rename to Skript and other small refactors

* Registration closing

* Child key

* Cleanup

* Moving

* hashcode

* Oops

* TODO

* Move event pattern transformation to the correct place

* Moves key implementation away

* Apply suggestions from code review

Co-authored-by: Patrick Miller <apickledwalrus@gmail.com>

* Apply suggestions from code review

Co-authored-by: Patrick Miller <apickledwalrus@gmail.com>

* Requested changes

* Package rename

* Refactoring

* Deprecation

* Builders

* Use builders

* Certified license header moment

* Not too proud of this one

* Fix tests

* Refactoring

* Replace with shorter version

* Fix order

* Refactoring

* Cherry-pick docs-tool into api-rework

* Fix documentation

* Double tabbing

* Fix registrations

* Attempt 2

* Apply suggestions from code review

Co-authored-by: Patrick Miller <apickledwalrus@gmail.com>
Co-authored-by: LimeGlass <16087552+TheLimeGlass@users.noreply.github.com>

* Allows updating of Skript instance.

* Fix error

* Deprecation

* Simpler keys

* SimpleSkriptRegistry

* Priority stuff

* Allow for multiple Skript instances

* Suppliers for syntax elements

* Fixes for event parsing changes

* Improve BukkitOrigin; Remove LegacyEventImpl

* Origin improvements

* Rework SyntaxInfo/Builder system

* Improve Registry and Key system

* Improve SyntaxRegister docs

* Improve SyntaxInfo implementations

* Add missing JavaDocs

* Require builders for creating SyntaxInfos

* Initial implementation of new Addon API

* Address UnderscoreTud's review

* Address Moderocky's review

* Improve compatibility implementation

* Remove Skript State system

* Minor Tweaks

* Localizer system draft

* Localizer Fixes

* Add support for property registration methods

* Add support for unregistering syntax infos

* First pass at SkriptAddon rework

This will not build. This is a design pivot to have SkriptAddon instances returned by registering an addon by name rather than implementing the interface.

* Implement child registries for addons

* Fix language and addon compatibility

* Fix building and tests

* Block registration of interfaces/abstract classes

They are permitted if a creation supplier is provided

* Origin reworks

Added a default SkriptAddon origin and pivoted internally to avoid using an "unknown" origin

* First pass at priority rework

* Add priority support to info builders

* Revert Structure priority changes

* Pivot to a relational priority system

* Replace ExpressionType with priority system

* Revert some unnecessary changes

* Add missing EventValueExpression registration api

* Rework Expression priorities to be for all SyntaxInfos

* Change Skript.createInstance to also return modifiable addon

* Add SyntaxPriority

An implementation of Priority that enables positioning itself around specific SyntaxElement classes.

* Move module loading out of registration

* Add missing PriorityImpl checks

* SyntaxPriority improvements and clarifications

* Limitations on SyntaxPriority

* Alternative module loading method

* Remove unnecessary SyntaxRegister interface

* Fix simple structure support

* Implement Registry interface

* Remove SyntaxPriority Implementation

It is too unstable in its current state. It may return in a future PR.

* Disconnect Event Info from Structure Info

For registration purposes, SkriptEvents are no longer tied to Structures (see StructEvent)

* Allow structures to be simple or section

* Improve SyntaxInfo parameter checks

* Add class loading utilities

* Replace ClassLoader Java 11 methods

* SkriptAddon: remove unnecessary annotations

* ClassLoader: add default loadClasses method

* Use builder method for Expression return type

* Remove license headers

* Prevent SkriptImpl from exposing its addons

* Use the same registry across all addons

Addon-specific registries may return but will need reworked

* Rename SkriptAddon#registry to SkriptAddon#syntaxRegistry

* Add ViewProvider interface

* Improve annotation usage and placement

* Remove deprecation annotations

We plan to merge this as experimental/preview API

* Add listening behavior to Event SyntaxInfo

Forgot about this...

* Add missing experimental annotations

* Rename BukkitInfos to BukkitSyntaxInfos

* Add unmodifiableView for Skript

With this change, I have also removed the weird NonNullPair from creating a default Skript.

* Add registry storage to SkriptAddon

* Add source requirement to SkriptAddon

* Remove source from Localizer

This can be obtained from the addon instance

* Fix old addon registration

* SyntaxInfo equals/hashCode improvements

* Implementation optimizations

* Fix ConditionType support

* Return the constructed info for static registration methods

* Builder interface

* Remove unncessary package infos

* Implement Buildable interface

Allows converting SyntaxInfos back into Builders

* Improve legacy collection methods

* Use a map for modern addon tracking

* Fix SkriptEventInfo Compatibility

* Allow Skript#getAddon to return Skript's addon instance

Fixes current test failures

* Tweak Preconditions check

* ExpressionInfo Builder: require returnType at creation

* Add 'clear' methods to builders

* AddonModule: add init phase

* Fix incorrect Expression Builder uses

---------

Co-authored-by: kiip1 <25848425+kiip1@users.noreply.github.com>
Co-authored-by: LimeGlass <16087552+TheLimeGlass@users.noreply.github.com>
Co-authored-by: Moderocky <admin@moderocky.com>
Co-authored-by: sovdee <10354869+sovdeeth@users.noreply.github.com>
Co-authored-by: Efnilite <35348263+Efnilite@users.noreply.github.com>
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.

7 participants

Comments