From 1a776fcf477eb02ff707c1a98c1f2caa45e911ab Mon Sep 17 00:00:00 2001 From: Provismet <17149901+Provismet@users.noreply.github.com> Date: Mon, 13 May 2024 04:57:12 +0100 Subject: [PATCH] API can add text to HUD and health bars. --- .../provihealth/ProviHealthClient.java | 2 + .../provihealth/api/ProviHealthApi.java | 20 ++++++ .../compat/ProviHealthConfigScreen.java | 14 +++++ .../provihealth/compat/SelfApiHook.java | 1 - .../provismet/provihealth/config/Options.java | 12 ++++ .../provihealth/hud/BorderRegistry.java | 28 +++++++++ .../provihealth/hud/TargetHealthBar.java | 26 +++++++- .../provihealth/world/EntityHealthBar.java | 62 ++++++++++++++----- .../assets/provihealth/lang/en_us.json | 4 ++ 9 files changed, 151 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/provismet/provihealth/ProviHealthClient.java b/src/main/java/com/provismet/provihealth/ProviHealthClient.java index 94d7a80..8f9f031 100644 --- a/src/main/java/com/provismet/provihealth/ProviHealthClient.java +++ b/src/main/java/com/provismet/provihealth/ProviHealthClient.java @@ -5,6 +5,7 @@ import com.provismet.provihealth.api.ProviHealthApi; import com.provismet.provihealth.config.Options; +import com.provismet.provihealth.hud.BorderRegistry; import com.provismet.provihealth.hud.TargetHealthBar; import com.provismet.provihealth.particle.Particles; @@ -36,6 +37,7 @@ public void onInitializeClient () { } } ); + BorderRegistry.sortTitles(); Options.load(); Particles.register(); diff --git a/src/main/java/com/provismet/provihealth/api/ProviHealthApi.java b/src/main/java/com/provismet/provihealth/api/ProviHealthApi.java index c851bc7..41dca60 100644 --- a/src/main/java/com/provismet/provihealth/api/ProviHealthApi.java +++ b/src/main/java/com/provismet/provihealth/api/ProviHealthApi.java @@ -1,6 +1,8 @@ package com.provismet.provihealth.api; +import net.minecraft.entity.LivingEntity; import net.minecraft.registry.tag.TagKey; +import net.minecraft.text.Text; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -209,4 +211,22 @@ public default boolean registerPortrait (EntityType type, @Nullable Identifie public default boolean registerPortrait (EntityType type, @Nullable Identifier resource, int priority) { return BorderRegistry.registerBorder(type, resource, priority); } + + /** + * Registers a function that places text on the healthbars or the HUD. + * Text can be used to place additional information about an entity on the HUD or the in-world healthbar. + * Text appears line-by-line on the HUD and in-world healthbar. Separate lines should be registered separately. + * The lambda is given context for whether it is appearing on the in-world or the HUD render. + * + * @param titleLambda Function of (LivingEntity entity, boolean isWorld, boolean isHUD) -> Text. Should return null if no text is wanted. + * @param order Determines the order of this line of text. Higher numbers appear at the top. + */ + public default void registerTitle (TitleGenerator titleLambda, int order) { + BorderRegistry.registerTitle(titleLambda, order); + } + + @FunctionalInterface + public interface TitleGenerator { + public Text apply (LivingEntity entity, boolean isWorld, boolean isHUD); + } } diff --git a/src/main/java/com/provismet/provihealth/compat/ProviHealthConfigScreen.java b/src/main/java/com/provismet/provihealth/compat/ProviHealthConfigScreen.java index 0ff7bc7..103e99a 100644 --- a/src/main/java/com/provismet/provihealth/compat/ProviHealthConfigScreen.java +++ b/src/main/java/com/provismet/provihealth/compat/ProviHealthConfigScreen.java @@ -76,6 +76,13 @@ public static Screen build (Screen parent) { .build() ); + hud.addEntry(entryBuilder.startBooleanToggle(Text.translatable("entry.provihealth.hudTitles"), Options.hudTitles) + .setDefaultValue(true) + .setTooltip(Text.translatable("tooltip.provihealth.hudTitles")) + .setSaveConsumer(newValue -> Options.hudTitles = newValue) + .build() + ); + hud.addEntry(entryBuilder.startBooleanToggle(Text.translatable("entry.provihealth.gradient"), Options.hudGradient) .setDefaultValue(false) .setTooltip(Text.translatable("tooltip.provihealth.gradient")) @@ -181,6 +188,13 @@ public static Screen build (Screen parent) { .build() ); + health.addEntry(entryBuilder.startBooleanToggle(Text.translatable("entry.provihealth.worldTitles"), Options.worldTitles) + .setDefaultValue(true) + .setTooltip(Text.translatable("tooltip.provihealth.worldTitles")) + .setSaveConsumer(newValue -> Options.worldTitles = newValue) + .build() + ); + health.addEntry(entryBuilder.startBooleanToggle(Text.translatable("entry.provihealth.gradient"), Options.worldGradient) .setDefaultValue(false) .setTooltip(Text.translatable("tooltip.provihealth.gradient")) diff --git a/src/main/java/com/provismet/provihealth/compat/SelfApiHook.java b/src/main/java/com/provismet/provihealth/compat/SelfApiHook.java index 8a4ac13..5e48323 100644 --- a/src/main/java/com/provismet/provihealth/compat/SelfApiHook.java +++ b/src/main/java/com/provismet/provihealth/compat/SelfApiHook.java @@ -3,7 +3,6 @@ import com.provismet.provihealth.ProviHealthClient; import com.provismet.provihealth.api.ProviHealthApi; -import net.minecraft.entity.EntityType; import net.minecraft.item.Items; import net.minecraft.registry.tag.EntityTypeTags; diff --git a/src/main/java/com/provismet/provihealth/config/Options.java b/src/main/java/com/provismet/provihealth/config/Options.java index dbda09c..fa6084c 100644 --- a/src/main/java/com/provismet/provihealth/config/Options.java +++ b/src/main/java/com/provismet/provihealth/config/Options.java @@ -58,6 +58,7 @@ public class Options { public static Vector3f unpackedStartHud = Vec3d.unpackRgb(hudStartColour).toVector3f(); public static Vector3f unpackedEndHud = Vec3d.unpackRgb(hudEndColour).toVector3f(); public static boolean hudGradient = false; + public static boolean hudTitles = true; public static boolean showTextInWorld = true; public static float maxRenderDistance = 24f; @@ -70,6 +71,7 @@ public class Options { public static boolean overrideLabels = false; public static boolean worldShadows = true; public static float worldOffsetY = 0f; + public static boolean worldTitles = true; public static boolean spawnDamageParticles = true; public static boolean spawnHealingParticles = false; @@ -163,10 +165,12 @@ public static void save () { .append("playerTarget", playersVisibilityOverride).newLine() .append("otherHealth", others.name()).newLine() .append("otherTarget", othersVisibilityOverride).newLine() + .append("worldTitles", worldTitles).newLine() .append("bossHUD", bossHUD.name()).newLine() .append("hostileHUD", hostileHUD.name()).newLine() .append("playerHUD", playerHUD.name()).newLine() .append("otherHUD", otherHUD.name()).newLine() + .append("hudTitles", hudTitles).newLine() .append("damageParticles", spawnDamageParticles).newLine() .append("healingParticles", spawnHealingParticles).newLine() .append("damageColour", damageColour).newLine() @@ -319,6 +323,10 @@ public static void load () { playersVisibilityOverride = parser.nextBoolean(); break; + case "worldTitles": + worldTitles = parser.nextBoolean(); + break; + case "playerHUD": playerHUD = HUDType.valueOf(parser.nextString()); break; @@ -335,6 +343,10 @@ public static void load () { otherHUD = HUDType.valueOf(parser.nextString()); break; + case "hudTitles": + hudTitles = parser.nextBoolean(); + break; + case "damageParticles": spawnDamageParticles = parser.nextBoolean(); break; diff --git a/src/main/java/com/provismet/provihealth/hud/BorderRegistry.java b/src/main/java/com/provismet/provihealth/hud/BorderRegistry.java index 24c730b..cb69937 100644 --- a/src/main/java/com/provismet/provihealth/hud/BorderRegistry.java +++ b/src/main/java/com/provismet/provihealth/hud/BorderRegistry.java @@ -1,8 +1,13 @@ package com.provismet.provihealth.hud; +import java.util.ArrayList; +import java.util.Comparator; import java.util.HashMap; +import java.util.List; +import com.provismet.provihealth.api.ProviHealthApi; import net.minecraft.registry.tag.TagKey; +import net.minecraft.text.Text; import org.jetbrains.annotations.Nullable; import com.provismet.provihealth.ProviHealthClient; @@ -21,6 +26,8 @@ public class BorderRegistry { private static final HashMap, BorderPriority> typeBorderPriorities = new HashMap<>(); private static final HashMap, ItemPriority> typeIconPriorities = new HashMap<>(); + private static final List orderedTitles = new ArrayList<>(); + private static final Identifier DEFAULT = ProviHealthClient.identifier("textures/gui/healthbars/default.png"); public static boolean registerBorder (TagKey> entityTag, @Nullable Identifier border, int priority) { @@ -71,6 +78,19 @@ else if (typeIconPriorities.containsKey(type) && priority <= typeIconPriorities. return true; } + public static void registerTitle (ProviHealthApi.TitleGenerator titleGen, int order) { + orderedTitles.add(new TitlePriority(titleGen, order)); + } + + public static void sortTitles () { + orderedTitles.sort(new Comparator() { + @Override + public int compare (TitlePriority a, TitlePriority b) { + return a.order() - b.order(); + } + }); + } + public static Identifier getBorder (@Nullable LivingEntity entity) { if (entity == null || !Options.useCustomHudPortraits) return DEFAULT; else { @@ -118,6 +138,14 @@ public static ItemStack getItem (LivingEntity entity) { return bestIcon; } + public static List getTitle (LivingEntity entity, boolean world, boolean hud) { + if (entity == null) return null; + + List titles = orderedTitles.stream().map(title -> title.titleGetter().apply(entity, world, hud)).filter(title -> title != null).toList(); + return titles; + } + private record ItemPriority (ItemStack itemStack, int priority) {} private record BorderPriority (Identifier borderId, int priority) {} + private record TitlePriority (ProviHealthApi.TitleGenerator titleGetter, int order) {} } diff --git a/src/main/java/com/provismet/provihealth/hud/TargetHealthBar.java b/src/main/java/com/provismet/provihealth/hud/TargetHealthBar.java index 7d87b7a..0fbf188 100644 --- a/src/main/java/com/provismet/provihealth/hud/TargetHealthBar.java +++ b/src/main/java/com/provismet/provihealth/hud/TargetHealthBar.java @@ -27,6 +27,8 @@ import net.minecraft.util.Identifier; import net.minecraft.util.math.MathHelper; +import java.util.List; + public class TargetHealthBar implements HudRenderCallback { public static boolean disabledLabels = false; @@ -149,9 +151,31 @@ public void onHudRender (DrawContext drawContext, float tickDelta) { if (expectedLeftPixel < armourX) expectedLeftPixel = armourX + 10; - int mountHealthX = drawContext.drawText(MinecraftClient.getInstance().textRenderer, mountHealthString, expectedLeftPixel, BAR_Y + BAR_HEIGHT + (vehicleMaxHealthDeep > 0f ? MOUNT_BAR_HEIGHT : 0) + 2, 0xFFFFFF, true); + int mountHealthX = drawContext.drawText(MinecraftClient.getInstance().textRenderer, mountHealthString, expectedLeftPixel, BAR_Y + BAR_HEIGHT + MOUNT_BAR_HEIGHT + 2, 0xFFFFFF, true); drawContext.drawTexture(MOUNT_HEART, mountHealthX, BAR_Y + BAR_HEIGHT + MOUNT_BAR_HEIGHT + 1, 9, 9, 0f, 0f, 9, 9, 9, 9); } + + // Render titles on HUD + if (Options.hudTitles) { + List titles = BorderRegistry.getTitle(this.target, false, true).reversed(); + + int titleX = 5; + int titleY = OFFSET_Y + FRAME_LENGTH + 5; + + if (Options.hudPosition == HUDPosition.LEFT) { + for (Text title : titles) { + drawContext.drawText(MinecraftClient.getInstance().textRenderer, title, titleX, titleY, 0xFFFFFF, true); + titleY += 10; + } + } + else { + for (Text title : titles) { + titleX = MinecraftClient.getInstance().getWindow().getScaledWidth() - 10 - MinecraftClient.getInstance().textRenderer.getWidth(title); + drawContext.drawText(MinecraftClient.getInstance().textRenderer, title, titleX, titleY, 0xFFFFFF, true); + titleY += 10; + } + } + } } if (hudType != HUDType.NONE) { diff --git a/src/main/java/com/provismet/provihealth/world/EntityHealthBar.java b/src/main/java/com/provismet/provihealth/world/EntityHealthBar.java index 41584a9..0b4d5a3 100644 --- a/src/main/java/com/provismet/provihealth/world/EntityHealthBar.java +++ b/src/main/java/com/provismet/provihealth/world/EntityHealthBar.java @@ -8,6 +8,7 @@ import com.provismet.provihealth.ProviHealthClient; import com.provismet.provihealth.config.Options; import com.provismet.provihealth.config.Options.SeeThroughText; +import com.provismet.provihealth.hud.BorderRegistry; import com.provismet.provihealth.interfaces.IMixinLivingEntity; import com.provismet.provihealth.util.Visibility; @@ -31,6 +32,8 @@ import net.minecraft.util.Identifier; import net.minecraft.util.math.MathHelper; +import java.util.List; + public class EntityHealthBar { private static final Identifier BARS = ProviHealthClient.identifier("textures/gui/healthbars/in_world.png"); private static final Identifier COMPAT_BARS = ProviHealthClient.identifier("textures/gui/healthbars/in_world_coloured.png"); @@ -107,46 +110,60 @@ public static void render (Entity entity, float tickDelta, MatrixStack matrices, Matrix4f textModel = matrices.peek().getPositionMatrix(); final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; final String healthString = String.format("%d/%d", Math.round(target.getHealth()), Math.round(target.getMaxHealth())); + final float lineHeight = 9; + + List titles = List.of(); // initialise an empty list + if (Options.worldTitles) titles = BorderRegistry.getTitle(target, true, false); if (Options.overrideLabels) { final Text targetName = getName(target); final float targetNameWidth = textRenderer.getWidth(targetName); - float healthX = 50f - textRenderer.getWidth(healthString); - final float healthY = -9; - float nameX = -50; - float nameY = -9; - boolean wrapLines = targetNameWidth - 50f > healthX - 2f; + // 0 is in the centre. + final float leftmost = -50f; + final float rightmost = -leftmost; + + float healthX = rightmost - textRenderer.getWidth(healthString); + final float healthY = -lineHeight; + float nameX = leftmost; + float nameY = -lineHeight; + boolean wrapLines = targetNameWidth - rightmost > healthX - 2f; if (wrapLines) { healthX = (healthX - 50) / 2f; nameX = -targetNameWidth / 2f; - nameY -= 9; + nameY -= lineHeight; } - if (target.shouldRenderName() && !target.isSneaky() && Options.seeThroughTextType != SeeThroughText.NONE) { + if ((target.shouldRenderName() || (target.hasCustomName() && target == MinecraftClient.getInstance().targetedEntity)) && !target.isSneaky() && Options.seeThroughTextType != SeeThroughText.NONE) { if (Options.seeThroughTextType == SeeThroughText.STANDARD) { if (Options.worldShadows) { - textRenderer.draw(targetName, nameX + 1, nameY + 1, 0x404040, false, textModel, vertexConsumers, TextLayerType.NORMAL, 0, light); - textRenderer.draw(healthString, healthX + 1, healthY + 1, 0x404040, false, textModel, vertexConsumers, TextLayerType.NORMAL, 0, light); + EntityHealthBar.renderFullText(textRenderer, targetName, healthString, titles, nameX + 1, nameY + 1, healthX + 1, healthY + 1, lineHeight, 1, 0x404040, false, textModel, vertexConsumers, TextLayerType.NORMAL, light); } matrices.translate(0, 0, 0.03f); textModel = matrices.peek().getPositionMatrix(); - textRenderer.draw(targetName, nameX, nameY, 0xFFFFFF, false, textModel, vertexConsumers, TextLayerType.SEE_THROUGH, 0, light); - textRenderer.draw(healthString, healthX, healthY, 0xFFFFFF, false, textModel, vertexConsumers, TextLayerType.SEE_THROUGH, 0, light); + EntityHealthBar.renderFullText(textRenderer, targetName, healthString, titles, nameX, nameY, healthX, healthY, lineHeight, 0, 0xFFFFFF, false, textModel, vertexConsumers, TextLayerType.SEE_THROUGH, light); } else { - textRenderer.draw(targetName, nameX, nameY, 0xFFFFFF, Options.worldShadows, textModel, vertexConsumers, TextLayerType.SEE_THROUGH, 0, light); - textRenderer.draw(healthString, healthX, healthY, 0xFFFFFF, Options.worldShadows, textModel, vertexConsumers, TextLayerType.SEE_THROUGH, 0, light); + EntityHealthBar.renderFullText(textRenderer, targetName, healthString, titles, nameX, nameY, healthX, healthY, lineHeight, 0, 0xFFFFFF, Options.worldShadows, textModel, vertexConsumers, TextLayerType.SEE_THROUGH, light); } } else { - textRenderer.draw(targetName, nameX, nameY, 0xFFFFFF, Options.worldShadows, textModel, vertexConsumers, TextLayerType.NORMAL, 0, light); - textRenderer.draw(healthString, healthX, healthY, 0xFFFFFF, Options.worldShadows, textModel, vertexConsumers, TextLayerType.NORMAL, 0, light); + EntityHealthBar.renderFullText(textRenderer, targetName, healthString, titles, nameX, nameY, healthX, healthY, lineHeight, 0, 0xFFFFFF, Options.worldShadows, textModel, vertexConsumers, TextLayerType.NORMAL, light); + } + } + else { + textRenderer.draw(healthString, -(textRenderer.getWidth(healthString)) / 2f, -lineHeight, 0xFFFFFF, Options.worldShadows, textModel, vertexConsumers, TextLayerType.NORMAL, 0, light); + + float titleX; + float titleY = -lineHeight; + for (Text title : titles) { + titleX = -textRenderer.getWidth(title) / 2f; + titleY -= lineHeight; + textRenderer.draw(title, titleX, titleY, 0xFFFFFF, Options.worldShadows, textModel, vertexConsumers, TextLayerType.NORMAL, 0, light); } } - else textRenderer.draw(healthString, -(textRenderer.getWidth(healthString)) / 2f, -10, 0xFFFFFF, Options.worldShadows, textModel, vertexConsumers, TextLayerType.NORMAL, 0, light); matrices.pop(); } @@ -155,6 +172,19 @@ public static void render (Entity entity, float tickDelta, MatrixStack matrices, matrices.pop(); } + private static void renderFullText (TextRenderer textRenderer, Text name, String health, List titles, float nameX, float nameY, float healthX, float healthY, float titleLineHeight, float titleLineOffset, int colour, boolean shadow, Matrix4f model, VertexConsumerProvider vertexes, TextLayerType layerType, int light) { + textRenderer.draw(name, nameX, nameY, colour, shadow, model, vertexes, layerType, 0, light); + textRenderer.draw(health, healthX, healthY, colour, shadow, model, vertexes, layerType, 0, light); + + float titleX; + float titleY = nameY; + for (Text title : titles) { + titleX = titleLineOffset - (textRenderer.getWidth(title) / 2f); + titleY -= titleLineHeight; + textRenderer.draw(title, titleX, titleY, colour, shadow, model, vertexes, layerType, 0, light); + } + } + @SuppressWarnings("resource") private static Text getName (LivingEntity entity) { if (entity instanceof PlayerEntity && entity.isInvisibleTo(MinecraftClient.getInstance().player)) return Text.translatable("entity.provihealth.unknownPlayer"); diff --git a/src/main/resources/assets/provihealth/lang/en_us.json b/src/main/resources/assets/provihealth/lang/en_us.json index adeeef0..f09e65b 100644 --- a/src/main/resources/assets/provihealth/lang/en_us.json +++ b/src/main/resources/assets/provihealth/lang/en_us.json @@ -27,6 +27,7 @@ "entry.provihealth.maxDistance": "Maximum Render Distance", "entry.provihealth.barScale": "Bar Size", "entry.provihealth.hudOffsetY": "HUD Offset Y", + "entry.provihealth.hudTitles": "Show Titles On HUD", "entry.provihealth.damageParticles": "Show Damage Particles", "entry.provihealth.healingParticles": "Show Healing Particles", "entry.provihealth.damageColour": "Damage Particle Color", @@ -46,6 +47,7 @@ "entry.provihealth.compatText": "In-World Health Bar SeeThrough Text Mode", "entry.provihealth.compatWorld": "In-World Health Bar Shader Compatibility", "entry.provihealth.worldOffsetY": "Health Bar Offset Y", + "entry.provihealth.worldTitles": "Show Titles In World", "entry.provihealth.damageAlpha": "Damage Particle Alpha", "entry.provihealth.healingAlpha": "Healing Particle Alpha", "entry.provihealth.compatHud": "HUD Paperdoll Render Mode", @@ -83,6 +85,8 @@ "tooltip.provihealth.overrideLabels": "When true health bar text will include the name of the mob and nameplates will not be rendered.\nWhen false health bar text will only show the health values, and the bar will move upwards when a nameplate is being rendered.\nWarning: Only the name of the mob is added to the health bar, if a mod/plugin/scoreboard adds additional lines of text to the nameplate then that text will be lost.", "tooltip.provihealth.compatText": "SeeThrough Text is any text that is visible through walls, this includes player nameplates.\nThis setting only applies when Replace Nameplates is active.\nSeeThrough Text-Shadows break easily in both vanilla and heavily modded environments. Change this setting if overridden nameplates render wrong.\nIf this setting does not solve the shadow issue, then disable text-shadows in the In-World menu.", "tooltip.provihealth.compatWorld": "Uses a different rendering program for in-world health bars. Only turn this on if shaders are preventing the bars from rendering.\nWarning: This option disables any color-related settings for in-world health bars.", + "tooltip.provihealth.hudTitles": "Allows other mods to show text under the HUD portrait.", + "tooltip.provihealth.worldTitles": "Allows other mods to show text above the in-world health bars.", "entity.provihealth.unknownPlayer": "[Invisible Player]" } \ No newline at end of file