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..635fe50 --- /dev/null +++ b/specter-config/src/client/java/dev/spiritstudios/specter/api/ConfigScreenManager.java @@ -0,0 +1,46 @@ +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; + +@SuppressWarnings("unchecked") +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); + } + + @ApiStatus.Internal + 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 + return switch (value.defaultValue()) { + case Boolean ignored -> BOOLEAN_WIDGET_FACTORY; + case Integer ignored -> INTEGER_WIDGET_FACTORY; + case Double ignored -> DOUBLE_WIDGET_FACTORY; + case Float ignored -> FLOAT_WIDGET_FACTORY; + case String ignored -> STRING_WIDGET_FACTORY; + case Enum ignored -> ENUM_WIDGET_FACTORY; + default -> widgetFactories.get(value.defaultValue().getClass()); + }; + } + + 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() { + } +} 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..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 @@ -1,43 +1,19 @@ 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.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; 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 public void onInitializeClient() { - ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> ConfigManager.reloadConfigs()); + ClientPlayConnectionEvents.DISCONNECT.register((handler, client) -> ConfigManager.reload()); 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); - })); + 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/SpecterConfigModMenu.java b/specter-config/src/client/java/dev/spiritstudios/specter/impl/config/SpecterConfigModMenu.java index d3c72c6..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; @@ -12,11 +11,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.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 21808fe..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 @@ -1,28 +1,31 @@ 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.screen.ScreenTexts; 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 static final Text MULTIPLAYER_SYNC_ERROR = Text.translatable("screen.specter.config.multiplayer_sync_error"); + + 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.%s.title".formatted(config.getId().getNamespace(), config.getId().getPath()))); this.config = config; this.parent = parent; } @@ -30,135 +33,53 @@ public ConfigScreen(Config config, Screen parent) { @Override protected void init() { super.init(); + Objects.requireNonNull(this.client); 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); - - 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()); - } - - @Override - public void close() { - save(); - if (this.client == null) return; - this.client.setScreen(this.parent); - } + List> values = config.getValues().toList(); - public void save() { - 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) - ); - } + if (this.client.player != null && !this.client.isInSingleplayer()) { + for (Config.Value option : values) { + if (!option.sync()) continue; - 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) - ); - } + this.client.player.sendMessage(MULTIPLAYER_SYNC_ERROR, false); + this.client.setScreen(this.parent); - 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) - ); + return; } + } - 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; + values.forEach(option -> { + BiFunction, Identifier, ? extends ClickableWidget> factory = ConfigScreenManager.getWidgetFactory(option); + if (factory == null) { + SpecterGlobals.LOGGER.warn("No widget factory found for {}", option.defaultValue().getClass().getSimpleName()); + return; } - case null, default -> { - if (SpecterGlobals.DEBUG) - SpecterGlobals.LOGGER.warn("Unsupported config type: {}", option.getType().getName()); + ClickableWidget widget = factory.apply(option, this.config.getId()); + if (widget == null) + throw new IllegalStateException("Widget factory returned null for %s".formatted(option.defaultValue().getClass().getSimpleName())); - yield null; - } + options.add(widget); }); - } - - 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); - } - } + scrollableWidget.addOptions(Arrays.copyOf(options.toArray(), options.size(), ClickableWidget[].class)); + this.addDrawableChild(scrollableWidget); + this.addDrawableChild(new ButtonWidget.Builder(ScreenTexts.DONE, button -> close()).dimensions(this.width / 2 - 100, this.height - 27, 200, 20).build()); + } - options.add(new ButtonWidget.Builder( - Text.translatable("config." + value.getId() + ".title"), - button -> { - save(); + @Override + public void close() { + save(); - if (this.client == null) return; - this.client.setScreen(nestedScreen); - } - ).dimensions(this.width / 2 - 100, 0, 200, 20).build()); + Objects.requireNonNull(this.client); + this.client.setScreen(this.parent); } - private String getTranslationKey(Field field) { - return "config." + this.config.getId() + "." + field.getName(); + public void save() { + config.save(); } @Override 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> nestedClasses, Class clazz) { - nestedClasses.add(clazz); - for (Class nestedClass : clazz.getDeclaredClasses()) - getNestedClasses(nestedClasses, nestedClass); +public abstract class Config> implements Codec { + @ApiStatus.Internal + protected Config() { + } + + 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())); + + throw new RuntimeException("Config with id %s already exists".formatted(instance.getId())); + } + + 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()))); + } + + 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) { + 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); } - Identifier getId(); + public abstract Identifier getId(); + + @Override + public DataResult encode(T input, DynamicOps ops, T1 prefix) { + RecordBuilder builder = ops.mapBuilder(); + for (Value value : getValues().toList()) builder = value.encode(ops, builder); + return builder.build(prefix); + } + + @Override + @SuppressWarnings("unchecked") + public DataResult> decode(DynamicOps ops, T1 input) { + for (Value value : getValues().toList()) { + if (value.decode(ops, input)) continue; + + 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 DataResult.success(Pair.of((T) this, input)); + } /** * 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(); + StringWriter stringWriter = new StringWriter(); + JsonWriter jsonWriter = new JsonWriter(stringWriter); + + jsonWriter.setLenient(true); + jsonWriter.setSerializeNulls(false); + jsonWriter.setIndent(" "); + + try { + Streams.write(object, jsonWriter); + } catch (IOException e) { + throw new RuntimeException(e); + } + + String json = stringWriter.toString(); 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()) { + 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; + + String comment = value.comment().orElse(null); + if (comment == null) continue; + + comments.put(field.getName(), comment); + } List newLines = new ArrayList<>(); for (String line : json.split("\n")) { @@ -78,7 +198,44 @@ default void save() { } } - default Path getPath() { + public boolean load() { + if (!Files.exists(getPath())) { + save(); + return true; + } + + 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 false; + } + + 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 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() { return Paths.get( FabricLoader.getInstance().getConfigDir().toString(), "", @@ -86,27 +243,143 @@ default Path getPath() { ); } - private void checkAnnotations(Map comments, Field field) { - Range rangeAnnotation = field.getAnnotation(Range.class); - if (rangeAnnotation == null) return; + @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) { + getValues() + .filter(Value::sync) + .forEach(value -> value.packetDecode(buf)); - if (rangeAnnotation.clamp()) { - Number value = ReflectionHelper.getFieldValue(this, field); - if (value != null) - ReflectionHelper.setFieldValue( - this, - field, - MathHelper.clamp( - value.doubleValue(), - rangeAnnotation.min(), - rangeAnnotation.max() - ) - ); + return (T) this; + } + + public void packetEncode(ByteBuf buf) { + Identifier.PACKET_CODEC.encode(buf, getId()); + getValues() + .filter(Value::sync) + .forEach(value -> value.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); + + void packetEncode(ByteBuf buf); + + 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; + private final RangeFunction> codecRange; + protected String comment; + protected boolean sync; + protected PacketCodec packetCodec; + protected Pair range; + + public RangedValueBuilder(T defaultValue, Codec codec, RangeFunction> codecRange) { + this.defaultValue = defaultValue; + this.codec = codec; + this.codecRange = codecRange; } - Comment commentAnnotation = field.getAnnotation(Comment.class); - if (commentAnnotation == null) return; + public RangedValueBuilder range(T min, T max) { + this.range = Pair.of(min, max); + return this; + } - comments.put(field.getName(), commentAnnotation.value()); + 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 deleted file mode 100644 index bae865b..0000000 --- a/specter-config/src/main/java/dev/spiritstudios/specter/api/config/ConfigManager.java +++ /dev/null @@ -1,117 +0,0 @@ -package dev.spiritstudios.specter.api.config; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonSyntaxException; -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 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 { - @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<>(); - - /** - * 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); - 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; - } - - // Save to make sure any new fields are added - loadedConfig.save(); - CACHED_PAYLOAD = null; - - 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); - } - - public static void reloadConfigs() { - Map oldConfigs = new Object2ObjectOpenHashMap<>(configs); - oldConfigs.values().stream().map(Config::getClass).forEach(ConfigManager::getConfig); - - CACHED_PAYLOAD = null; - } - - public static void reloadConfigs(MinecraftServer server) { - reloadConfigs(); - ConfigSyncS2CPayload payload = ConfigManager.createSyncPayload(); - server.getPlayerManager().getPlayerList().forEach(player -> ServerPlayNetworking.send(player, payload)); - } - - @ApiStatus.Internal - public static Map getConfigs() { - return configs; - } - - 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); - } -} 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/ConfigManager.java b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/ConfigManager.java new file mode 100644 index 0000000..d8dd009 --- /dev/null +++ b/specter-config/src/main/java/dev/spiritstudios/specter/impl/config/ConfigManager.java @@ -0,0 +1,33 @@ +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 createPayloads() { + return configs.values().stream() + .map(ConfigSyncS2CPayload::new) + .toList(); + } +} 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..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 @@ -1,12 +1,12 @@ 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 net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; + +import java.util.List; public class SpecterConfig implements ModInitializer { @Override @@ -17,12 +17,13 @@ public void onInitialize() { ); ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> { - ConfigSyncS2CPayload payload = ConfigManager.createSyncPayload(); - ServerPlayNetworking.send(handler.getPlayer(), payload); + List payloads = ConfigSyncS2CPayload.getPayloads(); + 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/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..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 @@ -1,27 +1,57 @@ 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 it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import net.minecraft.network.codec.PacketCodec; -import net.minecraft.network.codec.PacketCodecs; import net.minecraft.network.packet.CustomPayload; +import net.minecraft.server.MinecraftServer; import net.minecraft.util.Identifier; -import java.util.Map; +import java.util.ArrayList; +import java.util.List; -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 - ); +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); + SpecterGlobals.debug("Decoding config sync packet for %s".formatted(id)); + 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 getPayloads() { + if (CACHE.isEmpty()) CACHE.addAll(ConfigManager.createPayloads()); + return CACHE; + } + + public static void sendPayloadsToAll(MinecraftServer server) { + List payloads = ConfigSyncS2CPayload.getPayloads(); + + server.getPlayerManager().getPlayerList().forEach( + player -> payloads.forEach(payload -> ServerPlayNetworking.send(player, payload))); + } @Override - public Id getId() { - return ConfigSyncS2CPayload.ID; + 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..3b5461e 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,42 @@ 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 final CreateTestConfig INSTANCE = Config.create(CreateTestConfig.class); - @Comment("This is a nested class") - public Nested nested = new Nested(); + public Value testString = stringValue("test") + .comment("This is a test string") + .sync() + .build(); - @Comment("This is a test enum") - public TestEnum testEnum = TestEnum.TEST_1; + public Value testInt = intValue(1) + .comment("This is a test int") + .range(2, 10) + .build(); - public static class Nested implements NestedConfig { - @Comment("This is a nested string\n" + - "With a new line") - public String nestedString = "test"; + public Value testBool = booleanValue(true) + .comment("This is a test bool") + .build(); - @Override - public Identifier getId() { - return Identifier.of("testmod", "nested"); - } + public Value testDouble = doubleValue(1.0) + .comment("This is a test double") + .build(); - @Comment("This is a nested nested class") - public NestedNested nestedNested = new NestedNested(); + public Value testFloat = floatValue(1.0f) + .comment("This is a test float") + .build(); - public static class NestedNested implements NestedConfig { - @Comment("This is a nested nested string") - public String nestedNestedString = "test"; + public Value testEnum = enumValue(TestEnum.TEST_1, TestEnum.class) + .comment("This is a test enum") + .build(); - @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..87062dd 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,44 @@ 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 final GetTestConfig INSTANCE = Config.create(GetTestConfig.class); - @Comment("This is a nested class") - public CreateTestConfig.Nested nested = new CreateTestConfig.Nested(); + public String invalidField = "test"; - @Comment("This is a test enum") - public CreateTestConfig.TestEnum testEnum = CreateTestConfig.TestEnum.TEST_1; + public Value testString = stringValue("test") + .comment("This is a test string") + .sync() + .build(); - public static class Nested implements NestedConfig { - @Comment("This is a nested string\n" + - "With a new line") - public String nestedString = "test"; + public Value testInt = intValue(2) + .comment("This is a test int") + .range(2, 10) + .build(); - @Override - public Identifier getId() { - return Identifier.of("testmod", "nested"); - } + public Value testBool = booleanValue(true) + .comment("This is a test bool") + .build(); - @Comment("This is a nested nested class") - public CreateTestConfig.Nested.NestedNested nestedNested = new CreateTestConfig.Nested.NestedNested(); + public Value testDouble = doubleValue(1.0) + .comment("This is a test double") + .build(); - public static class NestedNested implements NestedConfig { - @Comment("This is a nested nested string") - public String nestedNestedString = "test"; + public Value testFloat = floatValue(1.0f) + .comment("This is a test float") + .build(); + + public Value testEnum = enumValue(TestEnum.TEST_1, TestEnum.class) + .comment("This is a test enum") + .build(); - @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..be28765 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,9 @@ public void testCreateConfigFile(TestContext context) throws IOException { ); Files.deleteIfExists(path); - CreateTestConfig config = ConfigManager.getConfig(CreateTestConfig.class); - + context.assertTrue(CreateTestConfig.INSTANCE.load(), "Config file failed to 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 +36,14 @@ public void testSaveConfigFile(TestContext context) throws IOException { ); Files.deleteIfExists(path); - GetTestConfig config = ConfigManager.getConfig(GetTestConfig.class); - config.testString = "test2"; - config.save(); - GetTestConfig newConfig = ConfigManager.getConfig(GetTestConfig.class); + context.assertTrue(GetTestConfig.INSTANCE.load(), "Config file failed to load"); + GetTestConfig.INSTANCE.testString.set("test2"); + GetTestConfig.INSTANCE.save(); + context.assertTrue(GetTestConfig.INSTANCE.load(), "Config file failed to 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 860cbf1..afebf0b 100644 --- a/specter-config/src/testmodClient/java/dev/spiritstudios/testmod/SpecterConfigTestmodClient.java +++ b/specter-config/src/testmodClient/java/dev/spiritstudios/testmod/SpecterConfigTestmodClient.java @@ -6,6 +6,6 @@ public class SpecterConfigTestmodClient implements ClientModInitializer { @Override public void onInitializeClient() { - ModMenuHelper.addConfig("specter-config-testmod", CreateTestConfig.class); + ModMenuHelper.addConfig("specter-config-testmod", GetTestConfig.INSTANCE.getId()); } } 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-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..819c942 --- /dev/null +++ b/specter-core/src/main/java/dev/spiritstudios/specter/api/core/util/CodecHelper.java @@ -0,0 +1,65 @@ +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; + +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); + } + + public static > PacketCodec createEnumPacketCodec(Class 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) { + 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() { + } +} 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()); }); }