Skip to content

Conversation

sovdeeth
Copy link
Member

@sovdeeth sovdeeth commented Aug 29, 2025

Problem

One of the most irritating problems that has plagued the Skript ecosystem for ages is the inability for two implementations of the same syntax pattern to exist at the same time. In Skript itself, it forces long, complicated Expr classes that handle 7 different types, and in addons it causes syntax conflicts or awkward naming schemes to avoid any possible conflict with other addons or Skript.

AnyX was somewhat recently introduced to assist in this case, by allowing any type to be converted to a uniform interface class that has a common method to use. This works well, but has little to no parse-time information capabilities. There's no way to check if a type that implements AnyContains can contain strings, items, or entities until runtime, when it's basically too late.

It's also difficult to handle changers and any non-get() behavior with AnyX, due to the same reasons of limited parse time information. This can make determining allowChange and possible return types near impossible to do accurately.

Solution

This PR introduces the concept of Type Properties (name wip, property is already rather prevalent in code). These are very free-form properties that can be registered with Skript using a name and a handler interface. Any ClassInfo can use the .property() method to declare that they implement this property and how they implement it.

This means that Skript, SkBee, and Disky may all have their own implementations of size of x for their own types without conflicting. You could do lengths of ("xyz" and {skript-particle-rectangle}) with no issue. toggle x could work for any type from any addon that wants to use that property.

Implementing a Property

This system revolves around the Property class, which is, by itself, just a string and an addon reference. Any addon can register a property, though no two addons can register the same property name. Properties require a "handler" class to be supplied at creation to declare the interface all implementations must follow. This can be as simple as:

interface LengthHandler<T> extends PropertyHandler<T> {
    double length(T object);
}

Property.LENGTH = Property.of("length", 
	"The length of something, e.g. the number of characters in a string or the side length of a rectangle.",
	Skript.getInstance(), 
	LengthHandler.class);
Skript.getPropertyRegistry().register(LENGTH);

or something much more complex, with changers or complicated behaviors. There are no restrictions, except for the handler must extend PropertyHandler<Type>, which is an empty interface that just says "This is a property handler".

When a ClassInfo wants to implement a property, they use the ClassInfo#property(Property, Handler) method. This adds the provided handler implementation to the class' properties for later use. Each property may only be implemented once on a ClassInfo.

new ClassInfo<Rectangle>(...)
	.property(Property.LENGTH,
		"The length (in the local x dimension) of a rectangle.",
		new LengthHandler<Rectangle>() {
        		double length(Rectangle rect) { return rect.length(); }
    		});

Alternatively, something as simple as length of could use the existing ExpressionPropertyHandler handler, which provides basic methods for expression-like properties, such as conversion, acceptChange/change for changer support, and custom return types:

Property.LENGTH = Property.of("length", 
	"The length of something, e.g. the number of characters in a string or the side length of a rectangle.",
	Skript.getInstance(), 
	ExpressionPropertyHandler.class);
Skript.getPropertyRegistry().register(LENGTH);

...

new ClassInfo<Rectangle>(...)
	.property(Property.LENGTH, 
		"The length (in the local x dimension) of a rectangle.",
		ExpressionPropertyHandler.of(Rectangle::length, Double.class));

Or for changer support:

new ClassInfo<Rectangle>(...)
	.property(Property.LENGTH, 
		"The length (in the local x dimension) of a rectangle.",
		new ExpressionPropertyHandler<Rectangle, Double>() {
			@Override
			public Double convert(Rectangle rect) {
				return rect.length();
			}
	
			@Override
			public Class<?> @Nullable [] acceptChange(ChangeMode mode) {
				if (mode == ChangeMode.SET)
					return new Class[] {Double.class};
				return null;
			}

			@Override
			public void change(Rectangle rect, Object @Nullable [] delta, ChangeMode mode) {
				assert mode == ChangeMode.SET;
				assert delta != null;
				rect.setLength((Double) delta[0]);
			}

			@Override
			public @NotNull Class<Double> returnType() {
				return Double.class;
			}
		});

