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

RFC 0057: Configuration metadata API #57

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
230 changes: 230 additions & 0 deletions specification/0057-configuration-metadata-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# Configuration Metadata API

## Summary

Define a common metadata API for all mods that have configuration that can be modified in-game. These metadata should be sufficient to automatically
generate user-friendly configuration screens for mods. In order to achieve this, this RFC defines metadata necessary to display configuration to the
user and to convert user input into configuration values.


## Motivation

Mod configuration is a common problem that mods must solve. Defining an API within QSL that Quilt mods use to define configuration metadata
unifies the configuration UI model among mods and allows mods to avoid reinventing the wheel. Exposing a common API to query config metadata
allows config screens to be generated rather than relying on mods to manually construct screens using general-purpose GUI libraries.


## Explanation

### Definitions
- **Setting**: A single value and its associated metadata.
- **Category**: A logical grouping of Settings. A Setting may be a member of multiple Categories.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't love the term "category" here. I would ordinarily assume that each setting belongs to only one category, similar to how game rules have a single category or biomes have a single category. I'm not sure what a better word would be, but wanted to open the discussion.

- **Validation Group**: A logical grouping of Settings used for validation. A Setting may be a member of only one Validation Group.
- **Value Converter**: An object that handles conversion between the value and UI state.
- **Formatter**: A function that maps a value to Minecraft `Text` suitable for display.
- **Setting Validator**: A function that is invoked to accept or reject the value of a single Setting.
- **Group Validator**: A function that is invoked to accept or reject the set of values in a single Validation Group.
- **Environment Policy**: A value that defines how a Setting interacts with the client and/or server.

A new QSL module, `quilt_config_metadata`, contains the implementation of this API. The API is built on top of the Loader config API, specialized
with metadata for Minecraft mods.

### Settings API Detail

A Setting has the following metadata.
- Widget Type
- Value Calculator
- Formatter
- Label
- Tooltip
- Categories
- Group Validator
- Environment Policy

#### Widget Type

```java
public enum WidgetType {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think supporting custom widget types is important. If somebody wants to implement a color wheel widget for their config option, they should be able to do so. Even if their widget doesn't appear alongside other widgets, a solution that opens another screen to edit the option would seem reasonable to me. I think restricting widgets to a small set of types will lead to some less-than-optimal end user experiences.

TOGGLE,
CYCLE,
SELECT,
FREE_TEXT,
SLIDER,
NUMERIC,
UNORDERED_COLLECTION
Copy link
Contributor

Choose a reason for hiding this comment

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

All config values supported by loaders config API are inherently ordered. Since the behavior of an unordered collection is undefined and I struggle to think of when any of the possible behaviors might be useful, I don't think it really needs to be included.

ORDERED_COLLECTION;
}

MetadataType<WidgetType, ?> WIDGET_TYPE;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's important that no matter what our system ends up being, we provide reasonable default behaviors for any type. This seems like it would be a good place to note that.

```

A Setting's Widget Type describes the type of widget that best represents the Setting.
- `TOGGLE` widgets are binary, typically displayed as check boxes or buttons.
- `CYCLE` widgets are small ordered lists of options, typically displayed as cycle buttons or radio buttons.
- `SELECT` widgets are large unordered lists of options, typically displayed as a drop-down or searchable menu.
- `FREE_TEXT` widgets are strings of text, typically displayed as a text box.
- `SLIDER` widgets are imprecise numeric values, typically displayed as a slider.
- `NUMERIC` widgets are precise numeric values, typically displayed as a text box.
- `UNORDERED_COLLECTION` widgets are made up of a collection of values with no defined ordering, all with the same Widget Type.
- `ORDERED_COLLECTION` widgets are made up of a sequence of values with a defined ordering, all with the same Widget Type.

The most appropriate Widget Type for a given Setting depends on the Setting's underlying data type and on any constraints a Setting's
value must obey.

If a Setting's Widget Type is `UNORDERED_COLLECTION` or `ORDERED_COLLECTION`, additional data is stored `WIDGET_ELEMENT_TYPE`.

```java
MetadataType<WidgetType, ?> WIDGET_ELEMENT_TYPE;
```
Comment on lines +74 to +78
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this is necessary at all. We should have one standard way of displaying collections, and collections should simply define the widget type used by all of their children.


#### Value Converter
Copy link
Contributor

Choose a reason for hiding this comment

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

I also don't think this is necessary. WIDGET_TYPE should just be a MetadataType<Function<TrackedValue<?>, Widget>>, with different kinds of widgets having either constructors or helper methods for different parameter types. ToggleWidget for instance could have the following method:

public static Widget create(TrackedValue<?> value) {
  Object defaultValue = value.value();

  if (defaultValue instanceof Boolean) {
    return new ToggleWidget(value.value());
  } else if (defaultValue instanceof Enum e && e.values().length == 2) {
    return new EnumToggleWidget(value.value());
  } else if (...) {
  
  } else {
    throw new WidgetCreationException(...); // Might be better to have some sort of result class instead of throwing errors, but ¯\_(ツ)_/¯ 
  }
}

The value would then simply call

builder.metadata(WIDGET_TYPE, builder -> builder.factory(ToggleWidget::create))


