From da20b4d55229315acde93ee570420ac7f9cca31e Mon Sep 17 00:00:00 2001 From: ASKLL Date: Fri, 16 Oct 2026 14:26:32 +0800 Subject: [PATCH 1/5] fix: reduce tab overlay mixin conflicts --- .../denick/GuiPlayerTabOverlayMixin.java | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/src/main/java/pit12/platform/mixin/feature/denick/GuiPlayerTabOverlayMixin.java b/src/main/java/pit12/platform/mixin/feature/denick/GuiPlayerTabOverlayMixin.java index 62d1833..d620505 100644 --- a/src/main/java/pit12/platform/mixin/feature/denick/GuiPlayerTabOverlayMixin.java +++ b/src/main/java/pit12/platform/mixin/feature/denick/GuiPlayerTabOverlayMixin.java @@ -23,7 +23,9 @@ 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.Redirect; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import pit12.feature.denick.DenickNamesBinding; import pit12.feature.denick.api.DenickNames; @@ -31,17 +33,38 @@ public abstract class GuiPlayerTabOverlayMixin implements DenickNamesBinding { @Unique private DenickNames pit12$denickNames; + @Unique + private boolean pit12$renderingName; @Override public void pit12$bindDenickNames(DenickNames names) { pit12$denickNames = names; } - @Redirect(method = "renderPlayerlist", at = @At(value = "INVOKE", + // Only decorate render calls. PlayerNameCache also calls getPlayerName. + @Inject(method = "renderPlayerlist", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiPlayerTabOverlay;getPlayerName(Lnet/minecraft/client/network/NetworkPlayerInfo;)Ljava/lang/String;")) - private String pit12$displayName(GuiPlayerTabOverlay overlay, NetworkPlayerInfo player) { - String original = overlay.getPlayerName(player); - return pit12$denickNames == null ? original - : pit12$denickNames.listName(player.getGameProfile().getId(), original); + private void pit12$beginName(CallbackInfo callback) { + pit12$renderingName = true; + } + + @Inject(method = "renderPlayerlist", at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/gui/GuiPlayerTabOverlay;getPlayerName(Lnet/minecraft/client/network/NetworkPlayerInfo;)Ljava/lang/String;", + shift = At.Shift.AFTER)) + private void pit12$endName(CallbackInfo callback) { + pit12$renderingName = false; + } + + @Inject(method = "getPlayerName", at = @At("RETURN"), cancellable = true) + private void pit12$displayName(NetworkPlayerInfo player, + CallbackInfoReturnable callback) { + if (!pit12$renderingName || pit12$denickNames == null) { + return; + } + String original = callback.getReturnValue(); + String name = pit12$denickNames.listName(player.getGameProfile().getId(), original); + if (!name.equals(original)) { + callback.setReturnValue(name); + } } } From 4849e68f943aeb8cf814330a1c63b5058f7e5d29 Mon Sep 17 00:00:00 2001 From: ASKLL Date: Fri, 16 Oct 2026 14:26:48 +0800 Subject: [PATCH 2/5] fix: restore yellow bold NICK heading for player list --- src/main/java/pit12/feature/playerlist/PlayerListGroup.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/pit12/feature/playerlist/PlayerListGroup.java b/src/main/java/pit12/feature/playerlist/PlayerListGroup.java index 6982ce5..a4dc747 100644 --- a/src/main/java/pit12/feature/playerlist/PlayerListGroup.java +++ b/src/main/java/pit12/feature/playerlist/PlayerListGroup.java @@ -23,7 +23,7 @@ public enum PlayerListGroup { FRIEND(source("§a§lFriend")), ENEMY(source("§c§lEnemy")), - NICK("Nick"), + NICK("§e§lNICK"), REGULARITY("§4§lREG"), DARK("§d§lDARK"), BOUNTY_HUNTER("§6§lBounty Hunter"); From f1ba259ad5114e199acee7552a2f5d93975ecbfa Mon Sep 17 00:00:00 2001 From: ASKLL Date: Fri, 16 Oct 2026 15:03:18 +0800 Subject: [PATCH 3/5] feat: add Scoreboard Fix feature --- docs/IMPLEMENTATION.md | 2 +- .../java/pit12/bootstrap/ClientBootstrap.java | 12 +++++ .../scoreboardfix/ScoreboardFixBinding.java | 25 ++++++++++ .../scoreboardfix/ScoreboardFixConfig.java | 31 ++++++++++++ .../scoreboardfix/ScoreboardFixFeature.java | 41 ++++++++++++++++ .../feature/scoreboardfix/MinecraftMixin.java | 41 ++++++++++++++++ .../scoreboardfix/ScoreboardMixin.java | 48 +++++++++++++++++++ .../mixin/feature/swap/GuiContainerMixin.java | 6 +-- .../mixin/feature/swap/GuiIngameMixin.java | 8 +++- .../swap/NetHandlerPlayClientMixin.java | 20 ++++---- src/main/resources/mixins.pit12.json | 2 + .../architecture/BootstrapLayoutTest.java | 2 +- 12 files changed, 223 insertions(+), 15 deletions(-) create mode 100644 src/main/java/pit12/feature/scoreboardfix/ScoreboardFixBinding.java create mode 100644 src/main/java/pit12/feature/scoreboardfix/ScoreboardFixConfig.java create mode 100644 src/main/java/pit12/feature/scoreboardfix/ScoreboardFixFeature.java create mode 100644 src/main/java/pit12/platform/mixin/feature/scoreboardfix/MinecraftMixin.java create mode 100644 src/main/java/pit12/platform/mixin/feature/scoreboardfix/ScoreboardMixin.java diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index 553d82e..e246ae7 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -8,7 +8,7 @@ This guide covers the shared APIs, calling conventions, and lifecycle rules used `ClientBootstrap` creates the components in its constructor. Each feature has a private `register...` method that creates and registers its config when needed, creates the feature, and adds it to `components`. Pass dependencies as arguments. Return the feature's API only when a later registration method needs it. -Organize the constructor into shared runtime, feature providers, other features, and platform integrations. Create runtime components and platform adapters directly in their sections. Group feature registration calls and method declarations by category in this order: Combat, Player, Render, Misc, Interface. Within each category, order methods alphabetically by name. Put features without config in the first `Category: No config` group. Use matching group and category comments in both places. Dependency order takes priority over category and name. Bootstrap starts components in list order and stops them in reverse. Registration methods only create and register objects. Register listeners and start workers in `start()`. +Organize the constructor into shared runtime, feature providers, other features, and platform integrations. Create runtime components and platform adapters directly in their sections. Group feature registration calls and method declarations by category in this order: Combat, Player, Render, Misc, Patch, Interface. Within each category, order methods alphabetically by name. Put features without config in the first `Category: No config` group. Use matching group and category comments in both places. Dependency order takes priority over category and name. Bootstrap starts components in list order and stops them in reverse. Registration methods only create and register objects. Register listeners and start workers in `start()`. Bootstrap registers the component and calls its `start()` method. If `start()` causes an exception, then Bootstrap will stop this component and all components that were started before it. `stop()` should release resources in any case, including partial initialization and even if it was called multiple times. An exception thrown by one of the components in `stop()` will be logged but all the others will be stopped. diff --git a/src/main/java/pit12/bootstrap/ClientBootstrap.java b/src/main/java/pit12/bootstrap/ClientBootstrap.java index c489dd0..70e68eb 100644 --- a/src/main/java/pit12/bootstrap/ClientBootstrap.java +++ b/src/main/java/pit12/bootstrap/ClientBootstrap.java @@ -68,6 +68,9 @@ import pit12.feature.relation.api.Relations; import pit12.feature.ringhelper.RingHelperConfig; import pit12.feature.ringhelper.RingHelperFeature; +import pit12.feature.scoreboardfix.ScoreboardFixBinding; +import pit12.feature.scoreboardfix.ScoreboardFixConfig; +import pit12.feature.scoreboardfix.ScoreboardFixFeature; import pit12.feature.smartblock.SmartBlockBinding; import pit12.feature.smartblock.SmartBlockConfig; import pit12.feature.smartblock.SmartBlockFeature; @@ -199,6 +202,8 @@ public ClientBootstrap() { // Category: Misc registerAutoQuickMath(configs); registerBanAlert(minecraft, configs, session, presence); + // Category: Patch + registerScoreboardFix(configs, (ScoreboardFixBinding) minecraft); // Platform integrations OneConfigSupport oneConfig = new OneConfigSupport(configs); components.add(oneConfig); @@ -407,6 +412,13 @@ private void registerBanAlert(Minecraft minecraft, ConfigCatalog configs, Client .add(new BanAlertFeature(minecraft, configs, config, session, presence, language)); } + // Category: Patch + private void registerScoreboardFix(ConfigCatalog configs, ScoreboardFixBinding binding) { + ScoreboardFixConfig config = new ScoreboardFixConfig(); + configs.register(config); + components.add(new ScoreboardFixFeature(config, binding)); + } + public void start() { client.check(); if (started) { diff --git a/src/main/java/pit12/feature/scoreboardfix/ScoreboardFixBinding.java b/src/main/java/pit12/feature/scoreboardfix/ScoreboardFixBinding.java new file mode 100644 index 0000000..a54e28c --- /dev/null +++ b/src/main/java/pit12/feature/scoreboardfix/ScoreboardFixBinding.java @@ -0,0 +1,25 @@ +/* + * This file is part of 12pit. + * + * Copyright (C) 2026 The 12pit Authors and contributors + * + * 12pit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 12pit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 12pit. If not, see . + */ +package pit12.feature.scoreboardfix; + +public interface ScoreboardFixBinding { + void pit12$bindScoreboardFixConfig(ScoreboardFixConfig config); + + boolean pit12$scoreboardFixEnabled(); +} diff --git a/src/main/java/pit12/feature/scoreboardfix/ScoreboardFixConfig.java b/src/main/java/pit12/feature/scoreboardfix/ScoreboardFixConfig.java new file mode 100644 index 0000000..77069ec --- /dev/null +++ b/src/main/java/pit12/feature/scoreboardfix/ScoreboardFixConfig.java @@ -0,0 +1,31 @@ +/* + * This file is part of 12pit. + * + * Copyright (C) 2026 The 12pit Authors and contributors + * + * 12pit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 12pit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 12pit. If not, see . + */ +package pit12.feature.scoreboardfix; + +import static pit12.runtime.languages.Languages.source; + +import pit12.runtime.config.ConfigCategory; +import pit12.runtime.config.FeatureConfig; + +public final class ScoreboardFixConfig extends FeatureConfig { + public ScoreboardFixConfig() { + super("scoreboardfix", source("Scoreboard Fix"), + new ConfigCategory("patch", source("Patch")), ""); + } +} diff --git a/src/main/java/pit12/feature/scoreboardfix/ScoreboardFixFeature.java b/src/main/java/pit12/feature/scoreboardfix/ScoreboardFixFeature.java new file mode 100644 index 0000000..21ff738 --- /dev/null +++ b/src/main/java/pit12/feature/scoreboardfix/ScoreboardFixFeature.java @@ -0,0 +1,41 @@ +/* + * This file is part of 12pit. + * + * Copyright (C) 2026 The 12pit Authors and contributors + * + * 12pit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 12pit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 12pit. If not, see . + */ +package pit12.feature.scoreboardfix; + +import pit12.shared.lifecycle.ClientLifecycle; + +public final class ScoreboardFixFeature implements ClientLifecycle { + private final ScoreboardFixConfig config; + private final ScoreboardFixBinding binding; + + public ScoreboardFixFeature(ScoreboardFixConfig config, ScoreboardFixBinding binding) { + this.config = config; + this.binding = binding; + } + + @Override + public void start() { + binding.pit12$bindScoreboardFixConfig(config); + } + + @Override + public void stop() { + binding.pit12$bindScoreboardFixConfig(null); + } +} diff --git a/src/main/java/pit12/platform/mixin/feature/scoreboardfix/MinecraftMixin.java b/src/main/java/pit12/platform/mixin/feature/scoreboardfix/MinecraftMixin.java new file mode 100644 index 0000000..64aa8bc --- /dev/null +++ b/src/main/java/pit12/platform/mixin/feature/scoreboardfix/MinecraftMixin.java @@ -0,0 +1,41 @@ +/* + * This file is part of 12pit. + * + * Copyright (C) 2026 The 12pit Authors and contributors + * + * 12pit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 12pit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 12pit. If not, see . + */ +package pit12.platform.mixin.feature.scoreboardfix; + +import net.minecraft.client.Minecraft; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import pit12.feature.scoreboardfix.ScoreboardFixBinding; +import pit12.feature.scoreboardfix.ScoreboardFixConfig; + +@Mixin(Minecraft.class) +public abstract class MinecraftMixin implements ScoreboardFixBinding { + @Unique + private ScoreboardFixConfig pit12$scoreboardFixConfig; + + @Override + public void pit12$bindScoreboardFixConfig(ScoreboardFixConfig config) { + pit12$scoreboardFixConfig = config; + } + + @Override + public boolean pit12$scoreboardFixEnabled() { + return pit12$scoreboardFixConfig != null && pit12$scoreboardFixConfig.enabled(); + } +} diff --git a/src/main/java/pit12/platform/mixin/feature/scoreboardfix/ScoreboardMixin.java b/src/main/java/pit12/platform/mixin/feature/scoreboardfix/ScoreboardMixin.java new file mode 100644 index 0000000..d0c0053 --- /dev/null +++ b/src/main/java/pit12/platform/mixin/feature/scoreboardfix/ScoreboardMixin.java @@ -0,0 +1,48 @@ +/* + * This file is part of 12pit. + * + * Copyright (C) 2026 The 12pit Authors and contributors + * + * 12pit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 12pit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 12pit. If not, see . + */ +package pit12.platform.mixin.feature.scoreboardfix; + +import net.minecraft.client.Minecraft; +import net.minecraft.scoreboard.ScoreObjective; +import net.minecraft.scoreboard.ScorePlayerTeam; +import net.minecraft.scoreboard.Scoreboard; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import pit12.feature.scoreboardfix.ScoreboardFixBinding; + +@Mixin(Scoreboard.class) +public abstract class ScoreboardMixin { + @Inject(method = "removeTeam", at = @At("HEAD"), cancellable = true, require = 1) + private void pit12$skipMissingTeam(ScorePlayerTeam team, CallbackInfo ci) { + Minecraft minecraft = Minecraft.getMinecraft(); + if (team == null && ((ScoreboardFixBinding) minecraft).pit12$scoreboardFixEnabled()) { + ci.cancel(); + } + } + + @Inject(method = "removeObjective", at = @At("HEAD"), cancellable = true, require = 1) + private void pit12$skipMissingObjective(ScoreObjective objective, CallbackInfo ci) { + Minecraft minecraft = Minecraft.getMinecraft(); + if (objective == null && ((ScoreboardFixBinding) minecraft).pit12$scoreboardFixEnabled()) { + ci.cancel(); + } + } +} diff --git a/src/main/java/pit12/platform/mixin/feature/swap/GuiContainerMixin.java b/src/main/java/pit12/platform/mixin/feature/swap/GuiContainerMixin.java index b3b6fae..a8b354e 100644 --- a/src/main/java/pit12/platform/mixin/feature/swap/GuiContainerMixin.java +++ b/src/main/java/pit12/platform/mixin/feature/swap/GuiContainerMixin.java @@ -18,7 +18,7 @@ */ package pit12.platform.mixin.feature.swap; -import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.inventory.GuiContainer; import net.minecraft.inventory.Slot; import org.spongepowered.asm.mixin.Mixin; @@ -29,10 +29,10 @@ import pit12.feature.swap.SwapHooksBinding; @Mixin(GuiContainer.class) -public abstract class GuiContainerMixin { +public abstract class GuiContainerMixin extends GuiScreen { @Inject(method = "drawSlot", at = @At("TAIL")) private void pit12$drawBinding(Slot slot, CallbackInfo ci) { - SwapHooks hooks = ((SwapHooksBinding) Minecraft.getMinecraft()).pit12$swapHooks(); + SwapHooks hooks = ((SwapHooksBinding) mc).pit12$swapHooks(); if (hooks != null) { hooks.drawBinding(slot.getStack(), slot.xDisplayPosition, slot.yDisplayPosition); } diff --git a/src/main/java/pit12/platform/mixin/feature/swap/GuiIngameMixin.java b/src/main/java/pit12/platform/mixin/feature/swap/GuiIngameMixin.java index a7ab07b..7256ab5 100644 --- a/src/main/java/pit12/platform/mixin/feature/swap/GuiIngameMixin.java +++ b/src/main/java/pit12/platform/mixin/feature/swap/GuiIngameMixin.java @@ -21,7 +21,9 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiIngame; import net.minecraft.entity.player.EntityPlayer; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; +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.callback.CallbackInfo; @@ -30,10 +32,14 @@ @Mixin(GuiIngame.class) public abstract class GuiIngameMixin { + @Shadow + @Final + protected Minecraft mc; + @Inject(method = "renderHotbarItem", at = @At("TAIL")) private void pit12$drawBinding(int index, int x, int y, float partialTicks, EntityPlayer player, CallbackInfo ci) { - SwapHooks hooks = ((SwapHooksBinding) Minecraft.getMinecraft()).pit12$swapHooks(); + SwapHooks hooks = ((SwapHooksBinding) mc).pit12$swapHooks(); if (hooks != null) { hooks.drawBinding(player.inventory.getStackInSlot(index), x, y); } diff --git a/src/main/java/pit12/platform/mixin/feature/swap/NetHandlerPlayClientMixin.java b/src/main/java/pit12/platform/mixin/feature/swap/NetHandlerPlayClientMixin.java index aa78492..41bcae9 100644 --- a/src/main/java/pit12/platform/mixin/feature/swap/NetHandlerPlayClientMixin.java +++ b/src/main/java/pit12/platform/mixin/feature/swap/NetHandlerPlayClientMixin.java @@ -25,6 +25,7 @@ import net.minecraft.network.play.server.S29PacketSoundEffect; import net.minecraft.network.play.server.S32PacketConfirmTransaction; import org.spongepowered.asm.mixin.Mixin; +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.callback.CallbackInfo; @@ -33,14 +34,17 @@ @Mixin(NetHandlerPlayClient.class) public abstract class NetHandlerPlayClientMixin { + @Shadow + private Minecraft gameController; + @Inject(method = "addToSendQueue", at = @At("HEAD")) private void pit12$beforeWindowClick(Packet packet, CallbackInfo callback) { if (!(packet instanceof C0EPacketClickWindow)) return; - Minecraft minecraft = Minecraft.getMinecraft(); - if (!minecraft.isCallingFromMinecraftThread() || (Object) this != minecraft.getNetHandler()) + if (!gameController.isCallingFromMinecraftThread() + || (Object) this != gameController.getNetHandler()) return; - SwapHooks hooks = ((SwapHooksBinding) minecraft).pit12$swapHooks(); + SwapHooks hooks = ((SwapHooksBinding) gameController).pit12$swapHooks(); if (hooks != null) { C0EPacketClickWindow click = (C0EPacketClickWindow) packet; hooks.clickSent(click.getWindowId(), click.getActionNumber()); @@ -51,10 +55,9 @@ public abstract class NetHandlerPlayClientMixin { @Inject(method = "handleConfirmTransaction", at = @At("TAIL")) private void pit12$afterInventoryConfirmation(S32PacketConfirmTransaction packet, CallbackInfo callback) { - Minecraft minecraft = Minecraft.getMinecraft(); - if ((Object) this != minecraft.getNetHandler()) + if ((Object) this != gameController.getNetHandler()) return; - SwapHooks hooks = ((SwapHooksBinding) minecraft).pit12$swapHooks(); + SwapHooks hooks = ((SwapHooksBinding) gameController).pit12$swapHooks(); if (hooks != null) hooks.confirmClick(packet.getWindowId(), packet.getActionNumber(), packet.func_148888_e()); @@ -63,10 +66,9 @@ public abstract class NetHandlerPlayClientMixin { // We're on the client thread here. Muted sounds still reach TAIL! @Inject(method = "handleSoundEffect", at = @At("TAIL")) private void pit12$afterSoundEffect(S29PacketSoundEffect packet, CallbackInfo callback) { - Minecraft minecraft = Minecraft.getMinecraft(); - if ((Object) this != minecraft.getNetHandler()) + if ((Object) this != gameController.getNetHandler()) return; - SwapHooks hooks = ((SwapHooksBinding) minecraft).pit12$swapHooks(); + SwapHooks hooks = ((SwapHooksBinding) gameController).pit12$swapHooks(); if (hooks != null) hooks.sound(packet.getSoundName(), packet.getX(), packet.getY(), packet.getZ(), packet.getVolume(), packet.getPitch()); diff --git a/src/main/resources/mixins.pit12.json b/src/main/resources/mixins.pit12.json index ffbc92b..687ccc2 100644 --- a/src/main/resources/mixins.pit12.json +++ b/src/main/resources/mixins.pit12.json @@ -27,6 +27,8 @@ "feature.nametag.RendererLivingEntityMixin", "feature.nojumpdelay.EntityLivingBaseMixin", "feature.nojumpdelay.MinecraftMixin", + "feature.scoreboardfix.MinecraftMixin", + "feature.scoreboardfix.ScoreboardMixin", "feature.sprint.EntityPlayerSPMixin", "feature.smartblock.EntityArrowAccessor", "feature.smartblock.MinecraftMixin", diff --git a/src/test/java/pit12/architecture/BootstrapLayoutTest.java b/src/test/java/pit12/architecture/BootstrapLayoutTest.java index 3bc594e..6eb6be7 100644 --- a/src/test/java/pit12/architecture/BootstrapLayoutTest.java +++ b/src/test/java/pit12/architecture/BootstrapLayoutTest.java @@ -74,7 +74,7 @@ public void sectionsAndRegistrationOrderMatchCategoriesAndDependencies() throws imports.put(name.substring(name.lastIndexOf('.') + 1), name); } List categories = - Arrays.asList("", "combat", "player", "render", "misc", "interface"); + Arrays.asList("", "combat", "player", "render", "misc", "patch", "interface"); Map registrations = new LinkedHashMap<>(); Set configTypes = new HashSet<>(); Matcher declarations = Pattern From 9ef71951fe7577a9726ae86a86742854ac2cb50d Mon Sep 17 00:00:00 2001 From: ASKLL Date: Fri, 16 Oct 2026 15:40:36 +0800 Subject: [PATCH 4/5] feat: add Resource Leak Fix feature --- .../java/pit12/bootstrap/ClientBootstrap.java | 14 +++++ .../ResourceLeakFixBinding.java | 23 +++++++ .../ResourceLeakFixConfig.java | 31 ++++++++++ .../ResourceLeakFixFeature.java | 41 +++++++++++++ .../SimpleResourceAccessor.java | 30 ++++++++++ .../resourceleakfix/TextureMapMixin.java | 55 +++++++++++++++++ .../mixinconfig/MixinConfigPlugin.java | 60 +++++++++++++++++++ src/main/resources/mixins.pit12.json | 3 + 8 files changed, 257 insertions(+) create mode 100644 src/main/java/pit12/feature/resourceleakfix/ResourceLeakFixBinding.java create mode 100644 src/main/java/pit12/feature/resourceleakfix/ResourceLeakFixConfig.java create mode 100644 src/main/java/pit12/feature/resourceleakfix/ResourceLeakFixFeature.java create mode 100644 src/main/java/pit12/platform/mixin/feature/resourceleakfix/SimpleResourceAccessor.java create mode 100644 src/main/java/pit12/platform/mixin/feature/resourceleakfix/TextureMapMixin.java create mode 100644 src/main/java/pit12/platform/mixinconfig/MixinConfigPlugin.java diff --git a/src/main/java/pit12/bootstrap/ClientBootstrap.java b/src/main/java/pit12/bootstrap/ClientBootstrap.java index 70e68eb..722d429 100644 --- a/src/main/java/pit12/bootstrap/ClientBootstrap.java +++ b/src/main/java/pit12/bootstrap/ClientBootstrap.java @@ -25,6 +25,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import net.minecraft.client.Minecraft; +import net.minecraftforge.fml.client.FMLClientHandler; import pit12.feature.autoclick.AutoClickBinding; import pit12.feature.autoclick.AutoClickConfig; import pit12.feature.autoclick.AutoClickFeature; @@ -66,6 +67,9 @@ import pit12.feature.relation.RelationFeature; import pit12.feature.relation.api.RelationLookup; import pit12.feature.relation.api.Relations; +import pit12.feature.resourceleakfix.ResourceLeakFixBinding; +import pit12.feature.resourceleakfix.ResourceLeakFixConfig; +import pit12.feature.resourceleakfix.ResourceLeakFixFeature; import pit12.feature.ringhelper.RingHelperConfig; import pit12.feature.ringhelper.RingHelperFeature; import pit12.feature.scoreboardfix.ScoreboardFixBinding; @@ -203,6 +207,10 @@ public ClientBootstrap() { registerAutoQuickMath(configs); registerBanAlert(minecraft, configs, session, presence); // Category: Patch + if (FMLClientHandler.instance().hasOptifine()) { + registerResourceLeakFix(configs, + (ResourceLeakFixBinding) minecraft.getTextureMapBlocks()); + } registerScoreboardFix(configs, (ScoreboardFixBinding) minecraft); // Platform integrations OneConfigSupport oneConfig = new OneConfigSupport(configs); @@ -413,6 +421,12 @@ private void registerBanAlert(Minecraft minecraft, ConfigCatalog configs, Client } // Category: Patch + private void registerResourceLeakFix(ConfigCatalog configs, ResourceLeakFixBinding binding) { + ResourceLeakFixConfig config = new ResourceLeakFixConfig(); + configs.register(config); + components.add(new ResourceLeakFixFeature(config, binding)); + } + private void registerScoreboardFix(ConfigCatalog configs, ScoreboardFixBinding binding) { ScoreboardFixConfig config = new ScoreboardFixConfig(); configs.register(config); diff --git a/src/main/java/pit12/feature/resourceleakfix/ResourceLeakFixBinding.java b/src/main/java/pit12/feature/resourceleakfix/ResourceLeakFixBinding.java new file mode 100644 index 0000000..eff11ae --- /dev/null +++ b/src/main/java/pit12/feature/resourceleakfix/ResourceLeakFixBinding.java @@ -0,0 +1,23 @@ +/* + * This file is part of 12pit. + * + * Copyright (C) 2026 The 12pit Authors and contributors + * + * 12pit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 12pit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 12pit. If not, see . + */ +package pit12.feature.resourceleakfix; + +public interface ResourceLeakFixBinding { + void pit12$bindResourceLeakFixConfig(ResourceLeakFixConfig config); +} diff --git a/src/main/java/pit12/feature/resourceleakfix/ResourceLeakFixConfig.java b/src/main/java/pit12/feature/resourceleakfix/ResourceLeakFixConfig.java new file mode 100644 index 0000000..20c4475 --- /dev/null +++ b/src/main/java/pit12/feature/resourceleakfix/ResourceLeakFixConfig.java @@ -0,0 +1,31 @@ +/* + * This file is part of 12pit. + * + * Copyright (C) 2026 The 12pit Authors and contributors + * + * 12pit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 12pit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 12pit. If not, see . + */ +package pit12.feature.resourceleakfix; + +import static pit12.runtime.languages.Languages.source; + +import pit12.runtime.config.ConfigCategory; +import pit12.runtime.config.FeatureConfig; + +public final class ResourceLeakFixConfig extends FeatureConfig { + public ResourceLeakFixConfig() { + super("resourceleakfix", source("Resource Leak Fix"), + new ConfigCategory("patch", source("Patch")), source("for Optifine")); + } +} diff --git a/src/main/java/pit12/feature/resourceleakfix/ResourceLeakFixFeature.java b/src/main/java/pit12/feature/resourceleakfix/ResourceLeakFixFeature.java new file mode 100644 index 0000000..8c1d1b7 --- /dev/null +++ b/src/main/java/pit12/feature/resourceleakfix/ResourceLeakFixFeature.java @@ -0,0 +1,41 @@ +/* + * This file is part of 12pit. + * + * Copyright (C) 2026 The 12pit Authors and contributors + * + * 12pit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 12pit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 12pit. If not, see . + */ +package pit12.feature.resourceleakfix; + +import pit12.shared.lifecycle.ClientLifecycle; + +public final class ResourceLeakFixFeature implements ClientLifecycle { + private final ResourceLeakFixConfig config; + private final ResourceLeakFixBinding binding; + + public ResourceLeakFixFeature(ResourceLeakFixConfig config, ResourceLeakFixBinding binding) { + this.config = config; + this.binding = binding; + } + + @Override + public void start() { + binding.pit12$bindResourceLeakFixConfig(config); + } + + @Override + public void stop() { + binding.pit12$bindResourceLeakFixConfig(null); + } +} diff --git a/src/main/java/pit12/platform/mixin/feature/resourceleakfix/SimpleResourceAccessor.java b/src/main/java/pit12/platform/mixin/feature/resourceleakfix/SimpleResourceAccessor.java new file mode 100644 index 0000000..a9e9718 --- /dev/null +++ b/src/main/java/pit12/platform/mixin/feature/resourceleakfix/SimpleResourceAccessor.java @@ -0,0 +1,30 @@ +/* + * This file is part of 12pit. + * + * Copyright (C) 2026 The 12pit Authors and contributors + * + * 12pit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 12pit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 12pit. If not, see . + */ +package pit12.platform.mixin.feature.resourceleakfix; + +import java.io.InputStream; +import net.minecraft.client.resources.SimpleResource; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(SimpleResource.class) +public interface SimpleResourceAccessor { + @Accessor("mcmetaInputStream") + InputStream pit12$metadataInputStream(); +} diff --git a/src/main/java/pit12/platform/mixin/feature/resourceleakfix/TextureMapMixin.java b/src/main/java/pit12/platform/mixin/feature/resourceleakfix/TextureMapMixin.java new file mode 100644 index 0000000..1a6b02f --- /dev/null +++ b/src/main/java/pit12/platform/mixin/feature/resourceleakfix/TextureMapMixin.java @@ -0,0 +1,55 @@ +/* + * This file is part of 12pit. + * + * Copyright (C) 2026 The 12pit Authors and contributors + * + * 12pit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 12pit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 12pit. If not, see . + */ +package pit12.platform.mixin.feature.resourceleakfix; + +import java.io.InputStream; +import net.minecraft.client.renderer.texture.TextureMap; +import net.minecraft.client.resources.IResource; +import org.apache.commons.io.IOUtils; +import org.spongepowered.asm.mixin.Dynamic; +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.Redirect; +import pit12.feature.resourceleakfix.ResourceLeakFixBinding; +import pit12.feature.resourceleakfix.ResourceLeakFixConfig; + +@Mixin(TextureMap.class) +public abstract class TextureMapMixin implements ResourceLeakFixBinding { + @Unique + private ResourceLeakFixConfig pit12$resourceLeakFixConfig; + + @Override + public void pit12$bindResourceLeakFixConfig(ResourceLeakFixConfig config) { + pit12$resourceLeakFixConfig = config; + } + + @Dynamic("Added by OptiFine") + @Redirect(method = "detectMinimumSpriteSize", remap = false, at = @At(value = "INVOKE", + target = "Lnet/minecraft/client/resources/IResource;getInputStream()Ljava/io/InputStream;", + remap = true), require = 0) + private InputStream pit12$closeUnusedMetadata(IResource resource) { + // The first texture reload runs before the config is bound. + if ((pit12$resourceLeakFixConfig == null || pit12$resourceLeakFixConfig.enabled()) + && resource instanceof SimpleResourceAccessor) { + IOUtils.closeQuietly(((SimpleResourceAccessor) resource).pit12$metadataInputStream()); + } + return resource.getInputStream(); + } +} diff --git a/src/main/java/pit12/platform/mixinconfig/MixinConfigPlugin.java b/src/main/java/pit12/platform/mixinconfig/MixinConfigPlugin.java new file mode 100644 index 0000000..a08eec0 --- /dev/null +++ b/src/main/java/pit12/platform/mixinconfig/MixinConfigPlugin.java @@ -0,0 +1,60 @@ +/* + * This file is part of 12pit. + * + * Copyright (C) 2026 The 12pit Authors and contributors + * + * 12pit is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 12pit is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 12pit. If not, see . + */ +package pit12.platform.mixinconfig; + +import java.util.List; +import java.util.Set; +import net.minecraft.launchwrapper.Launch; +import org.spongepowered.asm.lib.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +public final class MixinConfigPlugin implements IMixinConfigPlugin { + @Override + public void onLoad(String mixinPackage) {} + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + // Forge detects OptiFine after Mixin preparation. + return !mixinClassName.startsWith("pit12.platform.mixin.feature.resourceleakfix.") + || Launch.classLoader + .getResource("optifine/OptiFineClassTransformer.class") != null; + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) {} + + @Override + public List getMixins() { + return null; + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, + IMixinInfo mixinInfo) {} + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, + IMixinInfo mixinInfo) {} +} diff --git a/src/main/resources/mixins.pit12.json b/src/main/resources/mixins.pit12.json index 687ccc2..68196ef 100644 --- a/src/main/resources/mixins.pit12.json +++ b/src/main/resources/mixins.pit12.json @@ -2,6 +2,7 @@ "required": true, "minVersion": "0.7.11", "package": "${basePackage}.platform.mixin", + "plugin": "${basePackage}.platform.mixinconfig.MixinConfigPlugin", "refmap": "mixins.${modId}.refmap.json", "target": "@env(DEFAULT)", "compatibilityLevel": "JAVA_8", @@ -27,6 +28,8 @@ "feature.nametag.RendererLivingEntityMixin", "feature.nojumpdelay.EntityLivingBaseMixin", "feature.nojumpdelay.MinecraftMixin", + "feature.resourceleakfix.SimpleResourceAccessor", + "feature.resourceleakfix.TextureMapMixin", "feature.scoreboardfix.MinecraftMixin", "feature.scoreboardfix.ScoreboardMixin", "feature.sprint.EntityPlayerSPMixin", From 6e1af36acdfc99772b3bd797e64e83970fac8bdf Mon Sep 17 00:00:00 2001 From: ASKLL Date: Sat, 17 Oct 2026 15:25:57 +0800 Subject: [PATCH 5/5] refactor!: remove the Interface category --- docs/IMPLEMENTATION.md | 2 +- .../java/pit12/bootstrap/ClientBootstrap.java | 34 +++++++++---------- .../java/pit12/feature/webui/WebUiConfig.java | 4 +-- .../java/pit12/feature/webui/WebUiServer.java | 8 +++-- .../pit12/runtime/config/FeatureConfig.java | 5 ++- .../architecture/BootstrapLayoutTest.java | 15 ++++---- 6 files changed, 37 insertions(+), 31 deletions(-) diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index e246ae7..7a113fd 100644 --- a/docs/IMPLEMENTATION.md +++ b/docs/IMPLEMENTATION.md @@ -8,7 +8,7 @@ This guide covers the shared APIs, calling conventions, and lifecycle rules used `ClientBootstrap` creates the components in its constructor. Each feature has a private `register...` method that creates and registers its config when needed, creates the feature, and adds it to `components`. Pass dependencies as arguments. Return the feature's API only when a later registration method needs it. -Organize the constructor into shared runtime, feature providers, other features, and platform integrations. Create runtime components and platform adapters directly in their sections. Group feature registration calls and method declarations by category in this order: Combat, Player, Render, Misc, Patch, Interface. Within each category, order methods alphabetically by name. Put features without config in the first `Category: No config` group. Use matching group and category comments in both places. Dependency order takes priority over category and name. Bootstrap starts components in list order and stops them in reverse. Registration methods only create and register objects. Register listeners and start workers in `start()`. +Organize the constructor into shared runtime, feature providers, other features, and platform integrations. Create runtime components and platform adapters directly in their sections. Group feature registration calls and method declarations by category in this order: Combat, Player, Render, Misc, Patch. Within each category, order methods alphabetically by name. Put features without a feature config, including those with only global settings, in the first `Category: No config` group. Use matching group and category comments in both places. Dependency order takes priority over category and name. Bootstrap starts components in list order and stops them in reverse. Registration methods only create and register objects. Register listeners and start workers in `start()`. Bootstrap registers the component and calls its `start()` method. If `start()` causes an exception, then Bootstrap will stop this component and all components that were started before it. `stop()` should release resources in any case, including partial initialization and even if it was called multiple times. An exception thrown by one of the components in `stop()` will be logged but all the others will be stopped. diff --git a/src/main/java/pit12/bootstrap/ClientBootstrap.java b/src/main/java/pit12/bootstrap/ClientBootstrap.java index 722d429..a10c026 100644 --- a/src/main/java/pit12/bootstrap/ClientBootstrap.java +++ b/src/main/java/pit12/bootstrap/ClientBootstrap.java @@ -170,14 +170,14 @@ public ClientBootstrap() { playerEquipment, relations, commands, new File(minecraft.mcDataDir, "12pit/swap-bindings.json").toPath(), (SwapHooksBinding) minecraft, input); + // Category: No config + BooleanSetting discordRpc = + registerWebUi(webUiConfigs, configs, profiles, relations, hudEditor, swapBindings, + new File(minecraft.mcDataDir, "12pit/webui.json").toPath()); // Category: Misc DenickNames denickNames = registerDenick(minecraft, configs, session, pitContext, presence, playerNames, nicks, players, playerEquipment, commands, new File(minecraft.mcDataDir, "12pit/denick-cache.json").toPath()); - // Category: Interface - BooleanSetting discordRpc = - registerWebUi(webUiConfigs, configs, profiles, relations, hudEditor, swapBindings, - new File(minecraft.mcDataDir, "12pit/webui.json").toPath()); // Features // Category: No config registerDiscordRpc(webUiConfigs, discordRpc); @@ -257,6 +257,19 @@ private SwapBindings registerSwap(Minecraft minecraft, ClientThread client, return swap.bindings(); } + // Category: No config + private BooleanSetting registerWebUi(ConfigCatalog configs, ConfigCatalog featureConfigs, + Profiles profiles, Relations relations, HudEditorFeature hudEditor, + SwapBindings swapBindings, Path path) { + WebUiConfig config = new WebUiConfig(language); + configs.register(config); + WebUiFeature webUi = new WebUiFeature(featureConfigs, configs, profiles, relations, + hudEditor, swapBindings, config, language, path); + hudEditor.setWebUiOpener(config.keybind()::get, webUi::open, config.hudEditorHint()); + components.add(webUi); + return config.discordRpc(); + } + // Category: Misc private DenickNames registerDenick(Minecraft minecraft, ConfigCatalog configs, ClientSession session, PitContext pitContext, TabPresence presence, @@ -272,19 +285,6 @@ private DenickNames registerDenick(Minecraft minecraft, ConfigCatalog configs, return denick; } - // Category: Interface - private BooleanSetting registerWebUi(ConfigCatalog configs, ConfigCatalog featureConfigs, - Profiles profiles, Relations relations, HudEditorFeature hudEditor, - SwapBindings swapBindings, Path path) { - WebUiConfig config = new WebUiConfig(language); - configs.register(config); - WebUiFeature webUi = new WebUiFeature(featureConfigs, configs, profiles, relations, - hudEditor, swapBindings, config, language, path); - hudEditor.setWebUiOpener(config.keybind()::get, webUi::open, config.hudEditorHint()); - components.add(webUi); - return config.discordRpc(); - } - // Features // Category: No config private void registerDiscordRpc(ConfigCatalog configs, BooleanSetting enabled) { diff --git a/src/main/java/pit12/feature/webui/WebUiConfig.java b/src/main/java/pit12/feature/webui/WebUiConfig.java index 94ffcde..8aef05f 100644 --- a/src/main/java/pit12/feature/webui/WebUiConfig.java +++ b/src/main/java/pit12/feature/webui/WebUiConfig.java @@ -25,7 +25,6 @@ import pit12.runtime.config.BooleanSetting; import pit12.runtime.config.ChoiceSetting; import pit12.runtime.config.ChoiceSetting.Choice; -import pit12.runtime.config.ConfigCategory; import pit12.runtime.config.FeatureConfig; import pit12.runtime.config.KeybindSetting; import pit12.runtime.languages.Languages; @@ -38,8 +37,7 @@ public final class WebUiConfig extends FeatureConfig { private final BooleanSetting discordRpc; public WebUiConfig(Languages languages) { - super("webui", source("Settings"), new ConfigCategory("interface", source("Interface")), "", - false); + super("webui", source("Settings"), null, "", false); Map names = languages.names(); Choice[] choices = new Choice[names.size()]; int index = 0; diff --git a/src/main/java/pit12/feature/webui/WebUiServer.java b/src/main/java/pit12/feature/webui/WebUiServer.java index bf4916a..0ac711c 100644 --- a/src/main/java/pit12/feature/webui/WebUiServer.java +++ b/src/main/java/pit12/feature/webui/WebUiServer.java @@ -65,6 +65,7 @@ import pit12.feature.swap.api.SwapBindings; import pit12.runtime.config.ChoiceSetting; import pit12.runtime.config.ConfigCatalog; +import pit12.runtime.config.ConfigCategory; import pit12.runtime.config.ConfigChangeListener; import pit12.runtime.config.ConfigOption; import pit12.runtime.config.FeatureConfig; @@ -547,10 +548,11 @@ private Map featureState(FeatureConfig feature) { List options = (List) section.get("options"); options.add(optionState(option)); } + ConfigCategory category = feature.category(); return object("id", feature.id(), "name", feature.displayName(), "description", - feature.description(), "categoryId", feature.category().id(), "category", - feature.category().displayName(), "toggleable", feature.toggleable(), "enabled", - feature.enabled(), "sections", sections); + feature.description(), "categoryId", category == null ? "" : category.id(), + "category", category == null ? "" : category.displayName(), "toggleable", + feature.toggleable(), "enabled", feature.enabled(), "sections", sections); } private static String buildLabel() { diff --git a/src/main/java/pit12/runtime/config/FeatureConfig.java b/src/main/java/pit12/runtime/config/FeatureConfig.java index 2a550db..5784e4f 100644 --- a/src/main/java/pit12/runtime/config/FeatureConfig.java +++ b/src/main/java/pit12/runtime/config/FeatureConfig.java @@ -229,7 +229,9 @@ private void register(Setting setting, ConfigOption.Kind optionKind) { void localize(Languages language) { displayName = language.translate(originalDisplayName); description = language.translate(originalDescription); - category.localize(language); + if (category != null) { + category.localize(language); + } for (Setting setting : settings) { setting.localize(language); } @@ -255,6 +257,7 @@ public final String originalDisplayName() { return originalDisplayName; } + /** Returns null for global settings. */ public final ConfigCategory category() { return category; } diff --git a/src/test/java/pit12/architecture/BootstrapLayoutTest.java b/src/test/java/pit12/architecture/BootstrapLayoutTest.java index 6eb6be7..3d1fea7 100644 --- a/src/test/java/pit12/architecture/BootstrapLayoutTest.java +++ b/src/test/java/pit12/architecture/BootstrapLayoutTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.nio.charset.StandardCharsets; @@ -73,8 +74,7 @@ public void sectionsAndRegistrationOrderMatchCategoriesAndDependencies() throws String name = imported.group(1); imports.put(name.substring(name.lastIndexOf('.') + 1), name); } - List categories = - Arrays.asList("", "combat", "player", "render", "misc", "patch", "interface"); + List categories = Arrays.asList("", "combat", "player", "render", "misc", "patch"); Map registrations = new LinkedHashMap<>(); Set configTypes = new HashSet<>(); Matcher declarations = Pattern @@ -84,19 +84,22 @@ public void sectionsAndRegistrationOrderMatchCategoriesAndDependencies() throws String body = code.substring(declarations.end(), bodyEnd(code, declarations.end() - 1)); ConfigCategory category = null; Matcher config = Pattern.compile("\\bnew\\s+(\\w+Config)\\s*\\(").matcher(body); - if (config.find()) { + boolean hasConfig = config.find(); + if (hasConfig) { String type = imports.get(config.group(1)); assertNotNull("Missing config import in " + name, type); assertTrue("Config is constructed twice: " + type, configTypes.add(type)); if (WebUiConfig.class.getName().equals(type)) { category = new WebUiConfig(new Languages(ClientThread.current())).category(); + assertNull("Global settings have no feature category", category); } else { category = FeatureConfig.class .cast(Class.forName(type).getDeclaredConstructor().newInstance()) .category(); + assertNotNull("Missing config category in " + name, category); + assertTrue("Unknown config category in " + name, + categories.contains(category.id())); } - assertTrue("Unknown config category in " + name, - categories.contains(category.id())); assertFalse("Multiple configs in " + name, config.find()); } Registration registration = @@ -105,7 +108,7 @@ public void sectionsAndRegistrationOrderMatchCategoriesAndDependencies() throws sectionAt(source, declarations.start())); assertEquals("Component registrations in " + name, 1, calls(body, "components\\.add").size()); - assertEquals("Config registrations in " + name, category == null ? 0 : 1, + assertEquals("Config registrations in " + name, hasConfig ? 1 : 0, calls(body, "configs\\.register").size()); assertTrue("Config freeze belongs in the constructor: " + name, calls(body, "configs\\.freeze").isEmpty());