diff --git a/build-logic/src/main/java/juuxel/adorn/gradle/action/InlineServiceLoader.java b/build-logic/src/main/java/juuxel/adorn/gradle/action/InlineServiceLoader.java index 08c09b24f..fddde97f3 100644 --- a/build-logic/src/main/java/juuxel/adorn/gradle/action/InlineServiceLoader.java +++ b/build-logic/src/main/java/juuxel/adorn/gradle/action/InlineServiceLoader.java @@ -1,8 +1,6 @@ package juuxel.adorn.gradle.action; import juuxel.adorn.gradle.asm.Asm; -import juuxel.adorn.gradle.asm.InsnPattern; -import juuxel.adorn.gradle.asm.MethodBodyPattern; import org.gradle.api.Action; import org.gradle.api.Task; import org.objectweb.asm.ConstantDynamic; @@ -12,9 +10,7 @@ import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.LdcInsnNode; -import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.TypeInsnNode; import java.io.IOException; import java.io.UncheckedIOException; @@ -23,26 +19,6 @@ import java.util.Objects; public final class InlineServiceLoader implements Action { - private static final MethodBodyPattern INLINE_PATTERN = new MethodBodyPattern( - List.of( - new InsnPattern<>(LdcInsnNode.class, ldc -> ldc.cst instanceof Type), - new InsnPattern<>( - MethodInsnNode.class, - method -> method.getOpcode() == Opcodes.INVOKESTATIC - && "java/util/ServiceLoader".equals(method.owner) - && "load".equals(method.name) - && "(Ljava/lang/Class;)Ljava/util/ServiceLoader;".equals(method.desc) - ), - new InsnPattern<>( - MethodInsnNode.class, - method -> method.getOpcode() == Opcodes.INVOKEVIRTUAL - && "java/util/ServiceLoader".equals(method.owner) - && "findFirst".equals(method.name) - && "()Ljava/util/Optional;".equals(method.desc) - ) - ) - ); - @Override public void execute(Task task) { try { @@ -61,39 +37,6 @@ private void run(Task task) throws IOException { } for (MethodNode method : classNode.methods) { - boolean success = INLINE_PATTERN.match(method, ctx -> { - var ldc = (LdcInsnNode) ctx.instructions().get(0); - var type = ((Type) ldc.cst).getInternalName().replace('/', '.'); - var serviceFile = filer.apply("META-INF/services/" + type); - if (Files.exists(serviceFile)) { - try { - var serviceImpls = Files.readAllLines(serviceFile); - if (serviceImpls.size() != 1) throw new IllegalArgumentException("Service file " + type + " must have exactly one provider"); - var implType = serviceImpls.get(0).replace('.', '/'); - ctx.replaceWith( - List.of( - new TypeInsnNode(Opcodes.NEW, implType), - new InsnNode(Opcodes.DUP), - new MethodInsnNode(Opcodes.INVOKESPECIAL, implType, "", "()V"), - new MethodInsnNode( - Opcodes.INVOKESTATIC, - "java/util/Optional", - "of", - "(Ljava/lang/Object;)Ljava/util/Optional;" - ) - ) - ); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - } - }); - - if (success) { - method.maxStack++; - return true; - } - if (method.invisibleAnnotations == null) continue; for (AnnotationNode annotation : method.invisibleAnnotations) { if ("Ljuuxel/adorn/util/InlineServices$Getter;".equals(annotation.desc)) { @@ -125,13 +68,7 @@ private void run(Task task) throws IOException { "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/Class;Ljava/lang/invoke/MethodHandle;[Ljava/lang/Object;)Ljava/lang/Object;", false ), - new Handle( - Opcodes.H_NEWINVOKESPECIAL, - implType, - "", - "()V", - false - ) + new Handle(Opcodes.H_NEWINVOKESPECIAL, implType, "", "()V", false) ))); method.instructions.add(new InsnNode(Opcodes.ARETURN)); return true; diff --git a/common/src/main/java/juuxel/adorn/client/gui/screen/AbstractConfigScreen.java b/common/src/main/java/juuxel/adorn/client/gui/screen/AbstractConfigScreen.java new file mode 100644 index 000000000..9208af7f0 --- /dev/null +++ b/common/src/main/java/juuxel/adorn/client/gui/screen/AbstractConfigScreen.java @@ -0,0 +1,233 @@ +package juuxel.adorn.client.gui.screen; + +import com.mojang.blaze3d.systems.RenderSystem; +import juuxel.adorn.AdornCommon; +import juuxel.adorn.client.gui.widget.ConfigScreenHeading; +import juuxel.adorn.config.ConfigManager; +import juuxel.adorn.util.Colors; +import juuxel.adorn.util.Displayable; +import juuxel.adorn.util.PropertyRef; +import juuxel.adorn.util.animation.AnimationEngine; +import juuxel.adorn.util.animation.AnimationTask; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.NoticeScreen; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.tooltip.Tooltip; +import net.minecraft.client.gui.widget.CyclingButtonWidget; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.RotationAxis; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public abstract class AbstractConfigScreen extends Screen { + private static final int CONFIG_BUTTON_START_Y = 40; + public static final int BUTTON_HEIGHT = 20; + public static final int BUTTON_GAP = 4; + public static final int BUTTON_SPACING = BUTTON_HEIGHT + BUTTON_GAP; + private static final int HEART_SIZE = 12; + private static final int[] HEART_COLORS = new int[] { + 0xFF_FF0000, // Red + 0xFF_FC8702, // Orange + 0xFF_FFFF00, // Yellow + 0xFF_A7FC58, // Green + 0xFF_2D61FC, // Blue + 0xFF_A002FC, // Purple + 0xFF_58E9FC, // Light blue + 0xFF_FCA1DF, // Pink + }; + private static final Identifier HEART_TEXTURE = AdornCommon.id("textures/gui/heart.png"); + private static final double MIN_HEART_SPEED = 0.05; + private static final double MAX_HEART_SPEED = 1.5; + private static final double MAX_HEART_ANGULAR_SPEED = 0.07; + private static final int HEART_CHANCE = 65; + + private final Screen parent; + private final Random random = new Random(); + private final List hearts = new ArrayList<>(); + private boolean restartRequired = false; + private final AnimationEngine animationEngine = new AnimationEngine(); + + /** The Y-coordinate of the next config option or heading to be added. */ + protected int nextChildY = CONFIG_BUTTON_START_Y; + + protected AbstractConfigScreen(Text title, Screen parent) { + super(title); + this.parent = parent; + animationEngine.add(new HeartAnimationTask()); + } + + @Override + protected void init() { + nextChildY = CONFIG_BUTTON_START_Y; + animationEngine.start(); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + renderBackground(context, mouseX, mouseY, delta); + synchronized (hearts) { + renderHearts(context, delta); + } + context.drawCenteredTextWithShadow(textRenderer, title, width / 2, 20, Colors.WHITE); + super.render(context, mouseX, mouseY, delta); + } + + private void renderHearts(DrawContext context, float delta) { + for (var heart : hearts) { + RenderSystem.setShaderColor(Colors.redOf(heart.color), Colors.greenOf(heart.color), Colors.blueOf(heart.color), 1f); + var matrices = context.getMatrices(); + matrices.push(); + matrices.translate(heart.x, MathHelper.lerp(delta, heart.previousY, heart.y), 0.0); + matrices.translate(0.5 * HEART_SIZE, 0.5 * HEART_SIZE, 0.0); + var angle = MathHelper.lerp(delta, heart.previousAngle, heart.angle); + matrices.multiply(RotationAxis.POSITIVE_Z.rotation((float) angle)); + matrices.translate(-0.5 * HEART_SIZE, -0.5 * HEART_SIZE, 0.0); + context.drawTexture(HEART_TEXTURE, 0, 0, HEART_SIZE, HEART_SIZE, 0f, 0f, 8, 8, 8, 8); + matrices.pop(); + } + + RenderSystem.setShaderColor(1f, 1f, 1f, 1f); + } + + @Override + public void close() { + client.setScreen(restartRequired ? new NoticeScreen( + () -> client.setScreen(parent), + Text.translatable("gui.adorn.config.restart_required.title"), + Text.translatable("gui.adorn.config.restart_required.message"), + Text.translatable("gui.ok"), + true + ) : parent); + } + + @Override + public void removed() { + animationEngine.stop(); + } + + private void tickHearts() { + var iter = hearts.iterator(); + while (iter.hasNext()) { + var heart = iter.next(); + + if (heart.y - HEART_SIZE > height) { + iter.remove(); + } else { + heart.move(); + } + } + + if (random.nextInt(HEART_CHANCE) == 0) { + int x = random.nextInt(width); + int color = HEART_COLORS[random.nextInt(HEART_COLORS.length)]; + double speed = random.nextDouble(MIN_HEART_SPEED, MAX_HEART_SPEED); + double angularSpeed = random.nextDouble(-MAX_HEART_ANGULAR_SPEED, MAX_HEART_ANGULAR_SPEED); + hearts.add(new Heart(x, -HEART_SIZE, color, speed, angularSpeed)); + } + } + + private CyclingButtonWidget createConfigButton(CyclingButtonWidget.Builder builder, int x, int y, int width, PropertyRef property, boolean restartRequired) { + return builder + .tooltip(value -> { + var text = Text.translatable(getTooltipTranslationKey(property.getName())); + if (restartRequired) { + text.append(Text.literal("\n")) + .append(Text.translatable("gui.adorn.config.requires_restart").formatted(Formatting.ITALIC, Formatting.GOLD)); + } + return Tooltip.of(text); + }) + .build(x, y, width, BUTTON_HEIGHT, Text.translatable(getOptionTranslationKey(property.getName())), (button, value) -> { + property.set(value); + ConfigManager.get().save(); + + if (restartRequired) { + this.restartRequired = true; + } + }); + } + + protected void addConfigToggle(int width, PropertyRef property) { + addConfigToggle(width, property, false); + } + + protected void addConfigToggle(int width, PropertyRef property, boolean restartRequired) { + var button = createConfigButton( + CyclingButtonWidget.onOffBuilder(property.get()), + (this.width - width) / 2, nextChildY, width, property, restartRequired + ); + addDrawableChild(button); + nextChildY += BUTTON_SPACING; + } + + protected void addConfigButton(int width, PropertyRef property, List values) { + addConfigButton(width, property, values, false); + } + + protected void addConfigButton(int width, PropertyRef property, List values, boolean restartRequired) { + var button = createConfigButton( + CyclingButtonWidget.builder(Displayable::getDisplayName).values(values).initially(property.get()), + (this.width - width) / 2, nextChildY, width, property, restartRequired + ); + addDrawableChild(button); + nextChildY += BUTTON_SPACING; + } + + protected void addHeading(Text text, int width) { + addDrawable(new ConfigScreenHeading(text, (this.width - width) / 2, nextChildY, width)); + nextChildY += ConfigScreenHeading.HEIGHT; + } + + protected String getOptionTranslationKey(String name) { + return "gui.adorn.config.option." + name; + } + + private String getTooltipTranslationKey(String name) { + return getOptionTranslationKey(name) + ".description"; + } + + private static final class Heart { + private final int x; + private double y; + private final int color; + private final double speed; + private final double angularSpeed; + private double previousY; + private double previousAngle = 0.0; + private double angle = 0.0; + + private Heart(int x, double y, int color, double speed, double angularSpeed) { + this.x = x; + this.y = y; + this.color = color; + this.speed = speed; + this.angularSpeed = angularSpeed; + previousY = y; + } + + private void move() { + previousY = y; + y += speed; + previousAngle = angle; + angle = (angle + angularSpeed) % MathHelper.TAU; + } + } + + private final class HeartAnimationTask implements AnimationTask { + @Override + public boolean isAlive() { + return true; + } + + @Override + public void tick() { + synchronized (hearts) { + tickHearts(); + } + } + } +} diff --git a/common/src/main/java/juuxel/adorn/client/gui/screen/GameRuleDefaultsScreen.java b/common/src/main/java/juuxel/adorn/client/gui/screen/GameRuleDefaultsScreen.java new file mode 100644 index 000000000..100e0dda3 --- /dev/null +++ b/common/src/main/java/juuxel/adorn/client/gui/screen/GameRuleDefaultsScreen.java @@ -0,0 +1,36 @@ +package juuxel.adorn.client.gui.screen; + +import juuxel.adorn.config.ConfigManager; +import juuxel.adorn.util.PropertyRef; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.screen.ScreenTexts; +import net.minecraft.text.Text; + +public final class GameRuleDefaultsScreen extends AbstractConfigScreen { + private static final int BUTTON_WIDTH = 250; + + public GameRuleDefaultsScreen(Screen parent) { + super(Text.translatable("gui.adorn.config.game_rule_defaults"), parent); + } + + @Override + protected void init() { + super.init(); + var config = ConfigManager.config(); + addConfigToggle(BUTTON_WIDTH, PropertyRef.ofField(config.gameRuleDefaults, "skipNightOnSofas")); + addConfigToggle(BUTTON_WIDTH, PropertyRef.ofField(config.gameRuleDefaults, "infiniteKitchenSinks")); + addConfigToggle(BUTTON_WIDTH, PropertyRef.ofField(config.gameRuleDefaults, "dropLockedTradingStations")); + addDrawableChild( + ButtonWidget.builder(ScreenTexts.BACK, button -> close()) + .position(this.width / 2 - 100, this.height - 27) + .size(200, 20) + .build() + ); + } + + @Override + protected String getOptionTranslationKey(String name) { + return "gamerule.adorn:" + name; + } +} diff --git a/common/src/main/java/juuxel/adorn/client/gui/screen/MainConfigScreen.java b/common/src/main/java/juuxel/adorn/client/gui/screen/MainConfigScreen.java new file mode 100644 index 000000000..3a098bf50 --- /dev/null +++ b/common/src/main/java/juuxel/adorn/client/gui/screen/MainConfigScreen.java @@ -0,0 +1,52 @@ +package juuxel.adorn.client.gui.screen; + +import juuxel.adorn.config.ConfigManager; +import juuxel.adorn.fluid.FluidUnit; +import juuxel.adorn.item.group.ItemGroupingOption; +import juuxel.adorn.util.PropertyRef; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.screen.ScreenTexts; +import net.minecraft.text.Text; + +import java.util.Arrays; + +public final class MainConfigScreen extends AbstractConfigScreen { + private static final int BUTTON_WIDTH = 200; + + public MainConfigScreen(Screen parent) { + super(Text.translatable("gui.adorn.config.title"), parent); + } + + @Override + protected void init() { + super.init(); + var config = ConfigManager.config(); + int x = (width - BUTTON_WIDTH) / 2; + addHeading(Text.translatable("gui.adorn.config.visual"), BUTTON_WIDTH); + addConfigToggle(BUTTON_WIDTH, PropertyRef.ofField(config.client, "showTradingStationTooltips")); + addConfigButton(BUTTON_WIDTH, PropertyRef.ofField(config.client, "displayedFluidUnit"), Arrays.asList(FluidUnit.values())); + addHeading(Text.translatable("gui.adorn.config.creative_inventory"), BUTTON_WIDTH); + addConfigToggle(BUTTON_WIDTH, PropertyRef.ofField(config.client, "showItemsInStandardGroups")); + addConfigButton( + BUTTON_WIDTH, + PropertyRef.ofField(config, "groupItems"), + Arrays.asList(ItemGroupingOption.values()), + true + ); + addHeading(Text.translatable("gui.adorn.config.other"), BUTTON_WIDTH); + addDrawableChild( + ButtonWidget.builder(Text.translatable("gui.adorn.config.game_rule_defaults"), + widget -> client.setScreen(new GameRuleDefaultsScreen(this))) + .position(x, nextChildY) + .size(BUTTON_WIDTH, 20) + .build() + ); + addDrawableChild( + ButtonWidget.builder(ScreenTexts.DONE, widget -> close()) + .position(this.width / 2 - 100, this.height - 27) + .size(200, 20) + .build() + ); + } +} diff --git a/common/src/main/java/juuxel/adorn/client/gui/widget/SizedElement.java b/common/src/main/java/juuxel/adorn/client/gui/widget/SizedElement.java new file mode 100644 index 000000000..f59fdbb0e --- /dev/null +++ b/common/src/main/java/juuxel/adorn/client/gui/widget/SizedElement.java @@ -0,0 +1,8 @@ +package juuxel.adorn.client.gui.widget; + +import net.minecraft.client.gui.Element; + +public interface SizedElement extends Element { + int getWidth(); + int getHeight(); +} diff --git a/common/src/main/java/juuxel/adorn/config/Config.java b/common/src/main/java/juuxel/adorn/config/Config.java new file mode 100644 index 000000000..d26e37592 --- /dev/null +++ b/common/src/main/java/juuxel/adorn/config/Config.java @@ -0,0 +1,44 @@ +package juuxel.adorn.config; + +import blue.endless.jankson.Comment; +import juuxel.adorn.fluid.FluidUnit; +import juuxel.adorn.item.group.ItemGroupingOption; + +import java.util.HashMap; +import java.util.Map; + +public final class Config { + @Comment("How items will be grouped in Adorn's creative tab") + public ItemGroupingOption groupItems = ItemGroupingOption.BY_MATERIAL; + + @Comment("Client-side settings") + public Client client = new Client(); + + @Comment("Default values for game rules in new worlds") + public GameRuleDefaults gameRuleDefaults = new GameRuleDefaults(); + + @Comment("Mod compatibility toggles (enabled: true, disabled: false)") + public Map compat = new HashMap<>(); + + public static final class Client { + @Comment("If true, floating tooltips are shown above trading stations.") + public boolean showTradingStationTooltips = true; + + @Comment("If true, Adorn items will also be shown in matching vanilla item tabs.") + public boolean showItemsInStandardGroups = true; + + @Comment("The fluid unit to show fluid amounts in. Options: [litres, droplets]") + public FluidUnit displayedFluidUnit = FluidUnit.LITRE; + } + + public static final class GameRuleDefaults { + @Comment("If true, sleeping on sofas can skip the night.") + public boolean skipNightOnSofas = true; + + @Comment("If true, kitchen sinks are infinite sources for infinite fluids.") + public boolean infiniteKitchenSinks = true; + + @Comment("If true, broken trading stations drop a locked version with their contents inside.") + public boolean dropLockedTradingStations = true; + } +} diff --git a/common/src/main/java/juuxel/adorn/config/ConfigManager.java b/common/src/main/java/juuxel/adorn/config/ConfigManager.java new file mode 100644 index 000000000..772725e1d --- /dev/null +++ b/common/src/main/java/juuxel/adorn/config/ConfigManager.java @@ -0,0 +1,114 @@ +package juuxel.adorn.config; + +import blue.endless.jankson.Jankson; +import blue.endless.jankson.JsonObject; +import blue.endless.jankson.JsonPrimitive; +import blue.endless.jankson.api.DeserializationException; +import juuxel.adorn.fluid.FluidUnit; +import juuxel.adorn.util.InlineServices; +import juuxel.adorn.util.Logging; +import juuxel.adorn.util.Services; +import org.slf4j.Logger; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; + +@InlineServices +public abstract class ConfigManager { + private static final Jankson JANKSON = Jankson.builder() + .registerSerializer(FluidUnit.class, (unit, m) -> new JsonPrimitive(unit.getId())) + .registerDeserializer(JsonPrimitive.class, FluidUnit.class, + (json, m) -> Objects.requireNonNullElse(FluidUnit.byId(json.asString()), FluidUnit.LITRE)) + .build(); + private static final JsonObject DEFAULT = (JsonObject) JANKSON.toJson(new Config()); + private static final Logger LOGGER = Logging.logger(); + + private Config config; + private boolean saveScheduled = false; + private boolean finalized = false; + + @InlineServices.Getter + public static ConfigManager get() { + return Services.load(ConfigManager.class); + } + + public static Config config() { + return get().config; + } + + protected abstract Path getConfigDirectory(); + + private Path getConfigPath() { + return getConfigDirectory().resolve("Adorn.json5"); + } + + public void init() { + loadConfig(); + } + + private void loadConfig() { + var configPath = getConfigPath(); + if (Files.notExists(configPath)) { + save(new Config()); + } + + try { + var obj = JANKSON.load(Files.readString(configPath)); + Config config; + try { + config = JANKSON.fromJsonCarefully(obj, Config.class); + } catch (DeserializationException e) { + // Try deserializing carelessly and throw the exception if it returns null + config = JANKSON.fromJson(obj, Config.class); + if (config == null) throw e; + } + + if (isMissingKeys(obj, DEFAULT)) { + LOGGER.info("[Adorn] Upgrading config..."); + save(config); + } + + this.config = config; + } catch (Exception e) { + throw new RuntimeException("Failed to load Adorn config file!", e); + } + } + + public void save() { + if (finalized) { + save(config); + } else { + saveScheduled = true; + } + } + + public void finish() { + finalized = true; + if (saveScheduled) { + save(); + } + } + + private void save(Config config) { + try { + Files.writeString(getConfigPath(), JANKSON.toJson(config).toJson(true, true)); + } catch (IOException e) { + LOGGER.error("[Adorn] Could not save config file {}", getConfigPath(), e); + } + } + + private boolean isMissingKeys(JsonObject config, JsonObject defaults) { + for (var entry : defaults.entrySet()) { + if (!config.containsKey(entry.getKey())) return true; + + if (entry.getValue() instanceof JsonObject value) { + var actual = config.get(JsonObject.class, entry.getKey()); + return actual == null || isMissingKeys(actual, value); + } + } + + return false; + } +} diff --git a/common/src/main/java/juuxel/adorn/item/group/AdornItemGroups.java b/common/src/main/java/juuxel/adorn/item/group/AdornItemGroups.java index 8d15d7364..378c2c2c5 100644 --- a/common/src/main/java/juuxel/adorn/item/group/AdornItemGroups.java +++ b/common/src/main/java/juuxel/adorn/item/group/AdornItemGroups.java @@ -82,7 +82,7 @@ public final class AdornItemGroups { .build()); public static void init() { - if (ConfigManager.get().config().client.showItemsInStandardGroups) { + if (ConfigManager.config().client.showItemsInStandardGroups) { addToVanillaItemGroups(); } } diff --git a/common/src/main/kotlin/juuxel/adorn/client/gui/screen/AbstractConfigScreen.kt b/common/src/main/kotlin/juuxel/adorn/client/gui/screen/AbstractConfigScreen.kt deleted file mode 100644 index 9a992af79..000000000 --- a/common/src/main/kotlin/juuxel/adorn/client/gui/screen/AbstractConfigScreen.kt +++ /dev/null @@ -1,203 +0,0 @@ -package juuxel.adorn.client.gui.screen - -import com.mojang.blaze3d.systems.RenderSystem -import juuxel.adorn.AdornCommon -import juuxel.adorn.client.gui.widget.ConfigScreenHeading -import juuxel.adorn.config.ConfigManager -import juuxel.adorn.util.Colors -import juuxel.adorn.util.Displayable -import juuxel.adorn.util.PropertyRef -import juuxel.adorn.util.animation.AnimationEngine -import juuxel.adorn.util.animation.AnimationTask -import juuxel.adorn.util.color -import net.minecraft.client.gui.DrawContext -import net.minecraft.client.gui.screen.NoticeScreen -import net.minecraft.client.gui.screen.Screen -import net.minecraft.client.gui.tooltip.Tooltip -import net.minecraft.client.gui.widget.CyclingButtonWidget -import net.minecraft.text.Text -import net.minecraft.util.Formatting -import net.minecraft.util.math.MathHelper -import net.minecraft.util.math.RotationAxis -import kotlin.random.Random - -abstract class AbstractConfigScreen(title: Text, private val parent: Screen) : Screen(title) { - private val random: Random = Random.Default - private val hearts: MutableList = ArrayList() - private var restartRequired = false - private val animationEngine = AnimationEngine() - - /** The Y-coordinate of the next config option or heading to be added. */ - protected var nextChildY: Int = CONFIG_BUTTON_START_Y - - init { - animationEngine.add(HeartAnimationTask()) - } - - override fun init() { - nextChildY = CONFIG_BUTTON_START_Y - animationEngine.start() - } - - override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) { - renderBackground(context, mouseX, mouseY, delta) - synchronized(hearts) { - renderHearts(context, delta) - } - context.drawCenteredTextWithShadow(textRenderer, title, width / 2, 20, Colors.WHITE) - super.render(context, mouseX, mouseY, delta) - } - - private fun renderHearts(context: DrawContext, delta: Float) { - for (heart in hearts) { - RenderSystem.setShaderColor(Colors.redOf(heart.color), Colors.greenOf(heart.color), Colors.blueOf(heart.color), 1f) - val matrices = context.matrices - matrices.push() - matrices.translate(heart.x.toDouble(), MathHelper.lerp(delta.toDouble(), heart.previousY, heart.y), 0.0) - matrices.translate(HEART_SIZE.toDouble() / 2, HEART_SIZE.toDouble() / 2, 0.0) - matrices.multiply(RotationAxis.POSITIVE_Z.rotation(heart.angle.toFloat())) - matrices.translate(-HEART_SIZE.toDouble() / 2, -HEART_SIZE.toDouble() / 2, 0.0) - context.drawTexture(HEART_TEXTURE, 0, 0, HEART_SIZE, HEART_SIZE, 0f, 0f, 8, 8, 8, 8) - matrices.pop() - } - - RenderSystem.setShaderColor(1f, 1f, 1f, 1f) - } - - override fun close() { - client!!.setScreen( - if (restartRequired) { - NoticeScreen( - { client!!.setScreen(parent) }, - Text.translatable("gui.adorn.config.restart_required.title"), - Text.translatable("gui.adorn.config.restart_required.message"), - Text.translatable("gui.ok"), - true - ) - } else { - parent - } - ) - } - - override fun removed() { - animationEngine.stop() - } - - private fun tickHearts() { - val iter = hearts.iterator() - while (iter.hasNext()) { - val heart = iter.next() - - if (heart.y - HEART_SIZE > height) { - iter.remove() - } else { - heart.move() - } - } - - if (random.nextInt(HEART_CHANCE) == 0) { - val x = random.nextInt(width) - val color = HEART_COLORS.random(random) - val speed = random.nextDouble(MIN_HEART_SPEED, MAX_HEART_SPEED) - val angularSpeed = random.nextDouble(-MAX_HEART_ANGULAR_SPEED, MAX_HEART_ANGULAR_SPEED) - hearts += Heart(x, -HEART_SIZE.toDouble(), color, speed, angularSpeed) - } - } - - private fun createConfigButton( - builder: CyclingButtonWidget.Builder, x: Int, y: Int, width: Int, property: PropertyRef, restartRequired: Boolean - ) = builder.tooltip { - val text = Text.translatable(getTooltipTranslationKey(property.name)) - if (restartRequired) { - text.append(Text.literal("\n")) - .append(Text.translatable("gui.adorn.config.requires_restart").formatted(Formatting.ITALIC, Formatting.GOLD)) - } - Tooltip.of(text) - }.build(x, y, width, BUTTON_HEIGHT, Text.translatable(getOptionTranslationKey(property.name))) { _, value -> - property.set(value) - ConfigManager.INSTANCE.save() - - if (restartRequired) { - this.restartRequired = true - } - } - - protected fun addConfigToggle( - width: Int, property: PropertyRef, restartRequired: Boolean = false - ) { - val button = createConfigButton( - CyclingButtonWidget.onOffBuilder(property.get()), - (this.width - width) / 2, nextChildY, width, property, restartRequired - ) - addDrawableChild(button) - nextChildY += BUTTON_SPACING - } - - protected fun addConfigButton( - width: Int, property: PropertyRef, values: List, restartRequired: Boolean = false - ) { - val button = createConfigButton( - CyclingButtonWidget.builder { it.displayName }.values(values).initially(property.get()), - (this.width - width) / 2, nextChildY, width, property, restartRequired - ) - addDrawableChild(button) - nextChildY += BUTTON_SPACING - } - - protected fun addHeading(text: Text, width: Int) { - addDrawable(ConfigScreenHeading(text, (this.width - width) / 2, nextChildY, width)) - nextChildY += ConfigScreenHeading.HEIGHT - } - - protected open fun getOptionTranslationKey(name: String): String = - "gui.adorn.config.option.$name" - - private fun getTooltipTranslationKey(name: String): String = - "${getOptionTranslationKey(name)}.description" - - companion object { - private const val CONFIG_BUTTON_START_Y = 40 - const val BUTTON_HEIGHT = 20 - const val BUTTON_GAP = 4 - const val BUTTON_SPACING = BUTTON_HEIGHT + BUTTON_GAP - private const val HEART_SIZE = 12 - private val HEART_COLORS: List = listOf( - color(0xFF0000), // Red - color(0xFC8702), // Orange - color(0xFFFF00), // Yellow - color(0xA7FC58), // Green - color(0x2D61FC), // Blue - color(0xA002FC), // Purple - color(0x58E9FC), // Light blue - color(0xFCA1DF), // Pink - ) - private val HEART_TEXTURE = AdornCommon.id("textures/gui/heart.png") - private const val MIN_HEART_SPEED = 0.05 - private const val MAX_HEART_SPEED = 1.5 - private const val MAX_HEART_ANGULAR_SPEED = 0.07 - private const val HEART_CHANCE = 65 - } - - private class Heart(val x: Int, var y: Double, val color: Int, val speed: Double, val angularSpeed: Double) { - var previousY: Double = y - var previousAngle: Double = 0.0 - var angle: Double = 0.0 - - fun move() { - previousY = y - y += speed - previousAngle = angle - angle = (angle + angularSpeed) % MathHelper.TAU - } - } - - private inner class HeartAnimationTask : AnimationTask { - override fun isAlive(): Boolean = true - override fun tick() { - synchronized(hearts) { - tickHearts() - } - } - } -} diff --git a/common/src/main/kotlin/juuxel/adorn/client/gui/screen/GameRuleDefaultsScreen.kt b/common/src/main/kotlin/juuxel/adorn/client/gui/screen/GameRuleDefaultsScreen.kt deleted file mode 100644 index b69b20429..000000000 --- a/common/src/main/kotlin/juuxel/adorn/client/gui/screen/GameRuleDefaultsScreen.kt +++ /dev/null @@ -1,31 +0,0 @@ -package juuxel.adorn.client.gui.screen - -import juuxel.adorn.config.ConfigManager -import juuxel.adorn.util.ref -import net.minecraft.client.gui.screen.Screen -import net.minecraft.client.gui.widget.ButtonWidget -import net.minecraft.screen.ScreenTexts -import net.minecraft.text.Text - -class GameRuleDefaultsScreen(parent: Screen) : AbstractConfigScreen(Text.translatable("gui.adorn.config.game_rule_defaults"), parent) { - override fun init() { - super.init() - val config = ConfigManager.config() - addConfigToggle(BUTTON_WIDTH, config.gameRuleDefaults.ref { it::skipNightOnSofas }) - addConfigToggle(BUTTON_WIDTH, config.gameRuleDefaults.ref { it::infiniteKitchenSinks }) - addConfigToggle(BUTTON_WIDTH, config.gameRuleDefaults.ref { it::dropLockedTradingStations }) - addDrawableChild( - ButtonWidget.builder(ScreenTexts.BACK) { close() } - .position(this.width / 2 - 100, this.height - 27) - .size(200, 20) - .build() - ) - } - - override fun getOptionTranslationKey(name: String): String = - "gamerule.adorn:$name" - - companion object { - private const val BUTTON_WIDTH = 250 - } -} diff --git a/common/src/main/kotlin/juuxel/adorn/client/gui/screen/MainConfigScreen.kt b/common/src/main/kotlin/juuxel/adorn/client/gui/screen/MainConfigScreen.kt deleted file mode 100644 index a9c09e597..000000000 --- a/common/src/main/kotlin/juuxel/adorn/client/gui/screen/MainConfigScreen.kt +++ /dev/null @@ -1,48 +0,0 @@ -package juuxel.adorn.client.gui.screen - -import juuxel.adorn.config.ConfigManager -import juuxel.adorn.fluid.FluidUnit -import juuxel.adorn.item.group.ItemGroupingOption -import juuxel.adorn.util.ref -import net.minecraft.client.gui.screen.Screen -import net.minecraft.client.gui.widget.ButtonWidget -import net.minecraft.screen.ScreenTexts -import net.minecraft.text.Text - -class MainConfigScreen(parent: Screen) : AbstractConfigScreen(Text.translatable("gui.adorn.config.title"), parent) { - override fun init() { - super.init() - val config = ConfigManager.config() - val x = (width - BUTTON_WIDTH) / 2 - addHeading(Text.translatable("gui.adorn.config.visual"), BUTTON_WIDTH) - addConfigToggle(BUTTON_WIDTH, config.client.ref { it::showTradingStationTooltips }) - addConfigButton(BUTTON_WIDTH, config.client.ref { it::displayedFluidUnit }, FluidUnit.values().toList()) - addHeading(Text.translatable("gui.adorn.config.creative_inventory"), BUTTON_WIDTH) - addConfigToggle(BUTTON_WIDTH, config.client.ref { it::showItemsInStandardGroups }) - addConfigButton( - BUTTON_WIDTH, - config.ref { it::groupItems }, - ItemGroupingOption.values().toList(), - restartRequired = true - ) - addHeading(Text.translatable("gui.adorn.config.other"), BUTTON_WIDTH) - addDrawableChild( - ButtonWidget.builder(Text.translatable("gui.adorn.config.game_rule_defaults")) { - client!!.setScreen(GameRuleDefaultsScreen(this)) - } - .position(x, nextChildY) - .size(BUTTON_WIDTH, 20) - .build() - ) - addDrawableChild( - ButtonWidget.builder(ScreenTexts.DONE) { close() } - .position(this.width / 2 - 100, this.height - 27) - .size(200, 20) - .build() - ) - } - - companion object { - private const val BUTTON_WIDTH = 200 - } -} diff --git a/common/src/main/kotlin/juuxel/adorn/client/gui/widget/SizedElement.kt b/common/src/main/kotlin/juuxel/adorn/client/gui/widget/SizedElement.kt deleted file mode 100644 index 7911fe708..000000000 --- a/common/src/main/kotlin/juuxel/adorn/client/gui/widget/SizedElement.kt +++ /dev/null @@ -1,8 +0,0 @@ -package juuxel.adorn.client.gui.widget - -import net.minecraft.client.gui.Element - -interface SizedElement : Element { - val width: Int - val height: Int -} diff --git a/common/src/main/kotlin/juuxel/adorn/config/Config.kt b/common/src/main/kotlin/juuxel/adorn/config/Config.kt deleted file mode 100644 index f5a0f9747..000000000 --- a/common/src/main/kotlin/juuxel/adorn/config/Config.kt +++ /dev/null @@ -1,51 +0,0 @@ -package juuxel.adorn.config - -import blue.endless.jankson.Comment -import juuxel.adorn.fluid.FluidUnit -import juuxel.adorn.item.group.ItemGroupingOption - -class Config { - @JvmField - @field:Comment("How items will be grouped in Adorn's creative tab") - var groupItems: ItemGroupingOption = ItemGroupingOption.BY_MATERIAL - - @JvmField - @field:Comment("Client-side settings") - var client: Client = Client() - - @JvmField - @field:Comment("Default values for game rules in new worlds") - var gameRuleDefaults: GameRuleDefaults = GameRuleDefaults() - - @JvmField - @field:Comment("Mod compatibility toggles (enabled: true, disabled: false)") - var compat: MutableMap = HashMap() - - class Client { - @JvmField - @field:Comment("If true, floating tooltips are shown above trading stations.") - var showTradingStationTooltips: Boolean = true - - @JvmField - @field:Comment("If true, Adorn items will also be shown in matching vanilla item tabs.") - var showItemsInStandardGroups: Boolean = true - - @JvmField - @field:Comment("The fluid unit to show fluid amounts in. Options: [litres, droplets]") - var displayedFluidUnit: FluidUnit = FluidUnit.LITRE - } - - class GameRuleDefaults { - @JvmField - @field:Comment("If true, sleeping on sofas can skip the night.") - var skipNightOnSofas: Boolean = true - - @JvmField - @field:Comment("If true, kitchen sinks are infinite sources for infinite fluids.") - var infiniteKitchenSinks: Boolean = true - - @JvmField - @field:Comment("If true, broken trading stations drop a locked version with their contents inside.") - var dropLockedTradingStations: Boolean = true - } -} diff --git a/common/src/main/kotlin/juuxel/adorn/config/ConfigManager.kt b/common/src/main/kotlin/juuxel/adorn/config/ConfigManager.kt deleted file mode 100644 index 2f910717f..000000000 --- a/common/src/main/kotlin/juuxel/adorn/config/ConfigManager.kt +++ /dev/null @@ -1,97 +0,0 @@ -package juuxel.adorn.config - -import blue.endless.jankson.Jankson -import blue.endless.jankson.JsonObject -import blue.endless.jankson.JsonPrimitive -import blue.endless.jankson.api.DeserializationException -import juuxel.adorn.fluid.FluidUnit -import juuxel.adorn.util.InlineServices -import juuxel.adorn.util.loadService -import juuxel.adorn.util.logger -import java.nio.file.Files -import java.nio.file.Path - -@InlineServices -abstract class ConfigManager { - protected abstract val configDirectory: Path - private val configPath: Path by lazy { configDirectory.resolve("Adorn.json5") } - private var saveScheduled: Boolean = false - private var finalized: Boolean = false - - @get:JvmName("getConfig") - val config: Config by lazy { - if (Files.notExists(configPath)) { - save(Config()) - } - - try { - val obj = JANKSON.load(Files.readAllLines(configPath).joinToString("\n")) - val config = try { - JANKSON.fromJsonCarefully(obj, Config::class.java) - } catch (e: DeserializationException) { - // Try deserializing carelessly and throw the exception if it returns null - JANKSON.fromJson(obj, Config::class.java) ?: throw e - } - - if (isMissingKeys(obj, DEFAULT)) { - LOGGER.info("[Adorn] Upgrading config...") - save(config) - } - - config - } catch (e: Exception) { - throw RuntimeException("Failed to load Adorn config file!", e) - } - } - - fun init() { - // Initialize the config - config - } - - fun save() { - if (finalized) { - save(config) - } else { - saveScheduled = true - } - } - - fun finalize() { - finalized = true - if (saveScheduled) { - save() - } - } - - private fun save(config: Config) { - Files.write(configPath, JANKSON.toJson(config).toJson(true, true).lines()) - } - - private fun isMissingKeys(config: JsonObject, defaults: JsonObject): Boolean { - for ((key, value) in defaults) { - if (!config.containsKey(key)) return true - - if (value is JsonObject && isMissingKeys(config.get(JsonObject::class.java, key) ?: return true, value)) { - return true - } - } - - return false - } - - companion object { - @JvmStatic - @get:JvmName("get") - val INSTANCE: ConfigManager by lazy { loadService() } - private val JANKSON = Jankson.builder() - .registerSerializer(FluidUnit::class.java) { unit, _ -> JsonPrimitive(unit.id) } - .registerDeserializer(JsonPrimitive::class.java, FluidUnit::class.java) { json, _ -> FluidUnit.byId(json.asString()) ?: FluidUnit.LITRE } - .build() - private val DEFAULT = JANKSON.toJson(Config()) as JsonObject - private val LOGGER = logger() - - @JvmStatic - fun config() = INSTANCE.config - } -} diff --git a/common/src/main/kotlin/juuxel/adorn/util/PropertyRef.kt b/common/src/main/kotlin/juuxel/adorn/util/PropertyRef.kt index 0792855d9..d581cd733 100644 --- a/common/src/main/kotlin/juuxel/adorn/util/PropertyRef.kt +++ b/common/src/main/kotlin/juuxel/adorn/util/PropertyRef.kt @@ -15,6 +15,7 @@ interface PropertyRef { /** * Creates a reflected property reference for a field of the [owner]. */ + @JvmStatic fun ofField(owner: Any, fieldName: String): PropertyRef { val field = owner::class.java.getField(fieldName) field.isAccessible = true diff --git a/common/src/main/kotlin/juuxel/adorn/util/Services.kt b/common/src/main/kotlin/juuxel/adorn/util/Services.kt deleted file mode 100644 index 84211b76b..000000000 --- a/common/src/main/kotlin/juuxel/adorn/util/Services.kt +++ /dev/null @@ -1,10 +0,0 @@ -package juuxel.adorn.util - -import java.util.Optional -import java.util.ServiceLoader - -inline fun loadService(): T = - ServiceLoader.load(T::class.java).findFirst().unwrapService(T::class.java) - -fun Optional.unwrapService(type: Class): T = - orElseThrow { RuntimeException("Could not find Adorn platform service ${type.simpleName}") } diff --git a/fabric/src/main/java/juuxel/adorn/Adorn.java b/fabric/src/main/java/juuxel/adorn/Adorn.java index 8b8fc149e..7d0acc734 100644 --- a/fabric/src/main/java/juuxel/adorn/Adorn.java +++ b/fabric/src/main/java/juuxel/adorn/Adorn.java @@ -52,7 +52,7 @@ public static void init() { Compat.init(); BlockVariantSets.register(); AdornBlocksFabric.afterRegister(); - ConfigManager.get().finalize(); + ConfigManager.get().finish(); } @Environment(EnvType.CLIENT) diff --git a/forge/src/main/java/juuxel/adorn/platform/forge/Adorn.java b/forge/src/main/java/juuxel/adorn/platform/forge/Adorn.java index b8144a5cd..44a4650f1 100644 --- a/forge/src/main/java/juuxel/adorn/platform/forge/Adorn.java +++ b/forge/src/main/java/juuxel/adorn/platform/forge/Adorn.java @@ -59,6 +59,6 @@ private void registerToBus(Registrar registrar, IEventBus modBus) { private void init(FMLCommonSetupEvent event) { AdornStats.init(); - ConfigManager.get().finalize(); + ConfigManager.get().finish(); } }