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 children() { + return this.children; + } + + @Override + public List 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 fieldValue = ReflectionHelper.getFieldValue(this, field); + if (fieldValue.isEmpty()) continue; + + if (fieldValue.get() instanceof Value value) { + builder.put(field.getName(), Either.left(value)); + } else if (fieldValue.get() instanceof SubConfig subConfig) { + builder.put(field.getName(), Either.right(subConfig)); + } + } + + this.values = builder.build(); + } + + return values; + } - if (!Value.class.isAssignableFrom(field.getType()) || - Modifier.isStatic(field.getModifiers()) || - !Modifier.isFinal(field.getModifiers())) return null; + /** + * A config that can be nested inside another config. + */ + public abstract static class SubConfig extends Config { + private final Map, GuiHint> guiHints; + private final @Nullable String comment; - Value value = ReflectionHelper.getFieldValue(this, field); - if (value == null) return null; + public SubConfig(@Nullable String comment) { + this.guiHints = Collections.emptyMap(); + this.comment = comment; + } - return new ReflectionHelper.FieldValuePair>( - field, - value - ); - }).filter(Objects::nonNull).toList(); + @SafeVarargs + public SubConfig(GuiHint... guiHints) { + this(null, guiHints); } - return fields; - } - @ApiStatus.Internal - @SuppressWarnings("unchecked") - public PacketCodec packetCodec() { - return PacketCodec.of( - (value, buf) -> value.fields().forEach(pair -> { - if (!pair.value().sync()) return; - pair.value().packetEncode(buf); - }), - (buf) -> { - fields().forEach(pair -> { - if (!pair.value().sync()) return; - pair.value().packetDecode(buf); - }); - return (T) this; + @SafeVarargs + public SubConfig(@Nullable String comment, GuiHint... guiHints) { + if (guiHints.length == 0) { + this.guiHints = Collections.emptyMap(); + } else { + ImmutableMap.Builder, GuiHint> builder = ImmutableMap.builder(); + + for (GuiHint guiHint : guiHints) { + builder.put(guiHint.getClass(), guiHint); + } + + this.guiHints = builder.build(); } - ); - } - @Override - public DataResult encode(T input, DynamicOps ops, T1 prefix) { - RecordBuilder builder = ops.mapBuilder(); - for (ReflectionHelper.FieldValuePair> value : fields()) builder = value.value().encode(ops, builder); - return builder.build(prefix); - } + this.comment = comment; + } - @Override - @SuppressWarnings("unchecked") - public DataResult> decode(DynamicOps ops, T1 input) { - for (ReflectionHelper.FieldValuePair> pair : fields()) { - if (pair.value().decode(ops, input)) continue; - SpecterGlobals.LOGGER.error( - "Failed to decode config value \"{}\". Resetting to default value", - pair.value().name() - ); - pair.value().reset(); + public Optional comment() { + return Optional.ofNullable(comment); } - return DataResult.success(Pair.of((T) this, input)); + public > Optional hint(Class clazz) { + GuiHint guiHint = guiHints.get(clazz); + if (guiHint == null) return Optional.empty(); + return ReflectionHelper.cast(guiHint, clazz); + } } } diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/ConfigHolder.java b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/ConfigHolder.java index 3f830289..fc1eb656 100644 --- a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/ConfigHolder.java +++ b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/ConfigHolder.java @@ -8,9 +8,10 @@ import java.util.Arrays; import java.util.List; +import com.mojang.datafixers.util.Pair; import com.mojang.serialization.DataResult; -import io.netty.buffer.ByteBuf; -import org.jetbrains.annotations.ApiStatus; +import com.mojang.serialization.DynamicOps; +import com.mojang.serialization.RecordBuilder; import net.minecraft.util.Identifier; @@ -28,7 +29,7 @@ * @param The type of the config. * @param The type of the format used for serialization. */ -public class ConfigHolder, F> { +public class ConfigHolder { private final T config; private final Identifier id; private final String path; @@ -39,8 +40,9 @@ protected ConfigHolder(DynamicFormat language, Identifier id, String path, Cl this.id = id; this.path = path; - if (NestedConfig.class.isAssignableFrom(clazz)) + if (Config.SubConfig.class.isAssignableFrom(clazz)) { throw new IllegalArgumentException("Nested configs cannot be registered with config holders"); + } ConfigHolder existing = ConfigHolderRegistry.get(id); if (existing != null) throw new IllegalStateException("Config with id %s already exists".formatted(id)); @@ -49,15 +51,11 @@ protected ConfigHolder(DynamicFormat language, Identifier id, String path, Cl this.config = ReflectionHelper.instantiate(clazz); - this.config.fields().forEach(pair -> { - pair.value().init(pair.field().getName()); - SpecterGlobals.debug("Registered config value: %s".formatted(pair.value().translationKey(id))); - }); - - if (!load()) + if (!load()) { SpecterGlobals.LOGGER.error("Failed to load config file: {}, default values will be used", path()); - else + } else { save(); // Save the config to disk to ensure it's up to date + } } /** @@ -68,7 +66,7 @@ protected ConfigHolder(DynamicFormat language, Identifier id, String path, Cl * @param The type of the config. * @return A new config holder builder. */ - public static > Builder builder(Identifier id, Class clazz) { + public static Builder builder(Identifier id, Class clazz) { return new Builder<>(id, clazz); } @@ -77,7 +75,10 @@ public static > Builder builder(Identifier id, Class c */ @SuppressWarnings("ResultOfMethodCallIgnored") public void save() { - DataResult result = config.encodeStart(format, config); + RecordBuilder builder = format.mapBuilder(); + this.encodeConfig(builder, config); + + DataResult result = builder.build(format.empty()); if (result.error().isPresent()) { SpecterGlobals.LOGGER.error("Failed to encode config: {}", id); @@ -138,25 +139,75 @@ public boolean load() { return false; } - DataResult result = config.parse(format, element); - if (result.error().isPresent()) { - SpecterGlobals.LOGGER.error("Failed to decode config file: {}", path()); - SpecterGlobals.LOGGER.error(result.error().toString()); - - return false; - } + this.decodeConfig(element, config); return true; } + private void encodeConfig(RecordBuilder builder, Config config) { + config.values().forEach((key, either) -> { + either + .ifLeft(value -> encodeField(format, builder, key, value)) + .ifRight(subConfig -> { + RecordBuilder subConfigBuilder = format.mapBuilder(); + encodeConfig(subConfigBuilder, subConfig); + builder.add(key, subConfigBuilder.build(format.empty())); + }); + }); + } + + + private void decodeConfig(F input, Config config) { + config.values().forEach((key, either) -> { + either + .ifLeft(value -> decodeField(format, input, key, value)) + .ifRight(subConfig -> { + format.getMap(input) + .ifSuccess(map -> decodeConfig(map.get(key), subConfig)) + .ifError(error -> { + SpecterGlobals.LOGGER.error( + "Failed to decode config value \"{}\". Something is very wrong.", + key + ); + SpecterGlobals.LOGGER.error(error.message()); + }); + }); + }); + } + + private void encodeField(DynamicOps ops, RecordBuilder builder, String key, Value value) { + builder.add(key, value.codec().encodeStart(ops, value.get())); + } + + private void decodeField(DynamicOps ops, T1 input, String key, Value value) { + value.set(ops.getMap(input).flatMap(map -> value.codec().decode(ops, map.get(key))) + .mapOrElse( + Pair::getFirst, + error -> { + SpecterGlobals.LOGGER.error( + "Failed to decode config value \"{}\". Resetting to default value", + key + ); + SpecterGlobals.LOGGER.error(error.message()); + + return value.defaultValue(); + } + )); + } + private Path path() { return Paths.get( - FabricLoader.getInstance().getConfigDir().toString(), - "", - path + FabricLoader.getInstance().getConfigDir().toString(), + "", + path ); } + @Override + public String toString() { + return "ConfigHolder[" + id.toString() + "]"; + } + /** * Get the config. * @@ -175,31 +226,13 @@ public Identifier id() { return id; } - @ApiStatus.Internal - public ConfigHolder packetDecode(ByteBuf buf) { - config.fields().forEach(pair -> { - if (!pair.value().sync()) return; - pair.value().packetDecode(buf); - }); - - return this; - } - - @ApiStatus.Internal - public void packetEncode(ByteBuf buf) { - config.fields().forEach(pair -> { - if (!pair.value().sync()) return; - pair.value().packetEncode(buf); - }); - } - /** * A builder for a new config holder. * The format will default to {@link JsonCFormat}. * * @param The type of the config. */ - public static class Builder> { + public static class Builder { private final Identifier id; private final Class clazz; diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/Constraint.java b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/Constraint.java new file mode 100644 index 00000000..b1c44771 --- /dev/null +++ b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/Constraint.java @@ -0,0 +1,29 @@ +package dev.spiritstudios.specter.api.config; + +import com.mojang.serialization.DataResult; + +import dev.spiritstudios.specter.impl.config.DoubleRangeConstraint; +import dev.spiritstudios.specter.impl.config.FloatRangeConstraint; +import dev.spiritstudios.specter.impl.config.IntegerRangeConstraint; +import dev.spiritstudios.specter.impl.config.LongRangeConstraint; + +@FunctionalInterface +public interface Constraint { + static Constraint range(int min, int max) { + return new IntegerRangeConstraint(min, max); + } + + static Constraint range(float min, float max) { + return new FloatRangeConstraint(min, max); + } + + static Constraint range(double min, double max) { + return new DoubleRangeConstraint(min, max); + } + + static Constraint range(long min, long max) { + return new LongRangeConstraint(min, max); + } + + DataResult test(T value); +} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/NestedConfig.java b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/NestedConfig.java deleted file mode 100644 index 29888d2c..00000000 --- a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/NestedConfig.java +++ /dev/null @@ -1,10 +0,0 @@ -package dev.spiritstudios.specter.api.config; - -/** - * A config that can be nested inside another config. - * This is effectively a marker class. - * - * @param - */ -public abstract class NestedConfig> extends Config { -} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/NumericValue.java b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/NumericValue.java deleted file mode 100644 index 5ee65e1e..00000000 --- a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/NumericValue.java +++ /dev/null @@ -1,96 +0,0 @@ -package dev.spiritstudios.specter.api.config; - -import java.util.List; -import java.util.Optional; -import java.util.function.BiFunction; - -import com.mojang.serialization.Codec; -import io.netty.buffer.ByteBuf; - -import net.minecraft.network.codec.PacketCodec; - -import dev.spiritstudios.specter.api.core.math.Range; -import dev.spiritstudios.specter.impl.config.NumericValueImpl; - -/** - * A config value of a numeric type. - * - * @param The type of the numeric value. - */ -public interface NumericValue> extends Value { - Range range(); - - double step(); - - class Builder> { - protected final T defaultValue; - protected final Codec codec; - protected String comment; - protected boolean sync; - protected PacketCodec packetCodec; - protected Range range; - protected double step; - protected BiFunction> codecRange; - - public Builder(T defaultValue, Codec codec) { - this.defaultValue = defaultValue; - this.codec = codec; - } - - public NumericValue.Builder codecRange(BiFunction> codecRange) { - this.codecRange = codecRange; - return this; - } - - public NumericValue.Builder comment(String comment) { - this.comment = comment; - return this; - } - - public NumericValue.Builder range(T min, T max) { - this.range = new Range<>(min, max); - return this; - } - - public NumericValue.Builder range(Range range) { - this.range = range; - return this; - } - - public NumericValue.Builder step(double step) { - this.step = step; - - return this; - } - - public NumericValue.Builder sync() { - if (packetCodec == null) throw new IllegalStateException("Packet codec must be set to enable syncing"); - this.sync = true; - return this; - } - - public NumericValue.Builder packetCodec(PacketCodec packetCodec) { - this.packetCodec = packetCodec; - return this; - } - - public Value.Builder> toList() { - return new Value.Builder<>(List.of(defaultValue), Codec.list(codec)); - } - - public NumericValue build() { - Codec rangeCodec = range == null ? codec : - Optional.ofNullable(codecRange).map(function -> function.apply(range.min(), range.max())).orElse(codec); - - return new NumericValueImpl<>( - defaultValue, - rangeCodec, - packetCodec, - comment, - sync, - range, - range == null ? 0 : step / (range.max().doubleValue() - range.min().doubleValue()) - ); - } - } -} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/Value.java b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/Value.java index b0ded546..137d3cdb 100644 --- a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/Value.java +++ b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/Value.java @@ -1,66 +1,156 @@ package dev.spiritstudios.specter.api.config; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import com.mojang.serialization.Codec; -import com.mojang.serialization.DynamicOps; -import com.mojang.serialization.RecordBuilder; -import io.netty.buffer.ByteBuf; -import org.jetbrains.annotations.ApiStatus; +import com.mojang.serialization.DataResult; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import org.jetbrains.annotations.Nullable; +import net.minecraft.network.RegistryByteBuf; import net.minecraft.network.codec.PacketCodec; import net.minecraft.util.Identifier; +import dev.spiritstudios.specter.api.config.gui.GuiHint; +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.ValueImpl; +import dev.spiritstudios.specter.api.serialization.CommentedCodec; /** * A config value. * * @param The type of the value. */ -public interface Value { - T get(); +public class Value { + private final T defaultValue; + private final Codec codec; + private final @Nullable PacketCodec packetCodec; + private final boolean sync; + private final @Nullable String comment; + private final Map, Constraint> constraints; + private final Map, GuiHint> guiHints; + + private @Nullable T override; + private T value; + + protected Value( + T defaultValue, + Codec codec, + @Nullable PacketCodec packetCodec, + @Nullable String comment, + boolean sync, + Map, Constraint> constraints, Map, GuiHint> guiHints + ) { + this.defaultValue = defaultValue; + this.comment = comment; + this.sync = sync; + this.packetCodec = packetCodec; + this.constraints = constraints.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(constraints); + this.guiHints = guiHints.isEmpty() ? Collections.emptyMap() : Collections.unmodifiableMap(guiHints); + + this.codec = new CommentedCodec<>(codec, comment); + this.value = defaultValue; + } - T defaultValue(); + public static String translationKey(String key, Identifier configId) { + return String.format("config.%s.%s", configId.toTranslationKey(), key); + } - void set(T value); + public static String translationKey(String key, String configId) { + return String.format("config.%s.%s", configId, key); + } - default void reset() { - set(defaultValue()); + public DataResult checkConstraints(T value) { + List errors = new ArrayList<>(); + + for (Constraint constraint : constraints.values()) { + DataResult newResult = constraint.test(value); + newResult.ifError(error -> errors.add(error.message())); + } + + if (errors.isEmpty()) return DataResult.success(value); + return DataResult.error(() -> String.join("\n", errors)); } - @ApiStatus.Internal - void init(String name); + public T get() { + return override != null ? override : value; + } - RecordBuilder encode(DynamicOps ops, RecordBuilder builder); + public T defaultValue() { + return defaultValue; + } - boolean decode(DynamicOps ops, T1 input); + public void set(T value) { + DataResult result = checkConstraints(value); + if (result.isError()) { + result.ifError(error -> + SpecterGlobals.LOGGER.error(error.message())); - void packetDecode(ByteBuf buf); + return; + } - void packetEncode(ByteBuf buf); + this.value = value; + } + + /** + * Override the current value with a value that will not be saved. + * This should generally only be used on client sided values, as when + * syncing, this is used to store the server side value. + *

+ * On the client, this value will be cleared when the player leaves a world. + */ + public void override(@Nullable T value) { + this.override = value; + } + + public Codec codec() { + return codec; + } + + public Optional> packetCodec() { + return Optional.ofNullable(packetCodec); + } - Optional comment(); + public Optional comment() { + return Optional.ofNullable(comment); + } - boolean sync(); + public boolean sync() { + return sync; + } - String translationKey(String configId); + public > Optional constraint(Class clazz) { + Constraint constraint = constraints.get(clazz); + if (constraint == null) return Optional.empty(); + return ReflectionHelper.cast(constraint, clazz); + } - default String translationKey(Identifier configId) { - return translationKey(configId.toTranslationKey()); + public > Optional hint(Class clazz) { + GuiHint guiHint = guiHints.get(clazz); + if (guiHint == null) return Optional.empty(); + return ReflectionHelper.cast(guiHint, clazz); } - String name(); + // this is checked by the isAssignableFrom, javac is just dumb + @SuppressWarnings("unchecked") + public Optional> cast(Class clazz) { + return clazz.isAssignableFrom(defaultValue.getClass()) ? + Optional.of((Value) this) : + Optional.empty(); + } - class Builder { + public static class Builder { protected final T defaultValue; protected final Codec codec; protected String comment; protected boolean sync; - protected PacketCodec packetCodec; + protected PacketCodec packetCodec; + protected Map, Constraint> constraints = new Object2ObjectOpenHashMap<>(); + protected Map, GuiHint> guiHints = new Object2ObjectOpenHashMap<>(); public Builder(T defaultValue, Codec codec) { this.defaultValue = defaultValue; @@ -78,41 +168,23 @@ public Builder sync() { return this; } - public Builder packetCodec(PacketCodec packetCodec) { + public Builder packetCodec(PacketCodec packetCodec) { this.packetCodec = packetCodec; return this; } - public Builder> toList() { - return new Builder<>(List.of(defaultValue), Codec.list(codec)); - } - - public Value build() { - return new ValueImpl<>(defaultValue, codec, packetCodec, comment, sync); - } - } - - class NestedBuilder> { - protected final T defaultValue; - protected String comment; - protected boolean sync; - - public NestedBuilder(Class clazz) { - defaultValue = ReflectionHelper.instantiate(clazz); - } - - public NestedBuilder comment(String comment) { - this.comment = comment; + public Builder constrain(Constraint constraint) { + constraints.put(constraint.getClass(), constraint); return this; } - public NestedBuilder sync() { - this.sync = true; + public Builder guiHint(GuiHint guiHint) { + guiHints.put(guiHint.getClass(), guiHint); return this; } public Value build() { - return new NestedConfigValue<>(defaultValue, sync, comment); + return new Value<>(defaultValue, codec, packetCodec, comment, sync, constraints, guiHints); } } } diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/gui/DoubleSliderHint.java b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/gui/DoubleSliderHint.java new file mode 100644 index 00000000..851c701a --- /dev/null +++ b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/gui/DoubleSliderHint.java @@ -0,0 +1,4 @@ +package dev.spiritstudios.specter.api.config.gui; + +public record DoubleSliderHint(double step) implements GuiHint { +} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/gui/FloatSliderHint.java b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/gui/FloatSliderHint.java new file mode 100644 index 00000000..3748f495 --- /dev/null +++ b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/gui/FloatSliderHint.java @@ -0,0 +1,4 @@ +package dev.spiritstudios.specter.api.config.gui; + +public record FloatSliderHint(float step) implements GuiHint { +} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/gui/GuiHint.java b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/gui/GuiHint.java new file mode 100644 index 00000000..8a9026b3 --- /dev/null +++ b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/gui/GuiHint.java @@ -0,0 +1,19 @@ +package dev.spiritstudios.specter.api.config.gui; + +import dev.spiritstudios.specter.api.config.Config; + +public interface GuiHint { + static GuiHint slider(int step) { + return new IntSliderHint(step); + } + + static GuiHint slider() { + return new IntSliderHint(1); + } + + static GuiHint tab() {return new SubConfigHints.TabHint();} + + static GuiHint innerMenu() {return new SubConfigHints.InnerMenuHint();} + + static GuiHint section() {return new SubConfigHints.SectionHint();} +} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/gui/IntSliderHint.java b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/gui/IntSliderHint.java new file mode 100644 index 00000000..279d5ac3 --- /dev/null +++ b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/gui/IntSliderHint.java @@ -0,0 +1,4 @@ +package dev.spiritstudios.specter.api.config.gui; + +public record IntSliderHint(int step) implements GuiHint { +} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/gui/LongSliderHint.java b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/gui/LongSliderHint.java new file mode 100644 index 00000000..1b67783c --- /dev/null +++ b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/gui/LongSliderHint.java @@ -0,0 +1,4 @@ +package dev.spiritstudios.specter.api.config.gui; + +public record LongSliderHint(long step) implements GuiHint { +} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/gui/SubConfigHints.java b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/gui/SubConfigHints.java new file mode 100644 index 00000000..6d7c8460 --- /dev/null +++ b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/gui/SubConfigHints.java @@ -0,0 +1,14 @@ +package dev.spiritstudios.specter.api.config.gui; + +import dev.spiritstudios.specter.api.config.Config; + +public class SubConfigHints { + public record InnerMenuHint() implements GuiHint { + } + + public record TabHint() implements GuiHint { + } + + public record SectionHint() implements GuiHint { + } +} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/ConfigHolderRegistry.java b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/ConfigHolderRegistry.java index c49ba7e9..9f576c1e 100644 --- a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/ConfigHolderRegistry.java +++ b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/ConfigHolderRegistry.java @@ -7,6 +7,7 @@ import net.minecraft.util.Identifier; +import dev.spiritstudios.specter.api.config.Config; import dev.spiritstudios.specter.api.config.ConfigHolder; import dev.spiritstudios.specter.impl.config.network.ConfigSyncS2CPayload; @@ -21,6 +22,16 @@ public static void register(Identifier id, ConfigHolder holder) { return holders.get(id); } + public static void clearOverrides() { + holders.values().forEach(holder -> clearOverrides(holder.get())); + } + + private static void clearOverrides(Config config) { + config.values().values().forEach(either -> either + .ifLeft(value -> value.override(null)) + .ifRight(ConfigHolderRegistry::clearOverrides)); + } + public static void reload() { holders.values().forEach(ConfigHolder::load); @@ -29,7 +40,8 @@ public static void reload() { public static List createPayloads() { return holders.values().stream() - .map(ConfigSyncS2CPayload::new) - .toList(); + .filter(holder -> holder.get().shouldSync()) + .map(ConfigSyncS2CPayload::new) + .toList(); } } diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/DoubleRangeConstraint.java b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/DoubleRangeConstraint.java new file mode 100644 index 00000000..7b6f045e --- /dev/null +++ b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/DoubleRangeConstraint.java @@ -0,0 +1,19 @@ +package dev.spiritstudios.specter.impl.config; + +import com.mojang.serialization.DataResult; + +import dev.spiritstudios.specter.api.config.Constraint; + +public record DoubleRangeConstraint(double min, double max) implements Constraint { + public DoubleRangeConstraint { + if (min > max) throw new IllegalArgumentException("min > max"); + + } + + @Override + public DataResult test(Double value) { + if (value >= min && value <= max) return DataResult.success(value); + return DataResult.error(() -> + "Value %s out of bounds for range of %s to %s.".formatted(value, min, max)); + } +} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/FloatRangeConstraint.java b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/FloatRangeConstraint.java new file mode 100644 index 00000000..7814296f --- /dev/null +++ b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/FloatRangeConstraint.java @@ -0,0 +1,19 @@ +package dev.spiritstudios.specter.impl.config; + +import com.mojang.serialization.DataResult; + +import dev.spiritstudios.specter.api.config.Constraint; + +public record FloatRangeConstraint(float min, float max) implements Constraint { + public FloatRangeConstraint { + if (min > max) throw new IllegalArgumentException("min > max"); + + } + + @Override + public DataResult test(Float value) { + if (value >= min && value <= max) return DataResult.success(value); + return DataResult.error(() -> + "Value %s out of bounds for range of %s to %s.".formatted(value, min, max)); + } +} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/IntegerRangeConstraint.java b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/IntegerRangeConstraint.java new file mode 100644 index 00000000..886b3952 --- /dev/null +++ b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/IntegerRangeConstraint.java @@ -0,0 +1,18 @@ +package dev.spiritstudios.specter.impl.config; + +import com.mojang.serialization.DataResult; + +import dev.spiritstudios.specter.api.config.Constraint; + +public record IntegerRangeConstraint(int min, int max) implements Constraint { + public IntegerRangeConstraint { + if (min > max) throw new IllegalArgumentException("min > max"); + } + + @Override + public DataResult test(Integer value) { + if (value >= min && value <= max) return DataResult.success(value); + return DataResult.error(() -> + "Value %s out of bounds for range of %s to %s.".formatted(value, min, max)); + } +} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/LongRangeConstraint.java b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/LongRangeConstraint.java new file mode 100644 index 00000000..89036342 --- /dev/null +++ b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/LongRangeConstraint.java @@ -0,0 +1,19 @@ +package dev.spiritstudios.specter.impl.config; + +import com.mojang.serialization.DataResult; + +import dev.spiritstudios.specter.api.config.Constraint; + +public record LongRangeConstraint(long min, long max) implements Constraint { + public LongRangeConstraint { + if (min > max) throw new IllegalArgumentException("min > max"); + + } + + @Override + public DataResult test(Long value) { + if (value >= min && value <= max) return DataResult.success(value); + return DataResult.error(() -> + "Value %s out of bounds for range of %s to %s.".formatted(value, min, max)); + } +} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/NestedConfigValue.java b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/NestedConfigValue.java deleted file mode 100644 index 7cacd1c1..00000000 --- a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/NestedConfigValue.java +++ /dev/null @@ -1,115 +0,0 @@ -package dev.spiritstudios.specter.impl.config; - -import java.util.Optional; - -import com.mojang.serialization.DataResult; -import com.mojang.serialization.DynamicOps; -import com.mojang.serialization.MapCodec; -import com.mojang.serialization.RecordBuilder; -import io.netty.buffer.ByteBuf; - -import dev.spiritstudios.specter.api.config.NestedConfig; -import dev.spiritstudios.specter.api.config.Value; -import dev.spiritstudios.specter.api.core.SpecterGlobals; -import dev.spiritstudios.specter.api.serialization.CommentedCodec; - -public class NestedConfigValue> implements Value { - private final T defaultValue; - private final boolean sync; - private final String comment; - private T value; - private MapCodec mapCodec; - private String name; - - public NestedConfigValue(T defaultValue, boolean sync, String comment) { - this.defaultValue = defaultValue; - this.defaultValue.fields().forEach(pair -> { - pair.value().init(pair.field().getName()); - SpecterGlobals.debug("Registered config value: %s".formatted(pair.value().name())); - }); - - this.sync = sync; - this.comment = comment; - this.value = defaultValue; - } - - @Override - public T get() { - return value; - } - - @Override - public T defaultValue() { - return defaultValue; - } - - @Override - public void set(T value) { - this.value = value; - } - - @Override - public void init(String name) { - this.name = name; - this.mapCodec = (comment().isPresent() ? new CommentedCodec<>(defaultValue, comment) : defaultValue).fieldOf(name); - } - - @Override - public RecordBuilder encode(DynamicOps ops, RecordBuilder builder) { - if (mapCodec == null) { - SpecterGlobals.LOGGER.error("Value not initialized, cannot encode"); - return builder; - } - - return mapCodec.encode(get(), ops, builder); - } - - @Override - public boolean decode(DynamicOps ops, T1 input) { - if (mapCodec == null) { - SpecterGlobals.LOGGER.error("Value not initialized, cannot decode"); - return false; - } - - DataResult result = mapCodec.decoder().parse(ops, input); - if (result.error().isPresent()) { - SpecterGlobals.LOGGER.error("Failed to decode value: {}", result.error().get()); - return false; - } - - T value = result.result().orElseThrow(); - this.set(value); - - return true; - } - - @Override - public void packetDecode(ByteBuf buf) { - value.packetCodec().decode(buf); - } - - @Override - public void packetEncode(ByteBuf buf) { - value.packetCodec().encode(buf, value); - } - - @Override - public Optional comment() { - return Optional.ofNullable(comment); - } - - @Override - public boolean sync() { - return sync; - } - - @Override - public String translationKey(String configId) { - return String.format("config.%s.%s", configId, name); - } - - @Override - public String name() { - return name; - } -} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/NumericValueImpl.java b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/NumericValueImpl.java deleted file mode 100644 index c4256a4e..00000000 --- a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/NumericValueImpl.java +++ /dev/null @@ -1,35 +0,0 @@ -package dev.spiritstudios.specter.impl.config; - -import com.mojang.serialization.Codec; -import io.netty.buffer.ByteBuf; - -import net.minecraft.network.codec.PacketCodec; - -import dev.spiritstudios.specter.api.config.NumericValue; -import dev.spiritstudios.specter.api.core.math.Range; - -public class NumericValueImpl> extends ValueImpl implements NumericValue { - private final Range range; - private final double step; - - public NumericValueImpl(T defaultValue, Codec codec, PacketCodec packetCodec, String comment, boolean sync, Range range, double step) { - super(defaultValue, codec, packetCodec, comment, sync); - this.range = range; - this.step = step; - } - - @Override - public Range range() { - return range; - } - - @Override - public double step() { - return step; - } - - @Override - public void set(T value) { - super.set(range.clamp(value)); - } -} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/ValueImpl.java b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/ValueImpl.java deleted file mode 100644 index 156947e9..00000000 --- a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/ValueImpl.java +++ /dev/null @@ -1,120 +0,0 @@ -package dev.spiritstudios.specter.impl.config; - -import java.util.Optional; - -import com.mojang.serialization.*; -import io.netty.buffer.ByteBuf; - -import net.minecraft.network.codec.PacketCodec; - -import dev.spiritstudios.specter.api.config.Value; -import dev.spiritstudios.specter.api.core.SpecterGlobals; -import dev.spiritstudios.specter.api.serialization.CommentedCodec; - -public class ValueImpl implements Value { - private final T defaultValue; - private final Codec codec; - private final PacketCodec packetCodec; - private final boolean sync; - private final String comment; - - private MapCodec mapCodec; - private String name; - - private T value; - - public ValueImpl(T defaultValue, - Codec codec, - PacketCodec packetCodec, - String comment, - boolean sync - ) { - this.defaultValue = defaultValue; - this.comment = comment; - this.sync = sync; - this.packetCodec = packetCodec; - - this.codec = codec; - this.value = defaultValue; - } - - @Override - public T get() { - return value; - } - - @Override - public T defaultValue() { - return defaultValue; - } - - @Override - public void set(T value) { - this.value = value; - } - - @Override - public void init(String name) { - this.mapCodec = (comment().isPresent() ? new CommentedCodec<>(codec, comment) : codec).fieldOf(name); - this.name = name; - } - - @Override - public RecordBuilder encode(DynamicOps ops, RecordBuilder builder) { - if (mapCodec == null) { - SpecterGlobals.LOGGER.error("Value not initialized, cannot encode"); - return builder; - } - - return mapCodec.encode(get(), ops, builder); - } - - @Override - public boolean decode(DynamicOps ops, T1 input) { - if (mapCodec == null) { - SpecterGlobals.LOGGER.error("Value not initialized, cannot decode"); - return false; - } - - DataResult result = mapCodec.decoder().parse(ops, input); - if (result.error().isPresent()) { - SpecterGlobals.LOGGER.error("Failed to decode value: {}", result.error().get()); - return false; - } - - T value = result.result().orElseThrow(); - this.set(value); - - return true; - } - - @Override - public void packetDecode(ByteBuf buf) { - set(packetCodec.decode(buf)); - } - - @Override - public void packetEncode(ByteBuf buf) { - packetCodec.encode(buf, get()); - } - - @Override - public Optional comment() { - return Optional.ofNullable(comment); - } - - @Override - public boolean sync() { - return sync; - } - - @Override - public String translationKey(String configId) { - return String.format("config.%s.%s", configId, name); - } - - @Override - public String name() { - return name; - } -} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/network/ConfigSyncS2CPayload.java b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/network/ConfigSyncS2CPayload.java index 1ac9058f..2663a8b9 100644 --- a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/network/ConfigSyncS2CPayload.java +++ b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/network/ConfigSyncS2CPayload.java @@ -5,8 +5,7 @@ import java.util.ArrayList; import java.util.List; -import io.netty.buffer.ByteBuf; - +import net.minecraft.network.RegistryByteBuf; import net.minecraft.network.codec.PacketCodec; import net.minecraft.network.packet.CustomPayload; import net.minecraft.server.MinecraftServer; @@ -14,30 +13,32 @@ import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import dev.spiritstudios.specter.api.config.Config; import dev.spiritstudios.specter.api.config.ConfigHolder; +import dev.spiritstudios.specter.api.config.Value; import dev.spiritstudios.specter.api.core.SpecterGlobals; import dev.spiritstudios.specter.impl.config.ConfigHolderRegistry; public record ConfigSyncS2CPayload(ConfigHolder config) implements CustomPayload { public static final Id ID = new Id<>(Identifier.of(MODID, "config_sync")); - public static final PacketCodec CODEC = PacketCodec.tuple( - PacketCodec.>of( - (value, buf) -> { - Identifier.PACKET_CODEC.encode(buf, value.id()); - SpecterGlobals.debug("Encoding config sync packet for %s".formatted(value.id())); - value.packetEncode(buf); - }, - buf -> { - Identifier id = Identifier.PACKET_CODEC.decode(buf); - SpecterGlobals.debug("Decoding config sync packet for %s".formatted(id)); - ConfigHolder config = ConfigHolderRegistry.get(id); - config.save(); - - return config.packetDecode(buf); - } - ), - ConfigSyncS2CPayload::config, - ConfigSyncS2CPayload::new + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodec.of( + (holder, buf) -> { + Identifier.PACKET_CODEC.encode(buf, holder.id()); + SpecterGlobals.debug("Encoding config sync packet for %s".formatted(holder.id())); + encodeConfigValues(buf, holder.get()); + }, + buf -> { + Identifier id = Identifier.PACKET_CODEC.decode(buf); + ConfigHolder holder = ConfigHolderRegistry.get(id); + + decodeConfigValues(buf, holder.get()); + + return holder; + } + ), + ConfigSyncS2CPayload::config, + ConfigSyncS2CPayload::new ); private static final List CACHE = new ArrayList<>(); @@ -55,7 +56,37 @@ public static void sendPayloadsToAll(MinecraftServer server) { List payloads = ConfigSyncS2CPayload.getPayloads(); server.getPlayerManager().getPlayerList().forEach( - player -> payloads.forEach(payload -> ServerPlayNetworking.send(player, payload))); + player -> payloads.forEach(payload -> ServerPlayNetworking.send(player, payload))); + } + + private static void encodeConfigValues(RegistryByteBuf buffer, Config config) { + config.values().forEach((key, either) -> { + either + .ifLeft(value -> { + if (value.sync()) encodeField(buffer, value); + }) + .ifRight(subConfig -> encodeConfigValues(buffer, subConfig)); + }); + } + + private static void decodeConfigValues(RegistryByteBuf buffer, Config config) { + config.values().forEach((key, either) -> { + either + .ifLeft(value -> { + if (value.sync()) decodeField(buffer, value); + }) + .ifRight(subConfig -> decodeConfigValues(buffer, subConfig)); + }); + } + + private static void encodeField(RegistryByteBuf buffer, Value value) { + value.packetCodec().ifPresent(codec -> + codec.encode(buffer, value.get())); + } + + private static void decodeField(RegistryByteBuf buffer, Value value) { + value.packetCodec().ifPresent(codec -> + value.override(codec.decode(buffer))); } @Override diff --git a/specter-config/src/testmod/java/dev/spiritstudios/testmod/config/SpecterConfigGameTest.java b/specter-config/src/testmod/java/dev/spiritstudios/testmod/config/SpecterConfigGameTest.java index d83fd524..913261af 100644 --- a/specter-config/src/testmod/java/dev/spiritstudios/testmod/config/SpecterConfigGameTest.java +++ b/specter-config/src/testmod/java/dev/spiritstudios/testmod/config/SpecterConfigGameTest.java @@ -5,64 +5,65 @@ import java.nio.file.Path; import java.nio.file.Paths; +import net.minecraft.test.TestContext; +import net.minecraft.text.Text; + import net.fabricmc.fabric.api.gametest.v1.GameTest; import net.fabricmc.loader.api.FabricLoader; -import net.minecraft.test.TestContext; -import net.minecraft.text.Text; +import dev.spiritstudios.specter.api.config.ConfigHolder; @SuppressWarnings("unused") public final class SpecterConfigGameTest { - @GameTest - public void testTomlConfig(TestContext context) throws IOException { - Path path = Paths.get( - FabricLoader.getInstance().getConfigDir().toString(), - "", - "tomltestconfig.toml" - ); - + private void testConfig(TestContext context, Path path, ConfigHolder holder) throws IOException { Files.deleteIfExists(path); - TestConfig.TOML_HOLDER.save(); - context.assertTrue(TestConfig.TOML_HOLDER.load(), Text.of("Config file failed to load")); + holder.save(); + + TestConfig config = holder.get(); + + context.assertTrue(holder.load(), Text.of("Config file failed to load")); context.assertTrue(Files.exists(path), Text.of("Config file does not exist")); - context.assertTrue(TestConfig.TOML_HOLDER.get().testString.get().equals("test"), Text.of("String is not equal to test, Make sure you haven't modified the config")); - context.assertTrue(TestConfig.TOML_HOLDER.get().nestedConfig.get().nestedString.get().equals("nested"), Text.of("String is not equal to nested, Make sure you haven't modified the config")); - context.assertTrue(TestConfig.TOML_HOLDER.get().nestedConfig.get().nestedNestedConfig.get().nestedNestedString.get().equals("nestednested"), Text.of("String is not equal to nestednested, Make sure you haven't modified the config")); + context.assertFalse( + config.testString.checkConstraints("meow").isSuccess(), + Text.of("Constraint on value testString did not flag \"meow\" as invalid") + ); - TestConfig.TOML_HOLDER.get().testString.set("test2"); - TestConfig.TOML_HOLDER.save(); - context.assertTrue(TestConfig.TOML_HOLDER.load(), Text.of("Config file failed to load")); + context.assertTrue(config.testString.get().equals("test@example.com"), Text.of("String is not equal to test, Make sure you haven't modified the config")); + context.assertTrue(config.nestedConfig.nestedString.get().equals("nested"), Text.of("String is not equal to nested, Make sure you haven't modified the config")); + context.assertTrue(config.nestedConfig.nestedNestedConfig.nestedNestedString.get().equals("nestednested"), Text.of("String is not equal to nestednested, Make sure you haven't modified the config")); + + config.testString.set("test2@example.com"); + holder.save(); + + context.assertTrue(holder.load(), Text.of("Config file failed to load")); context.assertTrue(Files.exists(path), Text.of("Config file does not exist")); - context.assertTrue(TestConfig.TOML_HOLDER.get().testString.get().equals("test2"), Text.of("String is not equal to test2, Make sure you haven't modified the config")); + + context.assertTrue( + config.testString.get().equals("test2@example.com"), + Text.of("String is not equal to test2@example.com, Make sure you haven't modified the config") + ); + Files.deleteIfExists(path); context.complete(); } + @GameTest + public void testTomlConfig(TestContext context) throws IOException { + testConfig(context, Paths.get( + FabricLoader.getInstance().getConfigDir().toString(), + "", + "tomltestconfig.toml" + ), TestConfig.TOML_HOLDER); + } + @GameTest public void testJsonCConfig(TestContext context) throws IOException { - Path path = Paths.get( + testConfig(context, Paths.get( FabricLoader.getInstance().getConfigDir().toString(), "", "jsontestconfig.json" - ); - - Files.deleteIfExists(path); - context.assertTrue(TestConfig.JSON_HOLDER.load(), Text.of("Config file failed to load")); - context.assertTrue(Files.exists(path), Text.of("Config file does not exist")); - - context.assertTrue(TestConfig.JSON_HOLDER.get().testString.get().equals("test"), Text.of("String is not equal to test, Make sure you haven't modified the config")); - context.assertTrue(TestConfig.JSON_HOLDER.get().nestedConfig.get().nestedString.get().equals("nested"), Text.of("String is not equal to nested, Make sure you haven't modified the config")); - context.assertTrue(TestConfig.JSON_HOLDER.get().nestedConfig.get().nestedNestedConfig.get().nestedNestedString.get().equals("nestednested"), Text.of("String is not equal to nestednested, Make sure you haven't modified the config")); - - TestConfig.JSON_HOLDER.get().testString.set("test2"); - TestConfig.JSON_HOLDER.save(); - context.assertTrue(TestConfig.JSON_HOLDER.load(), Text.of("Config file failed to load")); - context.assertTrue(Files.exists(path), Text.of("Config file does not exist")); - context.assertTrue(TestConfig.JSON_HOLDER.get().testString.get().equals("test2"), Text.of("String is not equal to test2, Make sure you haven't modified the config")); - Files.deleteIfExists(path); - - context.complete(); + ), TestConfig.JSON_HOLDER); } } diff --git a/specter-config/src/testmod/java/dev/spiritstudios/testmod/config/TestConfig.java b/specter-config/src/testmod/java/dev/spiritstudios/testmod/config/TestConfig.java index 5199bbf9..98bf3b5f 100644 --- a/specter-config/src/testmod/java/dev/spiritstudios/testmod/config/TestConfig.java +++ b/specter-config/src/testmod/java/dev/spiritstudios/testmod/config/TestConfig.java @@ -1,5 +1,7 @@ package dev.spiritstudios.testmod.config; +import com.mojang.serialization.DataResult; + import net.minecraft.item.Item; import net.minecraft.item.Items; import net.minecraft.registry.Registries; @@ -7,63 +9,62 @@ import dev.spiritstudios.specter.api.config.Config; import dev.spiritstudios.specter.api.config.ConfigHolder; -import dev.spiritstudios.specter.api.config.NestedConfig; +import dev.spiritstudios.specter.api.config.Constraint; import dev.spiritstudios.specter.api.config.Value; +import dev.spiritstudios.specter.api.config.gui.GuiHint; +import dev.spiritstudios.specter.api.config.gui.SubConfigHints; import dev.spiritstudios.specter.api.serialization.format.JsonCFormat; import dev.spiritstudios.specter.api.serialization.format.TomlFormat; -public class TestConfig extends Config { +public class TestConfig extends Config { public static final ConfigHolder JSON_HOLDER = ConfigHolder - .builder(Identifier.of("specter-config-testmod", "jsontestconfig"), TestConfig.class) - .format(JsonCFormat.INSTANCE) - .build(); + .builder(Identifier.of("specter-config-testmod", "jsontestconfig"), TestConfig.class) + .format(JsonCFormat.INSTANCE) + .build(); public static final ConfigHolder TOML_HOLDER = ConfigHolder - .builder(Identifier.of("specter-config-testmod", "tomltestconfig"), TestConfig.class) - .format(TomlFormat.INSTANCE) - .build(); + .builder(Identifier.of("specter-config-testmod", "tomltestconfig"), TestConfig.class) + .format(TomlFormat.INSTANCE) + .build(); public final String invalidField = "test"; - public final Value testString = stringValue("test") - .comment("This is a test string") - .sync() - .build(); + public final Value testString = stringValue("test@example.com") + .comment("This is a test string") + .constrain(str -> str.matches("^.*@.*$") ? DataResult.success(str) : DataResult.error(() -> "Not an email")) + .sync() + .build(); public final Value testInt = intValue(2) - .comment("This is a test int") - .range(2, 10) - .step(3) - .build(); + .comment("This is a test int") + .constrain(Constraint.range(2, 10)) + .guiHint(GuiHint.slider(3)) + .sync() + .build(); public final Value testBool = booleanValue(true) - .comment("This is a test bool") - .build(); + .comment("This is a test bool") + .build(); public final Value testDouble = doubleValue(1.0) - .comment("This is a test double") - .range(0.0, 10.0) - .step(0.05) - .build(); + .comment("This is a test double") + .constrain(Constraint.range(0.0, 10.0)) + .build(); public final Value testFloat = floatValue(1.0f) - .comment("This is a test float") - .range(0.0f, 5.0f) - .step(0.5) - .build(); + .comment("This is a test float") + .constrain(Constraint.range(0.0f, 5.0f)) + .build(); public final Value testEnum = enumValue(TestEnum.TEST_1, TestEnum.class) - .comment("This is a test enum") - .build(); + .comment("This is a test enum") + .build(); public final Value testItem = registryValue(Items.BEDROCK, Registries.ITEM) - .comment("This is a test item") - .build(); + .comment("This is a test item") + .build(); - public final Value nestedConfig = nestedValue(NestedTestConfig.class) - .comment("This is a nested config") - .sync() - .build(); + public final SubTestConfig nestedConfig = new SubTestConfig(); public enum TestEnum { TEST_1, @@ -71,72 +72,73 @@ public enum TestEnum { TEST_3 } - public static class NestedTestConfig extends NestedConfig { + public static class SubTestConfig extends SubConfig { public final Value nestedString = stringValue("nested") - .comment("This is a nested string") - .sync() - .build(); + .comment("This is a nested string") + .sync() + .build(); public final Value nestedInt = intValue(3) - .comment("This is a nested int") - .sync() - .build(); + .comment("This is a nested int") + .sync() + .build(); public final Value nestedBool = booleanValue(false) - .comment("This is a nested bool") - .sync() - .build(); + .comment("This is a nested bool") + .sync() + .build(); public final Value nestedDouble = doubleValue(2.0) - .comment("This is a nested double") - .sync() - .build(); + .comment("This is a nested double") + .sync() + .build(); public final Value nestedFloat = floatValue(2.0f) - .comment("This is a nested float") - .sync() - .build(); + .comment("This is a nested float") + .sync() + .build(); public final Value nestedEnum = enumValue(TestEnum.TEST_2, TestEnum.class) - .comment("This is a nested enum") - .sync() - .build(); + .comment("This is a nested enum") + .sync() + .build(); - public final Value nestedNestedConfig = nestedValue(NestedNestedTestConfig.class) - .comment("This is a nested nested config") - .sync() - .build(); + public final NestedSubTestConfig nestedNestedConfig = new NestedSubTestConfig(); + + public SubTestConfig() { + super(new SubConfigHints.SectionHint()); + } - public static class NestedNestedTestConfig extends NestedConfig { + public static class NestedSubTestConfig extends SubConfig { public final Value nestedNestedString = stringValue("nestednested") - .comment("This is a nested nested string") - .sync() - .build(); + .comment("This is a nested nested string") + .sync() + .build(); public final Value nestedNestedInt = intValue(4) - .comment("This is a nested nested int") - .sync() - .build(); + .comment("This is a nested nested int") + .sync() + .build(); public final Value nestedNestedBool = booleanValue(true) - .comment("This is a nested nested bool") - .sync() - .build(); + .comment("This is a nested nested bool") + .sync() + .build(); public final Value nestedNestedDouble = doubleValue(3.0) - .comment("This is a nested nested double") - .sync() - .build(); + .comment("This is a nested nested double") + .sync() + .build(); public final Value nestedNestedFloat = floatValue(3.0f) - .comment("This is a nested nested float") - .sync() - .build(); + .comment("This is a nested nested float") + .sync() + .build(); public final Value nestedNestedEnum = enumValue(TestEnum.TEST_3, TestEnum.class) - .comment("This is a nested nested enum") - .sync() - .build(); + .comment("This is a nested nested enum") + .sync() + .build(); } } } diff --git a/specter-config/src/testmod/resources/assets/specter-config-testmod/lang/en_us.json b/specter-config/src/testmod/resources/assets/specter-config-testmod/lang/en_us.json index 6fa6190e..81eabede 100644 --- a/specter-config/src/testmod/resources/assets/specter-config-testmod/lang/en_us.json +++ b/specter-config/src/testmod/resources/assets/specter-config-testmod/lang/en_us.json @@ -1,5 +1,6 @@ { "config.specter-config-testmod.tomltestconfig.testBool": "Test Boolean", + "config.specter-config-testmod.tomltestconfig.testBool.description": "This is a test description! It's nice and long to test the text wrapping several times :3", "config.specter-config-testmod.tomltestconfig.testDouble": "Test Double", "config.specter-config-testmod.tomltestconfig.testEnum": "Test Enum", "config.specter-config-testmod.tomltestconfig.testEnum.test_1": "1", diff --git a/specter-config/src/testmod/resources/fabric.mod.json b/specter-config/src/testmod/resources/fabric.mod.json index 08cf53f4..c08243c8 100644 --- a/specter-config/src/testmod/resources/fabric.mod.json +++ b/specter-config/src/testmod/resources/fabric.mod.json @@ -27,5 +27,11 @@ "minecraft": "~${minecraft_version}-", "fabric-api": "*", "java": ">=21" - } + }, + "mixins": [ + { + "config": "specter-config-testmod.client.mixins.json", + "environment": "client" + } + ] } diff --git a/specter-config/src/testmodClient/java/dev/spiritstudios/testmod/config/mixin/TitleScreenMixin.java b/specter-config/src/testmodClient/java/dev/spiritstudios/testmod/config/mixin/TitleScreenMixin.java new file mode 100644 index 00000000..eabb42ce --- /dev/null +++ b/specter-config/src/testmodClient/java/dev/spiritstudios/testmod/config/mixin/TitleScreenMixin.java @@ -0,0 +1,37 @@ +package dev.spiritstudios.testmod.config.mixin; + +import dev.spiritstudios.specter.api.config.client.TabbedListConfigScreen; +import dev.spiritstudios.testmod.config.TestConfig; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.TitleScreen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.text.Text; + +@Mixin(TitleScreen.class) +public abstract class TitleScreenMixin extends Screen { + + protected TitleScreenMixin(Text title) { + super(title); + } + + @Inject( + method = "init", + at = @At("HEAD") + ) + protected void init(CallbackInfo ci) { + if (this.client == null) return; + + this.addDrawableChild(ButtonWidget.builder( + Text.of("SPECTRE CONFIG TEST"), + button -> this.client.setScreen(new TabbedListConfigScreen(TestConfig.TOML_HOLDER, this))) + .dimensions(0, 30, 100, 20) + .build() + ); + } +} diff --git a/specter-config/src/testmodClient/resources/specter-config-testmod.client.mixins.json b/specter-config/src/testmodClient/resources/specter-config-testmod.client.mixins.json new file mode 100644 index 00000000..24bd1d65 --- /dev/null +++ b/specter-config/src/testmodClient/resources/specter-config-testmod.client.mixins.json @@ -0,0 +1,8 @@ +{ + "required": true, + "package": "dev.spiritstudios.testmod.config.mixin", + "compatibilityLevel": "JAVA_21", + "client": [ + "TitleScreenMixin" + ] +} diff --git a/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/DoubleRange.java b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/DoubleRange.java new file mode 100644 index 00000000..54c6a97c --- /dev/null +++ b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/DoubleRange.java @@ -0,0 +1,39 @@ +package dev.spiritstudios.specter.api.core.math; + +import org.jetbrains.annotations.NotNull; + +import net.minecraft.util.math.MathHelper; + +public record DoubleRange(@NotNull Double min, @NotNull Double max) implements Range { + /** + * A range from 0 to 1 + */ + public static final DoubleRange ZERO_ONE = new DoubleRange(0.0, 1.0); + + + /** + * A range from {@link Double#MIN_VALUE} to {@link Double#MAX_VALUE} + */ + public static final DoubleRange FULL = new DoubleRange(Double.MIN_VALUE, Double.MAX_VALUE); + + + @Override + public @NotNull Double clamp(@NotNull Double value) { + return Math.clamp(value, min, max); + } + + @Override + public @NotNull Double range() { + return max - min; + } + + @Override + public @NotNull Double lerp(@NotNull Double delta) { + return MathHelper.lerp(delta, min, max); + } + + @Override + public @NotNull Double map(@NotNull Double value, @NotNull Range from) { + return lerp((value - from.min()) / (from.max() - from.min())); + } +} diff --git a/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/FloatRange.java b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/FloatRange.java new file mode 100644 index 00000000..cd289ad4 --- /dev/null +++ b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/FloatRange.java @@ -0,0 +1,38 @@ +package dev.spiritstudios.specter.api.core.math; + +import org.jetbrains.annotations.NotNull; + +import net.minecraft.util.math.MathHelper; + +public record FloatRange(@NotNull Float min, @NotNull Float max) implements Range { + /** + * A range from 0 to 1 + */ + public static final FloatRange ZERO_ONE = new FloatRange(0.0F, 1.0F); + + /** + * A range from {@link Float#MIN_VALUE} to {@link Float#MAX_VALUE} + */ + public static final FloatRange FULL = new FloatRange(Float.MIN_VALUE, Float.MAX_VALUE); + + + @Override + public @NotNull Float clamp(@NotNull Float value) { + return Math.clamp(value, min, max); + } + + @Override + public @NotNull Float range() { + return max - min; + } + + @Override + public @NotNull Float lerp(@NotNull Float delta) { + return MathHelper.lerp(delta, min, max); + } + + @Override + public @NotNull Float map(@NotNull Float value, @NotNull Range from) { + return lerp((value - from.min()) / (from.max() - from.min())); + } +} diff --git a/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/GenericMath.java b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/GenericMath.java deleted file mode 100644 index d0fd076d..00000000 --- a/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/GenericMath.java +++ /dev/null @@ -1,117 +0,0 @@ -package dev.spiritstudios.specter.api.core.math; - -import java.util.function.BiFunction; - -/** - * A few very cursed methods for doing arithmetic with {@link Number}s. - */ -public final class GenericMath { - private static final Ops BYTE_OPS = Ops.create( - (a, b) -> (byte) (a + b), - (a, b) -> (byte) (a - b), - (a, b) -> (byte) (a * b), - (a, b) -> (byte) (a / b) - ); - private static final Ops SHORT_OPS = Ops.create( - (a, b) -> (short) (a + b), - (a, b) -> (short) (a - b), - (a, b) -> (short) (a * b), - (a, b) -> (short) (a / b) - ); - private static final Ops INTEGER_OPS = Ops.create( - Integer::sum, - (a, b) -> a - b, - (a, b) -> a * b, - (a, b) -> a / b - ); - private static final Ops LONG_OPS = Ops.create( - Long::sum, - (a, b) -> a - b, - (a, b) -> a * b, - (a, b) -> a / b - ); - private static final Ops FLOAT_OPS = Ops.create( - Float::sum, - (a, b) -> a - b, - (a, b) -> a * b, - (a, b) -> a / b - ); - private static final Ops DOUBLE_OPS = Ops.create( - Double::sum, - (a, b) -> a - b, - (a, b) -> a * b, - (a, b) -> a / b - ); - - private GenericMath() { - throw new UnsupportedOperationException("Cannot instantiate utility class."); - } - - @SuppressWarnings("unchecked") - private static Ops getOps(T x) { - return switch (x) { - case Byte ignored -> (Ops) BYTE_OPS; - case Short ignored -> (Ops) SHORT_OPS; - case Integer ignored -> (Ops) INTEGER_OPS; - case Long ignored -> (Ops) LONG_OPS; - case Float ignored -> (Ops) FLOAT_OPS; - case Double ignored -> (Ops) DOUBLE_OPS; - default -> throw new IllegalArgumentException("Unsupported number type: " + x.getClass()); - }; - } - - public static T add(T a, T b) { - return getOps(a).add(a, b); - } - - public static T subtract(T a, T b) { - return getOps(a).subtract(a, b); - } - - public static T multiply(T a, T b) { - return getOps(a).multiply(a, b); - } - - public static T divide(T a, T b) { - return getOps(a).divide(a, b); - } - - /** - * A set of arithmetic operations that can be performed on {@link Number}s. - * - * @param The type of number to perform operations on. - */ - public abstract static class Ops { - public static Ops create(BiFunction add, BiFunction subtract, BiFunction multiply, BiFunction divide) { - return new Ops<>() { - @Override - T add(T a, T b) { - return add.apply(a, b); - } - - @Override - T subtract(T a, T b) { - return subtract.apply(a, b); - } - - @Override - T multiply(T a, T b) { - return multiply.apply(a, b); - } - - @Override - T divide(T a, T b) { - return divide.apply(a, b); - } - }; - } - - abstract T add(T a, T b); - - abstract T subtract(T a, T b); - - abstract T multiply(T a, T b); - - abstract T divide(T a, T b); - } -} diff --git a/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/IntegerRange.java b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/IntegerRange.java new file mode 100644 index 00000000..713667ca --- /dev/null +++ b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/IntegerRange.java @@ -0,0 +1,32 @@ +package dev.spiritstudios.specter.api.core.math; + +import org.jetbrains.annotations.NotNull; + +import net.minecraft.util.math.MathHelper; + +public record IntegerRange(@NotNull Integer min, @NotNull Integer max) implements Range { + /** + * A range from {@link Integer#MIN_VALUE} to {@link Integer#MAX_VALUE} + */ + public static final IntegerRange FULL = new IntegerRange(Integer.MIN_VALUE, Integer.MAX_VALUE); + + @Override + public @NotNull Integer clamp(@NotNull Integer value) { + return MathHelper.clamp(value, min, max); + } + + @Override + public @NotNull Integer range() { + return max - min; + } + + @Override + public @NotNull Integer lerp(@NotNull Integer delta) { + return MathHelper.lerp(delta, min, max); + } + + @Override + public @NotNull Integer map(@NotNull Integer value, @NotNull Range from) { + return lerp((value - from.min()) / (from.max() - from.min())); + } +} diff --git a/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/LongRange.java b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/LongRange.java new file mode 100644 index 00000000..7e7d79bf --- /dev/null +++ b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/LongRange.java @@ -0,0 +1,30 @@ +package dev.spiritstudios.specter.api.core.math; + +import org.jetbrains.annotations.NotNull; + +public record LongRange(@NotNull Long min, @NotNull Long max) implements Range { + /** + * A range from {@link Long#MIN_VALUE} to {@link Long#MAX_VALUE} + */ + public static final LongRange FULL = new LongRange(Long.MIN_VALUE, Long.MAX_VALUE); + + @Override + public @NotNull Long clamp(@NotNull Long value) { + return Math.clamp(value, min, max); + } + + @Override + public @NotNull Long range() { + return max - min; + } + + @Override + public @NotNull Long lerp(@NotNull Long delta) { + return min + delta * (max - min); + } + + @Override + public @NotNull Long map(@NotNull Long value, @NotNull Range from) { + return lerp((value - from.min()) / (from.max() - from.min())); + } +} diff --git a/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/Range.java b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/Range.java index 7f981f11..920b31bd 100644 --- a/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/Range.java +++ b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/Range.java @@ -1,31 +1,21 @@ package dev.spiritstudios.specter.api.core.math; -public record Range>(T min, T max) { - public boolean contains(T value) { - return value.compareTo(min) >= 0 && value.compareTo(max) <= 0; - } +import org.jetbrains.annotations.NotNull; - public T clamp(T value) { - return value.compareTo(min) < 0 ? min : value.compareTo(max) > 0 ? max : value; +public interface Range> { + default boolean contains(@NotNull T value) { + return value.compareTo(min()) >= 0 && value.compareTo(max()) <= 0; } - public T range() { - return GenericMath.subtract(max, min); - } + @NotNull T clamp(@NotNull T value); - public T lerp(T delta) { - return GenericMath.add(min, GenericMath.multiply(delta, range())); - } + @NotNull T range(); - public T lerpProgress(T value) { - return GenericMath.divide(GenericMath.subtract(value, min), range()); - } + @NotNull T lerp(@NotNull T delta); - public T map(T value, Range from) { - return lerp(from.lerpProgress(value)); - } + @NotNull T map(@NotNull T value, @NotNull Range from); - public T map01(T value) { - return GenericMath.add(GenericMath.multiply(value, range()), min); - } + @NotNull T min(); + + @NotNull T max(); } diff --git a/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/SpecterMath.java b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/SpecterMath.java index f6753469..c0938f60 100644 --- a/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/SpecterMath.java +++ b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/math/SpecterMath.java @@ -7,7 +7,7 @@ public final class SpecterMath { public static final List HORIZONTAL_DIRECTIONS = Arrays.stream(Direction.values()).filter(direction -> - direction.getAxis().isHorizontal()).toList(); + direction.getAxis().isHorizontal()).toList(); private SpecterMath() { } @@ -16,4 +16,44 @@ public static double wrap(double value, double min, double max) { double range = max - min; return value - range * Math.floor((value - min) / range); } + + public static boolean canParseFloat(String s) { + try { + Float.parseFloat(s); + } catch (NumberFormatException e) { + return false; + } + + return true; + } + + public static boolean canParseDouble(String s) { + try { + Double.parseDouble(s); + } catch (NumberFormatException e) { + return false; + } + + return true; + } + + public static boolean canParseLong(String s) { + try { + Long.parseLong(s); + } catch (NumberFormatException e) { + return false; + } + + return true; + } + + public static boolean canParseInteger(String s) { + try { + Integer.parseInt(s); + } catch (NumberFormatException e) { + return false; + } + + return true; + } } diff --git a/specter-core/src/main/java/dev/spiritstudios/specter/api/core/reflect/ReflectionHelper.java b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/reflect/ReflectionHelper.java index ad45cc50..61dca63d 100644 --- a/specter-core/src/main/java/dev/spiritstudios/specter/api/core/reflect/ReflectionHelper.java +++ b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/reflect/ReflectionHelper.java @@ -34,11 +34,11 @@ public static T instantiate(Class clazz, Object... args) { } catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) { throw new RuntimeException( - (e instanceof NoSuchMethodException ? - "No constructor without arguments found for class " : - "Failed to instantiate class " - ) + clazz.getName(), - e + (e instanceof NoSuchMethodException ? + "No constructor without arguments found for class " : + "Failed to instantiate class " + ) + clazz.getName(), + e ); } @@ -56,22 +56,22 @@ public static T instantiate(Class clazz, Object... args) { */ public static Stream> getStaticFields(Class clazz, Class target) { return Arrays.stream(clazz.getDeclaredFields()) - .map(field -> { - if (!Modifier.isStatic(field.getModifiers())) return null; - if (field.isAnnotationPresent(Ignore.class)) return null; - F value; - try { - Object objectValue = field.get(null); - if (!target.isAssignableFrom(objectValue.getClass())) return null; - value = target.cast(objectValue); - } catch (IllegalAccessException e) { - throw new RuntimeException("Failed to access field " + field.getName(), e); - } catch (ClassCastException e) { - return null; - } - - return new FieldValuePair<>(field, value); - }).filter(Objects::nonNull); + .map(field -> { + if (!Modifier.isStatic(field.getModifiers())) return null; + if (field.isAnnotationPresent(Ignore.class)) return null; + F value; + try { + Object objectValue = field.get(null); + if (!target.isAssignableFrom(objectValue.getClass())) return null; + value = target.cast(objectValue); + } catch (IllegalAccessException e) { + throw new RuntimeException("Failed to access field " + field.getName(), e); + } catch (ClassCastException e) { + return null; + } + + return new FieldValuePair<>(field, value); + }).filter(Objects::nonNull); } /** @@ -83,14 +83,14 @@ public static Stream> getStaticFields(Class clazz, C * @return Value of the field */ @SuppressWarnings("unchecked") - public static T getFieldValue(Object instance, Field field) { + public static Optional getFieldValue(Object instance, Field field) { try { Object value = field.get(instance); - if (value == null) return null; + if (value == null) return Optional.empty(); - return (T) value; - } catch (IllegalAccessException e) { - throw new RuntimeException("Failed to access field " + field.getName(), e); + return Optional.of((T) value); + } catch (IllegalAccessException | ClassCastException e) { + return Optional.empty(); } } @@ -101,7 +101,7 @@ public static T getFieldValue(Object instance, Field field) { * @param Type of the field * @return Value of the field */ - public static T getFieldValue(Field field) { + public static Optional getFieldValue(Field field) { return getFieldValue(null, field); } @@ -134,6 +134,11 @@ public static T getAnnotationValue(AnnotationNode annotation, String key, T return defaultValue; } + public static Optional cast(Object object, Class clazz) { + //noinspection unchecked + return clazz.isAssignableFrom(object.getClass()) ? Optional.of((T) object) : Optional.empty(); + } + public record FieldValuePair(Field field, T value) { } } diff --git a/specter-entity/src/testmod/java/dev/spiritstudios/testmod/entity/SpecterEntityGameTest.java b/specter-entity/src/testmod/java/dev/spiritstudios/testmod/entity/SpecterEntityGameTest.java index a7787318..f1e0b156 100644 --- a/specter-entity/src/testmod/java/dev/spiritstudios/testmod/entity/SpecterEntityGameTest.java +++ b/specter-entity/src/testmod/java/dev/spiritstudios/testmod/entity/SpecterEntityGameTest.java @@ -1,12 +1,12 @@ package dev.spiritstudios.testmod.entity; -import net.fabricmc.fabric.api.gametest.v1.GameTest; - import net.minecraft.entity.EntityType; import net.minecraft.entity.mob.WardenEntity; import net.minecraft.test.TestContext; import net.minecraft.text.Text; +import net.fabricmc.fabric.api.gametest.v1.GameTest; + public class SpecterEntityGameTest { @GameTest public void testDefaultAttributes(TestContext context) { diff --git a/specter-gui/src/client/java/dev/spiritstudios/specter/api/gui/client/widget/SpecterSliderWidget.java b/specter-gui/src/client/java/dev/spiritstudios/specter/api/gui/client/widget/SpecterSliderWidget.java index 8ba3b5cd..507ba9bb 100644 --- a/specter-gui/src/client/java/dev/spiritstudios/specter/api/gui/client/widget/SpecterSliderWidget.java +++ b/specter-gui/src/client/java/dev/spiritstudios/specter/api/gui/client/widget/SpecterSliderWidget.java @@ -1,8 +1,8 @@ package dev.spiritstudios.specter.api.gui.client.widget; -import java.util.function.Consumer; import java.util.function.Function; +import it.unimi.dsi.fastutil.doubles.Double2ObjectFunction; import org.lwjgl.glfw.GLFW; import net.minecraft.client.MinecraftClient; @@ -12,63 +12,130 @@ import net.minecraft.client.gui.screen.narration.NarrationPart; import net.minecraft.client.gui.widget.ClickableWidget; import net.minecraft.client.input.KeyCodes; +import net.minecraft.client.option.SimpleOption; import net.minecraft.client.render.RenderLayer; import net.minecraft.client.sound.SoundManager; +import net.minecraft.screen.ScreenTexts; import net.minecraft.text.MutableText; import net.minecraft.text.Text; import net.minecraft.util.Identifier; import net.minecraft.util.math.ColorHelper; import net.minecraft.util.math.MathHelper; -import dev.spiritstudios.specter.api.core.math.Range; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; public class SpecterSliderWidget extends ClickableWidget { - private static final Identifier SLIDER = Identifier.ofVanilla("widget/slider"); - private static final Identifier SLIDER_HIGHLIGHTED = Identifier.ofVanilla("widget/slider_highlighted"); - private static final Identifier SLIDER_HANDLE = Identifier.ofVanilla("widget/slider_handle"); - private static final Identifier SLIDER_HANDLE_HIGHLIGHTED = Identifier.ofVanilla("widget/slider_handle_highlighted"); - - private static final Range ZERO_ONE = new Range<>(0.0, 1.0); - protected final double step; - protected final Range range; - protected final Consumer valueChangedListener; - protected final Function messageSupplier; - protected double value; - protected boolean sliderFocused; - - protected SpecterSliderWidget(int x, int y, int width, int height, double value, double step, Range range, Consumer valueChangedListener, Function messageSupplier) { - super(x, y, width, height, messageSupplier.apply(value)); - - this.value = value; + private static final Identifier TEXTURE = Identifier.ofVanilla("widget/slider"); + private static final Identifier HIGHLIGHTED_TEXTURE = Identifier.ofVanilla("widget/slider_highlighted"); + private static final Identifier HANDLE_TEXTURE = Identifier.ofVanilla("widget/slider_handle"); + private static final Identifier HANDLE_HIGHLIGHTED_TEXTURE = Identifier.ofVanilla("widget/slider_handle_highlighted"); + + private final Double2ObjectFunction valueToText; + private final Function narrationMessageFactory; + + private final UpdateCallback callback; + + private final boolean optionTextOmitted; + private final SimpleOption.TooltipFactory tooltipFactory; + private final Text optionText; + private final double min; + private final double max; + private final double step; + private double value; + private boolean sliderFocused; + + protected SpecterSliderWidget( + int x, int y, + int width, int height, + Text message, + Text optionText, + double initialValue, + Double2ObjectFunction valueToText, + Function narrationMessageFactory, + UpdateCallback callback, + boolean optionTextOmitted, + SimpleOption.TooltipFactory tooltipFactory, double min, double max, double step + ) { + super(x, y, width, height, message); + this.valueToText = valueToText; + this.narrationMessageFactory = narrationMessageFactory; + this.callback = callback; + this.optionTextOmitted = optionTextOmitted; + this.tooltipFactory = tooltipFactory; + this.optionText = optionText; + this.value = initialValue; + this.min = min; + this.max = max; this.step = step; - this.range = range; - this.valueChangedListener = valueChangedListener; - this.messageSupplier = messageSupplier; } - public static Builder builder(double value) { - return new Builder(value); + private void refreshTooltip() { + this.setTooltip(this.tooltipFactory.apply(this.value)); } - // region Input + // region Rendering @Override - public void onClick(double mouseX, double mouseY) { - this.setValueFromMouse(mouseX); + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float deltaTicks) { + MinecraftClient client = MinecraftClient.getInstance(); + + context.drawGuiTexture( + RenderLayer::getGuiTextured, this.getTexture(), this.getX(), this.getY(), this.getWidth(), this.getHeight(), ColorHelper.getWhite(this.alpha) + ); + + + context.drawGuiTexture( + RenderLayer::getGuiTextured, + this.getHandleTexture(), + this.getX() + (int) MathHelper.map(this.value, min, max, 0, this.width - 8), + this.getY(), + 8, + this.getHeight(), + ColorHelper.getWhite(this.alpha) + ); + + this.drawScrollableText( + context, + client.textRenderer, + 2, + this.active ? 0xffffff : 0xa0a0a0 | MathHelper.ceil(this.alpha * 255.0F) << 24 + ); } - @Override - protected void onDrag(double mouseX, double mouseY, double deltaX, double deltaY) { - this.setValueFromMouse(mouseX); - super.onDrag(mouseX, mouseY, deltaX, deltaY); + private Identifier getTexture() { + return this.isNarratable() && this.isFocused() && !this.sliderFocused ? HIGHLIGHTED_TEXTURE : TEXTURE; } - @Override - public void onRelease(double mouseX, double mouseY) { - super.playDownSound(MinecraftClient.getInstance().getSoundManager()); + private Identifier getHandleTexture() { + return !this.isNarratable() || !this.hovered && !this.sliderFocused ? HANDLE_TEXTURE : HANDLE_HIGHLIGHTED_TEXTURE; } + // endregion - private void setValueFromMouse(double mouseX) { - setValue(range.map(MathHelper.clamp((mouseX - (double) (this.getX() + 4)) / (double) (this.getWidth() - 8), 0.0, 1.0), ZERO_ONE)); + public double getValue() { + return value; + } + + public void setValue(double value) { + value = MathHelper.clamp(value, min, max); + Text text = this.composeText(value); + this.setMessage(text); + this.value = value; + this.refreshTooltip(); + this.callback.onValueChange(this, this.value); + } + + // region Input + @Override + public void setFocused(boolean focused) { + super.setFocused(focused); + if (!focused) { + this.sliderFocused = false; + } else { + GuiNavigationType guiNavigationType = MinecraftClient.getInstance().getNavigationType(); + if (guiNavigationType == GuiNavigationType.MOUSE || guiNavigationType == GuiNavigationType.KEYBOARD_TAB) { + this.sliderFocused = true; + } + } } @Override @@ -78,153 +145,116 @@ public boolean keyPressed(int keyCode, int scanCode, int modifiers) { return true; } - if (!this.sliderFocused) return false; - - if (keyCode == GLFW.GLFW_KEY_LEFT || keyCode == GLFW.GLFW_KEY_RIGHT) { - float sign = keyCode == GLFW.GLFW_KEY_LEFT ? -1.0F : 1.0F; - this.setValue(this.value + sign * (this.step == 0.0 ? 0.01 : this.step)); - - return true; + if (this.sliderFocused) { + boolean left = keyCode == GLFW.GLFW_KEY_LEFT; + if (left || keyCode == GLFW.GLFW_KEY_RIGHT) { + this.setValue(this.value + step * (left ? -1.0F : 1.0F)); + return true; + } } return false; } - // endregion - - // region Navigation - @Override - public void setFocused(boolean focused) { - super.setFocused(focused); - if (!focused) { - this.sliderFocused = false; - return; - } - GuiNavigationType navigationType = MinecraftClient.getInstance().getNavigationType(); - if (navigationType == GuiNavigationType.MOUSE || navigationType == GuiNavigationType.KEYBOARD_TAB) - this.sliderFocused = true; + public void onClick(double mouseX, double mouseY) { + this.setValueFromMouse(mouseX); } - protected void onValueChanged() { - this.valueChangedListener.accept(value); + @Override + protected void onDrag(double mouseX, double mouseY, double deltaX, double deltaY) { + this.setValueFromMouse(mouseX); + super.onDrag(mouseX, mouseY, deltaX, deltaY); } - @Override - public void playDownSound(SoundManager soundManager) { + private void setValueFromMouse(double mouseX) { + setValue(Math.clamp(min + (step * Math.round((((mouseX - (double) (this.getX() + 4)) / (double) (this.getWidth() - 8)) * (max - min)) / step)), min, max)); } // endregion - // region Rendering + // region Narration @Override - protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { - MinecraftClient client = MinecraftClient.getInstance(); - - context.drawGuiTexture( - RenderLayer::getGuiTextured, - this.getTexture(), - this.getX(), - this.getY(), - this.getWidth(), - this.getHeight(), - ColorHelper.fromFloats(this.alpha, 1.0F, 1.0F, 1.0F) - ); - - context.drawGuiTexture( - RenderLayer::getGuiTextured, - this.getHandleTexture(), - this.getX() + (int) (ZERO_ONE.map(this.value, range) * (this.getWidth() - 8)), - this.getY(), - 8, - this.getHeight(), - ColorHelper.fromFloats(this.alpha, 1.0F, 1.0F, 1.0F) - ); + public void appendClickableNarrations(NarrationMessageBuilder builder) { + builder.put(NarrationPart.TITLE, this.getNarrationMessage()); - int color = this.active ? 0xffffff : 0xa0a0a0; + if (this.active) { + builder.put( + NarrationPart.USAGE, + this.isFocused() ? + Text.translatable("narration.slider.usage.focused") : + Text.translatable("narration.slider.usage.hovered") + ); + } + } - this.drawScrollableText(context, client.textRenderer, 2, color | MathHelper.ceil(this.alpha * 255.0F) << 24); + private Text composeText(double value) { + return this.optionTextOmitted ? this.valueToText.apply(value) : this.composeGenericOptionText(value); } - @Override - public Text getMessage() { - return this.messageSupplier.apply(value); + private MutableText composeGenericOptionText(double value) { + return ScreenTexts.composeGenericOptionText(this.optionText, this.valueToText.apply(value)); } - protected Identifier getTexture() { - return this.isFocused() && !this.sliderFocused ? SLIDER_HIGHLIGHTED : SLIDER; + public MutableText getGenericNarrationMessage() { + return getNarrationMessage(this.optionTextOmitted ? this.composeGenericOptionText(this.value) : this.getMessage()); } - protected Identifier getHandleTexture() { - return !this.hovered && !this.sliderFocused ? SLIDER_HANDLE : SLIDER_HANDLE_HIGHLIGHTED; + @Override + protected MutableText getNarrationMessage() { + return this.narrationMessageFactory.apply(this); } // endregion - // region Narration @Override - protected void appendClickableNarrations(NarrationMessageBuilder builder) { - builder.put(NarrationPart.TITLE, this.getNarrationMessage()); - if (!this.active) return; - - builder.put( - NarrationPart.USAGE, - Text.translatable(isFocused() ? "narration.slider.usage.focused" : "narration.slider.usage.hovered") - ); + public void playDownSound(SoundManager soundManager) { } @Override - protected MutableText getNarrationMessage() { - return Text.translatable("gui.narrate.slider", this.getMessage()); + public void onRelease(double mouseX, double mouseY) { + super.playDownSound(MinecraftClient.getInstance().getSoundManager()); } - // endregion - - protected void setValue(double value) { - double oldValue = this.value; - double newValue = value; - newValue = step <= 0.0 ? newValue : range.map(Math.round(ZERO_ONE.map(newValue, range) / step) * step, ZERO_ONE); - this.value = range.clamp(newValue); - - if (oldValue != this.value) onValueChanged(); + @Environment(EnvType.CLIENT) + public interface UpdateCallback { + void onValueChange(SpecterSliderWidget slider, double value); } public static class Builder { - private final double value; - private int x; - private int y; - private int width = 150; - private int height = 20; + private final Double2ObjectFunction valueToText; + private double value; + private SimpleOption.TooltipFactory tooltipFactory = value -> null; + private Function narrationMessageFactory = SpecterSliderWidget::getGenericNarrationMessage; + private boolean optionTextOmitted; + private double min; + private double max; private double step; - private Range range = new Range<>(0.0, 1.0); - private Consumer valueChangedListener = value -> { - }; - private Function messageSupplier = (value) -> Text.of(String.format("%.2f", value)); - protected Builder(double value) { - this.value = value; + public Builder(Double2ObjectFunction valueToText) { + this.valueToText = valueToText; } - public Builder position(int x, int y) { - this.x = x; - this.y = y; + public Builder tooltip(SimpleOption.TooltipFactory tooltipFactory) { + this.tooltipFactory = tooltipFactory; return this; } - public Builder size(int width, int height) { - this.width = width; - this.height = height; + public Builder initially(double value) { + this.value = value; return this; } - public Builder dimensions(int width, int height, int x, int y) { - return position(x, y).size(width, height); + public Builder narration(Function narrationMessageFactory) { + this.narrationMessageFactory = narrationMessageFactory; + return this; } - public Builder message(Text message) { - messageSupplier = (ignored) -> message; + public Builder omitKeyText() { + this.optionTextOmitted = true; return this; } - public Builder message(Function messageSupplier) { - this.messageSupplier = messageSupplier; + public Builder range(double min, double max) { + this.min = min; + this.max = max; return this; } @@ -233,31 +263,33 @@ public Builder step(double step) { return this; } - public Builder range(Range range) { - this.range = range; - return this; + public SpecterSliderWidget build(Text optionText, UpdateCallback callback) { + return this.build(0, 0, 150, 20, optionText, callback); } - public Builder range(double min, double max) { - return range(new Range<>(min, max)); + public SpecterSliderWidget build(int x, int y, int width, int height, Text optionText) { + return this.build(x, y, width, height, optionText, (button, value) -> { + }); } - public Builder onValueChanged(Consumer valueChangedListener) { - this.valueChangedListener = valueChangedListener; - return this; - } + public SpecterSliderWidget build(int x, int y, int width, int height, Text optionText, UpdateCallback callback) { + Text text = this.valueToText.apply(value); + Text message = this.optionTextOmitted ? text : ScreenTexts.composeGenericOptionText(optionText, text); - public SpecterSliderWidget build() { return new SpecterSliderWidget( x, y, width, height, + message, + optionText, value, - step, - range, - valueChangedListener, - messageSupplier + this.valueToText, + this.narrationMessageFactory, + callback, + this.optionTextOmitted, + this.tooltipFactory, + min, max, step ); } } diff --git a/specter-gui/src/testmodClient/java/dev/spiritstudios/testmod/gui/mixin/TitleScreenMixin.java b/specter-gui/src/testmodClient/java/dev/spiritstudios/testmod/gui/mixin/TitleScreenMixin.java index a0b6a7c4..a68c67e4 100644 --- a/specter-gui/src/testmodClient/java/dev/spiritstudios/testmod/gui/mixin/TitleScreenMixin.java +++ b/specter-gui/src/testmodClient/java/dev/spiritstudios/testmod/gui/mixin/TitleScreenMixin.java @@ -20,17 +20,17 @@ protected TitleScreenMixin(Text title) { } @Inject( - method = "init", - at = @At("HEAD") + method = "init", + at = @At("HEAD") ) protected void init(CallbackInfo ci) { if (this.client == null) return; this.addDrawableChild(ButtonWidget.builder( - Text.of("SPECTRE GUI TEST"), - button -> this.client.setScreen(new SpecterGuiTestScreen())) - .dimensions(0, 0, 100, 20) - .build() + Text.of("SPECTRE GUI TEST"), + button -> this.client.setScreen(new SpecterGuiTestScreen())) + .dimensions(0, 10, 100, 20) + .build() ); } } diff --git a/specter-item/src/main/java/dev/spiritstudios/specter/api/item/datagen/SpecterItemGroupProvider.java b/specter-item/src/main/java/dev/spiritstudios/specter/api/item/datagen/SpecterItemGroupProvider.java index be66bc2a..a1ce5103 100644 --- a/specter-item/src/main/java/dev/spiritstudios/specter/api/item/datagen/SpecterItemGroupProvider.java +++ b/specter-item/src/main/java/dev/spiritstudios/specter/api/item/datagen/SpecterItemGroupProvider.java @@ -4,8 +4,6 @@ import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; -import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput; -import net.fabricmc.fabric.api.datagen.v1.provider.FabricCodecDataProvider; import org.jetbrains.annotations.ApiStatus; import net.minecraft.data.DataOutput; @@ -14,6 +12,9 @@ import net.minecraft.registry.RegistryWrapper; import net.minecraft.util.Identifier; +import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput; +import net.fabricmc.fabric.api.datagen.v1.provider.FabricCodecDataProvider; + import dev.spiritstudios.specter.api.item.SpecterItemRegistryKeys; import dev.spiritstudios.specter.impl.item.DataItemGroup; diff --git a/specter-item/src/testmod/java/dev/spiritstudios/testmod/item/SpecterItemGameTest.java b/specter-item/src/testmod/java/dev/spiritstudios/testmod/item/SpecterItemGameTest.java index 2f785a1d..3e6e07d5 100644 --- a/specter-item/src/testmod/java/dev/spiritstudios/testmod/item/SpecterItemGameTest.java +++ b/specter-item/src/testmod/java/dev/spiritstudios/testmod/item/SpecterItemGameTest.java @@ -1,7 +1,5 @@ package dev.spiritstudios.testmod.item; -import net.fabricmc.fabric.api.gametest.v1.GameTest; - import net.minecraft.block.Blocks; import net.minecraft.block.ComposterBlock; import net.minecraft.block.HopperBlock; @@ -17,6 +15,8 @@ import net.minecraft.util.math.Direction; import net.minecraft.world.GameMode; +import net.fabricmc.fabric.api.gametest.v1.GameTest; + @SuppressWarnings("unused") public class SpecterItemGameTest { @GameTest diff --git a/specter-registry/src/main/java/dev/spiritstudios/specter/api/registry/metatag/Metatag.java b/specter-registry/src/main/java/dev/spiritstudios/specter/api/registry/metatag/Metatag.java index 274cad7e..880e2d64 100644 --- a/specter-registry/src/main/java/dev/spiritstudios/specter/api/registry/metatag/Metatag.java +++ b/specter-registry/src/main/java/dev/spiritstudios/specter/api/registry/metatag/Metatag.java @@ -6,8 +6,6 @@ import java.util.function.Supplier; import com.mojang.serialization.Codec; -import net.fabricmc.api.EnvType; -import net.fabricmc.loader.api.FabricLoader; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -17,6 +15,9 @@ import net.minecraft.resource.ResourceType; import net.minecraft.util.Identifier; +import net.fabricmc.api.EnvType; +import net.fabricmc.loader.api.FabricLoader; + import dev.spiritstudios.specter.api.core.SpecterGlobals; import dev.spiritstudios.specter.impl.registry.metatag.ExistingCombinedMetatag; import dev.spiritstudios.specter.impl.registry.metatag.MetatagHolder; @@ -112,7 +113,7 @@ public Builder packetCodec(PacketCodec packetCodec) { } public Builder existingCombined(Function existingGetter, - Supplier>> existingIterator) { + Supplier>> existingIterator) { this.existingGetter = existingGetter; this.existingIterator = existingIterator; return this; diff --git a/specter-registry/src/main/java/dev/spiritstudios/specter/impl/registry/reloadable/SpecterReloadableRegistriesImpl.java b/specter-registry/src/main/java/dev/spiritstudios/specter/impl/registry/reloadable/SpecterReloadableRegistriesImpl.java index e02ac04a..8e71091d 100644 --- a/specter-registry/src/main/java/dev/spiritstudios/specter/impl/registry/reloadable/SpecterReloadableRegistriesImpl.java +++ b/specter-registry/src/main/java/dev/spiritstudios/specter/impl/registry/reloadable/SpecterReloadableRegistriesImpl.java @@ -14,8 +14,6 @@ import net.minecraft.registry.RegistryKey; import net.minecraft.util.Identifier; -import dev.spiritstudios.specter.api.core.SpecterGlobals; - public final class SpecterReloadableRegistriesImpl { private static final Map> RELOADABLE_REGISTRIES = new Object2ObjectLinkedOpenHashMap<>(); @@ -39,12 +37,6 @@ public static Optional manager() { public static void setManager(DynamicRegistryManager manager) { SpecterReloadableRegistriesImpl.manager = manager; - - if (SpecterGlobals.DEBUG && manager != null) { - manager.streamAllRegistryKeys().forEach(key -> { - SpecterGlobals.debug(key.getValue().toString()); - }); - } } public record ReloadableRegistryInfo( diff --git a/specter-registry/src/testmod/java/dev/spiritstudios/testmod/registry/SpecterRegistryGameTest.java b/specter-registry/src/testmod/java/dev/spiritstudios/testmod/registry/SpecterRegistryGameTest.java index 23d19b56..d4d0acc2 100644 --- a/specter-registry/src/testmod/java/dev/spiritstudios/testmod/registry/SpecterRegistryGameTest.java +++ b/specter-registry/src/testmod/java/dev/spiritstudios/testmod/registry/SpecterRegistryGameTest.java @@ -1,13 +1,13 @@ package dev.spiritstudios.testmod.registry; -import net.fabricmc.fabric.api.gametest.v1.GameTest; - import net.minecraft.block.Blocks; import net.minecraft.registry.RegistryKey; import net.minecraft.test.TestContext; import net.minecraft.text.Text; import net.minecraft.util.Identifier; +import net.fabricmc.fabric.api.gametest.v1.GameTest; + import dev.spiritstudios.specter.api.core.util.SpecterAssertions; import dev.spiritstudios.specter.api.registry.reloadable.SpecterReloadableRegistries; diff --git a/specter-registry/src/testmod/java/dev/spiritstudios/testmod/registry/SpecterRegistryTestMod.java b/specter-registry/src/testmod/java/dev/spiritstudios/testmod/registry/SpecterRegistryTestMod.java index a4d4a894..86574fe1 100644 --- a/specter-registry/src/testmod/java/dev/spiritstudios/testmod/registry/SpecterRegistryTestMod.java +++ b/specter-registry/src/testmod/java/dev/spiritstudios/testmod/registry/SpecterRegistryTestMod.java @@ -1,9 +1,6 @@ package dev.spiritstudios.testmod.registry; import com.mojang.serialization.Codec; -import net.fabricmc.api.ModInitializer; -import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; -import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.minecraft.block.Block; import net.minecraft.network.codec.PacketCodecs; @@ -13,6 +10,10 @@ import net.minecraft.resource.ResourceType; import net.minecraft.util.Identifier; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; + import dev.spiritstudios.specter.api.core.SpecterGlobals; import dev.spiritstudios.specter.api.registry.metatag.Metatag; import dev.spiritstudios.specter.api.registry.reloadable.SpecterReloadableRegistries;