diff --git a/src/main/java/com/provismet/provihealth/ProviHealthClient.java b/src/main/java/com/provismet/provihealth/ProviHealthClient.java index 8f9f031..13946a5 100644 --- a/src/main/java/com/provismet/provihealth/ProviHealthClient.java +++ b/src/main/java/com/provismet/provihealth/ProviHealthClient.java @@ -1,5 +1,6 @@ package com.provismet.provihealth; +import com.provismet.provihealth.util.StatusEffectIdentifier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,7 +34,7 @@ public void onInitializeClient () { entrypoint.getEntrypoint().onInitialize(); } catch (Exception e) { - LOGGER.error("Mod " + otherModId + " caused an error during inter-mod initialisation: ", e); + LOGGER.error("Mod {} caused an error during inter-mod initialisation: ", otherModId, e); } } ); @@ -41,6 +42,7 @@ public void onInitializeClient () { Options.load(); Particles.register(); + StatusEffectIdentifier.setup(); } } diff --git a/src/main/java/com/provismet/provihealth/compat/ProviHealthConfigScreen.java b/src/main/java/com/provismet/provihealth/compat/ProviHealthConfigScreen.java index 8c6ef02..a8a059a 100644 --- a/src/main/java/com/provismet/provihealth/compat/ProviHealthConfigScreen.java +++ b/src/main/java/com/provismet/provihealth/compat/ProviHealthConfigScreen.java @@ -83,6 +83,12 @@ public static Screen build (Screen parent) { .build() ); + hud.addEntry(entryBuilder.startBooleanToggle(Text.translatable("entry.provihealth.hudStatuses"), Options.hudStatuses) + .setDefaultValue(true) + .setSaveConsumer(newValue -> Options.hudStatuses = newValue) + .build() + ); + hud.addEntry(entryBuilder.startBooleanToggle(Text.translatable("entry.provihealth.gradient"), Options.hudGradient) .setDefaultValue(false) .setTooltip(Text.translatable("tooltip.provihealth.gradient")) diff --git a/src/main/java/com/provismet/provihealth/config/Options.java b/src/main/java/com/provismet/provihealth/config/Options.java index f80f451..3845a0d 100644 --- a/src/main/java/com/provismet/provihealth/config/Options.java +++ b/src/main/java/com/provismet/provihealth/config/Options.java @@ -60,6 +60,7 @@ public class Options { public static Vector3f unpackedEndHud = Vec3d.unpackRgb(hudEndColour).toVector3f(); public static boolean hudGradient = false; public static boolean hudTitles = true; + public static boolean hudStatuses = true; public static boolean showTextInWorld = true; public static float maxRenderDistance = 24f; @@ -176,6 +177,7 @@ public static void save () { .append("playerHUD", playerHUD.name()).newLine() .append("otherHUD", otherHUD.name()).newLine() .append("hudTitles", hudTitles).newLine() + .append("hudStatusEffects", hudStatuses).newLine() .append("damageParticles", spawnDamageParticles).newLine() .append("healingParticles", spawnHealingParticles).newLine() .append("damageColour", damageColour).newLine() @@ -360,6 +362,10 @@ public static void load () { hudTitles = parser.nextBoolean(); break; + case "hudStatusEffects": + hudStatuses = parser.nextBoolean(); + break; + case "damageParticles": spawnDamageParticles = parser.nextBoolean(); break; @@ -443,7 +449,7 @@ public static void load () { break; default: - ProviHealthClient.LOGGER.warn("Unknown label \"" + label + "\" found in config."); + ProviHealthClient.LOGGER.warn("Unknown label \"{}\" found in config.", label); parser.skipValue(); break; } diff --git a/src/main/java/com/provismet/provihealth/hud/TargetHealthBar.java b/src/main/java/com/provismet/provihealth/hud/TargetHealthBar.java index 4031827..b5ce03e 100644 --- a/src/main/java/com/provismet/provihealth/hud/TargetHealthBar.java +++ b/src/main/java/com/provismet/provihealth/hud/TargetHealthBar.java @@ -1,7 +1,15 @@ package com.provismet.provihealth.hud; +import com.provismet.provihealth.interfaces.IMixinLivingEntity; +import net.minecraft.client.gui.screen.ingame.InventoryScreen; +import net.minecraft.client.render.RenderLayer; import net.minecraft.client.render.RenderTickCounter; import org.jetbrains.annotations.Nullable; +import net.minecraft.client.render.VertexConsumer; +import net.minecraft.client.texture.Sprite; +import net.minecraft.client.texture.StatusEffectSpriteManager; +import net.minecraft.entity.effect.StatusEffect; +import net.minecraft.registry.entry.RegistryEntry; import org.joml.Matrix4f; import org.joml.Quaternionf; import org.joml.Vector3f; @@ -50,6 +58,12 @@ public class TargetHealthBar implements HudRenderCallback { private static int BAR_X = FRAME_LENGTH - 5; private static int BAR_Y = OFFSET_Y + FRAME_LENGTH / 2 - (BAR_HEIGHT + MOUNT_BAR_HEIGHT) / 2; + private static int TEXT_BASE_Y = BAR_Y + BAR_HEIGHT + 1; + + private static int EFFECT_X = FRAME_LENGTH + 2; + private static int EFFECT_BASE_Y = TEXT_BASE_Y + 11; + private static int EFFECT_X_OFFSET = 17; + private static final float BAR_V2 = ((float)BAR_HEIGHT / (float)(BAR_HEIGHT + MOUNT_BAR_HEIGHT)) / 2f; // Accounting for index. private static final float MOUNT_BAR_U2 = (float)MOUNT_BAR_WIDTH / (float)BAR_WIDTH; private static final float MOUNT_BAR_V1 = ((float)BAR_HEIGHT / (float)(BAR_HEIGHT + MOUNT_BAR_HEIGHT)) / 2f; @@ -136,14 +150,15 @@ public void onHudRender (DrawContext drawContext, RenderTickCounter tickCounter) } // Render health value and heart icons + int offsetFromMountBar = (vehicleMaxHealthDeep > 0f ? MOUNT_BAR_HEIGHT : 0); int healthX = drawContext.drawText(MinecraftClient.getInstance().textRenderer, String.format("%d/%d", Math.round(this.target.getHealth()), Math.round(this.target.getMaxHealth())), infoLeftX, BAR_Y + BAR_HEIGHT + (vehicleMaxHealthDeep > 0f ? MOUNT_BAR_HEIGHT : 0) + 2, 0xFFFFFF, true); // Health Value - drawContext.drawTexture(HEART, healthX, BAR_Y + BAR_HEIGHT + (vehicleMaxHealthDeep > 0f ? MOUNT_BAR_HEIGHT : 0) + 1, 9, 9, 0f, 0f, 9, 9, 9, 9); + drawContext.drawTexture(HEART, healthX, TEXT_BASE_Y + offsetFromMountBar, 9, 9, 0f, 0f, 9, 9, 9, 9); // Render armour icon if necessary int armourX = MinecraftClient.getInstance().textRenderer.getWidth(String.format("%d/%d", Math.round(this.target.getMaxHealth()), Math.round(this.target.getMaxHealth()))) + infoLeftX + 18; if (this.target.getArmor() > 0) { - armourX = drawContext.drawText(MinecraftClient.getInstance().textRenderer, String.format("%d", this.target.getArmor()), armourX, BAR_Y + BAR_HEIGHT + (vehicleMaxHealthDeep > 0f ? MOUNT_BAR_HEIGHT : 0) + 2, 0xFFFFFF, true); - drawContext.drawTexture(ARMOUR, armourX, BAR_Y + BAR_HEIGHT + (vehicleMaxHealthDeep > 0f ? MOUNT_BAR_HEIGHT : 0) + 1, 9, 9, 0f, 0f, 9, 9, 9, 9); + armourX = drawContext.drawText(MinecraftClient.getInstance().textRenderer, String.format("%d", this.target.getArmor()), armourX, TEXT_BASE_Y + 1 + offsetFromMountBar, 0xFFFFFF, true); + drawContext.drawTexture(ARMOUR, armourX, TEXT_BASE_Y + offsetFromMountBar, 9, 9, 0f, 0f, 9, 9, 9, 9); } if (vehicleMaxHealthDeep > 0f) { @@ -153,8 +168,22 @@ public void onHudRender (DrawContext drawContext, RenderTickCounter tickCounter) if (expectedLeftPixel < armourX) expectedLeftPixel = armourX + 10; - 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); + int mountHealthX = drawContext.drawText(MinecraftClient.getInstance().textRenderer, mountHealthString, expectedLeftPixel, TEXT_BASE_Y + 1 + MOUNT_BAR_HEIGHT, 0xFFFFFF, true); + drawContext.drawTexture(MOUNT_HEART, mountHealthX, TEXT_BASE_Y + MOUNT_BAR_HEIGHT, 9, 9, 0f, 0f, 9, 9, 9, 9); + } + + if (Options.hudStatuses) { + List> effects = ((IMixinLivingEntity)this.target).provi_Health$getClientSideStatusEffects(); + + if (!effects.isEmpty()) { + StatusEffectSpriteManager statusEffectSpriteManager = MinecraftClient.getInstance().getStatusEffectSpriteManager(); + int effectXOffset = 0; + for (RegistryEntry effect : effects) { + Sprite effectSprite = statusEffectSpriteManager.getSprite(effect); + drawContext.drawSprite(EFFECT_X + effectXOffset, EFFECT_BASE_Y + offsetFromMountBar, 0, 16, 16, effectSprite); + effectXOffset += EFFECT_X_OFFSET; + } + } } // Render titles on HUD @@ -298,15 +327,21 @@ private void reset () { private void adjustForScreenSize () { OFFSET_Y = Math.min((int)(MinecraftClient.getInstance().getWindow().getScaledHeight() * (Options.hudOffsetPercent / 100f)), MinecraftClient.getInstance().getWindow().getScaledHeight() - FRAME_LENGTH); BAR_Y = OFFSET_Y + FRAME_LENGTH / 2 - (BAR_HEIGHT + MOUNT_BAR_HEIGHT) / 2; + TEXT_BASE_Y = BAR_Y + BAR_HEIGHT + 1; + EFFECT_BASE_Y = TEXT_BASE_Y + 11; if (Options.hudPosition == HUDPosition.LEFT) { OFFSET_X = 0; BAR_X = FRAME_LENGTH - 5; + EFFECT_X = FRAME_LENGTH + 2; + EFFECT_X_OFFSET = 17; } else { int width = MinecraftClient.getInstance().getWindow().getScaledWidth(); OFFSET_X = width - FRAME_LENGTH; BAR_X = OFFSET_X + 5 - BAR_WIDTH; + EFFECT_X = OFFSET_X - 18; + EFFECT_X_OFFSET = -17; } } diff --git a/src/main/java/com/provismet/provihealth/interfaces/IMixinLivingEntity.java b/src/main/java/com/provismet/provihealth/interfaces/IMixinLivingEntity.java index 52598e4..ca00b35 100644 --- a/src/main/java/com/provismet/provihealth/interfaces/IMixinLivingEntity.java +++ b/src/main/java/com/provismet/provihealth/interfaces/IMixinLivingEntity.java @@ -1,7 +1,14 @@ package com.provismet.provihealth.interfaces; +import net.minecraft.entity.effect.StatusEffect; +import net.minecraft.registry.entry.RegistryEntry; + +import java.util.List; + public interface IMixinLivingEntity { public float provihealth_glideHealth (float glideFactor); public float provihealth_glideVehicle (float trueValue, float glideFactor); + + List> provi_Health$getClientSideStatusEffects (); } diff --git a/src/main/java/com/provismet/provihealth/mixin/LivingEntityMixin.java b/src/main/java/com/provismet/provihealth/mixin/LivingEntityMixin.java index f9b9b35..310506d 100644 --- a/src/main/java/com/provismet/provihealth/mixin/LivingEntityMixin.java +++ b/src/main/java/com/provismet/provihealth/mixin/LivingEntityMixin.java @@ -1,5 +1,14 @@ package com.provismet.provihealth.mixin; +import com.provismet.provihealth.config.Options; +import com.provismet.provihealth.interfaces.IMixinLivingEntity; +import com.provismet.provihealth.util.StatusEffectIdentifier; +import net.minecraft.entity.data.TrackedData; +import net.minecraft.entity.effect.StatusEffect; +import net.minecraft.particle.EntityEffectParticleEffect; +import net.minecraft.particle.ParticleEffect; +import net.minecraft.registry.entry.RegistryEntry; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; @@ -7,8 +16,6 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import com.provismet.provihealth.config.Options; -import com.provismet.provihealth.interfaces.IMixinLivingEntity; import com.provismet.provihealth.particle.TextParticleEffect; import net.minecraft.client.MinecraftClient; @@ -18,12 +25,16 @@ import net.minecraft.util.math.MathHelper; import net.minecraft.world.World; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; + @Mixin(LivingEntity.class) public abstract class LivingEntityMixin extends Entity implements IMixinLivingEntity { protected LivingEntityMixin (EntityType type, World world) { super(type, world); } - + @Unique private float currentHealthPercent = 1f; @@ -39,6 +50,8 @@ protected LivingEntityMixin (EntityType type, World world) { @Shadow public abstract float getMaxHealth(); + @Shadow @Final private static TrackedData> POTION_SWIRLS; + @Override public float provihealth_glideHealth (float glideFactor) { float trueValue = MathHelper.clamp(this.getHealth() / this.getMaxHealth(), 0f, 1f); @@ -56,6 +69,21 @@ public float provihealth_glideVehicle (float trueValue, float glideFactor) { return this.currentVehicleHealthPercent; } + @Override + public List> provi_Health$getClientSideStatusEffects () { + List particles = this.dataTracker.get(POTION_SWIRLS); + if (particles.isEmpty()) return List.of(); + + return particles.stream() + .filter(particle -> particle instanceof EntityEffectParticleEffect) + .map(particle -> StatusEffectIdentifier.fromParticleEffect((EntityEffectParticleEffect)particle)) + .distinct() + .filter(Objects::nonNull) + .sorted(Comparator.comparing(effect -> effect.value().getName().getString())) + .sorted(Comparator.comparingInt(effect -> effect.value().getCategory().ordinal())) + .toList(); + } + @Inject(method="tick", at=@At("TAIL")) private void spawnParticles (CallbackInfo info) { if (this.prevHealth == -404.404f) this.prevHealth = this.getMaxHealth(); diff --git a/src/main/java/com/provismet/provihealth/util/StatusEffectIdentifier.java b/src/main/java/com/provismet/provihealth/util/StatusEffectIdentifier.java new file mode 100644 index 0000000..5325d65 --- /dev/null +++ b/src/main/java/com/provismet/provihealth/util/StatusEffectIdentifier.java @@ -0,0 +1,22 @@ +package com.provismet.provihealth.util; + +import net.minecraft.entity.effect.StatusEffect; +import net.minecraft.particle.EntityEffectParticleEffect; +import net.minecraft.registry.Registries; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.util.math.ColorHelper; + +import java.util.HashMap; +import java.util.Map; + +public abstract class StatusEffectIdentifier { + private static final Map> colourToEffect = new HashMap<>(); + + public static RegistryEntry fromParticleEffect (EntityEffectParticleEffect particleEffect) { + return colourToEffect.getOrDefault(particleEffect.color, null); + } + + public static void setup () { + Registries.STATUS_EFFECT.streamEntries().forEach(effect -> colourToEffect.putIfAbsent(ColorHelper.Argb.fullAlpha(effect.value().getColor()), effect)); + } +} diff --git a/src/main/resources/assets/provihealth/lang/en_us.json b/src/main/resources/assets/provihealth/lang/en_us.json index e1648cf..6854551 100644 --- a/src/main/resources/assets/provihealth/lang/en_us.json +++ b/src/main/resources/assets/provihealth/lang/en_us.json @@ -51,6 +51,7 @@ "entry.provihealth.damageAlpha": "Damage Particle Alpha", "entry.provihealth.healingAlpha": "Healing Particle Alpha", "entry.provihealth.compatHud": "HUD Paperdoll Render Mode", + "entry.provihealth.hudStatuses": "Show Status Effects", "entry.provihealth.tintBackground": "Tint Background", "entry.provihealth.teamColours": "Use Team Colors", diff --git a/src/main/resources/provihealth.accesswidener b/src/main/resources/provihealth.accesswidener index 4e22935..ddcfb87 100644 --- a/src/main/resources/provihealth.accesswidener +++ b/src/main/resources/provihealth.accesswidener @@ -1,4 +1,5 @@ accessWidener v2 named accessible method net/minecraft/client/gui/DrawContext drawTexturedQuad (Lnet/minecraft/util/Identifier;IIIIIFFFF)V -accessible method net/minecraft/client/gui/DrawContext drawTexturedQuad (Lnet/minecraft/util/Identifier;IIIIIFFFFFFFF)V \ No newline at end of file +accessible method net/minecraft/client/gui/DrawContext drawTexturedQuad (Lnet/minecraft/util/Identifier;IIIIIFFFFFFFF)V +accessible field net/minecraft/particle/EntityEffectParticleEffect color I \ No newline at end of file