Initial Fabric 26.2 port

This commit is contained in:
Darto KLoning
2026-09-28 08:24:43 +07:00
commit 5a2d63c282
28 changed files with 983 additions and 0 deletions
@@ -0,0 +1,8 @@
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 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;}}
}
@@ -0,0 +1,3 @@
package whirlfrenzy.itemdespawntimer.client;
import java.util.concurrent.ConcurrentHashMap;
public final class ClientTimerData { public record Timer(int age,int lifespan,long receivedAtMs){} private static final ConcurrentHashMap<Integer,Timer> DATA=new ConcurrentHashMap<>(); public static void put(int id,int age,int life){DATA.put(id,new Timer(age,life,System.currentTimeMillis()));} public static Timer get(int id){return DATA.get(id);} }
@@ -0,0 +1,3 @@
package whirlfrenzy.itemdespawntimer.client;
import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; import whirlfrenzy.itemdespawntimer.network.TimerSyncPayload;
public final class ItemDespawnTimerClient implements ClientModInitializer { @Override public void onInitializeClient(){ ClientConfig.load(); ClientPlayNetworking.registerGlobalReceiver(TimerSyncPayload.TYPE,(p,c)->ClientTimerData.put(p.entityId(),p.age(),p.lifespan())); } }
@@ -0,0 +1,2 @@
package whirlfrenzy.itemdespawntimer.client;
public interface TimerRenderStateAccess { void itemDespawnTimer$set(int entityId,String name); int itemDespawnTimer$id(); String itemDespawnTimer$name(); }
@@ -0,0 +1,3 @@
package whirlfrenzy.itemdespawntimer.mixin.client;
import net.minecraft.client.renderer.entity.state.ItemEntityRenderState; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; import whirlfrenzy.itemdespawntimer.client.TimerRenderStateAccess;
@Mixin(ItemEntityRenderState.class) public class ItemEntityRenderStateMixin implements TimerRenderStateAccess { @Unique private int itemDespawnTimer$id=-1; @Unique private String itemDespawnTimer$name=""; public void itemDespawnTimer$set(int id,String name){this.itemDespawnTimer$id=id;this.itemDespawnTimer$name=name;} public int itemDespawnTimer$id(){return itemDespawnTimer$id;} public String itemDespawnTimer$name(){return itemDespawnTimer$name;} }
@@ -0,0 +1,31 @@
package whirlfrenzy.itemdespawntimer.mixin.client;
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.network.chat.Component;
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.callback.CallbackInfo;
import whirlfrenzy.itemdespawntimer.client.*;
@Mixin(ItemEntityRenderer.class)
public abstract class ItemEntityRendererMixin {
@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()); }
@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;
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();
}
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 @@
{"required":true,"package":"whirlfrenzy.itemdespawntimer.mixin.client","compatibilityLevel":"JAVA_25","client":["ItemEntityRendererMixin","ItemEntityRenderStateMixin"],"injectors":{"defaultRequire":1}}
@@ -0,0 +1,19 @@
package whirlfrenzy.itemdespawntimer;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry;
import net.minecraft.resources.Identifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import whirlfrenzy.itemdespawntimer.config.ServerConfig;
import whirlfrenzy.itemdespawntimer.network.TimerSyncPayload;
public final class ItemDespawnTimer implements ModInitializer {
public static final String MOD_ID = "item-despawn-timer";
public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
public static Identifier id(String path) { return Identifier.fromNamespaceAndPath(MOD_ID, path); }
@Override public void onInitialize() {
ServerConfig.load();
PayloadTypeRegistry.clientboundPlay().register(TimerSyncPayload.TYPE, TimerSyncPayload.CODEC);
}
}
@@ -0,0 +1,33 @@
package whirlfrenzy.itemdespawntimer.config;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.entity.item.ItemEntity;
import whirlfrenzy.itemdespawntimer.ItemDespawnTimer;
import java.io.*; import java.nio.file.*; import java.util.*;
public final class ServerConfig {
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private static Data data = new Data();
public static final class Data {
public int defaultDespawnSeconds = 300;
public boolean allowInfiniteLifetime = true;
public Map<String,Integer> itemDespawnSeconds = new LinkedHashMap<>();
}
private ServerConfig() {}
public static void load() {
Path p = FabricLoader.getInstance().getConfigDir().resolve("item-despawn-timer-server.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 { Files.createDirectories(p.getParent()); try(Writer w=Files.newBufferedWriter(p)){GSON.toJson(data,w);} }
} catch(Exception e){ ItemDespawnTimer.LOGGER.error("Cannot load server config",e); }
}
public static int lifespanTicks(ItemEntity entity) {
String id = BuiltInRegistries.ITEM.getKey(entity.getItem().getItem()).toString();
int seconds = data.itemDespawnSeconds.getOrDefault(id, data.defaultDespawnSeconds);
if (seconds < 0 && data.allowInfiniteLifetime) return Integer.MAX_VALUE;
return Math.max(1, seconds) * 20;
}
}
@@ -0,0 +1,32 @@
package whirlfrenzy.itemdespawntimer.mixin;
import net.fabricmc.fabric.api.networking.v1.PlayerLookup;
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.item.ItemEntity;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import whirlfrenzy.itemdespawntimer.config.ServerConfig;
import whirlfrenzy.itemdespawntimer.network.TimerSyncPayload;
@Mixin(ItemEntity.class)
public abstract class ItemEntityMixin {
@Shadow private int age;
@ModifyConstant(method="tick", constant=@Constant(intValue=6000))
private int itemDespawnTimer$serverLifetime(int vanillaLifetime) {
return ServerConfig.lifespanTicks((ItemEntity)(Object)this);
}
@Inject(method="tick", at=@At("TAIL"))
private void itemDespawnTimer$syncTimer(CallbackInfo ci) {
ItemEntity self=(ItemEntity)(Object)this;
if (!self.level().isClientSide() && (self.tickCount % 20 == 0 || self.tickCount < 3)) {
int lifespan=ServerConfig.lifespanTicks(self);
TimerSyncPayload payload=new TimerSyncPayload(self.getId(), this.age, lifespan);
for (ServerPlayer player : PlayerLookup.tracking(self)) ServerPlayNetworking.send(player,payload);
}
}
}
@@ -0,0 +1,17 @@
package whirlfrenzy.itemdespawntimer.network;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import whirlfrenzy.itemdespawntimer.ItemDespawnTimer;
public record TimerSyncPayload(int entityId, int age, int lifespan) implements CustomPacketPayload {
public static final Type<TimerSyncPayload> TYPE = new Type<>(ItemDespawnTimer.id("timer_sync"));
public static final StreamCodec<RegistryFriendlyByteBuf, TimerSyncPayload> CODEC = StreamCodec.composite(
ByteBufCodecs.VAR_INT, TimerSyncPayload::entityId,
ByteBufCodecs.VAR_INT, TimerSyncPayload::age,
ByteBufCodecs.VAR_INT, TimerSyncPayload::lifespan,
TimerSyncPayload::new);
@Override public Type<? extends CustomPacketPayload> type() { return TYPE; }
}
+17
View File
@@ -0,0 +1,17 @@
{
"schemaVersion": 1,
"id": "item-despawn-timer",
"version": "${version}",
"name": "Item Despawn Timer",
"description": "Server-authoritative item despawn times with client-only countdown presentation settings.",
"environment": "*",
"entrypoints": {
"main": ["whirlfrenzy.itemdespawntimer.ItemDespawnTimer"],
"client": ["whirlfrenzy.itemdespawntimer.client.ItemDespawnTimerClient"]
},
"mixins": [
"item-despawn-timer.mixins.json",
{ "config": "item-despawn-timer.client.mixins.json", "environment": "client" }
],
"depends": { "fabricloader": ">=0.19.3", "minecraft": "~26.2", "java": ">=25", "fabric-api": "*" }
}
@@ -0,0 +1 @@
{"required":true,"package":"whirlfrenzy.itemdespawntimer.mixin","compatibilityLevel":"JAVA_25","mixins":["ItemEntityMixin"],"injectors":{"defaultRequire":1}}