Validation

The Fzzy Config validation system is applied to every setting either implicitly or explicitly by the creator.

Settings are tightly managed and fail-soft; no catastrophic failure if the config changes and the old file isn't valid, or if a user modifies the .toml directly and makes a mistake.

Every read, write, and update is checked for errors, and problems are reported and either automatically corrected or a fallback is used.

Validation Options

Below is a list of types that Fzzy Config has validation tools for.


Validation Manipulation

ValidatedFields can be manipulated to enhance their effectiveness in more niche situations

Validation Concepts

What is it?

"Validation" is a bit of a misnomer, as the toolset does much more than that. It's a label stemming from the base class ValidatedField. To borrow from the KDoc for Entry, validation:

  • serializes contents
  • deserializes input
  • validates updates
  • corrects errors
  • provides widgets
  • applies inputs
  • supplies outputs
  • creates instances
  • manages flags
  • accepts listeners

Providing Validation

Every setting that appears in a Config GUI is backed by validation. Even when you don't explicitly provide it, Fzzy Config will wrap supported types with validation in the background.

For advanced control of your settings, like Minecraft GameOption, explicitly define the validation for your setting using a ValidatedField class. This grants you the ability to:

  • Provide input restrictions
  • Suggest inputs to users
  • Define the widget used in-GUI
  • Attach listeners, conditions, and feature flags to the setting

Mapping

Validation can be mapped to another convertible type, like Mojang's Codecs. The mapped-from validation type will be used for serialization, GUI widgets, suggestions and corrections, and so on, but in-code you can interact directly with the mapped-to type.

Example: Character

Fzzy Config doesn't have built in validation for Characters. With mapping, we can easily build our own.

//Starting with a ValidatedInt, which characters map easily to, we define validation that bounds the int to the valid character range
//Then using map, we map the int to and from a Char, just like Codec mapping.
//This provides a ValidatedField<Char>, so calling get() will provide a character!
ValidatedField<Character> validatedCharacter = new ValidatedInt(0, Character.MAX_VALUE, Character.MIN_VALUE).map(
i -> (char)i,
c -> Character.getNumericValue(c)
);

Conditional Settings

Validation can be wrapped with conditions that define whether the setting is "active" or not. You can check:

  • Whether a certain mod is loaded
  • The status of another config setting; ValidatedBoolean or ValidatedTriState can be used directly
  • Etc. Etc.
// conditions should supply live values. Validated fields are a convenient mechanism to do that. A plain boolean won't update in-GUI until changes are applied.
ValidatedBoolean validatedBooleanGate = new ValidatedBoolean();
//create a conditional validation with toCondition. Note that the type is no longer ValidatedInt directly.
ValidatedCondition<Int> validatedConditionInt = (new ValidatedInt(5, 100, 0)).toCondition(validatedBooleanGate, Text.literal("Gate must be true"), () -> 0);

You can add more features to your condition once you've converted it. Beyond withCondition mentioned above, you can add a title to show when the conditions aren't met.

//We can add more conditions and set up a "fail title" once we have the initial condition
ValidatedCondition<Int> validatedConditionInt = (new ValidatedInt(5, 100, 0))
.toCondition(validatedBooleanGate, Text.literal("Gate must be true"), () -> 0)
.withCondition(Text.literal("This also must be true"), () -> mySecondCheck())
.withFailTitle(Text.literal("Condition not met"), Text.literal("Conditions not net")); //you can add both a singular and plural of your fail message, useful if you have more than one condition

Listeners

You can listen to changes made to any ValidatedField by supplying a consumer of that field.

Listening works on both server and client and will trigger on config load. The listeners apply any time the stored value is set (with a couple exceptions, see example below).

//boolean with an attached listener that setups some system when the setting is flipped to true.
ValidatedBoolean listenerBoolean = (new ValidatedBoolean(false)).withListener(lb -> {
if (lb.get())
setupSomething
});
//the below example has two booleans that toggle each other on/off, providing a sort of "cross-linked" setting where only one can be active at a time.
//it's important to note the usage of validateAndSetFlagged here, which lets the partner setting be updated "quietly", otherwise you will deadlock and stack overflow
//QUIET = no listeners applied
//UPDATE = applies updates to the config manager system, syncing/saving/etc any changes made by the listeners
ValidatedBoolean aBool = (new ValidatedBoolean(false)).withListener(a -> {
if (a.get())
bBool.validateAndSetFlagged(false, EntryFlag.Flag.QUIET, EntryFlag.Flag.UPDATE);
});
ValidatedBoolean bBool = (new ValidatedBoolean(false)).withListener(b -> {
if (b.get())
aBool.validateAndSetFlagged(false, EntryFlag.Flag.QUIET, EntryFlag.Flag.UPDATE);
});

Codecs

Any ValidatedField can be used to generate a Codec of the validation's type. The resulting Codec will be backed by the validation present in the field.

//this codec will parse integers, clamping the allowed value to 0 to 100.
Codec<Int> intCodec = (new ValidatedInt(5, 100, 0)).codec();

Attachments

Validation can be provided with custom behavior using various attachment mechanisms.

These providers attach "live" providers to the setting that are evaluated whenever needed. This means they are always provided with the most up-to-date versions of their inputs, and the provider should return a new value every time it is applicable to.

translationProvider

Attaches a custom translation mechanism for the setting name (the label on the left side of the config GUI)

descriptionProvider

Attaches a custom description to the setting (the tooltip shown when hovering over the setting row in the GUI)

attachProvider

Attaches an arbitrary Provider to the setting, with various uses and effects in-game

As of 0.7.6, the only usable provider is Translatable.Provider.WIDGET_TITLE. Use this to provide a custom widget title for the setting (not the name on the left, the actual label directly written on the button(s)).

ValidatedBoolean myField1 = ValidatedField
//attach a custom translation provider to the setting.
//this example just echoes out the current value of the setting without any decoration
.translationProvider(new ValidatedBoolean(), (value, translationKey) -> Text.literal(value.toString()));
ValidatedBoolean myField2 = ValidatedField
//attach a custom description (tooltip) provider to the setting.
//this example attaches a simple literal string. Probably don't do this in practice (just use the lang file for this)
.descriptionProvider(new ValidatedBoolean(), (value, translationKey) -> Text.literal("This setting controls the blahblah blah."));
ValidatedBoolean myField3 = ValidatedField
//attach a widget title to the setting. This will appear as the label on the setting button in the GUI
//this example replaces "True" and "False", the boolean setting widget labels, with "Enabled" and "Disabled"
.attachProvider(new ValidatedBoolean(), Provider.getWIDGET_TITLE(), (bl, defaultText) -> {
return bl ? Text.translatable("my.key.enabled") : Text.translatable("my.key.disabled"); //translate to "Enabled" and "Disabled"
});