diff --git a/.gitignore b/.gitignore index 09cd281f..bc0caec9 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,5 @@ bin/ # fabric run/ + +DEVNODE/ \ No newline at end of file diff --git a/README.md b/README.md index 1b89fe1c..6bbf8030 100644 --- a/README.md +++ b/README.md @@ -214,4 +214,4 @@ This project is provided **as-is** with **no warranty** of any kind. > I am **not responsible** for any **bans, data loss, in-game money loss**, or **any other consequences** that may arise from using this addon. > -> **Use at your own risk.** If you lose items, get banned, or otherwise suffer any kind of issue — I simply don't care. You've been warned. Use an alt. +> **Use at your own risk.** If you lose items, get banned, or otherwise suffer any kind of issue — I simply don't care. You've been warned. Use an alt. \ No newline at end of file diff --git a/VERSION b/VERSION index c32b0ec5..f6eb05e3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -16.1 +16.2 diff --git a/build.gradle.kts b/build.gradle.kts index c592d63f..2ca21245 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -44,6 +44,9 @@ repositories { implementation("com.google.code.gson:gson:2.10.1") include(implementation(annotationProcessor("com.github.bawnorton.mixinsquared:mixinsquared-fabric:0.3.7-beta.1")!!)!!) + + // MixinExtras for @WrapOperation + include(implementation(annotationProcessor("io.github.llamalad7:mixinextras-fabric:0.4.1")!!)!!) diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/com/nnpg/glazed/GlazedAddon.java b/src/main/java/com/nnpg/glazed/GlazedAddon.java index de3d0a99..30ce8835 100644 --- a/src/main/java/com/nnpg/glazed/GlazedAddon.java +++ b/src/main/java/com/nnpg/glazed/GlazedAddon.java @@ -1,9 +1,14 @@ package com.nnpg.glazed; +import com.nnpg.glazed.commands.*; +import com.nnpg.glazed.MyScreen; import com.nnpg.glazed.modules.esp.*; import com.nnpg.glazed.modules.main.*; import com.nnpg.glazed.modules.pvp.*; +import com.nnpg.glazed.protection.ModRegistry; +import com.nnpg.glazed.protection.TranslationProtectionHandler; import meteordevelopment.meteorclient.addons.MeteorAddon; +import meteordevelopment.meteorclient.commands.Commands; import meteordevelopment.meteorclient.systems.modules.Modules; import meteordevelopment.meteorclient.systems.modules.Category; import meteordevelopment.orbit.EventHandler; @@ -11,8 +16,11 @@ import meteordevelopment.meteorclient.events.game.GameLeftEvent; import net.minecraft.item.Items; import net.minecraft.item.ItemStack; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class GlazedAddon extends MeteorAddon { + private static final Logger LOGGER = LoggerFactory.getLogger("Glazed"); public static final Category CATEGORY = new Category("Glazed", new ItemStack(Items.CAKE)); public static final Category esp = new Category("Glazed ESP ", new ItemStack(Items.VINE)); @@ -22,6 +30,8 @@ public class GlazedAddon extends MeteorAddon { @Override public void onInitialize() { + LOGGER.debug("[Glazed] Initializing protection modules"); + Modules.get().add(new SpawnerProtect()); Modules.get().add(new AntiTrap()); Modules.get().add(new CoordSnapper()); @@ -101,6 +111,8 @@ public void onInitialize() { Modules.get().add(new PremiumTunnelBaseFinder()); Modules.get().add(new AdminList()); Modules.get().add(new AutoTreeFarmer()); + + Commands.add(new OrderItemCommand()); } @EventHandler @@ -111,6 +123,9 @@ private void onGameJoined(GameJoinedEvent event) { @EventHandler private void onGameLeft(GameLeftEvent event) { MyScreen.resetSessionCheck(); + // Clear protection caches on disconnect + TranslationProtectionHandler.clearCache(); + ModRegistry.clearServerPackKeys(); } @Override diff --git a/src/main/java/com/nnpg/glazed/MyScreen.java b/src/main/java/com/nnpg/glazed/MyScreen.java index 7990a878..af49d06e 100644 --- a/src/main/java/com/nnpg/glazed/MyScreen.java +++ b/src/main/java/com/nnpg/glazed/MyScreen.java @@ -1,5 +1,6 @@ package com.nnpg.glazed; +import com.nnpg.glazed.GlazedAddon; import meteordevelopment.meteorclient.gui.GuiTheme; import meteordevelopment.meteorclient.gui.WindowScreen; import meteordevelopment.meteorclient.gui.widgets.containers.WHorizontalList; diff --git a/src/main/java/com/nnpg/glazed/commands/OrderItemCommand.java b/src/main/java/com/nnpg/glazed/commands/OrderItemCommand.java new file mode 100644 index 00000000..b89a4a80 --- /dev/null +++ b/src/main/java/com/nnpg/glazed/commands/OrderItemCommand.java @@ -0,0 +1,76 @@ +package com.nnpg.glazed.commands; + +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import meteordevelopment.meteorclient.commands.Command; +import net.minecraft.component.type.ItemEnchantmentsComponent; +import net.minecraft.enchantment.Enchantment; +import net.minecraft.enchantment.EnchantmentHelper; +import net.minecraft.item.ItemStack; +import net.minecraft.registry.entry.RegistryEntry; +import net.minecraft.command.CommandSource; + +import java.util.ArrayList; +import java.util.List; + +public class OrderItemCommand extends Command { + + public OrderItemCommand() { + super("orderitem", "Runs /order for the item in your main hand with its enchantments."); + } + + @Override + public void build(LiteralArgumentBuilder builder) { + builder.executes(context -> { + if (mc.player == null) return SINGLE_SUCCESS; + + ItemStack mainHandItem = mc.player.getMainHandStack(); + if (mainHandItem.isEmpty()) { + error("You are not holding any item in your main hand."); + return SINGLE_SUCCESS; + } + + String itemName = getItemName(mainHandItem); + List enchantmentStrings = getEnchantments(mainHandItem); + + StringBuilder searchCommand = new StringBuilder("order "); + searchCommand.append(itemName); + + if (!enchantmentStrings.isEmpty()) { + searchCommand.append(" "); + searchCommand.append(String.join(" ", enchantmentStrings)); + } + + String command = searchCommand.toString(); + info("Ordering: /" + command); + mc.getNetworkHandler().sendChatCommand(command); + return SINGLE_SUCCESS; + }); + } + + private String getItemName(ItemStack stack) { + String itemId = stack.getItem().toString(); + if (itemId.contains(":")) { + itemId = itemId.split(":")[1]; + } + return itemId.toLowerCase().replace(" ", "_"); + } + + private List getEnchantments(ItemStack stack) { + List result = new ArrayList<>(); + ItemEnchantmentsComponent enchantments = EnchantmentHelper.getEnchantments(stack); + for (RegistryEntry entry : enchantments.getEnchantments()) { + int level = enchantments.getLevel(entry); + String enchantmentName = getEnchantmentName(entry); + result.add(enchantmentName + " " + level); + } + return result; + } + + private String getEnchantmentName(RegistryEntry enchantmentEntry) { + String enchantmentId = enchantmentEntry.getIdAsString(); + if (enchantmentId.contains(":")) { + enchantmentId = enchantmentId.split(":")[1]; + } + return enchantmentId.toLowerCase().replace(" ", "_"); + } +} diff --git a/src/main/java/com/nnpg/glazed/mixin/MeteorMixinCanceller.java b/src/main/java/com/nnpg/glazed/mixin/MeteorMixinCanceller.java index 65bf37ae..ba7ea7f3 100644 --- a/src/main/java/com/nnpg/glazed/mixin/MeteorMixinCanceller.java +++ b/src/main/java/com/nnpg/glazed/mixin/MeteorMixinCanceller.java @@ -15,7 +15,7 @@ public class MeteorMixinCanceller implements MixinCanceller { static { if (METEOR_PRESENT) { - //LOGGER.info("[Glazed] Meteor autoban on donut is active"); + LOGGER.info("[Glazed Protection] Meteor Client detected - Meteor's key resolution protection will be disabled to avoid detection."); } } diff --git a/src/main/java/com/nnpg/glazed/mixin/protection/ClientConnectionMixin.java b/src/main/java/com/nnpg/glazed/mixin/protection/ClientConnectionMixin.java new file mode 100644 index 00000000..22448514 --- /dev/null +++ b/src/main/java/com/nnpg/glazed/mixin/protection/ClientConnectionMixin.java @@ -0,0 +1,66 @@ +package com.nnpg.glazed.mixin.protection; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.nnpg.glazed.protection.ClientSpoofer; +import com.nnpg.glazed.protection.PacketContext; +import com.nnpg.glazed.protection.TranslationProtectionHandler; +import net.minecraft.network.ClientConnection; +import net.minecraft.network.listener.PacketListener; +import net.minecraft.network.packet.Packet; +import net.minecraft.network.packet.CustomPayload; +import net.minecraft.util.Identifier; +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; + +@Mixin(ClientConnection.class) +public class ClientConnectionMixin { + + @WrapOperation( + method = "handlePacket", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/network/packet/Packet;apply(Lnet/minecraft/network/listener/PacketListener;)V") + ) + private static void glazed$wrapApply( + Packet packet, + T listener, + Operation original) { + PacketContext.setPacketName(packet); + PacketContext.setProcessingPacket(true); + try { + original.call(packet, listener); + } finally { + PacketContext.setProcessingPacket(false); + } + } + + @Inject(method = "send(Lnet/minecraft/network/packet/Packet;)V", at = @At("HEAD"), cancellable = true) + private void glazed$onSend(Packet packet, CallbackInfo ci) { + + if (packet.getClass().getName().contains("CustomPayloadC2SPacket")) { + try { + java.lang.reflect.Method payloadMethod = packet.getClass().getMethod("payload"); + Object payload = payloadMethod.invoke(packet); + + java.lang.reflect.Method idAccessor = payload.getClass().getMethod("id"); + Object idObj = idAccessor.invoke(payload); + + Identifier id; + if (idObj instanceof Identifier) { + id = (Identifier) idObj; + } else { + java.lang.reflect.Method idMethod = idObj.getClass().getMethod("id"); + id = (Identifier) idMethod.invoke(idObj); + } + + if (ClientSpoofer.shouldBlockPayload(id)) { + ci.cancel(); + } + } catch (Throwable t) { + + } + } + } +} diff --git a/src/main/java/com/nnpg/glazed/mixin/protection/DecoderHandlerMixin.java b/src/main/java/com/nnpg/glazed/mixin/protection/DecoderHandlerMixin.java new file mode 100644 index 00000000..7c8732c0 --- /dev/null +++ b/src/main/java/com/nnpg/glazed/mixin/protection/DecoderHandlerMixin.java @@ -0,0 +1,27 @@ +package com.nnpg.glazed.mixin.protection; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.nnpg.glazed.protection.PacketContext; +import net.minecraft.network.codec.PacketCodec; +import net.minecraft.network.handler.DecoderHandler; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(DecoderHandler.class) +public class DecoderHandlerMixin { + + @WrapOperation( + method = "decode", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/network/codec/PacketCodec;decode(Ljava/lang/Object;)Ljava/lang/Object;") + ) + private Object glazed$wrapDecode(PacketCodec instance, Object buffer, Operation original) { + PacketContext.setProcessingPacket(true); + try { + return original.call(instance, buffer); + } finally { + PacketContext.setProcessingPacket(false); + } + } +} diff --git a/src/main/java/com/nnpg/glazed/mixin/protection/KeybindTextContentMixin.java b/src/main/java/com/nnpg/glazed/mixin/protection/KeybindTextContentMixin.java new file mode 100644 index 00000000..ac0acbd8 --- /dev/null +++ b/src/main/java/com/nnpg/glazed/mixin/protection/KeybindTextContentMixin.java @@ -0,0 +1,93 @@ +package com.nnpg.glazed.mixin.protection; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.nnpg.glazed.protection.PacketContext; +import com.nnpg.glazed.protection.TranslationProtectionHandler; +import com.nnpg.glazed.protection.TranslationProtectionHandler.InterceptionType; +import com.nnpg.glazed.protection.KeybindDefaults; +import net.minecraft.client.option.KeyBinding; +import net.minecraft.text.KeybindTextContent; +import net.minecraft.text.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.function.Supplier; + +@Mixin(KeybindTextContent.class) +public class KeybindTextContentMixin { + private static final Logger LOGGER = LoggerFactory.getLogger("Glazed-Protection"); + + @Shadow @Final + private String key; + + @Unique + private boolean glazed$fromPacket = false; + + @Inject(method = "(Ljava/lang/String;)V", at = @At("TAIL")) + private void glazed$tagFromPacket(String key, CallbackInfo ci) { + this.glazed$fromPacket = PacketContext.isProcessingPacket(); + } + + @WrapOperation( + method = "getTranslated", + at = @At(value = "INVOKE", target = "Ljava/util/function/Supplier;get()Ljava/lang/Object;"), + require = 0 + ) + private Object glazed$interceptKeybind(Supplier supplier, Operation original) { + try { + if (!this.glazed$fromPacket || glazed$isIntegratedServerRunning()) { + return original.call(supplier); + } + } catch (Throwable t) { + + return original.call(supplier); + } + + if (KeybindDefaults.hasDefault(key)) { + String spoofedValue = KeybindDefaults.getDefault(key); + glazed$logBlocked(key, spoofedValue); + return Text.literal(spoofedValue); + } + + glazed$logBlocked(key, key); + return Text.translatable(key); + } + + @Unique + private static boolean glazed$isIntegratedServerRunning() { + try { + + return net.minecraft.client.MinecraftClient.getInstance().isIntegratedServerRunning(); + } catch (Exception e) { + return false; + } + } + + @Unique + private void glazed$logBlocked(String keybindName, String spoofedValue) { + String realValue = glazed$readKeybindDisplay(); + + TranslationProtectionHandler.logDetection(InterceptionType.KEYBIND, keybindName, realValue, spoofedValue); + } + + @Unique + private String glazed$readKeybindDisplay() { + try { + Text display = KeyBinding.getLocalizedName(key).get(); + if (display != null) { + return display.getString(); + } + } catch (Exception e) { + LOGGER.info("[Glazed Protection] Failed to read keybind '{}': {}", key, e.getMessage()); + } + return key; + } +} diff --git a/src/main/java/com/nnpg/glazed/mixin/protection/PacketUtilsMixin.java b/src/main/java/com/nnpg/glazed/mixin/protection/PacketUtilsMixin.java new file mode 100644 index 00000000..d59ae877 --- /dev/null +++ b/src/main/java/com/nnpg/glazed/mixin/protection/PacketUtilsMixin.java @@ -0,0 +1,35 @@ +package com.nnpg.glazed.mixin.protection; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.nnpg.glazed.protection.PacketContext; +import com.nnpg.glazed.protection.TranslationProtectionHandler; +import net.minecraft.network.NetworkThreadUtils; +import net.minecraft.network.listener.PacketListener; +import net.minecraft.network.packet.Packet; +import net.minecraft.util.thread.ThreadExecutor; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(NetworkThreadUtils.class) +public class PacketUtilsMixin { + + @WrapOperation( + method = "forceMainThread(Lnet/minecraft/network/packet/Packet;Lnet/minecraft/network/listener/PacketListener;Lnet/minecraft/util/thread/ThreadExecutor;)V", + at = @At(value = "INVOKE", target = "Lnet/minecraft/util/thread/ThreadExecutor;execute(Ljava/lang/Runnable;)V"), + require = 0 + ) + private static void glazed$wrapForceMainThread(ThreadExecutor engine, Runnable task, Operation original, + Packet packet, PacketListener listener) { + + original.call(engine, (Runnable) () -> { + PacketContext.setPacketName(packet.getClass().getSimpleName()); + PacketContext.setProcessingPacket(true); + try { + task.run(); + } finally { + PacketContext.setProcessingPacket(false); + } + }); + } +} diff --git a/src/main/java/com/nnpg/glazed/mixin/protection/ServerResourcePackLoaderMixin.java b/src/main/java/com/nnpg/glazed/mixin/protection/ServerResourcePackLoaderMixin.java new file mode 100644 index 00000000..4080c797 --- /dev/null +++ b/src/main/java/com/nnpg/glazed/mixin/protection/ServerResourcePackLoaderMixin.java @@ -0,0 +1,55 @@ +package com.nnpg.glazed.mixin.protection; + +import com.nnpg.glazed.protection.ModRegistry; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.resource.server.ServerResourcePackLoader; +import net.minecraft.resource.ResourcePackProfile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; + +@Mixin(ServerResourcePackLoader.class) +public class ServerResourcePackLoaderMixin { + + @Unique + private static final Logger LOGGER = LoggerFactory.getLogger("Glazed-Protection"); + + @Inject( + method = "loadServerPack(Lnet/minecraft/resource/ResourcePackProfile;Ljava/util/List;)V", + at = @At("HEAD"), + require = 0 + ) + private static void glazed$onServerPackLoad( + ResourcePackProfile profile, + List profiles, + CallbackInfo ci) { + try { + LOGGER.info("[Glazed Protection] Server resource pack loading: {}", + profile != null ? profile.getId() : "unknown"); + } catch (Throwable t) { + LOGGER.error("[Glazed Protection] Error in server pack load", t); + } + } + + @Inject( + method = "loadServerPack(Lnet/minecraft/resource/ResourcePackProfile;Ljava/util/List;)V", + at = @At("RETURN"), + require = 0 + ) + private static void glazed$afterServerPackLoad( + ResourcePackProfile profile, + List profiles, + CallbackInfo ci) { + try { + LOGGER.info("[Glazed Protection] Server resource pack load complete"); + } catch (Throwable t) { + LOGGER.error("[Glazed Protection] Error after server pack load", t); + } + } +} diff --git a/src/main/java/com/nnpg/glazed/mixin/protection/SessionProtectionMixin.java b/src/main/java/com/nnpg/glazed/mixin/protection/SessionProtectionMixin.java new file mode 100644 index 00000000..416d0fbb --- /dev/null +++ b/src/main/java/com/nnpg/glazed/mixin/protection/SessionProtectionMixin.java @@ -0,0 +1,28 @@ +package com.nnpg.glazed.mixin.protection; + +import com.nnpg.glazed.protection.TranslationProtectionHandler; +import net.minecraft.client.network.ClientCommonNetworkHandler; +import net.minecraft.client.network.ClientPlayNetworkHandler; +import net.minecraft.network.DisconnectionInfo; +import net.minecraft.network.packet.s2c.play.GameJoinS2CPacket; +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; + +@Mixin(ClientCommonNetworkHandler.class) +public abstract class SessionProtectionMixin { + + @Inject(method = "onDisconnected", at = @At("HEAD"), require = 0) + private void glazed$onDisconnect(DisconnectionInfo info, CallbackInfo ci) { + TranslationProtectionHandler.clearCache(); + } + + @Mixin(ClientPlayNetworkHandler.class) + public static abstract class JoinMixin { + @Inject(method = "onGameJoin", at = @At("HEAD"), require = 0) + private void glazed$onJoin(GameJoinS2CPacket packet, CallbackInfo ci) { + TranslationProtectionHandler.clearCache(); + } + } +} diff --git a/src/main/java/com/nnpg/glazed/mixin/protection/TranslatableTextContentMixin.java b/src/main/java/com/nnpg/glazed/mixin/protection/TranslatableTextContentMixin.java new file mode 100644 index 00000000..811d18c7 --- /dev/null +++ b/src/main/java/com/nnpg/glazed/mixin/protection/TranslatableTextContentMixin.java @@ -0,0 +1,153 @@ +package com.nnpg.glazed.mixin.protection; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.nnpg.glazed.protection.PacketContext; +import com.nnpg.glazed.protection.TranslationProtectionHandler; +import com.nnpg.glazed.protection.TranslationProtectionHandler.InterceptionType; +import com.nnpg.glazed.protection.ModRegistry; +import net.minecraft.text.TranslatableTextContent; +import net.minecraft.util.Language; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Mixin(value = TranslatableTextContent.class, priority = 1500) +public abstract class TranslatableTextContentMixin { + + @Unique + private static final Logger LOGGER = LoggerFactory.getLogger("Glazed-Protection"); + + @Shadow @Final private String key; + @Shadow @Final private String fallback; + + @Unique + private boolean glazed$fromPacket = false; + + @Inject(method = "(Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)V", at = @At("TAIL"), require = 0) + private void glazed$tagFromPacket(String key, String fallback, Object[] args, CallbackInfo ci) { + try { + this.glazed$fromPacket = PacketContext.isProcessingPacket(); + if (this.glazed$fromPacket) { + LOGGER.info("[Glazed-Debug] TranslatableTextContent created from packet: {} | key='{}' fallback='{}'", + PacketContext.getPacketName(), key, fallback); + } + } catch (Throwable t) { + + this.glazed$fromPacket = false; + } + } + + @Unique + private static final String GLAZED_ALLOW_ORIGINAL = "\0__glazed_allow__"; + + @WrapOperation( + method = { + "decompose(Lnet/minecraft/text/LanguageVisitor;Lnet/minecraft/text/Style;)Ljava/util/Optional;", + "updateTranslations()V" + }, + at = @At(value = "INVOKE", + target = "Lnet/minecraft/util/Language;get(Ljava/lang/String;)Ljava/lang/String;"), + require = 0 + ) + private String glazed$wrapGetSingle(Language instance, String keyArg, Operation original) { + + if (!this.glazed$fromPacket) { + return original.call(instance, keyArg); + } + + String result = glazed$handleTranslationLookup(keyArg, keyArg); + if (result == GLAZED_ALLOW_ORIGINAL) { + return original.call(instance, keyArg); + } + return result; + } + + @WrapOperation( + method = { + "decompose(Lnet/minecraft/text/LanguageVisitor;Lnet/minecraft/text/Style;)Ljava/util/Optional;", + "updateTranslations()V" + }, + at = @At(value = "INVOKE", + target = "Lnet/minecraft/util/Language;get(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"), + require = 0 + ) + private String glazed$wrapGet(Language instance, String keyArg, String fallbackArg, Operation original) { + + if (!this.glazed$fromPacket) { + return original.call(instance, keyArg, fallbackArg); + } + + String result = glazed$handleTranslationLookup(keyArg, fallbackArg); + if (result == GLAZED_ALLOW_ORIGINAL) { + return original.call(instance, keyArg, fallbackArg); + } + return result; + } + + @Unique + private String glazed$handleTranslationLookup(String translationKey, String defaultValue) { + + try { + + if (!this.glazed$fromPacket || glazed$isIntegratedServerRunning()) { + return GLAZED_ALLOW_ORIGINAL; + } + } catch (Throwable t) { + + return GLAZED_ALLOW_ORIGINAL; + } + + if (ModRegistry.isVanillaTranslationKey(translationKey)) { + return GLAZED_ALLOW_ORIGINAL; + } + + if (ModRegistry.isServerPackTranslationKey(translationKey)) { + return GLAZED_ALLOW_ORIGINAL; + } + + String blockedValue = defaultValue; + glazed$logBlocked(translationKey, blockedValue); + return blockedValue; + } + + @Unique + private static boolean glazed$isIntegratedServerRunning() { + try { + + return net.minecraft.client.MinecraftClient.getInstance().isIntegratedServerRunning(); + } catch (Exception e) { + return false; + } + } + + @Unique + private void glazed$logBlocked(String translationKey, String defaultValue) { + String originalValue = glazed$getRealTranslation(translationKey, defaultValue); + + TranslationProtectionHandler.logDetection(InterceptionType.TRANSLATION, translationKey, originalValue, defaultValue); + } + + @Unique + private String glazed$getRealTranslation(String translationKey, String defaultValue) { + try { + Language lang = Language.getInstance(); + if (lang instanceof TranslationStorageAccessor accessor) { + Map translations = accessor.glazed$getTranslations(); + String value = translations.get(translationKey); + return value != null ? value : defaultValue; + } + } catch (Exception e) { + + } + return defaultValue; + } +} diff --git a/src/main/java/com/nnpg/glazed/mixin/protection/TranslationStorageAccessor.java b/src/main/java/com/nnpg/glazed/mixin/protection/TranslationStorageAccessor.java new file mode 100644 index 00000000..307f468e --- /dev/null +++ b/src/main/java/com/nnpg/glazed/mixin/protection/TranslationStorageAccessor.java @@ -0,0 +1,13 @@ +package com.nnpg.glazed.mixin.protection; + +import net.minecraft.client.resource.language.TranslationStorage; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.Map; + +@Mixin(TranslationStorage.class) +public interface TranslationStorageAccessor { + @Accessor("translations") + Map glazed$getTranslations(); +} diff --git a/src/main/java/com/nnpg/glazed/mixin/protection/TranslationStorageMixin.java b/src/main/java/com/nnpg/glazed/mixin/protection/TranslationStorageMixin.java new file mode 100644 index 00000000..30c53905 --- /dev/null +++ b/src/main/java/com/nnpg/glazed/mixin/protection/TranslationStorageMixin.java @@ -0,0 +1,330 @@ +package com.nnpg.glazed.mixin.protection; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Local; +import com.nnpg.glazed.protection.ModRegistry; +import net.fabricmc.loader.api.FabricLoader; +import net.minecraft.client.resource.language.TranslationStorage; +import net.minecraft.resource.DefaultResourcePack; +import net.minecraft.resource.Resource; +import net.minecraft.resource.ResourceManager; +import net.minecraft.resource.ResourcePack; +import net.minecraft.resource.VanillaDataPackProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.io.InputStream; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; + +@Mixin(TranslationStorage.class) +public class TranslationStorageMixin { + + @Unique + private static final Logger LOGGER = LoggerFactory.getLogger("Glazed-Protection"); + + @Unique + private static boolean glazed$loggedOnce = false; + + @Unique + private static final ThreadLocal CURRENT_PACK = new ThreadLocal<>(); + + @Inject( + method = "load(Lnet/minecraft/resource/ResourceManager;Ljava/util/List;Z)Lnet/minecraft/client/resource/language/TranslationStorage;", + at = @At("HEAD"), + require = 0 + ) + private static void glazed$onLoadStart( + ResourceManager resourceManager, + List definitions, + boolean rightToLeft, + CallbackInfoReturnable cir) { + try { + ModRegistry.clearTranslationKeys(); + LOGGER.info("[Glazed Protection] Starting language load, clearing caches"); + glazed$loggedOnce = false; + } catch (Throwable t) { + LOGGER.error("[Glazed Protection] Error clearing translation keys", t); + } + } + + @Inject( + method = "load(Lnet/minecraft/resource/ResourceManager;Ljava/util/List;Z)Lnet/minecraft/client/resource/language/TranslationStorage;", + at = @At("RETURN"), + require = 0 + ) + private static void glazed$onLoadComplete( + ResourceManager resourceManager, + List definitions, + boolean rightToLeft, + CallbackInfoReturnable cir) { + try { + ModRegistry.markInitialized(); + + if (!glazed$loggedOnce) { + glazed$loggedOnce = true; + LOGGER.info("[Glazed Protection] Translation system initialized - {} vanilla keys, {} server pack keys, {} total keys tracked", + ModRegistry.getVanillaKeyCount(), ModRegistry.getServerPackKeyCount(), ModRegistry.getTranslationKeyCount()); + } + } catch (Throwable t) { + LOGGER.error("[Glazed Protection] Error in load complete", t); + } + } + + @Inject( + method = "load(Lnet/minecraft/resource/ResourceManager;Ljava/util/List;Z)Lnet/minecraft/client/resource/language/TranslationStorage;", + at = @At(value = "INVOKE", target = "Lnet/minecraft/util/Language;load(Ljava/io/InputStream;Ljava/util/function/BiConsumer;)V"), + require = 0 + ) + private static void glazed$capturePack( + ResourceManager resourceManager, List definitions, boolean rightToLeft, CallbackInfoReturnable cir, + @Local Resource resource) { + try { + CURRENT_PACK.set(glazed$getPackFromResource(resource)); + } catch (Throwable t) { + CURRENT_PACK.set(null); + } + } + + @Unique + private static ResourcePack glazed$getPackFromResource(Resource resource) { + if (resource == null) return null; + try { + + var method = resource.getClass().getMethod("getPack"); + return (ResourcePack) method.invoke(resource); + } catch (Throwable t) { + try { + + var method = resource.getClass().getMethod("pack"); + return (ResourcePack) method.invoke(resource); + } catch (Exception e) { + return null; + } + } + } + + @Unique + private static String glazed$getPackId(ResourcePack pack) { + if (pack == null) return "unknown"; + try { + var method = pack.getClass().getMethod("getId"); + return (String) method.invoke(pack); + } catch (Throwable t) { + try { + var method = pack.getClass().getMethod("getName"); + return (String) method.invoke(pack); + } catch (Exception e) { + return pack.getClass().getSimpleName(); + } + } + } + + @Unique + private static String glazed$getModIdFromPack(ResourcePack pack) { + if (pack == null) return null; + try { + var method = pack.getClass().getMethod("getFabricModMetadata"); + var metadata = method.invoke(pack); + if (metadata != null) { + var getIdMethod = metadata.getClass().getMethod("getId"); + String id = (String) getIdMethod.invoke(metadata); + if (id != null) return id; + } + } catch (Exception e) {} + try { + var method = pack.getClass().getMethod("getModMetadata"); + var metadata = method.invoke(pack); + if (metadata != null) { + var getIdMethod = metadata.getClass().getMethod("getId"); + String id = (String) getIdMethod.invoke(metadata); + if (id != null) return id; + } + } catch (Exception e) {} + + String packId = glazed$getPackId(pack); + if (packId != null && !packId.equals("vanilla") && !packId.startsWith("file/") && !packId.startsWith("server/")) { + if (FabricLoader.getInstance().getModContainer(packId).isPresent()) { + return packId; + } + + String extractedModId = packId.replace("fabric/", "").replace("mod/", ""); + if (!extractedModId.isEmpty() && FabricLoader.getInstance().getModContainer(extractedModId).isPresent()) { + return extractedModId; + } + } + return null; + } + + @WrapOperation( + method = "load(Lnet/minecraft/resource/ResourceManager;Ljava/util/List;Z)Lnet/minecraft/client/resource/language/TranslationStorage;", + at = @At(value = "INVOKE", + target = "Lnet/minecraft/util/Language;load(Ljava/io/InputStream;Ljava/util/function/BiConsumer;)V"), + require = 0 + ) + private static void glazed$wrapLanguageLoad(InputStream stream, BiConsumer consumer, Operation original) { + original.call(stream, (BiConsumer) (key, value) -> { + glazed$trackKeyBySource(key, value); + consumer.accept(key, value); + }); + } + + @Unique + private static void glazed$trackKeyBySource(String key, String value) { + ResourcePack pack = CURRENT_PACK.get(); + + try { + if (pack != null) { + String packId = glazed$getPackId(pack); + + boolean isVanillaPack = (pack instanceof DefaultResourcePack) || "vanilla".equals(packId); + + boolean isServerPack = packId != null && (packId.equals("server") || packId.startsWith("server/")); + + if (isVanillaPack) { + ModRegistry.recordVanillaTranslationKey(key); + } else if (isServerPack) { + ModRegistry.recordServerPackKey(key); + } else { + String modId = glazed$getModIdFromPack(pack); + if (modId != null) { + ModRegistry.recordTranslationKey(modId, key); + } else { + + String extracted = glazed$extractModId(key); + if (extracted != null) { + ModRegistry.recordTranslationKey(extracted, key); + } + } + } + } + } catch (Throwable t) { + + LOGGER.info("[Glazed Protection] Error tracking key '{}': {}", key, t.getMessage()); + } + } + + @Unique + private static boolean glazed$isVanillaKey(String key) { + if (key == null) return false; + + return key.startsWith("key.") && !key.contains("-") && !key.contains("_") + || key.startsWith("gui.") && !key.contains("-") + || key.startsWith("menu.") + || key.startsWith("options.") + || key.startsWith("chat.") + || key.startsWith("commands.") + || key.startsWith("block.minecraft.") + || key.startsWith("item.minecraft.") + || key.startsWith("entity.minecraft.") + || key.startsWith("biome.minecraft.") + || key.startsWith("enchantment.minecraft.") + || key.startsWith("effect.minecraft.") + || key.startsWith("container.") + || key.startsWith("death.") + || key.startsWith("gameMode.") + || key.startsWith("selectWorld.") + || key.startsWith("createWorld.") + || key.startsWith("multiplayer.") + || key.startsWith("lanServer.") + || key.startsWith("advMode.") + || key.startsWith("narrator.") + || key.startsWith("subtitles.") + || key.startsWith("language.") + || key.startsWith("resourcePack.") + || key.startsWith("dataPack.") + || key.startsWith("tutorial.") + || key.startsWith("demo.") + || key.startsWith("disconnect.") + || key.startsWith("book.") + || key.startsWith("sign.") + || key.startsWith("filled_map.") + || key.startsWith("structure_block.") + || key.startsWith("jigsaw_block.") + || key.startsWith("argument.") + || key.startsWith("parsing.") + || key.startsWith("color.minecraft.") + || key.startsWith("stat.") + || key.startsWith("controls.") + || key.startsWith("attribute.name.") + || key.startsWith("attribute.modifier.") + || key.startsWith("gamerule.") + || key.startsWith("difficulty.") + || key.startsWith("potion.") + || key.startsWith("recipe.") + || key.startsWith("advancements.") + || key.startsWith("translation.") + || key.startsWith("pack.source.") + || key.startsWith("pack.nameAndSource") + || key.startsWith("soundCategory.") + || key.startsWith("title.") + || key.startsWith("screenshot.") + || key.startsWith("mco.") + || key.startsWith("realms.") + || key.startsWith("telemetry.") + || key.startsWith("accessibility.") + || key.startsWith("editGamerule.") + || key.startsWith("spectatorMenu.") + || key.startsWith("record.") + || key.startsWith("instrument.") + || key.startsWith("painting.") + || key.startsWith("trim_"); + } + + @Unique + private static String glazed$extractModId(String key) { + if (key == null || !key.contains(".")) return null; + + String[] parts = key.split("\\."); + if (parts.length < 2) return null; + + String candidate = parts[1]; + if (candidate.contains("-") || candidate.contains("_")) { + return candidate; + } + + candidate = parts[0]; + if (candidate.contains("-") || candidate.contains("_")) { + return candidate; + } + + if (!glazed$isVanillaCategory(parts[0])) { + return parts[1]; + } + + return null; + } + + @Unique + private static boolean glazed$isVanillaCategory(String category) { + return category.equals("key") || category.equals("gui") || category.equals("menu") + || category.equals("options") || category.equals("chat") || category.equals("commands") + || category.equals("block") || category.equals("item") || category.equals("entity") + || category.equals("biome") || category.equals("enchantment") || category.equals("effect") + || category.equals("container") || category.equals("death") || category.equals("gameMode") + || category.equals("selectWorld") || category.equals("createWorld") || category.equals("multiplayer") + || category.equals("lanServer") || category.equals("advMode") || category.equals("narrator") + || category.equals("subtitles") || category.equals("language") || category.equals("resourcePack") + || category.equals("dataPack") || category.equals("tutorial") || category.equals("demo") + || category.equals("disconnect") || category.equals("book") || category.equals("sign") + || category.equals("filled_map") || category.equals("structure_block") || category.equals("jigsaw_block") + || category.equals("argument") || category.equals("parsing") || category.equals("color") + || category.equals("stat") || category.equals("controls") || category.equals("attribute") + || category.equals("gamerule") || category.equals("difficulty") || category.equals("potion") + || category.equals("recipe") || category.equals("advancements") || category.equals("translation") + || category.equals("pack") || category.equals("soundCategory") || category.equals("title") + || category.equals("screenshot") || category.equals("mco") || category.equals("realms") + || category.equals("telemetry") || category.equals("accessibility") || category.equals("editGamerule") + || category.equals("spectatorMenu") || category.equals("record") || category.equals("instrument") + || category.equals("painting") || category.equals("trim"); + } +} diff --git a/src/main/java/com/nnpg/glazed/mixins/ScreenHandlerAccessor.java b/src/main/java/com/nnpg/glazed/mixins/ScreenHandlerAccessor.java new file mode 100644 index 00000000..c87ee82b --- /dev/null +++ b/src/main/java/com/nnpg/glazed/mixins/ScreenHandlerAccessor.java @@ -0,0 +1,12 @@ +// com/nnpg/glazed/mixins/ScreenHandlerAccessor.java +package com.nnpg.glazed.mixins; + +import net.minecraft.screen.ScreenHandler; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(ScreenHandler.class) +public interface ScreenHandlerAccessor { + @Accessor("revision") + int getRevision(); +} \ No newline at end of file diff --git a/src/main/java/com/nnpg/glazed/modules/esp/InvisESP.java b/src/main/java/com/nnpg/glazed/modules/esp/InvisESP.java index 2f5a9a90..1871ba4c 100644 --- a/src/main/java/com/nnpg/glazed/modules/esp/InvisESP.java +++ b/src/main/java/com/nnpg/glazed/modules/esp/InvisESP.java @@ -1,9 +1,9 @@ package com.nnpg.glazed.modules.esp; +import com.nnpg.glazed.GlazedAddon; import meteordevelopment.meteorclient.events.render.Render3DEvent; import meteordevelopment.meteorclient.renderer.ShapeMode; import meteordevelopment.meteorclient.settings.*; -import meteordevelopment.meteorclient.systems.modules.Categories; import meteordevelopment.meteorclient.systems.modules.Module; import meteordevelopment.meteorclient.utils.render.color.Color; import meteordevelopment.meteorclient.utils.render.color.SettingColor; @@ -130,7 +130,7 @@ public class InvisESP extends Module { ); public InvisESP() { - super(Categories.Render, "invis-esp", "Shows 3D hitbox for invisible players and mobs"); + super(GlazedAddon.esp, "invis-esp", "Shows 3D hitbox for invisible players and mobs"); } @Override diff --git a/src/main/java/com/nnpg/glazed/modules/main/UIHelper.java b/src/main/java/com/nnpg/glazed/modules/main/UIHelper.java index 99e5dcc8..848fad61 100644 --- a/src/main/java/com/nnpg/glazed/modules/main/UIHelper.java +++ b/src/main/java/com/nnpg/glazed/modules/main/UIHelper.java @@ -73,10 +73,6 @@ public UIHelper() { .build() ); - // Won't add for creating orders at the moment or for cancelling orders - // as creating orders requires more interaction and cancelling - // orders is not frequent enough to warrant an auto-confirm - // Progression: /tpa {player_name} -> "CONFIRM REQUEST" private final Setting acTPA = sgAutoConfirm.add(new BoolSetting.Builder() .name("tpa") @@ -128,11 +124,6 @@ public UIHelper() { .build() ); - - // Progression: "SHOP" -> "SHOP - {END/NETHER/GEAR/FOOD}" -> "BUYING {ITEM}" - // Not including shop at the moment due to normally having to set quantities - - // Progression: "CHOOSE 1 ITEM" -> "CONFIRM" private final Setting acCrateBuy = sgAutoConfirm.add(new BoolSetting.Builder() .name("crate-buy") @@ -151,7 +142,7 @@ public UIHelper() { .build() ); - // Progression: "{amount} {PIG/COW/ZOMBIE/SPIDER/SKELETON/CREEPER/ZOMBIFIED PIGLIN/BLAZE/IRON GOLEM} SPAWNERS" -> "CONFIRM SELL" + // Progression: "{amount} {PIG/COW/...} SPAWNERS" -> "CONFIRM SELL" private final Setting acSpawnerSellAll = sgAutoConfirm.add(new BoolSetting.Builder() .name("spawner-sell-all") .description("Automatically confirms selling all items in spawners.") @@ -170,6 +161,10 @@ public UIHelper() { .build() ); + // ───────────────────────────────────────────── + // AutoConfirm State + // ───────────────────────────────────────────── + private final CircularBuffer lastScreens = new CircularBuffer<>(5); private String currentScreen = null; @@ -177,126 +172,280 @@ public UIHelper() { private long commandTime = 0; private static final long COMMAND_TIMEOUT = 10000; + /** + * The screen title we are actively trying to confirm. + * Non-null means a confirm attempt is in progress. + */ + private String pendingConfirmScreen = null; + + /** + * How many times we have retried pressing the confirm button for the + * current pendingConfirmScreen without success. + */ + private int confirmRetryCount = 0; + + /** + * Maximum number of retries before giving up. + * 12 retries × 75 ms = ~900 ms total window — enough for slow servers. + */ + private static final int MAX_RETRIES = 12; + + /** + * Delay between retry attempts in milliseconds. + * Short enough to feel instant, long enough for the server to send slot data. + */ + private static final long RETRY_INTERVAL_MS = 75; + + /** Scheduled time (epoch ms) at which the next confirm attempt fires. */ private long acTimer = 0; + + /** When we last successfully sent a click, to prevent double-firing. */ private long lastClickTime = 0; private static final long CLICK_COOLDOWN = 1000; + // ───────────────────────────────────────────── + // Screen Tracking + // ───────────────────────────────────────────── + @EventHandler private void onOpenScreen(OpenScreenEvent event) { if (event.screen == null) { - // Reset timer if screen closes unexpectedly - if (acTimer > 0) { - acTimer = 0; - } + // Screen closed. We intentionally do NOT cancel pendingConfirmScreen here, + // because the server sometimes briefly closes then reopens the GUI + // (e.g. rapid order-fulfill flow). The retry logic in tryPressConfirmButton + // will handle it: if the screen stays gone it exhausts MAX_RETRIES and gives up. + currentScreen = null; return; } - if (event.screen instanceof HandledScreen) { - String newScreen = StringUtils.convertUnicodeToAscii(((HandledScreen) event.screen).getTitle().getString()).toUpperCase(); + if (!(event.screen instanceof HandledScreen)) { + return; + } - // Only update screen tracking if it's actually a new screen - if (currentScreen != null && !currentScreen.equals(newScreen)) { - lastScreens.add(currentScreen); - } - currentScreen = newScreen; + String newScreen = StringUtils.convertUnicodeToAscii( + ((HandledScreen) event.screen).getTitle().getString() + ).toUpperCase(); + + // Push previous screen into history only when it actually changed + if (currentScreen != null && !currentScreen.equals(newScreen)) { + lastScreens.add(currentScreen); } + currentScreen = newScreen; - if (shouldConfirm(currentScreen)) { - // Check cooldown to prevent rapid clicking - long currentTime = System.currentTimeMillis(); - if (currentTime - lastClickTime < CLICK_COOLDOWN) { - return; - } - acTimer = currentTime + acRandomDelay.get().getRandom(); + if (!enableAutoConfirm.get()) return; + + // Respect click cooldown before scheduling a new confirm + if (System.currentTimeMillis() - lastClickTime < CLICK_COOLDOWN) return; + + if (shouldConfirm(newScreen)) { + // Start a fresh confirm attempt for this screen + pendingConfirmScreen = newScreen; + confirmRetryCount = 0; + acTimer = System.currentTimeMillis() + acRandomDelay.get().getRandom(); } } + + // ───────────────────────────────────────────── + // Command Tracking (for context-sensitive confirms) + // ───────────────────────────────────────────── + @EventHandler private void onSendPacket(PacketEvent.Send event) { + if (!isActive()) return; + + // AutoConfirm: track relevant commands for context checks + String command = null; if (event.packet instanceof ChatMessageC2SPacket packet) { - String message = packet.chatMessage().trim(); - if (message.startsWith("/")) { - if (message.startsWith("/ah sell") || message.startsWith("/tpa ") || - message.startsWith("/tpahere ") || message.startsWith("/tpaccept ") || - message.startsWith("/bounty add ")) { - currentCommand = message; - commandTime = System.currentTimeMillis(); + String msg = packet.chatMessage().trim(); + if (msg.startsWith("/")) command = msg; + } else if (event.packet instanceof CommandExecutionC2SPacket packet) { + command = "/" + packet.command().trim(); + } + + if (command != null && isTrackedCommand(command)) { + currentCommand = command; + commandTime = System.currentTimeMillis(); + } + + // AutoAdvance: track "drop loot" clicks + if (enableAutoAdvance.get()) { + if (event.packet instanceof ClickSlotC2SPacket packet) { + if (StringUtils.convertUnicodeToAscii( + packet.getStack().getName().getString()).equals("drop loot")) { + aaTimer = System.currentTimeMillis() + aaRandomDelay.get().getRandom(); } } - } else if (event.packet instanceof CommandExecutionC2SPacket packet) { - String command = "/" + packet.command().trim(); - // Check if it's a relevant command - if (command.startsWith("/ah sell") || command.startsWith("/tpa ") || - command.startsWith("/tpahere ") || command.startsWith("/tpaccept ") || - command.startsWith("/bounty add ")) { - currentCommand = command; - commandTime = System.currentTimeMillis(); + } + } + + private boolean isTrackedCommand(String cmd) { + return cmd.startsWith("/ah sell") || + cmd.startsWith("/tpa ") || + cmd.startsWith("/tpahere ") || + cmd.startsWith("/tpaccept ") || + cmd.startsWith("/bounty add "); + } + + + // ───────────────────────────────────────────── + // Tick: fire timers + // ───────────────────────────────────────────── + + @EventHandler + private void onTick(TickEvent.Pre event) { + if (acTimer > 0 && System.currentTimeMillis() >= acTimer) { + acTimer = 0; + if (pendingConfirmScreen != null) { + tryPressConfirmButton(); } } + + if (aaTimer > 0 && System.currentTimeMillis() >= aaTimer) { + aaTimer = 0; + advancePage(aaDirection.get()); + } } - private boolean shouldConfirm(String currentScreenTitle) { - if (currentScreenTitle == null) { - return false; + + // ───────────────────────────────────────────── + // Core confirm logic — robust with retries + // ───────────────────────────────────────────── + + /** + * Attempts to click the confirm/accept button in the currently open screen. + * + * If the screen is not yet open, or if the inventory slots are not yet + * populated by the server, the attempt is retried up to MAX_RETRIES times + * with a short RETRY_INTERVAL_MS delay between each try. This covers the + * two main race conditions: + * + * (a) Screen opened but server hasn't sent slot data yet → button missing. + * (b) Rapid open/close cycle → mc.currentScreen is momentarily null. + * + * Only gives up if: + * – MAX_RETRIES is exhausted, or + * – a completely unrelated (non-confirm) screen is now open. + */ + private void tryPressConfirmButton() { + if (mc.player == null || mc.interactionManager == null) { + cancelPending(); + return; } - if (!(currentScreenTitle.contains("CONFIRM") || currentScreenTitle.contains("ACCEPT"))) { - return false; + + // ── Case: screen not open yet (transient null after rapid open/close) ── + if (mc.currentScreen == null) { + scheduleRetry(); + return; } + // ── Case: open screen is not a handled/inventory screen ── + if (!(mc.currentScreen instanceof HandledScreen)) { + cancelPending(); + return; + } + + HandledScreen screen = (HandledScreen) mc.currentScreen; + String actualTitle = StringUtils.convertUnicodeToAscii( + screen.getTitle().getString() + ).toUpperCase(); + + // ── Case: a different screen opened (not the one we planned to confirm) ── + if (!actualTitle.equals(pendingConfirmScreen)) { + if (shouldConfirm(actualTitle)) { + // A new valid confirm screen appeared — update and retry from fresh + pendingConfirmScreen = actualTitle; + confirmRetryCount = 0; + scheduleRetry(); + } else { + // User navigated somewhere else entirely — abandon + cancelPending(); + } + return; + } + + // ── Correct screen is open — search for the button ── + ScreenHandler handler = screen.getScreenHandler(); + + for (int i = 0; i < handler.slots.size(); i++) { + ItemStack stack = handler.getSlot(i).getStack(); + if (isConfirmButton(stack)) { + // Click twice (vanilla double-click protection workaround) + mc.interactionManager.clickSlot(handler.syncId, i, 0, SlotActionType.PICKUP, mc.player); + mc.interactionManager.clickSlot(handler.syncId, i, 0, SlotActionType.PICKUP, mc.player); + lastClickTime = System.currentTimeMillis(); + cancelPending(); // success — clear state + return; + } + } + + // ── Button not found: slots likely not populated by server yet ── + scheduleRetry(); + } + + /** Arms the next retry if we still have budget, otherwise gives up. */ + private void scheduleRetry() { + if (confirmRetryCount < MAX_RETRIES) { + confirmRetryCount++; + acTimer = System.currentTimeMillis() + RETRY_INTERVAL_MS; + } else { + cancelPending(); + } + } + + /** Clears all pending confirm state. */ + private void cancelPending() { + pendingConfirmScreen = null; + confirmRetryCount = 0; + acTimer = 0; + } + + + // ───────────────────────────────────────────── + // shouldConfirm + // ───────────────────────────────────────────── + + private boolean shouldConfirm(String screenTitle) { + if (screenTitle == null) return false; + if (!(screenTitle.contains("CONFIRM") || screenTitle.contains("ACCEPT"))) return false; + boolean shouldConfirm = false; - switch (currentScreenTitle) { + switch (screenTitle) { case "CONFIRM PURCHASE" -> { boolean foundAuction = false; boolean foundShardShop = false; - for (int i = 0; i < Math.min(lastScreens.size, 3); i++) { try { - String recentScreen = lastScreens.get(i); - if (recentScreen != null) { - if (recentScreen.contains("AUCTION")) { - foundAuction = true; - } - if (recentScreen.contains("SHOP - SHARD SHOP")) { - foundShardShop = true; - } + String recent = lastScreens.get(i); + if (recent != null) { + if (recent.contains("AUCTION")) foundAuction = true; + if (recent.contains("SHOP - SHARD SHOP")) foundShardShop = true; } - } catch (Exception e) { - // Ignore screen check errors - } - } - - if (acAHBuy.get() && foundAuction) { - shouldConfirm = true; - } else if (acShardshopBuy.get() && foundShardShop) { - shouldConfirm = true; + } catch (Exception ignored) {} } + if (acAHBuy.get() && foundAuction) shouldConfirm = true; + else if (acShardshopBuy.get() && foundShardShop) shouldConfirm = true; } case "CONFIRM LISTING" -> { - if (acAHSell.get()) { - shouldConfirm = true; - } + if (acAHSell.get()) shouldConfirm = true; } case "ORDERS -> CONFIRM DELIVERY" -> { - // Check previous screen for Orders - String prevScreen = lastScreens.get(0); - if (acOrderFulfill.get() && prevScreen != null && prevScreen.contains("ORDERS")) { - shouldConfirm = true; - } + try { + String prevScreen = lastScreens.get(0); + if (acOrderFulfill.get() && prevScreen != null && prevScreen.contains("ORDERS")) { + shouldConfirm = true; + } + } catch (Exception ignored) {} } case "CONFIRM REQUEST" -> { - // Check current command for /tpa or /tpahere if (currentCommand != null && System.currentTimeMillis() - commandTime < COMMAND_TIMEOUT) { - if (acTPA.get() && currentCommand.startsWith("/tpa ")) { - shouldConfirm = true; - } else if (acTPAHere.get() && currentCommand.startsWith("/tpahere ")) { - shouldConfirm = true; - } + if (acTPA.get() && currentCommand.startsWith("/tpa ")) shouldConfirm = true; + else if (acTPAHere.get() && currentCommand.startsWith("/tpahere ")) shouldConfirm = true; } } case "ACCEPT REQUEST" -> { - // Check current command for /tpaccept if (acTPAReceive.get() && currentCommand != null && System.currentTimeMillis() - commandTime < COMMAND_TIMEOUT && currentCommand.startsWith("/tpaccept ")) { @@ -304,7 +453,6 @@ private boolean shouldConfirm(String currentScreenTitle) { } } case "ACCEPT TPAHERE REQUEST" -> { - // Check current command for /tpaccept if (acTPAHereReceive.get() && currentCommand != null && System.currentTimeMillis() - commandTime < COMMAND_TIMEOUT && currentCommand.startsWith("/tpaccept ")) { @@ -312,23 +460,19 @@ private boolean shouldConfirm(String currentScreenTitle) { } } case "CONFIRM" -> { - // Check recent screens for CHOOSE 1 ITEM if (acCrateBuy.get()) { for (int i = 0; i < Math.min(lastScreens.size, 3); i++) { try { - String recentScreen = lastScreens.get(i); - if (recentScreen != null && recentScreen.contains("CHOOSE 1 ITEM")) { + String recent = lastScreens.get(i); + if (recent != null && recent.contains("CHOOSE 1 ITEM")) { shouldConfirm = true; break; } - } catch (Exception e) { - // Ignore screen access errors - } + } catch (Exception ignored) {} } } } case "CONFIRM BOUNTY" -> { - // Check current command for /bounty add if (acBounty.get() && currentCommand != null && System.currentTimeMillis() - commandTime < COMMAND_TIMEOUT && currentCommand.startsWith("/bounty add ")) { @@ -336,71 +480,32 @@ private boolean shouldConfirm(String currentScreenTitle) { } } case "CONFIRM SELL" -> { - // Check previous screen for spawner sell all try { String prevScreen = lastScreens.get(0); - if (acSpawnerSellAll.get() && prevScreen != null && (prevScreen.contains("SPAWNER"))) { + if (acSpawnerSellAll.get() && prevScreen != null && prevScreen.contains("SPAWNER")) { shouldConfirm = true; } - } catch (Exception e) { - // Ignore if no previous screen - } + } catch (Exception ignored) {} } } - return shouldConfirm; - } - - - private void pressConfirmButton() { - if (mc.player == null || mc.interactionManager == null) { - return; - } - - if (mc.currentScreen == null) { - // Don't retry if recently clicked - if (System.currentTimeMillis() - lastClickTime < CLICK_COOLDOWN) { - acTimer = 0; - return; - } - acTimer = System.currentTimeMillis() + 10; // Retry - return; - } - - if (!(mc.currentScreen instanceof HandledScreen)) { - return; - } - - HandledScreen screen = (HandledScreen) mc.currentScreen; - ScreenHandler handler = screen.getScreenHandler(); - // Find the confirm button (green/lime stained glass pane with "confirm" or "accept" in the name) - for (int i = 0; i < handler.slots.size(); i++) { - ItemStack stack = handler.getSlot(i).getStack(); - if (isConfirmButton(stack)) { - // Double Click - mc.interactionManager.clickSlot(handler.syncId, i, 0, SlotActionType.PICKUP, mc.player); - mc.interactionManager.clickSlot(handler.syncId, i, 0, SlotActionType.PICKUP, mc.player); - lastClickTime = System.currentTimeMillis(); - acTimer = 0; // Clear timer to prevent immediate retry - return; - } - } + return shouldConfirm; } private boolean isConfirmButton(ItemStack stack) { if (stack.isEmpty()) return false; - - // Check if it's a green/lime stained glass pane boolean isGreenGlass = stack.getItem() == Items.LIME_STAINED_GLASS_PANE || - stack.getItem() == Items.GREEN_STAINED_GLASS_PANE; - - // Check if the name contains confirm or accept + stack.getItem() == Items.GREEN_STAINED_GLASS_PANE; String name = StringUtils.convertUnicodeToAscii(stack.getName().getString()).toLowerCase(); boolean hasConfirmText = name.contains("confirm") || name.contains("accept"); - return isGreenGlass && hasConfirmText; } + + // ───────────────────────────────────────────── + // AutoAdvance + // ───────────────────────────────────────────── + private final SettingGroup sgAutoAdvance = settings.createGroup("AutoAdvance"); private final Setting aaDescription = sgAutoAdvance.add(new TextDisplaySetting.Builder() @@ -438,20 +543,16 @@ private boolean isConfirmButton(ItemStack stack) { private long aaTimer = 0; private void advancePage(Direction dir) { - if (mc.player == null || mc.interactionManager == null || mc.currentScreen == null) { - return; - } - if (!(mc.currentScreen instanceof HandledScreen screen)) { - return; - } + if (mc.player == null || mc.interactionManager == null || mc.currentScreen == null) return; + if (!(mc.currentScreen instanceof HandledScreen screen)) return; ScreenHandler handler = screen.getScreenHandler(); for (int i = 0; i < handler.slots.size(); i++) { - String name = StringUtils.convertUnicodeToAscii(handler.getSlot(i).getStack().getName().getString()); - if ((dir == Direction.FORWARDS && name.equals("next")) || - (dir == Direction.BACKWARDS && name.equals("back"))) { - // Double click + String name = StringUtils.convertUnicodeToAscii( + handler.getSlot(i).getStack().getName().getString()); + if ((dir == Direction.FORWARDS && name.equals("next")) || + (dir == Direction.BACKWARDS && name.equals("back"))) { mc.interactionManager.clickSlot(handler.syncId, i, 0, SlotActionType.PICKUP, mc.player); mc.interactionManager.clickSlot(handler.syncId, i, 0, SlotActionType.PICKUP, mc.player); return; @@ -464,36 +565,10 @@ private enum Direction { BACKWARDS } - @EventHandler - private void onPacketSend(PacketEvent.Send event) { - if (!isActive()) return; - - // AutoAdvance functionality - if (enableAutoAdvance.get()) { - // Check if the packet is a slot click packet (when player clicks items in GUI) - if (event.packet instanceof ClickSlotC2SPacket packet) { - // Detect if the slot clicked contained a dropper - if (StringUtils.convertUnicodeToAscii(packet.getStack().getName().getString()).equals("drop loot")) { - // Start timer after dropping loot (delay is already in milliseconds) - aaTimer = System.currentTimeMillis() + aaRandomDelay.get().getRandom(); - } - } - } - } - @EventHandler - private void onTick(TickEvent.Pre event) { - if (acTimer > 0 && System.currentTimeMillis() >= acTimer) { - acTimer = 0; - if (currentScreen != null && shouldConfirm(currentScreen)) { - pressConfirmButton(); - } - } - if (aaTimer > 0 && System.currentTimeMillis() >= aaTimer) { - aaTimer = 0; - advancePage(aaDirection.get()); - } - } + // ───────────────────────────────────────────── + // Utilities + // ───────────────────────────────────────────── private static class CircularBuffer { private final Object[] buffer; diff --git a/src/main/java/com/nnpg/glazed/protection/ClientSpoofer.java b/src/main/java/com/nnpg/glazed/protection/ClientSpoofer.java new file mode 100644 index 00000000..fbcb02d0 --- /dev/null +++ b/src/main/java/com/nnpg/glazed/protection/ClientSpoofer.java @@ -0,0 +1,40 @@ +package com.nnpg.glazed.protection; + +import net.minecraft.util.Identifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Set; + +public class ClientSpoofer { + private static final Logger LOGGER = LoggerFactory.getLogger("Glazed-Protection"); + + private static final String MINECRAFT_NAMESPACE = "minecraft"; + + private static final Set ALLOWED_CHANNELS = Set.of( + "minecraft:brand", + "minecraft:client_information", + "minecraft:register", + "minecraft:unregister" + ); + + private ClientSpoofer() {} + + public static boolean shouldBlockPayload(Identifier id) { + if (id == null) return false; + + String channel = id.toString(); + String namespace = id.getNamespace(); + + if (ALLOWED_CHANNELS.contains(channel)) { + return false; + } + + if (!MINECRAFT_NAMESPACE.equals(namespace)) { + LOGGER.info("[Glazed Protection] Blocking mod channel: {}", channel); + return true; + } + + return false; + } +} diff --git a/src/main/java/com/nnpg/glazed/protection/KeybindDefaults.java b/src/main/java/com/nnpg/glazed/protection/KeybindDefaults.java new file mode 100644 index 00000000..c3179fd4 --- /dev/null +++ b/src/main/java/com/nnpg/glazed/protection/KeybindDefaults.java @@ -0,0 +1,60 @@ +package com.nnpg.glazed.protection; + +import java.util.HashMap; +import java.util.Map; + +public class KeybindDefaults { + private static final Map DEFAULTS = new HashMap<>(); + + static { + + DEFAULTS.put("key.forward", "W"); + DEFAULTS.put("key.left", "A"); + DEFAULTS.put("key.back", "S"); + DEFAULTS.put("key.right", "D"); + DEFAULTS.put("key.jump", "Space"); + DEFAULTS.put("key.sneak", "Left Shift"); + DEFAULTS.put("key.sprint", "Left Control"); + + DEFAULTS.put("key.attack", "Left Button"); + DEFAULTS.put("key.use", "Right Button"); + DEFAULTS.put("key.pickItem", "Middle Button"); + DEFAULTS.put("key.drop", "Q"); + DEFAULTS.put("key.swapOffhand", "F"); + + DEFAULTS.put("key.inventory", "E"); + DEFAULTS.put("key.hotbar.1", "1"); + DEFAULTS.put("key.hotbar.2", "2"); + DEFAULTS.put("key.hotbar.3", "3"); + DEFAULTS.put("key.hotbar.4", "4"); + DEFAULTS.put("key.hotbar.5", "5"); + DEFAULTS.put("key.hotbar.6", "6"); + DEFAULTS.put("key.hotbar.7", "7"); + DEFAULTS.put("key.hotbar.8", "8"); + DEFAULTS.put("key.hotbar.9", "9"); + + DEFAULTS.put("key.chat", "T"); + DEFAULTS.put("key.playerlist", "Tab"); + DEFAULTS.put("key.command", "/"); + DEFAULTS.put("key.socialInteractions", "P"); + DEFAULTS.put("key.advancements", "L"); + DEFAULTS.put("key.screenshot", "F2"); + DEFAULTS.put("key.fullscreen", "F11"); + DEFAULTS.put("key.spectatorOutlines", ""); + + DEFAULTS.put("key.saveToolbarActivator", "C"); + DEFAULTS.put("key.loadToolbarActivator", "X"); + + DEFAULTS.put("key.smoothCamera", ""); + } + + public static boolean hasDefault(String keybindName) { + return keybindName != null && DEFAULTS.containsKey(keybindName); + } + + public static String getDefault(String keybindName) { + return keybindName != null ? DEFAULTS.getOrDefault(keybindName, keybindName) : null; + } + + private KeybindDefaults() {} +} diff --git a/src/main/java/com/nnpg/glazed/protection/ModRegistry.java b/src/main/java/com/nnpg/glazed/protection/ModRegistry.java new file mode 100644 index 00000000..e329aa0e --- /dev/null +++ b/src/main/java/com/nnpg/glazed/protection/ModRegistry.java @@ -0,0 +1,204 @@ +package com.nnpg.glazed.protection; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class ModRegistry { + private static final Logger LOGGER = LoggerFactory.getLogger("Glazed-Protection"); + + private static final Set vanillaTranslationKeys = ConcurrentHashMap.newKeySet(); + + private static final Set vanillaKeybinds = ConcurrentHashMap.newKeySet(); + + private static final Set serverPackKeys = ConcurrentHashMap.newKeySet(); + + private static final Set allKnownTranslationKeys = ConcurrentHashMap.newKeySet(); + + private static final Map translationKeyToModId = new ConcurrentHashMap<>(); + + private static final Map modRegistry = new ConcurrentHashMap<>(); + + private static volatile boolean initialized = false; + + private ModRegistry() {} + + public static class ModInfo { + private final String modId; + private final Set translationKeys = ConcurrentHashMap.newKeySet(); + private final Set keybinds = ConcurrentHashMap.newKeySet(); + private boolean whitelisted = false; + + public ModInfo(String modId) { + this.modId = modId; + } + + public String getModId() { + return modId; + } + + public Set getTranslationKeys() { + return translationKeys; + } + + public Set getKeybinds() { + return keybinds; + } + + public boolean isWhitelisted() { + return whitelisted; + } + + public void setWhitelisted(boolean whitelisted) { + this.whitelisted = whitelisted; + } + + public void addTranslationKey(String key) { + translationKeys.add(key); + } + + public void addKeybind(String keybind) { + keybinds.add(keybind); + } + + public boolean hasTranslationKeys() { + return !translationKeys.isEmpty(); + } + + public boolean hasKeybinds() { + return !keybinds.isEmpty(); + } + } + + public static void recordTranslationKey(String modId, String key) { + if (modId == null || key == null) return; + allKnownTranslationKeys.add(key); + translationKeyToModId.put(key, modId); + + ModInfo info = modRegistry.computeIfAbsent(modId, ModInfo::new); + info.addTranslationKey(key); + } + + public static void recordVanillaTranslationKey(String key) { + if (key == null) return; + vanillaTranslationKeys.add(key); + allKnownTranslationKeys.add(key); + } + + public static void recordServerPackKey(String key) { + if (key == null) return; + serverPackKeys.add(key); + allKnownTranslationKeys.add(key); + } + + public static boolean isVanillaTranslationKey(String key) { + return key != null && vanillaTranslationKeys.contains(key); + } + + public static boolean isServerPackTranslationKey(String key) { + return key != null && serverPackKeys.contains(key); + } + + public static String getModForTranslationKey(String key) { + if (key == null) return null; + return translationKeyToModId.get(key); + } + + public static boolean isWhitelistedTranslationKey(String key) { + if (key == null) return false; + String modId = translationKeyToModId.get(key); + if (modId == null) return false; + ModInfo info = modRegistry.get(modId); + return info != null && info.isWhitelisted(); + } + + public static void clearTranslationKeys() { + vanillaTranslationKeys.clear(); + serverPackKeys.clear(); + allKnownTranslationKeys.clear(); + translationKeyToModId.clear(); + LOGGER.info("[ModRegistry] Cleared translation key cache"); + } + + public static void clearServerPackKeys() { + serverPackKeys.clear(); + LOGGER.info("[ModRegistry] Cleared server pack keys"); + } + + public static void recordVanillaKeybind(String keybindName) { + if (keybindName == null) return; + vanillaKeybinds.add(keybindName); + } + + public static void recordModKeybind(String modId, String keybindName) { + if (modId == null || keybindName == null) return; + ModInfo info = modRegistry.computeIfAbsent(modId, ModInfo::new); + info.addKeybind(keybindName); + } + + public static boolean isVanillaKeybind(String keybindName) { + return keybindName != null && vanillaKeybinds.contains(keybindName); + } + + public static boolean isWhitelistedKeybind(String keybindName) { + if (keybindName == null) return false; + + for (ModInfo info : modRegistry.values()) { + if (info.getKeybinds().contains(keybindName)) { + return info.isWhitelisted(); + } + } + return false; + } + + public static ModInfo getModInfo(String modId) { + return modRegistry.get(modId); + } + + public static Set getAllModIds() { + return modRegistry.keySet(); + } + + public static void setModWhitelisted(String modId, boolean whitelisted) { + ModInfo info = modRegistry.get(modId); + if (info != null) { + info.setWhitelisted(whitelisted); + LOGGER.info("[ModRegistry] Mod '{}' whitelist status: {}", modId, whitelisted); + } + } + + public static void markInitialized() { + initialized = true; + LOGGER.info("[ModRegistry] Initialized with {} translation keys", + allKnownTranslationKeys.size()); + } + + public static boolean isInitialized() { + return initialized; + } + + public static int getVanillaKeyCount() { + return vanillaTranslationKeys.size(); + } + + public static int getServerPackKeyCount() { + return serverPackKeys.size(); + } + + public static int getTranslationKeyCount() { + return allKnownTranslationKeys.size(); + } + + public static int getModCount() { + return modRegistry.size(); + } + + public static int getWhitelistedModCount() { + return (int) modRegistry.values().stream() + .filter(ModInfo::isWhitelisted) + .count(); + } +} diff --git a/src/main/java/com/nnpg/glazed/protection/PacketContext.java b/src/main/java/com/nnpg/glazed/protection/PacketContext.java new file mode 100644 index 00000000..e6016c3e --- /dev/null +++ b/src/main/java/com/nnpg/glazed/protection/PacketContext.java @@ -0,0 +1,37 @@ +package com.nnpg.glazed.protection; + +import net.minecraft.network.packet.Packet; + +public class PacketContext { + private static final ThreadLocal PROCESSING_PACKET = + ThreadLocal.withInitial(() -> false); + + private static final ThreadLocal PACKET_NAME = + ThreadLocal.withInitial(() -> "unknown"); + + private PacketContext() {} + + public static boolean isProcessingPacket() { + return PROCESSING_PACKET.get(); + } + + public static void setProcessingPacket(boolean value) { + PROCESSING_PACKET.set(value); + } + + public static String getPacketName() { + return PACKET_NAME.get(); + } + + public static void setPacketName(Object packet) { + if (packet instanceof Packet p) { + try { + + String name = p.getPacketType().toString(); + PACKET_NAME.set(name != null ? name : p.getClass().getSimpleName()); + } catch (Exception e) { + PACKET_NAME.set(p.getClass().getSimpleName()); + } + } + } +} diff --git a/src/main/java/com/nnpg/glazed/protection/TranslationProtectionHandler.java b/src/main/java/com/nnpg/glazed/protection/TranslationProtectionHandler.java new file mode 100644 index 00000000..90f0e092 --- /dev/null +++ b/src/main/java/com/nnpg/glazed/protection/TranslationProtectionHandler.java @@ -0,0 +1,62 @@ +package com.nnpg.glazed.protection; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class TranslationProtectionHandler { + private static final Logger LOGGER = LoggerFactory.getLogger("Glazed-Protection"); + + public enum InterceptionType { + TRANSLATION("Translation"), + KEYBIND("Keybind"); + + private final String displayName; + InterceptionType(String displayName) { this.displayName = displayName; } + public String getDisplayName() { return displayName; } + } + + private record AlertDedupeKey(InterceptionType type, String keyName) {} + private record LogDedupeKey(InterceptionType type, String packetName, String keyName, String originalValue, String spoofedValue) {} + + private static final Set alertedKeys = ConcurrentHashMap.newKeySet(); + private static final Set loggedKeys = ConcurrentHashMap.newKeySet(); + + private static final int MAX_DEDUPE_ENTRIES = 500; + + private TranslationProtectionHandler() {} + + public static void notifyExploitDetected() { + + } + + public static void sendDetail(InterceptionType type, String keyName, String originalValue, String spoofedValue) { + + } + + public static void sendDetailDebug(InterceptionType type, String keyName, String originalValue, String spoofedValue) { + + } + + public static void logDetection(InterceptionType type, String keyName, String originalValue, String spoofedValue) { + String packetName = PacketContext.getPacketName(); + + if (loggedKeys.size() >= MAX_DEDUPE_ENTRIES) { + loggedKeys.clear(); + } + + if (!loggedKeys.add(new LogDedupeKey(type, packetName, keyName, originalValue, spoofedValue))) { + return; + } + + LOGGER.info("[{}:{}] '{}' '{}' -> '{}'", + type.getDisplayName(), packetName, keyName, originalValue, spoofedValue); + } + + public static void clearCache() { + alertedKeys.clear(); + loggedKeys.clear(); + } +} diff --git a/src/main/java/com/nnpg/glazed/utils/glazed/RotationUtil.java b/src/main/java/com/nnpg/glazed/utils/glazed/RotationUtil.java new file mode 100644 index 00000000..096889ca --- /dev/null +++ b/src/main/java/com/nnpg/glazed/utils/glazed/RotationUtil.java @@ -0,0 +1,272 @@ +package com.nnpg.glazed.utils.glazed; + +import net.minecraft.client.MinecraftClient; +import net.minecraft.util.math.MathHelper; +import net.minecraft.util.math.Vec3d; + +import java.util.concurrent.ThreadLocalRandom; + +public class RotationUtil { + + public enum CurveType { + SIGMOID, + ACCELERATION, + LINEAR + } + + public static class RotationConfig { + final CurveType curve; + final float maxDegreesPerTick; + final float yawAccelMin; + final float yawAccelMax; + final float pitchAccelMin; + final float pitchAccelMax; + final float sigmoidSteepness; + final float sigmoidMidpoint; + final float yawAccelError; + final float pitchAccelError; + final float yawConstError; + final float pitchConstError; + final float inputBlendWeight; + final float resetThreshold; + + private RotationConfig(Builder b) { + this.curve = b.curve; + this.maxDegreesPerTick = b.maxDegreesPerTick; + this.yawAccelMin = b.yawAccelMin; + this.yawAccelMax = b.yawAccelMax; + this.pitchAccelMin = b.pitchAccelMin; + this.pitchAccelMax = b.pitchAccelMax; + this.sigmoidSteepness = b.sigmoidSteepness; + this.sigmoidMidpoint = b.sigmoidMidpoint; + this.yawAccelError = b.yawAccelError; + this.pitchAccelError = b.pitchAccelError; + this.yawConstError = b.yawConstError; + this.pitchConstError = b.pitchConstError; + this.inputBlendWeight = MathHelper.clamp(b.inputBlendWeight, 0f, 1f); + this.resetThreshold = b.resetThreshold; + } + + public static class Builder { + private CurveType curve = CurveType.ACCELERATION; + private float maxDegreesPerTick = 30.0f; + private float yawAccelMin = 20.0f; + private float yawAccelMax = 25.0f; + private float pitchAccelMin = 20.0f; + private float pitchAccelMax = 25.0f; + private float sigmoidSteepness = 10.0f; + private float sigmoidMidpoint = 0.3f; + private float yawAccelError = 0.1f; + private float pitchAccelError = 0.1f; + private float yawConstError = 0.1f; + private float pitchConstError = 0.1f; + private float inputBlendWeight = 0.0f; + private float resetThreshold = 0.5f; + + public Builder curve(CurveType v) { this.curve = v; return this; } + public Builder maxDegreesPerTick(float v) { this.maxDegreesPerTick = v; return this; } + public Builder yawAccel(float min, float max) { yawAccelMin = min; yawAccelMax = max; return this; } + public Builder pitchAccel(float min, float max) { pitchAccelMin = min; pitchAccelMax = max; return this; } + public Builder sigmoidSteepness(float v) { this.sigmoidSteepness = v; return this; } + public Builder sigmoidMidpoint(float v) { this.sigmoidMidpoint = v; return this; } + public Builder yawAccelError(float v) { this.yawAccelError = v; return this; } + public Builder pitchAccelError(float v) { this.pitchAccelError = v; return this; } + public Builder yawConstError(float v) { this.yawConstError = v; return this; } + public Builder pitchConstError(float v) { this.pitchConstError = v; return this; } + public Builder inputBlendWeight(float v) { this.inputBlendWeight = v; return this; } + public Builder resetThreshold(float v) { this.resetThreshold = v; return this; } + public RotationConfig build() { return new RotationConfig(this); } + } + } + + private static final MinecraftClient mc = MinecraftClient.getInstance(); + + private boolean active = false; + private float targetYaw = 0f; + private float targetPitch = 0f; + private float currentYaw = 0f; + private float currentPitch = 0f; + private float previousYaw = 0f; + private float previousPitch = 0f; + private float prevMouseYaw = 0f; + private float prevMousePitch = 0f; + + private RotationConfig config; + + public void start(float targetYaw, float targetPitch, RotationConfig config) { + if (mc.player == null) return; + this.config = config; + this.targetYaw = MathHelper.wrapDegrees(targetYaw); + this.targetPitch = MathHelper.clamp(targetPitch, -90f, 90f); + this.currentYaw = mc.player.getYaw(); + this.currentPitch = mc.player.getPitch(); + this.previousYaw = this.currentYaw; + this.previousPitch = this.currentPitch; + this.prevMouseYaw = this.currentYaw; + this.prevMousePitch = this.currentPitch; + this.active = true; + } + + public boolean tick() { + if (!active || mc.player == null) return true; + + float remainingYaw = angleDiff(currentYaw, targetYaw); + float remainingPitch = targetPitch - currentPitch; + float geometricAngle = (float) Math.sqrt(remainingYaw * remainingYaw + remainingPitch * remainingPitch); + + if (geometricAngle <= config.resetThreshold) { + applyRotationWithFixedYaw(targetYaw, targetPitch); + active = false; + return true; + } + + float rawMouseYaw = mc.player.getYaw(); + float rawMousePitch = mc.player.getPitch(); + float mouseDeltaYaw = angleDiff(prevMouseYaw, rawMouseYaw); + float mouseDeltaPitch = rawMousePitch - prevMousePitch; + prevMouseYaw = rawMouseYaw; + prevMousePitch = rawMousePitch; + + float prevDeltaYaw = angleDiff(previousYaw, currentYaw); + float prevDeltaPitch = currentPitch - previousPitch; + + float newDeltaYaw; + float newDeltaPitch; + + switch (config.curve) { + case ACCELERATION -> { + newDeltaYaw = computeAccel(remainingYaw, prevDeltaYaw, geometricAngle, true); + newDeltaPitch = computeAccel(remainingPitch, prevDeltaPitch, geometricAngle, false); + } + case SIGMOID -> { + float factor = sigmoidFactor(geometricAngle); + float speedYaw = randomRange(config.yawAccelMin, config.yawAccelMax) * factor; + float speedPitch = randomRange(config.pitchAccelMin, config.pitchAccelMax) * factor; + newDeltaYaw = Math.abs(remainingYaw) > 0 ? MathHelper.clamp(remainingYaw, -speedYaw, speedYaw) : 0f; + newDeltaPitch = Math.abs(remainingPitch) > 0 ? MathHelper.clamp(remainingPitch, -speedPitch, speedPitch) : 0f; + } + default -> { + float speedYaw = Math.min(config.maxDegreesPerTick, Math.abs(remainingYaw)); + float speedPitch = Math.min(config.maxDegreesPerTick, Math.abs(remainingPitch)); + newDeltaYaw = Math.signum(remainingYaw) * speedYaw; + newDeltaPitch = Math.signum(remainingPitch) * speedPitch; + } + } + + newDeltaYaw = MathHelper.clamp(newDeltaYaw, -config.maxDegreesPerTick, config.maxDegreesPerTick); + newDeltaPitch = MathHelper.clamp(newDeltaPitch, -config.maxDegreesPerTick, config.maxDegreesPerTick); + + newDeltaYaw += errorForAxis(newDeltaYaw, config.yawAccelError, config.yawConstError); + newDeltaPitch += errorForAxis(newDeltaPitch, config.pitchAccelError, config.pitchConstError); + + if (config.inputBlendWeight > 0f) { + float w = config.inputBlendWeight; + newDeltaYaw = newDeltaYaw * (1f - w) + mouseDeltaYaw * w; + newDeltaPitch = newDeltaPitch * (1f - w) + mouseDeltaPitch * w; + } + + double gcd = computeGcd(); + newDeltaYaw = snapToGcd(newDeltaYaw, gcd); + newDeltaPitch = snapToGcd(newDeltaPitch, gcd); + + previousYaw = currentYaw; + previousPitch = currentPitch; + + float newYaw = currentYaw + newDeltaYaw; + float newPitch = MathHelper.clamp(currentPitch + newDeltaPitch, -90f, 90f); + + applyRotation(newYaw, newPitch); + + return false; + } + + public boolean isActive() { return active; } + public float getCurrentYaw() { return currentYaw; } + public float getCurrentPitch() { return currentPitch; } + + public void cancel() { + if (!active || mc.player == null) { active = false; return; } + applyRotationWithFixedYaw(currentYaw, currentPitch); + active = false; + } + + /** + * Correct the velocity vector for movement packets when rotation is active. + * Call this in the movement input handler of the module using this util. + * Returns corrected movement input velocity using the rotated yaw. + */ + public Vec3d correctMovement(Vec3d inputVelocity, float speed) { + if (!active || mc.player == null) return inputVelocity; + float yaw = (float) Math.toRadians(currentYaw); + double sin = Math.sin(yaw); + double cos = Math.cos(yaw); + double x = inputVelocity.x * cos - inputVelocity.z * sin; + double z = inputVelocity.x * sin + inputVelocity.z * cos; + return new Vec3d(x, inputVelocity.y, z).normalize().multiply(speed); + } + + private void applyRotation(float yaw, float pitch) { + if (mc.player == null) return; + currentYaw = yaw; + currentPitch = pitch; + mc.player.prevYaw = mc.player.getYaw(); + mc.player.prevPitch = mc.player.getPitch(); + mc.player.bodyYaw = yaw; + mc.player.prevBodyYaw = yaw; + mc.player.setYaw(yaw); + mc.player.setPitch(pitch); + } + + /** + * On cancel/reset: smoothly fix yaw drift rather than snapping. + * Mirrors LiquidBounce's withFixedYaw: rotation.yaw + angleDiff(player.yRot, rotation.yaw) + */ + private void applyRotationWithFixedYaw(float yaw, float pitch) { + if (mc.player == null) return; + float fixedYaw = yaw + angleDiff(mc.player.getYaw(), yaw); + applyRotation(fixedYaw, pitch); + } + + private float computeAccel(float remaining, float prevDelta, float geometricAngle, boolean isYaw) { + float accelMin = isYaw ? config.yawAccelMin : config.pitchAccelMin; + float accelMax = isYaw ? config.yawAccelMax : config.pitchAccelMax; + float range = randomRange(accelMin, accelMax); + float decFactor = sigmoidFactor(geometricAngle); + float accel = MathHelper.wrapDegrees(remaining - prevDelta); + accel = MathHelper.clamp(accel, -range, range) * decFactor; + return prevDelta + accel; + } + + private float sigmoidFactor(float geometricAngle) { + float scaled = geometricAngle / 120f; + double sigmoid = 1.0 / (1.0 + Math.exp(-config.sigmoidSteepness * (scaled - config.sigmoidMidpoint))); + return (float) MathHelper.clamp(sigmoid, 0.0, 1.0); + } + + private float errorForAxis(float delta, float accelErr, float constErr) { + ThreadLocalRandom rng = ThreadLocalRandom.current(); + return delta * (float)(rng.nextDouble() * 2.0 - 1.0) * accelErr + + (float)(rng.nextDouble() * 2.0 - 1.0) * constErr; + } + + private float randomRange(float min, float max) { + if (min >= max) return min; + return min + ThreadLocalRandom.current().nextFloat() * (max - min); + } + + private float snapToGcd(float delta, double gcd) { + if (gcd <= 0.0) return delta; + return (float)(Math.round(delta / gcd) * gcd); + } + + private double computeGcd() { + if (mc.options == null) return 0.0; + double s = mc.options.getMouseSensitivity().getValue(); + double f = s * 0.6 + 0.2; + return f * f * f * 8.0 * 0.15; + } + + private float angleDiff(float from, float to) { + return MathHelper.wrapDegrees(to - from); + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 25390f6a..5402dc85 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -4,9 +4,7 @@ "version": "${version}", "name": "Glazed", "description": "An Addon made for donutsmp", - "authors": [ - "nnpg" - ], + "authors": ["nnpg"], "contact": { "homepage": "https://glazedclient.com", "repo": "https://github.com/realnnpg/", @@ -16,16 +14,10 @@ "icon": "assets/template/icon.png", "environment": "client", "entrypoints": { - "meteor": [ - "com.nnpg.glazed.GlazedAddon" - ], - "mixinsquared": [ - "com.nnpg.glazed.mixin.MeteorMixinCanceller" - ] + "meteor": ["com.nnpg.glazed.GlazedAddon"], + "mixinsquared": ["com.nnpg.glazed.mixin.MeteorMixinCanceller"] }, - "mixins": [ - "mixins.json" - ], + "mixins": ["glazed-mixins.json", "glazed-mixin.json"], "custom": { "meteor-client:color": "225,25,25", "modmenu": { diff --git a/src/main/resources/glazed-mixin.json b/src/main/resources/glazed-mixin.json new file mode 100644 index 00000000..f19760d7 --- /dev/null +++ b/src/main/resources/glazed-mixin.json @@ -0,0 +1,22 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.nnpg.glazed.mixin", + "compatibilityLevel": "JAVA_21", + "mixins": [], + "client": [ + "protection.TranslatableTextContentMixin", + "protection.KeybindTextContentMixin", + "protection.TranslationStorageAccessor", + "protection.DecoderHandlerMixin", + "protection.ClientConnectionMixin", + "protection.PacketUtilsMixin", + "protection.SessionProtectionMixin", + "protection.TranslationStorageMixin", + "protection.ServerResourcePackLoaderMixin" + ], + "server": [], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/src/main/resources/mixins.json b/src/main/resources/glazed-mixins.json similarity index 51% rename from src/main/resources/mixins.json rename to src/main/resources/glazed-mixins.json index 9c4200d9..59647232 100644 --- a/src/main/resources/mixins.json +++ b/src/main/resources/glazed-mixins.json @@ -1,13 +1,15 @@ { "required": true, + "minVersion": "0.8", "package": "com.nnpg.glazed.mixins", - "compatibilityLevel": "JAVA_17", + "compatibilityLevel": "JAVA_21", "mixins": [], "client": [ "HandledScreenMixin", - "DefaultSettingsWidgetFactoryAccessor", - "DefaultSettingsWidgetFactoryMixin" + "DefaultSettingsWidgetFactoryMixin", + "DefaultSettingsWidgetFactoryAccessor" ], + "server": [], "injectors": { "defaultRequire": 1 }