diff --git a/README.md b/README.md index 30d74d2..6fcd8cf 100644 --- a/README.md +++ b/README.md @@ -1 +1,11 @@ -test \ No newline at end of file +# Icons Enhanced + +Icons Enhanced mejora la lectura de encantamientos y la UI sin cambiar el gameplay. + +## Caracteristicas +- Iconos y colores por encantamiento en los tooltips. +- Descripciones de encantamientos. +- Bordes en slots de inventario y contenedores. + +## Licencia +Apache-2.0 diff --git a/gradle.properties b/gradle.properties index 190cc96..1a61a72 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,16 +5,21 @@ org.gradle.parallel=true # IntelliJ IDEA is not yet fully compatible with configuration cache, see: https://github.com/FabricMC/fabric-loom/issues/1349 org.gradle.configuration-cache=false -# Fabric Properties -# check these on https://fabricmc.net/develop -minecraft_version=1.21.11 -loader_version=0.18.4 +# Fabric tooling loom_version=1.14-SNAPSHOT -# Mod Properties -mod_version=1.0.0 -maven_group=com.straice -archives_base_name=template-mod +# Mod metadata +mod.id=iconsenhanced +mod.name=Icons Enhanced +mod.version=0.1.0 +mod.group=dev.dekin +mod.description=Client-only suite of enchantment and UI improvements. +mod.license=Apache-2.0 +mod.author=DekinDev -# Dependencies -fabric_api_version=0.141.1+1.21.11 \ No newline at end of file +# Dependencies (overridden per version in versions/*/gradle.properties) +deps.fabric_loader=0.18.4 +deps.fabric_api=0.141.1+1.21.11 + +# Defaults (overridden per version as needed) +java_version=21 diff --git a/settings.gradle b/settings.gradle index 75c4d72..f134370 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,10 +1,33 @@ pluginManagement { repositories { + gradlePluginPortal() + maven { + name = 'KikuGie Releases' + url = 'https://maven.kikugie.dev/releases' + } + maven { + name = 'KikuGie Snapshots' + url = 'https://maven.kikugie.dev/snapshots' + } maven { name = 'Fabric' url = 'https://maven.fabricmc.net/' } mavenCentral() - gradlePluginPortal() } -} \ No newline at end of file +} + +plugins { + id 'dev.kikugie.stonecutter' version '0.8.2' +} + +stonecutter { + // Stonecutter uses Kotlin controller by default; keep Groovy for consistency. + kotlinController = false + centralScript = 'build.gradle' + + create(rootProject) { + versions '1.20.1', '1.20.4', '1.20.6', '1.21.1', '1.21.4', '1.21.11' + vcsVersion = '1.21.11' + } +} diff --git a/src/client/java/com/straice/TemplateModClient.java b/src/client/java/com/straice/TemplateModClient.java deleted file mode 100644 index 25d31e2..0000000 --- a/src/client/java/com/straice/TemplateModClient.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.straice; - -import net.fabricmc.api.ClientModInitializer; - -public class TemplateModClient implements ClientModInitializer { - @Override - public void onInitializeClient() { - // This entrypoint is suitable for setting up client-specific logic, such as rendering. - } -} \ No newline at end of file diff --git a/src/client/java/com/straice/mixin/client/ExampleClientMixin.java b/src/client/java/com/straice/mixin/client/ExampleClientMixin.java deleted file mode 100644 index 2c88d92..0000000 --- a/src/client/java/com/straice/mixin/client/ExampleClientMixin.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.straice.mixin.client; - -import net.minecraft.client.Minecraft; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(Minecraft.class) -public class ExampleClientMixin { - @Inject(at = @At("HEAD"), method = "run") - private void init(CallbackInfo info) { - // This code is injected into the start of Minecraft.run()V - } -} \ No newline at end of file diff --git a/src/client/java/dev/dekin/iconsenhanced/client/BorderRenderer.java b/src/client/java/dev/dekin/iconsenhanced/client/BorderRenderer.java new file mode 100644 index 0000000..40d9cf8 --- /dev/null +++ b/src/client/java/dev/dekin/iconsenhanced/client/BorderRenderer.java @@ -0,0 +1,59 @@ +package dev.dekin.iconsenhanced.client; + +import dev.dekin.iconsenhanced.common.BorderColorLogic; +import dev.dekin.iconsenhanced.common.ConfigManager; +import dev.dekin.iconsenhanced.common.IconsEnhancedConfig; +import dev.dekin.iconsenhanced.mixin.AbstractContainerScreenAccessor; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; + +public final class BorderRenderer { + private static final BorderColorLogic COLOR_LOGIC = new BorderColorLogic(); + + private BorderRenderer() { + } + + public static void applyConfig() { + COLOR_LOGIC.applyConfig(ConfigManager.get()); + } + + public static void render(Screen screen, GuiGraphics graphics, int mouseX, int mouseY) { + IconsEnhancedConfig config = ConfigManager.get(); + if (!config.borders.enabled) { + return; + } + if (!(screen instanceof AbstractContainerScreen handled)) { + return; + } + + int thickness = clamp(config.borders.thickness, 1, 3); + AbstractContainerScreenAccessor accessor = (AbstractContainerScreenAccessor) handled; + int left = accessor.iconsenhanced$getLeftPos(); + int top = accessor.iconsenhanced$getTopPos(); + + for (Slot slot : handled.getMenu().slots) { + ItemStack stack = slot.getItem(); + if (stack.isEmpty()) { + continue; + } + int color = COLOR_LOGIC.resolve(stack); + drawBorder(graphics, left + slot.x, top + slot.y, thickness, color); + } + } + + private static void drawBorder(GuiGraphics graphics, int x, int y, int thickness, int color) { + int x2 = x + 16; + int y2 = y + 16; + graphics.fill(x, y, x2, y + thickness, color); + graphics.fill(x, y2 - thickness, x2, y2, color); + graphics.fill(x, y, x + thickness, y2, color); + graphics.fill(x2 - thickness, y, x2, y2, color); + } + + private static int clamp(int value, int min, int max) { + return Math.min(max, Math.max(min, value)); + } +} diff --git a/src/client/java/dev/dekin/iconsenhanced/client/ClientInit.java b/src/client/java/dev/dekin/iconsenhanced/client/ClientInit.java new file mode 100644 index 0000000..6c90dd6 --- /dev/null +++ b/src/client/java/dev/dekin/iconsenhanced/client/ClientInit.java @@ -0,0 +1,21 @@ +package dev.dekin.iconsenhanced.client; + +import dev.dekin.iconsenhanced.common.ConfigManager; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; + +public final class ClientInit implements ClientModInitializer { + @Override + public void onInitializeClient() { + ConfigManager.load(); + BorderRenderer.applyConfig(); + TooltipAugmenter.register(); + + ScreenEvents.AFTER_INIT.register((client, screen, scaledWidth, scaledHeight) -> { + ScreenEvents.afterRender(screen).register((current, graphics, mouseX, mouseY, delta) -> { + BorderRenderer.render(current, graphics, mouseX, mouseY); + TooltipAugmenter.renderOverlay(current, graphics, mouseX, mouseY); + }); + }); + } +} diff --git a/src/client/java/dev/dekin/iconsenhanced/client/EnchantTooltipComponent.java b/src/client/java/dev/dekin/iconsenhanced/client/EnchantTooltipComponent.java new file mode 100644 index 0000000..32707e3 --- /dev/null +++ b/src/client/java/dev/dekin/iconsenhanced/client/EnchantTooltipComponent.java @@ -0,0 +1,201 @@ +package dev.dekin.iconsenhanced.client; + +import dev.dekin.iconsenhanced.common.IconAtlas; +import dev.dekin.iconsenhanced.common.TooltipLayoutModel; +import dev.dekin.iconsenhanced.common.TooltipLayoutModel.Row; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.Font; +import net.minecraft.client.gui.screens.inventory.tooltip.ClientTooltipComponent; +import net.minecraft.network.chat.Component; +//? if >=1.21 { +import net.minecraft.client.renderer.RenderPipelines; +//?} else { +/*import net.minecraft.client.renderer.MultiBufferSource; +import org.joml.Matrix4f; +*///?} + +public final class EnchantTooltipComponent implements ClientTooltipComponent { + private static final int ROW_GAP = 2; + private static final int TEXT_PADDING = 4; + private static final int BADGE_BG = 0xAA000000; + private static final int BADGE_TEXT = 0xFFFFFFFF; + private static final int FULL_BRIGHT = 0xF000F0; + + private final TooltipLayoutModel layout; + private final int iconSize; + private final boolean showLevelBadge; + + public EnchantTooltipComponent(EnchantTooltipData data) { + this.layout = data.layout(); + this.iconSize = Math.max(4, data.iconSize()); + this.showLevelBadge = data.showLevelBadge(); + } + + @Override + public int getWidth(Font font) { + int textStart = iconSize + TEXT_PADDING; + int width = 0; + for (Row row : layout.rows()) { + width = Math.max(width, textStart + font.width(row.text)); + for (Component desc : row.descriptions) { + width = Math.max(width, textStart + font.width(desc)); + } + } + return width; + } + + //? if >=1.21 { + @Override + public int getHeight(Font font) { + return computeHeight(font); + } + //?} else { + /*@Override + public int getHeight() { + return computeHeight(null); + } + *///?} + + private int computeHeight(Font font) { + int height = 0; + int lineHeight = font == null ? 9 : font.lineHeight; + int rowHeight = Math.max(iconSize, lineHeight); + for (int i = 0; i < layout.rows().size(); i++) { + Row row = layout.rows().get(i); + height += rowHeight; + height += row.descriptions.size() * lineHeight; + if (i < layout.rows().size() - 1) { + height += ROW_GAP; + } + } + return height; + } + + //? if >=1.21 { + @Override + public boolean showTooltipWithItemInHand() { + return true; + } + //?} + + //? if >=1.21 { + @Override + public void renderText(GuiGraphics graphics, Font font, int x, int y) { + int yOffset = 0; + int rowHeight = Math.max(iconSize, font.lineHeight); + int textX = x + iconSize + TEXT_PADDING; + for (Row row : layout.rows()) { + int textY = y + yOffset + (rowHeight - font.lineHeight) / 2; + graphics.drawString(font, row.text, textX, textY, 0xFFFFFFFF, false); + yOffset += rowHeight; + for (Component desc : row.descriptions) { + graphics.drawString(font, desc, textX, y + yOffset, 0xFFFFFFFF, false); + yOffset += font.lineHeight; + } + yOffset += ROW_GAP; + } + } + //?} else { + /*@Override + public void renderText(Font font, int x, int y, Matrix4f matrix, MultiBufferSource.BufferSource buffer) { + int yOffset = 0; + int rowHeight = Math.max(iconSize, font.lineHeight); + int textX = x + iconSize + TEXT_PADDING; + for (Row row : layout.rows()) { + int textY = y + yOffset + (rowHeight - font.lineHeight) / 2; + font.drawInBatch(row.text, textX, textY, 0xFFFFFFFF, false, matrix, buffer, Font.DisplayMode.NORMAL, 0, FULL_BRIGHT); + yOffset += rowHeight; + for (Component desc : row.descriptions) { + font.drawInBatch(desc, textX, y + yOffset, 0xFFFFFFFF, false, matrix, buffer, Font.DisplayMode.NORMAL, 0, FULL_BRIGHT); + yOffset += font.lineHeight; + } + yOffset += ROW_GAP; + } + } + *///?} + + //? if >=1.21 { + @Override + public void renderImage(Font font, int x, int y, int width, int height, GuiGraphics graphics) { + renderIcons(font, x, y, graphics); + } + //?} else { + /*@Override + public void renderImage(Font font, int x, int y, GuiGraphics graphics) { + renderIcons(font, x, y, graphics); + } + *///?} + + private void renderIcons(Font font, int x, int y, GuiGraphics graphics) { + int yOffset = 0; + int rowHeight = Math.max(iconSize, font.lineHeight); + for (Row row : layout.rows()) { + int iconY = y + yOffset + (rowHeight - iconSize) / 2; + drawIcon(graphics, x, iconY, row.iconKey); + if (showLevelBadge) { + drawBadge(graphics, font, x, iconY, row.level); + } + yOffset += rowHeight; + yOffset += row.descriptions.size() * font.lineHeight; + yOffset += ROW_GAP; + } + } + + private void drawIcon(GuiGraphics graphics, int x, int y, String key) { + IconAtlas.IconSprite sprite = IconAtlas.get(key); + float scale = iconSize / (float) IconAtlas.ICON_SOURCE_SIZE; + //? if >=1.21 { + graphics.pose().pushMatrix(); + graphics.pose().translate(x, y); + graphics.pose().scale(scale, scale); + //?} else { + /*graphics.pose().pushPose(); + graphics.pose().translate(x, y, 0); + graphics.pose().scale(scale, scale, 1.0f); + *///?} + blit(graphics, 0, 0, sprite.u, sprite.v, sprite.size); + //? if >=1.21 { + graphics.pose().popMatrix(); + //?} else { + /*graphics.pose().popPose(); + *///?} + } + + private void drawBadge(GuiGraphics graphics, Font font, int x, int y, int level) { + String text = levelToRoman(level); + if (text.isEmpty()) { + return; + } + int textWidth = font.width(text); + int badgeWidth = textWidth + 2; + int badgeHeight = font.lineHeight; + int badgeX = x + iconSize - badgeWidth; + int badgeY = y - 1; + graphics.fill(badgeX, badgeY, badgeX + badgeWidth, badgeY + badgeHeight, BADGE_BG); + graphics.drawString(font, text, badgeX + 1, badgeY + 1, BADGE_TEXT, false); + } + + private static String levelToRoman(int level) { + return switch (level) { + case 1 -> "I"; + case 2 -> "II"; + case 3 -> "III"; + case 4 -> "IV"; + case 5 -> "V"; + case 6 -> "VI"; + case 7 -> "VII"; + case 8 -> "VIII"; + case 9 -> "IX"; + case 10 -> "X"; + default -> Integer.toString(level); + }; + } + + private static void blit(GuiGraphics graphics, int x, int y, int u, int v, int size) { + //? if >=1.21 { + graphics.blit(RenderPipelines.GUI_TEXTURED, IconAtlas.ATLAS, x, y, u, v, size, size, IconAtlas.ATLAS_SIZE, IconAtlas.ATLAS_SIZE); + //?} else { + /*graphics.blit(IconAtlas.ATLAS, x, y, 0, (float) u, (float) v, size, size, IconAtlas.ATLAS_SIZE, IconAtlas.ATLAS_SIZE); + *///?} + } +} diff --git a/src/client/java/dev/dekin/iconsenhanced/client/EnchantTooltipData.java b/src/client/java/dev/dekin/iconsenhanced/client/EnchantTooltipData.java new file mode 100644 index 0000000..9ed70e4 --- /dev/null +++ b/src/client/java/dev/dekin/iconsenhanced/client/EnchantTooltipData.java @@ -0,0 +1,28 @@ +package dev.dekin.iconsenhanced.client; + +import dev.dekin.iconsenhanced.common.TooltipLayoutModel; +import net.minecraft.world.inventory.tooltip.TooltipComponent; + +public final class EnchantTooltipData implements TooltipComponent { + private final TooltipLayoutModel layout; + private final int iconSize; + private final boolean showLevelBadge; + + public EnchantTooltipData(TooltipLayoutModel layout, int iconSize, boolean showLevelBadge) { + this.layout = layout; + this.iconSize = iconSize; + this.showLevelBadge = showLevelBadge; + } + + public TooltipLayoutModel layout() { + return layout; + } + + public int iconSize() { + return iconSize; + } + + public boolean showLevelBadge() { + return showLevelBadge; + } +} diff --git a/src/client/java/dev/dekin/iconsenhanced/client/TooltipAugmenter.java b/src/client/java/dev/dekin/iconsenhanced/client/TooltipAugmenter.java new file mode 100644 index 0000000..0ff9a88 --- /dev/null +++ b/src/client/java/dev/dekin/iconsenhanced/client/TooltipAugmenter.java @@ -0,0 +1,218 @@ +package dev.dekin.iconsenhanced.client; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import dev.dekin.iconsenhanced.IconsEnhanced; +import dev.dekin.iconsenhanced.adapters.DefaultEnchantmentAdapter; +import dev.dekin.iconsenhanced.adapters.EnchantmentAdapter; +import dev.dekin.iconsenhanced.common.ConfigManager; +import dev.dekin.iconsenhanced.common.EnchEntry; +import dev.dekin.iconsenhanced.common.EnchantSortLogic; +import dev.dekin.iconsenhanced.common.IconsEnhancedConfig; +import dev.dekin.iconsenhanced.common.TooltipLayoutModel; +import dev.dekin.iconsenhanced.mixin.AbstractContainerScreenAccessor; +import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback; +import net.fabricmc.fabric.api.client.rendering.v1.TooltipComponentCallback; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.inventory.tooltip.TooltipComponent; + +public final class TooltipAugmenter { + private static final EnchantmentAdapter ADAPTER = new DefaultEnchantmentAdapter(); + private static final EnchantSortLogic SORT_LOGIC = new EnchantSortLogic(); + private static final ThreadLocal CAPTURE = ThreadLocal.withInitial(() -> Boolean.FALSE); + private static final ThreadLocal CAPTURED_DATA = new ThreadLocal<>(); + + private TooltipAugmenter() { + } + + public static void register() { + //? if >=1.21 { + ItemTooltipCallback.EVENT.register((stack, context, flag, lines) -> onTooltip(stack, lines)); + //?} else { + /*ItemTooltipCallback.EVENT.register((stack, context, lines) -> onTooltip(stack, lines)); + *///?} + + TooltipComponentCallback.EVENT.register(data -> { + if (data instanceof EnchantTooltipData enchData) { + return new EnchantTooltipComponent(enchData); + } + return null; + }); + } + + public static void renderOverlay(Screen screen, GuiGraphics graphics, int mouseX, int mouseY) { + IconsEnhancedConfig config = ConfigManager.get(); + if (!config.tooltips.enabled || !config.tooltips.showIcons) { + return; + } + if (!(screen instanceof AbstractContainerScreen handled)) { + return; + } + + Slot hovered = findHoveredSlot(handled, mouseX, mouseY); + if (hovered == null) { + return; + } + ItemStack stack = hovered.getItem(); + if (stack.isEmpty()) { + return; + } + + Minecraft client = Minecraft.getInstance(); + List lines; + EnchantTooltipData data; + try { + CAPTURE.set(Boolean.TRUE); + CAPTURED_DATA.remove(); + lines = Screen.getTooltipFromItem(client, stack); + data = CAPTURED_DATA.get(); + } finally { + CAPTURE.remove(); + CAPTURED_DATA.remove(); + } + + if (data == null) { + return; + } + + Optional tooltipData = Optional.of(data); + //? if >=1.21 { + graphics.setTooltipForNextFrame(client.font, lines, tooltipData, mouseX, mouseY); + //?} else { + /*graphics.renderTooltip(client.font, lines, tooltipData, mouseX, mouseY); + *///?} + } + + private static void onTooltip(ItemStack stack, List lines) { + try { + IconsEnhancedConfig config = ConfigManager.get(); + if (!config.tooltips.enabled) { + return; + } + + List entries = ADAPTER.getEnchantments(stack); + if (entries.isEmpty()) { + return; + } + + TooltipBlock block = TooltipBlock.find(lines, entries); + if (!block.contiguous) { + return; + } + + List sorted = SORT_LOGIC.sort(entries, config.tooltips); + + if (Boolean.TRUE.equals(CAPTURE.get()) && config.tooltips.showIcons) { + EnchantTooltipData data = buildTooltipData(sorted, config.tooltips); + CAPTURED_DATA.set(data); + block.remove(lines); + return; + } + + if (!config.tooltips.reorderEnchantments && !config.tooltips.showDescriptions) { + return; + } + + List ordered = config.tooltips.reorderEnchantments ? sorted : entries; + List replacement = buildTextLines(ordered, config.tooltips.showDescriptions); + block.replace(lines, replacement); + } catch (Exception e) { + IconsEnhanced.LOGGER.warn("Tooltip augmentation failed.", e); + } + } + + private static EnchantTooltipData buildTooltipData(List entries, IconsEnhancedConfig.Tooltips tooltips) { + List rows = new ArrayList<>(); + for (EnchEntry entry : entries) { + List desc = tooltips.showDescriptions + ? TooltipTextUtil.getDescriptionLines(entry.descKey) + : List.of(); + rows.add(new TooltipLayoutModel.Row(entry.displayName, desc, entry.iconKey, entry.level, entry.isCurse)); + } + return new EnchantTooltipData(new TooltipLayoutModel(rows), tooltips.iconSize, tooltips.showLevelBadge); + } + + private static List buildTextLines(List entries, boolean showDescriptions) { + List lines = new ArrayList<>(); + for (EnchEntry entry : entries) { + lines.add(entry.displayName); + if (showDescriptions) { + lines.addAll(TooltipTextUtil.getDescriptionLines(entry.descKey)); + } + } + return lines; + } + + private static Slot findHoveredSlot(AbstractContainerScreen screen, int mouseX, int mouseY) { + AbstractContainerScreenAccessor accessor = (AbstractContainerScreenAccessor) screen; + int left = accessor.iconsenhanced$getLeftPos(); + int top = accessor.iconsenhanced$getTopPos(); + for (Slot slot : screen.getMenu().slots) { + int sx = left + slot.x; + int sy = top + slot.y; + if (mouseX >= sx && mouseX < sx + 16 && mouseY >= sy && mouseY < sy + 16) { + return slot; + } + } + return null; + } + + private static final class TooltipBlock { + private final int start; + private final int end; + private final boolean contiguous; + + private TooltipBlock(int start, int end, boolean contiguous) { + this.start = start; + this.end = end; + this.contiguous = contiguous; + } + + private void remove(List lines) { + lines.subList(start, end + 1).clear(); + } + + private void replace(List lines, List replacement) { + lines.subList(start, end + 1).clear(); + lines.addAll(start, replacement); + } + + private static TooltipBlock find(List lines, List entries) { + Set names = new HashSet<>(); + for (EnchEntry entry : entries) { + names.add(entry.displayNameString); + } + + int first = -1; + int last = -1; + int count = 0; + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i).getString(); + if (names.contains(line)) { + if (first == -1) { + first = i; + } + last = i; + count++; + } + } + + if (first == -1) { + return new TooltipBlock(0, 0, false); + } + + boolean contiguous = (last - first + 1) == count; + return new TooltipBlock(first, last, contiguous); + } + } +} diff --git a/src/client/java/dev/dekin/iconsenhanced/client/TooltipTextUtil.java b/src/client/java/dev/dekin/iconsenhanced/client/TooltipTextUtil.java new file mode 100644 index 0000000..038401b --- /dev/null +++ b/src/client/java/dev/dekin/iconsenhanced/client/TooltipTextUtil.java @@ -0,0 +1,44 @@ +package dev.dekin.iconsenhanced.client; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import net.minecraft.client.resources.language.I18n; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.Style; + +public final class TooltipTextUtil { + private static final Map> CACHE = new HashMap<>(); + private static final Style DESCRIPTION_STYLE = Style.EMPTY; + + private TooltipTextUtil() { + } + + public static List getDescriptionLines(String key) { + if (key == null || key.isBlank()) { + return Collections.emptyList(); + } + if (!I18n.exists(key)) { + return Collections.emptyList(); + } + return CACHE.computeIfAbsent(key, TooltipTextUtil::splitLines); + } + + private static List splitLines(String key) { + String text = I18n.get(key); + if (text == null || text.isEmpty()) { + return Collections.emptyList(); + } + String[] parts = text.split("\\\\n"); + List lines = new ArrayList<>(parts.length); + for (String part : parts) { + if (!part.isEmpty()) { + lines.add(Component.literal(part).setStyle(DESCRIPTION_STYLE)); + } + } + return Collections.unmodifiableList(lines); + } +} diff --git a/src/client/java/dev/dekin/iconsenhanced/mixin/AbstractContainerScreenAccessor.java b/src/client/java/dev/dekin/iconsenhanced/mixin/AbstractContainerScreenAccessor.java new file mode 100644 index 0000000..d06e0c1 --- /dev/null +++ b/src/client/java/dev/dekin/iconsenhanced/mixin/AbstractContainerScreenAccessor.java @@ -0,0 +1,14 @@ +package dev.dekin.iconsenhanced.mixin; + +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(AbstractContainerScreen.class) +public interface AbstractContainerScreenAccessor { + @Accessor("leftPos") + int iconsenhanced$getLeftPos(); + + @Accessor("topPos") + int iconsenhanced$getTopPos(); +} diff --git a/src/client/resources/template-mod.client.mixins.json b/src/client/resources/template-mod.client.mixins.json deleted file mode 100644 index e232005..0000000 --- a/src/client/resources/template-mod.client.mixins.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "required": true, - "package": "com.straice.mixin.client", - "compatibilityLevel": "JAVA_21", - "client": [ - "ExampleClientMixin" - ], - "injectors": { - "defaultRequire": 1 - } -} \ No newline at end of file diff --git a/src/main/java/com/straice/TemplateMod.java b/src/main/java/com/straice/TemplateMod.java deleted file mode 100644 index 1f53723..0000000 --- a/src/main/java/com/straice/TemplateMod.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.straice; - -import net.fabricmc.api.ModInitializer; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class TemplateMod implements ModInitializer { - public static final String MOD_ID = "template-mod"; - - // This logger is used to write text to the console and the log file. - // It is considered best practice to use your mod id as the logger's name. - // That way, it's clear which mod wrote info, warnings, and errors. - public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); - - @Override - public void onInitialize() { - // This code runs as soon as Minecraft is in a mod-load-ready state. - // However, some things (like resources) may still be uninitialized. - // Proceed with mild caution. - - LOGGER.info("Hello Fabric world!"); - } -} \ No newline at end of file diff --git a/src/main/java/com/straice/mixin/ExampleMixin.java b/src/main/java/com/straice/mixin/ExampleMixin.java deleted file mode 100644 index 386c877..0000000 --- a/src/main/java/com/straice/mixin/ExampleMixin.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.straice.mixin; - -import net.minecraft.server.MinecraftServer; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -@Mixin(MinecraftServer.class) -public class ExampleMixin { - @Inject(at = @At("HEAD"), method = "loadLevel") - private void init(CallbackInfo info) { - // This code is injected into the start of MinecraftServer.loadLevel()V - } -} \ No newline at end of file diff --git a/src/main/java/dev/dekin/iconsenhanced/IconsEnhanced.java b/src/main/java/dev/dekin/iconsenhanced/IconsEnhanced.java new file mode 100644 index 0000000..3d5c4c1 --- /dev/null +++ b/src/main/java/dev/dekin/iconsenhanced/IconsEnhanced.java @@ -0,0 +1,12 @@ +package dev.dekin.iconsenhanced; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class IconsEnhanced { + public static final String MOD_ID = "iconsenhanced"; + public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); + + private IconsEnhanced() { + } +} diff --git a/src/main/java/dev/dekin/iconsenhanced/adapters/DefaultEnchantmentAdapter.java b/src/main/java/dev/dekin/iconsenhanced/adapters/DefaultEnchantmentAdapter.java new file mode 100644 index 0000000..e84988d --- /dev/null +++ b/src/main/java/dev/dekin/iconsenhanced/adapters/DefaultEnchantmentAdapter.java @@ -0,0 +1,116 @@ +package dev.dekin.iconsenhanced.adapters; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +//? if <1.21 { +/*import java.util.Map; +*///?} + +import dev.dekin.iconsenhanced.IconsEnhanced; +import dev.dekin.iconsenhanced.common.EnchEntry; +import dev.dekin.iconsenhanced.common.IconKeyResolver; +import dev.dekin.iconsenhanced.common.IconKeyResolver.Visual; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.network.chat.Component; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.enchantment.Enchantment; +//? if >=1.21 { +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import net.minecraft.core.Holder; +import net.minecraft.core.component.DataComponents; +import net.minecraft.tags.EnchantmentTags; +import net.minecraft.world.item.enchantment.ItemEnchantments; +//?} else { +/*import net.minecraft.nbt.ListTag; +import net.minecraft.world.item.EnchantedBookItem; +import net.minecraft.world.item.enchantment.EnchantmentHelper; +*///?} + +public final class DefaultEnchantmentAdapter implements EnchantmentAdapter { + @Override + public List getEnchantments(ItemStack stack) { + if (stack == null || stack.isEmpty()) { + return Collections.emptyList(); + } + + List entries = new ArrayList<>(); + + //? if >=1.21 { + appendEntries(entries, stack, stack.getEnchantments()); + ItemEnchantments stored = stack.get(DataComponents.STORED_ENCHANTMENTS); + if (stored != null && !stored.isEmpty()) { + appendEntries(entries, stack, stored); + } + //?} else { + /*Map enchantments = EnchantmentHelper.getEnchantments(stack); + if (!enchantments.isEmpty()) { + appendEntries(entries, stack, enchantments); + } + if (stack.getItem() instanceof EnchantedBookItem) { + ListTag list = EnchantedBookItem.getEnchantments(stack); + Map stored = EnchantmentHelper.deserializeEnchantments(list); + appendEntries(entries, stack, stored); + } + *///?} + + return entries; + } + + //? if >=1.21 { + private static void appendEntries(List entries, ItemStack stack, ItemEnchantments enchantments) { + if (enchantments == null || enchantments.isEmpty()) { + return; + } + for (Object2IntMap.Entry> entry : enchantments.entrySet()) { + Holder holder = entry.getKey(); + Enchantment enchantment = holder.value(); + int level = entry.getIntValue(); + String id = holder.unwrapKey() + .map(key -> key.identifier().toString()) + .orElse(""); + boolean isCurse = holder.is(EnchantmentTags.CURSE); + Visual visual = IconKeyResolver.resolve(id, isCurse); + Component name = colorize(enchantment.getFullname(holder, level), visual.color); + String descKey = buildDescKey(id); + entries.add(new EnchEntry(id, level, name, isCurse, visual.iconKey, descKey)); + } + } + //?} else { + /*private static void appendEntries(List entries, ItemStack stack, Map enchantments) { + if (enchantments == null || enchantments.isEmpty()) { + return; + } + for (Map.Entry entry : enchantments.entrySet()) { + Enchantment enchantment = entry.getKey(); + int level = entry.getValue(); + String id = BuiltInRegistries.ENCHANTMENT.getKey(enchantment).toString(); + boolean isCurse = enchantment.isCurse(); + Visual visual = IconKeyResolver.resolve(id, isCurse); + Component name = colorize(enchantment.getFullname(level), visual.color); + String descKey = buildDescKey(id); + entries.add(new EnchEntry(id, level, name, isCurse, visual.iconKey, descKey)); + } + } + *///?} + + private static String buildDescKey(String id) { + if (id == null || id.isBlank()) { + return ""; + } + int split = id.indexOf(':'); + if (split <= 0 || split >= id.length() - 1) { + return ""; + } + String namespace = id.substring(0, split); + String path = id.substring(split + 1); + return IconsEnhanced.MOD_ID + ".desc." + namespace + "." + path; + } + + private static Component colorize(Component name, int color) { + if (color == 0) { + return name; + } + return name.copy().withStyle(style -> style.withColor(color)); + } +} diff --git a/src/main/java/dev/dekin/iconsenhanced/adapters/EnchantmentAdapter.java b/src/main/java/dev/dekin/iconsenhanced/adapters/EnchantmentAdapter.java new file mode 100644 index 0000000..6cbcae7 --- /dev/null +++ b/src/main/java/dev/dekin/iconsenhanced/adapters/EnchantmentAdapter.java @@ -0,0 +1,10 @@ +package dev.dekin.iconsenhanced.adapters; + +import java.util.List; + +import dev.dekin.iconsenhanced.common.EnchEntry; +import net.minecraft.world.item.ItemStack; + +public interface EnchantmentAdapter { + List getEnchantments(ItemStack stack); +} diff --git a/src/main/java/dev/dekin/iconsenhanced/common/BorderColorLogic.java b/src/main/java/dev/dekin/iconsenhanced/common/BorderColorLogic.java new file mode 100644 index 0000000..a909645 --- /dev/null +++ b/src/main/java/dev/dekin/iconsenhanced/common/BorderColorLogic.java @@ -0,0 +1,115 @@ +package dev.dekin.iconsenhanced.common; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import dev.dekin.iconsenhanced.IconsEnhanced; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Rarity; + +public final class BorderColorLogic { + private final Map rarityColors = new HashMap<>(); + private final Map customRuleColors = new HashMap<>(); + private boolean enchantedOverride; + private int enchantedColor; + private boolean useCustomRules; + private String mode; + private int alpha; + + public void applyConfig(IconsEnhancedConfig config) { + IconsEnhancedConfig.Borders borders = config.borders; + rarityColors.clear(); + for (Map.Entry entry : borders.rarityColors.entrySet()) { + rarityColors.put(entry.getKey(), parseColor(entry.getValue(), 0xFFAAAAAA)); + } + + customRuleColors.clear(); + for (IconsEnhancedConfig.CustomBorderRule rule : borders.customRules) { + String id = normalizeId(rule.itemId); + if (id == null) { + IconsEnhanced.LOGGER.warn("Invalid border rule item id: {}", rule.itemId); + continue; + } + customRuleColors.put(id, parseColor(rule.color, 0xFFFFFFFF)); + } + + enchantedOverride = borders.enchantedOverride.enabled; + enchantedColor = parseColor(borders.enchantedOverride.color, 0xFFFFD700); + useCustomRules = borders.useCustomRules; + mode = borders.mode == null ? "RARITY" : borders.mode.trim().toUpperCase(); + alpha = clamp(borders.alpha, 0, 255); + } + + public int resolve(ItemStack stack) { + String itemId = BuiltInRegistries.ITEM.getKey(stack.getItem()).toString(); + if (useCustomRules) { + Integer ruleColor = customRuleColors.get(itemId); + if (ruleColor != null) { + return withAlpha(ruleColor, alpha); + } + } + + if (enchantedOverride && stack.isEnchanted()) { + return withAlpha(enchantedColor, alpha); + } + + if ("CUSTOM".equals(mode)) { + Integer ruleColor = customRuleColors.get(itemId); + if (ruleColor != null) { + return withAlpha(ruleColor, alpha); + } + } + + int rarityColor = rarityColors.getOrDefault(rarityKey(stack.getRarity()), 0xFFAAAAAA); + return withAlpha(rarityColor, alpha); + } + + private static String normalizeId(String raw) { + if (raw == null) { + return null; + } + String cleaned = raw.trim().toLowerCase(Locale.ROOT); + if (cleaned.isEmpty() || !cleaned.contains(":")) { + return null; + } + return cleaned; + } + + private static String rarityKey(Rarity rarity) { + if (rarity == Rarity.UNCOMMON) { + return "uncommon"; + } + if (rarity == Rarity.RARE) { + return "rare"; + } + if (rarity == Rarity.EPIC) { + return "epic"; + } + return "common"; + } + + private static int parseColor(String value, int fallback) { + if (value == null) { + return fallback; + } + try { + String cleaned = value.trim(); + if (cleaned.isEmpty()) { + return fallback; + } + return (int) Long.decode(cleaned).longValue(); + } catch (NumberFormatException ex) { + return fallback; + } + } + + private static int withAlpha(int color, int alpha) { + return (color & 0x00FFFFFF) | ((alpha & 0xFF) << 24); + } + + private static int clamp(int value, int min, int max) { + return Math.min(max, Math.max(min, value)); + } +} diff --git a/src/main/java/dev/dekin/iconsenhanced/common/ConfigManager.java b/src/main/java/dev/dekin/iconsenhanced/common/ConfigManager.java new file mode 100644 index 0000000..56f927c --- /dev/null +++ b/src/main/java/dev/dekin/iconsenhanced/common/ConfigManager.java @@ -0,0 +1,72 @@ +package dev.dekin.iconsenhanced.common; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonSyntaxException; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; + +import dev.dekin.iconsenhanced.IconsEnhanced; +import net.fabricmc.loader.api.FabricLoader; + +public final class ConfigManager { + private static final Gson GSON = new GsonBuilder() + .setPrettyPrinting() + .disableHtmlEscaping() + .create(); + private static final Path CONFIG_PATH = FabricLoader.getInstance() + .getConfigDir() + .resolve(IconsEnhanced.MOD_ID + ".json"); + + private static IconsEnhancedConfig config = new IconsEnhancedConfig(); + + private ConfigManager() { + } + + public static IconsEnhancedConfig get() { + return config; + } + + public static void load() { + IconsEnhancedConfig loaded = null; + boolean needsSave = false; + if (Files.exists(CONFIG_PATH)) { + try (Reader reader = Files.newBufferedReader(CONFIG_PATH)) { + loaded = GSON.fromJson(reader, IconsEnhancedConfig.class); + } catch (JsonSyntaxException | IOException e) { + IconsEnhanced.LOGGER.warn("Config parse failed, recreating defaults.", e); + needsSave = true; + } + } else { + needsSave = true; + } + + if (loaded == null) { + loaded = new IconsEnhancedConfig(); + needsSave = true; + } else { + needsSave |= loaded.applyDefaults(); + } + + config = loaded; + + if (needsSave) { + save(); + } + } + + public static void save() { + try { + Files.createDirectories(CONFIG_PATH.getParent()); + try (Writer writer = Files.newBufferedWriter(CONFIG_PATH)) { + GSON.toJson(config, writer); + } + } catch (IOException e) { + IconsEnhanced.LOGGER.warn("Failed to write config to disk.", e); + } + } +} diff --git a/src/main/java/dev/dekin/iconsenhanced/common/EnchEntry.java b/src/main/java/dev/dekin/iconsenhanced/common/EnchEntry.java new file mode 100644 index 0000000..594a574 --- /dev/null +++ b/src/main/java/dev/dekin/iconsenhanced/common/EnchEntry.java @@ -0,0 +1,41 @@ +package dev.dekin.iconsenhanced.common; + +import net.minecraft.network.chat.Component; + +public final class EnchEntry { + public final String id; + public final String namespace; + public final String path; + public final int level; + public final Component displayName; + public final String displayNameString; + public final boolean isCurse; + public final String iconKey; + public final String descKey; + + public EnchEntry(String id, int level, Component displayName, boolean isCurse, String iconKey, String descKey) { + this.id = id == null ? "" : id; + IdParts parts = IdParts.parse(this.id); + this.namespace = parts.namespace; + this.path = parts.path; + this.level = level; + this.displayName = displayName; + this.displayNameString = displayName.getString(); + this.isCurse = isCurse; + this.iconKey = iconKey; + this.descKey = descKey; + } + + private record IdParts(String namespace, String path) { + private static IdParts parse(String id) { + if (id == null || id.isEmpty()) { + return new IdParts("", ""); + } + int split = id.indexOf(':'); + if (split <= 0 || split >= id.length() - 1) { + return new IdParts("minecraft", id); + } + return new IdParts(id.substring(0, split), id.substring(split + 1)); + } + } +} diff --git a/src/main/java/dev/dekin/iconsenhanced/common/EnchantSortLogic.java b/src/main/java/dev/dekin/iconsenhanced/common/EnchantSortLogic.java new file mode 100644 index 0000000..f5f3bb7 --- /dev/null +++ b/src/main/java/dev/dekin/iconsenhanced/common/EnchantSortLogic.java @@ -0,0 +1,63 @@ +package dev.dekin.iconsenhanced.common; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public final class EnchantSortLogic { + public List sort(List entries, IconsEnhancedConfig.Tooltips tooltips) { + List sorted = new ArrayList<>(entries); + Comparator comparator = buildComparator(tooltips); + sorted.sort(comparator); + return sorted; + } + + private static Comparator buildComparator(IconsEnhancedConfig.Tooltips tooltips) { + Comparator base = switch (SortMode.fromString(tooltips.sortMode)) { + case BY_NAME -> Comparator.comparing(entry -> entry.displayNameString.toLowerCase(Locale.ROOT)); + case BY_LEVEL_DESC -> Comparator.comparingInt(entry -> entry.level).reversed() + .thenComparing(entry -> entry.displayNameString.toLowerCase(Locale.ROOT)); + case CUSTOM_PRIORITY_LIST -> customPriorityComparator(tooltips.customPriorityList); + case BY_VANILLA_LIKE -> Comparator + .comparing((EnchEntry entry) -> entry.namespace) + .thenComparing(entry -> entry.path); + }; + + if (tooltips.cursesLast) { + return Comparator.comparingInt((EnchEntry entry) -> entry.isCurse ? 1 : 0).thenComparing(base); + } + return base; + } + + private static Comparator customPriorityComparator(List priorityList) { + Map priority = new HashMap<>(); + for (int i = 0; i < priorityList.size(); i++) { + priority.put(priorityList.get(i), i); + } + return Comparator + .comparingInt((EnchEntry entry) -> priority.getOrDefault(entry.id, Integer.MAX_VALUE)) + .thenComparing(entry -> entry.displayNameString.toLowerCase(Locale.ROOT)); + } + + public enum SortMode { + BY_VANILLA_LIKE, + BY_NAME, + BY_LEVEL_DESC, + CUSTOM_PRIORITY_LIST; + + public static SortMode fromString(String raw) { + if (raw == null) { + return BY_LEVEL_DESC; + } + for (SortMode value : values()) { + if (value.name().equalsIgnoreCase(raw)) { + return value; + } + } + return BY_LEVEL_DESC; + } + } +} diff --git a/src/main/java/dev/dekin/iconsenhanced/common/IconAtlas.java b/src/main/java/dev/dekin/iconsenhanced/common/IconAtlas.java new file mode 100644 index 0000000..a259e6e --- /dev/null +++ b/src/main/java/dev/dekin/iconsenhanced/common/IconAtlas.java @@ -0,0 +1,69 @@ +package dev.dekin.iconsenhanced.common; + +import java.util.HashMap; +import java.util.Map; + +import dev.dekin.iconsenhanced.IconsEnhanced; +//? if >=1.21 { +import net.minecraft.resources.Identifier; +//?} else { +/*import net.minecraft.resources.ResourceLocation; +*///?} + +public final class IconAtlas { + //? if >=1.21 { + public static final Identifier ATLAS = Identifier.fromNamespaceAndPath(IconsEnhanced.MOD_ID, "textures/gui/enchant_icons.png"); + //?} else { + /*public static final ResourceLocation ATLAS = new ResourceLocation(IconsEnhanced.MOD_ID, "textures/gui/enchant_icons.png"); + *///?} + public static final int ICON_SOURCE_SIZE = 16; + public static final int ATLAS_COLUMNS = 4; + public static final int ATLAS_SIZE = ICON_SOURCE_SIZE * ATLAS_COLUMNS; + + private static final Map ICONS = new HashMap<>(); + + static { + register("boots", 0); + register("bow", 1); + register("chestplate", 2); + register("crossbow", 3); + register("fishing", 4); + register("helmet", 5); + register("leggings", 6); + register("mace", 7); + register("pickaxe", 8); + register("curse", 9); + register("default", 10); + register("sword", 11); + register("trident", 12); + } + + private IconAtlas() { + } + + public static IconSprite get(String key) { + IconSprite sprite = ICONS.get(key); + if (sprite != null) { + return sprite; + } + return ICONS.get("default"); + } + + private static void register(String key, int index) { + int col = index % ATLAS_COLUMNS; + int row = index / ATLAS_COLUMNS; + ICONS.put(key, new IconSprite(col * ICON_SOURCE_SIZE, row * ICON_SOURCE_SIZE, ICON_SOURCE_SIZE)); + } + + public static final class IconSprite { + public final int u; + public final int v; + public final int size; + + private IconSprite(int u, int v, int size) { + this.u = u; + this.v = v; + this.size = size; + } + } +} diff --git a/src/main/java/dev/dekin/iconsenhanced/common/IconKeyResolver.java b/src/main/java/dev/dekin/iconsenhanced/common/IconKeyResolver.java new file mode 100644 index 0000000..9fb3198 --- /dev/null +++ b/src/main/java/dev/dekin/iconsenhanced/common/IconKeyResolver.java @@ -0,0 +1,89 @@ +package dev.dekin.iconsenhanced.common; + +import java.util.HashMap; +import java.util.Map; + +public final class IconKeyResolver { + public static final class Visual { + public final String iconKey; + public final int color; + + private Visual(String iconKey, int color) { + this.iconKey = iconKey; + this.color = color; + } + } + + private static final Visual DEFAULT = new Visual("default", 0xFCFCFC); + private static final Visual CURSE_DEFAULT = new Visual("curse", 0xA80000); + private static final Map VISUALS = new HashMap<>(); + + static { + register("minecraft:aqua_affinity", "helmet", 0x54FCFC); + register("minecraft:bane_of_arthropods", "sword", 0xA800A8); + register("minecraft:curse_of_binding", "curse", 0xA80000); + register("minecraft:blast_protection", "chestplate", 0x00A800); + register("minecraft:channeling", "trident", 0xFCA800); + register("minecraft:depth_strider", "boots", 0x54FCFC); + register("minecraft:efficiency", "pickaxe", 0x00A800); + register("minecraft:feather_falling", "boots", 0x5454FC); + register("minecraft:fire_aspect", "sword", 0xFC5454); + register("minecraft:fire_protection", "chestplate", 0xFC5454); + register("minecraft:flame", "bow", 0xFC5454); + register("minecraft:fortune", "pickaxe", 0xFC54FC); + register("minecraft:frost_walker", "boots", 0x54FCFC); + register("minecraft:impaling", "trident", 0x54FCFC); + register("minecraft:infinity", "bow", 0xFC54FC); + register("minecraft:knockback", "sword", 0x00A8A8); + register("minecraft:looting", "sword", 0xFC54FC); + register("minecraft:loyalty", "trident", 0x0000A8); + register("minecraft:luck_of_the_sea", "fishing", 0x54FCFC); + register("minecraft:lure", "fishing", 0x54FCFC); + register("minecraft:mending", "default", 0xFCFC54); + register("minecraft:multishot", "crossbow", 0xFC54FC); + register("minecraft:piercing", "crossbow", 0xFCA800); + register("minecraft:power", "bow", 0xFCA800); + register("minecraft:projectile_protection", "chestplate", 0x54FC54); + register("minecraft:protection", "chestplate", 0xFC54FC); + register("minecraft:punch", "bow", 0x54FC54); + register("minecraft:quick_charge", "crossbow", 0x54FC54); + register("minecraft:respiration", "helmet", 0x00A8A8); + register("minecraft:riptide", "trident", 0x00A8A8); + register("minecraft:sharpness", "sword", 0x54FC54); + register("minecraft:silk_touch", "pickaxe", 0x54FCFC); + register("minecraft:smite", "sword", 0xFCA800); + register("minecraft:soul_speed", "boots", 0x00A8A8); + register("minecraft:sweeping_edge", "sword", 0x54FCFC); + register("minecraft:swift_sneak", "leggings", 0x00A8A8); + register("minecraft:thorns", "chestplate", 0x00A800); + register("minecraft:unbreaking", "default", 0xFCFC54); + register("minecraft:curse_of_vanishing", "curse", 0xA80000); + + register("minecraft:breach", "mace", 0xA800A8); + register("minecraft:density", "mace", 0xA80000); + register("minecraft:wind_burst", "mace", 0x54FCFC); + } + + private IconKeyResolver() { + } + + public static Visual resolve(String id, boolean isCurse) { + String key = normalizeId(id); + Visual visual = VISUALS.get(key); + if (visual != null) { + return visual; + } + if (isCurse) { + return CURSE_DEFAULT; + } + return DEFAULT; + } + + private static void register(String id, String iconKey, int color) { + VISUALS.put(id, new Visual(iconKey, color)); + } + + private static String normalizeId(String id) { + return id == null ? "" : id; + } +} diff --git a/src/main/java/dev/dekin/iconsenhanced/common/IconsEnhancedConfig.java b/src/main/java/dev/dekin/iconsenhanced/common/IconsEnhancedConfig.java new file mode 100644 index 0000000..c7c24cb --- /dev/null +++ b/src/main/java/dev/dekin/iconsenhanced/common/IconsEnhancedConfig.java @@ -0,0 +1,164 @@ +package dev.dekin.iconsenhanced.common; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public final class IconsEnhancedConfig { + public Borders borders = new Borders(); + public Tooltips tooltips = new Tooltips(); + + public boolean applyDefaults() { + boolean changed = false; + if (borders == null) { + borders = new Borders(); + changed = true; + } else { + changed |= borders.applyDefaults(); + } + if (tooltips == null) { + tooltips = new Tooltips(); + changed = true; + } else { + changed |= tooltips.applyDefaults(); + } + return changed; + } + + public static final class Borders { + public boolean enabled = true; + public int thickness = 1; + public int alpha = 200; + public String mode = "RARITY"; + public Map rarityColors = defaultRarityColors(); + public EnchantedOverride enchantedOverride = new EnchantedOverride(); + public boolean useCustomRules = false; + public List customRules = new ArrayList<>(); + + private boolean applyDefaults() { + boolean changed = false; + if (mode == null || mode.isBlank()) { + mode = "RARITY"; + changed = true; + } + if (rarityColors == null) { + rarityColors = defaultRarityColors(); + changed = true; + } else { + changed |= ensureRarityKey("common", "0xFFAAAAAA"); + changed |= ensureRarityKey("uncommon", "0xFF55FF55"); + changed |= ensureRarityKey("rare", "0xFF5555FF"); + changed |= ensureRarityKey("epic", "0xFFFF55FF"); + } + if (enchantedOverride == null) { + enchantedOverride = new EnchantedOverride(); + changed = true; + } + if (customRules == null) { + customRules = new ArrayList<>(); + changed = true; + } + return changed; + } + + private boolean ensureRarityKey(String key, String value) { + if (!rarityColors.containsKey(key)) { + rarityColors.put(key, value); + return true; + } + return false; + } + + private static Map defaultRarityColors() { + Map defaults = new LinkedHashMap<>(); + defaults.put("common", "0xFFAAAAAA"); + defaults.put("uncommon", "0xFF55FF55"); + defaults.put("rare", "0xFF5555FF"); + defaults.put("epic", "0xFFFF55FF"); + return defaults; + } + } + + public static final class EnchantedOverride { + public boolean enabled = true; + public String color = "0xFFFFD700"; + } + + public static final class CustomBorderRule { + public String itemId = ""; + public String color = "0xFFFFFFFF"; + } + + public static final class Tooltips { + public boolean enabled = true; + public boolean reorderEnchantments = true; + public String sortMode = "CUSTOM_PRIORITY_LIST"; + public boolean cursesLast = true; + public boolean showDescriptions = true; + public boolean showIcons = true; + public int iconSize = 10; + public boolean showLevelBadge = true; + public List customPriorityList = defaultCustomPriorityList(); + + private boolean applyDefaults() { + boolean changed = false; + if (sortMode == null || sortMode.isBlank()) { + sortMode = "CUSTOM_PRIORITY_LIST"; + changed = true; + } + if (customPriorityList == null || customPriorityList.isEmpty()) { + customPriorityList = defaultCustomPriorityList(); + changed = true; + } + return changed; + } + + private static List defaultCustomPriorityList() { + List defaults = new ArrayList<>(); + defaults.add("minecraft:bane_of_arthropods"); + defaults.add("minecraft:density"); + defaults.add("minecraft:efficiency"); + defaults.add("minecraft:impaling"); + defaults.add("minecraft:power"); + defaults.add("minecraft:sharpness"); + defaults.add("minecraft:smite"); + defaults.add("minecraft:blast_protection"); + defaults.add("minecraft:breach"); + defaults.add("minecraft:feather_falling"); + defaults.add("minecraft:fire_protection"); + defaults.add("minecraft:piercing"); + defaults.add("minecraft:projectile_protection"); + defaults.add("minecraft:protection"); + defaults.add("minecraft:depth_strider"); + defaults.add("minecraft:fortune"); + defaults.add("minecraft:looting"); + defaults.add("minecraft:loyalty"); + defaults.add("minecraft:luck_of_the_sea"); + defaults.add("minecraft:lure"); + defaults.add("minecraft:quick_charge"); + defaults.add("minecraft:respiration"); + defaults.add("minecraft:riptide"); + defaults.add("minecraft:soul_speed"); + defaults.add("minecraft:sweeping_edge"); + defaults.add("minecraft:swift_sneak"); + defaults.add("minecraft:thorns"); + defaults.add("minecraft:unbreaking"); + defaults.add("minecraft:wind_burst"); + defaults.add("minecraft:fire_aspect"); + defaults.add("minecraft:frost_walker"); + defaults.add("minecraft:knockback"); + defaults.add("minecraft:punch"); + defaults.add("minecraft:aqua_affinity"); + defaults.add("minecraft:channeling"); + defaults.add("minecraft:curse_of_binding"); + defaults.add("minecraft:curse_of_vanishing"); + defaults.add("minecraft:flame"); + defaults.add("minecraft:infinity"); + defaults.add("minecraft:mending"); + defaults.add("minecraft:multishot"); + defaults.add("minecraft:silk_touch"); + return defaults; + } + } +} diff --git a/src/main/java/dev/dekin/iconsenhanced/common/TooltipLayoutModel.java b/src/main/java/dev/dekin/iconsenhanced/common/TooltipLayoutModel.java new file mode 100644 index 0000000..7d9e2cf --- /dev/null +++ b/src/main/java/dev/dekin/iconsenhanced/common/TooltipLayoutModel.java @@ -0,0 +1,33 @@ +package dev.dekin.iconsenhanced.common; + +import java.util.List; + +import net.minecraft.network.chat.Component; + +public final class TooltipLayoutModel { + private final List rows; + + public TooltipLayoutModel(List rows) { + this.rows = rows; + } + + public List rows() { + return rows; + } + + public static final class Row { + public final Component text; + public final List descriptions; + public final String iconKey; + public final int level; + public final boolean isCurse; + + public Row(Component text, List descriptions, String iconKey, int level, boolean isCurse) { + this.text = text; + this.descriptions = descriptions; + this.iconKey = iconKey; + this.level = level; + this.isCurse = isCurse; + } + } +} diff --git a/src/main/resources/assets/iconsenhanced/icon.png b/src/main/resources/assets/iconsenhanced/icon.png new file mode 100644 index 0000000..4f2fe2b Binary files /dev/null and b/src/main/resources/assets/iconsenhanced/icon.png differ diff --git a/src/main/resources/assets/iconsenhanced/lang/en_us.json b/src/main/resources/assets/iconsenhanced/lang/en_us.json new file mode 100644 index 0000000..db99b81 --- /dev/null +++ b/src/main/resources/assets/iconsenhanced/lang/en_us.json @@ -0,0 +1,11 @@ +{ + "iconsenhanced.desc.minecraft.sharpness": "Increases melee damage.", + "iconsenhanced.desc.minecraft.protection": "Reduces incoming damage.", + "iconsenhanced.desc.minecraft.unbreaking": "Items lose durability slower.", + "iconsenhanced.desc.minecraft.mending": "Repairs items with experience.", + "iconsenhanced.desc.minecraft.efficiency": "Increases mining speed.", + "iconsenhanced.desc.minecraft.power": "Increases bow damage.", + "iconsenhanced.desc.minecraft.thorns": "Reflects damage to attackers.", + "iconsenhanced.desc.minecraft.binding_curse": "Cursed: stays equipped.", + "iconsenhanced.desc.minecraft.vanishing_curse": "Cursed: disappears on death." +} diff --git a/src/main/resources/assets/iconsenhanced/lang/es_es.json b/src/main/resources/assets/iconsenhanced/lang/es_es.json new file mode 100644 index 0000000..6812a6a --- /dev/null +++ b/src/main/resources/assets/iconsenhanced/lang/es_es.json @@ -0,0 +1,11 @@ +{ + "iconsenhanced.desc.minecraft.sharpness": "Aumenta el dano cuerpo a cuerpo.", + "iconsenhanced.desc.minecraft.protection": "Reduce el dano recibido.", + "iconsenhanced.desc.minecraft.unbreaking": "Los objetos pierden durabilidad mas lento.", + "iconsenhanced.desc.minecraft.mending": "Repara objetos con experiencia.", + "iconsenhanced.desc.minecraft.efficiency": "Aumenta la velocidad de mineria.", + "iconsenhanced.desc.minecraft.power": "Aumenta el dano del arco.", + "iconsenhanced.desc.minecraft.thorns": "Refleja dano a atacantes.", + "iconsenhanced.desc.minecraft.binding_curse": "Maldito: queda equipado.", + "iconsenhanced.desc.minecraft.vanishing_curse": "Maldito: desaparece al morir." +} diff --git a/src/main/resources/assets/iconsenhanced/textures/gui/enchant_icons.png b/src/main/resources/assets/iconsenhanced/textures/gui/enchant_icons.png new file mode 100644 index 0000000..e7e89d3 Binary files /dev/null and b/src/main/resources/assets/iconsenhanced/textures/gui/enchant_icons.png differ diff --git a/src/main/resources/assets/template-mod/icon.png b/src/main/resources/assets/template-mod/icon.png deleted file mode 100644 index 66db08f..0000000 Binary files a/src/main/resources/assets/template-mod/icon.png and /dev/null differ diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index cc5139b..867dd77 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -1,38 +1,26 @@ { - "schemaVersion": 1, - "id": "template-mod", - "version": "${version}", - "name": "Template Mod", - "description": "This is an example description! Tell everyone what your mod is about!", - "authors": [ - "Me!" - ], - "contact": { - "homepage": "https://fabricmc.net/", - "sources": "https://github.com/FabricMC/fabric-example-mod" - }, - "license": "CC0-1.0", - "icon": "assets/template-mod/icon.png", - "environment": "*", - "entrypoints": { - "main": [ - "com.straice.TemplateMod" - ], - "client": [ - "com.straice.TemplateModClient" - ] - }, - "mixins": [ - "template-mod.mixins.json", - { - "config": "template-mod.client.mixins.json", - "environment": "client" - } - ], - "depends": { - "fabricloader": ">=0.18.4", - "minecraft": "~1.21.11", - "java": ">=21", - "fabric-api": "*" - } -} \ No newline at end of file + "schemaVersion": 1, + "id": "${mod_id}", + "version": "${version}", + "name": "${mod_name}", + "description": "${mod_description}", + "authors": [ + "${mod_author}" + ], + "license": "${mod_license}", + "environment": "client", + "entrypoints": { + "client": [ + "dev.dekin.iconsenhanced.client.ClientInit" + ] + }, + "mixins": [ + "iconsenhanced.mixins.json" + ], + "depends": { + "fabricloader": ">=${fabric_loader_version}", + "fabric": "*", + "minecraft": "${minecraft_version}" + }, + "icon": "assets/${mod_id}/icon.png" +} diff --git a/src/main/resources/iconsenhanced.mixins.json b/src/main/resources/iconsenhanced.mixins.json new file mode 100644 index 0000000..cd00aac --- /dev/null +++ b/src/main/resources/iconsenhanced.mixins.json @@ -0,0 +1,11 @@ +{ + "required": true, + "package": "dev.dekin.iconsenhanced.mixin", + "compatibilityLevel": "JAVA_17", + "client": [ + "AbstractContainerScreenAccessor" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/src/main/resources/template-mod.mixins.json b/src/main/resources/template-mod.mixins.json deleted file mode 100644 index 63ee472..0000000 --- a/src/main/resources/template-mod.mixins.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "required": true, - "package": "com.straice.mixin", - "compatibilityLevel": "JAVA_21", - "mixins": [ - "ExampleMixin" - ], - "injectors": { - "defaultRequire": 1 - }, - "overwrites": { - "requireAnnotations": true - } -} \ No newline at end of file diff --git a/stonecutter.gradle b/stonecutter.gradle new file mode 100644 index 0000000..42f43a1 --- /dev/null +++ b/stonecutter.gradle @@ -0,0 +1,15 @@ +plugins { + id 'dev.kikugie.stonecutter' +} + +stonecutter.active "1.21.11" + +tasks.register("chiseledBuild") { + group = "build" + dependsOn(stonecutter.tree.nodes.collect { it.project.tasks.named("build") }) +} + +tasks.register("chiseledClean") { + group = "build" + dependsOn(stonecutter.tree.nodes.collect { it.project.tasks.named("clean") }) +} diff --git a/versions/1.20.1/gradle.properties b/versions/1.20.1/gradle.properties new file mode 100644 index 0000000..13a3076 --- /dev/null +++ b/versions/1.20.1/gradle.properties @@ -0,0 +1,7 @@ +# Fabric dependencies + +deps.fabric_loader=0.18.4 +deps.fabric_api=0.92.6+1.20.1 + +# Java +java_version=17 diff --git a/versions/1.20.4/gradle.properties b/versions/1.20.4/gradle.properties new file mode 100644 index 0000000..34b45a4 --- /dev/null +++ b/versions/1.20.4/gradle.properties @@ -0,0 +1,7 @@ +# Fabric dependencies + +deps.fabric_loader=0.18.4 +deps.fabric_api=0.97.3+1.20.4 + +# Java +java_version=17 diff --git a/versions/1.20.6/gradle.properties b/versions/1.20.6/gradle.properties new file mode 100644 index 0000000..3cab226 --- /dev/null +++ b/versions/1.20.6/gradle.properties @@ -0,0 +1,7 @@ +# Fabric dependencies + +deps.fabric_loader=0.18.4 +deps.fabric_api=0.99.4+1.20.6 + +# Java +java_version=21 diff --git a/versions/1.21.1/gradle.properties b/versions/1.21.1/gradle.properties new file mode 100644 index 0000000..3696d6e --- /dev/null +++ b/versions/1.21.1/gradle.properties @@ -0,0 +1,7 @@ +# Fabric dependencies + +deps.fabric_loader=0.18.4 +deps.fabric_api=0.116.7+1.21.1 + +# Java +java_version=21 diff --git a/versions/1.21.11/gradle.properties b/versions/1.21.11/gradle.properties new file mode 100644 index 0000000..4ae4286 --- /dev/null +++ b/versions/1.21.11/gradle.properties @@ -0,0 +1,7 @@ +# Fabric dependencies + +deps.fabric_loader=0.18.4 +deps.fabric_api=0.141.1+1.21.11 + +# Java +java_version=21 diff --git a/versions/1.21.4/gradle.properties b/versions/1.21.4/gradle.properties new file mode 100644 index 0000000..f4157d3 --- /dev/null +++ b/versions/1.21.4/gradle.properties @@ -0,0 +1,7 @@ +# Fabric dependencies + +deps.fabric_loader=0.18.4 +deps.fabric_api=0.119.4+1.21.4 + +# Java +java_version=21