```java
public interface ToggleValueConverter<T> {
Optional<T> widgetStateToValue(boolean widgetState, ValidationDiagnostics diagnostics);
boolean valueToWidgetState(T value);
}

MetadataType<ToggleValueConverter<?>, ?> TOGGLE_VALUE_CONVERTER;

public interface CycleValueConverter<T> {
Optional<T> widgetStateToValue(int widgetState, ValidationDiagnostics diagnostics);
int valueToWidgetState(T value);
int valueCount();
int hiddenValueCount();
}

MetadataType<CycleValueConverter<?>, ?> CYCLE_VALUE_CONVERTER;

// etc...
```

A Value Converter handles conversion between the stored value in a Setting and the canonical widget state. It also defines properties necessary for the widget to
properly handle user input. For example, toggle widgets' state is a boolean value (checked or unchecked), while the stored values in the Setting may be any two
values of any type.

### Formatter

```java
@FunctionalInterface
public interface Formatter<T> {
Text format(T value);
}

MetadataType<Formatter, ?> FORMATTER;
```

A Formatter is a function that converts a Setting's stored value into Text for displaying to players. This text is not meant to be mapped back to
the value that produced it.

#### Label

```java
MetadataType<Text, ?> LABEL;
```

A Setting's Label is the simple display name of a Setting.

#### Tooltip

```java
@FunctionalInterface
public interface TooltipSupplier<T> {
Text getTooltip(T value);
}

MetadataType<TooltipSupplier<?>, ?> TOOLTIP;
```

A Setting's tooltip is a brief description of the current state of the Setting, dependent on the Setting's value.

#### Categories

```java
MetadataType<Collection<Identifier>, ?> CATEGORIES;
```

A Category is a logical grouping of Settings; for example, video, audio, accessibility, or gameplay. A Setting may be a member of any number
of Categories. Categories are intended to be used to partition Settings into broad groupings for display purposes.

This RFC defines a standard Category `c:accessibility`, to be used for Settings that are used to make a mod more accessible.

#### Setting Validator

Settings will use the existing `Constaint<T>` interface in Quilt Loader's configuration API to define Setting Validators.

#### Group Validator

```java
@FunctionalInterface
public interface GroupValidator {
void validate(ValidationDiagnostics diagnositics);
boolean equals(Object o);
}

MetadataType<GroupValidator, ?> GROUP_VALIDATOR;
```

A Group Validator is used to validate properties that pretain to the relationship among multiple Settings. For example, two Settings that
represent the minimum and maximum value of a range may have a Group Validator that ensures the maximum is always greater than or equal to
the minimum. It is intended that Group Validators that perform the same validation compare equal so that consumers of this API do not
run validation logic multiple times.

#### Environment Policy

```java
public enum EnvironmentPolicy {
Copy link
Contributor

Choose a reason for hiding this comment

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

CLIENT_ONLY and SERVER_ONLY are functionally identical. I would therefore rename this class to SyncPolicy or something along those lines and condense those two options to NONE, with SERVER_SYNCED becoming S2C and CLIENT_SYNCED becoming C2S. Additionally, I might add a P2P sync type that shares user configs with other clients in a way that allows them to be accessed on a per-player basis. This has a lot of valuable implications for user-set display information (nicknames come to mind).

CLIENT_ONLY,
SERVER_ONLY,
CLIENT_SYNCED,
SERVER_SYNCED
}

MetadataType<EnvironmentPolicy, ?> ENVIRONMENT_POLICY;
```

- `CLIENT_ONLY` Settings are specific to a player and are always editable.
- `CLIENT_SYNCED` Settings are specific to a player and may be overridden by the server.
- If a `CLIENT_SYNCED` Setting is a member of the Category `c:accessibility`, then the API shall issue a warning.
- `SERVER_ONLY` Settings are specific to a level and are not sent to the client from a dedicated server.
- `SERVER_SYNCED` Settings are specific to a level and are sent to the client from a dedicated server

## Drawbacks

This API depends on (and is therefore limited by) Quilt Loader's configuration API.

This API also uses Minecraft types. Therefore, mod configuration that uses this API cannot be constructed or queried before game initialization (for example,
in mixin plugins or CHASM transformers). However, this type of configuration can't be changed during gameplay, so displaying it to users has limited
benefits.

## Rationale and Alternatives

This RFC describes a common API between mods that define configuration and mods that consume configuration.
This approach gives the most control to mods that wish to consume multiple mods' configuration, e.g. to construct a config screen,
while avoiding including too many "batteries" that not all mods may use.

### Alternatives

Define an API for constructing config UI elements that is completely separate from the underlying config. This would give mods more freedom to choose
specific UI representations.

Do nothing. In the absence of a config API, mods will likely tend toward using fully featured GUI libraries for displaying configuration.

## Prior Art

- [ModConfig](https://github.com/kvverti/mod-config), a global config screen that extracts configuration metadata from mods that use Cloth UI.
- [Fiber](https://github.com/FabLabsMC/fiber), a library that defines a rich config tree.


## Unresolved Questions

- How precisely should Value Converters define widget state?
- Should Setting Validators be separate from Loader's `Constraint` type?

### Implementation
- Which other common formatter/validator/widget type instances should this API provide? E.g. enum, registry entry, `Identifier`, etc.
- How strict should the API be on the well-formedness of metadata?

### Out of Scope
- Key binding configuration is considered out of scope for this API because Minecraft handle key bindings differently from other game options.
- Creating a standard consumer of this API, e.g. a config screen generator may be specified in another RFC.