512 lines
20 KiB
Java
512 lines
20 KiB
Java
package carpet.settings;
|
|
|
|
import carpet.CarpetSettings;
|
|
import carpet.api.settings.CarpetRule;
|
|
import carpet.api.settings.InvalidRuleValueException;
|
|
import carpet.api.settings.RuleHelper;
|
|
import carpet.api.settings.SettingsManager;
|
|
import carpet.api.settings.Validators;
|
|
import carpet.utils.Messenger;
|
|
import carpet.utils.TranslationKeys;
|
|
import carpet.utils.Translations;
|
|
import net.minecraft.commands.CommandSourceStack;
|
|
import net.minecraft.network.chat.Component;
|
|
|
|
import java.lang.reflect.Constructor;
|
|
import java.lang.reflect.Field;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.Collection;
|
|
import java.util.Iterator;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
import java.util.function.Function;
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.Stream;
|
|
|
|
import org.apache.commons.lang3.ClassUtils;
|
|
|
|
/**
|
|
* A Carpet rule parsed from a field, with its name, value, and other useful stuff.
|
|
*
|
|
* It is used for the fields with the {@link Rule} annotation
|
|
* when being parsed by {@link SettingsManager#parseSettingsClass(Class)}.
|
|
*
|
|
* @param <T> The field's (and rule's) type
|
|
* @deprecated Use the type {@link CarpetRule} instead
|
|
*/
|
|
@Deprecated(forRemoval = true) // to move to api.settings package and visibility to package private
|
|
@SuppressWarnings("removal") // Gradle needs the explicit suppression
|
|
public final class ParsedRule<T> implements CarpetRule<T>, Comparable<ParsedRule<?>> {
|
|
private static final Map<Class<?>, FromStringConverter<?>> CONVERTER_MAP = Map.ofEntries(
|
|
Map.entry(String.class, str -> str),
|
|
Map.entry(Boolean.class, str -> {
|
|
return switch (str) {
|
|
case "true" -> true;
|
|
case "false" -> false;
|
|
default -> throw new InvalidRuleValueException("Invalid boolean value");
|
|
};
|
|
}),
|
|
numericalConverter(Integer.class, Integer::parseInt),
|
|
numericalConverter(Double.class, Double::parseDouble),
|
|
numericalConverter(Long.class, Long::parseLong),
|
|
numericalConverter(Float.class, Float::parseFloat)
|
|
);
|
|
/**
|
|
* @deprecated No replacement for this, since a {@link CarpetRule} may not always use a {@link Field}.
|
|
* Use {@link #value()} to access the rule's value
|
|
*/
|
|
@Deprecated(forRemoval = true) // to private
|
|
public final Field field;
|
|
/**
|
|
* @deprecated Use {@link CarpetRule#name()} instead
|
|
*/
|
|
@Deprecated(forRemoval = true) // to private
|
|
public final String name;
|
|
/**
|
|
* @deprecated Use {@link RuleHelper#translatedDescription(CarpetRule)}, or get it from the translation system
|
|
*/
|
|
@Deprecated(forRemoval = true) // to remove
|
|
public final String description;
|
|
/**
|
|
* @deprecated Use {@link CarpetRule#extraInfo()} instead
|
|
*/
|
|
@Deprecated(forRemoval = true) // to remove
|
|
public final List<String> extraInfo;
|
|
/**
|
|
* @deprecated Use {@link CarpetRule#categories()} instead
|
|
*/
|
|
@Deprecated(forRemoval = true) // to private
|
|
public final List<String> categories;
|
|
/**
|
|
* @deprecated Use {@link CarpetRule#suggestions()} instead
|
|
*/
|
|
@Deprecated(forRemoval = true) // to private (and rename?)
|
|
public final List<String> options;
|
|
/**
|
|
* @deprecated Use {@link CarpetRule#strict()} instead
|
|
*/
|
|
@Deprecated(forRemoval = true) // to remove or fix
|
|
public boolean isStrict;
|
|
/**
|
|
* @deprecated Use {@link CarpetRule#canBeToggledClientSide()} instead
|
|
*/
|
|
@Deprecated(forRemoval = true) // to private (and maybe rename?)
|
|
public boolean isClient;
|
|
/**
|
|
* @deprecated Use {@link CarpetRule#type()} instead
|
|
*/
|
|
@Deprecated(forRemoval = true) // to private (or remove and delegate to typedfield?)
|
|
public final Class<T> type;
|
|
/**
|
|
* @deprecated Use {@link CarpetRule#defaultValue()} instead
|
|
*/
|
|
@Deprecated(forRemoval = true) // to private
|
|
public final T defaultValue;
|
|
/**
|
|
* @deprecated Use {@link CarpetRule#settingsManager()} instead.
|
|
* This field may be {@code null} if the settings manager isn't an instance of the old type
|
|
*/
|
|
@Deprecated(forRemoval = true) // to remove in favour of realSettingsManager
|
|
public final carpet.settings.SettingsManager settingsManager;
|
|
/**
|
|
* @deprecated No replacement for this. A Carpet rule may not use {@link Validator}
|
|
*/
|
|
@Deprecated(forRemoval = true) // to remove (in favour of realValidators)
|
|
public final List<Validator<T>> validators;
|
|
/**
|
|
* @deprecated Use {@link CarpetRule#defaultValue()} and pass it to {@link RuleHelper#toRuleString(Object)} instead
|
|
*/
|
|
@Deprecated(forRemoval = true) // to remove
|
|
public final String defaultAsString;
|
|
/**
|
|
* @deprecated No replacement for this, Scarpet Rules should be managed by the rule implementation
|
|
*/
|
|
@Deprecated(forRemoval = true) // to private/subclass
|
|
public final String scarpetApp;
|
|
private final FromStringConverter<T> converter;
|
|
private final SettingsManager realSettingsManager; // to rename to settingsManager
|
|
/**
|
|
* If you reference this field I'll steal your kneecaps
|
|
*/
|
|
@Deprecated(forRemoval = true)
|
|
public final List<carpet.api.settings.Validator<T>> realValidators; // to rename to validators and to package private for printRulesToLog
|
|
private final boolean isLegacy; // to remove, only used for fallbacks
|
|
|
|
@FunctionalInterface
|
|
interface FromStringConverter<T> {
|
|
T convert(String value) throws InvalidRuleValueException;
|
|
}
|
|
|
|
record RuleAnnotation(boolean isLegacy, String name, String desc, String[] extra, String[] category, String[] options, boolean strict, String appSource, Class<? extends carpet.api.settings.Validator>[] validators) {
|
|
}
|
|
|
|
/**
|
|
* If you call this method I'll steal your kneecaps
|
|
*/
|
|
@Deprecated(forRemoval = true)
|
|
public static <T> ParsedRule<T> of(Field field, SettingsManager settingsManager) {
|
|
RuleAnnotation rule;
|
|
if (settingsManager instanceof carpet.settings.SettingsManager && field.isAnnotationPresent(Rule.class)) { // Legacy path
|
|
Rule a = field.getAnnotation(Rule.class);
|
|
rule = new RuleAnnotation(true, a.name(), a.desc(), a.extra(), a.category(), a.options(), a.strict(), a.appSource(), a.validate());
|
|
} else if (field.isAnnotationPresent(carpet.api.settings.Rule.class)) {
|
|
carpet.api.settings.Rule a = field.getAnnotation(carpet.api.settings.Rule.class);
|
|
rule = new RuleAnnotation(false, null, null, null, a.categories(), a.options(), a.strict(), a.appSource(), a.validators());
|
|
} else {
|
|
// Don't allow to use old rule types in custom AND migrated settings manager
|
|
throw new IllegalArgumentException("Old rule annotation is only supported in legacy SettngsManager!");
|
|
}
|
|
return new ParsedRule<>(field, rule, settingsManager);
|
|
}
|
|
|
|
private ParsedRule(Field field, RuleAnnotation rule, SettingsManager settingsManager)
|
|
{
|
|
this.isLegacy = rule.isLegacy();
|
|
this.name = !isLegacy || rule.name().isEmpty() ? field.getName() : rule.name();
|
|
this.field = field;
|
|
@SuppressWarnings("unchecked") // We are "defining" T here
|
|
Class<T> type = (Class<T>)ClassUtils.primitiveToWrapper(field.getType());
|
|
this.type = type;
|
|
this.isStrict = rule.strict();
|
|
this.categories = List.of(rule.category());
|
|
this.scarpetApp = rule.appSource();
|
|
this.realSettingsManager = settingsManager;
|
|
if (!(settingsManager instanceof carpet.settings.SettingsManager)) {
|
|
// this is awkward... but people using a custom, new (extends only new api) manager should not be using this anyway but the interface method
|
|
this.settingsManager = null;
|
|
} else {
|
|
this.settingsManager = (carpet.settings.SettingsManager) settingsManager;
|
|
}
|
|
this.realValidators = Stream.of(rule.validators()).map(this::instantiateValidator).collect(Collectors.toList());
|
|
this.defaultValue = value();
|
|
FromStringConverter<T> converter0 = null;
|
|
|
|
if (categories.contains(RuleCategory.COMMAND))
|
|
{
|
|
this.realValidators.add(new Validator._COMMAND<T>());
|
|
if (this.type == String.class)
|
|
{
|
|
this.realValidators.add(instantiateValidator(Validators.CommandLevel.class));
|
|
}
|
|
}
|
|
|
|
this.isClient = categories.contains(RuleCategory.CLIENT);
|
|
if (this.isClient)
|
|
{
|
|
this.realValidators.add(new Validator._CLIENT<>());
|
|
}
|
|
|
|
if (!scarpetApp.isEmpty())
|
|
{
|
|
this.realValidators.add(new Validator.ScarpetValidator<>());
|
|
}
|
|
|
|
if (rule.options().length > 0)
|
|
{
|
|
this.options = List.of(rule.options());
|
|
}
|
|
else if (this.type == Boolean.class) {
|
|
this.options = List.of("true", "false");
|
|
}
|
|
else if (this.type == String.class && categories.contains(RuleCategory.COMMAND))
|
|
{
|
|
this.options = Validators.CommandLevel.OPTIONS;
|
|
}
|
|
else if (this.type.isEnum())
|
|
{
|
|
this.options = Arrays.stream(this.type.getEnumConstants()).map(e -> ((Enum<?>) e).name().toLowerCase(Locale.ROOT)).toList();
|
|
converter0 = str -> {
|
|
try {
|
|
@SuppressWarnings({"unchecked", "rawtypes"}) // Raw necessary because of signature. Unchecked because compiler doesn't know T extends Enum
|
|
T ret = (T)Enum.valueOf((Class<? extends Enum>) type, str.toUpperCase(Locale.ROOT));
|
|
return ret;
|
|
} catch (IllegalArgumentException e) {
|
|
throw new InvalidRuleValueException("Valid values for this rule are: " + this.options);
|
|
}
|
|
};
|
|
}
|
|
else
|
|
{
|
|
this.options = List.of();
|
|
}
|
|
if (isStrict && !this.options.isEmpty())
|
|
{
|
|
this.realValidators.add(0, new Validator.StrictValidator<>()); // at 0 prevents validators with side effects from running when invalid
|
|
}
|
|
if (converter0 == null) {
|
|
@SuppressWarnings("unchecked")
|
|
FromStringConverter<T> converterFromMap = (FromStringConverter<T>)CONVERTER_MAP.get(type);
|
|
if (converterFromMap == null) throw new UnsupportedOperationException("Unsupported type for ParsedRule" + type);
|
|
converter0 = converterFromMap;
|
|
}
|
|
this.converter = converter0;
|
|
|
|
// Language "constants"
|
|
String nameKey = TranslationKeys.RULE_NAME_PATTERN.formatted(settingsManager().identifier(), name());
|
|
String descKey = TranslationKeys.RULE_DESC_PATTERN.formatted(settingsManager().identifier(), name());
|
|
String extraPrefix = TranslationKeys.RULE_EXTRA_PREFIX_PATTERN.formatted(settingsManager().identifier(), name());
|
|
|
|
// to remove
|
|
this.description = isLegacy ? rule.desc() : Objects.requireNonNull(Translations.trOrNull(descKey), "No language key provided for " + descKey);
|
|
this.extraInfo = isLegacy ? List.of(rule.extra()) : getTranslationArray(extraPrefix);
|
|
this.defaultAsString = RuleHelper.toRuleString(this.defaultValue);
|
|
this.validators = realValidators.stream().filter(Validator.class::isInstance).map(v -> (Validator<T>) v).toList();
|
|
if (!isLegacy && !validators.isEmpty()) throw new IllegalArgumentException("Can't use legacy validators with new rules!");
|
|
|
|
// Language fallbacks - Also asserts the strings will be available in non-english languages, given current system has no fallback
|
|
if (isLegacy && !rule.name().isEmpty()) Translations.registerFallbackTranslation(nameKey, name);
|
|
Translations.registerFallbackTranslation(descKey, description);
|
|
Iterator<String> infoIterator = extraInfo.iterator();
|
|
for (int i = 0; infoIterator.hasNext(); i++) {
|
|
Translations.registerFallbackTranslation(extraPrefix + i, infoIterator.next());
|
|
}
|
|
}
|
|
|
|
@SuppressWarnings({"unchecked", "rawtypes"}) // Needed because of the annotation
|
|
private carpet.api.settings.Validator<T> instantiateValidator(Class<? extends carpet.api.settings.Validator> cls)
|
|
{
|
|
try
|
|
{
|
|
Constructor<? extends carpet.api.settings.Validator> constr = cls.getDeclaredConstructor();
|
|
constr.setAccessible(true);
|
|
return constr.newInstance();
|
|
}
|
|
catch (ReflectiveOperationException e)
|
|
{
|
|
throw new IllegalArgumentException(e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void set(CommandSourceStack source, String value) throws InvalidRuleValueException
|
|
{
|
|
set(source, converter.convert(value), value);
|
|
}
|
|
|
|
private void set(CommandSourceStack source, T value, String userInput) throws InvalidRuleValueException
|
|
{
|
|
for (carpet.api.settings.Validator<T> validator : this.realValidators)
|
|
{
|
|
value = validator.validate(source, this, value, userInput); // should this recalculate the string? Another validator may have changed value
|
|
if (value == null) {
|
|
if (source != null) validator.notifyFailure(source, this, userInput);
|
|
throw new InvalidRuleValueException();
|
|
}
|
|
}
|
|
if (!value.equals(value()) || source == null)
|
|
{
|
|
try {
|
|
this.field.set(null, value);
|
|
} catch (IllegalAccessException e) {
|
|
throw new IllegalStateException("Couldn't access field for rule: " + name, e);
|
|
}
|
|
if (source != null) settingsManager().notifyRuleChanged(source, this, userInput);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object obj)
|
|
{
|
|
return obj instanceof ParsedRule && ((ParsedRule<?>) obj).name.equals(this.name);
|
|
}
|
|
|
|
@Override
|
|
public int hashCode()
|
|
{
|
|
return this.name.hashCode();
|
|
}
|
|
|
|
@Override
|
|
public String toString()
|
|
{
|
|
return this.name + ": " + RuleHelper.toRuleString(value());
|
|
}
|
|
|
|
@Override
|
|
public String name() {
|
|
return name;
|
|
}
|
|
|
|
@Override
|
|
public List<Component> extraInfo() {
|
|
return getTranslationArray(TranslationKeys.RULE_EXTRA_PREFIX_PATTERN.formatted(settingsManager().identifier(), name()))
|
|
.stream()
|
|
.map(str -> Messenger.c("g " + str))
|
|
.toList();
|
|
}
|
|
|
|
private List<String> getTranslationArray(String prefix) {
|
|
List<String> ret = new ArrayList<>();
|
|
for (int i = 0; Translations.hasTranslation(prefix + i); i++) {
|
|
ret.add(Translations.tr(prefix + i));
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
@Override
|
|
public Collection<String> categories() {
|
|
return categories;
|
|
}
|
|
|
|
@Override
|
|
public Collection<String> suggestions() {
|
|
return options;
|
|
}
|
|
|
|
@Override
|
|
public SettingsManager settingsManager() {
|
|
return realSettingsManager;
|
|
}
|
|
|
|
@Override
|
|
@SuppressWarnings("unchecked") // T comes from the field
|
|
public T value() {
|
|
try {
|
|
return (T) field.get(null);
|
|
} catch (IllegalAccessException e) {
|
|
// Can't happen at regular runtime because we'd have thrown it on construction
|
|
throw new IllegalArgumentException("Couldn't access field for rule: " + name, e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean canBeToggledClientSide() {
|
|
return isClient;
|
|
}
|
|
|
|
@Override
|
|
public Class<T> type() {
|
|
return type;
|
|
}
|
|
|
|
@Override
|
|
public T defaultValue() {
|
|
return defaultValue;
|
|
}
|
|
|
|
@Override
|
|
public void set(CommandSourceStack source, T value) throws InvalidRuleValueException {
|
|
set(source, value, RuleHelper.toRuleString(value));
|
|
}
|
|
|
|
@Override
|
|
public boolean strict() {
|
|
return !realValidators.isEmpty() && realValidators.get(0) instanceof Validator.StrictValidator;
|
|
}
|
|
|
|
private static <T> Map.Entry<Class<T>, FromStringConverter<T>> numericalConverter(Class<T> outputClass, Function<String, T> converter) {
|
|
return Map.entry(outputClass, str -> {
|
|
try {
|
|
return converter.apply(str);
|
|
} catch (NumberFormatException e) {
|
|
throw new InvalidRuleValueException("Invalid number for rule");
|
|
}
|
|
});
|
|
}
|
|
|
|
//TO REMOVE
|
|
|
|
/**
|
|
* @deprecated Use {@link CarpetRule#value()} instead
|
|
*/
|
|
@Deprecated(forRemoval = true)
|
|
public T get()
|
|
{
|
|
return value();
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use {@link RuleHelper#toRuleString(Object) RuleHelper.convertToRuleString(rule.value())}
|
|
*/
|
|
@Deprecated(forRemoval = true)
|
|
public String getAsString()
|
|
{
|
|
return RuleHelper.toRuleString(value());
|
|
}
|
|
|
|
/**
|
|
* @return The value of this {@link ParsedRule}, converted to a {@link boolean}.
|
|
* It will only return {@link true} if it's a true {@link boolean} or
|
|
* a number greater than zero.
|
|
* @deprecated Use {@link RuleHelper#getBooleanValue(CarpetRule)}
|
|
*/
|
|
@Deprecated(forRemoval = true)
|
|
public boolean getBoolValue()
|
|
{
|
|
return RuleHelper.getBooleanValue(this);
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use {@link RuleHelper#isInDefaultValue(CarpetRule)}
|
|
*/
|
|
@Deprecated(forRemoval = true)
|
|
public boolean isDefault()
|
|
{
|
|
return RuleHelper.isInDefaultValue(this);
|
|
}
|
|
|
|
/**
|
|
* @deprecated Use {@link RuleHelper#resetToDefault(CarpetRule, CommandSourceStack)}
|
|
*/
|
|
@Deprecated(forRemoval = true)
|
|
public void resetToDefault(CommandSourceStack source)
|
|
{
|
|
RuleHelper.resetToDefault(this, source);
|
|
}
|
|
|
|
/**
|
|
* @deprecated Forcing {@link Comparable} isn't a thing on {@link CarpetRule}s. Instead, pass a comparator by name to your
|
|
* sort methods, you can get one by calling {@code Comparator.comparing(CarpetRule::name)}
|
|
*/
|
|
@Override
|
|
@Deprecated(forRemoval = true)
|
|
public int compareTo(ParsedRule<?> o)
|
|
{
|
|
if (!warnedComparable) {
|
|
warnedComparable = true;
|
|
CarpetSettings.LOG.warn("""
|
|
Extension is relying on carpet rules to be comparable! This is not true for all carpet rules anymore, \
|
|
and will crash the game in future versions or if an extension adds non-comparable rules!
|
|
Fixing it is as simple as passing Comparator.comparing(CarpetRule::name) to the sorting method!""",
|
|
new Throwable("Location:").fillInStackTrace());
|
|
}
|
|
return this.name.compareTo(o.name);
|
|
}
|
|
private static boolean warnedComparable = false;
|
|
|
|
/**
|
|
* @return A {@link String} being the translated {@link ParsedRule#name} of this rule,
|
|
* in Carpet's configured language.
|
|
* @deprecated Use {@link RuleHelper#translatedName(CarpetRule)} instead
|
|
*/
|
|
@Deprecated(forRemoval = true)
|
|
public String translatedName() {
|
|
return RuleHelper.translatedName(this);
|
|
}
|
|
|
|
/**
|
|
* @return A {@link String} being the translated {@link ParsedRule#description description} of this rule,
|
|
* in Carpet's configured language.
|
|
* @deprecated Use {@link RuleHelper#translatedDescription(CarpetRule)} instead
|
|
*/
|
|
@Deprecated(forRemoval = true)
|
|
public String translatedDescription()
|
|
{
|
|
return RuleHelper.translatedDescription(this);
|
|
}
|
|
|
|
/**
|
|
* @return A {@link String} being the translated {@link ParsedRule#extraInfo extraInfo} of this
|
|
* {@link ParsedRule}, in Carpet's configured language.
|
|
* @deprecated Use {@link CarpetRule#extraInfo()} instead
|
|
*/
|
|
@Deprecated(forRemoval = true)
|
|
public List<String> translatedExtras()
|
|
{
|
|
return extraInfo().stream().map(Component::getString).toList();
|
|
}
|
|
}
|