clock image

This commit is contained in:
Darto KLoning
2026-09-28 19:07:17 +07:00
parent 2342c07072
commit 1e9c190c06
7 changed files with 82 additions and 13 deletions
+5 -1
View File
@@ -9,7 +9,7 @@ Minecraft 26.2 uses the unobfuscated Mojang/official names and Java 25. This por
Run `compiler.bat` on Windows or `./compiler.sh` on Linux/macOS. The script checks whether the existing JAR contains the same `mod_version+mc<minecraft_version>` as `gradle.properties`; matching builds offer Push/Recompile/Quit. After a successful build it offers Git push and retries failed pushes in groups of three.
## Commands (v0.3.2)
## Commands (v0.3.4)
`/idt reload`
@@ -17,3 +17,7 @@ Run `compiler.bat` on Windows or `./compiler.sh` on Linux/macOS. The script chec
- Client player (operator/moderator): reloads the client config and requests a server-config reload. The server validates permission before accepting the request.
- Dedicated-server console: reloads only `item-despawn-timer-server.json`.
- The server-side `/idt reload` node requires Minecraft moderator/operator permission, so it is not advertised by the server command tree to ordinary players. The client-side command remains available because ordinary players are allowed to reload their own presentation config.
## Client display colors
`clockColor`, `timerTextColor`, and `nameTextColor` use `#RRGGBB` values. The bundled `clock.png` is rendered as a bitmap glyph on the same baseline as the countdown text.
+12 -1
View File
@@ -1 +1,12 @@
{"timerVisible":true,"nameVisible":false,"timerLabelHeight":0.75,"nameLabelHeight":1.1,"useTimerVisibilityThreshold":false,"timerVisibilityThresholdSeconds":300,"timerTextColor":"#FFFFFF","nameTextColor":"#FFFFFF","backgroundAlpha":64}
{
"timerVisible": true,
"nameVisible": false,
"timerLabelHeight": 0.75,
"nameLabelHeight": 1.1,
"useTimerVisibilityThreshold": false,
"timerVisibilityThresholdSeconds": 300,
"clockColor": "#FFFFFF",
"timerTextColor": "#FFFFFF",
"nameTextColor": "#FFFFFF",
"backgroundAlpha": 64
}
+1 -1
View File
@@ -3,6 +3,6 @@ org.gradle.configuration-cache=false
minecraft_version=26.2
loader_version=0.19.3
fabric_version=0.160.0+26.2
mod_version=0.3.2
mod_version=0.3.4
maven_group=whirlfrenzy.itemdespawntimer
archives_base_name=item-despawn-timer
@@ -2,7 +2,13 @@ package whirlfrenzy.itemdespawntimer.client;
import com.google.gson.*; import net.fabricmc.loader.api.FabricLoader; import java.io.*; import java.nio.file.*;
public final class ClientConfig {
private static final Gson GSON=new GsonBuilder().setPrettyPrinting().create(); public static Data data=new Data();
public static final class Data { public boolean timerVisible=true; public boolean nameVisible=false; public float timerLabelHeight=0.75f; public float nameLabelHeight=1.1f; public boolean useTimerVisibilityThreshold=false; public int timerVisibilityThresholdSeconds=300; public String timerTextColor="#FFFFFF"; public String nameTextColor="#FFFFFF"; public int backgroundAlpha=64; }
public static final class Data {
public boolean timerVisible=true; public boolean nameVisible=false;
public float timerLabelHeight=0.75f; public float nameLabelHeight=1.1f;
public boolean useTimerVisibilityThreshold=false; public int timerVisibilityThresholdSeconds=300;
public String clockColor="#FFFFFF"; public String timerTextColor="#FFFFFF"; public String nameTextColor="#FFFFFF";
public int backgroundAlpha=64;
}
public static void load(){ Path p=FabricLoader.getInstance().getConfigDir().resolve("item-despawn-timer-client.json"); try{if(Files.exists(p)){try(Reader r=Files.newBufferedReader(p)){Data d=GSON.fromJson(r,Data.class);if(d!=null)data=d;}}else{try(Writer w=Files.newBufferedWriter(p)){GSON.toJson(data,w);}}}catch(Exception ignored){}}
public static int color(String s){try{return 0xFF000000|Integer.parseInt(s.replace("#",""),16);}catch(Exception e){return 0xFFFFFFFF;}}
}
@@ -4,28 +4,63 @@ import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.SubmitNodeCollector;
import net.minecraft.client.renderer.entity.ItemEntityRenderer;
import net.minecraft.client.renderer.state.level.CameraRenderState;
import net.minecraft.client.renderer.entity.state.ItemEntityRenderState;
import net.minecraft.client.renderer.state.level.CameraRenderState;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.FontDescription;
import net.minecraft.network.chat.MutableComponent;
import net.minecraft.resources.Identifier;
import net.minecraft.world.entity.item.ItemEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import whirlfrenzy.itemdespawntimer.client.*;
import whirlfrenzy.itemdespawntimer.client.ClientConfig;
import whirlfrenzy.itemdespawntimer.client.ClientTimerData;
import whirlfrenzy.itemdespawntimer.client.TimerRenderStateAccess;
@Mixin(ItemEntityRenderer.class)
public abstract class ItemEntityRendererMixin {
private static final String CLOCK_GLYPH = "\uE000";
private static final FontDescription CLOCK_FONT =
new FontDescription.Resource(Identifier.fromNamespaceAndPath("item-despawn-timer", "clock"));
@Inject(method="extractRenderState", at=@At("TAIL"))
private void itemDespawnTimer$extract(ItemEntity entity, ItemEntityRenderState state, float partialTick, CallbackInfo ci){ ((TimerRenderStateAccess)(Object)state).itemDespawnTimer$set(entity.getId(),entity.getItem().getHoverName().getString()); }
private void itemDespawnTimer$extract(ItemEntity entity, ItemEntityRenderState state, float partialTick, CallbackInfo ci) {
((TimerRenderStateAccess)(Object)state).itemDespawnTimer$set(entity.getId(),entity.getItem().getHoverName().getString());
}
@Inject(method="submit", at=@At("TAIL"))
private void itemDespawnTimer$submit(ItemEntityRenderState state, PoseStack pose, SubmitNodeCollector queue, CameraRenderState camera, CallbackInfo ci){
TimerRenderStateAccess ext=(TimerRenderStateAccess)(Object)state; ClientTimerData.Timer timer=ClientTimerData.get(ext.itemDespawnTimer$id()); if(timer==null)return;
long elapsed=Math.max(0,System.currentTimeMillis()-timer.receivedAtMs()); int age=timer.age()+(int)(elapsed/50L); int remainTicks=timer.lifespan()==Integer.MAX_VALUE?Integer.MAX_VALUE:Math.max(0,timer.lifespan()-age); int seconds=remainTicks==Integer.MAX_VALUE?Integer.MAX_VALUE:(remainTicks+19)/20;
private void itemDespawnTimer$submit(ItemEntityRenderState state, PoseStack pose, SubmitNodeCollector queue, CameraRenderState camera, CallbackInfo ci) {
TimerRenderStateAccess ext=(TimerRenderStateAccess)(Object)state;
ClientTimerData.Timer timer=ClientTimerData.get(ext.itemDespawnTimer$id()); if(timer==null)return;
long elapsed=Math.max(0,System.currentTimeMillis()-timer.receivedAtMs());
int age=timer.age()+(int)(elapsed/50L);
int remainTicks=timer.lifespan()==Integer.MAX_VALUE?Integer.MAX_VALUE:Math.max(0,timer.lifespan()-age);
int seconds=remainTicks==Integer.MAX_VALUE?Integer.MAX_VALUE:(remainTicks+19)/20;
var cfg=ClientConfig.data; var font=Minecraft.getInstance().font;
if(cfg.timerVisible && (!cfg.useTimerVisibilityThreshold || seconds<=cfg.timerVisibilityThresholdSeconds)){
String s=seconds==Integer.MAX_VALUE?"∞":"◷ "+seconds+"s"; float w=font.width(s); pose.pushPose(); pose.translate(0,cfg.timerLabelHeight,0); pose.mulPose(camera.orientation); pose.scale(0.025f,-0.025f,0.025f); queue.submitText(pose,-w/2f,0,Component.literal(s).getVisualOrderText(),false,net.minecraft.client.gui.Font.DisplayMode.SEE_THROUGH,state.lightCoords,ClientConfig.color(cfg.timerTextColor),(cfg.backgroundAlpha&255)<<24,0); pose.popPose();
String countdown=seconds==Integer.MAX_VALUE?"∞":seconds+"s";
MutableComponent icon=Component.literal(CLOCK_GLYPH).withStyle(style -> style
.withFont(CLOCK_FONT).withColor(ClientConfig.color(cfg.clockColor)&0xFFFFFF));
MutableComponent timerText=Component.literal(" "+countdown).withStyle(style -> style
.withColor(ClientConfig.color(cfg.timerTextColor)&0xFFFFFF));
MutableComponent line=Component.empty().append(icon).append(timerText);
float w=font.width(line);
pose.pushPose(); pose.translate(0,cfg.timerLabelHeight,0); pose.mulPose(camera.orientation); pose.scale(0.025f,-0.025f,0.025f);
queue.submitText(pose,-w/2f,0,line.getVisualOrderText(),false,
net.minecraft.client.gui.Font.DisplayMode.SEE_THROUGH,state.lightCoords,
0xFFFFFFFF,(cfg.backgroundAlpha&255)<<24,0);
pose.popPose();
}
if(cfg.nameVisible){
String s=ext.itemDespawnTimer$name(); float w=font.width(s);
pose.pushPose(); pose.translate(0,cfg.nameLabelHeight,0); pose.mulPose(camera.orientation); pose.scale(0.025f,-0.025f,0.025f);
queue.submitText(pose,-w/2f,0,Component.literal(s).getVisualOrderText(),false,
net.minecraft.client.gui.Font.DisplayMode.SEE_THROUGH,state.lightCoords,
ClientConfig.color(cfg.nameTextColor),(cfg.backgroundAlpha&255)<<24,0);
pose.popPose();
}
if(cfg.nameVisible){String s=ext.itemDespawnTimer$name();float w=font.width(s);pose.pushPose();pose.translate(0,cfg.nameLabelHeight,0);pose.mulPose(camera.orientation);pose.scale(0.025f,-0.025f,0.025f);queue.submitText(pose,-w/2f,0,Component.literal(s).getVisualOrderText(),false,net.minecraft.client.gui.Font.DisplayMode.SEE_THROUGH,state.lightCoords,ClientConfig.color(cfg.nameTextColor),(cfg.backgroundAlpha&255)<<24,0);pose.popPose();}
}
}
@@ -0,0 +1,13 @@
{
"providers": [
{
"type": "bitmap",
"file": "item-despawn-timer:font/clock.png",
"ascent": 8,
"height": 9,
"chars": [
""
]
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 278 B