A similar handler exists for simple conditions, the ConditionPropertyHandler<Type>.

Creating a syntax that uses a property's handler

Up till now, this has been very simple for the user. The more complex part is creating a syntax that takes advantage of these properties to actually do something. For simple property expressions (properties that lead to patterns similar to the actual SPE syntaxes, like name of x), I provide a relative simple base class to extend:

@RelatedProperty("name")
public class PropExprName extends PropertyBaseExpression<ExpressionPropertyHandler<?,?>> {

	static {
		// A return type of Object.class and ExpressionType of PROPERTY are automatically used.
		register(PropExprName.class , "name[s]", "objects");
	}

	@Override
	public Property<ExpressionPropertyHandler<?, ?>> getProperty() {
		return Property.NAME;
	}

	// toString is automatically generated based on the name() field of the Property, but can be
	// changed by overriding it or getPropertyName()

}

This does require the user to either use the ExpressionPropertyHandler hander directly, or have their handler implement it:

	public interface ExpressionPropertyHandler<Type, ReturnType> extends PropertyHandler<Type> {
		ReturnType convert(Type propertyHolder);

		default Class<?> @Nullable [] acceptChange(ChangeMode mode) {
			return null;
		}

		default void change(Type named, Object @Nullable [] delta, ChangeMode mode) {
			throw new UnsupportedOperationException("Changing the name is not supported for this property.");
		}

		@NotNull Class<ReturnType> returnType();

		default Class<?> @NotNull [] possibleReturnTypes() {
			return new Class[]{ returnType() };
		}

	}

In return, changers and property checks are handled for you. This is what I believe most properties will use.

Some handlers may require state, for which newInstance() and init(Expression, ParserInstance) should be overridden:

private static class ScriptNameHandler implements ExpressionPropertyHandler<Script, String> {
	//<editor-fold desc="name property handler" defaultstate="collapsed">

	private boolean useResolvedName;

	@Override
	public PropertyHandler<Script> newInstance() {
		return new ScriptNameHandler();
	}

	@Override
	public boolean init(Expression<?> parentExpression, ParserInstance parser) {
		useResolvedName = parser.hasExperiment(Feature.SCRIPT_REFLECTION);
		return true;
	}

	@Override
	public String convert(final Script script) {
		if (useResolvedName)
			return script.getConfig().name();
		return script.nameAndPath();
	}

	@Override
	public @NotNull Class<String> returnType() {
		return String.class;
	}
	//</editor-fold>
}

Similarly, I provide an easily extensible base class for simple conditions:

public class PropCondIsEmpty extends PropertyBaseCondition<ConditionPropertyHandler<?>> {

	static {
		register(PropCondIsEmpty.class, "empty", "objects");
	}

	@Override
	public @NotNull Property<ConditionPropertyHandler<?>> getProperty() {
		return Property.IS_EMPTY;
	}
}

Note this only works for simple conditions that do not require secondary input.

Custom property syntax implementations

However, this API is very powerful and allows way more outside of that. The power, though, comes at the cost of complexity. Users who want unique properties will have to handle property management, type checks, and evaluation themselves. It's not that bad but it is certainly complex.

To start with, users making custom syntax should consider inheriting PropertyBaseSyntax for standardization reasons. It provides a getProperty and getPropertyName methods which can be used to determine property and for toStrings regardless of the details of the specific syntax.

The main things to handle are:

Determining the relevant properties:

Nearly every property will contain something like this in their init():

this.haystack = PropertyBaseSyntax.asProperty(Property.CONTAINS, expressions[0]);
if (haystack == null) {
	Skript.error(getBadTypesErrorMessage(expressions[0]));
	return false;
}
// determine if the expression truly has a name property

properties = PropertyBaseSyntax.getPossiblePropertyInfos(Property.CONTAINS, haystack);
if (properties.isEmpty()) {
	Skript.error(getBadTypesErrorMessage(haystack));
	return false; // no name property found
}

