Skip to content

Commit

Permalink
feat: config rewrite
Browse files Browse the repository at this point in the history
Config module rewrite
  • Loading branch information
CallMeEchoCodes authored Sep 8, 2024
2 parents 3946b39 + 955e546 commit 5cb6a55
Show file tree
Hide file tree
Showing 30 changed files with 845 additions and 608 deletions.
Original file line number Diff line number Diff line change
@@ -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<Class<?>, BiFunction<Config.Value<?>, Identifier, ? extends ClickableWidget>> widgetFactories = new Object2ObjectOpenHashMap<>();

public static void registerWidgetFactory(Class<?> clazz, BiFunction<Config.Value<?>, Identifier, ? extends ClickableWidget> factory) {
widgetFactories.put(clazz, factory);
}

@ApiStatus.Internal
public static <T> BiFunction<Config.Value<?>, Identifier, ? extends ClickableWidget> getWidgetFactory(Config.Value<T> 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<Config.Value<?>, Identifier, ? extends ClickableWidget> BOOLEAN_WIDGET_FACTORY = (configValue, id) -> new BooleanButtonWidget((Config.Value<Boolean>) configValue, id);
private static final BiFunction<Config.Value<?>, Identifier, ? extends ClickableWidget> INTEGER_WIDGET_FACTORY = (configValue, id) -> new IntegerSliderWidget((Config.Value<Integer>) configValue, id);
private static final BiFunction<Config.Value<?>, Identifier, ? extends ClickableWidget> DOUBLE_WIDGET_FACTORY = (configValue, id) -> new DoubleSliderWidget((Config.Value<Double>) configValue, id);
private static final BiFunction<Config.Value<?>, Identifier, ? extends ClickableWidget> FLOAT_WIDGET_FACTORY = (configValue, id) -> new FloatSliderWidget((Config.Value<Float>) configValue, id);
private static final BiFunction<Config.Value<?>, Identifier, ? extends ClickableWidget> STRING_WIDGET_FACTORY = (configValue, id) -> new TextBoxWidget((Config.Value<String>) configValue, id);
private static final BiFunction<Config.Value<?>, Identifier, ? extends ClickableWidget> ENUM_WIDGET_FACTORY = (configValue, id) -> new EnumButtonWidget((Config.Value<Enum<?>>) configValue, id);

private ConfigScreenManager() {
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -11,22 +11,22 @@
* Helper class for ModMenu integration without having to depend on ModMenu directly.
*/
public class ModMenuHelper {
private static final Map<String, Class<? extends Config>> screens = new Object2ObjectOpenHashMap<>();
private static final Map<String, Identifier> 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<? extends Config> 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<String, Class<? extends Config>> getConfigScreens() {
public static Map<String, Identifier> getConfigScreens() {
return screens;
}
}
Original file line number Diff line number Diff line change
@@ -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<Identifier, String> 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));
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -12,11 +11,15 @@
public class SpecterConfigModMenu implements ModMenuApi {
@Override
public Map<String, ConfigScreenFactory<?>> 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)
)
);
}
}
Original file line number Diff line number Diff line change
@@ -1,164 +1,85 @@
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;
}

@Override
protected void init() {
super.init();
Objects.requireNonNull(this.client);

OptionsScrollableWidget scrollableWidget = new OptionsScrollableWidget(this.client, this.width, this.height - 64, 32, 25);
List<ClickableWidget> 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<Config.Value<?>> 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<ClickableWidget> 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<Config.Value<?>, 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<ClickableWidget> 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
Expand Down
Loading

0 comments on commit 5cb6a55

Please sign in to comment.