From bb4c6fdea3f883cb3ad8c74a33cae074f3b3d708 Mon Sep 17 00:00:00 2001 From: CallMeEchoCodes Date: Thu, 5 Sep 2024 23:13:13 +1000 Subject: [PATCH 1/9] feat: begin rewrite of configs Currently needs new tests and to finish syncing --- .../specter/api/ConfigScreenManager.java | 41 +++ .../specter/api/ModMenuHelper.java | 14 +- .../impl/config/SpecterConfigClient.java | 27 +- .../impl/config/SpecterConfigModMenu.java | 16 +- .../specter/impl/config/gui/ConfigScreen.java | 140 ++------- .../gui/widget/BooleanButtonWidget.java | 22 +- .../config/gui/widget/DoubleSliderWidget.java | 33 +-- .../config/gui/widget/EnumButtonWidget.java | 49 +-- .../config/gui/widget/FloatSliderWidget.java | 32 +- .../gui/widget/IntegerSliderWidget.java | 32 +- .../impl/config/gui/widget/TextBoxWidget.java | 25 +- .../specter/api/config/Config.java | 279 ++++++++++++++++-- .../specter/api/config/ConfigManager.java | 54 ++-- .../specter/api/config/NestedConfig.java | 7 - .../api/config/annotations/Comment.java | 18 -- .../specter/api/config/annotations/Range.java | 19 -- .../specter/api/config/annotations/Sync.java | 14 - .../impl/config/NonSyncExclusionStrategy.java | 17 -- .../specter/impl/config/SpecterConfig.java | 4 +- .../specter/impl/config/ValueImpl.java | 122 ++++++++ .../config/network/ConfigSyncS2CPayload.java | 63 ++-- .../testmod/CreateTestConfig.java | 62 ++-- .../spiritstudios/testmod/GetTestConfig.java | 62 ++-- .../testmod/SpecterConfigGameTest.java | 2 +- .../testmod/SpecterConfigTestmodClient.java | 5 +- .../specter/api/core/util/CodecHelper.java | 51 ++++ 26 files changed, 705 insertions(+), 505 deletions(-) create mode 100644 specter-config/src/client/java/dev/spiritstudios/specter/api/ConfigScreenManager.java delete mode 100644 specter-config/src/main/java/dev/spiritstudios/specter/api/config/NestedConfig.java delete mode 100644 specter-config/src/main/java/dev/spiritstudios/specter/api/config/annotations/Comment.java delete mode 100644 specter-config/src/main/java/dev/spiritstudios/specter/api/config/annotations/Range.java delete mode 100644 specter-config/src/main/java/dev/spiritstudios/specter/api/config/annotations/Sync.java delete mode 100644 specter-config/src/main/java/dev/spiritstudios/specter/impl/config/NonSyncExclusionStrategy.java create mode 100644 specter-config/src/main/java/dev/spiritstudios/specter/impl/config/ValueImpl.java create mode 100644 specter-core/src/main/java/dev/spiritstudios/specter/api/core/util/CodecHelper.java diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/api/ConfigScreenManager.java b/specter-config/src/client/java/dev/spiritstudios/specter/api/ConfigScreenManager.java new file mode 100644 index 0000000..d4e948f --- /dev/null +++ b/specter-config/src/client/java/dev/spiritstudios/specter/api/ConfigScreenManager.java @@ -0,0 +1,41 @@ +package dev.spiritstudios.specter.api; + +import dev.spiritstudios.specter.api.config.Config; +import dev.spiritstudios.specter.impl.config.gui.widget.*; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.ApiStatus; + +import java.util.Map; +import java.util.function.BiFunction; + +public final class ConfigScreenManager { + private static final Map, BiFunction, Identifier, ? extends ClickableWidget>> widgetFactories = new Object2ObjectOpenHashMap<>(); + + public static void registerWidgetFactory(Class clazz, BiFunction, Identifier, ? extends ClickableWidget> factory) { + widgetFactories.put(clazz, factory); + } + + @SuppressWarnings("unchecked") + @ApiStatus.Internal + public static BiFunction, Identifier, ? extends ClickableWidget> getWidgetFactory(Config.Value value, Identifier configId) { + // We are using a switch instead of just adding to our map for 2 reasons: + // 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 -> + (configValue, id) -> new BooleanButtonWidget((Config.Value) configValue, id); + case Integer ignored -> + (configValue, id) -> new IntegerSliderWidget((Config.Value) configValue, id); + case Double ignored -> (configValue, id) -> new DoubleSliderWidget((Config.Value) configValue, id); + case Float ignored -> (configValue, id) -> new FloatSliderWidget((Config.Value) configValue, id); + case String ignored -> (configValue, id) -> new TextBoxWidget((Config.Value) configValue, id); + case Enum ignored -> (configValue, id) -> new EnumButtonWidget((Config.Value>) configValue, id); + default -> widgetFactories.get(value.defaultValue().getClass()); + }; + } + + private ConfigScreenManager() { + } +} diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/api/ModMenuHelper.java b/specter-config/src/client/java/dev/spiritstudios/specter/api/ModMenuHelper.java index 86b14b3..90eb2fe 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/api/ModMenuHelper.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/api/ModMenuHelper.java @@ -1,8 +1,8 @@ package dev.spiritstudios.specter.api; -import dev.spiritstudios.specter.api.config.Config; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.util.Identifier; import org.jetbrains.annotations.ApiStatus; import java.util.Map; @@ -11,22 +11,22 @@ * Helper class for ModMenu integration without having to depend on ModMenu directly. */ public class ModMenuHelper { - private static final Map> screens = new Object2ObjectOpenHashMap<>(); + private static final Map screens = new Object2ObjectOpenHashMap<>(); private static final boolean modMenuLoaded = FabricLoader.getInstance().isModLoaded("modmenu"); /** * Adds a config screen to ModMenu. * - * @param modid The modid of the mod that owns the config screen. - * @param configClass The class of the config screen. + * @param modid The modid of the mod that owns the config screen. + * @param configId The identifier of the config screen. */ - public static void addConfig(String modid, Class configClass) { + public static void addConfig(String modid, Identifier configId) { if (!modMenuLoaded) return; - screens.put(modid, configClass); + screens.put(modid, configId); } @ApiStatus.Internal - public static Map> getConfigScreens() { + public static Map getConfigScreens() { return screens; } } diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigClient.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigClient.java index 6e316f3..ff0b822 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigClient.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigClient.java @@ -1,17 +1,10 @@ package dev.spiritstudios.specter.impl.config; -import dev.spiritstudios.specter.api.config.Config; import dev.spiritstudios.specter.api.config.ConfigManager; -import dev.spiritstudios.specter.api.config.annotations.Sync; -import dev.spiritstudios.specter.api.core.util.ReflectionHelper; import dev.spiritstudios.specter.impl.config.network.ConfigSyncS2CPayload; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; -import net.minecraft.util.Identifier; - -import java.util.Arrays; -import java.util.Map; public class SpecterConfigClient implements ClientModInitializer { @Override @@ -19,25 +12,7 @@ public void onInitializeClient() { ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> ConfigManager.reloadConfigs()); ClientPlayNetworking.registerGlobalReceiver(ConfigSyncS2CPayload.ID, (payload, context) -> { - Map configs = payload.configs(); - - context.client().execute(() -> configs.forEach((id, data) -> { - Config config = ConfigManager.getConfigById(id); - if (config == null) return; - - config.save(); - Config serverConfig = ConfigManager.GSON.fromJson(data, config.getClass()); - - Arrays.stream(serverConfig.getClass().getDeclaredFields()) - .filter(field -> field.isAnnotationPresent(Sync.class)) - .forEach(field -> ReflectionHelper.setFieldValue( - config, - field, - ReflectionHelper.getFieldValue(serverConfig, field) - )); - - ConfigManager.getConfigs().put(id, config); - })); + // We don't need to do anything here, the codec will handle it }); } } diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigModMenu.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigModMenu.java index d3c72c6..1fefde4 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigModMenu.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigModMenu.java @@ -12,11 +12,15 @@ public class SpecterConfigModMenu implements ModMenuApi { @Override public Map> getProvidedConfigScreenFactories() { - return ModMenuHelper.getConfigScreens().entrySet().stream().collect( - Collectors.toMap( - Map.Entry::getKey, - entry -> parent -> new ConfigScreen(ConfigManager.getConfig(entry.getValue()), parent) - ) - ); + return ModMenuHelper.getConfigScreens() + .entrySet() + .stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, + entry -> + parent -> new ConfigScreen(ConfigManager.getConfigById(entry.getValue()), parent) + ) + ); } } diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java index 21808fe..85187d4 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java @@ -1,28 +1,30 @@ package dev.spiritstudios.specter.impl.config.gui; +import dev.spiritstudios.specter.api.ConfigScreenManager; import dev.spiritstudios.specter.api.config.Config; -import dev.spiritstudios.specter.api.config.NestedConfig; -import dev.spiritstudios.specter.api.config.annotations.Range; import dev.spiritstudios.specter.api.core.SpecterGlobals; import dev.spiritstudios.specter.api.core.util.ReflectionHelper; -import dev.spiritstudios.specter.impl.config.gui.widget.*; +import dev.spiritstudios.specter.impl.config.gui.widget.OptionsScrollableWidget; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.client.gui.widget.ClickableWidget; import net.minecraft.text.Text; +import net.minecraft.util.Identifier; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; +import java.util.function.BiFunction; public class ConfigScreen extends Screen { - private final Config config; + private final Config config; private final Screen parent; - public ConfigScreen(Config config, Screen parent) { - super(Text.translatable("config." + config.getId() + ".title")); + public ConfigScreen(Config config, Screen parent) { + super(Text.translatable("config.%s.title".formatted(config.getId()))); this.config = config; this.parent = parent; } @@ -33,7 +35,20 @@ protected void init() { OptionsScrollableWidget scrollableWidget = new OptionsScrollableWidget(this.client, this.width, this.height - 64, 32, 25); List options = new ArrayList<>(); - for (Field field : config.getClass().getDeclaredFields()) addOptionWidget(field, options); + Arrays.stream(config.getClass().getDeclaredFields()) + .filter(field -> field.getType().isAssignableFrom(Config.Value.class)) + .map(field -> (Config.Value) ReflectionHelper.getFieldValue(config, field)) + .filter(Objects::nonNull) + .forEach(option -> { + BiFunction, Identifier, ? extends ClickableWidget> factory = ConfigScreenManager.getWidgetFactory(option, this.config.getId()); + if (factory == null) { + SpecterGlobals.LOGGER.warn("No widget factory found for {}", option.defaultValue().getClass().getSimpleName()); + return; + } + + ClickableWidget widget = factory.apply(option, this.config.getId()); + if (widget != null) options.add(widget); + }); scrollableWidget.addOptions(Arrays.copyOf(options.toArray(), options.size(), ClickableWidget[].class)); this.addDrawableChild(scrollableWidget); @@ -49,114 +64,13 @@ public void close() { } public void save() { - if (config instanceof NestedConfig && this.parent instanceof ConfigScreen) { - ((ConfigScreen) this.parent).save(); - return; - } +// if (config instanceof NestedConfig && this.parent instanceof ConfigScreen) { +// ((ConfigScreen) this.parent).save(); +// return; +// } config.save(); } - - private void addOptionWidget(Field option, List options) { - Object value = ReflectionHelper.getFieldValue(config, option); - options.add(switch (value) { - case String ignored -> - new TextBoxWidget(getTranslationKey(option), () -> ReflectionHelper.getFieldValue(config, option), newValue -> ReflectionHelper.setFieldValue(config, option, newValue)); - - case Boolean ignored -> - new BooleanButtonWidget(getTranslationKey(option), () -> ReflectionHelper.getFieldValue(config, option), newValue -> ReflectionHelper.setFieldValue(config, option, newValue)); - - case Float ignored -> { - float min = 0; - float max = 100; - if (option.isAnnotationPresent(Range.class)) { - Range range = option.getAnnotation(Range.class); - min = (float) range.min(); - max = (float) range.max(); - } - - yield new FloatSliderWidget( - getTranslationKey(option), - min, - max, - () -> ReflectionHelper.getFieldValue(config, option), - newValue -> ReflectionHelper.setFieldValue(config, option, newValue) - ); - } - - case Integer ignored -> { - int min = 0; - int max = 100; - if (option.isAnnotationPresent(Range.class)) { - Range range = option.getAnnotation(Range.class); - min = (int) range.min(); - max = (int) range.max(); - } - - yield new IntegerSliderWidget( - getTranslationKey(option), - min, - max, - () -> ReflectionHelper.getFieldValue(config, option), - newValue -> ReflectionHelper.setFieldValue(config, option, newValue) - ); - } - - case Double ignored -> { - double min = 0; - double max = 100; - if (option.isAnnotationPresent(Range.class)) { - Range range = option.getAnnotation(Range.class); - min = range.min(); - max = range.max(); - } - - yield new DoubleSliderWidget( - getTranslationKey(option), - min, - max, - () -> ReflectionHelper.getFieldValue(config, option), - newValue -> ReflectionHelper.setFieldValue(config, option, newValue) - ); - } - - case Enum ignored -> - new EnumButtonWidget(option.getName(), () -> ReflectionHelper.getFieldValue(config, option), newValue -> ReflectionHelper.setFieldValue(config, option, newValue), (Enum) value); - - case NestedConfig nestedValue -> { - getNestedClass(options, nestedValue); - yield null; - } - - case null, default -> { - if (SpecterGlobals.DEBUG) - SpecterGlobals.LOGGER.warn("Unsupported config type: {}", option.getType().getName()); - - yield null; - } - }); - } - - private void getNestedClass(List options, NestedConfig value) { - ConfigScreen nestedScreen = new ConfigScreen(value, this); - - for (Field nestedField : value.getClass().getDeclaredFields()) { - if (nestedField.getType().isAssignableFrom(NestedConfig.class)) { - NestedConfig nestedValue = ReflectionHelper.getFieldValue(value.getClass().getDeclaredFields(), nestedField); - getNestedClass(options, nestedValue); - } - } - - options.add(new ButtonWidget.Builder( - Text.translatable("config." + value.getId() + ".title"), - button -> { - save(); - - if (this.client == null) return; - this.client.setScreen(nestedScreen); - } - ).dimensions(this.width / 2 - 100, 0, 200, 20).build()); - } - + private String getTranslationKey(Field field) { return "config." + this.config.getId() + "." + field.getName(); } diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/BooleanButtonWidget.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/BooleanButtonWidget.java index 420c8e8..3db5719 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/BooleanButtonWidget.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/BooleanButtonWidget.java @@ -1,38 +1,34 @@ package dev.spiritstudios.specter.impl.config.gui.widget; +import dev.spiritstudios.specter.api.config.Config; import net.minecraft.client.gui.tooltip.Tooltip; import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.screen.ScreenTexts; import net.minecraft.text.Text; - -import java.util.function.Consumer; -import java.util.function.Supplier; +import net.minecraft.util.Identifier; public class BooleanButtonWidget extends ButtonWidget { - private final Supplier getter; + private final Config.Value configValue; - public BooleanButtonWidget(String translationKey, Supplier getter, Consumer setter) { + public BooleanButtonWidget(Config.Value configValue, Identifier configId) { super( 0, 0, 0, 20, - Text.translatable(translationKey), - button -> setter.accept(!getter.get()), + Text.translatable(configValue.translationKey(configId)), + button -> configValue.set(!configValue.get()), button -> null ); - Text tooltip = Text.translatableWithFallback(translationKey + ".tooltip", ""); + Text tooltip = Text.translatableWithFallback("%s.tooltip".formatted(configValue.translationKey(configId)), ""); if (!tooltip.getString().isEmpty()) this.setTooltip(Tooltip.of(tooltip)); - this.getter = getter; + this.configValue = configValue; } @Override public Text getMessage() { - return Text.literal(super.getMessage().getString() - + ": " - + ScreenTexts.onOrOff(this.getter.get()).getString() - ); + return Text.literal("%s: %s".formatted(super.getMessage().getString(), ScreenTexts.onOrOff(this.configValue.get()).getString())); } } diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/DoubleSliderWidget.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/DoubleSliderWidget.java index 4121ef7..c04f26c 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/DoubleSliderWidget.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/DoubleSliderWidget.java @@ -1,50 +1,47 @@ package dev.spiritstudios.specter.impl.config.gui.widget; +import com.mojang.datafixers.util.Pair; +import dev.spiritstudios.specter.api.config.Config; import net.minecraft.client.gui.tooltip.Tooltip; import net.minecraft.client.gui.widget.SliderWidget; import net.minecraft.text.Text; +import net.minecraft.util.Identifier; import net.minecraft.util.math.MathHelper; -import java.util.function.Consumer; -import java.util.function.Supplier; - public class DoubleSliderWidget extends SliderWidget { - private final Supplier getter; - private final Consumer setter; + private final Config.Value configValue; private final double min; private final double max; - public DoubleSliderWidget(String translationKey, double min, double max, Supplier getter, Consumer setter) { - super(0, 0, 0, 20, Text.translatable(translationKey), 0); - this.getter = getter; - this.setter = setter; + public DoubleSliderWidget(Config.Value configValue, Identifier configId) { + super(0, 0, 0, 20, Text.translatable(configValue.translationKey(configId)), 0); + this.configValue = configValue; - Text tooltip = Text.translatableWithFallback(translationKey + ".tooltip", ""); + Text tooltip = Text.translatableWithFallback("%s.tooltip".formatted(configValue.translationKey(configId)), ""); if (!tooltip.getString().isEmpty()) this.setTooltip(Tooltip.of(tooltip)); - this.min = min; - this.max = max; + Pair range = configValue.range(); + this.min = range == null ? 0.0D : range.getFirst(); + this.max = range == null ? 1.0D : range.getSecond(); - this.value = (getter.get() - min) / (max - min); + this.value = configValue.get(); applyValue(); } + @Override protected void updateMessage() { } @Override public Text getMessage() { - return Text.of(super.getMessage().getString() - + ": " - + String.format("%.2f", getter.get()) - ); + return Text.of("%s: %s".formatted(super.getMessage().getString(), String.format("%.2f", configValue.get()))); } @Override protected void applyValue() { value = MathHelper.clamp(value, 0, 1); - setter.accept(value * (max - min) + min); + configValue.set(value * (max - min) + min); } } diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/EnumButtonWidget.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/EnumButtonWidget.java index 5c88bd5..79af22f 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/EnumButtonWidget.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/EnumButtonWidget.java @@ -1,47 +1,44 @@ package dev.spiritstudios.specter.impl.config.gui.widget; +import dev.spiritstudios.specter.api.config.Config; import net.minecraft.client.gui.tooltip.Tooltip; import net.minecraft.client.gui.widget.ButtonWidget; import net.minecraft.text.Text; +import net.minecraft.util.Identifier; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.function.Consumer; -import java.util.function.Supplier; public class EnumButtonWidget extends ButtonWidget { - private final Supplier> getter; - private final Consumer> setter; - + private final Config.Value> configValue; + private final Identifier configId; private final List> enumValues = new ArrayList<>(); - - private final String translationKey; - - public EnumButtonWidget(String translationKey, Supplier> getter, Consumer> setter, Enum enumValue) { + + public EnumButtonWidget(Config.Value> configValue, Identifier configId) { super( 0, 0, 0, 20, - Text.translatable(translationKey), + Text.translatable(configValue.translationKey(configId)), button -> { }, button -> null ); - this.getter = getter; - this.setter = setter; - - List values = Arrays.asList(enumValue.getClass().getEnumConstants()); + this.configValue = configValue; + this.configId = configId; + List values = Arrays.asList(configValue.defaultValue().getClass().getEnumConstants()); if (values.isEmpty()) throw new IllegalArgumentException("Enum values cannot be null"); - for (Object value : values) if (value instanceof Enum) enumValues.add((Enum) value); + values.stream() + .filter(value -> value instanceof Enum) + .map(value -> (Enum) value) + .forEach(enumValues::add); - Text tooltip = Text.translatableWithFallback(translationKey + ".tooltip", ""); + Text tooltip = Text.translatableWithFallback("%s.tooltip".formatted(configValue.translationKey(configId)), ""); if (!tooltip.getString().isEmpty()) this.setTooltip(Tooltip.of(tooltip)); - - this.translationKey = translationKey; } @Override @@ -51,16 +48,22 @@ public void onClick(double mouseX, double mouseY) { } private void cycle() { - Enum current = getter.get(); + Enum current = configValue.get(); int index = enumValues.indexOf(current); - setter.accept(enumValues.get((index + 1) % enumValues.size())); + configValue.set(enumValues.get((index + 1) % enumValues.size())); } @Override public Text getMessage() { - return Text.of(super.getMessage().getString() - + ": " - + Text.translatable(translationKey + "." + getter.get().toString().toLowerCase()).getString() + return Text.of("%s: %s".formatted( + super.getMessage().getString(), + Text.translatable( + "%s.%s".formatted( + configValue.translationKey(configId), + configValue.get().toString().toLowerCase() + ) + ).getString() + ) ); } } diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/FloatSliderWidget.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/FloatSliderWidget.java index 62f6f2b..3d41f96 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/FloatSliderWidget.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/FloatSliderWidget.java @@ -1,32 +1,31 @@ package dev.spiritstudios.specter.impl.config.gui.widget; +import com.mojang.datafixers.util.Pair; +import dev.spiritstudios.specter.api.config.Config; import net.minecraft.client.gui.tooltip.Tooltip; import net.minecraft.client.gui.widget.SliderWidget; import net.minecraft.text.Text; +import net.minecraft.util.Identifier; import net.minecraft.util.math.MathHelper; -import java.util.function.Consumer; -import java.util.function.Supplier; - public class FloatSliderWidget extends SliderWidget { - private final Supplier getter; - private final Consumer setter; + private final Config.Value configValue; private final float min; private final float max; - public FloatSliderWidget(String translationKey, float min, float max, Supplier getter, Consumer setter) { - super(0, 0, 0, 20, Text.translatable(translationKey), 0); - this.getter = getter; - this.setter = setter; + public FloatSliderWidget(Config.Value configValue, Identifier configId) { + super(0, 0, 0, 20, Text.translatable(configValue.translationKey(configId)), 0); + this.configValue = configValue; - Text tooltip = Text.translatableWithFallback(translationKey + ".tooltip", ""); + Text tooltip = Text.translatableWithFallback("%s.tooltip".formatted(configValue.translationKey(configId)), ""); if (!tooltip.getString().isEmpty()) this.setTooltip(Tooltip.of(tooltip)); - this.min = min; - this.max = max; + Pair range = configValue.range(); + this.min = range == null ? 0.0F : range.getFirst(); + this.max = range == null ? 1.0F : range.getSecond(); - this.value = (getter.get() - min) / (max - min); + this.value = configValue.get(); applyValue(); } @@ -36,15 +35,12 @@ protected void updateMessage() { @Override public Text getMessage() { - return Text.of(super.getMessage().getString() - + ": " - + String.format("%.1f", getter.get()) - ); + return Text.of("%s: %s".formatted(super.getMessage().getString(), String.format("%.1f", configValue.get()))); } @Override protected void applyValue() { value = MathHelper.clamp(value, 0, 1); - setter.accept((float) (value * (max - min) + min)); + configValue.set((float) (value * (max - min) + min)); } } diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/IntegerSliderWidget.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/IntegerSliderWidget.java index ac60cdb..09a3013 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/IntegerSliderWidget.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/IntegerSliderWidget.java @@ -1,32 +1,31 @@ package dev.spiritstudios.specter.impl.config.gui.widget; +import com.mojang.datafixers.util.Pair; +import dev.spiritstudios.specter.api.config.Config; import net.minecraft.client.gui.tooltip.Tooltip; import net.minecraft.client.gui.widget.SliderWidget; import net.minecraft.text.Text; +import net.minecraft.util.Identifier; import net.minecraft.util.math.MathHelper; -import java.util.function.Consumer; -import java.util.function.Supplier; - public class IntegerSliderWidget extends SliderWidget { - private final Supplier getter; - private final Consumer setter; + private final Config.Value configValue; private final int min; private final int max; - public IntegerSliderWidget(String translationKey, int min, int max, Supplier getter, Consumer setter) { - super(0, 0, 0, 20, Text.translatable(translationKey), 0); - this.getter = getter; - this.setter = setter; + public IntegerSliderWidget(Config.Value configValue, Identifier configId) { + super(0, 0, 0, 20, Text.translatable(configValue.translationKey(configId)), 0); + this.configValue = configValue; - Text tooltip = Text.translatableWithFallback(translationKey + ".tooltip", ""); + Text tooltip = Text.translatableWithFallback("%s.tooltip".formatted(configValue.translationKey(configId)), ""); if (!tooltip.getString().isEmpty()) this.setTooltip(Tooltip.of(tooltip)); - this.min = min; - this.max = max; + Pair range = configValue.range(); + this.min = range == null ? 0 : range.getFirst(); + this.max = range == null ? 100 : range.getSecond(); - this.value = (double) (getter.get() - min) / (max - min); + this.value = configValue.get(); applyValue(); } @@ -36,15 +35,12 @@ protected void updateMessage() { @Override public Text getMessage() { - return Text.of(super.getMessage().getString() - + ": " - + getter.get() - ); + return Text.of("%s: %d".formatted(super.getMessage().getString(), configValue.get())); } @Override protected void applyValue() { this.value = MathHelper.clamp(value, 0, 1.0); - setter.accept((int) Math.round(this.value * (max - min) + min)); + configValue.set((int) Math.round(this.value * (max - min) + min)); } } diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/TextBoxWidget.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/TextBoxWidget.java index 0910972..742dccc 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/TextBoxWidget.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/widget/TextBoxWidget.java @@ -1,27 +1,26 @@ package dev.spiritstudios.specter.impl.config.gui.widget; +import dev.spiritstudios.specter.api.config.Config; import net.minecraft.client.MinecraftClient; import net.minecraft.client.gui.tooltip.Tooltip; import net.minecraft.client.gui.widget.TextFieldWidget; import net.minecraft.text.Text; import net.minecraft.util.Formatting; - -import java.util.function.Consumer; -import java.util.function.Supplier; +import net.minecraft.util.Identifier; public class TextBoxWidget extends TextFieldWidget { - private final Consumer setter; + private final Config.Value configValue; - public TextBoxWidget(String translationKey, Supplier getter, Consumer setter) { - super(MinecraftClient.getInstance().textRenderer, 0, 0, 0, 20, Text.of(getter.get())); - this.setter = setter; + public TextBoxWidget(Config.Value configValue, Identifier configId) { + super(MinecraftClient.getInstance().textRenderer, 0, 0, 0, 20, Text.of(configValue.get())); + this.configValue = configValue; - setPlaceholder(Text.translatableWithFallback(translationKey + ".placeholder", "").formatted(Formatting.DARK_GRAY)); + setPlaceholder(Text.translatableWithFallback("%s.placeholder".formatted(configValue.translationKey(configId)), "").formatted(Formatting.DARK_GRAY)); - Text tooltip = Text.translatableWithFallback(translationKey + ".tooltip", ""); + Text tooltip = Text.translatableWithFallback("%s.tooltip".formatted(configValue.translationKey(configId)), ""); if (!tooltip.getString().isEmpty()) this.setTooltip(Tooltip.of(tooltip)); - this.setText(getter.get()); + this.setText(configValue.get()); setSelectionEnd(0); setSelectionStart(0); } @@ -29,18 +28,18 @@ public TextBoxWidget(String translationKey, Supplier getter, Consumer> implements Codec { + public Config() { + for (Field field : this.getClass().getDeclaredFields()) { + Value value = ReflectionHelper.getFieldValue(this, field); + if (value == null) continue; + + value.init(field.getName()); + } + } + /** * WARNING: Recursive method */ @@ -32,21 +51,105 @@ private static void getNestedClasses(List> nestedClasses, Class claz getNestedClasses(nestedClasses, nestedClass); } - Identifier getId(); + protected static ValueBuilder value(T defaultValue, Codec codec) { + return new ValueBuilder<>(defaultValue, codec); + } + + protected static > ValueBuilder enumValue(T defaultValue, Class clazz) { + return new ValueBuilder<>(defaultValue, CodecHelper.createEnumCodec(clazz)).packetCodec( + CodecHelper.createEnumPacketCodec(clazz) + ); + } + + protected static ValueBuilder booleanValue(boolean defaultValue) { + return new ValueBuilder<>(defaultValue, Codec.BOOL).packetCodec(PacketCodecs.BOOL); + } + + protected static RangedValueBuilder intValue(int defaultValue) { + return new RangedValueBuilder<>(defaultValue, Codec.INT, CodecHelper::clampedRangeInt).packetCodec(PacketCodecs.INTEGER); + } + + protected static RangedValueBuilder floatValue(float defaultValue) { + return new RangedValueBuilder<>(defaultValue, Codec.FLOAT, CodecHelper::clampedRangeFloat).packetCodec(PacketCodecs.FLOAT); + } + + protected static RangedValueBuilder doubleValue(double defaultValue) { + return new RangedValueBuilder<>(defaultValue, Codec.DOUBLE, CodecHelper::clampedRangeDouble).packetCodec(PacketCodecs.DOUBLE); + } + + protected static ValueBuilder stringValue(String defaultValue) { + return new ValueBuilder<>(defaultValue, Codec.STRING).packetCodec(PacketCodecs.STRING); + } + + public abstract Identifier getId(); + + @Override + public DataResult encode(T input, DynamicOps ops, T1 prefix) { + RecordBuilder builder = ops.mapBuilder(); + for (Field field : this.getClass().getDeclaredFields()) { + Value value = ReflectionHelper.getFieldValue(this, field); + if (value == null) continue; + + builder = value.encode(ops, builder); + } + + return builder.build(prefix); + } + + @Override + @SuppressWarnings("unchecked") + public DataResult> decode(DynamicOps ops, T1 input) { + boolean success = true; + + for (Field field : this.getClass().getDeclaredFields()) { + Value value = ReflectionHelper.getFieldValue(this, field); + if (value == null) continue; + + success &= value.decode(ops, input); + } + + return success ? DataResult.success(Pair.of((T) this, input)) : DataResult.error(() -> "Failed to decode config"); + } /** * Saves the config to disk. */ @SuppressWarnings("ResultOfMethodCallIgnored") - default void save() { - String json = ConfigManager.GSON.toJson(this); - List> nestedClasses = new ArrayList<>(); - getNestedClasses(nestedClasses, this.getClass()); + public void save() { + @SuppressWarnings("unchecked") + DataResult result = encodeStart(JsonOps.INSTANCE, (T) this); + if (result.error().isPresent()) { + SpecterGlobals.LOGGER.error("Failed to encode config: {}", getId()); + SpecterGlobals.LOGGER.error(result.error().toString()); + return; + } + + JsonObject object = result.result().orElseThrow().getAsJsonObject(); + + String json; + try { + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + + jsonWriter.setLenient(true); + jsonWriter.setSerializeNulls(false); + jsonWriter.setIndent(" "); + + Streams.write(object, jsonWriter); + + json = stringWriter.toString(); + } catch (IOException e) { + throw new AssertionError(e); + } Map comments = new Object2ObjectOpenHashMap<>(); - for (Field field : this.getClass().getDeclaredFields()) checkAnnotations(comments, field); - for (Class nestedClass : nestedClasses) - for (Field field : nestedClass.getDeclaredFields()) checkAnnotations(comments, field); + + for (Field field : this.getClass().getDeclaredFields()) { + Value value = ReflectionHelper.getFieldValue(this, field); + if (value == null) continue; + + value.comment().ifPresent(comment -> comments.put(field.getName(), comment)); + } List newLines = new ArrayList<>(); for (String line : json.split("\n")) { @@ -75,10 +178,11 @@ default void save() { Files.write(path, newLines); } catch (IOException e) { SpecterGlobals.LOGGER.error("Failed to save config file: {}", path, e); + } } - default Path getPath() { + public Path getPath() { return Paths.get( FabricLoader.getInstance().getConfigDir().toString(), "", @@ -86,27 +190,138 @@ default Path getPath() { ); } - private void checkAnnotations(Map comments, Field field) { - Range rangeAnnotation = field.getAnnotation(Range.class); - if (rangeAnnotation == null) return; + @SuppressWarnings("unchecked") + public T packetDecode(ByteBuf buf) { + for (Field field : this.getClass().getDeclaredFields()) { + Value value = ReflectionHelper.getFieldValue(this, field); + if (value == null) continue; + + value.packetDecode(buf); + } + + return (T) this; + } + + public void packetEncode(ByteBuf buf) { + Identifier.PACKET_CODEC.encode(buf, getId()); - if (rangeAnnotation.clamp()) { - Number value = ReflectionHelper.getFieldValue(this, field); - if (value != null) - ReflectionHelper.setFieldValue( - this, - field, - MathHelper.clamp( - value.doubleValue(), - rangeAnnotation.min(), - rangeAnnotation.max() - ) - ); + for (Field field : this.getClass().getDeclaredFields()) { + Value val = ReflectionHelper.getFieldValue(this, field); + if (val == null) continue; + + val.packetEncode(buf); } + } + + public interface Value { + T get(); + + T defaultValue(); + + void set(T value); + + @ApiStatus.Internal + void init(String name); + + RecordBuilder encode(DynamicOps ops, RecordBuilder builder); + + boolean decode(DynamicOps ops, T1 input); + + void packetDecode(ByteBuf buf); - Comment commentAnnotation = field.getAnnotation(Comment.class); - if (commentAnnotation == null) return; + void packetEncode(ByteBuf buf); - comments.put(field.getName(), commentAnnotation.value()); + Optional comment(); + + boolean sync(); + + Pair range(); + + String translationKey(Identifier configId); + } + + protected static class ValueBuilder { + protected final T defaultValue; + protected final Codec codec; + protected String comment; + protected boolean sync; + protected PacketCodec packetCodec; + + public ValueBuilder(T defaultValue, Codec codec) { + this.defaultValue = defaultValue; + this.codec = codec; + } + + public ValueBuilder comment(String comment) { + this.comment = comment; + return this; + } + + public ValueBuilder sync() { + if (packetCodec == null) throw new IllegalStateException("Packet codec must be set to enable syncing"); + this.sync = true; + return this; + } + + public ValueBuilder packetCodec(PacketCodec packetCodec) { + this.packetCodec = packetCodec; + return this; + } + + public ValueBuilder> toList() { + return new ValueBuilder<>(List.of(defaultValue), Codec.list(codec)); + } + + public Value build() { + return new ValueImpl<>(defaultValue, codec, packetCodec, comment, sync, null); + } + } + + protected static class RangedValueBuilder { + protected final T defaultValue; + protected final Codec codec; + protected String comment; + protected boolean sync; + protected PacketCodec packetCodec; + private final RangeFunction> codecRange; + protected Pair range; + + public RangedValueBuilder(T defaultValue, Codec codec, RangeFunction> codecRange) { + this.defaultValue = defaultValue; + this.codec = codec; + this.codecRange = codecRange; + } + + public RangedValueBuilder range(T min, T max) { + this.range = Pair.of(min, max); + return this; + } + + public RangedValueBuilder comment(String comment) { + this.comment = comment; + return this; + } + + public RangedValueBuilder sync() { + if (packetCodec == null) throw new IllegalStateException("Packet codec must be set to enable syncing"); + this.sync = true; + return this; + } + + public RangedValueBuilder packetCodec(PacketCodec packetCodec) { + this.packetCodec = packetCodec; + return this; + } + + public Value build() { + Codec rangeCodec = range == null ? codec : + Optional.ofNullable(codecRange).map(f -> f.apply(range.getFirst(), range.getSecond())).orElse(codec); + return new ValueImpl<>(defaultValue, rangeCodec, packetCodec, comment, sync, range); + } + + @FunctionalInterface + public interface RangeFunction { + R apply(T min, T max); + } } } diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/ConfigManager.java b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/ConfigManager.java index bae865b..31eaae2 100644 --- a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/ConfigManager.java +++ b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/ConfigManager.java @@ -1,16 +1,15 @@ package dev.spiritstudios.specter.api.config; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonSyntaxException; +import com.google.gson.JsonObject; +import com.mojang.serialization.JsonOps; import dev.spiritstudios.specter.api.core.SpecterGlobals; import dev.spiritstudios.specter.api.core.util.ReflectionHelper; -import dev.spiritstudios.specter.impl.config.NonSyncExclusionStrategy; import dev.spiritstudios.specter.impl.config.network.ConfigSyncS2CPayload; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import net.minecraft.server.MinecraftServer; import net.minecraft.util.Identifier; +import net.minecraft.util.JsonHelper; import org.jetbrains.annotations.ApiStatus; import java.io.IOException; @@ -20,13 +19,7 @@ import java.util.Map; public final class ConfigManager { - @ApiStatus.Internal - public static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); - - @ApiStatus.Internal - public static final Gson GSON_NON_SYNC = new GsonBuilder().setPrettyPrinting().addSerializationExclusionStrategy(new NonSyncExclusionStrategy()).create(); - - private static final Map configs = new Object2ObjectOpenHashMap<>(); + private static final Map> configs = new Object2ObjectOpenHashMap<>(); /** * Get a config file. If the file does not exist, it will be created and saved. @@ -36,7 +29,7 @@ public final class ConfigManager { * @param The type of the config file * @return The config file */ - public static T getConfig(Class clazz) { + public static > T getConfig(Class clazz) { T config = ReflectionHelper.instantiate(clazz); if (!Files.exists(config.getPath())) { @@ -57,18 +50,12 @@ public static T getConfig(Class clazz) { lines.removeIf(line -> line.trim().startsWith("//")); StringBuilder stringBuilder = new StringBuilder(); lines.forEach(stringBuilder::append); - T loadedConfig; - - try { - loadedConfig = GSON.fromJson(stringBuilder.toString(), clazz); - } catch (JsonSyntaxException e) { - SpecterGlobals.LOGGER.error("Failed to parse config file {}. Resetting to default values.", config.getPath().toString()); - loadedConfig = config; - } + JsonObject json = JsonHelper.deserialize(stringBuilder.toString()); + T loadedConfig = config.parse(JsonOps.INSTANCE, json).getOrThrow(); // Save to make sure any new fields are added loadedConfig.save(); - CACHED_PAYLOAD = null; + ConfigSyncS2CPayload.clearCache(); T existingConfig = getConfigById(loadedConfig.getId()); if (existingConfig != null) { @@ -83,35 +70,28 @@ public static T getConfig(Class clazz) { } @SuppressWarnings("unchecked") - public static T getConfigById(Identifier id) { + public static > T getConfigById(Identifier id) { return (T) configs.get(id); } + @SuppressWarnings("unchecked") public static void reloadConfigs() { - Map oldConfigs = new Object2ObjectOpenHashMap<>(configs); + Map> oldConfigs = new Object2ObjectOpenHashMap<>(configs); oldConfigs.values().stream().map(Config::getClass).forEach(ConfigManager::getConfig); - CACHED_PAYLOAD = null; + ConfigSyncS2CPayload.clearCache(); } public static void reloadConfigs(MinecraftServer server) { reloadConfigs(); - ConfigSyncS2CPayload payload = ConfigManager.createSyncPayload(); - server.getPlayerManager().getPlayerList().forEach(player -> ServerPlayNetworking.send(player, payload)); - } + List payloads = ConfigSyncS2CPayload.createPayloads(); - @ApiStatus.Internal - public static Map getConfigs() { - return configs; + server.getPlayerManager().getPlayerList().forEach( + player -> payloads.forEach(payload -> ServerPlayNetworking.send(player, payload))); } - private static ConfigSyncS2CPayload CACHED_PAYLOAD; - @ApiStatus.Internal - public static ConfigSyncS2CPayload createSyncPayload() { - if (CACHED_PAYLOAD != null) return CACHED_PAYLOAD; - - Map configs = getConfigs().values().stream().collect(Object2ObjectOpenHashMap::new, (map, config) -> map.put(config.getId(), GSON_NON_SYNC.toJson(config)), Map::putAll); - return CACHED_PAYLOAD = new ConfigSyncS2CPayload(configs); + public static Map> getConfigs() { + return configs; } } 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 817cf1a..0000000 --- a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/NestedConfig.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.spiritstudios.specter.api.config; - -public interface NestedConfig extends Config { - default void save() { - throw new UnsupportedOperationException("Nested configs cannot be saved"); - } -} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/annotations/Comment.java b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/annotations/Comment.java deleted file mode 100644 index 7adbf94..0000000 --- a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/annotations/Comment.java +++ /dev/null @@ -1,18 +0,0 @@ -package dev.spiritstudios.specter.api.config.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * A comment for a field in a config file. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface Comment { - /** - * The comment text. Can contain newlines using \n. - */ - String value() default ""; -} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/annotations/Range.java b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/annotations/Range.java deleted file mode 100644 index f4d0730..0000000 --- a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/annotations/Range.java +++ /dev/null @@ -1,19 +0,0 @@ -package dev.spiritstudios.specter.api.config.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * A range for a number field in a config file. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface Range { - double min() default 0; - - double max() default 1; - - boolean clamp() default false; -} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/annotations/Sync.java b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/annotations/Sync.java deleted file mode 100644 index 1624059..0000000 --- a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/annotations/Sync.java +++ /dev/null @@ -1,14 +0,0 @@ -package dev.spiritstudios.specter.api.config.annotations; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Marks a field to be synchronized with the server. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.FIELD) -public @interface Sync { -} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/NonSyncExclusionStrategy.java b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/NonSyncExclusionStrategy.java deleted file mode 100644 index 387f412..0000000 --- a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/NonSyncExclusionStrategy.java +++ /dev/null @@ -1,17 +0,0 @@ -package dev.spiritstudios.specter.impl.config; - -import com.google.gson.ExclusionStrategy; -import com.google.gson.FieldAttributes; -import dev.spiritstudios.specter.api.config.annotations.Sync; - -public class NonSyncExclusionStrategy implements ExclusionStrategy { - @Override - public boolean shouldSkipField(FieldAttributes f) { - return f.getAnnotation(Sync.class) == null; - } - - @Override - public boolean shouldSkipClass(Class clazz) { - return false; - } -} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/SpecterConfig.java b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/SpecterConfig.java index 4216056..5b9544a 100644 --- a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/SpecterConfig.java +++ b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/SpecterConfig.java @@ -6,7 +6,6 @@ import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; -import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; public class SpecterConfig implements ModInitializer { @Override @@ -17,8 +16,7 @@ public void onInitialize() { ); ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { - ConfigSyncS2CPayload payload = ConfigManager.createSyncPayload(); - ServerPlayNetworking.send(handler.getPlayer(), payload); + }); ServerLifecycleEvents.END_DATA_PACK_RELOAD.register((server, serverResourceManager, success) -> { 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 new file mode 100644 index 0000000..9aeee61 --- /dev/null +++ b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/ValueImpl.java @@ -0,0 +1,122 @@ +package dev.spiritstudios.specter.impl.config; + +import com.mojang.datafixers.util.Pair; +import com.mojang.serialization.*; +import dev.spiritstudios.specter.api.config.Config; +import dev.spiritstudios.specter.api.core.SpecterGlobals; +import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.util.Identifier; + +import java.util.Optional; + +public class ValueImpl implements Config.Value { + private final T defaultValue; + private final Codec codec; + private final PacketCodec packetCodec; + private final boolean sync; + private final String comment; + private final Pair range; + + private MapCodec mapCodec; + private String name; + + private T value; + + public ValueImpl(T defaultValue, + Codec codec, + PacketCodec packetCodec, + String comment, + boolean sync, + Pair range + ) { + this.defaultValue = defaultValue; + this.codec = codec; + this.comment = comment; + this.sync = sync; + this.packetCodec = packetCodec; + this.range = range; + + 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 = 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 Pair range() { + return range; + } + + @Override + public String translationKey(Identifier configId) { + return String.format("config.%s.%s.%s", configId.getNamespace(), configId.getPath(), 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 c0cfcf6..227cbc5 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 @@ -1,27 +1,52 @@ package dev.spiritstudios.specter.impl.config.network; -import dev.spiritstudios.specter.api.core.SpecterGlobals; +import dev.spiritstudios.specter.api.config.Config; +import dev.spiritstudios.specter.api.config.ConfigManager; import io.netty.buffer.ByteBuf; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.minecraft.network.codec.PacketCodec; -import net.minecraft.network.codec.PacketCodecs; import net.minecraft.network.packet.CustomPayload; import net.minecraft.util.Identifier; -import java.util.Map; - -public record ConfigSyncS2CPayload(Map configs) implements CustomPayload { - public static final CustomPayload.Id ID = new CustomPayload.Id<>(Identifier.of(SpecterGlobals.MODID, "config_sync")); - public static final PacketCodec CODEC = - PacketCodec.tuple( - PacketCodecs.map(Object2ObjectOpenHashMap::new, Identifier.PACKET_CODEC, PacketCodecs.STRING), - ConfigSyncS2CPayload::configs, - ConfigSyncS2CPayload::new - ); - - @Override - public Id getId() { - return ConfigSyncS2CPayload.ID; - } -} +import java.util.ArrayList; +import java.util.List; + +import static dev.spiritstudios.specter.api.core.SpecterGlobals.MODID; + +public record ConfigSyncS2CPayload(Config config) implements CustomPayload { + public static final Id ID = new Id<>(Identifier.of(MODID, "config_sync")); + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodec.of( + Config::packetEncode, + buf -> { + Identifier id = Identifier.PACKET_CODEC.decode(buf); + return ConfigManager.getConfigById(id).packetDecode(buf); + } + ), + ConfigSyncS2CPayload::config, + ConfigSyncS2CPayload::new + ); + + private static final List CACHE = new ArrayList<>(); + + public static void clearCache() { + CACHE.clear(); + } + public static List createPayloads() { + if (CACHE.isEmpty()) { + CACHE.addAll( + ConfigManager.getConfigs().values().stream() + .map(ConfigSyncS2CPayload::new) + .toList() + ); + } + + + return CACHE; + } + + @Override + public Id getId() { + return ID; + } +} diff --git a/specter-config/src/testmod/java/dev/spiritstudios/testmod/CreateTestConfig.java b/specter-config/src/testmod/java/dev/spiritstudios/testmod/CreateTestConfig.java index 3dfa7e7..ad34cf5 100644 --- a/specter-config/src/testmod/java/dev/spiritstudios/testmod/CreateTestConfig.java +++ b/specter-config/src/testmod/java/dev/spiritstudios/testmod/CreateTestConfig.java @@ -1,60 +1,40 @@ package dev.spiritstudios.testmod; import dev.spiritstudios.specter.api.config.Config; -import dev.spiritstudios.specter.api.config.NestedConfig; -import dev.spiritstudios.specter.api.config.annotations.Comment; -import dev.spiritstudios.specter.api.config.annotations.Range; -import dev.spiritstudios.specter.api.config.annotations.Sync; import net.minecraft.util.Identifier; -public class CreateTestConfig implements Config { +public class CreateTestConfig extends Config { @Override public Identifier getId() { return Identifier.of("specter-config-testmod", "createtestconfig"); } - @Comment("This is a test string") - @Sync - public String testString = "test"; - @Comment("This is a test int") - @Range(min = 2, max = 10) - public int testInt = 1; - @Comment("This is a test bool") - public boolean testBool = true; - @Comment("This is a test double") - public double testDouble = 1.0; - @Comment("This is a test float") - public float testFloat = 1.0f; + public static Value testString = stringValue("test") + .comment("This is a test string") + .sync() + .build(); - @Comment("This is a nested class") - public Nested nested = new Nested(); + public static Value testInt = intValue(1) + .comment("This is a test int") + .range(2, 10) + .build(); - @Comment("This is a test enum") - public TestEnum testEnum = TestEnum.TEST_1; + public static Value testBool = booleanValue(true) + .comment("This is a test bool") + .build(); - public static class Nested implements NestedConfig { - @Comment("This is a nested string\n" + - "With a new line") - public String nestedString = "test"; + public static Value testDouble = doubleValue(1.0) + .comment("This is a test double") + .build(); - @Override - public Identifier getId() { - return Identifier.of("testmod", "nested"); - } + public static Value testFloat = floatValue(1.0f) + .comment("This is a test float") + .build(); - @Comment("This is a nested nested class") - public NestedNested nestedNested = new NestedNested(); + public static Value testEnum = enumValue(TestEnum.TEST_1, TestEnum.class) + .comment("This is a test enum") + .build(); - public static class NestedNested implements NestedConfig { - @Comment("This is a nested nested string") - public String nestedNestedString = "test"; - - @Override - public Identifier getId() { - return Identifier.of("testmod", "nestednested"); - } - } - } public enum TestEnum { TEST_1, diff --git a/specter-config/src/testmod/java/dev/spiritstudios/testmod/GetTestConfig.java b/specter-config/src/testmod/java/dev/spiritstudios/testmod/GetTestConfig.java index 5e5519a..4170856 100644 --- a/specter-config/src/testmod/java/dev/spiritstudios/testmod/GetTestConfig.java +++ b/specter-config/src/testmod/java/dev/spiritstudios/testmod/GetTestConfig.java @@ -1,60 +1,40 @@ package dev.spiritstudios.testmod; import dev.spiritstudios.specter.api.config.Config; -import dev.spiritstudios.specter.api.config.NestedConfig; -import dev.spiritstudios.specter.api.config.annotations.Comment; -import dev.spiritstudios.specter.api.config.annotations.Range; -import dev.spiritstudios.specter.api.config.annotations.Sync; import net.minecraft.util.Identifier; -public class GetTestConfig implements Config { +public class GetTestConfig extends Config { @Override public Identifier getId() { return Identifier.of("specter-config-testmod", "gettestconfig"); } - @Comment("This is a test string") - @Sync - public String testString = "test"; - @Comment("This is a test int") - @Range(min = 2, max = 10) - public int testInt = 1; - @Comment("This is a test bool") - public boolean testBool = true; - @Comment("This is a test double") - public double testDouble = 1.0; - @Comment("This is a test float") - public float testFloat = 1.0f; + public static Value testString = stringValue("test") + .comment("This is a test string") + .sync() + .build(); - @Comment("This is a nested class") - public CreateTestConfig.Nested nested = new CreateTestConfig.Nested(); + public static Value testInt = intValue(2) + .comment("This is a test int") + .range(2, 10) + .build(); - @Comment("This is a test enum") - public CreateTestConfig.TestEnum testEnum = CreateTestConfig.TestEnum.TEST_1; + public static Value testBool = booleanValue(true) + .comment("This is a test bool") + .build(); - public static class Nested implements NestedConfig { - @Comment("This is a nested string\n" + - "With a new line") - public String nestedString = "test"; + public static Value testDouble = doubleValue(1.0) + .comment("This is a test double") + .build(); - @Override - public Identifier getId() { - return Identifier.of("testmod", "nested"); - } + public static Value testFloat = floatValue(1.0f) + .comment("This is a test float") + .build(); - @Comment("This is a nested nested class") - public CreateTestConfig.Nested.NestedNested nestedNested = new CreateTestConfig.Nested.NestedNested(); + public static Value testEnum = enumValue(TestEnum.TEST_1, TestEnum.class) + .comment("This is a test enum") + .build(); - public static class NestedNested implements NestedConfig { - @Comment("This is a nested nested string") - public String nestedNestedString = "test"; - - @Override - public Identifier getId() { - return Identifier.of("testmod", "nestednested"); - } - } - } public enum TestEnum { TEST_1, diff --git a/specter-config/src/testmod/java/dev/spiritstudios/testmod/SpecterConfigGameTest.java b/specter-config/src/testmod/java/dev/spiritstudios/testmod/SpecterConfigGameTest.java index da428d5..e9b9317 100644 --- a/specter-config/src/testmod/java/dev/spiritstudios/testmod/SpecterConfigGameTest.java +++ b/specter-config/src/testmod/java/dev/spiritstudios/testmod/SpecterConfigGameTest.java @@ -39,7 +39,7 @@ public void testSaveConfigFile(TestContext context) throws IOException { Files.deleteIfExists(path); GetTestConfig config = ConfigManager.getConfig(GetTestConfig.class); - config.testString = "test2"; + GetTestConfig.testString.set("test2"); config.save(); GetTestConfig newConfig = ConfigManager.getConfig(GetTestConfig.class); diff --git a/specter-config/src/testmodClient/java/dev/spiritstudios/testmod/SpecterConfigTestmodClient.java b/specter-config/src/testmodClient/java/dev/spiritstudios/testmod/SpecterConfigTestmodClient.java index 860cbf1..32ce8ed 100644 --- a/specter-config/src/testmodClient/java/dev/spiritstudios/testmod/SpecterConfigTestmodClient.java +++ b/specter-config/src/testmodClient/java/dev/spiritstudios/testmod/SpecterConfigTestmodClient.java @@ -1,11 +1,14 @@ package dev.spiritstudios.testmod; import dev.spiritstudios.specter.api.ModMenuHelper; +import dev.spiritstudios.specter.api.config.ConfigManager; import net.fabricmc.api.ClientModInitializer; public class SpecterConfigTestmodClient implements ClientModInitializer { + public static final GetTestConfig CONFIG = ConfigManager.getConfig(GetTestConfig.class); + @Override public void onInitializeClient() { - ModMenuHelper.addConfig("specter-config-testmod", CreateTestConfig.class); + ModMenuHelper.addConfig("specter-config-testmod", CONFIG.getId()); } } diff --git a/specter-core/src/main/java/dev/spiritstudios/specter/api/core/util/CodecHelper.java b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/util/CodecHelper.java new file mode 100644 index 0000000..40b6117 --- /dev/null +++ b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/util/CodecHelper.java @@ -0,0 +1,51 @@ +package dev.spiritstudios.specter.api.core.util; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.codec.PacketCodecs; + +public final class CodecHelper { + public static > Codec createEnumCodec(Class clazz) { + return Codec.STRING.comapFlatMap(str -> { + T value; + try { + value = Enum.valueOf(clazz, str); + } catch (IllegalArgumentException e) { + return DataResult.error(() -> "Unknown enum value: %s".formatted(str)); + } + + return DataResult.success(value); + }, Enum::name); + } + + // TODO: Make this use the enum ordinal instead of the name + public static > PacketCodec createEnumPacketCodec(Class clazz) { + return PacketCodecs.codec(createEnumCodec(clazz)); + } + + public static Codec clampedRangeInt(int min, int max) { + return Codec.INT.xmap( + value -> Math.clamp(value, min, max), + value -> Math.clamp(value, min, max) + ); + } + + public static Codec clampedRangeFloat(float min, float max) { + return Codec.FLOAT.xmap( + value -> Math.clamp(value, min, max), + value -> Math.clamp(value, min, max) + ); + } + + public static Codec clampedRangeDouble(double min, double max) { + return Codec.DOUBLE.xmap( + value -> Math.clamp(value, min, max), + value -> Math.clamp(value, min, max) + ); + } + + private CodecHelper() { + } +} From e1f66d0626aacc296584f4a36730d37cb986a2cc Mon Sep 17 00:00:00 2001 From: CallMeEchoCodes Date: Fri, 6 Sep 2024 14:30:49 +1000 Subject: [PATCH 2/9] feat: rework config manager and finish syncing (i think) --- .../impl/config/SpecterConfigClient.java | 7 +- .../impl/config/SpecterConfigModMenu.java | 3 +- .../specter/impl/config/gui/ConfigScreen.java | 46 +++++---- .../specter/api/config/Config.java | 80 ++++++++++++--- .../specter/api/config/ConfigManager.java | 97 ------------------- .../specter/impl/config/ConfigManager.java | 31 ++++++ .../specter/impl/config/SpecterConfig.java | 9 +- .../config/network/ConfigSyncS2CPayload.java | 84 +++++++++------- .../testmod/CreateTestConfig.java | 14 +-- .../spiritstudios/testmod/GetTestConfig.java | 14 +-- .../testmod/SpecterConfigGameTest.java | 15 ++- .../testmod/SpecterConfigTestmodClient.java | 5 +- 12 files changed, 203 insertions(+), 202 deletions(-) delete mode 100644 specter-config/src/main/java/dev/spiritstudios/specter/api/config/ConfigManager.java create mode 100644 specter-config/src/main/java/dev/spiritstudios/specter/impl/config/ConfigManager.java diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigClient.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigClient.java index ff0b822..6b01ba8 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigClient.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigClient.java @@ -1,6 +1,6 @@ package dev.spiritstudios.specter.impl.config; -import dev.spiritstudios.specter.api.config.ConfigManager; +import dev.spiritstudios.specter.api.core.SpecterGlobals; import dev.spiritstudios.specter.impl.config.network.ConfigSyncS2CPayload; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents; @@ -9,10 +9,11 @@ public class SpecterConfigClient implements ClientModInitializer { @Override public void onInitializeClient() { - ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> ConfigManager.reloadConfigs()); + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> ConfigManager.reload()); ClientPlayNetworking.registerGlobalReceiver(ConfigSyncS2CPayload.ID, (payload, context) -> { - // We don't need to do anything here, the codec will handle it + SpecterGlobals.LOGGER.info("Received config sync packet"); + SpecterGlobals.LOGGER.info("Payload: {}", payload); }); } } diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigModMenu.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigModMenu.java index 1fefde4..2223272 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigModMenu.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigModMenu.java @@ -3,7 +3,6 @@ import com.terraformersmc.modmenu.api.ConfigScreenFactory; import com.terraformersmc.modmenu.api.ModMenuApi; import dev.spiritstudios.specter.api.ModMenuHelper; -import dev.spiritstudios.specter.api.config.ConfigManager; import dev.spiritstudios.specter.impl.config.gui.ConfigScreen; import java.util.Map; @@ -19,7 +18,7 @@ public Map> getProvidedConfigScreenFactories() { Collectors.toMap( Map.Entry::getKey, entry -> - parent -> new ConfigScreen(ConfigManager.getConfigById(entry.getValue()), parent) + parent -> new ConfigScreen(ConfigManager.getConfig(entry.getValue()), parent) ) ); } diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java index 85187d4..e5eec5b 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java @@ -12,7 +12,6 @@ import net.minecraft.text.Text; import net.minecraft.util.Identifier; -import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -24,7 +23,7 @@ public class ConfigScreen extends Screen { private final Screen parent; public ConfigScreen(Config config, Screen parent) { - super(Text.translatable("config.%s.title".formatted(config.getId()))); + super(Text.translatable("config.%s.%s.title".formatted(config.getId().getNamespace(), config.getId().getPath()))); this.config = config; this.parent = parent; } @@ -32,23 +31,38 @@ public ConfigScreen(Config config, Screen parent) { @Override protected void init() { super.init(); + if (this.client == null) close(); OptionsScrollableWidget scrollableWidget = new OptionsScrollableWidget(this.client, this.width, this.height - 64, 32, 25); List options = new ArrayList<>(); - Arrays.stream(config.getClass().getDeclaredFields()) + List> values = Arrays.stream(config.getClass().getDeclaredFields()) .filter(field -> field.getType().isAssignableFrom(Config.Value.class)) .map(field -> (Config.Value) ReflectionHelper.getFieldValue(config, field)) .filter(Objects::nonNull) - .forEach(option -> { - BiFunction, Identifier, ? extends ClickableWidget> factory = ConfigScreenManager.getWidgetFactory(option, this.config.getId()); - if (factory == null) { - SpecterGlobals.LOGGER.warn("No widget factory found for {}", option.defaultValue().getClass().getSimpleName()); - return; - } + .toList(); - ClickableWidget widget = factory.apply(option, this.config.getId()); - if (widget != null) options.add(widget); - }); + + boolean failed = false; + for (Config.Value option : values) { + if (!option.sync() || this.client.player == null || this.client.isInSingleplayer()) continue; + + this.client.player.sendMessage(Text.of("Configs cannot be edited in multiplayer"), true); + this.client.setScreen(this.parent); + failed = true; + break; + } + if (failed) return; + + values.forEach(option -> { + BiFunction, Identifier, ? extends ClickableWidget> factory = ConfigScreenManager.getWidgetFactory(option, this.config.getId()); + if (factory == null) { + SpecterGlobals.LOGGER.warn("No widget factory found for {}", option.defaultValue().getClass().getSimpleName()); + return; + } + + ClickableWidget widget = factory.apply(option, this.config.getId()); + if (widget != null) options.add(widget); + }); scrollableWidget.addOptions(Arrays.copyOf(options.toArray(), options.size(), ClickableWidget[].class)); this.addDrawableChild(scrollableWidget); @@ -64,16 +78,8 @@ public void close() { } public void save() { -// if (config instanceof NestedConfig && this.parent instanceof ConfigScreen) { -// ((ConfigScreen) this.parent).save(); -// return; -// } config.save(); } - - private String getTranslationKey(Field field) { - return "config." + this.config.getId() + "." + field.getName(); - } @Override public void render(DrawContext context, int mouseX, int mouseY, float delta) { 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 6730221..4ed6b31 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 @@ -2,6 +2,8 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; import com.google.gson.internal.Streams; import com.google.gson.stream.JsonWriter; import com.mojang.datafixers.util.Pair; @@ -9,6 +11,7 @@ import dev.spiritstudios.specter.api.core.SpecterGlobals; import dev.spiritstudios.specter.api.core.util.CodecHelper; import dev.spiritstudios.specter.api.core.util.ReflectionHelper; +import dev.spiritstudios.specter.impl.config.ConfigManager; import dev.spiritstudios.specter.impl.config.ValueImpl; import io.netty.buffer.ByteBuf; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; @@ -21,6 +24,7 @@ import java.io.IOException; import java.io.StringWriter; import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -33,7 +37,11 @@ * A configuration file that can be saved to disk. */ public abstract class Config> implements Codec { - public Config() { + protected Config() { + if (ConfigManager.getConfig(getId()) != null) + throw new IllegalStateException("Config with id %s already exists".formatted(getId())); + + ConfigManager.registerConfig(getId(), this); for (Field field : this.getClass().getDeclaredFields()) { Value value = ReflectionHelper.getFieldValue(this, field); if (value == null) continue; @@ -87,6 +95,8 @@ protected static ValueBuilder stringValue(String defaultValue) { public DataResult encode(T input, DynamicOps ops, T1 prefix) { RecordBuilder builder = ops.mapBuilder(); for (Field field : this.getClass().getDeclaredFields()) { + if (!Value.class.isAssignableFrom(field.getType()) || Modifier.isStatic(field.getModifiers())) continue; + Value value = ReflectionHelper.getFieldValue(this, field); if (value == null) continue; @@ -102,6 +112,8 @@ public DataResult> decode(DynamicOps ops, T1 input) { boolean success = true; for (Field field : this.getClass().getDeclaredFields()) { + if (!Value.class.isAssignableFrom(field.getType()) || Modifier.isStatic(field.getModifiers())) continue; + Value value = ReflectionHelper.getFieldValue(this, field); if (value == null) continue; @@ -125,26 +137,26 @@ public void save() { } JsonObject object = result.result().orElseThrow().getAsJsonObject(); + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); - String json; - try { - StringWriter stringWriter = new StringWriter(); - JsonWriter jsonWriter = new JsonWriter(stringWriter); - - jsonWriter.setLenient(true); - jsonWriter.setSerializeNulls(false); - jsonWriter.setIndent(" "); + jsonWriter.setLenient(true); + jsonWriter.setSerializeNulls(false); + jsonWriter.setIndent(" "); + try { Streams.write(object, jsonWriter); - - json = stringWriter.toString(); } catch (IOException e) { - throw new AssertionError(e); + throw new RuntimeException(e); } + String json = stringWriter.toString(); + Map comments = new Object2ObjectOpenHashMap<>(); for (Field field : this.getClass().getDeclaredFields()) { + if (!Value.class.isAssignableFrom(field.getType()) || Modifier.isStatic(field.getModifiers())) continue; + Value value = ReflectionHelper.getFieldValue(this, field); if (value == null) continue; @@ -178,7 +190,39 @@ public void save() { Files.write(path, newLines); } catch (IOException e) { SpecterGlobals.LOGGER.error("Failed to save config file: {}", path, e); + } + } + + public void load() { + if (!Files.exists(getPath())) { + save(); + return; + } + + List lines; + try { + lines = Files.readAllLines(getPath()); + } catch (IOException e) { + SpecterGlobals.LOGGER.error("Failed to load config file {}. Default values will be used instead.", getPath().toString()); + return; + } + lines.removeIf(line -> line.trim().startsWith("//")); + String json = String.join("\n", lines); + + JsonElement jsonElement; + try { + jsonElement = JsonParser.parseString(json); + } catch (JsonSyntaxException e) { + SpecterGlobals.LOGGER.error("Failed to parse config file: {}", getPath()); + SpecterGlobals.LOGGER.error(e.toString()); + return; + } + + DataResult result = parse(JsonOps.INSTANCE, jsonElement); + if (result.error().isPresent()) { + SpecterGlobals.LOGGER.error("Failed to decode config file: {}", getPath()); + SpecterGlobals.LOGGER.error(result.error().toString()); } } @@ -193,8 +237,11 @@ public Path getPath() { @SuppressWarnings("unchecked") public T packetDecode(ByteBuf buf) { for (Field field : this.getClass().getDeclaredFields()) { + if (!Value.class.isAssignableFrom(field.getType()) || Modifier.isStatic(field.getModifiers())) continue; + Value value = ReflectionHelper.getFieldValue(this, field); if (value == null) continue; + if (!value.sync()) continue; value.packetDecode(buf); } @@ -206,10 +253,13 @@ public void packetEncode(ByteBuf buf) { Identifier.PACKET_CODEC.encode(buf, getId()); for (Field field : this.getClass().getDeclaredFields()) { - Value val = ReflectionHelper.getFieldValue(this, field); - if (val == null) continue; + if (!Value.class.isAssignableFrom(field.getType()) || Modifier.isStatic(field.getModifiers())) continue; + + Value value = ReflectionHelper.getFieldValue(this, field); + if (value == null) continue; + if (!value.sync()) continue; - val.packetEncode(buf); + value.packetEncode(buf); } } diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/ConfigManager.java b/specter-config/src/main/java/dev/spiritstudios/specter/api/config/ConfigManager.java deleted file mode 100644 index 31eaae2..0000000 --- a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/ConfigManager.java +++ /dev/null @@ -1,97 +0,0 @@ -package dev.spiritstudios.specter.api.config; - -import com.google.gson.JsonObject; -import com.mojang.serialization.JsonOps; -import dev.spiritstudios.specter.api.core.SpecterGlobals; -import dev.spiritstudios.specter.api.core.util.ReflectionHelper; -import dev.spiritstudios.specter.impl.config.network.ConfigSyncS2CPayload; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; -import net.minecraft.server.MinecraftServer; -import net.minecraft.util.Identifier; -import net.minecraft.util.JsonHelper; -import org.jetbrains.annotations.ApiStatus; - -import java.io.IOException; -import java.lang.reflect.Field; -import java.nio.file.Files; -import java.util.List; -import java.util.Map; - -public final class ConfigManager { - private static final Map> configs = new Object2ObjectOpenHashMap<>(); - - /** - * Get a config file. If the file does not exist, it will be created and saved. - * Could also be described as a load function. - * - * @param clazz The class of the config file - * @param The type of the config file - * @return The config file - */ - public static > T getConfig(Class clazz) { - T config = ReflectionHelper.instantiate(clazz); - - if (!Files.exists(config.getPath())) { - config.save(); - configs.put(config.getId(), config); - return config; - } - - List lines; - try { - lines = Files.readAllLines(config.getPath()); - } catch (IOException e) { - SpecterGlobals.LOGGER.error("Failed to load config file {}. Default values will be used instead.", config.getPath().toString()); - configs.put(config.getId(), config); - return config; - } - - lines.removeIf(line -> line.trim().startsWith("//")); - StringBuilder stringBuilder = new StringBuilder(); - lines.forEach(stringBuilder::append); - JsonObject json = JsonHelper.deserialize(stringBuilder.toString()); - T loadedConfig = config.parse(JsonOps.INSTANCE, json).getOrThrow(); - - // Save to make sure any new fields are added - loadedConfig.save(); - ConfigSyncS2CPayload.clearCache(); - - T existingConfig = getConfigById(loadedConfig.getId()); - if (existingConfig != null) { - for (Field field : clazz.getDeclaredFields()) - ReflectionHelper.setFieldValue(existingConfig, field, ReflectionHelper.getFieldValue(loadedConfig, field)); - - return existingConfig; - } - - configs.put(loadedConfig.getId(), loadedConfig); - return loadedConfig; - } - - @SuppressWarnings("unchecked") - public static > T getConfigById(Identifier id) { - return (T) configs.get(id); - } - - @SuppressWarnings("unchecked") - public static void reloadConfigs() { - Map> oldConfigs = new Object2ObjectOpenHashMap<>(configs); - oldConfigs.values().stream().map(Config::getClass).forEach(ConfigManager::getConfig); - - ConfigSyncS2CPayload.clearCache(); - } - - public static void reloadConfigs(MinecraftServer server) { - reloadConfigs(); - List payloads = ConfigSyncS2CPayload.createPayloads(); - - server.getPlayerManager().getPlayerList().forEach( - player -> payloads.forEach(payload -> ServerPlayNetworking.send(player, payload))); - } - - @ApiStatus.Internal - public static Map> getConfigs() { - return configs; - } -} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/ConfigManager.java b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/ConfigManager.java new file mode 100644 index 0000000..e3ca027 --- /dev/null +++ b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/ConfigManager.java @@ -0,0 +1,31 @@ +package dev.spiritstudios.specter.impl.config; + +import dev.spiritstudios.specter.api.config.Config; +import dev.spiritstudios.specter.impl.config.network.ConfigSyncS2CPayload; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.util.Identifier; + +import java.util.List; +import java.util.Map; + +public final class ConfigManager { + private static final Map> configs = new Object2ObjectOpenHashMap<>(); + + public static void registerConfig(Identifier id, Config config) { + configs.put(id, config); + } + + public static Config getConfig(Identifier id) { + return configs.get(id); + } + + public static void reload() { + configs.values().forEach(Config::load); + ConfigSyncS2CPayload.clearCache(); + } + + + public static List> getConfigs() { + return List.copyOf(configs.values()); + } +} diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/SpecterConfig.java b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/SpecterConfig.java index 5b9544a..38eac06 100644 --- a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/SpecterConfig.java +++ b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/SpecterConfig.java @@ -1,12 +1,13 @@ package dev.spiritstudios.specter.impl.config; -import dev.spiritstudios.specter.api.config.ConfigManager; import dev.spiritstudios.specter.impl.config.network.ConfigSyncS2CPayload; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import java.util.List; + public class SpecterConfig implements ModInitializer { @Override public void onInitialize() { @@ -16,11 +17,13 @@ public void onInitialize() { ); ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { - + List payloads = ConfigSyncS2CPayload.createPayloads(); + payloads.forEach(sender::sendPacket); }); ServerLifecycleEvents.END_DATA_PACK_RELOAD.register((server, serverResourceManager, success) -> { - ConfigManager.reloadConfigs(server); + ConfigManager.reload(); + ConfigSyncS2CPayload.sendPayloadsToAll(server); }); } } 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 227cbc5..8638c0a 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 @@ -1,10 +1,12 @@ package dev.spiritstudios.specter.impl.config.network; import dev.spiritstudios.specter.api.config.Config; -import dev.spiritstudios.specter.api.config.ConfigManager; +import dev.spiritstudios.specter.impl.config.ConfigManager; import io.netty.buffer.ByteBuf; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import net.minecraft.network.codec.PacketCodec; import net.minecraft.network.packet.CustomPayload; +import net.minecraft.server.MinecraftServer; import net.minecraft.util.Identifier; import java.util.ArrayList; @@ -13,40 +15,48 @@ import static dev.spiritstudios.specter.api.core.SpecterGlobals.MODID; public record ConfigSyncS2CPayload(Config config) implements CustomPayload { - public static final Id ID = new Id<>(Identifier.of(MODID, "config_sync")); - public static final PacketCodec CODEC = PacketCodec.tuple( - PacketCodec.of( - Config::packetEncode, - buf -> { - Identifier id = Identifier.PACKET_CODEC.decode(buf); - return ConfigManager.getConfigById(id).packetDecode(buf); - } - ), - ConfigSyncS2CPayload::config, - ConfigSyncS2CPayload::new - ); - - private static final List CACHE = new ArrayList<>(); - - public static void clearCache() { - CACHE.clear(); - } - - public static List createPayloads() { - if (CACHE.isEmpty()) { - CACHE.addAll( - ConfigManager.getConfigs().values().stream() - .map(ConfigSyncS2CPayload::new) - .toList() - ); - } - - - return CACHE; - } - - @Override - public Id getId() { - return ID; - } + public static final Id ID = new Id<>(Identifier.of(MODID, "config_sync")); + public static final PacketCodec CODEC = PacketCodec.tuple( + PacketCodec.of( + Config::packetEncode, + buf -> { + Identifier id = Identifier.PACKET_CODEC.decode(buf); + Config config = ConfigManager.getConfig(id); + config.save(); + return config.packetDecode(buf); + } + ), + ConfigSyncS2CPayload::config, + ConfigSyncS2CPayload::new + ); + + private static final List CACHE = new ArrayList<>(); + + public static void clearCache() { + CACHE.clear(); + } + + public static List createPayloads() { + if (CACHE.isEmpty()) { + CACHE.addAll( + ConfigManager.getConfigs().stream() + .map(ConfigSyncS2CPayload::new) + .toList() + ); + } + + return CACHE; + } + + public static void sendPayloadsToAll(MinecraftServer server) { + List payloads = ConfigSyncS2CPayload.createPayloads(); + + server.getPlayerManager().getPlayerList().forEach( + player -> payloads.forEach(payload -> ServerPlayNetworking.send(player, payload))); + } + + @Override + public Id getId() { + return ID; + } } diff --git a/specter-config/src/testmod/java/dev/spiritstudios/testmod/CreateTestConfig.java b/specter-config/src/testmod/java/dev/spiritstudios/testmod/CreateTestConfig.java index ad34cf5..80e70cd 100644 --- a/specter-config/src/testmod/java/dev/spiritstudios/testmod/CreateTestConfig.java +++ b/specter-config/src/testmod/java/dev/spiritstudios/testmod/CreateTestConfig.java @@ -9,29 +9,31 @@ public Identifier getId() { return Identifier.of("specter-config-testmod", "createtestconfig"); } - public static Value testString = stringValue("test") + public static final CreateTestConfig INSTANCE = new CreateTestConfig(); + + public Value testString = stringValue("test") .comment("This is a test string") .sync() .build(); - public static Value testInt = intValue(1) + public Value testInt = intValue(1) .comment("This is a test int") .range(2, 10) .build(); - public static Value testBool = booleanValue(true) + public Value testBool = booleanValue(true) .comment("This is a test bool") .build(); - public static Value testDouble = doubleValue(1.0) + public Value testDouble = doubleValue(1.0) .comment("This is a test double") .build(); - public static Value testFloat = floatValue(1.0f) + public Value testFloat = floatValue(1.0f) .comment("This is a test float") .build(); - public static Value testEnum = enumValue(TestEnum.TEST_1, TestEnum.class) + public Value testEnum = enumValue(TestEnum.TEST_1, TestEnum.class) .comment("This is a test enum") .build(); diff --git a/specter-config/src/testmod/java/dev/spiritstudios/testmod/GetTestConfig.java b/specter-config/src/testmod/java/dev/spiritstudios/testmod/GetTestConfig.java index 4170856..58f3a21 100644 --- a/specter-config/src/testmod/java/dev/spiritstudios/testmod/GetTestConfig.java +++ b/specter-config/src/testmod/java/dev/spiritstudios/testmod/GetTestConfig.java @@ -9,29 +9,31 @@ public Identifier getId() { return Identifier.of("specter-config-testmod", "gettestconfig"); } - public static Value testString = stringValue("test") + public static final GetTestConfig INSTANCE = new GetTestConfig(); + + public Value testString = stringValue("test") .comment("This is a test string") .sync() .build(); - public static Value testInt = intValue(2) + public Value testInt = intValue(2) .comment("This is a test int") .range(2, 10) .build(); - public static Value testBool = booleanValue(true) + public Value testBool = booleanValue(true) .comment("This is a test bool") .build(); - public static Value testDouble = doubleValue(1.0) + public Value testDouble = doubleValue(1.0) .comment("This is a test double") .build(); - public static Value testFloat = floatValue(1.0f) + public Value testFloat = floatValue(1.0f) .comment("This is a test float") .build(); - public static Value testEnum = enumValue(TestEnum.TEST_1, TestEnum.class) + public Value testEnum = enumValue(TestEnum.TEST_1, TestEnum.class) .comment("This is a test enum") .build(); diff --git a/specter-config/src/testmod/java/dev/spiritstudios/testmod/SpecterConfigGameTest.java b/specter-config/src/testmod/java/dev/spiritstudios/testmod/SpecterConfigGameTest.java index e9b9317..4a2238b 100644 --- a/specter-config/src/testmod/java/dev/spiritstudios/testmod/SpecterConfigGameTest.java +++ b/specter-config/src/testmod/java/dev/spiritstudios/testmod/SpecterConfigGameTest.java @@ -1,6 +1,5 @@ package dev.spiritstudios.testmod; -import dev.spiritstudios.specter.api.config.ConfigManager; import net.fabricmc.fabric.api.gametest.v1.FabricGameTest; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.test.GameTest; @@ -22,10 +21,10 @@ public void testCreateConfigFile(TestContext context) throws IOException { ); Files.deleteIfExists(path); - CreateTestConfig config = ConfigManager.getConfig(CreateTestConfig.class); + CreateTestConfig.INSTANCE.load(); context.assertTrue(Files.exists(path), "Config file does not exist"); - context.assertTrue(config.testString.equals("test"), "String is not equal to test, Make sure you haven't modified the config"); + context.assertTrue(CreateTestConfig.INSTANCE.testString.get().equals("test"), "String is not equal to test, Make sure you haven't modified the config"); context.complete(); } @@ -38,14 +37,12 @@ public void testSaveConfigFile(TestContext context) throws IOException { ); Files.deleteIfExists(path); - GetTestConfig config = ConfigManager.getConfig(GetTestConfig.class); - GetTestConfig.testString.set("test2"); - config.save(); - - GetTestConfig newConfig = ConfigManager.getConfig(GetTestConfig.class); + GetTestConfig.INSTANCE.testString.set("test2"); + GetTestConfig.INSTANCE.save(); + GetTestConfig.INSTANCE.load(); context.assertTrue(Files.exists(path), "Config file does not exist"); - context.assertTrue(newConfig.testString.equals("test2"), "String is not equal to test2, Make sure you haven't modified the config"); + context.assertTrue(GetTestConfig.INSTANCE.testString.get().equals("test2"), "String is not equal to test2, Make sure you haven't modified the config"); context.complete(); } } diff --git a/specter-config/src/testmodClient/java/dev/spiritstudios/testmod/SpecterConfigTestmodClient.java b/specter-config/src/testmodClient/java/dev/spiritstudios/testmod/SpecterConfigTestmodClient.java index 32ce8ed..afebf0b 100644 --- a/specter-config/src/testmodClient/java/dev/spiritstudios/testmod/SpecterConfigTestmodClient.java +++ b/specter-config/src/testmodClient/java/dev/spiritstudios/testmod/SpecterConfigTestmodClient.java @@ -1,14 +1,11 @@ package dev.spiritstudios.testmod; import dev.spiritstudios.specter.api.ModMenuHelper; -import dev.spiritstudios.specter.api.config.ConfigManager; import net.fabricmc.api.ClientModInitializer; public class SpecterConfigTestmodClient implements ClientModInitializer { - public static final GetTestConfig CONFIG = ConfigManager.getConfig(GetTestConfig.class); - @Override public void onInitializeClient() { - ModMenuHelper.addConfig("specter-config-testmod", CONFIG.getId()); + ModMenuHelper.addConfig("specter-config-testmod", GetTestConfig.INSTANCE.getId()); } } From 745b3bb3ba63a385657ea152bc7a1d688cfcccf2 Mon Sep 17 00:00:00 2001 From: CallMeEchoCodes Date: Fri, 6 Sep 2024 20:22:58 +1000 Subject: [PATCH 3/9] fix: many bugfixes --- .../impl/config/SpecterConfigClient.java | 4 +- .../specter/impl/config/gui/ConfigScreen.java | 8 +- .../specter/api/config/Config.java | 132 ++++++++++-------- .../config/network/ConfigSyncS2CPayload.java | 2 + .../testmod/CreateTestConfig.java | 2 +- .../spiritstudios/testmod/GetTestConfig.java | 4 +- .../testmod/SpecterConfigGameTest.java | 7 +- .../specter/api/core/SpecterGlobals.java | 5 + .../network/MetatagSyncS2CPayload.java | 2 +- 9 files changed, 90 insertions(+), 76 deletions(-) diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigClient.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigClient.java index 6b01ba8..a897f3f 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigClient.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigClient.java @@ -12,8 +12,8 @@ public void onInitializeClient() { ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> ConfigManager.reload()); ClientPlayNetworking.registerGlobalReceiver(ConfigSyncS2CPayload.ID, (payload, context) -> { - SpecterGlobals.LOGGER.info("Received config sync packet"); - SpecterGlobals.LOGGER.info("Payload: {}", payload); + SpecterGlobals.debug("Received config sync packet"); + SpecterGlobals.debug("Payload: %s".formatted(payload)); }); } } diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java index e5eec5b..e67289e 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java @@ -3,7 +3,6 @@ import dev.spiritstudios.specter.api.ConfigScreenManager; import dev.spiritstudios.specter.api.config.Config; import dev.spiritstudios.specter.api.core.SpecterGlobals; -import dev.spiritstudios.specter.api.core.util.ReflectionHelper; import dev.spiritstudios.specter.impl.config.gui.widget.OptionsScrollableWidget; import net.minecraft.client.gui.DrawContext; import net.minecraft.client.gui.screen.Screen; @@ -15,7 +14,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.Objects; import java.util.function.BiFunction; public class ConfigScreen extends Screen { @@ -35,12 +33,8 @@ protected void init() { OptionsScrollableWidget scrollableWidget = new OptionsScrollableWidget(this.client, this.width, this.height - 64, 32, 25); List options = new ArrayList<>(); - List> values = Arrays.stream(config.getClass().getDeclaredFields()) - .filter(field -> field.getType().isAssignableFrom(Config.Value.class)) - .map(field -> (Config.Value) ReflectionHelper.getFieldValue(config, field)) - .filter(Objects::nonNull) - .toList(); + List> values = config.getValues().toList(); boolean failed = false; for (Config.Value option : values) { 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 4ed6b31..1118bc5 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 @@ -28,35 +28,48 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; +import java.util.stream.Stream; /** * A configuration file that can be saved to disk. */ public abstract class Config> implements Codec { + @ApiStatus.Internal protected Config() { - if (ConfigManager.getConfig(getId()) != null) - throw new IllegalStateException("Config with id %s already exists".formatted(getId())); + } - ConfigManager.registerConfig(getId(), this); - for (Field field : this.getClass().getDeclaredFields()) { - Value value = ReflectionHelper.getFieldValue(this, field); + public static > T create(Class clazz) { + T instance = ReflectionHelper.instantiate(clazz); + Config existing = ConfigManager.getConfig(instance.getId()); + if (existing != null) { + if (existing.getClass() != clazz) + throw new IllegalArgumentException("Config with id %s already exists with a different class".formatted(instance.getId())); + + return clazz.cast(existing); + + // QUESTION: does instance get garbage collected here? + } + + ConfigManager.registerConfig(instance.getId(), instance); + for (Field field : clazz.getDeclaredFields()) { + if (!Value.class.isAssignableFrom(field.getType())) continue; + if (Modifier.isStatic(field.getModifiers())) continue; + if (Modifier.isFinal(field.getModifiers())) continue; + + Value value = ReflectionHelper.getFieldValue(instance, field); if (value == null) continue; value.init(field.getName()); + SpecterGlobals.debug("Registered config value: %s".formatted(value.translationKey(instance.getId()))); } - } - /** - * WARNING: Recursive method - */ - private static void getNestedClasses(List> nestedClasses, Class clazz) { - nestedClasses.add(clazz); - for (Class nestedClass : clazz.getDeclaredClasses()) - getNestedClasses(nestedClasses, nestedClass); + if (!instance.load()) + SpecterGlobals.LOGGER.error("Failed to load config file: {}, default values will be used", instance.getPath()); + else + instance.save(); // Save the config to disk to ensure it's up to date + + return instance; } protected static ValueBuilder value(T defaultValue, Codec codec) { @@ -94,33 +107,21 @@ protected static ValueBuilder stringValue(String defaultValue) { @Override public DataResult encode(T input, DynamicOps ops, T1 prefix) { RecordBuilder builder = ops.mapBuilder(); - for (Field field : this.getClass().getDeclaredFields()) { - if (!Value.class.isAssignableFrom(field.getType()) || Modifier.isStatic(field.getModifiers())) continue; - - Value value = ReflectionHelper.getFieldValue(this, field); - if (value == null) continue; - - builder = value.encode(ops, builder); - } - + for (Value value : getValues().toList()) builder = value.encode(ops, builder); return builder.build(prefix); } @Override @SuppressWarnings("unchecked") public DataResult> decode(DynamicOps ops, T1 input) { - boolean success = true; - - for (Field field : this.getClass().getDeclaredFields()) { - if (!Value.class.isAssignableFrom(field.getType()) || Modifier.isStatic(field.getModifiers())) continue; - - Value value = ReflectionHelper.getFieldValue(this, field); - if (value == null) continue; + for (Value value : getValues().toList()) { + if (value.decode(ops, input)) continue; - success &= value.decode(ops, input); + SpecterGlobals.LOGGER.error("Failed to decode config value: {}", value.translationKey(getId())); + return DataResult.error(() -> "Failed to decode config value: %s".formatted(value.translationKey(getId()))); } - return success ? DataResult.success(Pair.of((T) this, input)) : DataResult.error(() -> "Failed to decode config"); + return DataResult.success(Pair.of((T) this, input)); } /** @@ -130,6 +131,7 @@ public DataResult> decode(DynamicOps ops, T1 input) { public void save() { @SuppressWarnings("unchecked") DataResult result = encodeStart(JsonOps.INSTANCE, (T) this); + if (result.error().isPresent()) { SpecterGlobals.LOGGER.error("Failed to encode config: {}", getId()); SpecterGlobals.LOGGER.error(result.error().toString()); @@ -155,12 +157,17 @@ public void save() { Map comments = new Object2ObjectOpenHashMap<>(); for (Field field : this.getClass().getDeclaredFields()) { - if (!Value.class.isAssignableFrom(field.getType()) || Modifier.isStatic(field.getModifiers())) continue; + if (!Value.class.isAssignableFrom(field.getType())) continue; + if (Modifier.isStatic(field.getModifiers())) continue; + if (Modifier.isFinal(field.getModifiers())) continue; Value value = ReflectionHelper.getFieldValue(this, field); if (value == null) continue; - value.comment().ifPresent(comment -> comments.put(field.getName(), comment)); + String comment = value.comment().orElse(null); + if (comment == null) continue; + + comments.put(field.getName(), comment); } List newLines = new ArrayList<>(); @@ -193,10 +200,10 @@ public void save() { } } - public void load() { + public boolean load() { if (!Files.exists(getPath())) { save(); - return; + return true; } List lines; @@ -204,7 +211,7 @@ public void load() { lines = Files.readAllLines(getPath()); } catch (IOException e) { SpecterGlobals.LOGGER.error("Failed to load config file {}. Default values will be used instead.", getPath().toString()); - return; + return false; } lines.removeIf(line -> line.trim().startsWith("//")); @@ -216,14 +223,18 @@ public void load() { } catch (JsonSyntaxException e) { SpecterGlobals.LOGGER.error("Failed to parse config file: {}", getPath()); SpecterGlobals.LOGGER.error(e.toString()); - return; + return false; } DataResult result = parse(JsonOps.INSTANCE, jsonElement); if (result.error().isPresent()) { SpecterGlobals.LOGGER.error("Failed to decode config file: {}", getPath()); SpecterGlobals.LOGGER.error(result.error().toString()); + + return false; } + + return true; } public Path getPath() { @@ -234,33 +245,32 @@ public Path getPath() { ); } + @ApiStatus.Internal + public Stream> getValues() { + return Arrays.stream(this.getClass().getDeclaredFields()) + .filter(field -> + Value.class.isAssignableFrom(field.getType()) && + !Modifier.isStatic(field.getModifiers()) && + !Modifier.isFinal(field.getModifiers()) + ) + .>map(field -> ReflectionHelper.getFieldValue(this, field)) + .filter(Objects::nonNull); + } + @SuppressWarnings("unchecked") public T packetDecode(ByteBuf buf) { - for (Field field : this.getClass().getDeclaredFields()) { - if (!Value.class.isAssignableFrom(field.getType()) || Modifier.isStatic(field.getModifiers())) continue; - - Value value = ReflectionHelper.getFieldValue(this, field); - if (value == null) continue; - if (!value.sync()) continue; - - value.packetDecode(buf); - } + getValues() + .filter(Value::sync) + .forEach(value -> value.packetDecode(buf)); return (T) this; } public void packetEncode(ByteBuf buf) { Identifier.PACKET_CODEC.encode(buf, getId()); - - for (Field field : this.getClass().getDeclaredFields()) { - if (!Value.class.isAssignableFrom(field.getType()) || Modifier.isStatic(field.getModifiers())) continue; - - Value value = ReflectionHelper.getFieldValue(this, field); - if (value == null) continue; - if (!value.sync()) continue; - - value.packetEncode(buf); - } + getValues() + .filter(Value::sync) + .forEach(value -> value.packetEncode(buf)); } public interface Value { @@ -330,10 +340,10 @@ public Value build() { protected static class RangedValueBuilder { protected final T defaultValue; protected final Codec codec; + private final RangeFunction> codecRange; protected String comment; protected boolean sync; protected PacketCodec packetCodec; - private final RangeFunction> codecRange; protected Pair range; public RangedValueBuilder(T defaultValue, Codec codec, RangeFunction> codecRange) { 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 8638c0a..caafa03 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 @@ -1,6 +1,7 @@ package dev.spiritstudios.specter.impl.config.network; import dev.spiritstudios.specter.api.config.Config; +import dev.spiritstudios.specter.api.core.SpecterGlobals; import dev.spiritstudios.specter.impl.config.ConfigManager; import io.netty.buffer.ByteBuf; import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; @@ -21,6 +22,7 @@ public record ConfigSyncS2CPayload(Config config) implements CustomPayload { Config::packetEncode, buf -> { Identifier id = Identifier.PACKET_CODEC.decode(buf); + SpecterGlobals.debug("Decoding config sync packet for %s".formatted(id)); Config config = ConfigManager.getConfig(id); config.save(); return config.packetDecode(buf); diff --git a/specter-config/src/testmod/java/dev/spiritstudios/testmod/CreateTestConfig.java b/specter-config/src/testmod/java/dev/spiritstudios/testmod/CreateTestConfig.java index 80e70cd..3b5461e 100644 --- a/specter-config/src/testmod/java/dev/spiritstudios/testmod/CreateTestConfig.java +++ b/specter-config/src/testmod/java/dev/spiritstudios/testmod/CreateTestConfig.java @@ -9,7 +9,7 @@ public Identifier getId() { return Identifier.of("specter-config-testmod", "createtestconfig"); } - public static final CreateTestConfig INSTANCE = new CreateTestConfig(); + public static final CreateTestConfig INSTANCE = Config.create(CreateTestConfig.class); public Value testString = stringValue("test") .comment("This is a test string") diff --git a/specter-config/src/testmod/java/dev/spiritstudios/testmod/GetTestConfig.java b/specter-config/src/testmod/java/dev/spiritstudios/testmod/GetTestConfig.java index 58f3a21..87062dd 100644 --- a/specter-config/src/testmod/java/dev/spiritstudios/testmod/GetTestConfig.java +++ b/specter-config/src/testmod/java/dev/spiritstudios/testmod/GetTestConfig.java @@ -9,7 +9,9 @@ public Identifier getId() { return Identifier.of("specter-config-testmod", "gettestconfig"); } - public static final GetTestConfig INSTANCE = new GetTestConfig(); + public static final GetTestConfig INSTANCE = Config.create(GetTestConfig.class); + + public String invalidField = "test"; public Value testString = stringValue("test") .comment("This is a test string") diff --git a/specter-config/src/testmod/java/dev/spiritstudios/testmod/SpecterConfigGameTest.java b/specter-config/src/testmod/java/dev/spiritstudios/testmod/SpecterConfigGameTest.java index 4a2238b..be28765 100644 --- a/specter-config/src/testmod/java/dev/spiritstudios/testmod/SpecterConfigGameTest.java +++ b/specter-config/src/testmod/java/dev/spiritstudios/testmod/SpecterConfigGameTest.java @@ -21,8 +21,7 @@ public void testCreateConfigFile(TestContext context) throws IOException { ); Files.deleteIfExists(path); - CreateTestConfig.INSTANCE.load(); - + context.assertTrue(CreateTestConfig.INSTANCE.load(), "Config file failed to load"); context.assertTrue(Files.exists(path), "Config file does not exist"); context.assertTrue(CreateTestConfig.INSTANCE.testString.get().equals("test"), "String is not equal to test, Make sure you haven't modified the config"); context.complete(); @@ -37,9 +36,11 @@ public void testSaveConfigFile(TestContext context) throws IOException { ); Files.deleteIfExists(path); + + context.assertTrue(GetTestConfig.INSTANCE.load(), "Config file failed to load"); GetTestConfig.INSTANCE.testString.set("test2"); GetTestConfig.INSTANCE.save(); - GetTestConfig.INSTANCE.load(); + context.assertTrue(GetTestConfig.INSTANCE.load(), "Config file failed to load"); context.assertTrue(Files.exists(path), "Config file does not exist"); context.assertTrue(GetTestConfig.INSTANCE.testString.get().equals("test2"), "String is not equal to test2, Make sure you haven't modified the config"); diff --git a/specter-core/src/main/java/dev/spiritstudios/specter/api/core/SpecterGlobals.java b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/SpecterGlobals.java index 90df8a2..8cea974 100644 --- a/specter-core/src/main/java/dev/spiritstudios/specter/api/core/SpecterGlobals.java +++ b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/SpecterGlobals.java @@ -30,4 +30,9 @@ public final class SpecterGlobals { DEBUG = debug; } + + @ApiStatus.Internal + public static void debug(String message) { + if (DEBUG) LOGGER.info(message); + } } diff --git a/specter-registry/src/main/java/dev/spiritstudios/specter/impl/registry/metatag/network/MetatagSyncS2CPayload.java b/specter-registry/src/main/java/dev/spiritstudios/specter/impl/registry/metatag/network/MetatagSyncS2CPayload.java index 286d96c..976e62a 100644 --- a/specter-registry/src/main/java/dev/spiritstudios/specter/impl/registry/metatag/network/MetatagSyncS2CPayload.java +++ b/specter-registry/src/main/java/dev/spiritstudios/specter/impl/registry/metatag/network/MetatagSyncS2CPayload.java @@ -57,7 +57,7 @@ private static void fillCache() { if (entry.getValue().getSide() == ResourceType.CLIENT_RESOURCES) return; - SpecterGlobals.LOGGER.debug("Caching metatag {}", entry.getKey()); + SpecterGlobals.debug("Caching metatag %s".formatted(entry.getKey())); cacheMetatag(entry.getValue()); }); } From 15310df95601dd7108c4377cc225934d164ebc42 Mon Sep 17 00:00:00 2001 From: CallMeEchoCodes Date: Sat, 7 Sep 2024 19:37:33 +1000 Subject: [PATCH 4/9] chore: make createEnumPacketCodec encode ordinal --- .../specter/api/core/util/CodecHelper.java | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/specter-core/src/main/java/dev/spiritstudios/specter/api/core/util/CodecHelper.java b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/util/CodecHelper.java index 40b6117..819c942 100644 --- a/specter-core/src/main/java/dev/spiritstudios/specter/api/core/util/CodecHelper.java +++ b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/util/CodecHelper.java @@ -4,7 +4,6 @@ import com.mojang.serialization.DataResult; import io.netty.buffer.ByteBuf; import net.minecraft.network.codec.PacketCodec; -import net.minecraft.network.codec.PacketCodecs; public final class CodecHelper { public static > Codec createEnumCodec(Class clazz) { @@ -20,9 +19,24 @@ public static > Codec createEnumCodec(Class clazz) { }, Enum::name); } - // TODO: Make this use the enum ordinal instead of the name public static > PacketCodec createEnumPacketCodec(Class clazz) { - return PacketCodecs.codec(createEnumCodec(clazz)); + return new PacketCodec<>() { + @Override + public void encode(ByteBuf buf, T value) { + buf.writeInt(value.ordinal()); + } + + @Override + public T decode(ByteBuf buf) { + int ordinal = buf.readInt(); + T[] values = clazz.getEnumConstants(); + + if (ordinal < 0 || ordinal >= values.length) + throw new IndexOutOfBoundsException("Enum ordinal out of bounds: " + ordinal); + + return values[ordinal]; + } + }; } public static Codec clampedRangeInt(int min, int max) { From 164bab7945bb3cea006e0729deabe0b0ff094f43 Mon Sep 17 00:00:00 2001 From: CallMeEchoCodes Date: Sun, 8 Sep 2024 13:06:36 +1000 Subject: [PATCH 5/9] chore: fix several small issues --- .../specter/impl/config/gui/ConfigScreen.java | 8 ++++++-- .../spiritstudios/specter/api/config/Config.java | 4 +--- .../specter/impl/config/ConfigManager.java | 6 ++++-- .../specter/impl/config/SpecterConfig.java | 2 +- .../impl/config/network/ConfigSyncS2CPayload.java | 13 +++---------- 5 files changed, 15 insertions(+), 18 deletions(-) diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java index e67289e..21a7890 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java @@ -8,6 +8,7 @@ import net.minecraft.client.gui.screen.Screen; 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 net.minecraft.util.Identifier; @@ -55,12 +56,15 @@ protected void init() { } ClickableWidget widget = factory.apply(option, this.config.getId()); - if (widget != null) options.add(widget); + if (widget == null) + throw new IllegalStateException("Widget factory returned null for %s".formatted(option.defaultValue().getClass().getSimpleName())); + + options.add(widget); }); scrollableWidget.addOptions(Arrays.copyOf(options.toArray(), options.size(), ClickableWidget[].class)); this.addDrawableChild(scrollableWidget); - this.addDrawableChild(new ButtonWidget.Builder(Text.translatable("gui.done"), button -> close()).dimensions(this.width / 2 - 100, this.height - 27, 200, 20).build()); + this.addDrawableChild(new ButtonWidget.Builder(ScreenTexts.DONE, button -> close()).dimensions(this.width / 2 - 100, this.height - 27, 200, 20).build()); } @Override 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 1118bc5..9d3047f 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 @@ -46,9 +46,7 @@ public static > T create(Class clazz) { if (existing.getClass() != clazz) throw new IllegalArgumentException("Config with id %s already exists with a different class".formatted(instance.getId())); - return clazz.cast(existing); - - // QUESTION: does instance get garbage collected here? + throw new IllegalArgumentException("Config with id %s already exists".formatted(instance.getId())); } ConfigManager.registerConfig(instance.getId(), instance); diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/ConfigManager.java b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/ConfigManager.java index e3ca027..d8dd009 100644 --- a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/ConfigManager.java +++ b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/ConfigManager.java @@ -25,7 +25,9 @@ public static void reload() { } - public static List> getConfigs() { - return List.copyOf(configs.values()); + public static List createPayloads() { + return configs.values().stream() + .map(ConfigSyncS2CPayload::new) + .toList(); } } diff --git a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/SpecterConfig.java b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/SpecterConfig.java index 38eac06..352d0e5 100644 --- a/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/SpecterConfig.java +++ b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/SpecterConfig.java @@ -17,7 +17,7 @@ public void onInitialize() { ); ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { - List payloads = ConfigSyncS2CPayload.createPayloads(); + List payloads = ConfigSyncS2CPayload.getPayloads(); payloads.forEach(sender::sendPacket); }); 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 caafa03..59c7000 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 @@ -38,20 +38,13 @@ public static void clearCache() { CACHE.clear(); } - public static List createPayloads() { - if (CACHE.isEmpty()) { - CACHE.addAll( - ConfigManager.getConfigs().stream() - .map(ConfigSyncS2CPayload::new) - .toList() - ); - } - + public static List getPayloads() { + if (CACHE.isEmpty()) CACHE.addAll(ConfigManager.createPayloads()); return CACHE; } public static void sendPayloadsToAll(MinecraftServer server) { - List payloads = ConfigSyncS2CPayload.createPayloads(); + List payloads = ConfigSyncS2CPayload.getPayloads(); server.getPlayerManager().getPlayerList().forEach( player -> payloads.forEach(payload -> ServerPlayNetworking.send(player, payload))); From 7d65d9982f0987c6256fe0ee70a153a1c058b868 Mon Sep 17 00:00:00 2001 From: CallMeEchoCodes Date: Sun, 8 Sep 2024 17:06:14 +1000 Subject: [PATCH 6/9] chore: several more small fixes --- .../specter/api/ConfigScreenManager.java | 2 +- .../specter/impl/config/gui/ConfigScreen.java | 23 ++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/api/ConfigScreenManager.java b/specter-config/src/client/java/dev/spiritstudios/specter/api/ConfigScreenManager.java index d4e948f..867c1a4 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/api/ConfigScreenManager.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/api/ConfigScreenManager.java @@ -19,7 +19,7 @@ public static void registerWidgetFactory(Class clazz, BiFunction BiFunction, Identifier, ? extends ClickableWidget> getWidgetFactory(Config.Value value, Identifier configId) { + public static BiFunction, Identifier, ? extends ClickableWidget> getWidgetFactory(Config.Value value) { // We are using a switch instead of just adding to our map for 2 reasons: // 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 diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java index 21a7890..285d4f6 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.function.BiFunction; public class ConfigScreen extends Screen { @@ -30,26 +31,26 @@ public ConfigScreen(Config config, Screen parent) { @Override protected void init() { super.init(); - if (this.client == null) close(); + Objects.requireNonNull(this.client); OptionsScrollableWidget scrollableWidget = new OptionsScrollableWidget(this.client, this.width, this.height - 64, 32, 25); List options = new ArrayList<>(); List> values = config.getValues().toList(); - boolean failed = false; - for (Config.Value option : values) { - if (!option.sync() || this.client.player == null || this.client.isInSingleplayer()) continue; + if (this.client.player != null && !this.client.isInSingleplayer()) { + for (Config.Value option : values) { + if (!option.sync()) continue; - this.client.player.sendMessage(Text.of("Configs cannot be edited in multiplayer"), true); - this.client.setScreen(this.parent); - failed = true; - break; + this.client.player.sendMessage(Text.of("Configs cannot be edited in multiplayer"), true); + this.client.setScreen(this.parent); + + return; + } } - if (failed) return; values.forEach(option -> { - BiFunction, Identifier, ? extends ClickableWidget> factory = ConfigScreenManager.getWidgetFactory(option, this.config.getId()); + BiFunction, Identifier, ? extends ClickableWidget> factory = ConfigScreenManager.getWidgetFactory(option); if (factory == null) { SpecterGlobals.LOGGER.warn("No widget factory found for {}", option.defaultValue().getClass().getSimpleName()); return; @@ -71,7 +72,7 @@ protected void init() { public void close() { save(); - if (this.client == null) return; + Objects.requireNonNull(this.client); this.client.setScreen(this.parent); } From 99825c19b2ca1886f2723068ba0c41ba1cb767be Mon Sep 17 00:00:00 2001 From: CallMeEchoCodes Date: Sun, 8 Sep 2024 17:11:33 +1000 Subject: [PATCH 7/9] chore: make builtin factories constant --- .../specter/api/ConfigScreenManager.java | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/api/ConfigScreenManager.java b/specter-config/src/client/java/dev/spiritstudios/specter/api/ConfigScreenManager.java index 867c1a4..635fe50 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/api/ConfigScreenManager.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/api/ConfigScreenManager.java @@ -10,6 +10,7 @@ import java.util.Map; import java.util.function.BiFunction; +@SuppressWarnings("unchecked") public final class ConfigScreenManager { private static final Map, BiFunction, Identifier, ? extends ClickableWidget>> widgetFactories = new Object2ObjectOpenHashMap<>(); @@ -17,25 +18,29 @@ public static void registerWidgetFactory(Class clazz, BiFunction BiFunction, Identifier, ? extends ClickableWidget> getWidgetFactory(Config.Value value) { // We are using a switch instead of just adding to our map for 2 reasons: // 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 -> - (configValue, id) -> new BooleanButtonWidget((Config.Value) configValue, id); - case Integer ignored -> - (configValue, id) -> new IntegerSliderWidget((Config.Value) configValue, id); - case Double ignored -> (configValue, id) -> new DoubleSliderWidget((Config.Value) configValue, id); - case Float ignored -> (configValue, id) -> new FloatSliderWidget((Config.Value) configValue, id); - case String ignored -> (configValue, id) -> new TextBoxWidget((Config.Value) configValue, id); - case Enum ignored -> (configValue, id) -> new EnumButtonWidget((Config.Value>) configValue, id); + 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()); }; } + private static final BiFunction, Identifier, ? extends ClickableWidget> BOOLEAN_WIDGET_FACTORY = (configValue, id) -> new BooleanButtonWidget((Config.Value) configValue, id); + private static final BiFunction, Identifier, ? extends ClickableWidget> INTEGER_WIDGET_FACTORY = (configValue, id) -> new IntegerSliderWidget((Config.Value) configValue, id); + private static final BiFunction, Identifier, ? extends ClickableWidget> DOUBLE_WIDGET_FACTORY = (configValue, id) -> new DoubleSliderWidget((Config.Value) configValue, id); + private static final BiFunction, Identifier, ? extends ClickableWidget> FLOAT_WIDGET_FACTORY = (configValue, id) -> new FloatSliderWidget((Config.Value) configValue, id); + private static final BiFunction, Identifier, ? extends ClickableWidget> STRING_WIDGET_FACTORY = (configValue, id) -> new TextBoxWidget((Config.Value) configValue, id); + private static final BiFunction, Identifier, ? extends ClickableWidget> ENUM_WIDGET_FACTORY = (configValue, id) -> new EnumButtonWidget((Config.Value>) configValue, id); + private ConfigScreenManager() { } } From 8651359318a7097c8cbc6d13ab89d54187aee0f4 Mon Sep 17 00:00:00 2001 From: CallMeEchoCodes Date: Sun, 8 Sep 2024 19:28:16 +1000 Subject: [PATCH 8/9] fix: translate config screen error --- .../spiritstudios/specter/impl/config/gui/ConfigScreen.java | 4 +++- .../client/resources/assets/specter-config/lang/en_us.json | 3 +++ .../java/dev/spiritstudios/specter/api/config/Config.java | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 specter-config/src/client/resources/assets/specter-config/lang/en_us.json diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java index 285d4f6..5e9d72a 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java @@ -19,6 +19,8 @@ import java.util.function.BiFunction; public class ConfigScreen extends Screen { + private static final Text MULTIPLAYER_SYNC_ERROR = Text.translatable("screen.specter.config.multiplayer_sync_error"); + private final Config config; private final Screen parent; @@ -42,7 +44,7 @@ protected void init() { for (Config.Value option : values) { if (!option.sync()) continue; - this.client.player.sendMessage(Text.of("Configs cannot be edited in multiplayer"), true); + this.client.player.sendMessage(MULTIPLAYER_SYNC_ERROR, true); this.client.setScreen(this.parent); return; diff --git a/specter-config/src/client/resources/assets/specter-config/lang/en_us.json b/specter-config/src/client/resources/assets/specter-config/lang/en_us.json new file mode 100644 index 0000000..f677085 --- /dev/null +++ b/specter-config/src/client/resources/assets/specter-config/lang/en_us.json @@ -0,0 +1,3 @@ +{ + "screen.specter.config.multiplayer_sync_error": "Configs with server-synced options cannot be edited while connected to a remote server." +} 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 9d3047f..7327634 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 @@ -46,7 +46,7 @@ public static > T create(Class clazz) { if (existing.getClass() != clazz) throw new IllegalArgumentException("Config with id %s already exists with a different class".formatted(instance.getId())); - throw new IllegalArgumentException("Config with id %s already exists".formatted(instance.getId())); + throw new RuntimeException("Config with id %s already exists".formatted(instance.getId())); } ConfigManager.registerConfig(instance.getId(), instance); From 955e546220073dc1e81845418d03f22bb15e6469 Mon Sep 17 00:00:00 2001 From: CallMeEchoCodes Date: Sun, 8 Sep 2024 19:29:35 +1000 Subject: [PATCH 9/9] fix: move sync error to chat --- .../dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java index 5e9d72a..dce95ac 100644 --- a/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java +++ b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/gui/ConfigScreen.java @@ -44,7 +44,7 @@ protected void init() { for (Config.Value option : values) { if (!option.sync()) continue; - this.client.player.sendMessage(MULTIPLAYER_SYNC_ERROR, true); + this.client.player.sendMessage(MULTIPLAYER_SYNC_ERROR, false); this.client.setScreen(this.parent); return;