diff --git a/dependencies.gradle b/dependencies.gradle index 03e1f5dc0..17e9f81fb 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -99,6 +99,10 @@ dependencies { compileOnly(rfg.deobf("curse.maven:xaeros-minimap-263420:5060684")) compileOnly(rfg.deobf("curse.maven:security-craft-64760:2818228")) + compileOnly(rfg.deobf("curse.maven:hex-text-1379870:7197368")) { transitive = false } + // For Testing Only -- Hex Text + // runtimeOnlyNonPublishable(rfg.deobf("curse.maven:hex-text-1379870:7197368")) { transitive = false } + runtimeOnlyNonPublishable(rfg.deobf("CoreTweaks:CoreTweaks:0.3.3.2")) // For testing alternative splash screens, can be sourced from https://github.com/MalTeeez/ModernSplash/releases //runtimeOnly(rfg.deobf(files("dependencies/modernsplash-1.7.10-1.2.2-dev.jar"))) diff --git a/src/main/java/com/gtnewhorizons/angelica/AngelicaMod.java b/src/main/java/com/gtnewhorizons/angelica/AngelicaMod.java index 92e6384cc..ce1ee8c28 100644 --- a/src/main/java/com/gtnewhorizons/angelica/AngelicaMod.java +++ b/src/main/java/com/gtnewhorizons/angelica/AngelicaMod.java @@ -48,6 +48,7 @@ public void preInit(FMLPreInitializationEvent event) { @Mod.EventHandler public void init(FMLInitializationEvent event) { + ModStatus.init(); proxy.init(event); } diff --git a/src/main/java/com/gtnewhorizons/angelica/client/font/BatchingFontRenderer.java b/src/main/java/com/gtnewhorizons/angelica/client/font/BatchingFontRenderer.java index 6fed6313b..be20083bc 100644 --- a/src/main/java/com/gtnewhorizons/angelica/client/font/BatchingFontRenderer.java +++ b/src/main/java/com/gtnewhorizons/angelica/client/font/BatchingFontRenderer.java @@ -1,13 +1,20 @@ package com.gtnewhorizons.angelica.client.font; import com.google.common.collect.ImmutableSet; +import com.gtnewhorizons.angelica.client.font.color.AngelicaColorResolver; +import com.gtnewhorizons.angelica.client.font.color.AngelicaColorResolvers; +import com.gtnewhorizons.angelica.client.font.color.ResolvedText; +import com.gtnewhorizons.angelica.compat.hextext.HexTextCompat; +import com.gtnewhorizons.angelica.compat.hextext.HexTextCompat.EffectsHelper; +import com.gtnewhorizons.angelica.compat.hextext.HexTextCompat.Highlighter; +import com.gtnewhorizons.angelica.compat.hextext.HexTextServices; import com.gtnewhorizons.angelica.config.FontConfig; import com.gtnewhorizons.angelica.glsm.GLStateManager; import com.gtnewhorizons.angelica.mixins.interfaces.FontRendererAccessor; -import cpw.mods.fml.client.SplashProgress; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import net.coderbot.iris.gl.program.Program; import net.coderbot.iris.gl.program.ProgramBuilder; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; import net.minecraft.util.MathHelper; import net.minecraft.util.ResourceLocation; @@ -24,6 +31,7 @@ import java.util.Comparator; import java.util.Objects; + import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.*; /** @@ -44,6 +52,7 @@ public class BatchingFontRenderer { * drop shadows. */ private int[] colorCode; + private final AngelicaColorResolver colorResolver; /** Location of the primary font atlas to bind. */ protected final ResourceLocation locationFontTexture; @@ -82,6 +91,7 @@ public BatchingFontRenderer(FontRenderer underlying, int[] charWidth, byte[] gly this.glyphWidth = glyphWidth; this.colorCode = colorCode; this.locationFontTexture = locationFontTexture; + this.colorResolver = AngelicaColorResolvers.create(this, this.colorCode); for (int i = 0; i < 64; i++) { batchCommandPool.add(new FontDrawCmd()); @@ -390,7 +400,7 @@ public float getShadowOffset() { return forceDefaults() ? 1 : FontConfig.fontShadowOffset; } - private static final char FORMATTING_CHAR = 167; // § + public static final char FORMATTING_CHAR = 167; // § public float drawString(final float anchorX, final float anchorY, final int color, final boolean enableShadow, final boolean unicodeFlag, final CharSequence string, int stringOffset, int stringLength) { @@ -414,14 +424,6 @@ public float drawString(final float anchorX, final float anchorY, final int colo } final int stringEnd = stringOffset + stringLength; - int curColor = color; - int curShadowColor = shadowColor; - boolean curItalic = false; - boolean curRandom = false; - boolean curBold = false; - boolean curStrikethrough = false; - boolean curUnderline = false; - final float glyphScaleY = getGlyphScaleY(); final float heightNorth = anchorY + (underlying.FONT_HEIGHT - 1.0f) * (0.5f - glyphScaleY / 2); final float heightSouth = (underlying.FONT_HEIGHT - 1.0f) * glyphScaleY; @@ -433,69 +435,113 @@ public float drawString(final float anchorX, final float anchorY, final int colo float strikethroughStartX = 0.0f; float strikethroughEndX = 0.0f; - for (int charIdx = stringOffset; charIdx < stringEnd; charIdx++) { - char chr = string.charAt(charIdx); - if (chr == FORMATTING_CHAR && (charIdx + 1) < stringEnd) { - final char fmtCode = Character.toLowerCase(string.charAt(charIdx + 1)); - charIdx++; - - if (curUnderline && underlineStartX != underlineEndX) { - final int ulIdx = idxWriterIndex; - pushUntexRect(underlineStartX, underlineY, underlineEndX - underlineStartX, glyphScaleY, curColor); - pushDrawCmd(ulIdx, 6, null, false); - underlineStartX = underlineEndX; - } - if (curStrikethrough && strikethroughStartX != strikethroughEndX) { - final int ulIdx = idxWriterIndex; - pushUntexRect( - strikethroughStartX, - strikethroughY, - strikethroughEndX - strikethroughStartX, - glyphScaleY, - curColor); - pushDrawCmd(ulIdx, 6, null, false); - strikethroughStartX = strikethroughEndX; + final ResolvedText resolved = colorResolver.resolve(string, stringOffset, stringEnd, color, shadowColor); + + Highlighter highlighter = Highlighter.NOOP; + CharSequence highlightText = null; + boolean highlightActive = false; + EffectsHelper effectsHelper = null; + boolean hexTextCode = false; + long effectsTimestamp = 0L; + int visibleGlyphIndex = 0; + + // Hex Text Compatibility Section + boolean hexTextCompatActive = FontConfig.enableHexTextCompat && HexTextServices.isSupported(); + if (hexTextCompatActive) { + highlighter = HexTextCompat.createHighlighter(); + if (highlighter != Highlighter.NOOP) { + highlightText = resolved.asString(); + highlightActive = highlighter.begin(underlying, highlightText, anchorX, anchorY); + } + + if (resolved.hasDynamicEffects()) { + effectsHelper = HexTextCompat.getEffectsHelper(); + if (effectsHelper != null && effectsHelper.isOperational()) { + hexTextCode = true; + effectsTimestamp = Minecraft.getSystemTime(); } + } + } + + int lastColor = color; + boolean lastUnderline = false; + boolean lastStrikethrough = false; - final boolean is09 = charInRange(fmtCode, '0', '9'); - final boolean isAF = charInRange(fmtCode, 'a', 'f'); - if (is09 || isAF) { - curRandom = false; - curBold = false; - curStrikethrough = false; - curUnderline = false; - curItalic = false; - - final int colorIdx = is09 ? (fmtCode - '0') : (fmtCode - 'a' + 10); - final int rgb = this.colorCode[colorIdx]; - curColor = (curColor & 0xFF000000) | (rgb & 0x00FFFFFF); - final int shadowRgb = this.colorCode[colorIdx + 16]; - curShadowColor = (curShadowColor & 0xFF000000) | (shadowRgb & 0x00FFFFFF); - } else if (fmtCode == 'k') { - curRandom = true; - } else if (fmtCode == 'l') { - curBold = true; - } else if (fmtCode == 'm') { - curStrikethrough = true; - strikethroughStartX = curX - 1.0f; - strikethroughEndX = strikethroughStartX; - } else if (fmtCode == 'n') { - curUnderline = true; - underlineStartX = curX - 1.0f; - underlineEndX = underlineStartX; - } else if (fmtCode == 'o') { - curItalic = true; - } else if (fmtCode == 'r') { - curRandom = false; - curBold = false; - curStrikethrough = false; - curUnderline = false; - curItalic = false; - curColor = color; - curShadowColor = shadowColor; + for (int entryIndex = 0; entryIndex < resolved.length(); entryIndex++) { + if (highlightActive) { + highlighter.inspect(highlightText, entryIndex, curX); + } + + char chr = resolved.charAt(entryIndex); + final boolean curRandom = resolved.isRandom(entryIndex); + final boolean curBold = resolved.isBold(entryIndex); + final boolean curStrikethrough = resolved.isStrikethrough(entryIndex); + final boolean curUnderline = resolved.isUnderline(entryIndex); + final boolean curItalic = resolved.isItalic(entryIndex); + final int baseColorValue = resolved.colorAt(entryIndex); + final int baseShadowColorValue = resolved.shadowColorAt(entryIndex); + + int glyphColor = baseColorValue; + int glyphShadowColor = baseShadowColorValue; + float shakeOffset = 0.0f; + boolean effectDinnerbone = false; + + if (hexTextCode) { + final boolean rainbowActive = resolved.isRainbow(entryIndex); + final boolean igniteActive = resolved.isIgnite(entryIndex); + final boolean shakeActive = resolved.isShake(entryIndex); + effectDinnerbone = resolved.isDinnerbone(entryIndex); + + if (rainbowActive || igniteActive) { + int effectBase = resolved.effectBaseColorAt(entryIndex) & 0x00FFFFFF; + if (effectBase == 0) { + effectBase = glyphColor & 0x00FFFFFF; + } + int dynamicRgb = effectBase; + if (rainbowActive) { + dynamicRgb = effectsHelper.computeRainbowColor( + effectsTimestamp, + visibleGlyphIndex, + resolved.effectParameterAt(entryIndex) + ) & 0x00FFFFFF; + } + if (igniteActive) { + dynamicRgb = effectsHelper.computeIgniteColor(effectsTimestamp, dynamicRgb) & 0x00FFFFFF; + } + glyphColor = (glyphColor & 0xFF000000) | dynamicRgb; + int shadowRgb = HexTextCompat.computeShadowColor(dynamicRgb) & 0x00FFFFFF; + glyphShadowColor = (glyphShadowColor & 0xFF000000) | shadowRgb; } + if (shakeActive) { + shakeOffset = effectsHelper.computeShakeOffset(effectsTimestamp, visibleGlyphIndex); + } + } - continue; + if (lastUnderline && !curUnderline && underlineStartX != underlineEndX) { + final int ulIdx = idxWriterIndex; + pushUntexRect(underlineStartX, underlineY, underlineEndX - underlineStartX, glyphScaleY, lastColor); + pushDrawCmd(ulIdx, 6, null, false); + underlineStartX = underlineEndX; + } + if (lastStrikethrough && !curStrikethrough && strikethroughStartX != strikethroughEndX) { + final int ulIdx = idxWriterIndex; + pushUntexRect( + strikethroughStartX, + strikethroughY, + strikethroughEndX - strikethroughStartX, + glyphScaleY, + lastColor); + pushDrawCmd(ulIdx, 6, null, false); + strikethroughStartX = strikethroughEndX; + } + + if (curUnderline && !lastUnderline) { + underlineStartX = curX - 1.0f; + underlineEndX = underlineStartX; + } + if (curStrikethrough && !lastStrikethrough) { + strikethroughStartX = curX - 1.0f; + strikethroughEndX = strikethroughStartX; } if (curRandom) { @@ -507,6 +553,15 @@ public float drawString(final float anchorX, final float anchorY, final int colo // Check ASCII space, NBSP, NNBSP if (chr == ' ' || chr == '\u00A0' || chr == '\u202F') { curX += 4 * this.getWhitespaceScale(); + lastUnderline = curUnderline; + lastStrikethrough = curStrikethrough; + lastColor = glyphColor; + underlineEndX = curX; + strikethroughEndX = curX; + visibleGlyphIndex++; + if (highlightActive) { + highlighter.advance(entryIndex + 1, curX); + } continue; } @@ -525,37 +580,73 @@ public float drawString(final float anchorX, final float anchorY, final int colo int vtxCount = 0; + final float glyphHeight = Math.max(1.0f, heightSouth); + final float baselineOffset = Math.max(0.0f, underlying.FONT_HEIGHT - glyphHeight); + final float pivotY = (heightNorth + shakeOffset) + glyphHeight * 0.5f; + if (enableShadow) { - pushVtx(curX + itOff + shadowOffset, heightNorth + shadowOffset, curShadowColor, uStart, vStart, uStart, uStart + uSz, vStart, vStart + vSz); - pushVtx(curX - itOff + shadowOffset, heightNorth + heightSouth + shadowOffset, curShadowColor, uStart, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz); - pushVtx(curX + glyphW - 1.0F + itOff + shadowOffset, heightNorth + shadowOffset, curShadowColor, uStart + uSz, vStart, uStart, uStart + uSz, vStart, vStart + vSz); - pushVtx(curX + glyphW - 1.0F - itOff + shadowOffset, heightNorth + heightSouth + shadowOffset, curShadowColor, uStart + uSz, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz); + pushVtx(curX + itOff + shadowOffset, + applyVerticalEffects(heightNorth + shadowOffset, shakeOffset, effectDinnerbone, pivotY, baselineOffset), + glyphShadowColor, uStart, vStart, uStart, uStart + uSz, vStart, vStart + vSz); + pushVtx(curX - itOff + shadowOffset, + applyVerticalEffects(heightNorth + heightSouth + shadowOffset, shakeOffset, effectDinnerbone, pivotY, baselineOffset), + glyphShadowColor, uStart, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz); + pushVtx(curX + glyphW - 1.0F + itOff + shadowOffset, + applyVerticalEffects(heightNorth + shadowOffset, shakeOffset, effectDinnerbone, pivotY, baselineOffset), + glyphShadowColor, uStart + uSz, vStart, uStart, uStart + uSz, vStart, vStart + vSz); + pushVtx(curX + glyphW - 1.0F - itOff + shadowOffset, + applyVerticalEffects(heightNorth + heightSouth + shadowOffset, shakeOffset, effectDinnerbone, pivotY, baselineOffset), + glyphShadowColor, uStart + uSz, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz); pushQuadIdx(vtxId + vtxCount); vtxCount += 4; if (curBold) { final float shadowOffset2 = 2.0f * shadowOffset; - pushVtx(curX + itOff + shadowOffset2, heightNorth + shadowOffset, curShadowColor, uStart, vStart, uStart, uStart + uSz, vStart, vStart + vSz); - pushVtx(curX - itOff + shadowOffset2, heightNorth + heightSouth + shadowOffset, curShadowColor, uStart, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz); - pushVtx(curX + glyphW - 1.0F + itOff + shadowOffset2, heightNorth + shadowOffset, curShadowColor, uStart + uSz, vStart, uStart, uStart + uSz, vStart, vStart + vSz); - pushVtx(curX + glyphW - 1.0F - itOff + shadowOffset2, heightNorth + heightSouth + shadowOffset, curShadowColor, uStart + uSz, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz); + pushVtx(curX + itOff + shadowOffset2, + applyVerticalEffects(heightNorth + shadowOffset, shakeOffset, effectDinnerbone, pivotY, baselineOffset), + glyphShadowColor, uStart, vStart, uStart, uStart + uSz, vStart, vStart + vSz); + pushVtx(curX - itOff + shadowOffset2, + applyVerticalEffects(heightNorth + heightSouth + shadowOffset, shakeOffset, effectDinnerbone, pivotY, baselineOffset), + glyphShadowColor, uStart, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz); + pushVtx(curX + glyphW - 1.0F + itOff + shadowOffset2, + applyVerticalEffects(heightNorth + shadowOffset, shakeOffset, effectDinnerbone, pivotY, baselineOffset), + glyphShadowColor, uStart + uSz, vStart, uStart, uStart + uSz, vStart, vStart + vSz); + pushVtx(curX + glyphW - 1.0F - itOff + shadowOffset2, + applyVerticalEffects(heightNorth + heightSouth + shadowOffset, shakeOffset, effectDinnerbone, pivotY, baselineOffset), + glyphShadowColor, uStart + uSz, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz); pushQuadIdx(vtxId + vtxCount); vtxCount += 4; } } - pushVtx(curX + itOff, heightNorth, curColor, uStart, vStart, uStart, uStart + uSz, vStart, vStart + vSz); - pushVtx(curX - itOff, heightNorth + heightSouth, curColor, uStart, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz); - pushVtx(curX + glyphW - 1.0F + itOff, heightNorth, curColor, uStart + uSz, vStart, uStart, uStart + uSz, vStart, vStart + vSz); - pushVtx(curX + glyphW - 1.0F - itOff, heightNorth + heightSouth, curColor, uStart + uSz, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz); + pushVtx(curX + itOff, + applyVerticalEffects(heightNorth, shakeOffset, effectDinnerbone, pivotY, baselineOffset), + glyphColor, uStart, vStart, uStart, uStart + uSz, vStart, vStart + vSz); + pushVtx(curX - itOff, + applyVerticalEffects(heightNorth + heightSouth, shakeOffset, effectDinnerbone, pivotY, baselineOffset), + glyphColor, uStart, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz); + pushVtx(curX + glyphW - 1.0F + itOff, + applyVerticalEffects(heightNorth, shakeOffset, effectDinnerbone, pivotY, baselineOffset), + glyphColor, uStart + uSz, vStart, uStart, uStart + uSz, vStart, vStart + vSz); + pushVtx(curX + glyphW - 1.0F - itOff, + applyVerticalEffects(heightNorth + heightSouth, shakeOffset, effectDinnerbone, pivotY, baselineOffset), + glyphColor, uStart + uSz, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz); pushQuadIdx(vtxId + vtxCount); vtxCount += 4; if (curBold) { - pushVtx(shadowOffset + curX + itOff, heightNorth, curColor, uStart, vStart, uStart, uStart + uSz, vStart, vStart + vSz); - pushVtx(shadowOffset + curX - itOff, heightNorth + heightSouth, curColor, uStart, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz); - pushVtx(shadowOffset + curX + glyphW - 1.0F + itOff, heightNorth, curColor, uStart + uSz, vStart, uStart, uStart + uSz, vStart, vStart + vSz); - pushVtx(shadowOffset + curX + glyphW - 1.0F - itOff, heightNorth + heightSouth, curColor, uStart + uSz, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz); + pushVtx(shadowOffset + curX + itOff, + applyVerticalEffects(heightNorth, shakeOffset, effectDinnerbone, pivotY, baselineOffset), + glyphColor, uStart, vStart, uStart, uStart + uSz, vStart, vStart + vSz); + pushVtx(shadowOffset + curX - itOff, + applyVerticalEffects(heightNorth + heightSouth, shakeOffset, effectDinnerbone, pivotY, baselineOffset), + glyphColor, uStart, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz); + pushVtx(shadowOffset + curX + glyphW - 1.0F + itOff, + applyVerticalEffects(heightNorth, shakeOffset, effectDinnerbone, pivotY, baselineOffset), + glyphColor, uStart + uSz, vStart, uStart, uStart + uSz, vStart, vStart + vSz); + pushVtx(shadowOffset + curX + glyphW - 1.0F - itOff, + applyVerticalEffects(heightNorth + heightSouth, shakeOffset, effectDinnerbone, pivotY, baselineOffset), + glyphColor, uStart + uSz, vStart + vSz, uStart, uStart + uSz, vStart, vStart + vSz); pushQuadIdx(vtxId + vtxCount); vtxCount += 4; } @@ -564,24 +655,47 @@ public float drawString(final float anchorX, final float anchorY, final int colo curX += (xAdvance + (curBold ? shadowOffset : 0.0f)) + getGlyphSpacing(); underlineEndX = curX; strikethroughEndX = curX; + + lastUnderline = curUnderline; + lastStrikethrough = curStrikethrough; + lastColor = glyphColor; + visibleGlyphIndex++; + + if (highlightActive) { + highlighter.advance(entryIndex + 1, curX); + } } - if (curUnderline && underlineStartX != underlineEndX) { + if (lastUnderline && underlineStartX != underlineEndX) { final int ulIdx = idxWriterIndex; - pushUntexRect(underlineStartX, underlineY, underlineEndX - underlineStartX, glyphScaleY, curColor); + pushUntexRect(underlineStartX, underlineY, underlineEndX - underlineStartX, glyphScaleY, lastColor); pushDrawCmd(ulIdx, 6, null, false); } - if (curStrikethrough && strikethroughStartX != strikethroughEndX) { + if (lastStrikethrough && strikethroughStartX != strikethroughEndX) { final int ulIdx = idxWriterIndex; pushUntexRect( strikethroughStartX, strikethroughY, strikethroughEndX - strikethroughStartX, glyphScaleY, - curColor); + lastColor); pushDrawCmd(ulIdx, 6, null, false); } + if (highlightActive) { + highlighter.finish(resolved.length(), curX); + for (HexTextCompat.Highlight highlight : highlighter.highlights()) { + if (highlight.getWidth() <= 0.0f) { + continue; + } + final int hlIdx = idxWriterIndex; + float top = highlight.getY() - 1.0f; + float bottom = highlight.getY() + underlying.FONT_HEIGHT; + pushUntexRect(highlight.getX(), top, highlight.getWidth(), bottom - top, highlight.getColor()); + pushDrawCmd(hlIdx, 6, null, false); + } + } + } finally { this.endBatch(); } @@ -609,4 +723,13 @@ public void resetBlendFunc() { blendSrcRGB = GL11.GL_SRC_ALPHA; blendDstRGB = GL11.GL_ONE_MINUS_SRC_ALPHA; } + + private static float applyVerticalEffects(float originalY, float shakeOffset, boolean dinnerbone, float pivotY, + float baselineOffset) { + float adjusted = originalY + shakeOffset; + if (dinnerbone) { + adjusted = -adjusted + 2.0f * pivotY - baselineOffset; + } + return adjusted; + } } diff --git a/src/main/java/com/gtnewhorizons/angelica/client/font/color/AngelicaColorResolver.java b/src/main/java/com/gtnewhorizons/angelica/client/font/color/AngelicaColorResolver.java new file mode 100644 index 000000000..4013dda11 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/client/font/color/AngelicaColorResolver.java @@ -0,0 +1,10 @@ +package com.gtnewhorizons.angelica.client.font.color; + +/** + * Parses formatting markers into a {@link ResolvedText} description that the + * batching font renderer can consume. + */ +public interface AngelicaColorResolver { + + ResolvedText resolve(CharSequence text, int start, int end, int baseColor, int baseShadowColor); +} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/font/color/AngelicaColorResolvers.java b/src/main/java/com/gtnewhorizons/angelica/client/font/color/AngelicaColorResolvers.java new file mode 100644 index 000000000..55cca0714 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/client/font/color/AngelicaColorResolvers.java @@ -0,0 +1,60 @@ +package com.gtnewhorizons.angelica.client.font.color; + +import com.gtnewhorizons.angelica.client.font.BatchingFontRenderer; +import com.gtnewhorizons.angelica.compat.hextext.HexTextCompat; +import com.gtnewhorizons.angelica.compat.hextext.HexTextCompat.Bridge; +import com.gtnewhorizons.angelica.compat.hextext.HexTextServices; +import com.gtnewhorizons.angelica.config.FontConfig; + +public final class AngelicaColorResolvers { + + private AngelicaColorResolvers() { + } + + public static AngelicaColorResolver create(BatchingFontRenderer renderer, int[] vanillaPalette) { + AngelicaColorResolver fallback = new DefaultColorResolver(vanillaPalette); + if (!FontConfig.enableHexTextCompat) { + return fallback; + } + + return new SwitchingColorResolver(vanillaPalette, fallback); + } + + private static final class SwitchingColorResolver implements AngelicaColorResolver { + + private final int[] vanillaPalette; + private final AngelicaColorResolver fallback; + + private volatile HexTextColorResolver hexResolver; + + private SwitchingColorResolver(int[] vanillaPalette, AngelicaColorResolver fallback) { + this.vanillaPalette = vanillaPalette; + this.fallback = fallback; + } + + @Override + public ResolvedText resolve(CharSequence text, int start, int end, int baseColor, int baseShadowColor) { + if (!FontConfig.enableHexTextCompat) { + hexResolver = null; + return fallback.resolve(text, start, end, baseColor, baseShadowColor); + } + + if (HexTextServices.isSupported()) { + HexTextColorResolver resolver = hexResolver; + if (resolver == null) { + Bridge bridge = HexTextCompat.tryCreateBridge(); + if (bridge != null) { + resolver = new HexTextColorResolver(vanillaPalette, bridge); + hexResolver = resolver; + } else { + return fallback.resolve(text, start, end, baseColor, baseShadowColor); + } + } + return resolver.resolve(text, start, end, baseColor, baseShadowColor); + } + + hexResolver = null; + return fallback.resolve(text, start, end, baseColor, baseShadowColor); + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/font/color/DefaultColorResolver.java b/src/main/java/com/gtnewhorizons/angelica/client/font/color/DefaultColorResolver.java new file mode 100644 index 000000000..1ad486f4f --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/client/font/color/DefaultColorResolver.java @@ -0,0 +1,97 @@ +package com.gtnewhorizons.angelica.client.font.color; + +import static com.gtnewhorizons.angelica.client.font.BatchingFontRenderer.FORMATTING_CHAR; + +final class DefaultColorResolver implements AngelicaColorResolver { + + private final int[] vanillaPalette; + + DefaultColorResolver(int[] vanillaPalette) { + this.vanillaPalette = vanillaPalette; + } + + @Override + public ResolvedText resolve(CharSequence text, int start, int end, int baseColor, int baseShadowColor) { + ResolvedText.Builder builder = ResolvedText.builder(end - start); + FormattingState state = new FormattingState(baseColor, baseShadowColor); + + for (int index = Math.max(0, start); index < Math.min(text.length(), end); index++) { + char chr = text.charAt(index); + if (chr == FORMATTING_CHAR && index + 1 < end) { + char fmtCode = Character.toLowerCase(text.charAt(index + 1)); + index++; + applyVanillaFormatting(state, fmtCode); + continue; + } + + if (state.random) { + // keep vanilla behaviour; actual replacement happens during rendering + } + + builder.append(chr, state.color, state.shadowColor, state.bold, state.italic, state.underline, + state.strikethrough, state.random); + } + + return builder.build(); + } + + private void applyVanillaFormatting(FormattingState state, char fmtCode) { + if (charInRange(fmtCode, '0', '9') || charInRange(fmtCode, 'a', 'f')) { + state.random = false; + state.bold = false; + state.strikethrough = false; + state.underline = false; + state.italic = false; + int colorIdx = charInRange(fmtCode, '0', '9') ? (fmtCode - '0') : (fmtCode - 'a' + 10); + int rgb = vanillaPalette[colorIdx]; + state.color = (state.color & 0xFF000000) | (rgb & 0x00FFFFFF); + int shadowRgb = vanillaPalette[colorIdx + 16]; + state.shadowColor = (state.shadowColor & 0xFF000000) | (shadowRgb & 0x00FFFFFF); + } else if (fmtCode == 'k') { + state.random = true; + } else if (fmtCode == 'l') { + state.bold = true; + } else if (fmtCode == 'm') { + state.strikethrough = true; + } else if (fmtCode == 'n') { + state.underline = true; + } else if (fmtCode == 'o') { + state.italic = true; + } else if (fmtCode == 'r') { + state.reset(); + } + } + + private static boolean charInRange(char what, char fromInclusive, char toInclusive) { + return what >= fromInclusive && what <= toInclusive; + } + + private static final class FormattingState { + final int baseColor; + final int baseShadowColor; + int color; + int shadowColor; + boolean italic; + boolean random; + boolean bold; + boolean strikethrough; + boolean underline; + + FormattingState(int baseColor, int baseShadowColor) { + this.baseColor = baseColor; + this.baseShadowColor = baseShadowColor; + this.color = baseColor; + this.shadowColor = baseShadowColor; + } + + void reset() { + color = baseColor; + shadowColor = baseShadowColor; + italic = false; + random = false; + bold = false; + strikethrough = false; + underline = false; + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/font/color/HexTextColorResolver.java b/src/main/java/com/gtnewhorizons/angelica/client/font/color/HexTextColorResolver.java new file mode 100644 index 000000000..82b5c4a2d --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/client/font/color/HexTextColorResolver.java @@ -0,0 +1,460 @@ +package com.gtnewhorizons.angelica.client.font.color; + +import com.gtnewhorizons.angelica.compat.hextext.HexTextCompat.Bridge; +import com.gtnewhorizons.angelica.compat.hextext.HexTextServices; +import com.gtnewhorizons.angelica.compat.hextext.render.CompatRenderInstruction; +import com.gtnewhorizons.angelica.compat.hextext.render.HexTextRenderData; +import com.gtnewhorizons.angelica.config.FontConfig; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.gtnewhorizons.angelica.client.font.BatchingFontRenderer.FORMATTING_CHAR; + +final class HexTextColorResolver implements AngelicaColorResolver { + + private final int[] vanillaPalette; + private final Bridge bridge; + + HexTextColorResolver(int[] vanillaPalette, Bridge bridge) { + this.vanillaPalette = vanillaPalette; + this.bridge = bridge; + } + + @Override + public ResolvedText resolve(CharSequence text, int start, int end, int baseColor, int baseShadowColor) { + if (text == null || start >= end) { + return ResolvedText.builder(0).build(); + } + + int safeStart = Math.max(0, start); + int safeEnd = Math.min(text.length(), end); + if (safeStart >= safeEnd) { + return ResolvedText.builder(0).build(); + } + + String segment = text.subSequence(safeStart, safeEnd).toString(); + boolean rawMode = false; + if (HexTextServices.isSupported()) { + rawMode = HexTextServices.isRawTextRendering(); + } + HexTextRenderData prepared = bridge.prepare(segment, rawMode); + String sanitized = segment; + Map> instructions = Collections.emptyMap(); + if (prepared != null) { + if (prepared.shouldReplaceText() && prepared.getDisplayText() != null) { + sanitized = prepared.getDisplayText(); + } + instructions = prepared.getInstructions(); + } + + ResolvedText.Builder builder = ResolvedText.builder(sanitized.length()); + FormattingState state = new FormattingState(baseColor, baseShadowColor); + + for (int index = 0; index < sanitized.length(); index++) { + if (instructions != null && !instructions.isEmpty()) { + List bucket = instructions.get(index); + if (bucket != null) { + applyInstructions(state, bucket); + } + } + + char chr = sanitized.charAt(index); + if (chr == FORMATTING_CHAR && index + 1 < sanitized.length()) { + char fmtCode = Character.toLowerCase(sanitized.charAt(index + 1)); + index++; + applyVanillaFormatting(state, fmtCode); + continue; + } + + builder.append( + chr, + state.color, + state.shadowColor, + state.bold, + state.italic, + state.underline, + state.strikethrough, + state.random, + state.effects.flags(), + state.effects.rainbowAnchor(), + state.effects.baseColor() + ); + state.incrementGlyphIndex(); + } + + return builder.build(); + } + + private void applyInstructions(FormattingState state, List instructions) { + for (CompatRenderInstruction instruction : instructions) { + switch (instruction.getType()) { + case APPLY_RGB: + applyRgb(state, instruction.getRgb(), instruction.shouldClearStack()); + if (instruction.resetsFormatting()) { + state.resetFormattingFlags(); + } + break; + case PUSH_RGB: + pushRgb(state, instruction.getRgb()); + if (instruction.resetsFormatting()) { + state.resetFormattingFlags(); + } + break; + case POP_COLOR: + state.popColor(); + if (instruction.resetsFormatting()) { + state.resetFormattingFlags(); + } + break; + case RESET_TO_BASE: + state.reset(); + break; + case APPLY_VANILLA_COLOR: + applyVanillaPalette(state, instruction.getParameter()); + if (instruction.resetsFormatting()) { + state.resetFormattingFlags(); + } + break; + case SET_RANDOM: + state.random = instruction.isEnabled(); + break; + case SET_BOLD: + state.bold = instruction.isEnabled(); + break; + case SET_STRIKETHROUGH: + state.strikethrough = instruction.isEnabled(); + break; + case SET_UNDERLINE: + state.underline = instruction.isEnabled(); + break; + case SET_ITALIC: + state.italic = instruction.isEnabled(); + break; + case SET_RAINBOW: + if (instruction.shouldClearStack()) { + state.clearStacks(); + } + state.effects.resetDynamicEffects(); + state.effects.setRainbow(instruction.isEnabled(), state.glyphIndex, state.color); + if (instruction.resetsFormatting()) { + state.resetFormattingFlags(); + } + break; + case SET_DINNERBONE: + state.effects.setDinnerbone(instruction.isEnabled()); + break; + case SET_IGNITE: + state.effects.setIgnite(instruction.isEnabled(), state.color); + break; + case SET_SHAKE: + state.effects.setShake(instruction.isEnabled()); + break; + default: + break; + } + } + } + + private void applyRgb(FormattingState state, int rgb, boolean clearStack) { + if (!FontConfig.preferHexTextRGB) { + if (clearStack) { + state.clearStacks(); + } + state.effects.resetDynamicEffects(); + state.effects.updateBaseColor(state.color); + return; + } + if (clearStack) { + state.clearStacks(); + } + state.color = (state.alphaMask | (rgb & 0x00FFFFFF)); + if (FontConfig.inheritAngelicaShadow) { + state.shadowColor = (state.shadowAlphaMask | (((rgb & 0xFCFCFC) >> 2) & 0x00FFFFFF)); + } else { + state.shadowColor = (state.shadowAlphaMask | (rgb & 0x00FFFFFF)); + } + state.effects.resetDynamicEffects(); + state.effects.updateBaseColor(state.color); + } + + private void pushRgb(FormattingState state, int rgb) { + state.pushColor(); + if (FontConfig.preferHexTextRGB) { + state.color = (state.alphaMask | (rgb & 0x00FFFFFF)); + if (FontConfig.inheritAngelicaShadow) { + state.shadowColor = (state.shadowAlphaMask | (((rgb & 0xFCFCFC) >> 2) & 0x00FFFFFF)); + } else { + state.shadowColor = (state.shadowAlphaMask | (rgb & 0x00FFFFFF)); + } + } + state.effects.resetDynamicEffects(); + state.effects.updateBaseColor(state.color); + } + + private void applyVanillaPalette(FormattingState state, int paletteIndex) { + state.clearStacks(); + int clamped = Math.max(0, Math.min(15, paletteIndex)); + if (vanillaPalette != null && vanillaPalette.length >= 32) { + int main = vanillaPalette[clamped]; + state.color = (state.alphaMask | (main & 0x00FFFFFF)); + int shadow = vanillaPalette[clamped + 16]; + state.shadowColor = (state.shadowAlphaMask | (shadow & 0x00FFFFFF)); + } else { + state.color = state.alphaMask | (0xFFFFFF & vanillaFallback(clamped)); + state.shadowColor = state.shadowAlphaMask | (vanillaFallback(clamped) & 0x00FFFFFF); + } + state.resetFormattingFlags(); + state.effects.resetDynamicEffects(); + state.effects.updateBaseColor(state.color); + } + + private int vanillaFallback(int index) { + int rgb = 0xFFFFFF; + switch (index) { + case 0: + rgb = 0x000000; + break; + case 1: + rgb = 0x0000AA; + break; + case 2: + rgb = 0x00AA00; + break; + case 3: + rgb = 0x00AAAA; + break; + case 4: + rgb = 0xAA0000; + break; + case 5: + rgb = 0xAA00AA; + break; + case 6: + rgb = 0xFFAA00; + break; + case 7: + rgb = 0xAAAAAA; + break; + case 8: + rgb = 0x555555; + break; + case 9: + rgb = 0x5555FF; + break; + case 10: + rgb = 0x55FF55; + break; + case 11: + rgb = 0x55FFFF; + break; + case 12: + rgb = 0xFF5555; + break; + case 13: + rgb = 0xFF55FF; + break; + case 14: + rgb = 0xFFFF55; + break; + case 15: + rgb = 0xFFFFFF; + break; + default: + break; + } + return rgb; + } + + private void applyVanillaFormatting(FormattingState state, char fmtCode) { + if (charInRange(fmtCode, '0', '9') || charInRange(fmtCode, 'a', 'f')) { + applyVanillaPalette(state, charInRange(fmtCode, '0', '9') ? (fmtCode - '0') : (fmtCode - 'a' + 10)); + } else if (fmtCode == 'k') { + state.random = true; + } else if (fmtCode == 'l') { + state.bold = true; + } else if (fmtCode == 'm') { + state.strikethrough = true; + } else if (fmtCode == 'n') { + state.underline = true; + } else if (fmtCode == 'o') { + state.italic = true; + } else if (fmtCode == 'r') { + state.reset(); + } + } + + private static boolean charInRange(char what, char fromInclusive, char toInclusive) { + return what >= fromInclusive && what <= toInclusive; + } + + private static final class FormattingState { + final int baseColor; + final int baseShadowColor; + final int alphaMask; + final int shadowAlphaMask; + final Deque colorStack = new ArrayDeque<>(); + final Deque shadowStack = new ArrayDeque<>(); + final DynamicEffectState effects; + + int color; + int shadowColor; + boolean italic; + boolean random; + boolean bold; + boolean strikethrough; + boolean underline; + int glyphIndex; + + FormattingState(int baseColor, int baseShadowColor) { + this.baseColor = baseColor; + this.baseShadowColor = baseShadowColor; + this.alphaMask = baseColor & 0xFF000000; + this.shadowAlphaMask = baseShadowColor & 0xFF000000; + this.color = baseColor; + this.shadowColor = baseShadowColor; + this.effects = new DynamicEffectState(baseColor); + } + + void reset() { + clearStacks(); + color = baseColor; + shadowColor = baseShadowColor; + resetFormattingFlags(); + effects.resetToBase(baseColor); + } + + void resetFormattingFlags() { + italic = false; + random = false; + bold = false; + strikethrough = false; + underline = false; + } + + void clearStacks() { + colorStack.clear(); + shadowStack.clear(); + effects.clearStacks(); + } + + void pushColor() { + colorStack.push(color); + shadowStack.push(shadowColor); + effects.pushBaseColor(); + } + + void popColor() { + color = colorStack.isEmpty() ? baseColor : colorStack.pop(); + shadowColor = shadowStack.isEmpty() ? baseShadowColor : shadowStack.pop(); + effects.popBaseColor(color); + } + + void incrementGlyphIndex() { + glyphIndex++; + } + } + + private static final class DynamicEffectState { + private boolean rainbow; + private boolean dinnerbone; + private boolean ignite; + private boolean shake; + private int rainbowAnchor; + private int baseColor; + private final Deque baseColorStack = new ArrayDeque<>(); + + DynamicEffectState(int baseColor) { + resetToBase(baseColor); + } + + void resetDynamicEffects() { + rainbow = false; + dinnerbone = false; + ignite = false; + shake = false; + rainbowAnchor = 0; + } + + void resetToBase(int color) { + baseColorStack.clear(); + resetDynamicEffects(); + updateBaseColor(color); + } + + void updateBaseColor(int color) { + baseColor = color & 0x00FFFFFF; + } + + void clearStacks() { + baseColorStack.clear(); + } + + void pushBaseColor() { + baseColorStack.push(baseColor); + resetDynamicEffects(); + } + + void popBaseColor(int fallback) { + resetDynamicEffects(); + if (baseColorStack.isEmpty()) { + updateBaseColor(fallback); + } else { + baseColor = baseColorStack.pop(); + } + } + + void setRainbow(boolean enabled, int anchorIndex, int currentColor) { + rainbow = enabled; + if (enabled) { + rainbowAnchor = Math.max(0, anchorIndex); + updateBaseColor(currentColor); + } else { + rainbowAnchor = 0; + } + } + + void setDinnerbone(boolean enabled) { + dinnerbone = enabled; + } + + void setIgnite(boolean enabled, int currentColor) { + ignite = enabled; + if (enabled) { + updateBaseColor(currentColor); + } + } + + void setShake(boolean enabled) { + shake = enabled; + } + + byte flags() { + byte flagBits = 0; + if (rainbow) { + flagBits |= ResolvedText.EFFECT_RAINBOW; + } + if (dinnerbone) { + flagBits |= ResolvedText.EFFECT_DINNERBONE; + } + if (ignite) { + flagBits |= ResolvedText.EFFECT_IGNITE; + } + if (shake) { + flagBits |= ResolvedText.EFFECT_SHAKE; + } + return flagBits; + } + + int baseColor() { + return baseColor; + } + + int rainbowAnchor() { + return rainbowAnchor; + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/client/font/color/ResolvedText.java b/src/main/java/com/gtnewhorizons/angelica/client/font/color/ResolvedText.java new file mode 100644 index 000000000..947fd0a04 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/client/font/color/ResolvedText.java @@ -0,0 +1,204 @@ +package com.gtnewhorizons.angelica.client.font.color; + +import java.util.Arrays; + +/** + * Holds the text and formatting metadata that a {@link AngelicaColorResolver} + * produced for a draw call. + */ +public final class ResolvedText { + + private static final byte FLAG_BOLD = 1; + private static final byte FLAG_STRIKETHROUGH = 1 << 1; + private static final byte FLAG_UNDERLINE = 1 << 2; + private static final byte FLAG_ITALIC = 1 << 3; + private static final byte FLAG_RANDOM = 1 << 4; + + static final byte EFFECT_RAINBOW = 1; + static final byte EFFECT_DINNERBONE = 1 << 1; + static final byte EFFECT_IGNITE = 1 << 2; + static final byte EFFECT_SHAKE = 1 << 3; + + private final char[] characters; + private final int[] colors; + private final int[] shadowColors; + private final byte[] flags; + private final byte[] effectFlags; + private final int[] effectBaseColors; + private final int[] effectParameters; + + private ResolvedText(char[] characters, int[] colors, int[] shadowColors, byte[] flags, byte[] effectFlags, + int[] effectBaseColors, int[] effectParameters) { + this.characters = characters; + this.colors = colors; + this.shadowColors = shadowColors; + this.flags = flags; + this.effectFlags = effectFlags; + this.effectBaseColors = effectBaseColors; + this.effectParameters = effectParameters; + } + + public int length() { + return characters.length; + } + + public char charAt(int index) { + return characters[index]; + } + + public int colorAt(int index) { + return colors[index]; + } + + public int shadowColorAt(int index) { + return shadowColors[index]; + } + + public boolean isBold(int index) { + return (flags[index] & FLAG_BOLD) != 0; + } + + public boolean isStrikethrough(int index) { + return (flags[index] & FLAG_STRIKETHROUGH) != 0; + } + + public boolean isUnderline(int index) { + return (flags[index] & FLAG_UNDERLINE) != 0; + } + + public boolean isItalic(int index) { + return (flags[index] & FLAG_ITALIC) != 0; + } + + public boolean isRandom(int index) { + return (flags[index] & FLAG_RANDOM) != 0; + } + + public boolean hasDynamicEffects() { + return effectFlags.length > 0; + } + + public boolean isRainbow(int index) { + return (effectFlags[index] & EFFECT_RAINBOW) != 0; + } + + public boolean isDinnerbone(int index) { + return (effectFlags[index] & EFFECT_DINNERBONE) != 0; + } + + public boolean isIgnite(int index) { + return (effectFlags[index] & EFFECT_IGNITE) != 0; + } + + public boolean isShake(int index) { + return (effectFlags[index] & EFFECT_SHAKE) != 0; + } + + public int effectBaseColorAt(int index) { + return effectBaseColors[index]; + } + + public int effectParameterAt(int index) { + return effectParameters[index]; + } + + /** + * Returns the resolved characters as a {@link String}. This is primarily + * used by compatibility layers that need random access to the textual + * content after Angelica has applied HexText sanitization rules. + */ + public String asString() { + return new String(characters); + } + + public static Builder builder(int initialCapacity) { + return new Builder(initialCapacity); + } + + public static final class Builder { + + private char[] characters; + private int[] colors; + private int[] shadowColors; + private byte[] flags; + private byte[] effectFlags; + private int[] effectBaseColors; + private int[] effectParameters; + private int size; + + public Builder(int initialCapacity) { + int cap = Math.max(16, initialCapacity); + this.characters = new char[cap]; + this.colors = new int[cap]; + this.shadowColors = new int[cap]; + this.flags = new byte[cap]; + this.effectFlags = new byte[cap]; + this.effectBaseColors = new int[cap]; + this.effectParameters = new int[cap]; + } + + public void append(char character, int color, int shadowColor, boolean bold, boolean italic, + boolean underline, boolean strikethrough, boolean random) { + append(character, color, shadowColor, bold, italic, underline, strikethrough, random, (byte) 0, 0, 0); + } + + public void append(char character, int color, int shadowColor, boolean bold, boolean italic, + boolean underline, boolean strikethrough, boolean random, byte dynFlags, + int effectParameter, int effectBaseColor) { + ensureCapacity(size + 1); + characters[size] = character; + colors[size] = color; + shadowColors[size] = shadowColor; + byte flagBits = 0; + if (bold) { + flagBits |= FLAG_BOLD; + } + if (strikethrough) { + flagBits |= FLAG_STRIKETHROUGH; + } + if (underline) { + flagBits |= FLAG_UNDERLINE; + } + if (italic) { + flagBits |= FLAG_ITALIC; + } + if (random) { + flagBits |= FLAG_RANDOM; + } + flags[size] = flagBits; + effectFlags[size] = dynFlags; + effectParameters[size] = effectParameter; + effectBaseColors[size] = effectBaseColor; + size++; + } + + private void ensureCapacity(int minCapacity) { + if (minCapacity <= characters.length) { + return; + } + int newCap = characters.length * 2; + while (newCap < minCapacity) { + newCap *= 2; + } + characters = Arrays.copyOf(characters, newCap); + colors = Arrays.copyOf(colors, newCap); + shadowColors = Arrays.copyOf(shadowColors, newCap); + flags = Arrays.copyOf(flags, newCap); + effectFlags = Arrays.copyOf(effectFlags, newCap); + effectBaseColors = Arrays.copyOf(effectBaseColors, newCap); + effectParameters = Arrays.copyOf(effectParameters, newCap); + } + + public ResolvedText build() { + return new ResolvedText( + Arrays.copyOf(characters, size), + Arrays.copyOf(colors, size), + Arrays.copyOf(shadowColors, size), + Arrays.copyOf(flags, size), + Arrays.copyOf(effectFlags, size), + Arrays.copyOf(effectBaseColors, size), + Arrays.copyOf(effectParameters, size) + ); + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/ModStatus.java b/src/main/java/com/gtnewhorizons/angelica/compat/ModStatus.java index 98e8c9e59..227bfab26 100644 --- a/src/main/java/com/gtnewhorizons/angelica/compat/ModStatus.java +++ b/src/main/java/com/gtnewhorizons/angelica/compat/ModStatus.java @@ -1,6 +1,7 @@ package com.gtnewhorizons.angelica.compat; import com.gtnewhorizons.angelica.compat.backhand.BackhandReflectionCompat; +import com.gtnewhorizons.angelica.compat.hextext.HexTextServices; import com.gtnewhorizons.angelica.helpers.LoadControllerHelper; import cpw.mods.fml.common.Loader; import cpw.mods.fml.common.versioning.DefaultArtifactVersion; @@ -33,6 +34,7 @@ public class ModStatus { public static boolean isThaumicHorizonsLoaded; public static boolean isBaublesLoaded; public static boolean isFluidLoggedLoaded; + public static boolean isHexTextLoaded; public static void preInit(){ isBackhandLoaded = Loader.isModLoaded("backhand"); @@ -53,7 +55,6 @@ public static void preInit(){ isThaumicHorizonsLoaded = Loader.isModLoaded("ThaumicHorizons"); isBaublesLoaded = Loader.isModLoaded("Baubles"); isFluidLoggedLoaded = Loader.isModLoaded("fluidlogged"); - isHoloInventoryLoaded = Loader.isModLoaded("holoinventory"); // remove compat with original release of BG2 @@ -72,4 +73,12 @@ public static void preInit(){ } } } + + public static void init() { + // Hex Text API is initialized after Pre Init for Compatibility + isHexTextLoaded = Loader.isModLoaded("hextext"); + if (isHexTextLoaded) { + isHexTextLoaded = HexTextServices.isApiCompatible(); + } + } } diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/hextext/HexTextCompat.java b/src/main/java/com/gtnewhorizons/angelica/compat/hextext/HexTextCompat.java new file mode 100644 index 000000000..b05f92e38 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/hextext/HexTextCompat.java @@ -0,0 +1,221 @@ +package com.gtnewhorizons.angelica.compat.hextext; + +import com.gtnewhorizons.angelica.compat.ModStatus; +import com.gtnewhorizons.angelica.compat.hextext.effects.HexTextDynamicEffectsHelper; +import com.gtnewhorizons.angelica.compat.hextext.highlight.HexTextTokenHighlighter; +import com.gtnewhorizons.angelica.compat.hextext.render.HexTextRenderBridge; +import com.gtnewhorizons.angelica.compat.hextext.render.HexTextRenderData; +import kamkeel.hextext.api.rendering.ColorService; +import kamkeel.hextext.api.rendering.RenderingEnvironmentService; +import kamkeel.hextext.api.rendering.TokenHighlightService; +import kamkeel.hextext.api.text.TextFormatter; +import net.minecraft.client.gui.FontRenderer; + +import java.util.Collections; +import java.util.List; + +/** + * Consolidated HexText compatibility helpers for Angelica. + */ +public final class HexTextCompat { + + private HexTextCompat() { + } + + /** + * Factory for a HexText-aware token highlighter. + */ + public static Highlighter createHighlighter() { + if (!HexTextServices.isSupported()) { + return Highlighter.NOOP; + } + + RenderingEnvironmentService environmentService = HexTextServices.renderEnvironment(); + TokenHighlightService highlightService = HexTextServices.tokenHighlighter(); + TextFormatter formatter = HexTextServices.textFormatter(); + if (environmentService == null + || highlightService == null + || formatter == null + || !environmentService.isRawTextRendering()) { + return Highlighter.NOOP; + } + try { + return new HexTextTokenHighlighter(environmentService, highlightService, formatter); + } catch (Throwable t) { + ModStatus.LOGGER.warn("Failed to initialize HexText token highlighting", t); + return Highlighter.NOOP; + } + } + + /** + * Factory for the HexText render bridge used by the colour resolver. + */ + public static Bridge tryCreateBridge() { + if (!HexTextServices.isSupported()) { + return null; + } + try { + return new HexTextRenderBridge(); + } catch (Throwable t) { + ModStatus.LOGGER.warn("Failed to initialize HexText compatibility layer", t); + return null; + } + } + + /** + * Returns a helper for computing HexText dynamic effect values. + */ + public static EffectsHelper getEffectsHelper() { + return EffectsHelperHolder.INSTANCE; + } + + public static int computeShadowColor(int rgb) { + ColorService colorService = HexTextServices.colorService(); + if (colorService != null) { + return colorService.calculateShadowColor(rgb); + } + return (rgb & 0xFCFCFC) >> 2; + } + + public interface Highlighter { + + /** + * Starts a new highlighting session. + */ + boolean begin(FontRenderer renderer, CharSequence text, float posX, float posY); + + /** + * Inspects the upcoming character before it is emitted. + */ + void inspect(CharSequence text, int index, float currentX); + + /** + * Advances the cursor and finalises any highlights that ended before {@code nextIndex}. + */ + void advance(int nextIndex, float currentX); + + /** + * Flushes remaining highlights at the end of the render. + */ + void finish(int textLength, float currentX); + + /** + * Returns the highlights computed for the current session. + */ + List highlights(); + + /** + * A no-op implementation used when HexText is not available. + */ + Highlighter NOOP = new Highlighter() { + @Override + public boolean begin(FontRenderer renderer, CharSequence text, float posX, float posY) { + return false; + } + + @Override + public void inspect(CharSequence text, int index, float currentX) { + } + + @Override + public void advance(int nextIndex, float currentX) { + } + + @Override + public void finish(int textLength, float currentX) { + } + + @Override + public List highlights() { + return Collections.emptyList(); + } + }; + } + + public interface Bridge { + + HexTextRenderData prepare(CharSequence text, boolean rawMode); + } + + public interface EffectsHelper { + + int computeRainbowColor(long now, int glyphIndex, int anchorIndex); + + int computeIgniteColor(long now, int baseColor); + + float computeShakeOffset(long now, int glyphIndex); + + default boolean isOperational() { + return true; + } + } + + private static final class EffectsHelperHolder { + private static final EffectsHelper INSTANCE = create(); + + private static EffectsHelper create() { + if (!HexTextServices.isSupported()) { + return NOOP_EFFECTS_HELPER; + } + try { + EffectsHelper helper = new HexTextDynamicEffectsHelper(HexTextServices.dynamicEffects()); + return helper.isOperational() ? helper : NOOP_EFFECTS_HELPER; + } catch (Throwable t) { + ModStatus.LOGGER.warn("Failed to initialize HexText dynamic effects", t); + return NOOP_EFFECTS_HELPER; + } + } + } + + private static final EffectsHelper NOOP_EFFECTS_HELPER = new EffectsHelper() { + @Override + public int computeRainbowColor(long now, int glyphIndex, int anchorIndex) { + return 0; + } + + @Override + public int computeIgniteColor(long now, int baseColor) { + return baseColor & 0x00FFFFFF; + } + + @Override + public float computeShakeOffset(long now, int glyphIndex) { + return 0.0f; + } + + @Override + public boolean isOperational() { + return false; + } + }; + + public static final class Highlight { + private final float x; + private final float y; + private final float width; + private final int color; + + public Highlight(float x, float y, float width, int color) { + this.x = x; + this.y = y; + this.width = width; + this.color = color; + } + + public float getX() { + return x; + } + + public float getY() { + return y; + } + + public float getWidth() { + return width; + } + + public int getColor() { + return color; + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/hextext/HexTextServices.java b/src/main/java/com/gtnewhorizons/angelica/compat/hextext/HexTextServices.java new file mode 100644 index 000000000..6be0bc722 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/hextext/HexTextServices.java @@ -0,0 +1,112 @@ +package com.gtnewhorizons.angelica.compat.hextext; + +import com.gtnewhorizons.angelica.compat.ModStatus; +import kamkeel.hextext.api.HexTextApi; +import kamkeel.hextext.api.rendering.ColorService; +import kamkeel.hextext.api.rendering.DynamicEffectService; +import kamkeel.hextext.api.rendering.RenderPlan; +import kamkeel.hextext.api.rendering.RenderingEnvironmentService; +import kamkeel.hextext.api.rendering.TextRenderService; +import kamkeel.hextext.api.rendering.TokenHighlightService; +import kamkeel.hextext.api.text.TextFormatter; +import net.minecraft.client.gui.FontRenderer; + +import java.util.regex.Pattern; + +/** + * Lightweight helpers around the HexText API. + */ +public final class HexTextServices { + + private static final Pattern VERSION_SPLIT = Pattern.compile("\\."); + private static final int REQUIRED_MAJOR_VERSION = 1; + private static final int REQUIRED_MINOR_VERSION = 1; + + private HexTextServices() { + } + + public static boolean isApiCompatible() { + try { + String detected = HexTextApi.apiVersion(); + if (hasMatchingMajorMinor(detected)) { + ModStatus.LOGGER.info("HexText compatibility enabled for API version {}", detected); + return true; + } + + ModStatus.LOGGER.warn( + "HexText API version {} detected but Angelica expects {}.{}. Compatibility disabled.", + detected, + REQUIRED_MAJOR_VERSION, + REQUIRED_MINOR_VERSION + ); + } catch (Throwable t) { + ModStatus.LOGGER.warn("Failed to verify HexText API version", t); + } + return false; + } + + private static boolean hasMatchingMajorMinor(String detected) { + if (detected == null || detected.isEmpty()) { + return false; + } + String[] parts = VERSION_SPLIT.split(detected, 3); + if (parts.length < 2) { + return false; + } + int major = parsePart(parts[0]); + int minor = parsePart(parts[1]); + return major == REQUIRED_MAJOR_VERSION && minor == REQUIRED_MINOR_VERSION; + } + + private static int parsePart(String value) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException ignored) { + return -1; + } + } + + public static boolean isSupported() { + return ModStatus.isHexTextLoaded; + } + + public static TextRenderService textRenderer() { + return isSupported() ? HexTextApi.textRenderer() : null; + } + + public static RenderPlan prepare(TextRenderService renderer, String text, boolean rawMode) { + return renderer == null ? null : renderer.prepare(text, rawMode); + } + + public static TokenHighlightService tokenHighlighter() { + return isSupported() ? HexTextApi.tokenHighlighter() : null; + } + + public static TextFormatter textFormatter() { + return isSupported() ? HexTextApi.textFormatter() : null; + } + + public static RenderingEnvironmentService renderEnvironment() { + return isSupported() ? HexTextApi.renderEnvironment() : null; + } + + public static DynamicEffectService dynamicEffects() { + return isSupported() ? HexTextApi.dynamicEffects() : null; + } + + public static ColorService colorService() { + return isSupported() ? HexTextApi.colors() : null; + } + + public static boolean isRawTextRendering() { + RenderingEnvironmentService environment = renderEnvironment(); + return environment != null && environment.isRawTextRendering(); + } + + public static TokenHighlightService.WidthProvider createWidthProvider(FontRenderer renderer) { + if (renderer == null) { + return null; + } + return text -> renderer.getStringWidth(text == null ? "" : text); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/hextext/effects/HexTextDynamicEffectsHelper.java b/src/main/java/com/gtnewhorizons/angelica/compat/hextext/effects/HexTextDynamicEffectsHelper.java new file mode 100644 index 000000000..82e16cef1 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/hextext/effects/HexTextDynamicEffectsHelper.java @@ -0,0 +1,47 @@ +package com.gtnewhorizons.angelica.compat.hextext.effects; + +import com.gtnewhorizons.angelica.compat.hextext.HexTextCompat; +import kamkeel.hextext.api.rendering.DynamicEffectService; + +/** + * Wrapper around HexText's dynamic text effect computations. + */ +public final class HexTextDynamicEffectsHelper implements HexTextCompat.EffectsHelper { + + private final DynamicEffectService dynamicService; + private final boolean active; + + public HexTextDynamicEffectsHelper(DynamicEffectService dynamicService) { + this.dynamicService = dynamicService; + this.active = dynamicService != null; + } + + @Override + public int computeRainbowColor(long now, int glyphIndex, int anchorIndex) { + if (!active) { + return 0; + } + return dynamicService.computeRainbowColor(now, glyphIndex, anchorIndex); + } + + @Override + public int computeIgniteColor(long now, int baseColor) { + if (!active) { + return baseColor & 0x00FFFFFF; + } + return dynamicService.computeIgniteColor(now, baseColor); + } + + @Override + public float computeShakeOffset(long now, int glyphIndex) { + if (!active) { + return 0.0f; + } + return dynamicService.computeShakeOffset(now, glyphIndex); + } + + @Override + public boolean isOperational() { + return active; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/hextext/highlight/HexTextTokenHighlighter.java b/src/main/java/com/gtnewhorizons/angelica/compat/hextext/highlight/HexTextTokenHighlighter.java new file mode 100644 index 000000000..8ca103b98 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/hextext/highlight/HexTextTokenHighlighter.java @@ -0,0 +1,124 @@ +package com.gtnewhorizons.angelica.compat.hextext.highlight; + +import com.gtnewhorizons.angelica.compat.hextext.HexTextCompat; +import com.gtnewhorizons.angelica.compat.hextext.HexTextServices; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import kamkeel.hextext.api.rendering.HighlightSpan; +import kamkeel.hextext.api.rendering.RenderingEnvironmentService; +import kamkeel.hextext.api.rendering.TokenHighlightService; +import kamkeel.hextext.api.rendering.TokenHighlightService.WidthProvider; +import kamkeel.hextext.api.text.TextFormatter; +import kamkeel.hextext.api.text.TextFormatter.FormattingEnvironment; +import net.minecraft.client.gui.FontRenderer; + +import java.util.Collections; +import java.util.List; + +/** + * HexText-backed implementation that mirrors the behaviour of the HexText font renderer token highlighter. + */ +public final class HexTextTokenHighlighter implements HexTextCompat.Highlighter { + + private static final char VANILLA_FORMATTING_CHAR = 167; + + private final List highlights = new ObjectArrayList<>(); + private final RenderingEnvironmentService environmentService; + private final TokenHighlightService highlightService; + private final TextFormatter formatter; + + private FormattingEnvironment environment; + private WidthProvider widthProvider; + private boolean rawMode; + private float baseY; + private boolean active; + private int skip; + + public HexTextTokenHighlighter( + RenderingEnvironmentService environmentService, + TokenHighlightService highlightService, + TextFormatter formatter + ) { + this.environmentService = environmentService; + this.highlightService = highlightService; + this.formatter = formatter; + } + + @Override + public boolean begin(FontRenderer renderer, CharSequence text, float posX, float posY) { + highlights.clear(); + environment = null; + widthProvider = renderer == null ? null : HexTextServices.createWidthProvider(renderer); + skip = 0; + + if (renderer == null || text == null || text.length() == 0) { + active = false; + return false; + } + if (highlightService == null || formatter == null || environmentService == null) { + active = false; + return false; + } + + rawMode = environmentService.isRawTextRendering(); + environment = formatter.captureEnvironment(rawMode); + baseY = posY; + active = true; + return true; + } + + @Override + public void inspect(CharSequence text, int index, float currentX) { + if (!active || text == null || index < 0 || index >= text.length()) { + return; + } + if (skip > 0) { + skip--; + return; + } + int tokenLength = detectTokenLength(text, index); + if (tokenLength <= 0) { + return; + } + + if (widthProvider == null) { + skip = Math.max(tokenLength - 1, 0); + return; + } + + char current = text.charAt(index); + if (current != VANILLA_FORMATTING_CHAR) { + int color = highlightService.getTokenHighlightColor(text, index); + float width = highlightService.measureLiteralWidth(widthProvider, text, index, tokenLength); + if (width > 0.0f) { + HighlightSpan span = highlightService.createHighlight(currentX, baseY, width, color); + if (span != null) { + highlights.add(new HexTextCompat.Highlight(span.getX(), span.getY(), span.getWidth(), span.getColor())); + } + } + } + skip = Math.max(tokenLength - 1, 0); + } + + @Override + public void advance(int nextIndex, float currentX) { + // No-op: highlight spans are emitted eagerly in {@link #inspect}. + } + + @Override + public void finish(int textLength, float currentX) { + active = false; + } + + @Override + public List highlights() { + return active ? Collections.unmodifiableList(highlights) : highlights; + } + + private int detectTokenLength(CharSequence text, int index) { + int length = formatter.detectColorCodeLength(text, index, rawMode, environment); + if (length == 0 && text.charAt(index) == '&') { + length = formatter.detectColorCodeLength(text, index, true, environment); + } + return length; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/hextext/render/CompatRenderInstruction.java b/src/main/java/com/gtnewhorizons/angelica/compat/hextext/render/CompatRenderInstruction.java new file mode 100644 index 000000000..f2ae0dac8 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/hextext/render/CompatRenderInstruction.java @@ -0,0 +1,65 @@ +package com.gtnewhorizons.angelica.compat.hextext.render; + +/** + * Simplified representation of HexText render directives used by Angelica's colour resolver. + */ +public final class CompatRenderInstruction { + + public enum Type { + APPLY_RGB, + PUSH_RGB, + POP_COLOR, + RESET_TO_BASE, + APPLY_VANILLA_COLOR, + SET_RANDOM, + SET_BOLD, + SET_STRIKETHROUGH, + SET_UNDERLINE, + SET_ITALIC, + SET_RAINBOW, + SET_DINNERBONE, + SET_IGNITE, + SET_SHAKE + } + + private final Type type; + private final int rgb; + private final boolean clearStack; + private final int parameter; + private final boolean enabled; + private final boolean resetFormatting; + + CompatRenderInstruction(Type type, int rgb, boolean clearStack, int parameter, boolean enabled, + boolean resetFormatting) { + this.type = type; + this.rgb = rgb; + this.clearStack = clearStack; + this.parameter = parameter; + this.enabled = enabled; + this.resetFormatting = resetFormatting; + } + + public Type getType() { + return type; + } + + public int getRgb() { + return rgb; + } + + public boolean shouldClearStack() { + return clearStack; + } + + public int getParameter() { + return parameter; + } + + public boolean isEnabled() { + return enabled; + } + + public boolean resetsFormatting() { + return resetFormatting; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/hextext/render/HexTextRenderBridge.java b/src/main/java/com/gtnewhorizons/angelica/compat/hextext/render/HexTextRenderBridge.java new file mode 100644 index 000000000..6862ada63 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/hextext/render/HexTextRenderBridge.java @@ -0,0 +1,79 @@ +package com.gtnewhorizons.angelica.compat.hextext.render; + +import com.gtnewhorizons.angelica.compat.ModStatus; +import com.gtnewhorizons.angelica.compat.hextext.HexTextCompat; +import com.gtnewhorizons.angelica.compat.hextext.HexTextServices; +import kamkeel.hextext.api.rendering.RenderDirective; +import kamkeel.hextext.api.rendering.RenderPlan; +import kamkeel.hextext.api.rendering.TextRenderService; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Bridges render preprocessing information from HexText to Angelica's colour resolver. + */ +public final class HexTextRenderBridge implements HexTextCompat.Bridge { + + @Override + public HexTextRenderData prepare(CharSequence text, boolean rawMode) { + String asString = text == null ? "" : text.toString(); + + TextRenderService service = HexTextServices.textRenderer(); + if (service == null) { + return new HexTextRenderData(false, asString, null); + } + + try { + RenderPlan plan = HexTextServices.prepare(service, asString, rawMode); + if (plan == null) { + return new HexTextRenderData(false, asString, null); + } + + String sanitized = asString; + boolean shouldReplace = plan.shouldReplaceText(); + if (shouldReplace) { + String display = plan.getDisplayText(); + if (display != null) { + sanitized = display; + } + } + + Map> instructions = convert(plan.getInstructions()); + return new HexTextRenderData(shouldReplace, sanitized, instructions); + } catch (Throwable t) { + ModStatus.LOGGER.warn("Failed to query HexText render data", t); + return new HexTextRenderData(false, asString, null); + } + } + + private Map> convert(Map> source) { + if (source == null || source.isEmpty()) { + return new HashMap<>(); + } + + Map> converted = new HashMap<>(source.size()); + for (Map.Entry> entry : source.entrySet()) { + List directives = entry.getValue(); + if (directives == null || directives.isEmpty()) { + continue; + } + + List bucket = new ArrayList<>(directives.size()); + for (RenderDirective directive : directives) { + CompatRenderInstruction convertedDirective = RenderDirectiveAdapter.adapt(directive); + if (convertedDirective != null) { + bucket.add(convertedDirective); + } + } + + if (!bucket.isEmpty()) { + converted.put(entry.getKey(), bucket); + } + } + return converted; + } +} + diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/hextext/render/HexTextRenderData.java b/src/main/java/com/gtnewhorizons/angelica/compat/hextext/render/HexTextRenderData.java new file mode 100644 index 000000000..1ff1aed9e --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/hextext/render/HexTextRenderData.java @@ -0,0 +1,44 @@ +package com.gtnewhorizons.angelica.compat.hextext.render; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Sanitized text and directive buckets returned by HexText's preprocessing pipeline. + */ +public final class HexTextRenderData { + + private static final HexTextRenderData UNCHANGED = new HexTextRenderData(false, "", Collections.emptyMap()); + + private final boolean shouldReplaceText; + private final String displayText; + private final Map> instructions; + + public HexTextRenderData(boolean shouldReplaceText, String displayText, + Map> instructions) { + this.shouldReplaceText = shouldReplaceText; + this.displayText = displayText == null ? "" : displayText; + this.instructions = instructions == null ? Collections.emptyMap() : instructions; + } + + public boolean shouldReplaceText() { + return shouldReplaceText; + } + + public String getDisplayText() { + return displayText; + } + + public Map> getInstructions() { + return instructions; + } + + public boolean hasInstructions() { + return !instructions.isEmpty(); + } + + public static HexTextRenderData unchanged() { + return UNCHANGED; + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/compat/hextext/render/RenderDirectiveAdapter.java b/src/main/java/com/gtnewhorizons/angelica/compat/hextext/render/RenderDirectiveAdapter.java new file mode 100644 index 000000000..a107fbc95 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/compat/hextext/render/RenderDirectiveAdapter.java @@ -0,0 +1,49 @@ +package com.gtnewhorizons.angelica.compat.hextext.render; + +import com.gtnewhorizons.angelica.compat.ModStatus; +import com.gtnewhorizons.angelica.compat.hextext.render.CompatRenderInstruction.Type; +import kamkeel.hextext.api.rendering.RenderDirective; +import kamkeel.hextext.api.rendering.RenderDirective.InstructionType; + +/** + * Converts HexText {@link RenderDirective} instances into Angelica-friendly instruction records. + */ +final class RenderDirectiveAdapter { + + private RenderDirectiveAdapter() { + } + + static CompatRenderInstruction adapt(RenderDirective directive) { + if (directive == null) { + return null; + } + + Type type = mapType(directive.getType()); + if (type == null) { + return null; + } + + return new CompatRenderInstruction( + type, + directive.getRgb(), + directive.shouldClearStack(), + directive.getParameter(), + directive.isEnabled(), + directive.resetsFormatting() + ); + } + + private static Type mapType(InstructionType typeObject) { + if (typeObject == null) { + return null; + } + + String fallback = typeObject.toString(); + try { + return Type.valueOf(fallback); + } catch (IllegalArgumentException iae) { + ModStatus.LOGGER.warn("Unsupported HexText render directive: {}", fallback); + return null; + } + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/config/FontConfig.java b/src/main/java/com/gtnewhorizons/angelica/config/FontConfig.java index 6bbe9618d..2061561fd 100644 --- a/src/main/java/com/gtnewhorizons/angelica/config/FontConfig.java +++ b/src/main/java/com/gtnewhorizons/angelica/config/FontConfig.java @@ -55,4 +55,16 @@ public class FontConfig { @Config.DefaultInt(7) @Config.RangeInt(min = 1, max = 24) public static int fontAAStrength; + + @Config.Comment("Enable compatibility with HexText colour formatting.") + @Config.DefaultBoolean(true) + public static boolean enableHexTextCompat; + + @Config.Comment("Prefer HexText-provided RGB colours over the vanilla palette when available.") + @Config.DefaultBoolean(true) + public static boolean preferHexTextRGB; + + @Config.Comment("Apply Angelica's shadow darkening to HexText RGB colours.") + @Config.DefaultBoolean(true) + public static boolean inheritAngelicaShadow; }