These PropertyBaseSyntax methods are meant to assist in, respectively, converting an input expression into one that returns things with the given property and getting all the possible handlers that one could need given the expression's return types. getPossiblePropertyInfos in specific is important, since it returns the most useful part of the api, the PropertyMap.

A PropertyMap is a HashMap<Class, PropertyInfo> with 2 special methods: getHandler(Class) and get(Class). getHandler is the main method you should be using, which returns an appropriate Handler implementation for the given class. get(Class) returns the PropertyInfo for the given class or, if not found, determines the closest class to the given class that has a property in the map already. This lookup is cached, so after the call, a new entry should exist in the map, either to null if no property exists, or to a PropertyInfo. This allows, for example, OfflinePlayer to have a property implemented and for Player to also use that property.

Using the properties

This will vary depending on the property you're implementing, but nearly all will be relying on `PropertyMap#getHandler` extensively. The general flow should be:
  • evaluate expression
  • loop contents
  • get class of content
  • get handler for that class
  • use handler as required

For example, here's my implementation of CondContains using properties:

Object[] haystacks = haystack.getAll(event);
boolean haystackAnd = haystack.getAnd();
Object[] needles = this.needles.getAll(event);
boolean needlesAnd = this.needles.getAnd();
if (haystacks.length == 0) {
	return isNegated();
}

// We should compare the contents of the haystacks to the needles
if (explicitSingle) {
	// use properties
	return SimpleExpression.check(haystacks, (haystack) -> {
		// for each haystack, determine property
		//noinspection unchecked
		var handler = (ContainsHandler<Object, Object>) properties.getHandler(haystack.getClass());
		if (handler == null) {
			return false;
		}
		// if found, use it to check against needles
		return SimpleExpression.check(needles, (needle) ->
				handler.canContain(needle.getClass())
				&& handler.contains(haystack, needle),
			false, needlesAnd);
	}, isNegated(), haystackAnd);
}

Implementation details can vary wildly between properties, but PropertyBaseExpression is a good example of some of the more complex details a property may encounter. PropCondContains is a good example of a simpler custom property implementation.

Documentation

Any syntax element using properties should have the @RelatedProperty("property name") annotation added for documentation purposes. Each property requires a short description upon creation, describing what it is meant to represent. Likewise, each implementation on a class info also requires a short description, which should be used to describe exactly what on the type it represents, as well as any relevant info like available changers.

Documentation is supported via the JSON docs. An array containing all properties with their related types and syntaxes is provided, and any syntax annotated as @RelatedProperty will have a list of all the classes that implement that property, as well as the info about the property itself. ClassInfos will have their properties listed out with the property id, name, and a class-specific description of how it implements the property, plus a list of related syntax ids.

Testing Completed

Only manual testing has been done so far.

Supporting Information


Completes: none
Related: #7644 #7919 #7675 #7416 #8136

@sovdeeth sovdeeth added the feature Pull request adding a new feature. label Aug 29, 2025
@sovdeeth sovdeeth moved this to In Progress in 2.13 Releases Aug 29, 2025
@sovdeeth
Copy link
Member Author

I also acknowledge how bloated this may make the already bloated __Classes classes, so I intend a future PR to go through and separate all the classinfos each into their own classes: PlayerType extends ClassInfo<Player>, Classes.register(new PlayerType()).

sovdeeth and others added 8 commits August 28, 2025 20:52
- allows stateful property handlers
- removes use of AnyNamed
- removes NameHandler in favor of just using ExpressionPropertyHandler
- Avoid exposing silly cast to users when possible in PropertyBaseExpression
- fix issue with handling DELETE and RESET changers in PropertyBaseExpression
- add support for default toString impl in PropertyBaseExpression
@R4faar
Copy link

R4faar commented Sep 1, 2025

cool request

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Pull request adding a new feature.
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

3 participants