diff --git a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java index 053d89be0b..f1526b21d3 100644 --- a/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java +++ b/src/main/java/de/hysky/skyblocker/config/SkyblockerConfigManager.java @@ -55,7 +55,7 @@ public static void init() { throw new RuntimeException("Skyblocker: Called config init from an illegal place!"); } - HANDLER.load(); + HANDLER.load(); ClientCommandRegistrationCallback.EVENT.register(((dispatcher, registryAccess) -> dispatcher.register(ClientCommandManager.literal(SkyblockerMod.NAMESPACE).then(optionsLiteral("config")).then(optionsLiteral("options"))))); ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> { if (screen instanceof GenericContainerScreen genericContainerScreen && screen.getTitle().getString().equals("SkyBlock Menu")) { diff --git a/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java b/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java index e79ea878e4..a4509f6430 100644 --- a/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java +++ b/src/main/java/de/hysky/skyblocker/config/configs/GeneralConfig.java @@ -1,8 +1,8 @@ package de.hysky.skyblocker.config.configs; import de.hysky.skyblocker.SkyblockerMod; -import de.hysky.skyblocker.skyblock.item.CustomArmorAnimatedDyes; -import de.hysky.skyblocker.skyblock.item.CustomArmorTrims; +import de.hysky.skyblocker.skyblock.item.custom.CustomArmorAnimatedDyes; +import de.hysky.skyblocker.skyblock.item.custom.CustomArmorTrims; import de.hysky.skyblocker.skyblock.item.slottext.SlotTextMode; import dev.isxander.yacl3.config.v2.api.SerialEntry; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; diff --git a/src/main/java/de/hysky/skyblocker/mixins/ComponentHolderMixin.java b/src/main/java/de/hysky/skyblocker/mixins/ComponentHolderMixin.java index 177299bf29..eab413e3ce 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/ComponentHolderMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/ComponentHolderMixin.java @@ -6,7 +6,7 @@ import com.llamalad7.mixinextras.injector.ModifyReturnValue; import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.skyblock.item.CustomArmorTrims; +import de.hysky.skyblocker.skyblock.item.custom.CustomArmorTrims; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; diff --git a/src/main/java/de/hysky/skyblocker/mixins/DyedColorComponentMixin.java b/src/main/java/de/hysky/skyblocker/mixins/DyedColorComponentMixin.java index 12022b7c2a..dfb9dba823 100644 --- a/src/main/java/de/hysky/skyblocker/mixins/DyedColorComponentMixin.java +++ b/src/main/java/de/hysky/skyblocker/mixins/DyedColorComponentMixin.java @@ -4,7 +4,7 @@ import com.llamalad7.mixinextras.sugar.Local; import de.hysky.skyblocker.config.SkyblockerConfigManager; -import de.hysky.skyblocker.skyblock.item.CustomArmorAnimatedDyes; +import de.hysky.skyblocker.skyblock.item.custom.CustomArmorAnimatedDyes; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.Utils; import net.minecraft.component.type.DyedColorComponent; diff --git a/src/main/java/de/hysky/skyblocker/mixins/accessors/EntityRenderDispatcherAccessor.java b/src/main/java/de/hysky/skyblocker/mixins/accessors/EntityRenderDispatcherAccessor.java new file mode 100644 index 0000000000..4f5de5751d --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixins/accessors/EntityRenderDispatcherAccessor.java @@ -0,0 +1,12 @@ +package de.hysky.skyblocker.mixins.accessors; + +import net.minecraft.client.render.entity.EntityRenderDispatcher; +import net.minecraft.client.render.entity.equipment.EquipmentModelLoader; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(EntityRenderDispatcher.class) +public interface EntityRenderDispatcherAccessor { + @Accessor("equipmentModelLoader") + EquipmentModelLoader getEquipmentModelLoader(); +} diff --git a/src/main/java/de/hysky/skyblocker/mixins/accessors/SpriteContentsAccessor.java b/src/main/java/de/hysky/skyblocker/mixins/accessors/SpriteContentsAccessor.java new file mode 100644 index 0000000000..7445650530 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/mixins/accessors/SpriteContentsAccessor.java @@ -0,0 +1,12 @@ +package de.hysky.skyblocker.mixins.accessors; + +import net.minecraft.client.texture.NativeImage; +import net.minecraft.client.texture.SpriteContents; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(SpriteContents.class) +public interface SpriteContentsAccessor { + @Accessor("image") + NativeImage getImage(); +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorAnimatedDyes.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomArmorAnimatedDyes.java similarity index 55% rename from src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorAnimatedDyes.java rename to src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomArmorAnimatedDyes.java index 48f345c46c..0775af7015 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorAnimatedDyes.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomArmorAnimatedDyes.java @@ -1,41 +1,46 @@ -package de.hysky.skyblocker.skyblock.item; +package de.hysky.skyblocker.skyblock.item.custom; import com.mojang.brigadier.Command; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.BoolArgumentType; -import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.FloatArgumentType; import de.hysky.skyblocker.SkyblockerMod; import de.hysky.skyblocker.annotations.Init; import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.events.SkyblockEvents; import de.hysky.skyblocker.utils.Constants; import de.hysky.skyblocker.utils.ItemUtils; import de.hysky.skyblocker.utils.OkLabColor; import de.hysky.skyblocker.utils.Utils; import de.hysky.skyblocker.utils.command.argumenttypes.color.ColorArgumentType; import dev.isxander.yacl3.config.v2.api.SerialEntry; -import it.unimi.dsi.fastutil.objects.Object2ObjectFunction; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; -import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; +import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.RenderTickCounter; import net.minecraft.command.CommandRegistryAccess; import net.minecraft.item.ItemStack; import net.minecraft.registry.tag.ItemTags; import net.minecraft.text.Text; +import java.util.List; + import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.argument; import static net.fabricmc.fabric.api.client.command.v2.ClientCommandManager.literal; public class CustomArmorAnimatedDyes { private static final Object2ObjectOpenHashMap STATE_TRACKER_MAP = new Object2ObjectOpenHashMap<>(); - private static final Object2ObjectFunction NEW_STATE_TRACKER = _dye -> AnimatedDyeStateTracker.create(); - private static final int DEFAULT_TICK_DELAY = 4; - private static int ticks; + private static final float DEFAULT_DELAY = 0; + private static int frames; @Init public static void init() { ClientCommandRegistrationCallback.EVENT.register(CustomArmorAnimatedDyes::registerCommands); - ClientTickEvents.END_CLIENT_TICK.register(_client -> ++ticks); + WorldRenderEvents.START.register(ignored -> ++frames); + // have the animation restart on world change because why not? + SkyblockEvents.LOCATION_CHANGE.register(ignored -> cleanTrackers()); } private static void registerCommands(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess) { @@ -45,14 +50,14 @@ private static void registerCommands(CommandDispatcher customizeAnimatedDye(context.getSource(), Integer.MIN_VALUE, Integer.MIN_VALUE, 0, false, 0)) .then(argument("hex1", ColorArgumentType.hex()) .then(argument("hex2", ColorArgumentType.hex()) - .then(argument("samples", IntegerArgumentType.integer(1)) + .then(argument("duration", FloatArgumentType.floatArg(0.1f, 10f)) .then(argument("cycleBack", BoolArgumentType.bool()) - .executes(context -> customizeAnimatedDye(context.getSource(), ColorArgumentType.getIntFromHex(context, "hex1"), ColorArgumentType.getIntFromHex(context, "hex2"), IntegerArgumentType.getInteger(context, "samples"), BoolArgumentType.getBool(context, "cycleBack"), DEFAULT_TICK_DELAY)) - .then(argument("tickDelay", IntegerArgumentType.integer(0, 20)) - .executes(context ->customizeAnimatedDye(context.getSource(), ColorArgumentType.getIntFromHex(context, "hex1"), ColorArgumentType.getIntFromHex(context, "hex2"), IntegerArgumentType.getInteger(context, "samples"), BoolArgumentType.getBool(context, "cycleBack"), IntegerArgumentType.getInteger(context, "tickDelay"))))))))))); + .executes(context -> customizeAnimatedDye(context.getSource(), ColorArgumentType.getIntFromHex(context, "hex1"), ColorArgumentType.getIntFromHex(context, "hex2"), FloatArgumentType.getFloat(context, "duration"), BoolArgumentType.getBool(context, "cycleBack"), DEFAULT_DELAY)) + .then(argument("delay", FloatArgumentType.floatArg(0)) + .executes(context ->customizeAnimatedDye(context.getSource(), ColorArgumentType.getIntFromHex(context, "hex1"), ColorArgumentType.getIntFromHex(context, "hex2"), FloatArgumentType.getFloat(context, "duration"), BoolArgumentType.getBool(context, "cycleBack"), FloatArgumentType.getFloat(context, "delay"))))))))))); } - private static int customizeAnimatedDye(FabricClientCommandSource source, int color1, int color2, int samples, boolean cycleBack, int tickDelay) { + private static int customizeAnimatedDye(FabricClientCommandSource source, int color1, int color2, float duration, boolean cycleBack, float delay) { ItemStack heldItem = source.getPlayer().getMainHandStack(); if (Utils.isOnSkyblock() && heldItem != null && !heldItem.isEmpty()) { @@ -64,17 +69,15 @@ private static int customizeAnimatedDye(FabricClientCommandSource source, int co if (color1 == Integer.MIN_VALUE && color2 == Integer.MIN_VALUE) { if (customAnimatedDyes.containsKey(itemUuid)) { - customAnimatedDyes.remove(itemUuid); - SkyblockerConfigManager.save(); + SkyblockerConfigManager.update(config -> config.general.customAnimatedDyes.remove(itemUuid)); source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.customAnimatedDyes.removed"))); } else { source.sendError(Constants.PREFIX.get().append(Text.translatable("skyblocker.customAnimatedDyes.neverHad"))); } } else { - AnimatedDye animatedDye = new AnimatedDye(color1, color2, samples, cycleBack, tickDelay); + AnimatedDye animatedDye = new AnimatedDye(List.of(new DyeFrame(color1, 0), new DyeFrame(color2, 1)), cycleBack, delay, duration); - customAnimatedDyes.put(itemUuid, animatedDye); - SkyblockerConfigManager.save(); + SkyblockerConfigManager.update(config -> config.general.customAnimatedDyes.put(itemUuid, animatedDye)); source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.customAnimatedDyes.added"))); } } else { @@ -91,18 +94,31 @@ private static int customizeAnimatedDye(FabricClientCommandSource source, int co } public static int animateColorTransition(AnimatedDye animatedDye) { - AnimatedDyeStateTracker trackedState = STATE_TRACKER_MAP.computeIfAbsent(animatedDye, NEW_STATE_TRACKER); + AnimatedDyeStateTracker trackedState = STATE_TRACKER_MAP.computeIfAbsent(animatedDye, CustomArmorAnimatedDyes::createStateTracker); - if (trackedState.lastRecordedTick + animatedDye.tickDelay() > ticks) { + if (trackedState.lastRecordedFrame == frames) { return trackedState.lastColor; } - trackedState.lastRecordedTick = ticks; + trackedState.lastRecordedFrame = frames; - return animatedDye.interpolate(trackedState); + return animatedDye.interpolate(trackedState, MinecraftClient.getInstance().getRenderTickCounter()); } - private static class AnimatedDyeStateTracker { + private static AnimatedDyeStateTracker createStateTracker(AnimatedDye animatedDye) { + AnimatedDyeStateTracker tracker = new AnimatedDyeStateTracker(); + if (animatedDye.delay() > 0) { + if (animatedDye.cycleBack()) { + tracker.onBackCycle = true; + tracker.progress = Math.clamp(animatedDye.delay() / animatedDye.duration(), 0, 1); + } else { + tracker.progress = Math.clamp(1 - animatedDye.delay() / animatedDye.duration(), 0, 1); + } + } + return tracker; + } + + private static class AnimatedDyeStateTrackerOld { private int sampleCounter; private boolean onBackCycle = false; private int lastColor = 0; @@ -120,14 +136,64 @@ int getAndIncrement() { return sampleCounter++; } - static AnimatedDyeStateTracker create() { - return new AnimatedDyeStateTracker(); + static AnimatedDyeStateTrackerOld create() { + return new AnimatedDyeStateTrackerOld(); + } + } + + private static class AnimatedDyeStateTracker { + private float progress = 0; + private boolean onBackCycle = false; + private int lastColor = 0; + private int lastRecordedFrame = 0; + } + + public static void cleanTrackers() { + STATE_TRACKER_MAP.clear(); + } + + public record DyeFrame(@SerialEntry int color, @SerialEntry float time) {} + public record AnimatedDye(@SerialEntry List frames, @SerialEntry boolean cycleBack, @SerialEntry float delay, @SerialEntry float duration) { + + private int interpolate(AnimatedDyeStateTracker tracker, RenderTickCounter counter) { + + int dyeFrame = 0; + while (dyeFrame < frames.size() - 1 && frames.get(dyeFrame + 1).time < tracker.progress) dyeFrame++; + + + DyeFrame current = tracker.onBackCycle ? frames.get(dyeFrame + 1) : frames.get(dyeFrame); + DyeFrame next = tracker.onBackCycle ? frames.get(dyeFrame) : frames.get(dyeFrame + 1); + + float progress = (tracker.progress - current.time) / (next.time - current.time); + + tracker.lastColor = OkLabColor.interpolate(current.color, next.color, progress); + + float v = counter.getDynamicDeltaTicks() * 0.05f / duration(); + if (tracker.onBackCycle) { + tracker.progress -= v; + if (tracker.progress <= 0f) { + tracker.onBackCycle = false; + tracker.progress = Math.abs(tracker.progress); + } + } else { + tracker.progress += v; + if (tracker.progress >= 1f) { + if (cycleBack) { + tracker.onBackCycle = true; + tracker.progress = 2f - tracker.progress; + } else { + tracker.progress %= 1.f; + } + } + } + return tracker.lastColor; } } - public record AnimatedDye(@SerialEntry int color1, @SerialEntry int color2, @SerialEntry int samples, @SerialEntry boolean cycleBack, @SerialEntry int tickDelay) { - private int interpolate(AnimatedDyeStateTracker stateTracker) { + public record AnimatedDyeOld(@SerialEntry int color1, @SerialEntry int color2, @SerialEntry int samples, @SerialEntry boolean cycleBack, @SerialEntry int tickDelay) { + + private int interpolate(AnimatedDyeStateTrackerOld stateTracker) { if (stateTracker.shouldCycleBack(samples, cycleBack)) stateTracker.onBackCycle = true; if (stateTracker.onBackCycle) { @@ -152,5 +218,6 @@ private int interpolate(AnimatedDyeStateTracker stateTracker) { return interpolatedColor; } + } } diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorDyeColors.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomArmorDyeColors.java similarity index 92% rename from src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorDyeColors.java rename to src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomArmorDyeColors.java index 52b6f1ae56..6483b3a51b 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorDyeColors.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomArmorDyeColors.java @@ -1,4 +1,4 @@ -package de.hysky.skyblocker.skyblock.item; +package de.hysky.skyblocker.skyblock.item.custom; import com.mojang.brigadier.Command; import com.mojang.brigadier.CommandDispatcher; @@ -45,15 +45,13 @@ private static int customizeDyeColor(FabricClientCommandSource source, int color if (color == Integer.MIN_VALUE) { if (customDyeColors.containsKey(itemUuid)) { - customDyeColors.removeInt(itemUuid); - SkyblockerConfigManager.save(); + SkyblockerConfigManager.update(config-> config.general.customDyeColors.removeInt(itemUuid)); source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.customDyeColors.removed"))); } else { source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.customDyeColors.neverHad"))); } } else { - customDyeColors.put(itemUuid, color); - SkyblockerConfigManager.save(); + SkyblockerConfigManager.update(config-> config.general.customDyeColors.put(itemUuid, color)); source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.customDyeColors.added"))); } } else { diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorTrims.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomArmorTrims.java similarity index 90% rename from src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorTrims.java rename to src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomArmorTrims.java index f8dfb7bddb..85daf0bfbe 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomArmorTrims.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomArmorTrims.java @@ -1,4 +1,4 @@ -package de.hysky.skyblocker.skyblock.item; +package de.hysky.skyblocker.skyblock.item.custom; import com.mojang.brigadier.Command; import com.mojang.brigadier.CommandDispatcher; @@ -18,7 +18,6 @@ import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource; -import net.fabricmc.loader.api.FabricLoader; import net.minecraft.client.MinecraftClient; import net.minecraft.command.CommandRegistryAccess; import net.minecraft.command.CommandSource; @@ -49,13 +48,12 @@ public static void init() { private static void initializeTrimCache() { MinecraftClient client = MinecraftClient.getInstance(); - FabricLoader loader = FabricLoader.getInstance(); if (trimsInitialized || (client == null && !Debug.debugEnabled())) { return; } try { TRIMS_CACHE.clear(); - RegistryWrapper.WrapperLookup wrapperLookup = getWrapperLookup(loader, client); + RegistryWrapper.WrapperLookup wrapperLookup = Utils.getWrapperLookup(); for (Reference material : wrapperLookup.getOrThrow(RegistryKeys.TRIM_MATERIAL).streamEntries().toList()) { for (Reference pattern : wrapperLookup.getOrThrow(RegistryKeys.TRIM_PATTERN).streamEntries().toList()) { ArmorTrim trim = new ArmorTrim(material, pattern); @@ -71,10 +69,6 @@ private static void initializeTrimCache() { } } - private static RegistryWrapper.WrapperLookup getWrapperLookup(FabricLoader loader, MinecraftClient client) { - return client != null && client.getNetworkHandler() != null && client.getNetworkHandler().getRegistryManager() != null ? client.getNetworkHandler().getRegistryManager() : BuiltinRegistries.createWrapperLookup(); - } - private static void registerCommand(CommandDispatcher dispatcher, CommandRegistryAccess registryAccess) { dispatcher.register(ClientCommandManager.literal("skyblocker") .then(ClientCommandManager.literal("custom") @@ -106,8 +100,7 @@ private static int customizeTrim(FabricClientCommandSource source, Identifier ma if (material == null && pattern == null) { if (customArmorTrims.containsKey(itemUuid)) { - customArmorTrims.remove(itemUuid); - SkyblockerConfigManager.save(); + SkyblockerConfigManager.update(config -> config.general.customArmorTrims.remove(itemUuid)); source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.customArmorTrims.removed"))); } else { source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.customArmorTrims.neverHad"))); @@ -121,8 +114,7 @@ private static int customizeTrim(FabricClientCommandSource source, Identifier ma return Command.SINGLE_SUCCESS; } - customArmorTrims.put(itemUuid, trimId); - SkyblockerConfigManager.save(); + SkyblockerConfigManager.update(config -> config.general.customArmorTrims.put(itemUuid, trimId)); source.sendFeedback(Constants.PREFIX.get().append(Text.translatable("skyblocker.customArmorTrims.added"))); } } else { diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomItemNames.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomItemNames.java similarity index 98% rename from src/main/java/de/hysky/skyblocker/skyblock/item/CustomItemNames.java rename to src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomItemNames.java index a06307e991..23ff68e547 100644 --- a/src/main/java/de/hysky/skyblocker/skyblock/item/CustomItemNames.java +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/CustomItemNames.java @@ -1,4 +1,4 @@ -package de.hysky.skyblocker.skyblock.item; +package de.hysky.skyblocker.skyblock.item.custom; import com.mojang.brigadier.Command; import com.mojang.brigadier.CommandDispatcher; diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/AnimatedDyeTimelineWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/AnimatedDyeTimelineWidget.java new file mode 100644 index 0000000000..6172350a01 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/AnimatedDyeTimelineWidget.java @@ -0,0 +1,254 @@ +package de.hysky.skyblocker.skyblock.item.custom.screen; + +import com.google.common.collect.ImmutableList; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.item.custom.CustomArmorAnimatedDyes; +import de.hysky.skyblocker.utils.OkLabColor; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.gui.widget.ContainerWidget; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.texture.NativeImage; +import net.minecraft.client.texture.NativeImageBackedTexture; +import net.minecraft.text.Text; +import net.minecraft.util.Colors; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFW; + +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class AnimatedDyeTimelineWidget extends ContainerWidget implements Closeable { + + private static final Identifier GRADIENT_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "generated/dye_gradient"); + + private static final int HORIZONTAL_MARGIN = 3; + private static final int VERTICAL_MARGIN = 1; + + private final NativeImageBackedTexture gradientTexture; + private final int textureWidth; + private final int textureHeight; + private final FrameCallback frameCallback; + + private String uuid = ""; + + private final ArrayList frames = new ArrayList<>(); + private @Nullable FrameThing focusedFrame = null; + + public AnimatedDyeTimelineWidget(int x, int y, int width, int height, FrameCallback frameCallback) { + super(x, y, width, height, Text.literal("Animated Dye Timeline")); + gradientTexture = new NativeImageBackedTexture("TimelineGradient", width - HORIZONTAL_MARGIN * 2, height - VERTICAL_MARGIN * 2, true); + assert gradientTexture.getImage() != null; + textureWidth = gradientTexture.getImage().getWidth(); + textureHeight = gradientTexture.getImage().getHeight(); + MinecraftClient.getInstance().getTextureManager().registerTexture(GRADIENT_TEXTURE, gradientTexture); + this.frameCallback = frameCallback; + } + + @Override + public List children() { + return frames; + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + context.drawTexture(RenderLayer::getGuiTextured, + GRADIENT_TEXTURE, + getX() + HORIZONTAL_MARGIN, + getY() + VERTICAL_MARGIN, + 0, 0, + getWidth() - HORIZONTAL_MARGIN * 2, + getHeight() - VERTICAL_MARGIN * 2, + textureWidth, textureHeight, + textureWidth, textureHeight + ); + for (FrameThing frame : frames) { + frame.render(context, mouseX, mouseY, delta); + } + } + + @Override + public void setFocused(@Nullable Element focused) { + super.setFocused(focused); + if (focused instanceof FrameThing frameThing) { + frameCallback.onFrameSelected(frameThing.color, frameThing.time); + focusedFrame = frameThing; + } + } + + public void setAnimatedDye(String uuid) { + this.uuid = uuid; + CustomArmorAnimatedDyes.AnimatedDye dye = SkyblockerConfigManager.get().general.customAnimatedDyes.get(uuid); + frames.clear(); + frames.ensureCapacity(dye.frames().size()); + for (int i = 0; i < dye.frames().size(); i++) { + CustomArmorAnimatedDyes.DyeFrame dyeFrame = dye.frames().get(i); + frames.add(new FrameThing(dyeFrame.color(), dyeFrame.time(), i != 0 && i != dye.frames().size() - 1)); + } + setFocused(frames.getFirst()); + createGradientTexture(); + } + + private void createGradientTexture() { + NativeImage image = gradientTexture.getImage(); + assert image != null; + long l = System.currentTimeMillis(); + for (int i = 0; i < frames.size() - 1; i++) { + FrameThing frame = frames.get(i); + FrameThing nextFrame = frames.get(i + 1); + int startX = (int) ((image.getWidth() - 1) * frame.time); + int endX = (int) ((image.getWidth() - 1) * nextFrame.time); + int size = endX - startX; + for (int x = 0; x <= size; x++) { + int color = OkLabColor.interpolate(frame.color, nextFrame.color, (float) x / size); + for (int y = 0; y < image.getHeight(); y++) { + image.setColorArgb(x + startX, y, color | 0xFF_00_00_00); + } + } + } + double v = (System.currentTimeMillis() - l) / 1000.d; + CustomizeArmorScreen.LOGGER.debug("Time taken to generate gradient texture: {}s", v); + gradientTexture.upload(); + } + + private int deletedIndex = -1; + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + boolean b = super.mouseClicked(mouseX, mouseY, button); + if (b) { + if (deletedIndex != -1) { + setFocused(frames.get(deletedIndex)); + deletedIndex = -1; + } + return true; + } + if (isMouseOver(mouseX, mouseY)) { + mouseX -= getX() + HORIZONTAL_MARGIN; + FrameThing e = new FrameThing(0xFFFF0000, (float) (mouseX / (getWidth() - HORIZONTAL_MARGIN * 2 - 1)), true); + frames.add(e); + setFocused(e); + dataChanged(); + return true; + } + return false; + } + + public void setColor(int argb) { + if (focusedFrame == null) { + CustomizeArmorScreen.LOGGER.warn("tried to set color when no frame was focused"); + return; + } + focusedFrame.color = argb; + dataChanged(); + } + + private void dataChanged() { + frames.sort(Comparator.comparingDouble(f -> f.time)); + createGradientTexture(); + List configFrames = ImmutableList.copyOf(frames.stream().map(frameThing -> new CustomArmorAnimatedDyes.DyeFrame(frameThing.color, frameThing.time)).toList()); + CustomArmorAnimatedDyes.AnimatedDye dye = SkyblockerConfigManager.get().general.customAnimatedDyes.get(uuid); + CustomArmorAnimatedDyes.AnimatedDye newDye = new CustomArmorAnimatedDyes.AnimatedDye( + configFrames, + dye.cycleBack(), + dye.delay(), + dye.duration() + ); + SkyblockerConfigManager.get().general.customAnimatedDyes.put(uuid, newDye); + } + + private class FrameThing extends ClickableWidget { + + int color; + float time; + + private final boolean draggable; + + public FrameThing(int color, float time, boolean draggable) { + super(0, AnimatedDyeTimelineWidget.this.getY(), 7, AnimatedDyeTimelineWidget.this.getHeight(), Text.literal("Keyframe")); + this.draggable = draggable; + this.color = color; + this.time = time; + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + context.fill(getX(), getY(), getX() + getWidth(), getY() + getHeight(), color); + context.drawBorder(getX(), getY(), getWidth(), getHeight(), isFocused() ? -1 : Colors.GRAY); + } + + @Override + public int getX() { + AnimatedDyeTimelineWidget parent = AnimatedDyeTimelineWidget.this; + return (int) (parent.getX() + HORIZONTAL_MARGIN + time * (parent.getWidth() - HORIZONTAL_MARGIN * 2 - 1)) - 3; + } + + private boolean dragging = false; + @Override + protected void onDrag(double mouseX, double mouseY, double deltaX, double deltaY) { + super.onDrag(mouseX, mouseY, deltaX, deltaY); + if (!draggable) { + return; + } + AnimatedDyeTimelineWidget parent = AnimatedDyeTimelineWidget.this; + mouseX -= parent.getX() + HORIZONTAL_MARGIN; + float v = (float) (mouseX / (parent.getWidth() - HORIZONTAL_MARGIN * 2 - 1)); + time = Math.clamp(v, 0, 1); + dragging = true; + } + + @Override + public void onRelease(double mouseX, double mouseY) { + super.onRelease(mouseX, mouseY); + if (dragging) dataChanged(); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == GLFW.GLFW_KEY_DELETE) { + deleteThis(false); + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (button == GLFW.GLFW_MOUSE_BUTTON_RIGHT && isMouseOver(mouseX, mouseY)) { + deleteThis(true); + return true; + } + return super.mouseClicked(mouseX, mouseY, button); + } + + private void deleteThis(boolean mouse) { + if (!draggable) return; + int i = frames.indexOf(this); + AnimatedDyeTimelineWidget.this.setFocused(frames.get(i + 1)); + if (mouse) deletedIndex = i; + frames.remove(this); + dataChanged(); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) {} + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) {} + @Override + protected int getContentsHeightWithPadding() {return getHeight();} + @Override + protected double getDeltaYPerScroll() {return 0;} + @Override + public void close() {gradientTexture.close();} + + public interface FrameCallback { + void onFrameSelected(int color, float time); + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/ColorSelectionWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/ColorSelectionWidget.java new file mode 100644 index 0000000000..04d549cd40 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/ColorSelectionWidget.java @@ -0,0 +1,357 @@ +package de.hysky.skyblocker.skyblock.item.custom.screen; + +import com.demonwav.mcdev.annotations.Translatable; +import com.google.common.collect.ImmutableList; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.mixins.accessors.CheckboxWidgetAccessor; +import de.hysky.skyblocker.skyblock.item.custom.CustomArmorAnimatedDyes; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.render.gui.ColorPickerWidget; +import de.hysky.skyblocker.utils.render.gui.RGBTextInput; +import it.unimi.dsi.fastutil.floats.FloatConsumer; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.tooltip.Tooltip; +import net.minecraft.client.gui.widget.*; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.tag.ItemTags; +import net.minecraft.text.Text; +import net.minecraft.util.Colors; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; + +import java.io.Closeable; +import java.text.NumberFormat; +import java.util.List; +import java.util.Locale; + +public class ColorSelectionWidget extends ContainerWidget implements Closeable { + + private static final Identifier INNER_SPACE_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "menu_inner_space"); + private static final Text ADD_COLOR_TEXT = Text.translatable("skyblocker.armorCustomization.addCustomColor"); + private static final Text REMOVE_COLOR_TEXT = Text.translatable("skyblocker.armorCustomization.removeCustomColor"); + private static final Text CANNOT_CUSTOMIZE_COLOR_TEXT = Text.translatable("skyblocker.armorCustomization.cannotCustomizeColor"); + private static final Text ANIMATED_TEXT = Text.translatable("skyblocker.armorCustomization.animated"); + private static final Text CYCLE_BACK_TEXT = Text.translatable("skyblocker.armorCustomization.cycleBack"); + private static final Text DURATION_TOOLTIP_TEXT = Text.translatable("skyblocker.armorCustomization.durationTooltip"); + private static final Text DELAY_TOOLTIP_TEXT = Text.translatable("skyblocker.armorCustomization.delayTooltip"); + private static final String DURATION_TEXT = "skyblocker.armorCustomization.duration"; + private static final String DELAY_TEXT = "skyblocker.armorCustomization.delay"; + + private final ColorPickerWidget colorPicker; + private final RGBTextInput rgbTextInput; + + private final AnimatedDyeTimelineWidget timelineWidget; + private final CheckboxWidget cycleBackCheckbox; + private final Slider delaySlider; + private final Slider durationSlider; + + private final ButtonWidget addCustomColorButton; + private final ButtonWidget removeCustomColorButton; + private final CheckboxWidget animatedCheckbox; + private final TextWidget notCustomizableText; + + private ItemStack currentItem; + private boolean animated; + private State state = State.CANNOT_CUSTOMIZE; + + private final List children; + + public ColorSelectionWidget(int x, int y, int width, int height, TextRenderer textRenderer) { + super(x, y, width, height, Text.of("ColorSelectionWidget")); + int height1 = Math.min(2 * height / 3, width / 6); + + colorPicker = new ColorPickerWidget(x + 3, y + 3, height1 * 2, height1); + colorPicker.setOnColorChange(this::onPickerColorChanged); + rgbTextInput = new RGBTextInput(0, y + 3, textRenderer, true); + rgbTextInput.setX(colorPicker.getRight() + 5); + rgbTextInput.setOnChange(this::onTextInputColorChanged); + timelineWidget = new AnimatedDyeTimelineWidget(getX() + 5, getBottom() - 20, getWidth() - 10, 15, this::onTimelineFrameSelected); + + addCustomColorButton = ButtonWidget.builder(ADD_COLOR_TEXT, this::onAddCustomColor).build(); + SimplePositioningWidget.setPos(addCustomColorButton, getX(), getY(), getWidth(), getHeight()); + removeCustomColorButton = ButtonWidget.builder(REMOVE_COLOR_TEXT, this::onRemoveCustomColor).width(Math.min(150, x + width - rgbTextInput.getRight() - 5)).build(); + + removeCustomColorButton.setPosition(getRight() - removeCustomColorButton.getWidth() - 3, getY() + 3); + + notCustomizableText = new TextWidget(CANNOT_CUSTOMIZE_COLOR_TEXT, textRenderer); + SimplePositioningWidget.setPos(notCustomizableText, getX(), getY(), getWidth(), getHeight()); + + int x1 = removeCustomColorButton.getX(); + int width1 = removeCustomColorButton.getWidth(); + animatedCheckbox = CheckboxWidget.builder(ANIMATED_TEXT, textRenderer) + .pos(x1, removeCustomColorButton.getBottom() + 3) + .maxWidth(width1 / 2) + .callback(this::onAnimatedCheckbox) + .build(); + cycleBackCheckbox = CheckboxWidget.builder(CYCLE_BACK_TEXT, textRenderer) + .pos(animatedCheckbox.getRight() + 1, animatedCheckbox.getY()) + .maxWidth(width1 / 2) + .callback(this::onCycleBackCheckbox) + .build(); + + delaySlider = new Slider(x1, animatedCheckbox.getBottom() + 1, width1, 0.0f, 2.0f, f -> { + String itemUuid = ItemUtils.getItemUuid(currentItem); + CustomArmorAnimatedDyes.AnimatedDye dye = SkyblockerConfigManager.get().general.customAnimatedDyes.get(itemUuid); + CustomArmorAnimatedDyes.AnimatedDye newDye = new CustomArmorAnimatedDyes.AnimatedDye( + dye.frames(), + dye.cycleBack(), + f, + dye.duration() + ); + SkyblockerConfigManager.get().general.customAnimatedDyes.put(itemUuid, newDye); + }, DELAY_TEXT, true); + delaySlider.setTooltip(Tooltip.of(DELAY_TOOLTIP_TEXT)); + + durationSlider = new Slider(x1, delaySlider.getBottom() + 1, width1, 0.1f, 10.0f, f -> { + String itemUuid = ItemUtils.getItemUuid(currentItem); + CustomArmorAnimatedDyes.AnimatedDye dye = SkyblockerConfigManager.get().general.customAnimatedDyes.get(itemUuid); + CustomArmorAnimatedDyes.AnimatedDye newDye = new CustomArmorAnimatedDyes.AnimatedDye( + dye.frames(), + dye.cycleBack(), + dye.delay(), + f + ); + SkyblockerConfigManager.get().general.customAnimatedDyes.put(itemUuid, newDye); + }, DURATION_TEXT, true); + durationSlider.setTooltip(Tooltip.of(DURATION_TOOLTIP_TEXT)); + + + children = ImmutableList.builder().add(colorPicker, rgbTextInput, timelineWidget, addCustomColorButton, removeCustomColorButton, animatedCheckbox, notCustomizableText, cycleBackCheckbox, delaySlider, durationSlider).build(); + } + + private void onPickerColorChanged(int argb, boolean release) { + rgbTextInput.setRGBColor(argb); + if (release) timelineWidget.setColor(argb); + } + + private void onTextInputColorChanged(int argb) { + colorPicker.setRGBColor(argb); + timelineWidget.setColor(argb); + } + + private void onAddCustomColor(ButtonWidget button) { + state = State.CUSTOMIZED; + animated = false; + changeVisibilities(); + SkyblockerConfigManager.get().general.customDyeColors.put(ItemUtils.getItemUuid(currentItem), -1); + rgbTextInput.setRGBColor(-1); + colorPicker.setRGBColor(-1); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { + return hoveredElement(mouseX, mouseY).filter(element -> element.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)).isPresent() || super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount); + } + + private void onTimelineFrameSelected(int color, float time) { + rgbTextInput.setRGBColor(color); + colorPicker.setRGBColor(color); + } + + private void onRemoveCustomColor(ButtonWidget button) { + state = State.CUSTOMIZABLE; + animated = false; + changeVisibilities(); + String itemUuid = ItemUtils.getItemUuid(currentItem); + SkyblockerConfigManager.get().general.customDyeColors.removeInt(itemUuid); + SkyblockerConfigManager.get().general.customAnimatedDyes.remove(itemUuid); + } + + private void onAnimatedCheckbox(CheckboxWidget checkbox, boolean checked) { + animated = checked; + changeVisibilities(); + String itemUuid = ItemUtils.getItemUuid(currentItem); + if (animated) { + SkyblockerConfigManager.get().general.customAnimatedDyes.put(itemUuid, new CustomArmorAnimatedDyes.AnimatedDye( + List.of(new CustomArmorAnimatedDyes.DyeFrame(Colors.RED, 0), new CustomArmorAnimatedDyes.DyeFrame(Colors.BLUE, 1)), + true, + 0, + 1.f + )); + timelineWidget.setAnimatedDye(itemUuid); + delaySlider.setValue(0); + durationSlider.setValue(1); + } else { + SkyblockerConfigManager.get().general.customAnimatedDyes.remove(itemUuid); + } + } + + private void onCycleBackCheckbox(CheckboxWidget checkbox, boolean checked) { + String itemUuid = ItemUtils.getItemUuid(currentItem); + CustomArmorAnimatedDyes.AnimatedDye dye = SkyblockerConfigManager.get().general.customAnimatedDyes.get(itemUuid); + CustomArmorAnimatedDyes.AnimatedDye newDye = new CustomArmorAnimatedDyes.AnimatedDye( + dye.frames(), + checked, + dye.delay(), + dye.duration() + ); + SkyblockerConfigManager.get().general.customAnimatedDyes.put(itemUuid, newDye); + } + + private void changeVisibilities() { + colorPicker.visible = state == State.CUSTOMIZED; + rgbTextInput.visible = state == State.CUSTOMIZED; + + timelineWidget.visible = state == State.CUSTOMIZED && animated; + cycleBackCheckbox.visible = state == State.CUSTOMIZED && animated; + delaySlider.visible = state == State.CUSTOMIZED && animated; + durationSlider.visible = state == State.CUSTOMIZED && animated; + + addCustomColorButton.visible = state == State.CUSTOMIZABLE; + removeCustomColorButton.visible = state == State.CUSTOMIZED; + animatedCheckbox.visible = state == State.CUSTOMIZED; + notCustomizableText.visible = state == State.CANNOT_CUSTOMIZE; + } + + @Override + public List children() { + return children; + } + + @Override + protected int getContentsHeightWithPadding() { + return 0; + } + + @Override + protected double getDeltaYPerScroll() { + return 0; + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + context.drawGuiTexture(RenderLayer::getGuiTextured, INNER_SPACE_TEXTURE, getX(), getY(), getWidth(), getHeight()); + for (ClickableWidget child : children) { + child.render(context, mouseX, mouseY, delta); + } + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (!super.mouseClicked(mouseX, mouseY, button)) { + setFocused(null); + return false; + } + return true; + } + + @Override + public void close() { + timelineWidget.close(); + } + + public void setCurrentItem(ItemStack currentItem) { + this.currentItem = currentItem; + String itemUuid = ItemUtils.getItemUuid(currentItem); + if (!currentItem.isIn(ItemTags.DYEABLE)) { + state = State.CANNOT_CUSTOMIZE; + animated = false; + } else if (SkyblockerConfigManager.get().general.customAnimatedDyes.containsKey(itemUuid)) { + state = State.CUSTOMIZED; + animated = true; + CustomArmorAnimatedDyes.AnimatedDye animatedDye = SkyblockerConfigManager.get().general.customAnimatedDyes.get(itemUuid); + ((CheckboxWidgetAccessor) cycleBackCheckbox).setChecked(animatedDye.cycleBack()); + delaySlider.setValue(animatedDye.delay()); + durationSlider.setValue(animatedDye.duration()); + } else if (SkyblockerConfigManager.get().general.customDyeColors.containsKey(itemUuid)) { + state = State.CUSTOMIZED; + animated = false; + } else { + state = State.CUSTOMIZABLE; + animated = false; + } + changeVisibilities(); + ((CheckboxWidgetAccessor) animatedCheckbox).setChecked(animated); + if (animated) timelineWidget.setAnimatedDye(itemUuid); + } + + private enum State { + CANNOT_CUSTOMIZE, + CUSTOMIZABLE, + CUSTOMIZED + } + + private static class Slider extends SliderWidget { + private static final NumberFormat FORMATTER = Util.make(NumberFormat.getInstance(Locale.US), nf -> nf.setMaximumFractionDigits(3)); + + private final FloatConsumer onValueChanged; + private final float minValue; + private final float maxValue; + private final String translatable; + private final boolean linear; + + private boolean clicked = false; + + public Slider(int x, int y, int width, float min, float max, FloatConsumer onValueChanged, @Translatable String translatable, boolean linear) { + super(x, y, width, 15, Text.empty(), 0); + this.onValueChanged = onValueChanged; + this.minValue = min; + this.maxValue = max; + this.translatable = translatable; + this.linear = linear; // old code stuff... is always true, keeping it, can maybe be useful + updateMessage(); + } + + private float trueValue() { + double v = linear ? value : value*value; + return (float) (minValue + v * (maxValue - minValue)); + } + + @Override + protected void updateMessage() { + setMessage(Text.translatable(translatable, FORMATTER.format(trueValue()))); + } + + private void setValue(float val) { + float v = (val - minValue) / (maxValue - minValue); + value = linear ? v : Math.sqrt(v); + updateMessage(); + } + + @Override + public void onClick(double mouseX, double mouseY) { + super.onClick(mouseX, mouseY); + clicked = true; + } + + @Override + public void onRelease(double mouseX, double mouseY) { + super.onRelease(mouseX, mouseY); + if (clicked) { + onValueChanged.accept(trueValue()); + clicked = false; + } + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (super.keyPressed(keyCode, scanCode, modifiers)) { + onValueChanged.accept(trueValue()); + return true; + } + return false; + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) {if (verticalAmount == 0) return false; + float offset = verticalAmount > 0 ? 0.001f : -0.001f; + setValue(Math.clamp(trueValue() + offset, minValue, maxValue)); + onValueChanged.accept(trueValue()); + return true; + } + + // Not using this cuz it updates every drag + @Override + protected void applyValue() {} + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/CustomizeArmorScreen.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/CustomizeArmorScreen.java new file mode 100644 index 0000000000..f13f21caca --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/CustomizeArmorScreen.java @@ -0,0 +1,282 @@ +package de.hysky.skyblocker.skyblock.item.custom.screen; + +import com.google.common.collect.ImmutableMap; +import com.mojang.logging.LogUtils; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.annotations.Init; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.mixins.accessors.HandledScreenAccessor; +import de.hysky.skyblocker.skyblock.item.custom.CustomArmorAnimatedDyes; +import de.hysky.skyblocker.skyblock.item.custom.CustomArmorTrims; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.Utils; +import de.hysky.skyblocker.utils.scheduler.Scheduler; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandManager; +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.ingame.InventoryScreen; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ButtonWidget; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.network.OtherClientPlayerEntity; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.entity.EquipmentSlot; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.registry.tag.ItemTags; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.slf4j.Logger; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; + +public class CustomizeArmorScreen extends Screen { + static final Logger LOGGER = LogUtils.getLogger(); + private static final EquipmentSlot[] ARMOR_SLOTS = EquipmentSlot.VALUES.stream().filter(slot -> slot.getType() == EquipmentSlot.Type.HUMANOID_ARMOR).toArray(EquipmentSlot[]::new); + + private static final ItemStack BARRIER = new ItemStack(Items.BARRIER); + //private static final ModelData PLAYER_MODEL = PlayerEntityModel.getTexturedModelData(Dilation.NONE, false); + + private final OtherClientPlayerEntity player = new OtherClientPlayerEntity(MinecraftClient.getInstance().world, MinecraftClient.getInstance().getGameProfile()) { + @Override + public boolean isInvisibleTo(PlayerEntity player) { + return true; + } + @Override + public void onEquipStack(EquipmentSlot slot, ItemStack oldStack, ItemStack newStack) {} + }; + + private final ItemStack[] armor = new ItemStack[4]; + private int selectedSlot = 0; + private TrimSelectionWidget trimSelectionWidget; + private ColorSelectionWidget colorSelectionWidget; + + private final Screen previousScreen; + + private final Map previousConfigs; + + + @Init + public static void initThings() { + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> dispatcher.register( + ClientCommandManager.literal("skyblocker").then(ClientCommandManager.literal("custom").executes(context -> { + Scheduler.queueOpenScreen(new CustomizeArmorScreen(null)); + return 1; + })))); + ScreenEvents.AFTER_INIT.register((client1, screen, scaledWidth, scaledHeight) -> { + if (Utils.isOnSkyblock() && screen instanceof InventoryScreen inventoryScreen) { + Screens.getButtons(inventoryScreen).add(new Button( + ((HandledScreenAccessor) inventoryScreen).getX() + 63 , + ((HandledScreenAccessor) inventoryScreen).getY() + 10, + inventoryScreen + )); + } + }); + } + static boolean canEdit(ItemStack stack) { + return stack.isIn(ItemTags.TRIMMABLE_ARMOR) && !ItemUtils.getItemUuid(stack).isEmpty(); + } + + + private final boolean nothingCustomizable; + protected CustomizeArmorScreen(Screen previousScreen) { + super(Text.translatable("skyblocker.armorCustomization.title")); + List list = ItemUtils.getArmor(MinecraftClient.getInstance().player); + for (int i = 0; i < list.size(); i++) { + ItemStack copy = list.get(i).copy(); + armor[3 - i] = copy; + player.equipStack(ARMOR_SLOTS[i], copy); + } + while (selectedSlot < armor.length - 1 && !canEdit(armor[selectedSlot])) selectedSlot++; + this.previousScreen = previousScreen; + nothingCustomizable = !canEdit(armor[selectedSlot]); + + ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(4); + for (ItemStack stack : armor) { + if (canEdit(stack)) { + String uuid = ItemUtils.getItemUuid(stack); + builder.put(uuid, new Stuff( + SkyblockerConfigManager.get().general.customArmorTrims.containsKey(uuid) ? Optional.of(SkyblockerConfigManager.get().general.customArmorTrims.get(uuid)) : Optional.empty(), + SkyblockerConfigManager.get().general.customDyeColors.containsKey(uuid) ? OptionalInt.of(SkyblockerConfigManager.get().general.customDyeColors.getInt(uuid)) : OptionalInt.empty(), + SkyblockerConfigManager.get().general.customAnimatedDyes.containsKey(uuid) ? Optional.of(SkyblockerConfigManager.get().general.customAnimatedDyes.get(uuid)) : Optional.empty() + )); + } + } + previousConfigs = builder.build(); + } + + @Override + public void tick() { + player.age++; + } + + @Override + protected void init() { + super.init(); + int w = Math.min(500, width); + int x = (width - w) / 2; + + int y = (height - 190) / 2; + PlayerWidget playerWidget = new PlayerWidget(x + 5, y, 90, 165, player); + addDrawableChild(playerWidget); + PieceSelectionWidget pieceSelectionWidget = new PieceSelectionWidget(playerWidget.getX() + 3, playerWidget.getBottom() + 1); + addDrawableChild(pieceSelectionWidget); + + + + if (!nothingCustomizable) { + trimSelectionWidget = new TrimSelectionWidget(x + 105, y, w - 105 - 5, 80); + addDrawableChild(trimSelectionWidget); + trimSelectionWidget.setCurrentItem(armor[selectedSlot]); + + if (colorSelectionWidget != null) colorSelectionWidget.close(); + colorSelectionWidget = new ColorSelectionWidget(trimSelectionWidget.getX(), trimSelectionWidget.getBottom() + 10, trimSelectionWidget.getWidth(), 100, textRenderer); + addDrawableChild(colorSelectionWidget); + colorSelectionWidget.setCurrentItem(armor[selectedSlot]); + } + + addDrawableChild(ButtonWidget.builder(Text.translatable("gui.cancel"), b -> cancel()).position(width / 2 - 155, height - 25).build()); + addDrawableChild(ButtonWidget.builder(Text.translatable("gui.done"), b -> close()).position(width / 2 + 5, height - 25).build()); + } + + private void cancel() { + previousConfigs.forEach((uuid, stuff) -> { + stuff.armorTrimId().ifPresentOrElse( + trim -> SkyblockerConfigManager.get().general.customArmorTrims.put(uuid, trim), + () -> SkyblockerConfigManager.get().general.customArmorTrims.remove(uuid) + ); + stuff.color().ifPresentOrElse( + i -> SkyblockerConfigManager.get().general.customDyeColors.put(uuid, i), + () -> SkyblockerConfigManager.get().general.customDyeColors.removeInt(uuid) + ); + stuff.animatedDye().ifPresentOrElse( + animatedDye -> SkyblockerConfigManager.get().general.customAnimatedDyes.put(uuid, animatedDye), + () -> SkyblockerConfigManager.get().general.customAnimatedDyes.remove(uuid) + ); + }); + close(); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + super.render(context, mouseX, mouseY, delta); + context.drawCenteredTextWithShadow(textRenderer, getTitle(), this.width / 2, 5, -1); + } + + @Override + public boolean shouldPause() { + return false; + } + + @Override + public void removed() { + super.removed(); + SkyblockerConfigManager.update(config -> {}); + if (colorSelectionWidget != null) colorSelectionWidget.close(); + // clear all the trackers cuz the color selection maybe created a bunch. + CustomArmorAnimatedDyes.cleanTrackers(); + } + + @Override + public void close() { + client.setScreen(previousScreen); + } + + private class PieceSelectionWidget extends ClickableWidget { + + private static final Identifier HOTBAR_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "armor_customization_screen/mini_hotbar"); + private static final Identifier HOTBAR_SELECTION_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "hotbar_selection_full"); + + private final boolean[] selectable; + + public PieceSelectionWidget(int x, int y) { + super(x, y, 84, 24, Text.of("")); + selectable = new boolean[armor.length]; + for (int i = 0; i < armor.length; i++) { + selectable[i] = canEdit(armor[i]); + } + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + context.drawGuiTexture(RenderLayer::getGuiTextured, HOTBAR_TEXTURE, getX() + 1, getY() + 1, 82, 22); + + int hoveredSlot = -1; + int localX = mouseX - getX() - 2; + int localY = mouseY - getY() - 2; + if (localY >= 0 && localY < 20) { + hoveredSlot = localX / 20 >= armor.length ? -1 : localX / 20; + } + + if (hoveredSlot >= 0 && selectable[hoveredSlot]) { + int i = getX() + 2 + hoveredSlot * 20; + context.fill(i, getY() + 2, i + 20, getY() + 22, 0x20_FF_FF_FF); + } + + for (int i = 0; i < armor.length; i++) { + context.drawItem(armor[i], getX() + 4 + i * 20, getY() + 4); + if (!selectable[i]) { + context.drawItem(BARRIER, getX() + 4 + i * 20, getY() + 4); + } + } + context.drawGuiTexture(RenderLayer::getGuiTextured, HOTBAR_SELECTION_TEXTURE, getX() + selectedSlot * 20, getY(), 24, 24); + } + + @Override + public void onClick(double mouseX, double mouseY) { + double localX = mouseX - getX() - 2; + double localY = mouseY - getY() - 2; + if (localY < 0 || localY >= 20) return; + int i = (int) (localX / 20); + if (i < 0 || i >= armor.length || !selectable[i]) return; + if (i != selectedSlot) { + selectedSlot = i; + trimSelectionWidget.setCurrentItem(armor[selectedSlot]); + colorSelectionWidget.setCurrentItem(armor[selectedSlot]); + } + } + + @Override + public boolean isMouseOver(double mouseX, double mouseY) { + return this.active && this.visible && mouseX >= this.getX() + 2 && mouseY >= this.getY() + 2 && mouseX < this.getRight() - 2 && mouseY < this.getBottom() - 2; + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) {} + } + + private record Stuff(Optional armorTrimId, OptionalInt color, Optional animatedDye) {} + + private static class Button extends ClickableWidget { + + // thanks to @yuflow + private static final Identifier TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "armor_customization_screen/button"); + + private final Screen prevScreen; + public Button(int x, int y, Screen screen) { + super(x, y, 10, 10, Text.empty()); + prevScreen = screen; + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + context.drawGuiTexture(RenderLayer::getGuiTextured, TEXTURE, getX(), getY(), getWidth(), getHeight(), isHovered() ? 0xFFfafa96 : 0x80FFFFFF); + } + + @Override + public void onClick(double mouseX, double mouseY) { + MinecraftClient.getInstance().setScreen(new CustomizeArmorScreen(prevScreen)); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) {} + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/MaterialPlateTextures.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/MaterialPlateTextures.java new file mode 100644 index 0000000000..23671200fd --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/MaterialPlateTextures.java @@ -0,0 +1,207 @@ +package de.hysky.skyblocker.skyblock.item.custom.screen; + +import com.mojang.logging.LogUtils; +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.annotations.Init; +import de.hysky.skyblocker.events.SkyblockEvents; +import de.hysky.skyblocker.mixins.accessors.SpriteContentsAccessor; +import de.hysky.skyblocker.utils.Utils; +import it.unimi.dsi.fastutil.ints.Int2IntMap; +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents; +import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener; +import net.fabricmc.fabric.api.resource.ResourceManagerHelper; +import net.fabricmc.fabric.api.resource.ResourceReloadListenerKeys; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.texture.NativeImage; +import net.minecraft.client.texture.NativeImageBackedTexture; +import net.minecraft.client.texture.TextureManager; +import net.minecraft.item.equipment.trim.ArmorTrimMaterial; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.registry.RegistryWrapper; +import net.minecraft.resource.Resource; +import net.minecraft.resource.ResourceFinder; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourceType; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.ColorHelper; +import org.slf4j.Logger; + +import java.io.IOException; +import java.io.InputStream; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +public class MaterialPlateTextures { + private static final Identifier RELOAD_LISTENER_ID = Identifier.of(SkyblockerMod.NAMESPACE, "material_plates"); + private static final ResourceFinder RESOURCE_FINDER = new ResourceFinder("textures", ".png"); + + private static final Identifier MATERIAL_PLATE_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "armor_customization_screen/material_plate"); + private static final Identifier BASE_PALETTE_TEXTURE = Identifier.ofVanilla("trims/color_palettes/trim_palette"); + public static final Identifier TEXTURE_PREFIX = Identifier.of(SkyblockerMod.NAMESPACE, "generated/material_plate_"); + + private static final Logger LOGGER = LogUtils.getLogger(); + + private static CompletableFuture texturesFuture = null; + private static boolean errored = false; + + private static final Map SUFFIX_TO_TEXTURE = new Object2ObjectOpenHashMap<>(); + + @Init + public static void init() { + // Reset the generated textures when the resource pack is reloaded + ResourceManagerHelper.get(ResourceType.CLIENT_RESOURCES).registerReloadListener(new Listener()); + + SkyblockEvents.LEAVE.register(MaterialPlateTextures::closeTextures); + ClientLifecycleEvents.CLIENT_STOPPING.register(client -> closeTextures()); // close textures on client stop in case the previous event doesn't get ran for some reason + + } + + private static int[] openPalette(ArmorTrimMaterial material, String namespace) { + return openPalette(Identifier.of(namespace,"trims/color_palettes/" + material.assets().base().suffix())); + } + + /** + * Opens a palette + * @param texture the identifier of the palette + * @return an array of ints representing each color + */ + private static int[] openPalette(Identifier texture) { + Optional resource = MinecraftClient.getInstance().getResourceManager().getResource( + RESOURCE_FINDER.toResourcePath(texture) + ); + if (resource.isEmpty()) throw new IllegalArgumentException("Can't find texture: " + texture); + int[] pixels = null; + try (InputStream stream = resource.get().getInputStream()) { + try (NativeImage palette = NativeImage.read(stream)) { + pixels = palette.copyPixelsArgb(); + } catch (IOException e) { + LOGGER.error("Failed to load color palette", e); + } + } catch (IOException e) { + LOGGER.error("Failed to read color palette {}", texture, e); + } + return pixels; + } + + /** + * @return A map of all created textures with their suffix as a key + * @implNote Taken from {@link net.minecraft.client.texture.atlas.PalettedPermutationsAtlasSource} + */ + private static Map createTextures() { + Map map = new HashMap<>(); + int[] basePalette = openPalette(BASE_PALETTE_TEXTURE); + if (basePalette == null) { + LOGGER.error("Failed to load the base palette, see error above"); + return map; + } + + NativeImage image = ((SpriteContentsAccessor) MinecraftClient.getInstance().getGuiAtlasManager().getSprite(MATERIAL_PLATE_TEXTURE).getContents()).getImage(); + + RegistryWrapper materials = Utils.getWrapperLookup().getOrThrow(RegistryKeys.TRIM_MATERIAL); + List> futures = new ArrayList<>(); + + materials.streamEntries().forEach(entry -> { + ArmorTrimMaterial material = entry.value(); + String suffix = material.assets().base().suffix(); + int[] materialPalette = openPalette(material, entry.registryKey().getValue().getNamespace()); + if (materialPalette == null) return; + + // Run on main thread or else NativeImageBackedTexture is mad + futures.add(CompletableFuture.runAsync(() -> { + NativeImageBackedTexture texture = new NativeImageBackedTexture("SkyblockerMaterialPlate_" + suffix, image.getWidth(), image.getHeight(), false); + if (texture.getImage() == null) throw new RuntimeException("Failed to create NativeImageBackedTexture"); + + + // create map to convert color to palette + Int2IntMap palette = new Int2IntOpenHashMap(materialPalette.length); + for (int i = 0; i < basePalette.length; i++) { + int color = basePalette[i]; + if (ColorHelper.getAlpha(color) != 0) { + palette.put(ColorHelper.zeroAlpha(color), materialPalette[i]); + } + } + for (int i = 0; i < texture.getImage().getWidth(); i++) { + for (int j = 0; j < texture.getImage().getHeight(); j++) { + int baseColor = image.getColorArgb(i, j); + int alpha = ColorHelper.getAlpha(baseColor); + int color = ColorHelper.withAlpha(alpha, palette.getOrDefault(ColorHelper.zeroAlpha(baseColor), baseColor)); + texture.getImage().setColorArgb( + i, + j, + color + ); + } + } + texture.upload(); + map.put(suffix, texture); + }, MinecraftClient.getInstance())); // Use client executor to run on main thread + + }); + CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)).join(); + return map; + } + + private static void createTexturesAsync() { + errored = false; + texturesFuture = CompletableFuture + .supplyAsync(MaterialPlateTextures::createTextures) + .thenAccept(map -> { + SUFFIX_TO_TEXTURE.putAll(map); + TextureManager textureManager = MinecraftClient.getInstance().getTextureManager(); + for (Map.Entry entry : map.entrySet()) { + textureManager.registerTexture(TEXTURE_PREFIX.withSuffixedPath(entry.getKey()), entry.getValue()); + } + }) + .exceptionally(throwable -> { + errored = true; + LOGGER.error("Failed to create textures", throwable); + return null; + }); + } + + private static void closeTextures() { + if (SUFFIX_TO_TEXTURE.isEmpty()) return; + TextureManager textureManager = MinecraftClient.getInstance().getTextureManager(); + for (Map.Entry entry : SUFFIX_TO_TEXTURE.entrySet()) { + textureManager.destroyTexture(TEXTURE_PREFIX.withSuffixedPath(entry.getKey())); + } + SUFFIX_TO_TEXTURE.clear(); + } + + /** + * @return true if the textures are available, creates the textures in another thread if they aren't created + */ + public static boolean isAvailable() { + if (texturesFuture == null) { + createTexturesAsync(); + return false; + } + return texturesFuture.isDone() && !errored; + } + + private static final class Listener implements IdentifiableResourceReloadListener { + + @Override + public Identifier getFabricId() { + return RELOAD_LISTENER_ID; + } + + @Override + public CompletableFuture reload(Synchronizer synchronizer, ResourceManager manager, Executor prepareExecutor, Executor applyExecutor) { + return CompletableFuture.completedFuture(null) + .thenCompose(synchronizer::whenPrepared) // Tell the reload listener that we finished "preparing" + .thenAcceptAsync(o -> { + texturesFuture = null; + closeTextures(); + }, applyExecutor); + } + + @Override + public Collection getFabricDependencies() { + return Collections.singletonList(ResourceReloadListenerKeys.MODELS); + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/PlayerWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/PlayerWidget.java new file mode 100644 index 0000000000..c55144a11b --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/PlayerWidget.java @@ -0,0 +1,71 @@ +package de.hysky.skyblocker.skyblock.item.custom.screen; + +import de.hysky.skyblocker.SkyblockerMod; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.ingame.InventoryScreen; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.network.AbstractClientPlayerEntity; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.sound.SoundManager; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +public class PlayerWidget extends ClickableWidget { + + private static final Identifier INNER_SPACE_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "menu_inner_space"); + private static final Quaternionf FLIP_ROTATION = new Quaternionf().rotationXYZ(0, 0.0F, (float) Math.PI); + private final AbstractClientPlayerEntity player; + + private float xRotation = -10; + private float yRotation = 225; + + public PlayerWidget(int x, int y, int width, int height, AbstractClientPlayerEntity player) { + super(x, y, width, height, Text.literal("")); + this.player = player; + this.player.headYaw = this.player.lastHeadYaw = 0; + } + + @Override + protected void onDrag(double mouseX, double mouseY, double deltaX, double deltaY) { + super.onDrag(mouseX, mouseY, deltaX, deltaY); + this.xRotation = MathHelper.clamp(this.xRotation - (float)deltaY * 2.5F, -50.0F, 50.0F); + this.yRotation += (float)deltaX * 2.5F; + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + context.drawGuiTexture(RenderLayer::getGuiTextured, INNER_SPACE_TEXTURE, getX(), getY(), getWidth(), getHeight()); + MatrixStack matrices = context.getMatrices(); + matrices.push(); + float size = 64f; + matrices.translate(getX() + getWidth() / 2f, getY() + getHeight() / 2f, size); + Quaternionf quaternion = new Quaternionf().rotationXYZ(xRotation * MathHelper.PI / 180, yRotation * MathHelper.PI / 180, 0); + matrices.multiply(quaternion); + matrices.translate(0, 0, -50); + Vector3f vector3f = new Vector3f(0, player.getHeight() / 2.0F + .0625f, 0); + InventoryScreen.drawEntity( + context, + 0, + 0, + size, + vector3f, + FLIP_ROTATION, + null, + player + ); + matrices.pop(); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + + } + + @Override + public void playDownSound(SoundManager soundManager) {} +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/TrimElementButton.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/TrimElementButton.java new file mode 100644 index 0000000000..60d992b8f1 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/TrimElementButton.java @@ -0,0 +1,228 @@ +package de.hysky.skyblocker.skyblock.item.custom.screen; + +import de.hysky.skyblocker.mixins.accessors.EntityRenderDispatcherAccessor; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.Utils; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.tooltip.Tooltip; +import net.minecraft.client.gui.widget.PressableWidget; +import net.minecraft.client.render.DiffuseLighting; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.render.TexturedRenderLayers; +import net.minecraft.client.render.entity.equipment.EquipmentModel; +import net.minecraft.client.render.entity.equipment.EquipmentRenderer; +import net.minecraft.client.render.entity.model.ArmorEntityModel; +import net.minecraft.client.render.entity.model.EntityModelLayers; +import net.minecraft.client.render.entity.state.BipedEntityRenderState; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.component.DataComponentTypes; +import net.minecraft.component.type.EquippableComponent; +import net.minecraft.component.type.NbtComponent; +import net.minecraft.entity.EquipmentSlot; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.item.equipment.EquipmentAssetKeys; +import net.minecraft.item.equipment.trim.ArmorTrim; +import net.minecraft.item.equipment.trim.ArmorTrimMaterial; +import net.minecraft.item.equipment.trim.ArmorTrimMaterials; +import net.minecraft.item.equipment.trim.ArmorTrimPattern; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.RotationAxis; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; + +public sealed abstract class TrimElementButton extends PressableWidget permits TrimElementButton.Pattern, TrimElementButton.Material { + + private static final ItemStack BARRIER = new ItemStack(Items.BARRIER); + protected final @Nullable Identifier element; + private final Consumer onPress; + + public TrimElementButton(@Nullable Identifier element, Text name, Consumer onPress) { + super(0, 0, 20, 20, name); + this.element = element; + this.onPress = onPress; + setTooltip(Tooltip.of(getMessage())); + } + + public @Nullable Identifier getElement() { + return element; + } + + @Override + public void setMessage(Text message) { + super.setMessage(message); + setTooltip(Tooltip.of(getMessage())); + } + + @Override + public void drawMessage(DrawContext context, TextRenderer textRenderer, int color) { + draw(context); + } + + abstract void draw(DrawContext context); + + public static final class Pattern extends TrimElementButton { + + private static ArmorEntityModel OUTER_MODEL = null; + private static ArmorEntityModel INNER_MODEL = null; + private static EquipmentRenderer EQUIPMENT_RENDERER = null; + + private ItemStack item; + private final ArmorTrim trim; + private EquippableComponent equippableComponent; + + private float rotation = 15; + + public Pattern(@Nullable Identifier element, @Nullable ArmorTrimPattern pattern, Consumer onPress) { + super(element, pattern == null ? Text.translatable("gui.none") : pattern.description(), onPress); + if (element == null) { + trim = null; + return; + } + if (OUTER_MODEL == null) { + OUTER_MODEL = new ArmorEntityModel<>(MinecraftClient.getInstance().getLoadedEntityModels().getModelPart(EntityModelLayers.PLAYER_OUTER_ARMOR)); + INNER_MODEL = new ArmorEntityModel<>(MinecraftClient.getInstance().getLoadedEntityModels().getModelPart(EntityModelLayers.PLAYER_INNER_ARMOR)); + EQUIPMENT_RENDERER = new EquipmentRenderer( + ((EntityRenderDispatcherAccessor) MinecraftClient.getInstance().getEntityRenderDispatcher()).getEquipmentModelLoader(), + MinecraftClient.getInstance().getBlockRenderManager().getModels().getModelManager().getAtlas(TexturedRenderLayers.ARMOR_TRIMS_ATLAS_TEXTURE)); + } + trim = new ArmorTrim( + Utils.getWrapperLookup().getOrThrow(RegistryKeys.TRIM_MATERIAL).getOrThrow(ArmorTrimMaterials.QUARTZ), + RegistryEntry.of(pattern)); + } + + @Override + void draw(DrawContext context) { + if (trim == null) { + context.drawItem(BARRIER, getX() + getWidth() / 2 - 8, getY() + getHeight() / 2 - 8); + return; + } + if (isHovered()) { + rotation += MinecraftClient.getInstance().getRenderTickCounter().getDynamicDeltaTicks() * 0.05f * 90; + rotation %= 360; + } else rotation = 15; + + EquipmentSlot slot = equippableComponent.slot(); + ArmorEntityModel model = slot == EquipmentSlot.LEGS ? INNER_MODEL : OUTER_MODEL; + float offset = setVisibleAndGetOffset(model, slot); + + MatrixStack matrices = context.getMatrices(); + matrices.push(); + matrices.translate(getX() + getWidth() / 2f, getY() + getHeight() / 2f, 200); + matrices.translate(0, offset, 0); + matrices.scale(14, 14, 14); + matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(-5)); + matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(rotation)); + DiffuseLighting.enableGuiShaderLighting(); + context.draw(vertexConsumerProvider -> EQUIPMENT_RENDERER.render( + slot == EquipmentSlot.LEGS ? EquipmentModel.LayerType.HUMANOID_LEGGINGS : EquipmentModel.LayerType.HUMANOID, + equippableComponent.assetId().orElse(EquipmentAssetKeys.IRON), + model, + item, + matrices, + vertexConsumerProvider, + 15 + )); + DiffuseLighting.enableGuiDepthLighting(); + matrices.pop(); + + } + + public void setItem(@NotNull ItemStack newItem) { + this.item = newItem.copy(); + // Remove the uuid so it doesn't render with the selected trim + NbtCompound copy = ItemUtils.getCustomData(item).copy(); + copy.remove(ItemUtils.UUID); + item.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(copy)); + + equippableComponent = this.item.get(DataComponentTypes.EQUIPPABLE); + if (equippableComponent == null) throw new IllegalArgumentException("Trimmed stack must contain an equippable component"); + this.item.set(DataComponentTypes.TRIM, trim); + } + + private static float setVisibleAndGetOffset(ArmorEntityModel bipedModel, EquipmentSlot slot) { + bipedModel.setVisible(false); + switch (slot) { + case HEAD: + bipedModel.head.visible = true; + bipedModel.hat.visible = true; + return 4; + case CHEST: + bipedModel.body.visible = true; + bipedModel.rightArm.visible = true; + bipedModel.leftArm.visible = true; + return -6; + case LEGS: + bipedModel.body.visible = true; + bipedModel.rightLeg.visible = true; + bipedModel.leftLeg.visible = true; + return -14; + case FEET: + bipedModel.rightLeg.visible = true; + bipedModel.leftLeg.visible = true; + return -20; + } + return 0; + } + } + + public static final class Material extends TrimElementButton { + + /** + * Texture generated in {@link MaterialPlateTextures} + */ + private final Identifier texture; + + public Material(Identifier element, ArmorTrimMaterial material, Consumer onPress) { + super(element, material.description(), onPress); + texture = MaterialPlateTextures.TEXTURE_PREFIX.withSuffixedPath(material.assets().base().suffix()); + } + + + + @Override + void draw(DrawContext context) { + int x = getX() + getWidth() / 2 - 8; + int y = getY() + getHeight() / 2 - 8; + if (MaterialPlateTextures.isAvailable()) { + MatrixStack matrices = context.getMatrices(); + matrices.push(); + matrices.translate(0, 0, 10); + context.drawTexture( + RenderLayer::getGuiTextured, + texture, + x, + y, + 0, + 0, + 16, + 16, + 16, + 16 + ); + matrices.pop(); + } else { + context.drawItem(BARRIER, x, y); + } + } + } + + @Override + public void onPress() { + onPress.accept(this); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + + } +} diff --git a/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/TrimSelectionWidget.java b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/TrimSelectionWidget.java new file mode 100644 index 0000000000..cea3d52b55 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/skyblock/item/custom/screen/TrimSelectionWidget.java @@ -0,0 +1,174 @@ +package de.hysky.skyblocker.skyblock.item.custom.screen; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.config.SkyblockerConfigManager; +import de.hysky.skyblocker.skyblock.item.custom.CustomArmorTrims; +import de.hysky.skyblocker.utils.ItemUtils; +import de.hysky.skyblocker.utils.Utils; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.Element; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.gui.widget.ContainerWidget; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.text.Text; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +public class TrimSelectionWidget extends ContainerWidget { + + private static final Identifier INNER_SPACE_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "menu_inner_space"); + private static final int BUTTONS_PER_ROW_PATTERN = 7; + private static final int BUTTONS_PER_ROW_MATERIAL = 5; + + private final List patternButtons = new ArrayList<>(); + private final List materialButtons = new ArrayList<>(); + private final List children = new ArrayList<>(); + + private ItemStack currentItem = null; + private Identifier selectedPattern = null; + private Identifier selectedMaterial = null; + + public TrimSelectionWidget(int x, int y, int width, int height) { + super(x, y, width, height, Text.of("Trim Selection")); + // Patterns + TrimElementButton.Pattern patternNoneButton = new TrimElementButton.Pattern(null, null, this::onClickPattern); + patternNoneButton.setMessage(Text.translatable("gui.none")); + patternButtons.add(patternNoneButton); + + Utils.getWrapperLookup().getOrThrow(RegistryKeys.TRIM_PATTERN).streamEntries() + // Sort them in alphabetical order + .sorted(Comparator.comparing(reference -> reference.value().description().getString())) + .map(reference -> new TrimElementButton.Pattern( + reference.registryKey().getValue(), + reference.value(), + this::onClickPattern + )).forEachOrdered(patternButtons::add); + children.addAll(patternButtons); + for (int i = 0; i < patternButtons.size(); i++) { + TrimElementButton button = patternButtons.get(i); + button.setPosition(x + 5 + (i % BUTTONS_PER_ROW_PATTERN) * 20, y + 5 + (i / BUTTONS_PER_ROW_PATTERN) * 20); + } + + // Materials + Utils.getWrapperLookup().getOrThrow(RegistryKeys.TRIM_MATERIAL).streamEntries() + // Sort them in alphabetical order + .sorted(Comparator.comparing(reference -> reference.value().description().getString())) + .map(reference -> new TrimElementButton.Material( + reference.registryKey().getValue(), + reference.value(), + this::onClickMaterial + )).forEachOrdered(materialButtons::add); + children.addAll(materialButtons); + for (int i = 0; i < materialButtons.size(); i++) { + TrimElementButton button = materialButtons.get(i); + button.setPosition(x + getWidth() - 11 - BUTTONS_PER_ROW_MATERIAL * 20 + (i % BUTTONS_PER_ROW_MATERIAL) * 20, y + 5 + (i / BUTTONS_PER_ROW_MATERIAL) * 20); + } + } + + private void onClickPattern(TrimElementButton button) { + for (TrimElementButton patternButton : patternButtons) { + patternButton.active = true; + } + button.active = false; + selectedPattern = button.getElement(); + updateConfig(); + } + + private void onClickMaterial(TrimElementButton button) { + for (TrimElementButton materialButton : materialButtons) { + materialButton.active = true; + } + button.active = false; + selectedMaterial = button.getElement(); + updateConfig(); + } + + private void updateConfig() { + if (currentItem == null) return; + Map trims = SkyblockerConfigManager.get().general.customArmorTrims; + String itemUuid = ItemUtils.getItemUuid(currentItem); + if (selectedPattern == null) { + trims.remove(itemUuid); + } else { + trims.put(itemUuid, new CustomArmorTrims.ArmorTrimId(selectedMaterial, selectedPattern)); + } + } + + @Override + public List children() { + return children; + } + + @Override + protected int getContentsHeightWithPadding() { + return (Math.max(patternButtons.size(), materialButtons.size()) / BUTTONS_PER_ROW_PATTERN + 1) * 20 + 10; + } + + @Override + protected double getDeltaYPerScroll() { + return 10; + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + return super.mouseClicked(mouseX, mouseY + this.getScrollY(), button); + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + context.drawGuiTexture(RenderLayer::getGuiTextured, INNER_SPACE_TEXTURE, getX(), getY(), getWidth(), getHeight()); + context.enableScissor(getX() + 2, getY() + 2, getX() + getWidth() - 2, getY() + getHeight() - 2); + + int scrollY = (int) this.getScrollY(); + for (ClickableWidget widget : this.children) { + widget.setY(widget.getY() - scrollY); + widget.render(context, mouseX, mouseY, delta); + widget.setY(widget.getY() + scrollY); + } + + drawScrollbar(context); + context.disableScissor(); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + + } + + public void setCurrentItem(@NotNull ItemStack currentItem) { + this.currentItem = currentItem; + Map trims = SkyblockerConfigManager.get().general.customArmorTrims; + String itemUuid = ItemUtils.getItemUuid(currentItem); + for (TrimElementButton.Pattern button : patternButtons) { + button.setItem(currentItem); + } + if (!trims.containsKey(itemUuid)) { + selectedPattern = null; + selectedMaterial = materialButtons.getFirst().getElement(); + for (int i = 0; i < materialButtons.size(); i++) { + materialButtons.get(i).active = i != 0; + } + for (int i = 0; i < patternButtons.size(); i++) { + patternButtons.get(i).active = i != 0; + } + } else { + CustomArmorTrims.ArmorTrimId id = trims.get(itemUuid); + selectedMaterial = id.material(); + selectedPattern = id.pattern(); + for (TrimElementButton materialButton : materialButtons) { + materialButton.active = !selectedMaterial.equals(materialButton.getElement()); + } + for (TrimElementButton patternButton : patternButtons) { + patternButton.active = !selectedPattern.equals(patternButton.getElement()); + } + } + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/OkLabColor.java b/src/main/java/de/hysky/skyblocker/utils/OkLabColor.java index ce5c9f1aa0..717b747b4a 100644 --- a/src/main/java/de/hysky/skyblocker/utils/OkLabColor.java +++ b/src/main/java/de/hysky/skyblocker/utils/OkLabColor.java @@ -6,6 +6,7 @@ * @see OkLab Colour Space * @see Gamma Correct Rendering */ +@SuppressWarnings("UnaryPlus") public class OkLabColor { /** diff --git a/src/main/java/de/hysky/skyblocker/utils/Utils.java b/src/main/java/de/hysky/skyblocker/utils/Utils.java index 53af1148c3..5072bb7bee 100644 --- a/src/main/java/de/hysky/skyblocker/utils/Utils.java +++ b/src/main/java/de/hysky/skyblocker/utils/Utils.java @@ -28,6 +28,8 @@ import net.minecraft.client.MinecraftClient; import net.minecraft.client.network.ClientPlayerEntity; import net.minecraft.client.network.PlayerListEntry; +import net.minecraft.registry.BuiltinRegistries; +import net.minecraft.registry.RegistryWrapper; import net.minecraft.scoreboard.*; import net.minecraft.text.Text; import net.minecraft.util.Formatting; @@ -575,4 +577,10 @@ public static UUID getUuid() { public static String getUndashedUuid() { return UndashedUuid.toString(getUuid()); } + + public static RegistryWrapper.WrapperLookup getWrapperLookup() { + MinecraftClient client = MinecraftClient.getInstance(); + return client != null && client.getNetworkHandler() != null && client.getNetworkHandler().getRegistryManager() != null ? client.getNetworkHandler().getRegistryManager() : BuiltinRegistries.createWrapperLookup(); + + } } diff --git a/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java index f24d03c33a..1b90f7410b 100644 --- a/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java +++ b/src/main/java/de/hysky/skyblocker/utils/render/RenderHelper.java @@ -388,4 +388,15 @@ public static void renderNineSliceColored(DrawContext context, Identifier textur public static void renderNineSliceColored(DrawContext context, Identifier texture, int x, int y, int width, int height, Color color) { renderNineSliceColored(context, texture, x, y, width, height, ColorHelper.getArgb(color.getAlpha(), color.getRed(), color.getGreen(), color.getBlue())); } + + public static void drawHorizontalGradient(DrawContext context, float startX, float startY, float endX, float endY, int colorStart, int colorEnd) { + context.draw(provider -> { + VertexConsumer vertexConsumer = provider.getBuffer(RenderLayer.getGui()); + Matrix4f matrix4f = context.getMatrices().peek().getPositionMatrix(); + vertexConsumer.vertex(matrix4f, startX, startY, 0).color(colorStart); + vertexConsumer.vertex(matrix4f, startX, endY, 0).color(colorStart); + vertexConsumer.vertex(matrix4f, endX, endY, 0).color(colorEnd); + vertexConsumer.vertex(matrix4f, endX, startY, 0).color(colorEnd); + }); + } } diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/ColorPickerWidget.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/ColorPickerWidget.java new file mode 100644 index 0000000000..8b72c2568e --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/ColorPickerWidget.java @@ -0,0 +1,185 @@ +package de.hysky.skyblocker.utils.render.gui; + +import de.hysky.skyblocker.SkyblockerMod; +import de.hysky.skyblocker.utils.render.RenderHelper; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.ScreenRect; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.client.render.RenderLayer; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.text.Text; +import net.minecraft.util.Colors; +import net.minecraft.util.Identifier; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; + +/** + * @implNote Does not render a background. + */ +public class ColorPickerWidget extends ClickableWidget { + private static final Identifier SV_THUMB_TEXTURE = Identifier.of(SkyblockerMod.NAMESPACE, "color_picker/sv_thumb"); + + private final int[] rainbowColors; + + private double hThumbX = 0; + private double svThumbX = 0; + private double svThumbY = 0; + + private int svColor = 0xFF_FF_00_00; + + private boolean draggingSV = false; + private boolean draggingH = false; + + private final ScreenRect svRect; + private final ScreenRect hRect; + + private int rgbColor = -1; + private @Nullable Callback onColorChange = null; + + private static int[] createRainbowColors(int samples) { + int[] rainbowColors = new int[samples]; + for (int i = 0; i < samples; i++) { + rainbowColors[i] = Color.HSBtoRGB((float) i / samples, 1, 1); + } + return rainbowColors; + } + + public ColorPickerWidget(int x, int y, int width, int height) { + super(x, y, width, height, Text.literal("ColorPicker")); + rainbowColors = createRainbowColors(Math.min(width / 20, 8)); + hRect = new ScreenRect(getX() + 1, getBottom() - 9, getWidth() - 2, 8); + int i = 15; + svRect = new ScreenRect(getX() + 1 + i, getY() + 1, getWidth() - 2 - i, height - hRect.height() - 6); + } + + @Override + public void onRelease(double mouseX, double mouseY) { + super.onRelease(mouseX, mouseY); + if ((draggingH || draggingSV) && onColorChange != null) { + onColorChange.onColorChange(rgbColor, true); + } + draggingH = false; + draggingSV = false; + + } + + @Override + public void onClick(double mouseX, double mouseY) { + super.onClick(mouseX, mouseY); + int i = (int) mouseX; + int j = (int) mouseY; + if (hRect.contains(i, j)) { + draggingH = true; + onDrag(mouseX, mouseY, 0, 0); + } + if (svRect.contains(i, j)) { + draggingSV = true; + onDrag(mouseX, mouseY, 0, 0); + } + } + + @Override + protected void onDrag(double mouseX, double mouseY, double deltaX, double deltaY) { + super.onDrag(mouseX, mouseY, deltaX, deltaY); + if (draggingH) { + hThumbX = Math.clamp(mouseX - hRect.getLeft(), 0, hRect.width() - 1); + svColor = Color.HSBtoRGB((float) (hThumbX / (hRect.width() - 1)), 1, 1); + } + if (draggingSV) { + svThumbX = Math.clamp(mouseX - svRect.getLeft(), 0, svRect.width() - 1); + svThumbY = Math.clamp(mouseY - svRect.getTop(), 0, svRect.height() - 1); + } + if (draggingH || draggingSV) { + rgbColor = Color.HSBtoRGB( + (float) (hThumbX / (hRect.width() - 1)), + (float) (svThumbX / (svRect.width() - 1)), + (float) (1 - (svThumbY / (svRect.height() - 1)))); + if (onColorChange != null) onColorChange.onColorChange(rgbColor, false); + } + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + int color = 0x80_60_60_60; + // Hue + context.fill(hRect.getLeft() - 1, hRect.getTop() - 1, hRect.getRight() + 1, hRect.getBottom() + 1, color); + for (int i = 0; i < rainbowColors.length; i++) { + int startColor = rainbowColors[i]; + int endColor = rainbowColors[(i + 1) % rainbowColors.length]; + float segmentLength = (float) hRect.width() / rainbowColors.length; + float startX = hRect.getLeft() + segmentLength * i; + float endX = hRect.getLeft() + segmentLength * (i + 1); + RenderHelper.drawHorizontalGradient(context, startX, hRect.getTop(), endX, hRect.getBottom(), startColor, endColor); + } + context.fill(hRect.getLeft() + (int) hThumbX - 1, hRect.getTop(), hRect.getLeft() + (int) hThumbX + 2, hRect.getBottom(), Colors.BLACK); + context.fill(hRect.getLeft() + (int) hThumbX, hRect.getTop() - 1, hRect.getLeft() + (int) hThumbX + 1, hRect.getBottom() + 1, Colors.BLACK); + context.fill(hRect.getLeft() + (int) hThumbX, hRect.getTop(), hRect.getLeft() + (int) hThumbX + 1, hRect.getBottom(), Colors.WHITE); + + // Light and saturation or whatever + context.fill(svRect.getLeft() - 1, svRect.getTop() - 1, svRect.getRight() + 1, svRect.getBottom() + 1, color); + int pickerX = svRect.getLeft(); + int pickerY = svRect.getTop(); + int pickerEndX = svRect.getRight(); + int pickerEndY = svRect.getBottom(); + RenderHelper.drawHorizontalGradient(context, pickerX, pickerY, pickerEndX, pickerEndY, -1, svColor); + context.fillGradient(pickerX, pickerY, pickerEndX, pickerEndY, 1, 0, 0xFF_00_00_00); + + MatrixStack matrices = context.getMatrices(); + matrices.push(); + matrices.translate(0, 0, 5); + context.drawGuiTexture(RenderLayer::getGuiTextured, SV_THUMB_TEXTURE, + svRect.getLeft() + (int) svThumbX - 2, + svRect.getTop() + (int) svThumbY - 2, + 5, 5 + ); + matrices.pop(); + + // Preview + context.fill(getX(), getY(), svRect.getLeft() - 2, svRect.getBottom() + 1, color); + context.fill(getX() + 1, getY() + 1, svRect.getLeft() - 3, svRect.getBottom(), rgbColor); + } + + public int getRGBColor() { + return rgbColor; + } + + public void setRGBColor(int argb) { + this.rgbColor = argb; + float[] floats = Color.RGBtoHSB((argb >> 16) & 0xFF, (argb >> 8) & 0xFF, argb & 0xFF, null); + setHSV(floats[0], floats[1], floats[2]); + } + + /** + * values between 0 and 1 + */ + public void setHSV(float h, float s, float v) { + hThumbX = h * (hRect.width() - 1); + svThumbX = s * (svRect.width() - 1); + svThumbY = (1 - v) * (svRect.height() - 1); + svColor = Color.HSBtoRGB((float) (hThumbX / (hRect.width() - 1)), 1, 1); + } + + /** + * Sets a callback that will be called whenever the color is changed by the user (not when {@link ColorPickerWidget#setRGBColor(int)} is called). + * @param onColorChange The consumer + */ + public void setOnColorChange(@Nullable Callback onColorChange) { + this.onColorChange = onColorChange; + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + + } + + public interface Callback { + + /** + * @param color the new color + * @param mouseRelease true if the change is "final" after the user has released the mouse or false when it's from the user dragging on of the thumbs around. + */ + void onColorChange(int color, boolean mouseRelease); + } +} diff --git a/src/main/java/de/hysky/skyblocker/utils/render/gui/RGBTextInput.java b/src/main/java/de/hysky/skyblocker/utils/render/gui/RGBTextInput.java new file mode 100644 index 0000000000..64708ca155 --- /dev/null +++ b/src/main/java/de/hysky/skyblocker/utils/render/gui/RGBTextInput.java @@ -0,0 +1,218 @@ +package de.hysky.skyblocker.utils.render.gui; + +import com.mojang.logging.LogUtils; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.font.TextRenderer; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.gui.screen.Screen; +import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; +import net.minecraft.client.gui.widget.ClickableWidget; +import net.minecraft.text.Style; +import net.minecraft.text.Text; +import net.minecraft.util.Colors; +import net.minecraft.util.Formatting; +import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFW; +import org.slf4j.Logger; + +import java.util.Locale; +import java.util.OptionalInt; +import java.util.function.IntConsumer; + +public class RGBTextInput extends ClickableWidget { + private static final Logger LOGGER = LogUtils.getLogger(); + + private static final String SAMPLE_TEXT = "AAAAAA"; + private static final Formatting[] FORMATTINGS = new Formatting[] {Formatting.RED, Formatting.GREEN, Formatting.BLUE}; + private static final String HEXADECIMAL_CHARS = "0123456789aAbBcCdDeEfF"; + + private static final int LENGTH = 6; + private final boolean drawBackground; + private final TextRenderer textRenderer; + + private String input = "FFFFFF"; + int index = 0; + + private @Nullable IntConsumer onChange = null; + + /** + * Creates a new widget. + *

+ * Height and width are automatically computed to be the size of the hex number + some padding if {@code drawBackground} is true. + * If the size needs to be changed, use {@link RGBTextInput#setWidth(int)} and {@link RGBTextInput#setHeight(int)}. + * + * @see RGBTextInput#setOnChange(IntConsumer) + * + * @param x x position + * @param y y position + * @param textRenderer text renderer to render the text (duh!) + * @param drawBackground draws a black background and a white border if true + * + */ + public RGBTextInput(int x, int y, TextRenderer textRenderer, boolean drawBackground) { + super(x, y, textRenderer.getWidth(SAMPLE_TEXT) + (drawBackground ? 6 : 0), 10 + (drawBackground ? 4 : 0), Text.of("RGBTextInput")); + this.drawBackground = drawBackground; + this.textRenderer = textRenderer; + } + + protected OptionalInt getOptionalRGBColor(String input) { + try { + return OptionalInt.of(0xFF000000 | Integer.parseInt(input, 16)); + } catch (NumberFormatException e) { + LOGGER.error("Could not parse rgb color", e); + } + return OptionalInt.empty(); + } + + /** + * @return the color, or white if something somehow went wrong + */ + public int getRGBColor() { + return getOptionalRGBColor(input).orElse(-1); + } + + public void setRGBColor(int rgb) { + input = String.format("%06X",rgb & 0x00FFFFFF); + } + + /** + * Sets a consumer that will be called whenever the color is changed by the user (and not when {@link RGBTextInput#setRGBColor(int)} is called) with the new color. + * The alpha channel will be at 255 (or FF) + * @param onChange the consumer + */ + public void setOnChange(@Nullable IntConsumer onChange) { + this.onChange = onChange; + } + + @Override + protected void renderWidget(DrawContext context, int mouseX, int mouseY, float delta) { + int selectionStart = textRenderer.getWidth(input.substring(0, index)); + int selectionEnd = textRenderer.getWidth(input.substring(0, index + 1)); + int textX = getX() + (drawBackground ? 3 : 0); + int textY = getY() + (drawBackground ? 3 : 0); + if (drawBackground) { + context.fill(getX(), getY(), getRight(), getBottom(), isFocused() ? Colors.WHITE: Colors.GRAY); + context.fill(getX() + 1, getY() + 1, getRight() - 1, getBottom() - 1, Colors.BLACK); + } + + if (isFocused()) { + context.fill( + textX + selectionStart, + textY, + textX + selectionEnd, + textY + textRenderer.fontHeight, + 0xff00bbff + ); + context.fill( + textX + selectionStart, + textY + textRenderer.fontHeight - 1, + textX + selectionEnd, + textY + textRenderer.fontHeight, + -1 + ); + } + context.drawText( + textRenderer, + visitor -> { + for (int i = 0; i < input.length(); i++) { + if (!visitor.accept(i, isSelected() ? Style.EMPTY.withFormatting(FORMATTINGS[i / 2]) : Style.EMPTY, input.charAt(i))) return false; + } + return true; + }, + textX, + textY, + -1, + true + ); + } + + @Override + protected void appendClickableNarrations(NarrationMessageBuilder builder) { + + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (!isFocused()) return false; + boolean bl = switch (keyCode) { + case GLFW.GLFW_KEY_DELETE -> { + StringBuilder builder = new StringBuilder(input); + builder.setCharAt(index, '0'); + input = builder.toString(); + yield true; + } + case GLFW.GLFW_KEY_BACKSPACE -> { + StringBuilder builder = new StringBuilder(input); + builder.setCharAt(index, '0'); + input = builder.toString(); + index = Math.max(0, index - 1); + yield true; + } + case GLFW.GLFW_KEY_LEFT -> { + index = Math.max(0, index - 1); + yield true; + } + case GLFW.GLFW_KEY_RIGHT -> { + index = Math.min(LENGTH - 1, index + 1); + yield true; + } + default -> false; + }; + if (bl) { + callOnChange(); + return true; + } else { + if (Screen.isCopy(keyCode)) { + MinecraftClient.getInstance().keyboard.setClipboard(input); + return true; + } else if (Screen.isPaste(keyCode)) { + String clipboard = MinecraftClient.getInstance().keyboard.getClipboard(); + String s = clipboard.substring(0, 6); + getOptionalRGBColor(s.toUpperCase(Locale.ENGLISH)).ifPresent(color -> { + input = s; + callOnChange(); + }); + return true; + + } + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean charTyped(char chr, int modifiers) { + if (!isFocused()) return false; + if (HEXADECIMAL_CHARS.indexOf(chr) >= 0) { + input = new StringBuilder(input).replace(index, index+1, String.valueOf(chr).toUpperCase(Locale.ENGLISH)).toString(); + index = Math.min(LENGTH - 1, index + 1); + callOnChange(); + return true; + } + + return super.charTyped(chr, modifiers); + } + + protected void callOnChange() { + if (onChange != null) { + onChange.accept(getRGBColor()); + } + } + + @Override + public void onClick(double mouseX, double mouseY) { + super.onClick(mouseX, mouseY); + findClickedChar((int) mouseX); + } + + @Override + protected void onDrag(double mouseX, double mouseY, double deltaX, double deltaY) { + super.onDrag(mouseX, mouseY, deltaX, deltaY); + findClickedChar((int) mouseX); + } + + private void findClickedChar(int mouseX) { + index = Math.clamp(textRenderer.trimToWidth(input, mouseX - getX() - (drawBackground ? 3 : 0)).length(), 0, LENGTH - 1); + } + + +} diff --git a/src/main/resources/assets/skyblocker/lang/en_us.json b/src/main/resources/assets/skyblocker/lang/en_us.json index 22b6b5b20c..143c6f52b4 100644 --- a/src/main/resources/assets/skyblocker/lang/en_us.json +++ b/src/main/resources/assets/skyblocker/lang/en_us.json @@ -1361,6 +1361,17 @@ "skyblocker.profileviewer.inventory.inactive.description.backpack": "The selected backpack", "skyblocker.profileviewer.inventory.inactive.description.general": "does not contain this slot", + "skyblocker.armorCustomization.addCustomColor": "Add custom color", + "skyblocker.armorCustomization.animated": "Animated", + "skyblocker.armorCustomization.cannotCustomizeColor": "Cannot customize this piece's color :(", + "skyblocker.armorCustomization.cycleBack": "Cycle Back", + "skyblocker.armorCustomization.removeCustomColor": "Remove custom color", + "skyblocker.armorCustomization.delay": "Delay: %s", + "skyblocker.armorCustomization.delayTooltip": "The animation will be delayed by this amount in seconds.", + "skyblocker.armorCustomization.duration": "Duration: %s", + "skyblocker.armorCustomization.durationTooltip": "The duration of the animation in seconds.", + "skyblocker.armorCustomization.title": "Pimp Your Armor", + "skyblocker.chat.confirmationPromptNotification": "Click anywhere on screen within 60 seconds to accept the prompt.", "emi.category.skyblocker.skyblock": "Skyblock" diff --git a/src/main/resources/assets/skyblocker/textures/gui/sprites/armor_customization_screen/button.png b/src/main/resources/assets/skyblocker/textures/gui/sprites/armor_customization_screen/button.png new file mode 100644 index 0000000000..4e0da0e3c6 Binary files /dev/null and b/src/main/resources/assets/skyblocker/textures/gui/sprites/armor_customization_screen/button.png differ diff --git a/src/main/resources/assets/skyblocker/textures/gui/sprites/armor_customization_screen/material_plate.png b/src/main/resources/assets/skyblocker/textures/gui/sprites/armor_customization_screen/material_plate.png new file mode 100644 index 0000000000..70ff7480dd Binary files /dev/null and b/src/main/resources/assets/skyblocker/textures/gui/sprites/armor_customization_screen/material_plate.png differ diff --git a/src/main/resources/assets/skyblocker/textures/gui/sprites/armor_customization_screen/mini_hotbar.png b/src/main/resources/assets/skyblocker/textures/gui/sprites/armor_customization_screen/mini_hotbar.png new file mode 100644 index 0000000000..3a03dd27dc Binary files /dev/null and b/src/main/resources/assets/skyblocker/textures/gui/sprites/armor_customization_screen/mini_hotbar.png differ diff --git a/src/main/resources/assets/skyblocker/textures/gui/sprites/color_picker/sv_thumb.png b/src/main/resources/assets/skyblocker/textures/gui/sprites/color_picker/sv_thumb.png new file mode 100644 index 0000000000..d9f03e90e7 Binary files /dev/null and b/src/main/resources/assets/skyblocker/textures/gui/sprites/color_picker/sv_thumb.png differ diff --git a/src/main/resources/assets/skyblocker/textures/gui/sprites/hotbar_selection_full.png b/src/main/resources/assets/skyblocker/textures/gui/sprites/hotbar_selection_full.png new file mode 100644 index 0000000000..0cfd15cb81 Binary files /dev/null and b/src/main/resources/assets/skyblocker/textures/gui/sprites/hotbar_selection_full.png differ diff --git a/src/main/resources/assets/skyblocker/textures/gui/sprites/menu_inner_space.png b/src/main/resources/assets/skyblocker/textures/gui/sprites/menu_inner_space.png new file mode 100644 index 0000000000..03e635a736 Binary files /dev/null and b/src/main/resources/assets/skyblocker/textures/gui/sprites/menu_inner_space.png differ diff --git a/src/main/resources/assets/skyblocker/textures/gui/sprites/menu_inner_space.png.mcmeta b/src/main/resources/assets/skyblocker/textures/gui/sprites/menu_inner_space.png.mcmeta new file mode 100644 index 0000000000..94db0d64ef --- /dev/null +++ b/src/main/resources/assets/skyblocker/textures/gui/sprites/menu_inner_space.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 16, + "height": 16, + "border": 2 + } + } +} diff --git a/src/main/resources/skyblocker.mixins.json b/src/main/resources/skyblocker.mixins.json index c9155f6d24..5735a48ba0 100644 --- a/src/main/resources/skyblocker.mixins.json +++ b/src/main/resources/skyblocker.mixins.json @@ -56,6 +56,7 @@ "accessors.CheckboxWidgetAccessor", "accessors.DrawContextInvoker", "accessors.EndermanEntityAccessor", + "accessors.EntityRenderDispatcherAccessor", "accessors.FrustumInvoker", "accessors.HandledScreenAccessor", "accessors.InGameHudInvoker", @@ -65,6 +66,7 @@ "accessors.RecipeBookWidgetAccessor", "accessors.ScreenAccessor", "accessors.SlotAccessor", + "accessors.SpriteContentsAccessor", "accessors.WorldRendererAccessor", "discordipc.ConnectionMixin", "jgit.SystemReaderMixin", diff --git a/src/test/java/de/hysky/skyblocker/skyblock/item/ArmorTrimIdSerializationTest.java b/src/test/java/de/hysky/skyblocker/skyblock/item/ArmorTrimIdSerializationTest.java index 8d2bfd6025..a0b1b64fc3 100644 --- a/src/test/java/de/hysky/skyblocker/skyblock/item/ArmorTrimIdSerializationTest.java +++ b/src/test/java/de/hysky/skyblocker/skyblock/item/ArmorTrimIdSerializationTest.java @@ -4,7 +4,7 @@ import com.google.gson.JsonElement; import com.mojang.serialization.JsonOps; -import de.hysky.skyblocker.skyblock.item.CustomArmorTrims.ArmorTrimId; +import de.hysky.skyblocker.skyblock.item.custom.CustomArmorTrims.ArmorTrimId; import net.minecraft.util.Identifier; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test;