diff --git a/README.md b/README.md index bca4554..fb47a2a 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,12 @@ This branch is Fabric-only and intentionally separates authority: Minecraft 26.2 uses the unobfuscated Mojang/official names and Java 25. This port therefore does not reuse the old Yarn-named common/client GUI classes from 0.2.2. Client rendering lives only in `src/client`, while server/gameplay code lives in `src/main`. Run `compiler.bat` on Windows or `./compiler.sh` on Linux/macOS. The script checks whether the existing JAR contains the same `mod_version+mc` 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) + +`/idt reload` + +- Client player (non-operator): reloads only `item-despawn-timer-client.json`. +- 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. diff --git a/gradle.properties b/gradle.properties index d2e663f..ff05e6b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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.1 +mod_version=0.3.2 maven_group=whirlfrenzy.itemdespawntimer archives_base_name=item-despawn-timer diff --git a/src/client/java/whirlfrenzy/itemdespawntimer/client/ItemDespawnTimerClient.java b/src/client/java/whirlfrenzy/itemdespawntimer/client/ItemDespawnTimerClient.java index 4f7ba7e..9dadf32 100644 --- a/src/client/java/whirlfrenzy/itemdespawntimer/client/ItemDespawnTimerClient.java +++ b/src/client/java/whirlfrenzy/itemdespawntimer/client/ItemDespawnTimerClient.java @@ -1,3 +1,17 @@ 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())); } } + +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import whirlfrenzy.itemdespawntimer.client.command.ItemDespawnTimerClientCommands; +import whirlfrenzy.itemdespawntimer.network.TimerSyncPayload; + +public final class ItemDespawnTimerClient implements ClientModInitializer { + @Override + public void onInitializeClient() { + ClientConfig.load(); + ItemDespawnTimerClientCommands.register(); + ClientPlayNetworking.registerGlobalReceiver( + TimerSyncPayload.TYPE, + (payload, context) -> ClientTimerData.put(payload.entityId(), payload.age(), payload.lifespan())); + } +} diff --git a/src/client/java/whirlfrenzy/itemdespawntimer/client/command/ItemDespawnTimerClientCommands.java b/src/client/java/whirlfrenzy/itemdespawntimer/client/command/ItemDespawnTimerClientCommands.java new file mode 100644 index 0000000..8a760e9 --- /dev/null +++ b/src/client/java/whirlfrenzy/itemdespawntimer/client/command/ItemDespawnTimerClientCommands.java @@ -0,0 +1,33 @@ +package whirlfrenzy.itemdespawntimer.client.command; + +import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback; +import net.fabricmc.fabric.api.client.command.v2.ClientCommands; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.minecraft.network.chat.Component; +import whirlfrenzy.itemdespawntimer.client.ClientConfig; +import whirlfrenzy.itemdespawntimer.network.ReloadConfigPayload; + +/** + * Client-side /idt reload is intentionally available to every player. + * It always reloads the local presentation config. A C2S request is also sent; + * the server silently accepts it only when the player has operator/moderator permission. + */ +public final class ItemDespawnTimerClientCommands { + private ItemDespawnTimerClientCommands() {} + + public static void register() { + ClientCommandRegistrationCallback.EVENT.register((dispatcher, registryAccess) -> + dispatcher.register(ClientCommands.literal("idt") + .then(ClientCommands.literal("reload") + .executes(context -> { + ClientConfig.load(); + context.getSource().sendFeedback( + Component.literal("[Item Despawn Timer] Client config reloaded.")); + + if (ClientPlayNetworking.canSend(ReloadConfigPayload.TYPE)) { + ClientPlayNetworking.send(new ReloadConfigPayload(true)); + } + return 1; + })))); + } +} diff --git a/src/main/java/whirlfrenzy/itemdespawntimer/ItemDespawnTimer.java b/src/main/java/whirlfrenzy/itemdespawntimer/ItemDespawnTimer.java index 9dc43e0..b2ead8f 100644 --- a/src/main/java/whirlfrenzy/itemdespawntimer/ItemDespawnTimer.java +++ b/src/main/java/whirlfrenzy/itemdespawntimer/ItemDespawnTimer.java @@ -2,18 +2,43 @@ package whirlfrenzy.itemdespawntimer; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; +import net.minecraft.network.chat.Component; import net.minecraft.resources.Identifier; +import net.minecraft.server.permissions.Permissions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import whirlfrenzy.itemdespawntimer.command.ItemDespawnTimerCommands; import whirlfrenzy.itemdespawntimer.config.ServerConfig; +import whirlfrenzy.itemdespawntimer.network.ReloadConfigPayload; 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() { + + 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); + PayloadTypeRegistry.serverboundPlay().register(ReloadConfigPayload.TYPE, ReloadConfigPayload.CODEC); + + ServerPlayNetworking.registerGlobalReceiver(ReloadConfigPayload.TYPE, (payload, context) -> { + if (!payload.requested()) return; + + // Never trust the client: only an operator/moderator may reload server configuration. + if (!context.player().permissions().hasPermission(Permissions.COMMANDS_MODERATOR)) return; + + ServerConfig.load(); + context.player().sendSystemMessage( + Component.literal("[Item Despawn Timer] Client and server configs reloaded.")); + }); + + ItemDespawnTimerCommands.register(); } } diff --git a/src/main/java/whirlfrenzy/itemdespawntimer/command/ItemDespawnTimerCommands.java b/src/main/java/whirlfrenzy/itemdespawntimer/command/ItemDespawnTimerCommands.java new file mode 100644 index 0000000..665c966 --- /dev/null +++ b/src/main/java/whirlfrenzy/itemdespawntimer/command/ItemDespawnTimerCommands.java @@ -0,0 +1,29 @@ +package whirlfrenzy.itemdespawntimer.command; + +import com.mojang.brigadier.context.CommandContext; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; +import net.minecraft.server.permissions.Permissions; +import whirlfrenzy.itemdespawntimer.config.ServerConfig; + +/** Server-side /idt commands. The reload branch is only sent to command sources with moderator permission. */ +public final class ItemDespawnTimerCommands { + private ItemDespawnTimerCommands() {} + + public static void register() { + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> + dispatcher.register(Commands.literal("idt") + .then(Commands.literal("reload") + .requires(source -> source.permissions().hasPermission(Permissions.COMMANDS_MODERATOR)) + .executes(ItemDespawnTimerCommands::reloadServerConfig)))); + } + + private static int reloadServerConfig(CommandContext context) { + ServerConfig.load(); + context.getSource().sendSuccess( + () -> Component.literal("[Item Despawn Timer] Server config reloaded."), false); + return 1; + } +} diff --git a/src/main/java/whirlfrenzy/itemdespawntimer/network/ReloadConfigPayload.java b/src/main/java/whirlfrenzy/itemdespawntimer/network/ReloadConfigPayload.java new file mode 100644 index 0000000..aeefe3c --- /dev/null +++ b/src/main/java/whirlfrenzy/itemdespawntimer/network/ReloadConfigPayload.java @@ -0,0 +1,20 @@ +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; + +/** Client request to reload the server config. The server always validates operator permission. */ +public record ReloadConfigPayload(boolean requested) implements CustomPacketPayload { + public static final Type TYPE = new Type<>(ItemDespawnTimer.id("reload_config")); + public static final StreamCodec CODEC = StreamCodec.composite( + ByteBufCodecs.BOOL, ReloadConfigPayload::requested, + ReloadConfigPayload::new); + + @Override + public Type type() { + return TYPE; + } +}