From f95b7d8bc3ee7527d22a4103bc08fcd896e7089b Mon Sep 17 00:00:00 2001 From: nichtDanger Date: Sat, 2 Aug 2025 23:19:39 +0200 Subject: [PATCH 1/4] new Features + fix Excalibur null in SinglePlayer - better AFK Display - hideOwnArmor - formatting text_displays with dots - fixed excalibur name null when joining single player - renamed ExcaliburTimer - ExcaliburTime - renamed Player XP Options - Elements XP Options for better understanding --- .../elementsutils/ElementsUtilsClient.java | 14 ++- .../api/timer/ExcaliburTimerApi.java | 6 +- .../eposs/elementsutils/config/ModConfig.java | 54 ++++++--- .../armorhide/RenderArmourCallback.java | 61 ++++++++++ .../feature/armorhide/RenderListener.java | 69 +++++++++++ .../ExcaliburTimeData.java} | 14 +-- .../ExcaliburTimeDisplay.java} | 13 +- .../client/ArmorFeatureRendererMixin.java | 114 ++++++++++++++++++ .../mixin/client/InGameHudMixin.java | 93 ++++++++------ .../TextDisplayEntityRenderStateAccessor.java | 15 +++ .../TextDisplayEntityRendererMixin.java | 108 +++++++++++++++++ .../rendering/ScreenRendering.java | 4 +- .../elements-utils.client.mixins.json | 5 +- .../assets/elements-utils/lang/de_de.json | 27 +++-- .../assets/elements-utils/lang/en_us.json | 27 +++-- 15 files changed, 538 insertions(+), 86 deletions(-) create mode 100644 src/client/java/dev/eposs/elementsutils/feature/armorhide/RenderArmourCallback.java create mode 100644 src/client/java/dev/eposs/elementsutils/feature/armorhide/RenderListener.java rename src/client/java/dev/eposs/elementsutils/feature/{excaliburtimer/ExcaliburTimerData.java => excaliburtime/ExcaliburTimeData.java} (68%) rename src/client/java/dev/eposs/elementsutils/feature/{excaliburtimer/ExcaliburTimerDisplay.java => excaliburtime/ExcaliburTimeDisplay.java} (92%) create mode 100644 src/client/java/dev/eposs/elementsutils/mixin/client/ArmorFeatureRendererMixin.java create mode 100644 src/client/java/dev/eposs/elementsutils/mixin/client/TextDisplayEntityRenderStateAccessor.java create mode 100644 src/client/java/dev/eposs/elementsutils/mixin/client/TextDisplayEntityRendererMixin.java diff --git a/src/client/java/dev/eposs/elementsutils/ElementsUtilsClient.java b/src/client/java/dev/eposs/elementsutils/ElementsUtilsClient.java index a812266..76d1bba 100644 --- a/src/client/java/dev/eposs/elementsutils/ElementsUtilsClient.java +++ b/src/client/java/dev/eposs/elementsutils/ElementsUtilsClient.java @@ -1,10 +1,12 @@ package dev.eposs.elementsutils; +import dev.eposs.elementsutils.feature.armorhide.RenderArmourCallback; import dev.eposs.elementsutils.config.ModConfig; +import dev.eposs.elementsutils.feature.armorhide.RenderListener; import dev.eposs.elementsutils.feature.bosstimer.BossTimerData; import dev.eposs.elementsutils.feature.bosstimer.BossTimerDisplay; -import dev.eposs.elementsutils.feature.excaliburtimer.ExcaliburTimerData; -import dev.eposs.elementsutils.feature.excaliburtimer.ExcaliburTimerDisplay; +import dev.eposs.elementsutils.feature.excaliburtime.ExcaliburTimeData; +import dev.eposs.elementsutils.feature.excaliburtime.ExcaliburTimeDisplay; import dev.eposs.elementsutils.feature.loot.LootSound; import dev.eposs.elementsutils.feature.pet.PetDisplay; import dev.eposs.elementsutils.feature.playerbase.BaseBorderDisplay; @@ -48,8 +50,10 @@ public void onInitializeClient() { registerKeyBinding(); registerEvents(); - ExcaliburTimerData.startUpdateTimers(); + ExcaliburTimeData.startUpdateTimers(); BossTimerData.startUpdateTimers(); + + RenderArmourCallback.EVENT.register(new RenderListener()); } private void registerEvents() { @@ -79,7 +83,7 @@ private void onJoin(ClientPlayNetworkHandler handler, PacketSender sender, Minec if (server != ModConfig.InternalConfig.Servers.UNKNOWN) { PetDisplay.loadPet(handler.getRegistryManager()); BossTimerData.updateData(); - ExcaliburTimerData.updateData(); + ExcaliburTimeData.updateData(); } } @@ -161,7 +165,7 @@ private void onKeyEvent(MinecraftClient client) { BossTimerDisplay.toggleDisplay(client); } while (excaliburTimeToggle.wasPressed()) { - ExcaliburTimerDisplay.toggleDisplay(client); + ExcaliburTimeDisplay.toggleDisplay(client); } while (xpMeasureTrigger.wasPressed()) { XpMeter.startXPMeasurement(client); diff --git a/src/client/java/dev/eposs/elementsutils/api/timer/ExcaliburTimerApi.java b/src/client/java/dev/eposs/elementsutils/api/timer/ExcaliburTimerApi.java index 0fbc627..dc5a6f4 100644 --- a/src/client/java/dev/eposs/elementsutils/api/timer/ExcaliburTimerApi.java +++ b/src/client/java/dev/eposs/elementsutils/api/timer/ExcaliburTimerApi.java @@ -1,9 +1,9 @@ package dev.eposs.elementsutils.api.timer; -import dev.eposs.elementsutils.feature.excaliburtimer.ExcaliburTimerData; +import dev.eposs.elementsutils.feature.excaliburtime.ExcaliburTimeData; -public class ExcaliburTimerApi extends AbstractTimerApi { +public class ExcaliburTimerApi extends AbstractTimerApi { public ExcaliburTimerApi() { - super(ExcaliburTimerData.class, "https://elements-utils.eposs.dev/api/excalibur?server=$SERVER_ID"); + super(ExcaliburTimeData.class, "https://elements-utils.eposs.dev/api/excalibur?server=$SERVER_ID"); } } diff --git a/src/client/java/dev/eposs/elementsutils/config/ModConfig.java b/src/client/java/dev/eposs/elementsutils/config/ModConfig.java index dc17335..bdbc5b1 100644 --- a/src/client/java/dev/eposs/elementsutils/config/ModConfig.java +++ b/src/client/java/dev/eposs/elementsutils/config/ModConfig.java @@ -38,7 +38,6 @@ public static class PetData { } } - public boolean showMoonPhaseDisplay = true; public boolean showTimeDisplay = true; public boolean showPetDisplay = true; @@ -62,9 +61,9 @@ public static class BossTimerConfig { public boolean show = true; public boolean textOutline = true; - public TimeFormat bossTimeFormat = TimeFormat.RELATIVE; public boolean colorBossNames = true; public boolean colorBossTime = true; + public TimeFormat bossTimeFormat = TimeFormat.RELATIVE; } @ConfigEntry.Gui.CollapsibleObject @@ -96,10 +95,14 @@ public enum Position { } @ConfigEntry.Gui.CollapsibleObject - public XPMeterConfig xpMeterConfig = new XPMeterConfig(); - public static class XPMeterConfig { - public Integer measuringXpTarget = 500; - public Integer measuringTimeTarget = 300; + public PlayerEnhancementsConfig playerEnhancements = new PlayerEnhancementsConfig(); + + public static class PlayerEnhancementsConfig { + public boolean hideOwnArmor = false; + public boolean hideHelmet = true; + public boolean hideChestplate = true; + public boolean hideLeggings = true; + public boolean hideBoots = false; } @ConfigEntry.Gui.CollapsibleObject @@ -110,6 +113,33 @@ public static class PlayerLevelConfig { public KnownColor formattedPlayerListLevelColor = KnownColor.YELLOW; } + @ConfigEntry.Gui.CollapsibleObject + public ElementsXPConfig elementsXPConfig = new ElementsXPConfig(); + public static class ElementsXPConfig { + public boolean enabled = true; + public boolean showXpPerSecond = false; + public KnownColor xpPerSecondColor = KnownColor.GRAY; + public KnownColor overlayMessageColor = KnownColor.DARK_AQUA; + public boolean hideMaxPetXP = false; + public int overlayMessageYOffset = 0; + } + + @ConfigEntry.Gui.CollapsibleObject + public OverlaySettingsConfig overlaySettings = new OverlaySettingsConfig(); + public static class OverlaySettingsConfig { + public boolean overrideAfkTitleTime = false; + + @ConfigEntry.Gui.Tooltip + public AfkTitleTimeType afkTitleTimeType = AfkTitleTimeType.INFINITY; + + public int afkTitleTimeSeconds = 30; + + public enum AfkTitleTimeType { + INFINITY, + SECONDS + } + } + public enum KnownColor { BLACK(0x000000), DARK_BLUE(0x0000AA), @@ -134,14 +164,10 @@ public enum KnownColor { } @ConfigEntry.Gui.CollapsibleObject - public PlayerXPConfig playerXPConfig = new PlayerXPConfig(); - public static class PlayerXPConfig { - public boolean enabled = true; - public boolean showXpPerSecond = false; - public KnownColor xpPerSecondColor = KnownColor.GRAY; - public KnownColor overlayMessageColor = KnownColor.DARK_AQUA; - public boolean hideMaxPetXP = false; - public int overlayMessageYOffset = 0; + public XPMeterConfig xpMeterConfig = new XPMeterConfig(); + public static class XPMeterConfig { + public Integer measuringXpTarget = 500; + public Integer measuringTimeTarget = 300; } @ConfigEntry.Gui.CollapsibleObject diff --git a/src/client/java/dev/eposs/elementsutils/feature/armorhide/RenderArmourCallback.java b/src/client/java/dev/eposs/elementsutils/feature/armorhide/RenderArmourCallback.java new file mode 100644 index 0000000..c0e8e9d --- /dev/null +++ b/src/client/java/dev/eposs/elementsutils/feature/armorhide/RenderArmourCallback.java @@ -0,0 +1,61 @@ +package dev.eposs.elementsutils.feature.armorhide; + +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.feature.ArmorFeatureRenderer; +import net.minecraft.client.render.entity.model.BipedEntityModel; +import net.minecraft.client.render.entity.state.BipedEntityRenderState; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.EquipmentSlot; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ActionResult; + +/** + * Callback interface for intercepting armor rendering. + * Implementations can cancel or modify the rendering of armor pieces. + */ +@Environment(EnvType.CLIENT) +public interface RenderArmourCallback { + /** + * Event for registering {@link RenderArmourCallback} listeners. + * All listeners are called in order; the first non-PASS result is used. + */ + Event EVENT = EventFactory.createArrayBacked(RenderArmourCallback.class, + listeners -> (renderer, matrices, vertexConsumers, state, stack, slot, light, armorModel) -> { + for (RenderArmourCallback listener : listeners) { + ActionResult result = listener.preRenderArmour(renderer, matrices, vertexConsumers, state, stack, slot, light, armorModel); + if (result != ActionResult.PASS) { + return result; + } + } + return ActionResult.PASS; + }); + + /** + * Called before an armor piece is rendered. + * Return {@link ActionResult#FAIL} to cancel rendering, or {@link ActionResult#PASS} to allow it. + * + * @param renderer The armor feature renderer instance + * @param matrices The matrix stack for rendering + * @param vertexConsumers The vertex consumer provider + * @param state The render state of the entity + * @param stack The item stack of the armor + * @param slot The equipment slot + * @param light The light value + * @param armorModel The armor model + * @return {@link ActionResult#FAIL} to cancel rendering, {@link ActionResult#PASS} otherwise + */ + ActionResult preRenderArmour( + ArmorFeatureRenderer renderer, + MatrixStack matrices, + VertexConsumerProvider vertexConsumers, + BipedEntityRenderState state, + ItemStack stack, + EquipmentSlot slot, + int light, + BipedEntityModel armorModel + ); +} \ No newline at end of file diff --git a/src/client/java/dev/eposs/elementsutils/feature/armorhide/RenderListener.java b/src/client/java/dev/eposs/elementsutils/feature/armorhide/RenderListener.java new file mode 100644 index 0000000..6d0b96c --- /dev/null +++ b/src/client/java/dev/eposs/elementsutils/feature/armorhide/RenderListener.java @@ -0,0 +1,69 @@ +package dev.eposs.elementsutils.feature.armorhide; + +import dev.eposs.elementsutils.config.ModConfig; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.feature.ArmorFeatureRenderer; +import net.minecraft.client.render.entity.model.BipedEntityModel; +import net.minecraft.client.render.entity.state.BipedEntityRenderState; +import net.minecraft.client.render.entity.state.PlayerEntityRenderState; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.EquipmentSlot; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ActionResult; + +/** + * Listener for the RenderArmourCallback event. + * Prevents rendering of the player's own armor depending on config options. + */ +@Environment(EnvType.CLIENT) +public class RenderListener implements RenderArmourCallback { + /** + * Called before rendering an armor piece. + * Cancels rendering for the local player if the corresponding config option is enabled. + * + * @param instance The armor feature renderer instance + * @param matrices The matrix stack for rendering + * @param vertexConsumers The vertex consumer provider + * @param bipedEntityRenderState The render state of the entity + * @param stack The item stack of the armor + * @param slot The equipment slot + * @param light The light value + * @param armorModel The armor model + * @return ActionResult.FAIL to cancel rendering, ActionResult.PASS otherwise + */ + @Override + public ActionResult preRenderArmour( + ArmorFeatureRenderer instance, + MatrixStack matrices, + VertexConsumerProvider vertexConsumers, + BipedEntityRenderState bipedEntityRenderState, + ItemStack stack, + EquipmentSlot slot, + int light, + BipedEntityModel armorModel + ) { + ModConfig.PlayerEnhancementsConfig config = ModConfig.getConfig().playerEnhancements; + ClientPlayerEntity player = MinecraftClient.getInstance().player; + + if (config.hideOwnArmor + && bipedEntityRenderState instanceof PlayerEntityRenderState pers + && player != null + && player.getId() == pers.id) { + + // Check slot-specific visibility + boolean hide = switch (slot) { + case HEAD -> config.hideHelmet; + case CHEST -> config.hideChestplate; + case LEGS -> config.hideLeggings; + case FEET -> config.hideBoots; + default -> false; + }; + return hide ? ActionResult.FAIL : ActionResult.PASS; + } + return ActionResult.PASS; + } +} \ No newline at end of file diff --git a/src/client/java/dev/eposs/elementsutils/feature/excaliburtimer/ExcaliburTimerData.java b/src/client/java/dev/eposs/elementsutils/feature/excaliburtime/ExcaliburTimeData.java similarity index 68% rename from src/client/java/dev/eposs/elementsutils/feature/excaliburtimer/ExcaliburTimerData.java rename to src/client/java/dev/eposs/elementsutils/feature/excaliburtime/ExcaliburTimeData.java index 6a81ea4..0ce99d5 100644 --- a/src/client/java/dev/eposs/elementsutils/feature/excaliburtimer/ExcaliburTimerData.java +++ b/src/client/java/dev/eposs/elementsutils/feature/excaliburtime/ExcaliburTimeData.java @@ -1,4 +1,4 @@ -package dev.eposs.elementsutils.feature.excaliburtimer; +package dev.eposs.elementsutils.feature.excaliburtime; import dev.eposs.elementsutils.api.timer.ExcaliburTimerApi; import dev.eposs.elementsutils.util.TimerUtil; @@ -11,17 +11,17 @@ import java.util.concurrent.atomic.AtomicReference; -public class ExcaliburTimerData { +public class ExcaliburTimeData { private String next_user; private String time; - private static final AtomicReference INSTANCE = new AtomicReference<>(new ExcaliburTimerData()); + private static final AtomicReference INSTANCE = new AtomicReference<>(new ExcaliburTimeData()); private static Instant lastUpdate = Instant.MIN; public static void startUpdateTimers() { - new Timer("ExcaliburTimerData Update Timer").scheduleAtFixedRate(new TimerTask() { + new Timer("ExcaliburTimeData Update Timer").scheduleAtFixedRate(new TimerTask() { @Override public void run() { updateData(); @@ -33,16 +33,16 @@ public static void updateData() { // Only update data every 10 seconds to prevent spamming the API if (lastUpdate.isAfter(Instant.now().minusSeconds(10))) return; - Thread.ofVirtual().name("ExcaliburTimerData Update Thread").start(() -> { + Thread.ofVirtual().name("ExcaliburTimeData Update Thread").start(() -> { lastUpdate = Instant.now(); - ExcaliburTimerData data = new ExcaliburTimerApi().getTimerData(); + ExcaliburTimeData data = new ExcaliburTimerApi().getTimerData(); if (data != null) { INSTANCE.set(data); } }); } - public static ExcaliburTimerData getInstance() { + public static ExcaliburTimeData getInstance() { return INSTANCE.get(); } diff --git a/src/client/java/dev/eposs/elementsutils/feature/excaliburtimer/ExcaliburTimerDisplay.java b/src/client/java/dev/eposs/elementsutils/feature/excaliburtime/ExcaliburTimeDisplay.java similarity index 92% rename from src/client/java/dev/eposs/elementsutils/feature/excaliburtimer/ExcaliburTimerDisplay.java rename to src/client/java/dev/eposs/elementsutils/feature/excaliburtime/ExcaliburTimeDisplay.java index 6ebc1ec..58e9af8 100644 --- a/src/client/java/dev/eposs/elementsutils/feature/excaliburtimer/ExcaliburTimerDisplay.java +++ b/src/client/java/dev/eposs/elementsutils/feature/excaliburtime/ExcaliburTimeDisplay.java @@ -1,4 +1,4 @@ -package dev.eposs.elementsutils.feature.excaliburtimer; +package dev.eposs.elementsutils.feature.excaliburtime; import dev.eposs.elementsutils.config.ModConfig; import dev.eposs.elementsutils.util.TimerUtil; @@ -11,7 +11,7 @@ import java.time.Duration; import java.time.ZonedDateTime; -public class ExcaliburTimerDisplay { +public class ExcaliburTimeDisplay { private static final int DAYS = 7; private static final long EXTRA_SECONDS = DAYS * 20; @@ -21,24 +21,25 @@ public static void toggleDisplay(@NotNull MinecraftClient client) { ModConfig.getConfig().excaliburTime.show = !ModConfig.getConfig().excaliburTime.show; ModConfig.save(); - if (ModConfig.getConfig().excaliburTime.show) ExcaliburTimerData.updateData(); + if (ModConfig.getConfig().excaliburTime.show) ExcaliburTimeData.updateData(); } public static void render(DrawContext context, MinecraftClient client, int baseLine) { var config = ModConfig.getConfig().excaliburTime; if (!config.show) return; - var data = ExcaliburTimerData.getInstance(); + var data = ExcaliburTimeData.getInstance(); ZonedDateTime targetTime = calculateTargetTime(data.getTime()); Duration timeUntilTarget = targetTime == null ? Duration.ZERO : Duration.between(ZonedDateTime.now(), targetTime); drawText(client, context, baseLine, Text.translatable("elements-utils.display.excaliburTime.title").formatted(Formatting.UNDERLINE)); + String nextUser = data.getNextUser(); drawText(client, context, baseLine + 1, Text.literal("") .append(TimerUtil.optionalFormattedText(Text.translatable("elements-utils.display.excaliburTime.next_player"), config.colorExcaliburNames, Formatting.RED)) .append(TimerUtil.optionalFormattedText( - data.getNextUser().isEmpty() + (nextUser == null || nextUser.isEmpty()) ? Text.translatable("elements-utils.unknown") - : Text.literal(data.getNextUser()), + : Text.literal(nextUser), config.colorExcaliburNames, Formatting.GOLD )) diff --git a/src/client/java/dev/eposs/elementsutils/mixin/client/ArmorFeatureRendererMixin.java b/src/client/java/dev/eposs/elementsutils/mixin/client/ArmorFeatureRendererMixin.java new file mode 100644 index 0000000..0e6cd9e --- /dev/null +++ b/src/client/java/dev/eposs/elementsutils/mixin/client/ArmorFeatureRendererMixin.java @@ -0,0 +1,114 @@ +package dev.eposs.elementsutils.mixin.client; + +import dev.eposs.elementsutils.feature.armorhide.RenderArmourCallback; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.Environment; +import net.minecraft.client.render.VertexConsumerProvider; +import net.minecraft.client.render.entity.feature.ArmorFeatureRenderer; +import net.minecraft.client.render.entity.model.BipedEntityModel; +import net.minecraft.client.render.entity.state.BipedEntityRenderState; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.entity.EquipmentSlot; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ActionResult; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +/** + * Mixin for ArmorFeatureRenderer to allow interception of armor rendering. + * Enables custom logic (e.g. hiding armor) via the RenderArmourCallback event. + */ +@Mixin(ArmorFeatureRenderer.class) +@Environment(EnvType.CLIENT) +public abstract class ArmorFeatureRendererMixin { + /** + * Stores the current render state for use in the redirect. + */ + @Unique + @Nullable + private BipedEntityRenderState currentRenderState; + + /** + * Captures the current render state at the start of the render method. + * + * @param matrices The matrix stack + * @param vertexConsumers The vertex consumer provider + * @param i Light value + * @param state The entity render state + * @param f Unused + * @param g Unused + * @param ci Callback info + */ + @Inject( + method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/client/render/entity/state/BipedEntityRenderState;FF)V", + at = @At("HEAD") + ) + private void preRender(MatrixStack matrices, VertexConsumerProvider vertexConsumers, int i, BipedEntityRenderState state, float f, float g, CallbackInfo ci) { + this.currentRenderState = state; + } + + /** + * Clears the current render state after rendering is complete. + * + * @param matrices The matrix stack + * @param vertexConsumers The vertex consumer provider + * @param i Light value + * @param state The entity render state + * @param f Unused + * @param g Unused + * @param ci Callback info + */ + @Inject( + method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/client/render/entity/state/BipedEntityRenderState;FF)V", + at = @At("RETURN") + ) + private void postRender(MatrixStack matrices, VertexConsumerProvider vertexConsumers, int i, BipedEntityRenderState state, float f, float g, CallbackInfo ci) { + this.currentRenderState = null; + } + + /** + * Redirects the call to renderArmor to allow event-based cancellation. + * If the RenderArmourCallback returns FAIL, the armor piece is not rendered. + * + * @param instance The armor feature renderer instance + * @param matrices The matrix stack + * @param vertexConsumers The vertex consumer provider + * @param stack The item stack of the armor + * @param slot The equipment slot + * @param light The light value + * @param armorModel The armor model + */ + @Redirect( + method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/client/render/entity/state/BipedEntityRenderState;FF)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/entity/feature/ArmorFeatureRenderer;renderArmor(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;Lnet/minecraft/item/ItemStack;Lnet/minecraft/entity/EquipmentSlot;ILnet/minecraft/client/render/entity/model/BipedEntityModel;)V") + ) + private void renderArmor( + ArmorFeatureRenderer instance, + MatrixStack matrices, + VertexConsumerProvider vertexConsumers, + ItemStack stack, + EquipmentSlot slot, + int light, + BipedEntityModel armorModel + ) { + ActionResult result = RenderArmourCallback.EVENT.invoker().preRenderArmour( + instance, matrices, vertexConsumers, currentRenderState, stack, slot, light, armorModel + ); + if (result == ActionResult.FAIL) { + return; + } + this.renderArmor(matrices, vertexConsumers, stack, slot, light, armorModel); + } + + /** + * Shadowed method for rendering the armor piece. + */ + @Shadow + protected abstract void renderArmor(MatrixStack matrices, VertexConsumerProvider vertexConsumers, ItemStack stack, EquipmentSlot slot, int light, BipedEntityModel armorModel); +} \ No newline at end of file diff --git a/src/client/java/dev/eposs/elementsutils/mixin/client/InGameHudMixin.java b/src/client/java/dev/eposs/elementsutils/mixin/client/InGameHudMixin.java index 35987b9..a4bc4df 100644 --- a/src/client/java/dev/eposs/elementsutils/mixin/client/InGameHudMixin.java +++ b/src/client/java/dev/eposs/elementsutils/mixin/client/InGameHudMixin.java @@ -12,23 +12,25 @@ import net.minecraft.text.Text; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.ModifyArg; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import java.util.regex.Matcher; import java.util.regex.Pattern; @Mixin(value = InGameHud.class) public abstract class InGameHudMixin { - @Shadow - private Text overlayMessage; - @Shadow - private int overlayRemaining; + @Shadow private Text overlayMessage; + @Shadow private int overlayRemaining; + @Shadow public abstract void setTitleTicks(int fadeIn, int stay, int fadeOut); - /** + @Unique + private static final Pattern XP_PATTERN = Pattern.compile("^[^:]+: ([\\d,.]+)/[\\d,.]+ XP"); + + /** * Updates the pet XP display when an overlay message is shown. * * @param context The drawing context. @@ -37,20 +39,18 @@ public abstract class InGameHudMixin { */ @Inject(at = @At("HEAD"), method = "renderOverlayMessage") private void renderOverlayMessage(DrawContext context, RenderTickCounter tickCounter, CallbackInfo ci) { - if (this.overlayMessage != null && this.overlayRemaining > 0) { - PetDisplay.updatePetXP(this.overlayMessage, false); + if (overlayMessage != null && overlayRemaining > 0) { + PetDisplay.updatePetXP(overlayMessage, false); } } /** * Intercepts and formats the overlay message before it is displayed. - *

- * - Formats XP numbers with dots if enabled.
- * - Optionally appends colored XP/s directly after the first "XP".
- * - Hides max pet XP if configured.
- * - Applies a custom overlay color if set in the config.
+ * - Formats XP numbers with dots if enabled. + * - Optionally appends colored XP/s directly after the first "XP". + * - Hides max pet XP if configured. + * - Applies a custom overlay color if set in the config. * - Cancels the original method to display the modified message. - *

* * @param message The original overlay message to be displayed. * @param tinted Whether the message should be tinted. @@ -63,37 +63,37 @@ private void renderOverlayMessage(DrawContext context, RenderTickCounter tickCou ) private void onSetOverlayMessage(Text message, boolean tinted, CallbackInfo ci) { if (message == null) return; - + var config = ModConfig.getConfig(); String original = message.getString(); - String formatted = ModConfig.getConfig().playerXPConfig.enabled + String formatted = config.elementsXPConfig.enabled ? XpFormat.formatNumbersWithDots(original) : original; Style overlayStyle = message.getStyle(); - var overlayColor = ModConfig.getConfig().playerXPConfig.overlayMessageColor; + var overlayColor = config.elementsXPConfig.overlayMessageColor; if (overlayColor != null && formatted.contains("XP")) { overlayStyle = overlayStyle.withColor(overlayColor.color); } Text xpPerSecText = null; Style xpPerSecStyle = Style.EMPTY; - Matcher matcher = Pattern.compile("^[^:]+: ([\\d,.]+)/[\\d,.]+ XP").matcher(original); + var matcher = XP_PATTERN.matcher(original); if (matcher.find()) { int farmingXp = Integer.parseInt(matcher.group(1).replace(".", "").replace(",", "")); FarmingXpTracker.update(farmingXp); - if (ModConfig.getConfig().playerXPConfig.showXpPerSecond) { + if (config.elementsXPConfig.showXpPerSecond) { float xpPerSec = FarmingXpTracker.getXpPerSecond(); - var color = ModConfig.getConfig().playerXPConfig.xpPerSecondColor; + var color = config.elementsXPConfig.xpPerSecondColor; if (color != null) xpPerSecStyle = xpPerSecStyle.withColor(color.color); xpPerSecText = Text.literal(String.format("%.2fXP/s", xpPerSec)).setStyle(xpPerSecStyle); } } - if (ModConfig.getConfig().playerXPConfig.hideMaxPetXP) { + if (config.elementsXPConfig.hideMaxPetXP) { if (original.matches(".*Pet: [\\d,.]+/-1 XP$")) PetDisplay.setPetMaxLevel(); original = original.replaceFirst("\\s*\\p{So}?\\s*Pet: [\\d,.]+/-1 XP$", ""); - formatted = ModConfig.getConfig().playerXPConfig.enabled + formatted = config.elementsXPConfig.enabled ? XpFormat.formatNumbersWithDots(original) : original; } @@ -103,19 +103,19 @@ private void onSetOverlayMessage(Text message, boolean tinted, CallbackInfo ci) if (xpIndex != -1) { String beforeXp = formatted.substring(0, xpIndex + 2); String afterXp = formatted.substring(xpIndex + 2); - this.overlayMessage = Text.literal(beforeXp).setStyle(overlayStyle) + overlayMessage = Text.literal(beforeXp).setStyle(overlayStyle) .append(Text.literal(" (").setStyle(xpPerSecStyle)) .append(xpPerSecText) .append(Text.literal(")").setStyle(xpPerSecStyle)) .append(Text.literal(afterXp).setStyle(overlayStyle)); - this.overlayRemaining = 60; + overlayRemaining = 60; ci.cancel(); return; } } - this.overlayMessage = Text.literal(formatted).setStyle(overlayStyle); - this.overlayRemaining = 60; + overlayMessage = Text.literal(formatted).setStyle(overlayStyle); + overlayRemaining = 60; ci.cancel(); } @@ -135,8 +135,7 @@ private void onSetOverlayMessage(Text message, boolean tinted, CallbackInfo ci) index = 3 ) private int modifyOverlayMessageY(int originalY) { - int yOffset = ModConfig.getConfig().playerXPConfig.overlayMessageYOffset; - return originalY + yOffset; + return originalY + ModConfig.getConfig().elementsXPConfig.overlayMessageYOffset; } /** @@ -155,7 +154,8 @@ private int modifyOverlayMessageY(int originalY) { index = 1 ) private String modifyLevelText(String original) { - if (!ModConfig.getConfig().playerLevelConfig.enabled) return original; + var config = ModConfig.getConfig(); + if (!config.playerLevelConfig.enabled) return original; try { int level = Integer.parseInt(original); return Util.formatLevel(level); @@ -180,10 +180,35 @@ private String modifyLevelText(String original) { index = 4 ) private int modifyLevelColor(int originalColor) { - if (ModConfig.getConfig().playerLevelConfig.formattedPlayerLevelColor == null) return originalColor; - if (originalColor == 0) return originalColor; - return ModConfig.getConfig().playerLevelConfig.formattedPlayerLevelColor.color; + var config = ModConfig.getConfig(); + var color = config.playerLevelConfig.formattedPlayerLevelColor; + if (color == null || originalColor == 0) return originalColor; + return color.color; } - -} + /** + * Überschreibt die Titel-Anzeigezeit für AFK-Titel, wenn in der Config aktiviert. + * Die Dauer wird abhängig vom Enum-Wert gesetzt: + * - INFINITY: Maximale Dauer (`Integer.MAX_VALUE`) + * - SECONDS: Konfigurierbare Sekundenanzahl (\* 20 für Ticks, mindestens 1 Sekunde) + * Sonst werden Standardwerte verwendet. + * + * @param title Der anzuzeigende Titeltext. + * @param ci Callback-Info für das Mixin. + */ + @Inject( + method = "setTitle", + at = @At("HEAD") + ) + private void onSetTitle(Text title, CallbackInfo ci) { + var config = ModConfig.getConfig().overlaySettings; + if (config.overrideAfkTitleTime && title != null && title.getString().trim().equalsIgnoreCase("afk")) { + int stay = (config.afkTitleTimeType == ModConfig.OverlaySettingsConfig.AfkTitleTimeType.INFINITY) + ? Integer.MAX_VALUE + : Math.max(1, config.afkTitleTimeSeconds) * 20; + setTitleTicks(0, stay, 0); + } else { + setTitleTicks(10, 70, 20); + } + } +} \ No newline at end of file diff --git a/src/client/java/dev/eposs/elementsutils/mixin/client/TextDisplayEntityRenderStateAccessor.java b/src/client/java/dev/eposs/elementsutils/mixin/client/TextDisplayEntityRenderStateAccessor.java new file mode 100644 index 0000000..276e7b9 --- /dev/null +++ b/src/client/java/dev/eposs/elementsutils/mixin/client/TextDisplayEntityRenderStateAccessor.java @@ -0,0 +1,15 @@ +package dev.eposs.elementsutils.mixin.client; + +import net.minecraft.client.render.entity.state.TextDisplayEntityRenderState; +import net.minecraft.entity.decoration.DisplayEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(TextDisplayEntityRenderState.class) +public interface TextDisplayEntityRenderStateAccessor { + @Accessor("textLines") + DisplayEntity.TextDisplayEntity.TextLines getTextLines(); + + @Accessor("textLines") + void setTextLines(DisplayEntity.TextDisplayEntity.TextLines lines); +} \ No newline at end of file diff --git a/src/client/java/dev/eposs/elementsutils/mixin/client/TextDisplayEntityRendererMixin.java b/src/client/java/dev/eposs/elementsutils/mixin/client/TextDisplayEntityRendererMixin.java new file mode 100644 index 0000000..8b5842e --- /dev/null +++ b/src/client/java/dev/eposs/elementsutils/mixin/client/TextDisplayEntityRendererMixin.java @@ -0,0 +1,108 @@ +package dev.eposs.elementsutils.mixin.client; + +import dev.eposs.elementsutils.config.ModConfig; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.render.entity.DisplayEntityRenderer; +import net.minecraft.client.render.entity.state.TextDisplayEntityRenderState; +import net.minecraft.entity.decoration.DisplayEntity; +import net.minecraft.text.OrderedText; +import net.minecraft.text.Text; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Mixin(DisplayEntityRenderer.TextDisplayEntityRenderer.class) +public abstract class TextDisplayEntityRendererMixin { + /** + * Injects at the start of the render method to optionally format numbers in text lines, + * depending on the playerLevelConfig.enabled setting. + * + * @param state The current TextDisplayEntityRenderState. + * @param matrixStack The matrix stack for rendering. + * @param vertexConsumerProvider The vertex consumer provider. + * @param i Render parameter. + * @param f Render parameter. + * @param ci Callback info. + */ + @Inject( + method = "render(Lnet/minecraft/client/render/entity/state/TextDisplayEntityRenderState;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;IF)V", + at = @At("HEAD") + ) + private void onRender( + TextDisplayEntityRenderState state, + net.minecraft.client.util.math.MatrixStack matrixStack, + net.minecraft.client.render.VertexConsumerProvider vertexConsumerProvider, + int i, + float f, + CallbackInfo ci + ) { + if (!ModConfig.getConfig().playerLevelConfig.enabled) return; + + var lines = ((TextDisplayEntityRenderStateAccessor) state).getTextLines(); + if (lines == null) return; + + var textRenderer = MinecraftClient.getInstance().textRenderer; + List newLines = new ArrayList<>(); + int maxWidth = 0; + boolean changed = false; + + for (var line : lines.lines()) { + String original = orderedTextToString(line.contents()); + String formatted = formatNumbersInString(original); + changed |= !original.equals(formatted); + + int width = textRenderer.getWidth(formatted); + maxWidth = Math.max(maxWidth, width); + newLines.add(new DisplayEntity.TextDisplayEntity.TextLine(Text.literal(formatted).asOrderedText(), width)); + } + + if (changed) { + ((TextDisplayEntityRenderStateAccessor) state).setTextLines( + new DisplayEntity.TextDisplayEntity.TextLines(newLines, maxWidth) + ); + } + } + + /** + * Converts an OrderedText object to a plain String. + * + * @param orderedText The OrderedText to convert. + * @return The plain String representation. + */ + @Unique + private static String orderedTextToString(OrderedText orderedText) { + StringBuilder sb = new StringBuilder(); + orderedText.accept((i, s, c) -> { sb.appendCodePoint(c); return true; }); + return sb.toString(); + } + + /** + * Formats numbers in the input string with thousands separators, + * but only if the number is preceded by a space or at the start of the line. + * + * @param input The input string. + * @return The formatted string. + */ + @Unique + private static String formatNumbersInString(String input) { + Matcher matcher = Pattern.compile("(?<=\\s|^)\\d+").matcher(input); + StringBuilder sb = new StringBuilder(); + while (matcher.find()) { + try { + matcher.appendReplacement(sb, String.format(Locale.GERMAN, "%,d", Long.parseLong(matcher.group()))); + } catch (NumberFormatException e) { + matcher.appendReplacement(sb, matcher.group()); + } + } + matcher.appendTail(sb); + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/client/java/dev/eposs/elementsutils/rendering/ScreenRendering.java b/src/client/java/dev/eposs/elementsutils/rendering/ScreenRendering.java index 3939580..141070f 100644 --- a/src/client/java/dev/eposs/elementsutils/rendering/ScreenRendering.java +++ b/src/client/java/dev/eposs/elementsutils/rendering/ScreenRendering.java @@ -2,7 +2,7 @@ import dev.eposs.elementsutils.ElementsUtils; import dev.eposs.elementsutils.feature.bosstimer.BossTimerDisplay; -import dev.eposs.elementsutils.feature.excaliburtimer.ExcaliburTimerDisplay; +import dev.eposs.elementsutils.feature.excaliburtime.ExcaliburTimeDisplay; import dev.eposs.elementsutils.feature.moonphase.MoonPhaseDisplay; import dev.eposs.elementsutils.feature.pet.PetDisplay; import dev.eposs.elementsutils.feature.potion.PotionDisplay; @@ -33,7 +33,7 @@ private static void render(DrawContext context, RenderTickCounter tickCounter) { MoonPhaseDisplay.render(context, client); TimeDisplay.render(context, client); BossTimerDisplay.render(context, client); - ExcaliburTimerDisplay.render(context, client, 6); + ExcaliburTimeDisplay.render(context, client, 6); PetDisplay.render(context, client); PotionDisplay.render(context, client); XpMeter.render(context, client); diff --git a/src/client/resources/elements-utils.client.mixins.json b/src/client/resources/elements-utils.client.mixins.json index 1c6d0d5..95c2fc9 100644 --- a/src/client/resources/elements-utils.client.mixins.json +++ b/src/client/resources/elements-utils.client.mixins.json @@ -3,10 +3,13 @@ "package": "dev.eposs.elementsutils.mixin.client", "compatibilityLevel": "JAVA_21", "client": [ + "ArmorFeatureRendererMixin", "ExampleClientMixin", "InGameHudAccessor", "InGameHudMixin", - "PlayerListHudMixin" + "PlayerListHudMixin", + "TextDisplayEntityRendererMixin", + "TextDisplayEntityRenderStateAccessor" ], "injectors": { "defaultRequire": 1 diff --git a/src/main/resources/assets/elements-utils/lang/de_de.json b/src/main/resources/assets/elements-utils/lang/de_de.json index bec06c9..af1d699 100644 --- a/src/main/resources/assets/elements-utils/lang/de_de.json +++ b/src/main/resources/assets/elements-utils/lang/de_de.json @@ -26,6 +26,13 @@ "text.autoconfig.elements-utils.option.showBaseDisplay": "Spieler Base Grenzen anzeigen", + "text.autoconfig.elements-utils.option.playerEnhancements": "Spieler Verbesserungen", + "text.autoconfig.elements-utils.option.playerEnhancements.hideOwnArmor": "Eigene Rüstung ausblenden", + "text.autoconfig.elements-utils.option.playerEnhancements.hideHelmet": "Helm ausblenden", + "text.autoconfig.elements-utils.option.playerEnhancements.hideChestplate": "Brustplatte ausblenden", + "text.autoconfig.elements-utils.option.playerEnhancements.hideLeggings": "Hose ausblenden", + "text.autoconfig.elements-utils.option.playerEnhancements.hideBoots": "Schuhe ausblenden", + "text.autoconfig.elements-utils.option.potionDisplay": "Heil/Mana-Tränke Display", "text.autoconfig.elements-utils.option.potionDisplay.show": "Tränke anzeigen", "text.autoconfig.elements-utils.option.potionDisplay.position": "Display Position", @@ -39,13 +46,19 @@ "text.autoconfig.elements-utils.option.playerLevelConfig.formattedPlayerLevelColor": "Spielerlevel Farbe", "text.autoconfig.elements-utils.option.playerLevelConfig.formattedPlayerListLevelColor": "Farbe (Spielerlevel in der Tab-Liste)", - "text.autoconfig.elements-utils.option.playerXPConfig": "Spieler XP Optionen", - "text.autoconfig.elements-utils.option.playerXPConfig.enabled": "Formatiere Zahlen der Spieler-XP", - "text.autoconfig.elements-utils.option.playerXPConfig.showXpPerSecond": "XP/s anzeigen", - "text.autoconfig.elements-utils.option.playerXPConfig.xpPerSecondColor": "XP/s Farbe", - "text.autoconfig.elements-utils.option.playerXPConfig.hideMaxPetXP": "Max Pet-XP ausblenden", - "text.autoconfig.elements-utils.option.playerXPConfig.overlayMessageColor": "Farbe der Overlay-Nachricht", - "text.autoconfig.elements-utils.option.playerXPConfig.overlayMessageYOffset": "Overlay Nachricht Y-Offset", + "text.autoconfig.elements-utils.option.elementsXPConfig": "Elements XP Optionen", + "text.autoconfig.elements-utils.option.elementsXPConfig.enabled": "Formatiere Zahlen der Elements-XP", + "text.autoconfig.elements-utils.option.elementsXPConfig.showXpPerSecond": "XP/s anzeigen", + "text.autoconfig.elements-utils.option.elementsXPConfig.xpPerSecondColor": "XP/s Farbe", + "text.autoconfig.elements-utils.option.elementsXPConfig.hideMaxPetXP": "Max Pet-XP ausblenden", + "text.autoconfig.elements-utils.option.elementsXPConfig.overlayMessageColor": "Farbe der Overlay-Nachricht", + "text.autoconfig.elements-utils.option.elementsXPConfig.overlayMessageYOffset": "Overlay Nachricht Y-Offset", + + "text.autoconfig.elements-utils.option.overlaySettings": "Overlay Einstellungen", + "text.autoconfig.elements-utils.option.overlaySettings.overrideAfkTitleTime": "Überschreibe AFK Titel Verweildauer", + "text.autoconfig.elements-utils.option.overlaySettings.afkTitleTimeType": "AFK Titel Verweildauer Typ", + "text.autoconfig.elements-utils.option.overlaySettings.afkTitleTimeType.@Tooltip": "Wenn auf 'INFINITY' gesetzt', bleibt der AFK Titel bis zum nächsten Spieler-Move. Wenn auf 'SECONDS' gesetzt, bleibt der AFK Titel für die angegebene Anzahl an Sekunden.", + "text.autoconfig.elements-utils.option.overlaySettings.afkTitleTimeSeconds": "AFK Titel Verweildauer (Sekunden)", "text.autoconfig.elements-utils.option.devUtils": "Development Utils (bitte ignorieren)", "text.autoconfig.elements-utils.option.devUtils.enable": "Enabled", diff --git a/src/main/resources/assets/elements-utils/lang/en_us.json b/src/main/resources/assets/elements-utils/lang/en_us.json index dc43c7e..bfc99da 100644 --- a/src/main/resources/assets/elements-utils/lang/en_us.json +++ b/src/main/resources/assets/elements-utils/lang/en_us.json @@ -26,6 +26,13 @@ "text.autoconfig.elements-utils.option.showBaseDisplay": "Show Player Base Borders", + "text.autoconfig.elements-utils.option.playerEnhancements": "Player Enhancements", + "text.autoconfig.elements-utils.option.playerEnhancements.hideOwnArmor": "Hide Own Armor", + "text.autoconfig.elements-utils.option.playerEnhancements.hideHelmet": "Hide Helmet", + "text.autoconfig.elements-utils.option.playerEnhancements.hideChestplate": "Hide Chestplate", + "text.autoconfig.elements-utils.option.playerEnhancements.hideLeggings": "Hide Leggings", + "text.autoconfig.elements-utils.option.playerEnhancements.hideBoots": "Hide Boots", + "text.autoconfig.elements-utils.option.potionDisplay": "Health/Mana Potion Display", "text.autoconfig.elements-utils.option.potionDisplay.show": "Show Potion Display", "text.autoconfig.elements-utils.option.potionDisplay.position": "Display Position", @@ -39,13 +46,19 @@ "text.autoconfig.elements-utils.option.playerLevelConfig.formattedPlayerLevelColor": "Player Level Color", "text.autoconfig.elements-utils.option.playerLevelConfig.formattedPlayerListLevelColor": "Player List Level Color", - "text.autoconfig.elements-utils.option.playerXPConfig": "Player XP Options", - "text.autoconfig.elements-utils.option.playerXPConfig.enabled": "Format Numbers of Player XP's", - "text.autoconfig.elements-utils.option.playerXPConfig.showXpPerSecond": "Show XP/s", - "text.autoconfig.elements-utils.option.playerXPConfig.xpPerSecondColor": "XP/s Color", - "text.autoconfig.elements-utils.option.playerXPConfig.hideMaxPetXP": "Hide Max Pet XP", - "text.autoconfig.elements-utils.option.playerXPConfig.overlayMessageColor": "Overlay Message Color", - "text.autoconfig.elements-utils.option.playerXPConfig.overlayMessageYOffset": "Overlay Message Y-Offset", + "text.autoconfig.elements-utils.option.elementsXPConfig": "Elements XP Options", + "text.autoconfig.elements-utils.option.elementsXPConfig.enabled": "Format Numbers of Elements XP's", + "text.autoconfig.elements-utils.option.elementsXPConfig.showXpPerSecond": "Show XP/s", + "text.autoconfig.elements-utils.option.elementsXPConfig.xpPerSecondColor": "XP/s Color", + "text.autoconfig.elements-utils.option.elementsXPConfig.hideMaxPetXP": "Hide Max Pet XP", + "text.autoconfig.elements-utils.option.elementsXPConfig.overlayMessageColor": "Overlay Message Color", + "text.autoconfig.elements-utils.option.elementsXPConfig.overlayMessageYOffset": "Overlay Message Y-Offset", + + "text.autoconfig.elements-utils.option.overlaySettings": "Overlay Settings", + "text.autoconfig.elements-utils.option.overlaySettings.overrideAfkTitleTime": "Override AFK Title Stay Time", + "text.autoconfig.elements-utils.option.overlaySettings.afkTitleTimeType": "AFK Title Time Type", + "text.autoconfig.elements-utils.option.overlaySettings.afkTitleTimeType.@Tooltip": "If set to 'INFINITY', the AFK Title will stay until the player moves again. If set to 'SECONDS', the AFK Title will stay for the specified amount of seconds.", + "text.autoconfig.elements-utils.option.overlaySettings.afkTitleTimeSeconds": "AFK Title Time (Seconds)", "text.autoconfig.elements-utils.option.devUtils": "Development Utils (please ignore)", "text.autoconfig.elements-utils.option.devUtils.enable": "Enabled", From 88209c29d7c894124b403cc6d6c65cf75133888f Mon Sep 17 00:00:00 2001 From: nichtDanger Date: Sun, 3 Aug 2025 14:36:22 +0200 Subject: [PATCH 2/4] luckyDropSummary + customizeable Seconds for XP/s --- .../eposs/elementsutils/config/ModConfig.java | 10 ++ .../feature/xpformat/FarmingXpTracker.java | 11 +- .../mixin/client/ChatHudMixin.java | 123 ++++++++++++++++++ .../elements-utils.client.mixins.json | 1 + .../assets/elements-utils/lang/de_de.json | 6 + .../assets/elements-utils/lang/en_us.json | 6 + 6 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 src/client/java/dev/eposs/elementsutils/mixin/client/ChatHudMixin.java diff --git a/src/client/java/dev/eposs/elementsutils/config/ModConfig.java b/src/client/java/dev/eposs/elementsutils/config/ModConfig.java index bdbc5b1..4b80b2a 100644 --- a/src/client/java/dev/eposs/elementsutils/config/ModConfig.java +++ b/src/client/java/dev/eposs/elementsutils/config/ModConfig.java @@ -94,6 +94,14 @@ public enum Position { } + @ConfigEntry.Gui.CollapsibleObject + public ChatEnhancementsConfig chatEnhancements = new ChatEnhancementsConfig(); + + public static class ChatEnhancementsConfig { + public boolean showLuckyDropSummary = false; + public int luckyDropSummaryMinutes = 60; + } + @ConfigEntry.Gui.CollapsibleObject public PlayerEnhancementsConfig playerEnhancements = new PlayerEnhancementsConfig(); @@ -119,6 +127,8 @@ public static class ElementsXPConfig { public boolean enabled = true; public boolean showXpPerSecond = false; public KnownColor xpPerSecondColor = KnownColor.GRAY; + public int maxAgeSeconds = 20; + public int resetTimeoutSeconds = 5; public KnownColor overlayMessageColor = KnownColor.DARK_AQUA; public boolean hideMaxPetXP = false; public int overlayMessageYOffset = 0; diff --git a/src/client/java/dev/eposs/elementsutils/feature/xpformat/FarmingXpTracker.java b/src/client/java/dev/eposs/elementsutils/feature/xpformat/FarmingXpTracker.java index 5fc3cb7..02c803e 100644 --- a/src/client/java/dev/eposs/elementsutils/feature/xpformat/FarmingXpTracker.java +++ b/src/client/java/dev/eposs/elementsutils/feature/xpformat/FarmingXpTracker.java @@ -1,5 +1,6 @@ package dev.eposs.elementsutils.feature.xpformat; +import dev.eposs.elementsutils.config.ModConfig; import java.util.ArrayList; import java.util.List; @@ -14,16 +15,18 @@ public class FarmingXpTracker { /** * Updates the tracker with the current XP value. - * Resets the history if more than 5 seconds have passed since the last update. + * Resets the history if more than resetTimeoutSeconds have passed since the last update. * Only stores values if the XP has changed. - * Removes values older than 20 seconds. + * Removes values older than maxAgeSeconds. * * @param currentXp the current XP value */ public static void update(int currentXp) { long now = System.currentTimeMillis(); + int maxAgeSeconds = ModConfig.getConfig().elementsXPConfig.maxAgeSeconds; + int resetTimeoutSeconds = ModConfig.getConfig().elementsXPConfig.resetTimeoutSeconds; - if (now - lastMessageTime > 5000) { + if (now - lastMessageTime > resetTimeoutSeconds * 1000L) { timestamps.clear(); xpValues.clear(); lastXp = currentXp; @@ -36,7 +39,7 @@ public static void update(int currentXp) { } lastMessageTime = now; - while (!timestamps.isEmpty() && now - timestamps.getFirst() > 20000) { + while (!timestamps.isEmpty() && now - timestamps.getFirst() > maxAgeSeconds * 1000L) { timestamps.removeFirst(); xpValues.removeFirst(); } diff --git a/src/client/java/dev/eposs/elementsutils/mixin/client/ChatHudMixin.java b/src/client/java/dev/eposs/elementsutils/mixin/client/ChatHudMixin.java new file mode 100644 index 0000000..0399f33 --- /dev/null +++ b/src/client/java/dev/eposs/elementsutils/mixin/client/ChatHudMixin.java @@ -0,0 +1,123 @@ +package dev.eposs.elementsutils.mixin.client; + +import dev.eposs.elementsutils.config.ModConfig; +import net.minecraft.client.gui.hud.ChatHud; +import net.minecraft.text.MutableText; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Mixin(ChatHud.class) +public abstract class ChatHudMixin { + /** + * Pattern to match Lucky Drop messages in chat. + */ + @Unique + private static final Pattern LUCKY_DROP_PATTERN = Pattern.compile("Server: Du hast einen Lucky Drop in Höhe von (\\d+) Leveln bekommen\\."); + + /** + * Stores the history of Lucky Drop events as timestamp and level pairs. + */ + @Unique + private static final List> luckyDropHistory = new ArrayList<>(); + + /** + * Milliseconds per minute constant. + */ + @Unique + private static final long MILLIS_PER_MINUTE = 60_000L; + + /** + * Replaces the first occurrence of the original string in the given text with the replacement, + * preserving text formatting and siblings. + * + * @param text The original text object. + * @param original The string to be replaced. + * @param replacement The replacement string. + * @return A new MutableText with the replacement applied. + */ + @Unique + private MutableText replaceLevelInText(Text text, String original, String replacement) { + MutableText result = Text.empty(); + + if (text.getSiblings().isEmpty()) { + String content = text.getString() != null ? text.getString() : ""; + int idx = content.indexOf(original); + + if (idx != -1) { + if (idx > 0) { + result.append(Text.literal(content.substring(0, idx)).setStyle(text.getStyle())); + } + result.append(Text.literal(replacement).setStyle(text.getStyle())); + int endIdx = idx + original.length(); + if (endIdx < content.length()) { + result.append(Text.literal(content.substring(endIdx)).setStyle(text.getStyle())); + } + } else if (!content.isEmpty()) { + result.append(Text.literal(content).setStyle(text.getStyle())); + } + } else { + MutableText contentFirst = text.copy(); + contentFirst.getSiblings().clear(); + result.append(Text.literal(contentFirst.getString()).setStyle(text.getStyle())); + } + + for (Text sibling : text.getSiblings()) { + result.append(replaceLevelInText(sibling, original, replacement)); + } + + return result; + } + + /** + * Injects into the chat message handling to add a Lucky Drop summary if enabled in the config. + * + * @param message The chat message. + * @param ci The callback info. + */ + @Inject(method = "addMessage*", at = @At("HEAD"), cancellable = true) + private void onAddMessage(Text message, CallbackInfo ci) { + if (!ModConfig.getConfig().chatEnhancements.showLuckyDropSummary) { + return; + } + String msg = message.getString(); + if (msg.matches(".*Level/\\d+[Mm]in\\).*")) { + return; + } + Matcher matcher = LUCKY_DROP_PATTERN.matcher(msg); + if (matcher.find()) { + int level = Integer.parseInt(matcher.group(1)); + long now = System.currentTimeMillis(); + luckyDropHistory.add(new AbstractMap.SimpleEntry<>(now, level)); + luckyDropHistory.removeIf(entry -> now - entry.getKey() > ModConfig.getConfig().chatEnhancements.luckyDropSummaryMinutes * MILLIS_PER_MINUTE); + int sum = luckyDropHistory.stream().mapToInt(Map.Entry::getValue).sum(); + + java.text.NumberFormat nf = java.text.NumberFormat.getInstance(java.util.Locale.GERMAN); + String formattedLevel = nf.format(level); + String formattedSum = nf.format(sum); + + String original = matcher.group(1); + MutableText formattedText = replaceLevelInText(message, original, formattedLevel); + + MutableText prefix = Text.literal(" (").styled(style -> style.withColor(Formatting.GRAY)); + MutableText sumText = Text.literal(formattedSum + " Level").styled(style -> style.withColor(Formatting.AQUA)); + MutableText suffix = Text.literal("/" + ModConfig.getConfig().chatEnhancements.luckyDropSummaryMinutes + "Min)").styled(style -> style.withColor(Formatting.GRAY)); + + MutableText newMsg = formattedText.append(prefix).append(sumText).append(suffix); + + ((ChatHud)(Object)this).addMessage(newMsg); + ci.cancel(); + } + } +} \ No newline at end of file diff --git a/src/client/resources/elements-utils.client.mixins.json b/src/client/resources/elements-utils.client.mixins.json index 95c2fc9..42f18a4 100644 --- a/src/client/resources/elements-utils.client.mixins.json +++ b/src/client/resources/elements-utils.client.mixins.json @@ -4,6 +4,7 @@ "compatibilityLevel": "JAVA_21", "client": [ "ArmorFeatureRendererMixin", + "ChatHudMixin", "ExampleClientMixin", "InGameHudAccessor", "InGameHudMixin", diff --git a/src/main/resources/assets/elements-utils/lang/de_de.json b/src/main/resources/assets/elements-utils/lang/de_de.json index af1d699..973979e 100644 --- a/src/main/resources/assets/elements-utils/lang/de_de.json +++ b/src/main/resources/assets/elements-utils/lang/de_de.json @@ -26,6 +26,10 @@ "text.autoconfig.elements-utils.option.showBaseDisplay": "Spieler Base Grenzen anzeigen", + "text.autoconfig.elements-utils.option.chatEnhancements": "Chat Verbesserungen", + "text.autoconfig.elements-utils.option.chatEnhancements.showLuckyDropSummary": "Lucky Drop Zusammenfassung anzeigen", + "text.autoconfig.elements-utils.option.chatEnhancements.luckyDropSummaryMinutes": "Lucky Drop Zusammenfassung Minuten", + "text.autoconfig.elements-utils.option.playerEnhancements": "Spieler Verbesserungen", "text.autoconfig.elements-utils.option.playerEnhancements.hideOwnArmor": "Eigene Rüstung ausblenden", "text.autoconfig.elements-utils.option.playerEnhancements.hideHelmet": "Helm ausblenden", @@ -49,6 +53,8 @@ "text.autoconfig.elements-utils.option.elementsXPConfig": "Elements XP Optionen", "text.autoconfig.elements-utils.option.elementsXPConfig.enabled": "Formatiere Zahlen der Elements-XP", "text.autoconfig.elements-utils.option.elementsXPConfig.showXpPerSecond": "XP/s anzeigen", + "text.autoconfig.elements-utils.option.elementsXPConfig.maxAgeSeconds": "XP/s Max Daten Alter (Sekunden)", + "text.autoconfig.elements-utils.option.elementsXPConfig.resetTimeoutSeconds": "XP/s Daten Reset Timeout (Sekunden)", "text.autoconfig.elements-utils.option.elementsXPConfig.xpPerSecondColor": "XP/s Farbe", "text.autoconfig.elements-utils.option.elementsXPConfig.hideMaxPetXP": "Max Pet-XP ausblenden", "text.autoconfig.elements-utils.option.elementsXPConfig.overlayMessageColor": "Farbe der Overlay-Nachricht", diff --git a/src/main/resources/assets/elements-utils/lang/en_us.json b/src/main/resources/assets/elements-utils/lang/en_us.json index bfc99da..7050ce9 100644 --- a/src/main/resources/assets/elements-utils/lang/en_us.json +++ b/src/main/resources/assets/elements-utils/lang/en_us.json @@ -26,6 +26,10 @@ "text.autoconfig.elements-utils.option.showBaseDisplay": "Show Player Base Borders", + "text.autoconfig.elements-utils.option.chatEnhancements": "Chat Enhancements", + "text.autoconfig.elements-utils.option.chatEnhancements.showLuckyDropSummary": "Show Lucky Drop Summary", + "text.autoconfig.elements-utils.option.chatEnhancements.luckyDropSummaryMinutes": "Lucky Drop Summary Minutes", + "text.autoconfig.elements-utils.option.playerEnhancements": "Player Enhancements", "text.autoconfig.elements-utils.option.playerEnhancements.hideOwnArmor": "Hide Own Armor", "text.autoconfig.elements-utils.option.playerEnhancements.hideHelmet": "Hide Helmet", @@ -49,6 +53,8 @@ "text.autoconfig.elements-utils.option.elementsXPConfig": "Elements XP Options", "text.autoconfig.elements-utils.option.elementsXPConfig.enabled": "Format Numbers of Elements XP's", "text.autoconfig.elements-utils.option.elementsXPConfig.showXpPerSecond": "Show XP/s", + "text.autoconfig.elements-utils.option.elementsXPConfig.maxAgeSeconds": "XP/s Max Data Age (Seconds)", + "text.autoconfig.elements-utils.option.elementsXPConfig.resetTimeoutSeconds": "XP/s Data Reset Timeout (Seconds)", "text.autoconfig.elements-utils.option.elementsXPConfig.xpPerSecondColor": "XP/s Color", "text.autoconfig.elements-utils.option.elementsXPConfig.hideMaxPetXP": "Hide Max Pet XP", "text.autoconfig.elements-utils.option.elementsXPConfig.overlayMessageColor": "Overlay Message Color", From 28dae53970f79c014f1df7e448e8742124bd2094 Mon Sep 17 00:00:00 2001 From: nichtDanger <43505534+nichtDanger@users.noreply.github.com> Date: Sun, 3 Aug 2025 15:15:49 +0200 Subject: [PATCH 3/4] Update README.md --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b72b4cf..de8409d 100644 --- a/README.md +++ b/README.md @@ -26,15 +26,19 @@ _Elements 3 - 1.21.4_ - XP Meter (Basalt) - Measure Basalt Gen's with keybind (default: none) and target XP / target time (configurable) +- Chat Enhancements + - show a LuckyDrop Summary within the LuckyDrop Message for a customizable Time (Minutes) +- Player Enhancements + - Option to hide the Own Armor or Armorpieces (Cosmetic) - Player Level Enhancements - - Option to format level with dots as thousands separators (Player Level and Level in Player List) + - Option to format level with dots as thousands separators (Player Level, Top List Level and Level in Player List) - Option to change the color of the player levels - Option to change the color of the levels in the player list -- Player XP Enhancements +- Elements XP Enhancements - Option to format XP with dots as thousands separators - Option to change the color of the XP display - Option to hide the max pet - - Option to show a XP/s indicator + - Option to show a XP/s indicator with customizable Calculation - Option to make a Y-Axis offset for the XP display - Excalibur Time Display - Show the time until the next Excalibur is available From 2da2550c328463280752661888383ce45cd71a65 Mon Sep 17 00:00:00 2001 From: nichtDanger <43505534+nichtDanger@users.noreply.github.com> Date: Sun, 3 Aug 2025 15:21:19 +0200 Subject: [PATCH 4/4] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index de8409d..a4d2bc1 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ _Elements 3 - 1.21.4_ - Option to hide the max pet - Option to show a XP/s indicator with customizable Calculation - Option to make a Y-Axis offset for the XP display +- Overlay Settings + - Option for Override the AFK-Title Stay Time - INFINITY or Seconds - Excalibur Time Display - Show the time until the next Excalibur is available - Show the next Player who can pull Excalibur