Skip to content

Commit

Permalink
Merge pull request #4 from SpiritGameStudios/config-gui-updates
Browse files Browse the repository at this point in the history
Config upgrades and GUI module
  • Loading branch information
CallMeEchoCodes authored Oct 2, 2024
2 parents ebc8fc8 + 3985237 commit 3ac4f34
Show file tree
Hide file tree
Showing 68 changed files with 3,706 additions and 889 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ TODO: Add usage instructions here
## Special Thanks

To the QSL Team, for creating their REA system, which the Metatag API is heavily inspired by.

To [Pug](https://github.com/MerchantPug), for creating Greenhouse Config, which was used as reference for the JsonCWriter (and by extension the TomlWriter, which was based on the JsonCWriter).
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import net.fabricmc.loom.task.RemapJarTask
import java.net.URI

plugins {
`java-library`
`maven-publish`
id("fabric-loom") version "1.7-SNAPSHOT"
}

Expand Down Expand Up @@ -267,6 +269,7 @@ dependencies {
"clientImplementation"(project(":${it.name}").sourceSets.getByName("client").output)
include(project(":${it.name}"))

"testmodImplementation"("org.tomlj:tomlj:${property("deps.tomlj")}")
"testmodImplementation"(project(":${it.name}").sourceSets["testmod"].output)
"testmodClientImplementation"(project(":${it.name}").sourceSets["testmodClient"].output)
}
Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ deps.loader=0.15.11
deps.yarn=1.21+build.9
deps.modmenu=11.0.1
deps.fabricapi=0.103.0+1.21.1
deps.tomlj=1.1.1

2 changes: 2 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,5 @@ include("specter-item")
include("specter-block")
include("specter-entity")
include("specter-debug")
include("specter-gui")
include("specter-serialization")
2 changes: 1 addition & 1 deletion specter-config/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
moduleDependencies(project, "specter-core")
moduleDependencies(project, "specter-core", "specter-gui", "specter-serialization")

repositories {
maven("https://maven.terraformersmc.com/releases/")
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,47 +1,48 @@
package dev.spiritstudios.specter.impl.config.gui;
package dev.spiritstudios.specter.api.config;

import dev.spiritstudios.specter.api.ConfigScreenManager;
import dev.spiritstudios.specter.api.config.Config;
import dev.spiritstudios.specter.api.core.SpecterGlobals;
import dev.spiritstudios.specter.impl.config.NestedConfigScreen;
import dev.spiritstudios.specter.impl.config.NestedConfigValue;
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.tooltip.Tooltip;
import net.minecraft.client.gui.widget.ButtonWidget;
import net.minecraft.client.gui.widget.ClickableWidget;
import net.minecraft.screen.ScreenTexts;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;

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 {
public abstract 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;
protected final Config<?> config;
protected final Screen parent;
protected final String id;

public ConfigScreen(Config<?> config, Screen parent) {
super(Text.translatable("config.%s.%s.title".formatted(config.getId().getNamespace(), config.getId().getPath())));
public ConfigScreen(Config<?> config, String id, Screen parent) {
super(Text.translatable("config.%s.title".formatted(id)));
this.config = config;
this.parent = parent;
this.id = id;
}


@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<>();

List<Config.Value<?>> values = config.getValues().toList();
List<Value<?>> values = config.getValues().toList();

if (this.client.player != null && !this.client.isInSingleplayer()) {
for (Config.Value<?> option : values) {
for (Value<?> option : values) {
if (!option.sync()) continue;

this.client.player.sendMessage(MULTIPLAYER_SYNC_ERROR, false);
Expand All @@ -51,21 +52,46 @@ protected void init() {
}
}

List<ClickableWidget> options = new ArrayList<>();

values.forEach(option -> {
BiFunction<Config.Value<?>, Identifier, ? extends ClickableWidget> factory = ConfigScreenManager.getWidgetFactory(option);
if (option instanceof NestedConfigValue<?> nestedOption) {
String nestedId = "%s.%s".formatted(id, option.name());
ConfigScreen screen = new NestedConfigScreen(nestedOption.get(), nestedId, this);

options.add(
ButtonWidget.builder(
Text.translatable("config.%s.title".formatted(nestedId)),
button -> {
save();
this.client.setScreen(screen);
}
).dimensions(this.width / 2 - 100, 0, 200, 20).build()
);

return;
}

BiFunction<Value<?>, String, ? extends ClickableWidget> factory = ConfigScreenWidgets.getWidgetFactory(option);
if (factory == null) {
SpecterGlobals.LOGGER.warn("No widget factory found for {}", option.defaultValue().getClass().getSimpleName());
return;
}

ClickableWidget widget = factory.apply(option, this.config.getId());
ClickableWidget widget = factory.apply(option, id);
if (widget == null)
throw new IllegalStateException("Widget factory returned null for %s".formatted(option.defaultValue().getClass().getSimpleName()));

widget.setWidth(0);
widget.setHeight(20);

Text tooltip = Text.translatableWithFallback("%s.tooltip".formatted(option.translationKey(id)), "");
if (!tooltip.getString().isEmpty()) widget.setTooltip(Tooltip.of(tooltip));

options.add(widget);
});

scrollableWidget.addOptions(Arrays.copyOf(options.toArray(), options.size(), ClickableWidget[].class));
scrollableWidget.addOptions(options);
this.addDrawableChild(scrollableWidget);
this.addDrawableChild(new ButtonWidget.Builder(ScreenTexts.DONE, button -> close()).dimensions(this.width / 2 - 100, this.height - 27, 200, 20).build());
}
Expand All @@ -78,9 +104,7 @@ public void close() {
this.client.setScreen(this.parent);
}

public void save() {
config.save();
}
public abstract void save();

@Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package dev.spiritstudios.specter.api.config;

import dev.spiritstudios.specter.api.core.util.PatternMap;
import dev.spiritstudios.specter.api.gui.widget.SpecterButtonWidget;
import dev.spiritstudios.specter.api.gui.widget.SpecterSliderWidget;
import net.minecraft.block.Block;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.widget.ClickableWidget;
import net.minecraft.client.gui.widget.TextFieldWidget;
import net.minecraft.item.Item;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.screen.ScreenTexts;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import net.minecraft.util.Identifier;
import org.jetbrains.annotations.ApiStatus;

import java.util.Arrays;
import java.util.List;
import java.util.function.BiFunction;
import java.util.stream.Collectors;

@SuppressWarnings("unchecked")
public final class ConfigScreenWidgets {
private static final PatternMap<BiFunction<Value<?>, String, ? extends ClickableWidget>> widgetFactories = new PatternMap<>();

static {
addRegistry(Item.class, Registries.ITEM);
addRegistry(Block.class, Registries.BLOCK);
}

private static final BiFunction<Value<?>, String, ? extends ClickableWidget> BOOLEAN_WIDGET_FACTORY = (configValue, id) -> {
Value<Boolean> value = (Value<Boolean>) configValue;

return SpecterButtonWidget.builder(
() -> Text.translatable(value.translationKey(id)).append(": ").append(ScreenTexts.onOrOff(value.get())),
button -> value.set(!value.get())
).build();
};

private static final BiFunction<Value<?>, String, ? extends ClickableWidget> INTEGER_WIDGET_FACTORY = (configValue, id) -> {
NumericValue<Integer> value = (NumericValue<Integer>) configValue;

return SpecterSliderWidget.builder(value.get())
.message((val) -> Text.translatable(value.translationKey(id)).append(String.format(": %.0f", val)))
.range(value.range().min(), value.range().max())
.step(value.step() == 0 ? 1 : value.step())
.onValueChanged((val) -> value.set(val.intValue()))
.build();
};

private static final BiFunction<Value<?>, String, ? extends ClickableWidget> DOUBLE_WIDGET_FACTORY = (configValue, id) -> {
NumericValue<Double> value = (NumericValue<Double>) configValue;

return SpecterSliderWidget.builder(value.get())
.message((val) -> Text.translatable(value.translationKey(id)).append(String.format(": %.2f", val)))
.range(value.range())
.step(value.step())
.onValueChanged(value::set)
.build();
};

private static final BiFunction<Value<?>, String, ? extends ClickableWidget> FLOAT_WIDGET_FACTORY = (configValue, id) -> {
NumericValue<Float> value = (NumericValue<Float>) configValue;

return SpecterSliderWidget.builder(value.get())
.message((val) -> Text.translatable(configValue.translationKey(id)).append(String.format(": %.1f", val)))
.range(value.range().min(), value.range().max())
.step(value.step())
.onValueChanged((val) -> value.set(val.floatValue()))
.build();
};

private static final BiFunction<Value<?>, String, ? extends ClickableWidget> STRING_WIDGET_FACTORY = (configValue, id) -> {
Value<String> value = (Value<String>) configValue;

TextFieldWidget widget = new TextFieldWidget(MinecraftClient.getInstance().textRenderer, 0, 0, 0, 0, Text.of(value.get()));
widget.setPlaceholder(Text.translatableWithFallback("%s.placeholder".formatted(configValue.translationKey(id)), "").formatted(Formatting.DARK_GRAY));

widget.setText(value.get());
widget.setChangedListener(value::set);
widget.setSelectionEnd(0);
widget.setSelectionStart(0);

return widget;
};

private static final BiFunction<Value<?>, String, ? extends ClickableWidget> ENUM_WIDGET_FACTORY = (configValue, id) -> {
Value<Enum<?>> value = (Value<Enum<?>>) configValue;

List<Enum<?>> enumValues = Arrays.stream(configValue.defaultValue().getClass().getEnumConstants())
.filter(val -> val instanceof Enum<?>)
.map(val -> (Enum<?>) val)
.collect(Collectors.toList());

if (enumValues.isEmpty()) throw new IllegalArgumentException("Enum values cannot be null");

return SpecterButtonWidget.builder(
() -> Text.translatable(configValue.translationKey(id)).append(": ").append(Text.translatable("%s.%s".formatted(configValue.translationKey(id), value.get().toString().toLowerCase()))),
button -> {
Enum<?> current = value.get();
int index = enumValues.indexOf(current);
value.set(enumValues.get((index + 1) % enumValues.size()));
}
).build();
};

private ConfigScreenWidgets() {
}

public static <T> void addRegistry(Class<T> clazz, Registry<T> registry) {
widgetFactories.put(clazz, (configValue, id) -> {
Value<T> value = (Value<T>) configValue;

TextFieldWidget widget = new TextFieldWidget(MinecraftClient.getInstance().textRenderer, 0, 0, 0, 0, Text.of(registry.getEntry(value.get()).getIdAsString()));
widget.setPlaceholder(Text.translatableWithFallback("%s.placeholder".formatted(configValue.translationKey(id)), "").formatted(Formatting.DARK_GRAY));

widget.setText(value.get().toString());
widget.setChangedListener(val -> {
Identifier identifier = Identifier.tryParse(val);
if (identifier == null) return;

registry.getOrEmpty(identifier).ifPresent(value::set);
});
widget.setSelectionEnd(0);
widget.setSelectionStart(0);

return widget;
});
}

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

@ApiStatus.Internal
public static <T> BiFunction<Value<?>, String, ? extends ClickableWidget> getWidgetFactory(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());
};
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package dev.spiritstudios.specter.api;
package dev.spiritstudios.specter.api.config;

import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.fabricmc.loader.api.FabricLoader;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dev.spiritstudios.specter.api.config;

import net.minecraft.client.gui.screen.Screen;

public class RootConfigScreen extends ConfigScreen {
private final ConfigHolder<?, ?> holder;

public RootConfigScreen(ConfigHolder<?, ?> holder, Screen parent) {
super(holder.get(), holder.id().toTranslationKey(), parent);
this.holder = holder;
}

@Override
public void save() {
this.holder.save();
}
}
Loading

0 comments on commit 3ac4f34

Please sign in to comment.