diff --git a/.gitignore b/.gitignore
index 2b21e4d6..347f039a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -108,6 +108,7 @@ gradle-app.setting
# Cache of project
.gradletasknamecache
+**/.kotlin/*
**/build/
diff --git a/build.gradle.kts b/build.gradle.kts
index 98355de1..b11fb36a 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -9,7 +9,6 @@ plugins {
`maven-publish`
checkstyle
alias(libs.plugins.fabric.loom)
- alias(libs.plugins.spotless)
}
class ModInfo {
@@ -28,7 +27,6 @@ allprojects {
plugins.apply("java-library")
plugins.apply("checkstyle")
plugins.apply("fabric-loom")
- plugins.apply("com.diffplug.spotless")
tasks.withType().configureEach {
enabled = false
@@ -128,23 +126,6 @@ allprojects {
filesMatching("fabric.mod.json") { expand(map) }
}
- spotless {
- lineEndings = com.diffplug.spotless.LineEnding.UNIX
-
- java {
- removeUnusedImports()
- importOrder("java", "javax", "", "net.minecraft", "net.fabricmc", "dev.spiritstudios")
- leadingSpacesToTabs()
- trimTrailingWhitespace()
- }
-
- json {
- target("src/**/lang/*.json")
- targetExclude("src/**/generated/**")
- gson().indentWithSpaces(4).sortByKeys()
- }
- }
-
checkstyle {
configFile = rootProject.file("checkstyle.xml")
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index bd9e4b4f..934b154d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -1,12 +1,12 @@
[versions]
-fabric_loom = "1.10-SNAPSHOT"
+fabric_loom = "1.11-SNAPSHOT"
spotless = "7.0.3"
minecraft = "1.21.5"
yarn = "1.21.5+build.1"
fabric_loader = "0.16.14"
-fabric_api = "0.124.0+1.21.5"
+fabric_api = "0.128.1+1.21.5"
nightconfig = "3.8.1"
modmenu = "14.0.0-rc.2"
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 9bbc975c..1b33c55b 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index cea7a793..ca025c83 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
index faf93008..23d15a93 100755
--- a/gradlew
+++ b/gradlew
@@ -114,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
- org.gradle.wrapper.GradleWrapperMain \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
diff --git a/gradlew.bat b/gradlew.bat
index 9d21a218..db3a6ac2 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -70,11 +70,11 @@ goto fail
:execute
@rem Setup the command line
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+set CLASSPATH=
@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
diff --git a/specter-block/src/main/java/dev/spiritstudios/specter/api/block/FlammableBlockData.java b/specter-block/src/main/java/dev/spiritstudios/specter/api/block/FlammableBlockData.java
index 41a35f4e..f4e2b1f7 100644
--- a/specter-block/src/main/java/dev/spiritstudios/specter/api/block/FlammableBlockData.java
+++ b/specter-block/src/main/java/dev/spiritstudios/specter/api/block/FlammableBlockData.java
@@ -2,20 +2,21 @@
import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
-import net.fabricmc.fabric.api.registry.FlammableBlockRegistry;
import org.jetbrains.annotations.Range;
import net.minecraft.network.RegistryByteBuf;
import net.minecraft.network.codec.PacketCodec;
import net.minecraft.network.codec.PacketCodecs;
+import net.fabricmc.fabric.api.registry.FlammableBlockRegistry;
+
/**
* The flammability data of a block.
*
* @see BlockMetatags#FLAMMABLE
*/
public record FlammableBlockData(@Range(from = 0, to = Integer.MAX_VALUE) int burn,
- @Range(from = 0, to = Integer.MAX_VALUE) int spread) {
+ @Range(from = 0, to = Integer.MAX_VALUE) int spread) {
public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance
.group(
Codec.intRange(0, Integer.MAX_VALUE).fieldOf("burn").forGetter(FlammableBlockData::burn),
diff --git a/specter-block/src/testmod/java/dev/spiritstudios/testmod/block/SpecterBlockGameTest.java b/specter-block/src/testmod/java/dev/spiritstudios/testmod/block/SpecterBlockGameTest.java
index dd288453..17428a29 100644
--- a/specter-block/src/testmod/java/dev/spiritstudios/testmod/block/SpecterBlockGameTest.java
+++ b/specter-block/src/testmod/java/dev/spiritstudios/testmod/block/SpecterBlockGameTest.java
@@ -1,7 +1,5 @@
package dev.spiritstudios.testmod.block;
-import net.fabricmc.fabric.api.gametest.v1.GameTest;
-
import net.minecraft.block.Blocks;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack;
@@ -11,6 +9,8 @@
import net.minecraft.util.math.BlockPos;
import net.minecraft.world.GameMode;
+import net.fabricmc.fabric.api.gametest.v1.GameTest;
+
@SuppressWarnings("unused")
public final class SpecterBlockGameTest {
@GameTest
diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/api/config/client/ConfigScreen.java b/specter-config/src/client/java/dev/spiritstudios/specter/api/config/client/ConfigScreen.java
index 73547f75..14926f69 100644
--- a/specter-config/src/client/java/dev/spiritstudios/specter/api/config/client/ConfigScreen.java
+++ b/specter-config/src/client/java/dev/spiritstudios/specter/api/config/client/ConfigScreen.java
@@ -3,32 +3,25 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
-import java.util.function.BiFunction;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.Screen;
-import net.minecraft.client.gui.tooltip.Tooltip;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.ClickableWidget;
import net.minecraft.screen.ScreenTexts;
import net.minecraft.text.Text;
import dev.spiritstudios.specter.api.config.Config;
-import dev.spiritstudios.specter.api.config.Value;
-import dev.spiritstudios.specter.api.core.SpecterGlobals;
-import dev.spiritstudios.specter.api.core.reflect.ReflectionHelper;
-import dev.spiritstudios.specter.impl.config.NestedConfigValue;
-import dev.spiritstudios.specter.impl.config.client.NestedConfigScreen;
import dev.spiritstudios.specter.impl.config.client.gui.widget.OptionsScrollableWidget;
public abstract class ConfigScreen extends Screen {
private static final Text MULTIPLAYER_SYNC_ERROR = Text.translatable("screen.specter.config.multiplayer_sync_error");
- protected final Config> config;
+ protected final Config config;
protected final Screen parent;
protected final String id;
- public ConfigScreen(Config> config, String id, Screen parent) {
+ public ConfigScreen(Config config, String id, Screen parent) {
super(Text.translatable("config.%s.title".formatted(id)));
this.config = config;
this.parent = parent;
@@ -43,56 +36,44 @@ protected void init() {
OptionsScrollableWidget scrollableWidget = new OptionsScrollableWidget(this.client, this.width, this.height - 64, 32, 25);
- List>> values = config.fields();
-
- if (this.client.player != null && !this.client.isInSingleplayer()) {
- for (ReflectionHelper.FieldValuePair> pair : values) {
- if (!pair.value().sync()) continue;
-
- this.client.player.sendMessage(MULTIPLAYER_SYNC_ERROR, false);
- this.client.setScreen(this.parent);
-
- return;
- }
- }
-
List options = new ArrayList<>();
- values.forEach(pair -> {
- if (pair.value() instanceof NestedConfigValue> nestedOption) {
- String nestedId = "%s.%s".formatted(id, pair.value().name());
- ConfigScreen screen = new NestedConfigScreen(nestedOption.get(), nestedId, this);
-
- options.add(
- ButtonWidget.builder(
- Text.translatable("config.%s.title".formatted(nestedId)),
- button -> {
- save();
- this.client.setScreen(screen);
- }
- ).dimensions(this.width / 2 - 100, 0, 200, 20).build()
- );
-
- return;
- }
-
- BiFunction, String, ? extends ClickableWidget> factory = ConfigScreenWidgets.getWidgetFactory(pair.value());
- if (factory == null) {
- SpecterGlobals.LOGGER.warn("No widget factory found for {}", pair.value().defaultValue().getClass().getSimpleName());
- return;
- }
-
- ClickableWidget widget = factory.apply(pair.value(), id);
- if (widget == null)
- throw new IllegalStateException("Widget factory returned null for %s".formatted(pair.value().defaultValue().getClass().getSimpleName()));
-
- widget.setWidth(0);
- widget.setHeight(20);
-
- Text tooltip = Text.translatableWithFallback("%s.tooltip".formatted(pair.value().translationKey(id)), "");
- if (!tooltip.getString().isEmpty()) widget.setTooltip(Tooltip.of(tooltip));
-
- options.add(widget);
+ config.values().forEach((key, value) -> {
+// if (value instanceof Ne> nestedOption) {
+// String nestedId = "%s.%s".formatted(id, pair.value().name());
+// ConfigScreen screen = new NestedConfigScreen(nestedOption.get(), nestedId, this);
+//
+// options.add(
+// ButtonWidget.builder(
+// Text.translatable("config.%s.title".formatted(nestedId)),
+// button -> {
+// save();
+// this.client.setScreen(screen);
+// }
+// ).dimensions(this.width / 2 - 100, 0, 200, 20).build()
+// );
+//
+// return;
+// }
+
+// BiFunction, String, ? extends ClickableWidget> factory = ConfigScreenWidgets.getWidgetFactory(value);
+// if (factory == null) {
+// SpecterGlobals.LOGGER.warn("No widget factory found for {}", value.defaultValue().getClass().getSimpleName());
+// return;
+// }
+//
+// ClickableWidget widget = factory.apply(value, id);
+// if (widget == null)
+// throw new IllegalStateException("Widget factory returned null for %s".formatted(value.defaultValue().getClass().getSimpleName()));
+//
+// widget.setWidth(0);
+// widget.setHeight(20);
+//
+// Text tooltip = Text.translatableWithFallback("%s.tooltip"
+// .formatted(Value.translationKey(key, id)), "");
+// if (!tooltip.getString().isEmpty()) widget.setTooltip(Tooltip.of(tooltip));
+//
+// options.add(widget);
});
scrollableWidget.addOptions(options);
diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/api/config/client/ConfigScreenWidgets.java b/specter-config/src/client/java/dev/spiritstudios/specter/api/config/client/ConfigScreenWidgets.java
index febbf1ec..2e33256f 100644
--- a/specter-config/src/client/java/dev/spiritstudios/specter/api/config/client/ConfigScreenWidgets.java
+++ b/specter-config/src/client/java/dev/spiritstudios/specter/api/config/client/ConfigScreenWidgets.java
@@ -1,132 +1,18 @@
package dev.spiritstudios.specter.api.config.client;
-import java.util.Arrays;
-import java.util.List;
import java.util.function.BiFunction;
-import java.util.stream.Collectors;
import org.jetbrains.annotations.ApiStatus;
-import net.minecraft.block.Block;
-import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.widget.ClickableWidget;
-import net.minecraft.client.gui.widget.TextFieldWidget;
-import net.minecraft.item.Item;
-import net.minecraft.registry.Registries;
-import net.minecraft.registry.Registry;
-import net.minecraft.screen.ScreenTexts;
-import net.minecraft.text.Text;
-import net.minecraft.util.Formatting;
-import net.minecraft.util.Identifier;
-import dev.spiritstudios.specter.api.config.NumericValue;
import dev.spiritstudios.specter.api.config.Value;
import dev.spiritstudios.specter.api.core.collect.PatternMap;
-import dev.spiritstudios.specter.api.gui.client.widget.SpecterButtonWidget;
-import dev.spiritstudios.specter.api.gui.client.widget.SpecterSliderWidget;
@SuppressWarnings("unchecked")
public final class ConfigScreenWidgets {
private static final PatternMap, String, ? extends ClickableWidget>> widgetFactories = new PatternMap<>();
- private static final BiFunction, String, ? extends ClickableWidget> BOOLEAN_WIDGET_FACTORY = (configValue, id) -> {
- Value value = (Value) configValue;
- return SpecterButtonWidget.builder(
- () -> Text.translatable(value.translationKey(id)).append(": ").append(ScreenTexts.onOrOff(value.get())),
- button -> value.set(!value.get())
- ).build();
- };
- private static final BiFunction, String, ? extends ClickableWidget> INTEGER_WIDGET_FACTORY = (configValue, id) -> {
- NumericValue value = (NumericValue) configValue;
-
- return SpecterSliderWidget.builder(value.get())
- .message((val) -> Text.translatable(value.translationKey(id)).append(String.format(": %.0f", val)))
- .range(value.range().min(), value.range().max())
- .step(value.step() == 0 ? 1 : value.step())
- .onValueChanged((val) -> value.set(val.intValue()))
- .build();
- };
- private static final BiFunction, String, ? extends ClickableWidget> DOUBLE_WIDGET_FACTORY = (configValue, id) -> {
- NumericValue value = (NumericValue) configValue;
-
- return SpecterSliderWidget.builder(value.get())
- .message((val) -> Text.translatable(value.translationKey(id)).append(String.format(": %.2f", val)))
- .range(value.range())
- .step(value.step())
- .onValueChanged(value::set)
- .build();
- };
- private static final BiFunction, String, ? extends ClickableWidget> FLOAT_WIDGET_FACTORY = (configValue, id) -> {
- NumericValue value = (NumericValue) configValue;
-
- return SpecterSliderWidget.builder(value.get())
- .message((val) -> Text.translatable(configValue.translationKey(id)).append(String.format(": %.1f", val)))
- .range(value.range().min(), value.range().max())
- .step(value.step())
- .onValueChanged((val) -> value.set(val.floatValue()))
- .build();
- };
- private static final BiFunction, String, ? extends ClickableWidget> STRING_WIDGET_FACTORY = (configValue, id) -> {
- Value value = (Value) configValue;
-
- TextFieldWidget widget = new TextFieldWidget(MinecraftClient.getInstance().textRenderer, 0, 0, 0, 0, Text.of(value.get()));
- widget.setPlaceholder(Text.translatableWithFallback("%s.placeholder".formatted(configValue.translationKey(id)), "").formatted(Formatting.DARK_GRAY));
-
- widget.setText(value.get());
- widget.setChangedListener(value::set);
- widget.setSelectionEnd(0);
- widget.setSelectionStart(0);
-
- return widget;
- };
- private static final BiFunction, String, ? extends ClickableWidget> ENUM_WIDGET_FACTORY = (configValue, id) -> {
- Value> value = (Value>) configValue;
-
- List> enumValues = Arrays.stream(configValue.defaultValue().getClass().getEnumConstants())
- .filter(val -> val instanceof Enum>)
- .map(val -> (Enum>) val)
- .collect(Collectors.toList());
-
- if (enumValues.isEmpty()) throw new IllegalArgumentException("Enum values cannot be null");
-
- return SpecterButtonWidget.builder(
- () -> Text.translatable(configValue.translationKey(id)).append(": ").append(Text.translatable("%s.%s".formatted(configValue.translationKey(id), value.get().toString().toLowerCase()))),
- button -> {
- Enum> current = value.get();
- int index = enumValues.indexOf(current);
- value.set(enumValues.get((index + 1) % enumValues.size()));
- }
- ).build();
- };
-
- static {
- addRegistry(Item.class, Registries.ITEM);
- addRegistry(Block.class, Registries.BLOCK);
- }
-
- private ConfigScreenWidgets() {
- }
-
- public static void addRegistry(Class clazz, Registry registry) {
- widgetFactories.put(clazz, (configValue, id) -> {
- Value value = (Value) configValue;
-
- TextFieldWidget widget = new TextFieldWidget(MinecraftClient.getInstance().textRenderer, 0, 0, 0, 0, Text.of(registry.getEntry(value.get()).getIdAsString()));
- widget.setPlaceholder(Text.translatableWithFallback("%s.placeholder".formatted(configValue.translationKey(id)), "").formatted(Formatting.DARK_GRAY));
-
- widget.setText(value.get().toString());
- widget.setChangedListener(val -> {
- Identifier identifier = Identifier.tryParse(val);
- if (identifier == null) return;
-
- registry.getOptionalValue(identifier).ifPresent(value::set);
- });
- widget.setSelectionEnd(0);
- widget.setSelectionStart(0);
-
- return widget;
- });
- }
public static void add(Class> clazz, BiFunction, String, ? extends ClickableWidget> factory) {
widgetFactories.put(clazz, factory);
@@ -138,12 +24,6 @@ public static void add(Class> clazz, BiFunction, String, ? extends Cl
// 1. It's (usually) faster than a map lookup, as most of the time the value will be one of these types
// 2. It lets us handle the lowercased names of primitive types, which are different Class<> instances because reasons
return switch (value.defaultValue()) {
- case Boolean ignored -> BOOLEAN_WIDGET_FACTORY;
- case Integer ignored -> INTEGER_WIDGET_FACTORY;
- case Double ignored -> DOUBLE_WIDGET_FACTORY;
- case Float ignored -> FLOAT_WIDGET_FACTORY;
- case String ignored -> STRING_WIDGET_FACTORY;
- case Enum> ignored -> ENUM_WIDGET_FACTORY;
default -> widgetFactories.get(value.defaultValue().getClass());
};
}
diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/api/config/client/TabbedListConfigScreen.java b/specter-config/src/client/java/dev/spiritstudios/specter/api/config/client/TabbedListConfigScreen.java
new file mode 100644
index 00000000..65e2d8a8
--- /dev/null
+++ b/specter-config/src/client/java/dev/spiritstudios/specter/api/config/client/TabbedListConfigScreen.java
@@ -0,0 +1,260 @@
+package dev.spiritstudios.specter.api.config.client;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.common.collect.ImmutableList;
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.Element;
+import net.minecraft.client.gui.Selectable;
+import net.minecraft.client.gui.screen.Screen;
+import net.minecraft.client.gui.widget.ButtonWidget;
+import net.minecraft.client.gui.widget.ClickableWidget;
+import net.minecraft.client.gui.widget.DirectionalLayoutWidget;
+import net.minecraft.client.gui.widget.ElementListWidget;
+import net.minecraft.client.gui.widget.ThreePartsLayoutWidget;
+import net.minecraft.client.resource.language.I18n;
+import net.minecraft.screen.ScreenTexts;
+import net.minecraft.text.MutableText;
+import net.minecraft.text.OrderedText;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+
+import dev.spiritstudios.specter.api.config.ConfigHolder;
+import dev.spiritstudios.specter.api.config.Value;
+import dev.spiritstudios.specter.impl.config.client.gui.widget.gamerule.BooleanValueWidget;
+import dev.spiritstudios.specter.impl.config.client.gui.widget.gamerule.EnumValueWidget;
+import dev.spiritstudios.specter.impl.config.client.gui.widget.gamerule.GameruleConfigWidgetFactories;
+
+/**
+ * A simple config screen, meant to look consistent with other 'key-value' style configuration screens in the game.
+ */
+public class TabbedListConfigScreen extends Screen {
+ protected final ConfigHolder, ?> holder;
+ protected final Screen parent;
+
+ protected final String translationPrefix;
+
+ private final ThreePartsLayoutWidget layout = new ThreePartsLayoutWidget(this);
+ private ValueListWidget list;
+ private ButtonWidget doneButton;
+
+ public TabbedListConfigScreen(ConfigHolder, ?> holder, Screen parent) {
+ super(Text.translatable("config.%s.title".formatted(holder.id().toTranslationKey())));
+
+ this.translationPrefix = "config." + holder.id().toTranslationKey() + ".";
+ this.holder = holder;
+ this.parent = parent;
+ }
+
+ @Override
+ protected void init() {
+ this.layout.addHeader(translatable("title"), this.textRenderer);
+ this.list = this.layout.addBody(new ValueListWidget(
+ width,
+ layout.getContentHeight(),
+ layout.getHeaderHeight(),
+ holder
+ ));
+
+ DirectionalLayoutWidget buttons = this.layout.addFooter(DirectionalLayoutWidget.horizontal().spacing(8));
+
+ this.doneButton = buttons.add(ButtonWidget.builder(
+ ScreenTexts.DONE,
+ button -> {
+ for (ValueWidget value : this.list.valueWidgets) value.apply();
+ holder.save();
+ this.close();
+ }
+ ).build());
+
+ buttons.add(ButtonWidget.builder(
+ ScreenTexts.CANCEL,
+ button -> this.close()
+ ).build());
+
+ this.layout.forEachChild(this::addDrawableChild);
+ this.refreshWidgetPositions();
+ }
+
+ @Override
+ protected void refreshWidgetPositions() {
+ this.layout.refreshPositions();
+ if (this.list != null) {
+ this.list.position(this.width, this.layout);
+ }
+ }
+
+ @Override
+ public void close() {
+ assert this.client != null;
+ this.client.setScreen(parent);
+ }
+
+ private Text translatable(String key) {
+ return Text.translatable(translationPrefix + key);
+ }
+
+ @FunctionalInterface
+ public interface ValueWidgetFactory {
+ TabbedListConfigScreen.ValueWidget create(MinecraftClient client,
+ String translationPrefix,
+ List description,
+ Text narration,
+ Text name,
+ Value value);
+
+ default Text toString(String translationPrefix, T value) {
+ return Text.literal(String.valueOf(value));
+ }
+ }
+
+ public abstract static class ValueWidget extends ElementListWidget.Entry {
+ protected final @Nullable List description;
+ protected final List name;
+ protected final List children = new ArrayList<>();
+ protected final MinecraftClient client;
+
+ public ValueWidget(MinecraftClient client, @Nullable List description, Text name) {
+ this.client = client;
+ this.description = description;
+ this.name = client.textRenderer.wrapLines(name, 175);
+ }
+
+ public abstract void apply();
+
+ @Override
+ public List extends Element> children() {
+ return this.children;
+ }
+
+ @Override
+ public List extends Selectable> selectableChildren() {
+ return this.children;
+ }
+
+ @Override
+ public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickProgress) {
+ if (this.name.size() == 1) {
+ context.drawTextWithShadow(client.textRenderer, this.name.getFirst(), x, y + 5, -1);
+ } else if (this.name.size() >= 2) {
+ context.drawTextWithShadow(client.textRenderer, this.name.get(0), x, y, -1);
+ context.drawTextWithShadow(client.textRenderer, this.name.get(1), x, y + 10, -1);
+ }
+ }
+ }
+
+ private class ValueListWidget extends ElementListWidget {
+ private final List valueWidgets;
+
+ public ValueListWidget(int width, int height, int y, ConfigHolder, ?> holder) {
+ super(TabbedListConfigScreen.this.client, width, height, y, 24);
+
+ ImmutableList.Builder builder = ImmutableList.builder();
+
+ holder.get().values().forEach((key, either) -> {
+ ValueWidget widget = either.map(
+ value -> {
+ ValueWidgetFactory> factory = switch (value.defaultValue()) {
+ case Boolean ignored -> BooleanValueWidget.FACTORY;
+ case Integer ignored -> GameruleConfigWidgetFactories.INTEGER;
+ case Float ignored -> GameruleConfigWidgetFactories.FLOAT;
+ case Double ignored -> GameruleConfigWidgetFactories.DOUBLE;
+ case Long ignored -> GameruleConfigWidgetFactories.LONG;
+ case String ignored -> GameruleConfigWidgetFactories.STRING;
+ case Enum> ignored -> EnumValueWidget.FACTORY;
+ default -> null;
+ };
+ if (factory == null) return null;
+
+ return createValueWidget(
+ holder,
+ key,
+ value,
+ textRenderer,
+ factory
+ );
+ },
+ subConfig -> {
+ return null;
+ });
+
+ if (widget != null) {
+ addEntry(widget);
+ builder.add(widget);
+ }
+ });
+
+ this.valueWidgets = builder.build();
+ }
+
+ @Override
+ public int getRowWidth() {
+ return 320;
+ }
+
+ @Override
+ public void renderWidget(DrawContext context, int mouseX, int mouseY, float deltaTicks) {
+ super.renderWidget(context, mouseX, mouseY, deltaTicks);
+ ValueWidget hovered = this.getHoveredEntry();
+ if (hovered != null && hovered.description != null) {
+ TabbedListConfigScreen.this.setTooltip(hovered.description);
+ }
+ }
+
+ private ValueWidget createValueWidget(
+ ConfigHolder, ?> holder,
+ String key,
+ Value> value,
+ TextRenderer textRenderer,
+ ValueWidgetFactory factory
+ ) {
+ @SuppressWarnings("unchecked") Value casted = (Value) value;
+
+ String translationPrefix = "config." + holder.id().toTranslationKey() + "." + key;
+
+ Text name = Text.translatable(translationPrefix);
+ Text serializedName = Text.literal(key).formatted(Formatting.YELLOW);
+
+ Text defaultValue = Text.translatable(
+ "editGamerule.default",
+ factory.toString(translationPrefix, casted.defaultValue())
+ ).formatted(Formatting.GRAY);
+
+ String descriptionKey = translationPrefix + ".description";
+
+ List description;
+ Text narration;
+
+ if (I18n.hasTranslation(descriptionKey)) {
+ ImmutableList.Builder builder = ImmutableList.builder();
+
+ builder.add(serializedName.asOrderedText());
+
+ MutableText descriptionText = Text.translatable(descriptionKey);
+ textRenderer.wrapLines(descriptionText, 150).forEach(builder::add);
+
+ builder.add(defaultValue.asOrderedText());
+
+ description = builder.build();
+ narration = descriptionText.append("\n").append(defaultValue.getString());
+ } else {
+ description = ImmutableList.of(serializedName.asOrderedText(), defaultValue.asOrderedText());
+ narration = defaultValue;
+ }
+
+ return factory.create(
+ client,
+ translationPrefix,
+ description,
+ narration,
+ name,
+ casted
+ );
+ }
+ }
+}
diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/NestedConfigScreen.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/NestedConfigScreen.java
index 4474cf5e..c35b6b9a 100644
--- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/NestedConfigScreen.java
+++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/NestedConfigScreen.java
@@ -2,11 +2,11 @@
import net.minecraft.client.gui.screen.Screen;
-import dev.spiritstudios.specter.api.config.NestedConfig;
+import dev.spiritstudios.specter.api.config.Config;
import dev.spiritstudios.specter.api.config.client.ConfigScreen;
public class NestedConfigScreen extends ConfigScreen {
- public NestedConfigScreen(NestedConfig> config, String id, Screen parent) {
+ public NestedConfigScreen(Config.SubConfig config, String id, Screen parent) {
super(config, id, parent);
if (!(this.parent instanceof ConfigScreen))
throw new IllegalArgumentException("Parent of NestedConfigScreen must be a ConfigScreen");
diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/SpecterConfigClient.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/SpecterConfigClient.java
index 4dd1433a..c16befff 100644
--- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/SpecterConfigClient.java
+++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/SpecterConfigClient.java
@@ -11,11 +11,12 @@
public class SpecterConfigClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
- ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> ConfigHolderRegistry.reload());
+ ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> {
+ ConfigHolderRegistry.clearOverrides();
+ });
ClientPlayNetworking.registerGlobalReceiver(ConfigSyncS2CPayload.ID, (payload, context) -> {
- SpecterGlobals.debug("Received config sync packet");
- SpecterGlobals.debug("Payload: %s".formatted(payload));
+ SpecterGlobals.debug("Received config sync packet for config \"%s\"".formatted(payload.config().id()));
});
}
}
diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/SpecterConfigModMenu.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/SpecterConfigModMenu.java
index 75174820..37029756 100644
--- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/SpecterConfigModMenu.java
+++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/SpecterConfigModMenu.java
@@ -8,7 +8,7 @@
import dev.spiritstudios.specter.api.config.ConfigHolder;
import dev.spiritstudios.specter.api.config.client.ModMenuHelper;
-import dev.spiritstudios.specter.api.config.client.RootConfigScreen;
+import dev.spiritstudios.specter.api.config.client.TabbedListConfigScreen;
import dev.spiritstudios.specter.impl.config.ConfigHolderRegistry;
public class SpecterConfigModMenu implements ModMenuApi {
@@ -20,7 +20,7 @@ public Map> getProvidedConfigScreenFactories() {
entry ->
parent -> {
ConfigHolder, ?> holder = ConfigHolderRegistry.get(entry.getValue());
- return new RootConfigScreen(holder, parent);
+ return new TabbedListConfigScreen(holder, parent);
}
));
}
diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/BooleanValueWidget.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/BooleanValueWidget.java
new file mode 100644
index 00000000..75313bd0
--- /dev/null
+++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/BooleanValueWidget.java
@@ -0,0 +1,72 @@
+package dev.spiritstudios.specter.impl.config.client.gui.widget.gamerule;
+
+
+import java.util.List;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.widget.CyclingButtonWidget;
+import net.minecraft.screen.ScreenTexts;
+import net.minecraft.text.OrderedText;
+import net.minecraft.text.Text;
+
+import dev.spiritstudios.specter.api.config.Value;
+import dev.spiritstudios.specter.api.config.client.TabbedListConfigScreen;
+
+public class BooleanValueWidget extends TabbedListConfigScreen.ValueWidget {
+ public static final TabbedListConfigScreen.ValueWidgetFactory FACTORY = new TabbedListConfigScreen.ValueWidgetFactory<>() {
+ @Override
+ public TabbedListConfigScreen.ValueWidget create(MinecraftClient client, String translationPrefix, List description, Text narration, Text name, Value value) {
+ return new BooleanValueWidget(client, description, narration, name, value);
+ }
+
+ @Override
+ public Text toString(String translationPrefix, Boolean value) {
+ return ScreenTexts.onOrOff(value);
+ }
+ };
+
+
+ private final CyclingButtonWidget button;
+ private final Value value;
+
+ public BooleanValueWidget(
+ MinecraftClient client,
+ @Nullable List description,
+ Text narration,
+ Text name,
+ Value value
+ ) {
+ super(client, description, name);
+
+ this.value = value;
+
+ this.button = CyclingButtonWidget.onOffBuilder(value.get())
+ .omitKeyText()
+ .narration(button ->
+ button.getGenericNarrationMessage().append("\n").append(narration))
+ .build(
+ 10, 5,
+ 160, 20,
+ name
+ );
+
+ this.children.add(this.button);
+ }
+
+ @Override
+ public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickProgress) {
+ super.render(context, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickProgress);
+
+ this.button.setX(x + entryWidth - 165);
+ this.button.setY(y);
+ this.button.render(context, mouseX, mouseY, tickProgress);
+ }
+
+ @Override
+ public void apply() {
+ this.value.set(button.getValue());
+ }
+}
diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/DoubleInputWidget.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/DoubleInputWidget.java
new file mode 100644
index 00000000..213d2a91
--- /dev/null
+++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/DoubleInputWidget.java
@@ -0,0 +1,68 @@
+package dev.spiritstudios.specter.impl.config.client.gui.widget.gamerule;
+
+import java.util.List;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.widget.TextFieldWidget;
+import net.minecraft.text.OrderedText;
+import net.minecraft.text.Text;
+
+import dev.spiritstudios.specter.api.config.Value;
+import dev.spiritstudios.specter.api.config.client.TabbedListConfigScreen;
+import dev.spiritstudios.specter.api.core.math.SpecterMath;
+
+public class DoubleInputWidget extends TabbedListConfigScreen.ValueWidget {
+ private final TextFieldWidget textField;
+ private final Value value;
+
+ private boolean valid = true;
+
+ public DoubleInputWidget(
+ MinecraftClient client,
+ @Nullable List description,
+ Text narration,
+ Text name,
+ Value value
+ ) {
+ super(client, description, name);
+
+ this.value = value;
+
+ this.textField = new TextFieldWidget(
+ client.textRenderer,
+ 10, 5,
+ 160, 20,
+ name
+ );
+
+ this.textField.setText(Double.toString(value.get()));
+ this.textField.setChangedListener(s -> {
+ if (SpecterMath.canParseDouble(s)) {
+ this.textField.setEditableColor(0xe0e0e0);
+ this.valid = true;
+ } else {
+ this.textField.setEditableColor(0xff0000);
+ this.valid = false;
+ }
+ });
+
+ this.children.add(this.textField);
+ }
+
+ @Override
+ public void apply() {
+ if (valid) value.set(Double.parseDouble(textField.getText()));
+ }
+
+ @Override
+ public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickProgress) {
+ super.render(context, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickProgress);
+
+ this.textField.setX(x + entryWidth - 165);
+ this.textField.setY(y);
+ this.textField.render(context, mouseX, mouseY, tickProgress);
+ }
+}
diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/DoubleSliderWidget.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/DoubleSliderWidget.java
new file mode 100644
index 00000000..9078aee5
--- /dev/null
+++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/DoubleSliderWidget.java
@@ -0,0 +1,70 @@
+package dev.spiritstudios.specter.impl.config.client.gui.widget.gamerule;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.text.OrderedText;
+import net.minecraft.text.Text;
+
+import dev.spiritstudios.specter.api.config.Value;
+import dev.spiritstudios.specter.api.config.client.TabbedListConfigScreen;
+import dev.spiritstudios.specter.api.config.gui.DoubleSliderHint;
+import dev.spiritstudios.specter.api.gui.client.widget.SpecterSliderWidget;
+import dev.spiritstudios.specter.impl.config.DoubleRangeConstraint;
+
+public class DoubleSliderWidget extends TabbedListConfigScreen.ValueWidget {
+ private final SpecterSliderWidget slider;
+ private final Value value;
+
+ public DoubleSliderWidget(
+ MinecraftClient client,
+ @Nullable List description,
+ Text narration,
+ Text name,
+ Value value,
+ DoubleSliderHint sliderHint
+ ) {
+ super(client, description, name);
+
+ this.value = value;
+
+ double step = sliderHint.step();
+
+ Optional range = value.constraint(DoubleRangeConstraint.class);
+
+ double min = range.map(DoubleRangeConstraint::min).orElse(Double.MIN_VALUE);
+ double max = range.map(DoubleRangeConstraint::max).orElse(Double.MAX_VALUE);
+
+ this.slider = new SpecterSliderWidget.Builder(val -> Text.literal("%.2f".formatted(val)))
+ .initially(value.get())
+ .range(min, max)
+ .step(step)
+ .omitKeyText()
+ .build(
+ 10, 5,
+ 160, 20,
+ name
+ );
+
+
+ this.children.add(this.slider);
+ }
+
+ @Override
+ public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickProgress) {
+ super.render(context, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickProgress);
+
+ this.slider.setX(x + entryWidth - 165);
+ this.slider.setY(y);
+ this.slider.render(context, mouseX, mouseY, tickProgress);
+ }
+
+ @Override
+ public void apply() {
+ this.value.set(slider.getValue());
+ }
+}
diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/EnumValueWidget.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/EnumValueWidget.java
new file mode 100644
index 00000000..45036223
--- /dev/null
+++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/EnumValueWidget.java
@@ -0,0 +1,75 @@
+package dev.spiritstudios.specter.impl.config.client.gui.widget.gamerule;
+
+
+import java.util.List;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.widget.CyclingButtonWidget;
+import net.minecraft.text.OrderedText;
+import net.minecraft.text.Text;
+
+import dev.spiritstudios.specter.api.config.Value;
+import dev.spiritstudios.specter.api.config.client.TabbedListConfigScreen;
+
+public class EnumValueWidget> extends TabbedListConfigScreen.ValueWidget {
+ public static final TabbedListConfigScreen.ValueWidgetFactory> FACTORY = new TabbedListConfigScreen.ValueWidgetFactory<>() {
+ @Override
+ public TabbedListConfigScreen.ValueWidget create(MinecraftClient client, String translationPrefix, List description, Text narration, Text name, Value> value) {
+ return new EnumValueWidget<>(client, translationPrefix, description, narration, name, value);
+ }
+
+ @Override
+ public Text toString(String translationPrefix, Enum> value) {
+ return Text.translatable("%s.%s".formatted(translationPrefix, value.toString().toLowerCase()));
+ }
+ };
+
+
+ private final CyclingButtonWidget> button;
+ private final Value> value;
+
+ @SuppressWarnings("unchecked")
+ public EnumValueWidget(
+ MinecraftClient client,
+ String translationPrefix,
+ @Nullable List description,
+ Text narration,
+ Text name,
+ Value> value
+ ) {
+ super(client, description, name);
+
+ this.value = (Value>) (Object) value;
+
+ this.button = CyclingButtonWidget.>builder(val -> FACTORY.toString(translationPrefix, val))
+ .narration(button ->
+ button.getGenericNarrationMessage().append("\n").append(narration))
+ .values(this.value.defaultValue().getClass().getEnumConstants())
+ .initially(this.value.get())
+ .omitKeyText()
+ .build(
+ 10, 5,
+ 160, 20,
+ name
+ );
+
+ this.children.add(this.button);
+ }
+
+ @Override
+ public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickProgress) {
+ super.render(context, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickProgress);
+
+ this.button.setX(x + entryWidth - 165);
+ this.button.setY(y);
+ this.button.render(context, mouseX, mouseY, tickProgress);
+ }
+
+ @Override
+ public void apply() {
+ this.value.set(button.getValue());
+ }
+}
diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/FloatInputWidget.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/FloatInputWidget.java
new file mode 100644
index 00000000..24870faa
--- /dev/null
+++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/FloatInputWidget.java
@@ -0,0 +1,68 @@
+package dev.spiritstudios.specter.impl.config.client.gui.widget.gamerule;
+
+import java.util.List;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.widget.TextFieldWidget;
+import net.minecraft.text.OrderedText;
+import net.minecraft.text.Text;
+
+import dev.spiritstudios.specter.api.config.Value;
+import dev.spiritstudios.specter.api.config.client.TabbedListConfigScreen;
+import dev.spiritstudios.specter.api.core.math.SpecterMath;
+
+public class FloatInputWidget extends TabbedListConfigScreen.ValueWidget {
+ private final TextFieldWidget textField;
+ private final Value value;
+
+ private boolean valid = true;
+
+ public FloatInputWidget(
+ MinecraftClient client,
+ @Nullable List description,
+ Text narration,
+ Text name,
+ Value value
+ ) {
+ super(client, description, name);
+
+ this.value = value;
+
+ this.textField = new TextFieldWidget(
+ client.textRenderer,
+ 10, 5,
+ 160, 20,
+ name
+ );
+
+ this.textField.setText(Float.toString(value.get()));
+ this.textField.setChangedListener(s -> {
+ if (SpecterMath.canParseFloat(s)) {
+ this.textField.setEditableColor(0xe0e0e0);
+ this.valid = true;
+ } else {
+ this.textField.setEditableColor(0xff0000);
+ this.valid = false;
+ }
+ });
+
+ this.children.add(this.textField);
+ }
+
+ @Override
+ public void apply() {
+ if (valid) value.set(Float.parseFloat(textField.getText()));
+ }
+
+ @Override
+ public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickProgress) {
+ super.render(context, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickProgress);
+
+ this.textField.setX(x + entryWidth - 165);
+ this.textField.setY(y);
+ this.textField.render(context, mouseX, mouseY, tickProgress);
+ }
+}
diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/FloatSliderWidget.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/FloatSliderWidget.java
new file mode 100644
index 00000000..f4e352a6
--- /dev/null
+++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/FloatSliderWidget.java
@@ -0,0 +1,70 @@
+package dev.spiritstudios.specter.impl.config.client.gui.widget.gamerule;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.text.OrderedText;
+import net.minecraft.text.Text;
+
+import dev.spiritstudios.specter.api.config.Value;
+import dev.spiritstudios.specter.api.config.client.TabbedListConfigScreen;
+import dev.spiritstudios.specter.api.config.gui.FloatSliderHint;
+import dev.spiritstudios.specter.api.gui.client.widget.SpecterSliderWidget;
+import dev.spiritstudios.specter.impl.config.FloatRangeConstraint;
+
+public class FloatSliderWidget extends TabbedListConfigScreen.ValueWidget {
+ private final SpecterSliderWidget slider;
+ private final Value value;
+
+ public FloatSliderWidget(
+ MinecraftClient client,
+ @Nullable List description,
+ Text narration,
+ Text name,
+ Value value,
+ FloatSliderHint sliderHint
+ ) {
+ super(client, description, name);
+
+ this.value = value;
+
+ float step = sliderHint.step();
+
+ Optional range = value.constraint(FloatRangeConstraint.class);
+
+ float min = range.map(FloatRangeConstraint::min).orElse(Float.MIN_VALUE);
+ float max = range.map(FloatRangeConstraint::max).orElse(Float.MAX_VALUE);
+
+ this.slider = new SpecterSliderWidget.Builder(val -> Text.literal("%.1f".formatted(val)))
+ .initially(value.get())
+ .range(min, max)
+ .step(step)
+ .omitKeyText()
+ .build(
+ 10, 5,
+ 160, 20,
+ name
+ );
+
+
+ this.children.add(this.slider);
+ }
+
+ @Override
+ public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickProgress) {
+ super.render(context, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickProgress);
+
+ this.slider.setX(x + entryWidth - 165);
+ this.slider.setY(y);
+ this.slider.render(context, mouseX, mouseY, tickProgress);
+ }
+
+ @Override
+ public void apply() {
+ this.value.set((float) slider.getValue());
+ }
+}
diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/GameruleConfigWidgetFactories.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/GameruleConfigWidgetFactories.java
new file mode 100644
index 00000000..292d6157
--- /dev/null
+++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/GameruleConfigWidgetFactories.java
@@ -0,0 +1,32 @@
+package dev.spiritstudios.specter.impl.config.client.gui.widget.gamerule;
+
+import dev.spiritstudios.specter.api.config.client.TabbedListConfigScreen;
+import dev.spiritstudios.specter.api.config.gui.DoubleSliderHint;
+import dev.spiritstudios.specter.api.config.gui.FloatSliderHint;
+import dev.spiritstudios.specter.api.config.gui.IntSliderHint;
+import dev.spiritstudios.specter.api.config.gui.LongSliderHint;
+
+public final class GameruleConfigWidgetFactories {
+ public static final TabbedListConfigScreen.ValueWidgetFactory LONG = (client, translationPrefix, description, narration, name, value) -> value.hint(LongSliderHint.class)
+ .map(sliderHint ->
+ new LongSliderWidget(client, description, narration, name, value, sliderHint))
+ .orElseGet(() -> new LongInputWidget(client, description, narration, name, value));
+
+
+ public static final TabbedListConfigScreen.ValueWidgetFactory INTEGER = (client, translationPrefix, description, narration, name, value) -> value.hint(IntSliderHint.class)
+ .map(sliderHint ->
+ new IntegerSliderWidget(client, description, narration, name, value, sliderHint))
+ .orElseGet(() -> new IntegerInputWidget(client, description, narration, name, value));
+
+ public static final TabbedListConfigScreen.ValueWidgetFactory FLOAT = (client, translationPrefix, description, narration, name, value) -> value.hint(FloatSliderHint.class)
+ .map(sliderHint ->
+ new FloatSliderWidget(client, description, narration, name, value, sliderHint))
+ .orElseGet(() -> new FloatInputWidget(client, description, narration, name, value));
+
+ public static final TabbedListConfigScreen.ValueWidgetFactory DOUBLE = (client, translationPrefix, description, narration, name, value) -> value.hint(DoubleSliderHint.class)
+ .map(sliderHint ->
+ new DoubleSliderWidget(client, description, narration, name, value, sliderHint))
+ .orElseGet(() -> new DoubleInputWidget(client, description, narration, name, value));
+
+ public static final TabbedListConfigScreen.ValueWidgetFactory STRING = StringInputWidget::new;
+}
diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/IntegerInputWidget.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/IntegerInputWidget.java
new file mode 100644
index 00000000..1778cc1a
--- /dev/null
+++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/IntegerInputWidget.java
@@ -0,0 +1,68 @@
+package dev.spiritstudios.specter.impl.config.client.gui.widget.gamerule;
+
+import java.util.List;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.widget.TextFieldWidget;
+import net.minecraft.text.OrderedText;
+import net.minecraft.text.Text;
+
+import dev.spiritstudios.specter.api.config.Value;
+import dev.spiritstudios.specter.api.config.client.TabbedListConfigScreen;
+import dev.spiritstudios.specter.api.core.math.SpecterMath;
+
+public class IntegerInputWidget extends TabbedListConfigScreen.ValueWidget {
+ private final TextFieldWidget textField;
+ private final Value value;
+
+ private boolean valid = true;
+
+ public IntegerInputWidget(
+ MinecraftClient client,
+ @Nullable List description,
+ Text narration,
+ Text name,
+ Value value
+ ) {
+ super(client, description, name);
+
+ this.value = value;
+
+ this.textField = new TextFieldWidget(
+ client.textRenderer,
+ 10, 5,
+ 160, 20,
+ name
+ );
+
+ this.textField.setText(Integer.toString(value.get()));
+ this.textField.setChangedListener(s -> {
+ if (SpecterMath.canParseInteger(s)) {
+ this.textField.setEditableColor(0xe0e0e0);
+ this.valid = true;
+ } else {
+ this.textField.setEditableColor(0xff0000);
+ this.valid = false;
+ }
+ });
+
+ this.children.add(this.textField);
+ }
+
+ @Override
+ public void apply() {
+ if (valid) value.set(Integer.parseInt(textField.getText()));
+ }
+
+ @Override
+ public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickProgress) {
+ super.render(context, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickProgress);
+
+ this.textField.setX(x + entryWidth - 165);
+ this.textField.setY(y);
+ this.textField.render(context, mouseX, mouseY, tickProgress);
+ }
+}
diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/IntegerSliderWidget.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/IntegerSliderWidget.java
new file mode 100644
index 00000000..20b7769d
--- /dev/null
+++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/IntegerSliderWidget.java
@@ -0,0 +1,70 @@
+package dev.spiritstudios.specter.impl.config.client.gui.widget.gamerule;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.text.OrderedText;
+import net.minecraft.text.Text;
+
+import dev.spiritstudios.specter.api.config.Value;
+import dev.spiritstudios.specter.api.config.client.TabbedListConfigScreen;
+import dev.spiritstudios.specter.api.config.gui.IntSliderHint;
+import dev.spiritstudios.specter.api.gui.client.widget.SpecterSliderWidget;
+import dev.spiritstudios.specter.impl.config.IntegerRangeConstraint;
+
+public class IntegerSliderWidget extends TabbedListConfigScreen.ValueWidget {
+ private final SpecterSliderWidget slider;
+ private final Value value;
+
+ public IntegerSliderWidget(
+ MinecraftClient client,
+ @Nullable List description,
+ Text narration,
+ Text name,
+ Value value,
+ IntSliderHint sliderHint
+ ) {
+ super(client, description, name);
+
+ this.value = value;
+
+ int step = sliderHint.step();
+
+ Optional range = value.constraint(IntegerRangeConstraint.class);
+
+ int min = range.map(IntegerRangeConstraint::min).orElse(Integer.MIN_VALUE);
+ int max = range.map(IntegerRangeConstraint::max).orElse(Integer.MAX_VALUE);
+
+ this.slider = new SpecterSliderWidget.Builder(val -> Text.literal(String.valueOf((int) val)))
+ .initially(value.get())
+ .range(min, max)
+ .step(step)
+ .omitKeyText()
+ .build(
+ 10, 5,
+ 160, 20,
+ name
+ );
+
+
+ this.children.add(this.slider);
+ }
+
+ @Override
+ public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickProgress) {
+ super.render(context, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickProgress);
+
+ this.slider.setX(x + entryWidth - 165);
+ this.slider.setY(y);
+ this.slider.render(context, mouseX, mouseY, tickProgress);
+ }
+
+ @Override
+ public void apply() {
+ this.value.set((int) slider.getValue());
+ }
+}
diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/LongInputWidget.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/LongInputWidget.java
new file mode 100644
index 00000000..57299d21
--- /dev/null
+++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/LongInputWidget.java
@@ -0,0 +1,68 @@
+package dev.spiritstudios.specter.impl.config.client.gui.widget.gamerule;
+
+import java.util.List;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.widget.TextFieldWidget;
+import net.minecraft.text.OrderedText;
+import net.minecraft.text.Text;
+
+import dev.spiritstudios.specter.api.config.Value;
+import dev.spiritstudios.specter.api.config.client.TabbedListConfigScreen;
+import dev.spiritstudios.specter.api.core.math.SpecterMath;
+
+public class LongInputWidget extends TabbedListConfigScreen.ValueWidget {
+ private final TextFieldWidget textField;
+ private final Value value;
+
+ private boolean valid = true;
+
+ public LongInputWidget(
+ MinecraftClient client,
+ @Nullable List description,
+ Text narration,
+ Text name,
+ Value value
+ ) {
+ super(client, description, name);
+
+ this.value = value;
+
+ this.textField = new TextFieldWidget(
+ client.textRenderer,
+ 10, 5,
+ 160, 20,
+ name
+ );
+
+ this.textField.setText(Long.toString(value.get()));
+ this.textField.setChangedListener(s -> {
+ if (SpecterMath.canParseLong(s)) {
+ this.textField.setEditableColor(0xe0e0e0);
+ this.valid = true;
+ } else {
+ this.textField.setEditableColor(0xff0000);
+ this.valid = false;
+ }
+ });
+
+ this.children.add(this.textField);
+ }
+
+ @Override
+ public void apply() {
+ if (valid) value.set(Long.parseLong(textField.getText()));
+ }
+
+ @Override
+ public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickProgress) {
+ super.render(context, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickProgress);
+
+ this.textField.setX(x + entryWidth - 165);
+ this.textField.setY(y);
+ this.textField.render(context, mouseX, mouseY, tickProgress);
+ }
+}
diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/LongSliderWidget.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/LongSliderWidget.java
new file mode 100644
index 00000000..bf8b569d
--- /dev/null
+++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/LongSliderWidget.java
@@ -0,0 +1,67 @@
+package dev.spiritstudios.specter.impl.config.client.gui.widget.gamerule;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.text.OrderedText;
+import net.minecraft.text.Text;
+
+import dev.spiritstudios.specter.api.config.Value;
+import dev.spiritstudios.specter.api.config.client.TabbedListConfigScreen;
+import dev.spiritstudios.specter.api.config.gui.LongSliderHint;
+import dev.spiritstudios.specter.api.gui.client.widget.SpecterSliderWidget;
+import dev.spiritstudios.specter.impl.config.LongRangeConstraint;
+
+public class LongSliderWidget extends TabbedListConfigScreen.ValueWidget {
+ private final SpecterSliderWidget slider;
+ private final Value value;
+
+ public LongSliderWidget(
+ MinecraftClient client,
+ @Nullable List description,
+ Text narration,
+ Text name,
+ Value value,
+ LongSliderHint sliderHint
+ ) {
+ super(client, description, name);
+
+ this.value = value;
+
+ Optional range = value.constraint(LongRangeConstraint.class);
+
+ long min = range.map(LongRangeConstraint::min).orElse(Long.MIN_VALUE);
+ long max = range.map(LongRangeConstraint::max).orElse(Long.MAX_VALUE);
+
+ this.slider = new SpecterSliderWidget.Builder(val -> Text.literal(String.valueOf((int) val)))
+ .initially(value.get())
+ .range(min, max)
+ .step(sliderHint.step())
+ .omitKeyText()
+ .build(
+ 10, 5,
+ 160, 20,
+ name
+ );
+
+ this.children.add(this.slider);
+ }
+
+ @Override
+ public void apply() {
+ this.value.set((long) this.slider.getValue());
+ }
+
+ @Override
+ public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickProgress) {
+ super.render(context, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickProgress);
+
+ this.slider.setX(x + entryWidth - 165);
+ this.slider.setY(y);
+ this.slider.render(context, mouseX, mouseY, tickProgress);
+ }
+}
diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/StringInputWidget.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/StringInputWidget.java
new file mode 100644
index 00000000..c062a5af
--- /dev/null
+++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/client/gui/widget/gamerule/StringInputWidget.java
@@ -0,0 +1,68 @@
+package dev.spiritstudios.specter.impl.config.client.gui.widget.gamerule;
+
+import java.util.List;
+
+import org.jetbrains.annotations.Nullable;
+
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.widget.TextFieldWidget;
+import net.minecraft.text.OrderedText;
+import net.minecraft.text.Text;
+
+import dev.spiritstudios.specter.api.config.Value;
+import dev.spiritstudios.specter.api.config.client.TabbedListConfigScreen;
+
+public class StringInputWidget extends TabbedListConfigScreen.ValueWidget {
+ private final TextFieldWidget textField;
+ private final Value value;
+
+ private boolean valid = true;
+
+ public StringInputWidget(
+ MinecraftClient client,
+ String translationPrefix,
+ @Nullable List description,
+ Text narration,
+ Text name,
+ Value value
+ ) {
+ super(client, description, name);
+
+ this.value = value;
+
+ this.textField = new TextFieldWidget(
+ client.textRenderer,
+ 10, 5,
+ 160, 20,
+ name
+ );
+
+ this.textField.setText(value.get());
+ this.textField.setChangedListener(s -> {
+ if (value.checkConstraints(s).isSuccess()) {
+ this.textField.setEditableColor(0xe0e0e0);
+ this.valid = true;
+ } else {
+ this.textField.setEditableColor(0xff0000);
+ this.valid = false;
+ }
+ });
+
+ this.children.add(this.textField);
+ }
+
+ @Override
+ public void apply() {
+ if (valid) value.set(textField.getText());
+ }
+
+ @Override
+ public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickProgress) {
+ super.render(context, index, y, x, entryWidth, entryHeight, mouseX, mouseY, hovered, tickProgress);
+
+ this.textField.setX(x + entryWidth - 165);
+ this.textField.setY(y);
+ this.textField.render(context, mouseX, mouseY, tickProgress);
+ }
+}
diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/Config.java b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/Config.java
index 2a2d8989..a7b4c05b 100644
--- a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/Config.java
+++ b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/Config.java
@@ -1,24 +1,23 @@
package dev.spiritstudios.specter.api.config;
+import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
-import java.util.Objects;
+import java.util.Map;
+import java.util.Optional;
-import com.mojang.datafixers.util.Pair;
+import com.google.common.collect.ImmutableMap;
+import com.mojang.datafixers.util.Either;
import com.mojang.serialization.Codec;
-import com.mojang.serialization.DataResult;
-import com.mojang.serialization.DynamicOps;
-import com.mojang.serialization.RecordBuilder;
-import io.netty.buffer.ByteBuf;
-import org.jetbrains.annotations.ApiStatus;
+import org.jetbrains.annotations.Nullable;
-import net.minecraft.network.codec.PacketCodec;
import net.minecraft.network.codec.PacketCodecs;
import net.minecraft.registry.Registry;
import net.minecraft.util.Identifier;
-import dev.spiritstudios.specter.api.core.SpecterGlobals;
+import dev.spiritstudios.specter.api.config.gui.GuiHint;
import dev.spiritstudios.specter.api.core.reflect.Ignore;
import dev.spiritstudios.specter.api.core.reflect.ReflectionHelper;
import dev.spiritstudios.specter.api.core.util.SpecterPacketCodecs;
@@ -31,11 +30,12 @@
* You can use the provided static methods to create values of different types, or create your own with the {@link #value(Object, Codec)} method.
* Once you have created your configuration class, you can save and load it using {@link ConfigHolder}.
*
- *
- * @param The type of the configuration class. This must be the same as the class that extends this class.
*/
-public abstract class Config> implements Codec {
- private List>> fields;
+public abstract class Config {
+ private @Nullable Map, SubConfig>> values = null;
+ private @Nullable Boolean shouldSync = null;
+
+ // region Value builders
/**
* Creates a new value with the given default value and codec.
@@ -58,7 +58,8 @@ protected static Value.Builder value(T defaultValue, Codec codec) {
* @return A new value builder.
*/
protected static > Value.Builder enumValue(T defaultValue, Class clazz) {
- return value(defaultValue, SpecterCodecs.enumCodec(clazz)).packetCodec(SpecterPacketCodecs.enumCodec(clazz));
+ return value(defaultValue, SpecterCodecs.enumCodec(clazz))
+ .packetCodec(SpecterPacketCodecs.enumCodec(clazz).cast());
}
/**
@@ -69,49 +70,40 @@ protected static > Value.Builder enumValue(T defaultValue,
* @return A new value builder.
*/
protected static Value.Builder booleanValue(boolean defaultValue) {
- return value(defaultValue, Codec.BOOL).packetCodec(PacketCodecs.BOOLEAN);
+ return value(defaultValue, Codec.BOOL).packetCodec(PacketCodecs.BOOLEAN.cast());
}
/**
- * Creates a new integer value with the given default value and a default range of 0 to 100.
+ * Creates a new integer value with the given default value.
* The codec and packet codec are set to {@link Codec#INT} and {@link PacketCodecs#INTEGER} respectively.
*
* @param defaultValue The default value.
* @return A new value builder.
*/
- protected static NumericValue.Builder intValue(int defaultValue) {
- return new NumericValue.Builder<>(defaultValue, Codec.INT)
- .codecRange(SpecterCodecs::clampedRange)
- .range(0, 100)
- .packetCodec(PacketCodecs.INTEGER);
+ protected static Value.Builder intValue(int defaultValue) {
+ return new Value.Builder<>(defaultValue, Codec.INT).packetCodec(PacketCodecs.INTEGER.cast());
}
/**
- * Creates a new float value with the given default value and a default range of 0 to 1.
+ * Creates a new float value with the given default value.
* The codec and packet codec are set to {@link Codec#FLOAT} and {@link PacketCodecs#FLOAT} respectively.
*
* @param defaultValue The default value.
* @return A new value builder.
*/
- protected static NumericValue.Builder floatValue(float defaultValue) {
- return new NumericValue.Builder<>(defaultValue, Codec.FLOAT)
- .codecRange(SpecterCodecs::clampedRange)
- .range(0.0F, 1.0F)
- .packetCodec(PacketCodecs.FLOAT);
+ protected static Value.Builder floatValue(float defaultValue) {
+ return new Value.Builder<>(defaultValue, Codec.FLOAT).packetCodec(PacketCodecs.FLOAT.cast());
}
/**
- * Creates a new double value with the given default value and a default range of 0 to 1.
+ * Creates a new double value with the given default value.
* The codec and packet codec are set to {@link Codec#DOUBLE} and {@link PacketCodecs#DOUBLE} respectively.
*
* @param defaultValue The default value.
* @return A new value builder.
*/
- protected static NumericValue.Builder doubleValue(double defaultValue) {
- return new NumericValue.Builder<>(defaultValue, Codec.DOUBLE)
- .codecRange(SpecterCodecs::clampedRange)
- .range(0.0, 1.0)
- .packetCodec(PacketCodecs.DOUBLE);
+ protected static Value.Builder doubleValue(double defaultValue) {
+ return new Value.Builder<>(defaultValue, Codec.DOUBLE).packetCodec(PacketCodecs.DOUBLE.cast());
}
/**
@@ -122,7 +114,7 @@ protected static NumericValue.Builder doubleValue(double defaultValue) {
* @return A new value builder.
*/
protected static Value.Builder stringValue(String defaultValue) {
- return new Value.Builder<>(defaultValue, Codec.STRING).packetCodec(PacketCodecs.STRING);
+ return new Value.Builder<>(defaultValue, Codec.STRING).packetCodec(PacketCodecs.STRING.cast());
}
/**
@@ -135,79 +127,98 @@ protected static Value.Builder stringValue(String defaultValue) {
* @return A new value builder.
*/
protected static Value.Builder registryValue(T defaultValue, Registry registry) {
- return value(defaultValue, registry.getCodec());
+ return value(defaultValue, registry.getCodec())
+ .packetCodec(PacketCodecs.registryValue(registry.getKey()));
}
+ // endregion
+
+ public boolean shouldSync() {
+ if (shouldSync == null) {
+ shouldSync = values().values()
+ .stream()
+ .anyMatch(either -> either.map(
+ Value::sync,
+ Config::shouldSync
+ ));
+ }
- /**
- * Creates a new nested config value with the given class.
- *
- * @param clazz The class of the nested config.
- * @param The type of the nested config.
- * @return A new nested value builder.
- */
- protected static > Value.NestedBuilder nestedValue(Class clazz) {
- return new Value.NestedBuilder<>(clazz);
+ return shouldSync;
}
- @ApiStatus.Internal
- public List>> fields() {
- if (fields == null) {
- fields = Arrays.stream(this.getClass().getDeclaredFields()).map(field -> {
- if (field.isAnnotationPresent(Ignore.class)) return null;
+ public Map, SubConfig>> values() {
+ if (this.values == null) {
+ ImmutableMap.Builder, SubConfig>> builder = ImmutableMap.builder();
+
+ List fields = Arrays.stream(this.getClass().getDeclaredFields())
+ .filter(field ->
+ Value.class.isAssignableFrom(field.getType()) ||
+ SubConfig.class.isAssignableFrom(field.getType()))
+ .filter(field -> !field.isAnnotationPresent(Ignore.class))
+ .filter(field -> !Modifier.isStatic(field.getModifiers()) &&
+ Modifier.isFinal(field.getModifiers()) &&
+ !Modifier.isTransient(field.getModifiers()) &&
+ !field.isSynthetic())
+ .toList();
+
+ for (Field field : fields) {
+ Optional