diff --git a/.gitignore b/.gitignore index 09cd281f..1ed94c2b 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,7 @@ bin/ # fabric run/ +# temp files + +DEVNODE/ +gradlew \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index c592d63f..59c81a5e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -51,6 +51,14 @@ repositories { + sourceSets { + main { + java { + exclude("**/temp/**") + } + } + } + tasks { processResources { val propertyMap = mapOf( 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..88868888 100644 --- a/src/main/java/com/nnpg/glazed/GlazedAddon.java +++ b/src/main/java/com/nnpg/glazed/GlazedAddon.java @@ -101,8 +101,10 @@ public void onInitialize() { Modules.get().add(new PremiumTunnelBaseFinder()); Modules.get().add(new AdminList()); Modules.get().add(new AutoTreeFarmer()); + Modules.get().add(new CrystalTweaks()); + Modules.get().add(new CrystalDeathLock()); } - + @EventHandler private void onGameJoined(GameJoinedEvent event) { MyScreen.checkVersionOnServerJoin(); diff --git a/src/main/java/com/nnpg/glazed/mixins/CrystalTweaksMixin.java b/src/main/java/com/nnpg/glazed/mixins/CrystalTweaksMixin.java new file mode 100644 index 00000000..572a47f7 --- /dev/null +++ b/src/main/java/com/nnpg/glazed/mixins/CrystalTweaksMixin.java @@ -0,0 +1,56 @@ +package com.nnpg.glazed.mixins; + +import com.nnpg.glazed.modules.pvp.CrystalTweaks; +import meteordevelopment.meteorclient.systems.modules.Modules; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.client.network.ClientPlayerInteractionManager; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.util.ActionResult; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; +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 org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(ClientPlayerInteractionManager.class) +public class CrystalTweaksMixin { + + @Inject( + method = "clickSlot", + at = @At("HEAD"), + cancellable = true + ) + private void glazed$onClickSlot( + int syncId, + int slot, + int button, + SlotActionType actionType, + PlayerEntity player, + CallbackInfo ci + ) { + CrystalTweaks module = Modules.get().get(CrystalTweaks.class); + if (module != null && module.isActive() && module.shouldBlockSlotClick(syncId, slot, button, actionType)) { + ci.cancel(); + } + } + + @Inject( + method = "interactBlock", + at = @At("HEAD"), + cancellable = true + ) + private void glazed$onInteractBlock( + ClientPlayerEntity player, + Hand hand, + BlockHitResult hitResult, + CallbackInfoReturnable cir + ) { + CrystalTweaks module = Modules.get().get(CrystalTweaks.class); + if (module != null && module.isActive() && module.shouldBlockInteractBlock(player, hand, hitResult)) { + cir.setReturnValue(ActionResult.FAIL); + } + } +} diff --git a/src/main/java/com/nnpg/glazed/modules/pvp/CrystalDeathLock.java b/src/main/java/com/nnpg/glazed/modules/pvp/CrystalDeathLock.java new file mode 100644 index 00000000..6be0a9d7 --- /dev/null +++ b/src/main/java/com/nnpg/glazed/modules/pvp/CrystalDeathLock.java @@ -0,0 +1,149 @@ +package com.nnpg.glazed.modules.pvp; + +import com.nnpg.glazed.GlazedAddon; +import meteordevelopment.meteorclient.events.entity.player.AttackEntityEvent; +import meteordevelopment.meteorclient.events.packets.PacketEvent; +import meteordevelopment.meteorclient.settings.*; +import meteordevelopment.meteorclient.systems.modules.Module; +import meteordevelopment.meteorclient.utils.player.ChatUtils; +import meteordevelopment.orbit.EventHandler; +import meteordevelopment.orbit.EventPriority; +import net.minecraft.block.Blocks; +import net.minecraft.entity.decoration.EndCrystalEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.Entity; +import net.minecraft.item.Items; +import net.minecraft.network.packet.c2s.play.PlayerInteractBlockC2SPacket; +import net.minecraft.network.packet.s2c.play.EntityStatusS2CPacket; +import net.minecraft.util.math.BlockPos; + +public class CrystalDeathLock extends Module { + + private final SettingGroup sgGeneral = settings.getDefaultGroup(); + + private final Setting detectionRange = sgGeneral.add(new DoubleSetting.Builder() + .name("detection-range") + .description("Radius around your position in which a player death triggers the lock.") + .defaultValue(15.0) + .min(1.0) + .sliderMax(30.0) + .build() + ); + + private final Setting lockDurationMs = sgGeneral.add(new IntSetting.Builder() + .name("lock-duration-ms") + .description("How long (milliseconds) inputs are blocked after the death. Default: 1000 = 1 second.") + .defaultValue(1000) + .min(100) + .sliderRange(100, 5000) + .build() + ); + + private final Setting blockCrystalPlace = sgGeneral.add(new BoolSetting.Builder() + .name("block-crystal-place") + .description("Prevent placing end crystals during the lock window.") + .defaultValue(true) + .build() + ); + + private final Setting blockCrystalAttack = sgGeneral.add(new BoolSetting.Builder() + .name("block-crystal-attack") + .description("Prevent left-clicking (attacking) end crystals during the lock window.") + .defaultValue(true) + .build() + ); + + private final Setting blockAnchorInteract = sgGeneral.add(new BoolSetting.Builder() + .name("block-anchor-interact") + .description("Prevent right-clicking respawn anchors during the lock window.") + .defaultValue(true) + .build() + ); + + private final Setting notifications = sgGeneral.add(new BoolSetting.Builder() + .name("notifications") + .description("Show a chat message when the lock activates and expires.") + .defaultValue(true) + .build() + ); + + private volatile long lockUntilNano = 0L; + + public CrystalDeathLock() { + super(GlazedAddon.pvp, "crystal-death-lock", + "Blocks end crystal and respawn anchor inputs for a configurable window after a nearby player dies."); + } + + @Override + public void onDeactivate() { + lockUntilNano = 0L; + } + + @EventHandler(priority = EventPriority.HIGHEST) + private void onPacketReceive(PacketEvent.Receive event) { + if (!(event.packet instanceof EntityStatusS2CPacket statusPacket)) return; + if (statusPacket.getStatus() != 3) return; + + if (mc.player == null || mc.world == null) return; + Entity entity = statusPacket.getEntity(mc.world); + if (!(entity instanceof PlayerEntity dead)) return; + if (dead == mc.player) return; + double dist = mc.player.getPos().distanceTo(dead.getPos()); + if (dist > detectionRange.get()) return; + lockUntilNano = System.nanoTime() + (lockDurationMs.get() * 1_000_000L); + + if (notifications.get()) { + String playerName = dead.getName().getString(); + mc.execute(() -> ChatUtils.info( + String.format("[CrystalDeathLock] §cLocked §r— %s died (%.1f blocks away). " + + "Blocking for §e%dms§r.", + playerName, dist, lockDurationMs.get()))); + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + private void onPacketSend(PacketEvent.Send event) { + if (!isLocked()) return; + if (mc.player == null || mc.world == null) return; + if (!(event.packet instanceof PlayerInteractBlockC2SPacket packet)) return; + + BlockPos pos = packet.getBlockHitResult().getBlockPos(); + var block = mc.world.getBlockState(pos).getBlock(); + if (blockCrystalPlace.get()) { + boolean holdsCrystal = + mc.player.getMainHandStack().getItem() == Items.END_CRYSTAL + || mc.player.getOffHandStack().getItem() == Items.END_CRYSTAL; + + if (holdsCrystal) { + event.cancel(); + return; + } + } + if (blockAnchorInteract.get() && block == Blocks.RESPAWN_ANCHOR) { + event.cancel(); + } + } + + @EventHandler(priority = EventPriority.HIGHEST) + private void onAttackEntity(AttackEntityEvent event) { + if (!blockCrystalAttack.get()) return; + if (!isLocked()) return; + if (event.entity instanceof EndCrystalEntity) { + event.cancel(); + } + } + + private boolean isLocked() { + long now = System.nanoTime(); + if (lockUntilNano == 0L) return false; + + if (now < lockUntilNano) return true; + if (lockUntilNano != 0L) { + lockUntilNano = 0L; + if (notifications.get()) { + ChatUtils.info("[CrystalDeathLock] §aUnlocked §r— inputs restored."); + } + } + return false; + } +} diff --git a/src/main/java/com/nnpg/glazed/modules/pvp/CrystalTweaks.java b/src/main/java/com/nnpg/glazed/modules/pvp/CrystalTweaks.java new file mode 100644 index 00000000..c269e26b --- /dev/null +++ b/src/main/java/com/nnpg/glazed/modules/pvp/CrystalTweaks.java @@ -0,0 +1,321 @@ +package com.nnpg.glazed.modules.pvp; + +import com.nnpg.glazed.GlazedAddon; +import meteordevelopment.meteorclient.events.meteor.KeyEvent; +import meteordevelopment.meteorclient.events.packets.PacketEvent; +import meteordevelopment.meteorclient.events.world.TickEvent; +import meteordevelopment.meteorclient.settings.*; +import meteordevelopment.meteorclient.systems.modules.Module; +import meteordevelopment.meteorclient.utils.misc.input.KeyAction; +import meteordevelopment.orbit.EventHandler; +import net.minecraft.block.Blocks; +import net.minecraft.block.RespawnAnchorBlock; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.item.Items; +import net.minecraft.network.packet.s2c.play.EntityStatusS2CPacket; +import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.BlockHitResult; + +import java.util.HashSet; +import java.util.Set; + +public class CrystalTweaks extends Module { + + private final SettingGroup sgTotemProtect = settings.createGroup("Totem Slot Protection -- prevent accidental totem removal from offhand / backup slot"); + private final SettingGroup sgAntiDrop = settings.createGroup("Anti Drop -- block Q outside of any open inventory screen"); + private final SettingGroup sgAntiInterrupt = settings.createGroup("Anti Interrupt -- require double-tap of T to open chat"); + private final SettingGroup sgCursorGuard = settings.createGroup("Cursor Guard -- disable left/right-click item pickup in inventory"); + private final SettingGroup sgHotbarLock = settings.createGroup("Hotbar Lock -- freeze hotbar slot config, whitelist exceptions"); + private final SettingGroup sgGlowstone = settings.createGroup("Glowstone Block -- only allow right-clicking glowstone into anchors"); + private final SettingGroup sgAnchorFill = settings.createGroup("Anchor Max Fill -- limit anchor charging to 1 glowstone level"); + + private final Setting totemProtectEnabled = sgTotemProtect.add(new BoolSetting.Builder() + .name("enabled") + .description("Prevent totems from being accidentally removed from the offhand or your backup hotbar slot. " + + "Also locks those slots briefly after a pop to prevent mis-clicks during restock.") + .defaultValue(false) + .build()); + + private final Setting alwaysProtectOffhandTotem = sgTotemProtect.add(new BoolSetting.Builder() + .name("always-protect-offhand-totem") + .description("Always prevent removing a totem from the offhand slot.") + .defaultValue(false) + .visible(totemProtectEnabled::get) + .build()); + + private final Setting alwaysProtectHotbarTotem = sgTotemProtect.add(new BoolSetting.Builder() + .name("always-protect-hotbar-totem") + .description("Always prevent removing a totem from the configured hotbar slot.") + .defaultValue(false) + .visible(totemProtectEnabled::get) + .build()); + + private final Setting totemBackupSlot = sgTotemProtect.add(new IntSetting.Builder() + .name("backup-hotbar-slot") + .description("Hotbar slot (1-9) you keep a backup totem in. " + + "Swapping a totem OUT of this slot is always blocked. " + + "After a pop this slot is also temporarily locked against direct clicks. " + + "Set to 0 to only protect the offhand.") + .defaultValue(9) + .min(0).max(9) + .visible(totemProtectEnabled::get) + .build()); + + private final Setting restrictOffhandToTotems = sgTotemProtect.add(new BoolSetting.Builder() + .name("restrict-offhand-to-totems") + .description("Only allow Totems of Undying to be moved into the offhand slot.") + .defaultValue(true) + .visible(totemProtectEnabled::get) + .build()); + + private final Setting restrictBackupSlotToTotems = sgTotemProtect.add(new BoolSetting.Builder() + .name("restrict-backup-slot-to-totems") + .description("Only allow Totems of Undying to be moved into the configured backup hotbar slot.") + .defaultValue(true) + .visible(totemProtectEnabled::get) + .build()); + + private final Setting popLockTicks = sgTotemProtect.add(new IntSetting.Builder() + .name("pop-lock-ticks") + .description("How many ticks to lock the offhand + backup slot after a pop (20 ticks ≈ 1000 ms).") + .defaultValue(20) + .min(1).max(40).sliderMax(40) + .visible(totemProtectEnabled::get) + .build()); + + private final Setting antiDropEnabled = sgAntiDrop.add(new BoolSetting.Builder() + .name("enabled") + .description("Block Q (drop) when no inventory or container screen is open. Q works normally inside a screen.") + .defaultValue(false) + .build()); + + private final Setting antiInterruptEnabled = sgAntiInterrupt.add(new BoolSetting.Builder() + .name("enabled") + .description("Require a double-tap of T (chat) within the configured window. A single accidental press is silently swallowed.") + .defaultValue(false) + .build()); + + private final Setting doubleTapWindowMs = sgAntiInterrupt.add(new IntSetting.Builder() + .name("double-tap-window-ms") + .description("Time window (ms) in which a second T press counts as a double-tap.") + .defaultValue(300) + .min(100).max(700).sliderMax(600) + .visible(antiInterruptEnabled::get) + .build()); + + private final Setting cursorGuardEnabled = sgCursorGuard.add(new BoolSetting.Builder() + .name("enabled") + .description("Block PICKUP (left/right-click drag) and QUICK_CRAFT (multi-slot drag) in any open inventory. " + + "Items can only be moved via hotkey-binds (number keys) or F. " + + "Shift-click (QUICK_MOVE) still works.") + .defaultValue(false) + .build()); + + private final Setting hotbarLockEnabled = sgHotbarLock.add(new BoolSetting.Builder() + .name("enabled") + .description("Lock all hotbar slots against changes. Whitelist specific slots that are allowed to update freely.") + .defaultValue(false) + .build()); + + private final Setting hotbarWhitelistSlots = sgHotbarLock.add(new StringSetting.Builder() + .name("whitelist-slots") + .description("Comma-separated hotbar slots (1-9) that are free to change. " + + "Example: '1,2' allows slots 1 and 2. Leave empty to lock all slots.") + .defaultValue("") + .visible(hotbarLockEnabled::get) + .build()); + + private final Setting allowManualHotbarMove = sgHotbarLock.add(new BoolSetting.Builder() + .name("allow-manual-move") + .description("Allow manually picking up and moving items in the hotbar while locked. Only hotkey-swapping is blocked.") + .defaultValue(true) + .visible(hotbarLockEnabled::get) + .build()); + + private final Setting glowstoneBlockEnabled = sgGlowstone.add(new BoolSetting.Builder() + .name("enabled") + .description("While holding Glowstone, cancel right-click on any block that is NOT a Respawn Anchor.") + .defaultValue(false) + .build()); + + private final Setting anchorFillEnabled = sgAnchorFill.add(new BoolSetting.Builder() + .name("enabled") + .description("While holding Glowstone, block charging a Respawn Anchor that already has 1+ charges. " + + "One charge is all you need before exploding.") + .defaultValue(false) + .build()); + + private int popLockTimer = 0; + + private long lastChatKeyPressMs = 0L; + + public CrystalTweaks() { + super(GlazedAddon.pvp, "crystal-tweaks", "Crystal PvP inventory & combat safety toolkit. Prevents accidental drops, misclicks, interrupted inputs and inventory mistakes during fights."); + } + + @Override + public void onActivate() { + popLockTimer = 0; + lastChatKeyPressMs = 0L; + } + + @EventHandler + private void onTick(TickEvent.Pre event) { + if (popLockTimer > 0) popLockTimer--; + } + + @EventHandler + private void onPacketReceive(PacketEvent.Receive event) { + if (!totemProtectEnabled.get()) return; + if (!(event.packet instanceof EntityStatusS2CPacket packet)) return; + if (mc.player == null || mc.world == null) return; + + if (packet.getStatus() == 35 && packet.getEntity(mc.world) == mc.player) { + popLockTimer = popLockTicks.get(); + } + } + + @EventHandler + private void onKey(KeyEvent event) { + if (event.action != KeyAction.Press || mc.player == null) return; + if (antiDropEnabled.get() && mc.currentScreen == null) { + if (mc.options.dropKey.matchesKey(event.key, 0)) { + event.cancel(); + return; + } + } + if (antiInterruptEnabled.get() && mc.currentScreen == null) { + if (mc.options.chatKey.matchesKey(event.key, 0)) { + long now = System.currentTimeMillis(); + if (lastChatKeyPressMs != 0L && (now - lastChatKeyPressMs) <= doubleTapWindowMs.get()) { + lastChatKeyPressMs = 0L; + } else { + lastChatKeyPressMs = now; + event.cancel(); + } + } + } + } + + public boolean shouldBlockSlotClick(int syncId, int slot, int button, SlotActionType actionType) { + if (mc.player == null) return false; + + // --- Totem Protect Logic --- + if (totemProtectEnabled.get()) { + int backupSlot0 = totemBackupSlot.get() - 1; + + // 1. Prevent REMOVING a totem + if (alwaysProtectOffhandTotem.get() && (slot == 45 || (actionType == SlotActionType.SWAP && button == 40))) { + if (mc.player.getOffHandStack().isOf(Items.TOTEM_OF_UNDYING)) return true; + } + if (alwaysProtectHotbarTotem.get() && backupSlot0 >= 0 && (slot == 36 + backupSlot0 || (actionType == SlotActionType.SWAP && button == backupSlot0))) { + if (mc.player.getInventory().getStack(backupSlot0).isOf(Items.TOTEM_OF_UNDYING)) return true; + } + + // 2. Pop Lock (locked against clicks) + if (popLockTimer > 0 && syncId == 0) { + if (slot == 45) return true; + if (backupSlot0 >= 0 && slot == 36 + backupSlot0) return true; + } + + // 3. Prevent INSERTING non-totems + if (restrictOffhandToTotems.get()) { + if (slot == 45 || (actionType == SlotActionType.SWAP && button == 40)) { + if (!isIncomingItemTotem(slot, button, actionType)) return true; + } + } + if (restrictBackupSlotToTotems.get() && backupSlot0 >= 0) { + if (slot == 36 + backupSlot0 || (actionType == SlotActionType.SWAP && button == backupSlot0)) { + if (!isIncomingItemTotem(slot, button, actionType)) return true; + } + } + } + + // --- Cursor Guard --- + if (cursorGuardEnabled.get() && mc.currentScreen != null) { + if (actionType == SlotActionType.PICKUP || actionType == SlotActionType.QUICK_CRAFT) { + return true; + } + } + + // --- Hotbar Lock --- + if (hotbarLockEnabled.get()) { + int hotbarSlot = -1; + if (slot >= 0 && slot < mc.player.currentScreenHandler.slots.size()) { + var s = mc.player.currentScreenHandler.getSlot(slot); + if (s.inventory == mc.player.getInventory() && s.getIndex() >= 0 && s.getIndex() <= 8) { + hotbarSlot = s.getIndex(); + } + } + + Set whitelist = parseWhitelistSlots(); + + // Check hotkey target (SWAP) + if (actionType == SlotActionType.SWAP && button >= 0 && button <= 8) { + if (!whitelist.contains(button)) return true; + } + + // Check hovered slot + if (hotbarSlot >= 0 && !whitelist.contains(hotbarSlot)) { + // Block hotkeys and shift-clicks. Allow manual mouse actions if enabled. + // Added QUICK_CRAFT to support dragging/fast clicks. + if (allowManualHotbarMove.get() && (actionType == SlotActionType.PICKUP || actionType == SlotActionType.PICKUP_ALL || actionType == SlotActionType.QUICK_CRAFT)) { + // Allow manual move + } else { + return true; + } + } + } + + return false; + } + + private boolean isIncomingItemTotem(int slot, int button, SlotActionType actionType) { + if (mc.player == null || mc.player.currentScreenHandler == null) return false; + + if (actionType == SlotActionType.SWAP || actionType == SlotActionType.QUICK_MOVE) { + if (slot >= 0 && slot < mc.player.currentScreenHandler.slots.size()) { + var stack = mc.player.currentScreenHandler.getSlot(slot).getStack(); + return stack.isEmpty() || stack.isOf(Items.TOTEM_OF_UNDYING); + } + } else if (actionType == SlotActionType.PICKUP || actionType == SlotActionType.PICKUP_ALL) { + var stack = mc.player.currentScreenHandler.getCursorStack(); + return stack.isEmpty() || stack.isOf(Items.TOTEM_OF_UNDYING); + } + + return true; + } + + public boolean shouldBlockInteractBlock(ClientPlayerEntity player, Hand hand, BlockHitResult hitResult) { + if (mc.world == null) return false; + if (hand != Hand.MAIN_HAND) return false; + if (!player.getMainHandStack().isOf(Items.GLOWSTONE)) return false; + + var pos = hitResult.getBlockPos(); + var state = mc.world.getBlockState(pos); + if (glowstoneBlockEnabled.get() && !state.isOf(Blocks.RESPAWN_ANCHOR)) { + return true; + } + if (anchorFillEnabled.get() && state.isOf(Blocks.RESPAWN_ANCHOR)) { + if (state.get(RespawnAnchorBlock.CHARGES) >= 1) { + return true; + } + } + + return false; + } + + private Set parseWhitelistSlots() { + Set result = new HashSet<>(); + String raw = hotbarWhitelistSlots.get().trim(); + if (raw.isEmpty()) return result; + for (String part : raw.split(",")) { + try { + int slot = Integer.parseInt(part.trim()); + if (slot >= 1 && slot <= 9) result.add(slot - 1); + } catch (NumberFormatException ignored) {} + } + return result; + } +} \ No newline at end of file diff --git a/src/main/resources/mixins.json b/src/main/resources/mixins.json index 9c4200d9..673bb4f4 100644 --- a/src/main/resources/mixins.json +++ b/src/main/resources/mixins.json @@ -6,7 +6,8 @@ "client": [ "HandledScreenMixin", "DefaultSettingsWidgetFactoryAccessor", - "DefaultSettingsWidgetFactoryMixin" + "DefaultSettingsWidgetFactoryMixin", + "CrystalTweaksMixin" ], "injectors": { "defaultRequire": 1