diff --git a/docs/IMPLEMENTATION.md b/docs/IMPLEMENTATION.md index 553d82e..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, 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 c489dd0..a10c026 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,8 +67,14 @@ 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; +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; @@ -163,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); @@ -199,6 +206,12 @@ public ClientBootstrap() { // Category: Misc 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); components.add(oneConfig); @@ -244,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, @@ -259,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) { @@ -407,6 +420,19 @@ private void registerBanAlert(Minecraft minecraft, ConfigCatalog configs, Client .add(new BanAlertFeature(minecraft, configs, config, session, presence, language)); } + // 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); + components.add(new ScoreboardFixFeature(config, binding)); + } + public void start() { client.check(); if (started) { 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"); 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/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/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/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); + } } } 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/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/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/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/main/resources/mixins.pit12.json b/src/main/resources/mixins.pit12.json index ffbc92b..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,10 @@ "feature.nametag.RendererLivingEntityMixin", "feature.nojumpdelay.EntityLivingBaseMixin", "feature.nojumpdelay.MinecraftMixin", + "feature.resourceleakfix.SimpleResourceAccessor", + "feature.resourceleakfix.TextureMapMixin", + "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..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", "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());