From cb95c1cbb37ff69fbef08ac077ee25f035f9db7e Mon Sep 17 00:00:00 2001 From: HelixCraft Date: Fri, 13 Mar 2026 20:49:29 +0100 Subject: [PATCH 1/5] Add Triggerbot and S-Tab Sprint Reset modules --- .gitignore | 3 + build.gradle.kts | 8 + gradlew | 0 .../java/com/nnpg/glazed/GlazedAddon.java | 6 + .../glazed/modules/pvp/STabSprintReset.java | 332 ++++++++++++++++++ .../nnpg/glazed/modules/pvp/TriggerBot.java | 323 +++++++++++++++++ .../glazed/utils/glazed/MovementKeys.java | 138 ++++++++ 7 files changed, 810 insertions(+) mode change 100644 => 100755 gradlew create mode 100644 src/main/java/com/nnpg/glazed/modules/pvp/STabSprintReset.java create mode 100644 src/main/java/com/nnpg/glazed/modules/pvp/TriggerBot.java create mode 100644 src/main/java/com/nnpg/glazed/utils/glazed/MovementKeys.java diff --git a/.gitignore b/.gitignore index 09cd281f..16c658f6 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ bin/ # fabric run/ +# temp files + +temp/ 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..5229837c 100644 --- a/src/main/java/com/nnpg/glazed/GlazedAddon.java +++ b/src/main/java/com/nnpg/glazed/GlazedAddon.java @@ -101,6 +101,12 @@ public void onInitialize() { Modules.get().add(new PremiumTunnelBaseFinder()); Modules.get().add(new AdminList()); Modules.get().add(new AutoTreeFarmer()); + // Modules.get().add(new MovementTest()); + Modules.get().add(new STabSprintReset()); + // Modules.get().add(new JumpReset()); + Modules.get().add(new TriggerBot()); + // Modules.get().add(new AttributeSwapper()); + } @EventHandler diff --git a/src/main/java/com/nnpg/glazed/modules/pvp/STabSprintReset.java b/src/main/java/com/nnpg/glazed/modules/pvp/STabSprintReset.java new file mode 100644 index 00000000..19ef3a31 --- /dev/null +++ b/src/main/java/com/nnpg/glazed/modules/pvp/STabSprintReset.java @@ -0,0 +1,332 @@ +package com.nnpg.glazed.modules.pvp; + +import com.nnpg.glazed.utils.glazed.MovementKeys; +import meteordevelopment.meteorclient.events.entity.player.AttackEntityEvent; +import meteordevelopment.meteorclient.events.world.TickEvent; +import meteordevelopment.meteorclient.settings.BoolSetting; +import meteordevelopment.meteorclient.settings.DoubleSetting; +import meteordevelopment.meteorclient.settings.IntSetting; +import meteordevelopment.meteorclient.settings.Setting; +import meteordevelopment.meteorclient.settings.SettingGroup; +import meteordevelopment.meteorclient.systems.modules.Module; +import meteordevelopment.orbit.EventHandler; +import meteordevelopment.orbit.EventPriority; +import net.minecraft.entity.LivingEntity; + +import static com.nnpg.glazed.GlazedAddon.pvp; + +/** + * S-Tab Sprint Reset + * + * Mechanism: + * Attack stops sprint IMMEDIATELY in the same tick. + * Pressing S prevents sprint from restarting (W+S = net ~0 → no sprint). + * After releasing S, sprint restarts immediately if the sprint key is held. + * + * Timing (verified against Minecraft source / mcpk.wiki): + * Pre-Delay : 1–3 Ticks (50–150ms) — weighted toward lower values + * S-Hold : 1–3 Ticks (50–150ms) — weighted toward center + * Sub-Tick : 0–20ms jitter on release — breaks tick-boundary fingerprint + * + * Design goals for undetectability: + * - Pre-delay is never 0ms (no human reacts in the same tick as their click) + * - Weighted non-uniform distributions instead of flat random ranges + * - Sub-tick jitter on release so key-up never aligns exactly to a tick edge + * - Rate-limit guards against superhuman reset frequency at high CPS + * - Skip chance tuned to reflect a skilled-but-human success rate (~75-80%) + */ +public class STabSprintReset extends Module { + + private final SettingGroup sgGeneral = settings.getDefaultGroup(); + private final SettingGroup sgTiming = settings.createGroup("Timing"); + + // ── General Settings ────────────────────────────────────────────────────── + + private final Setting skipChance = sgGeneral.add(new DoubleSetting.Builder() + .name("skip-chance") + .description("Chance to skip the reset entirely (mimics human inconsistency)") + .defaultValue(20.0) // 20% skip → ~80% success rate, realistic for a skilled player + .min(0.0) + .max(100.0) + .sliderMax(100.0) + .build() + ); + + private final Setting advancedSettings = sgGeneral.add(new BoolSetting.Builder() + .name("advanced-settings") + .description("Show advanced timing settings") + .defaultValue(false) + .build() + ); + + // ── Timing Settings ─────────────────────────────────────────────────────── + + // Pre-delay: how many ticks to wait after the attack before pressing S. + // Default weighted distribution: 1T=40%, 2T=40%, 3T=20% (see rollPreDelay()). + // User-exposed min/max shifts the weight anchor, not a flat range. + private final Setting preDelayMin = sgTiming.add(new IntSetting.Builder() + .name("pre-delay-min") + .description("Minimum ticks before pressing S (1 = earliest human-possible)") + .defaultValue(1) + .min(1) // Never 0 — 0ms reaction is inhuman and directly detectable + .max(5) + .sliderMax(5) + .visible(() -> advancedSettings.get()) + .build() + ); + + private final Setting preDelayMax = sgTiming.add(new IntSetting.Builder() + .name("pre-delay-max") + .description("Maximum ticks before pressing S") + .defaultValue(3) + .min(1) + .max(8) + .sliderMax(8) + .visible(() -> advancedSettings.get()) + .build() + ); + + private final Setting sHoldMin = sgTiming.add(new IntSetting.Builder() + .name("s-hold-min") + .description("Minimum ticks to hold S") + .defaultValue(1) + .min(1) + .max(5) + .sliderMax(5) + .visible(() -> advancedSettings.get()) + .build() + ); + + private final Setting sHoldMax = sgTiming.add(new IntSetting.Builder() + .name("s-hold-max") + .description("Maximum ticks to hold S") + .defaultValue(3) + .min(1) + .max(8) + .sliderMax(8) + .visible(() -> advancedSettings.get()) + .build() + ); + + // ── State ───────────────────────────────────────────────────────────────── + + private boolean sKeyPressed = false; + private int preDelayTicks = 0; + private int sHoldTicks = 0; + private int currentPreDelay = 0; + private int currentSHold = 0; + private boolean waitingForPreDelay = false; + private boolean waitingForSRelease = false; + + // Sub-tick release jitter: once the hold tick count expires we don't + // release S immediately. Instead we set a real-time target (ms) and + // release when wall-clock time passes it. This means the key-up event + // is no longer aligned to a tick boundary — breaking the tick-edge + // fingerprint that behavioral anticheats look for. + private long releaseAtMs = -1L; + + // Rate-limiting: track the real-time of the last completed reset. + // A human doing S-Tab consistently faster than ~150ms is unrealistic. + private long lastResetTimeMs = 0L; + private static final int MIN_RESET_INTERVAL_MS = 150; + + // FIX 1 – Sprint-state cache. + // isSprinting() in onAttack() can already be false (sprint stops same + // tick as the attack). We cache it at tick-start, before attacks fire. + // isOnGround() is intentionally excluded: strafing (A/D) can briefly + // flip isSprinting() to false; we still want to catch those hits. + private boolean wasSprinting = false; + + // ── Constructor ─────────────────────────────────────────────────────────── + + public STabSprintReset() { + super(pvp, "s-tab-sprint-reset", "Prevents sprint restart after attack with S-tap"); + } + + // ── Lifecycle ───────────────────────────────────────────────────────────── + + @Override + public void onDeactivate() { + if (sKeyPressed) { + MovementKeys.back(false); + sKeyPressed = false; + } + resetState(); + } + + // ── Event Handlers ──────────────────────────────────────────────────────── + + @EventHandler(priority = EventPriority.HIGH) + private void onTick(TickEvent.Pre event) { + // Guard: dead player / world unload. + // onDeactivate() is not guaranteed to fire on death in Meteor Client. + if (mc.player == null || mc.player.isDead() || mc.world == null) { + if (sKeyPressed) { + MovementKeys.back(false); + sKeyPressed = false; + } + resetState(); + return; + } + + // FIX 1: Update sprint cache at tick-start, before AttackEvent fires. + wasSprinting = mc.player.isSprinting(); + + // FIX 3: Player left the ground during S-hold → release immediately. + // S pressed while airborne still blocks sprint technically, but + // produces a visible backward nudge that looks unnatural. + // Pre-delay phase does NOT abort on airborne – S pressed in the air + // after a ground hit still performs the sprint break correctly. + if (waitingForSRelease && !mc.player.isOnGround()) { + releaseS(); + return; + } + + // ── Pre-Delay Phase ─────────────────────────────────────────────────── + if (waitingForPreDelay) { + preDelayTicks++; + if (preDelayTicks >= currentPreDelay) { + pressS(); + } + return; + } + + // ── S-Hold Phase ────────────────────────────────────────────────────── + if (waitingForSRelease) { + sHoldTicks++; + + // Once the hold-tick threshold is reached, schedule a sub-tick + // release rather than releasing instantly on the tick boundary. + if (sHoldTicks >= currentSHold && releaseAtMs < 0) { + int jitterMs = (int)(Math.random() * 20); // 0–20ms sub-tick jitter + releaseAtMs = System.currentTimeMillis() + jitterMs; + } + + // Release only when real-time wall clock has passed the target. + if (releaseAtMs >= 0 && System.currentTimeMillis() >= releaseAtMs) { + releaseAtMs = -1L; + releaseS(); + } + return; + } + + // Safety-net: S is physically held but state machine is idle. + // Caused by: two attacks in the exact same tick, or external corruption. + if (sKeyPressed) { + releaseS(); + } + } + + @EventHandler(priority = EventPriority.HIGH) + private void onAttack(AttackEntityEvent event) { + if (!(event.entity instanceof LivingEntity)) return; + + // Ground check stays here, separate from the sprint cache. + if (!mc.player.isOnGround()) return; + + // FIX 1: Use cached sprint state. + if (!wasSprinting) return; + + // Rate-limit: prevent superhuman reset frequency at high CPS. + // No human consistently S-tabs faster than once every 150ms. + long now = System.currentTimeMillis(); + if (now - lastResetTimeMs < MIN_RESET_INTERVAL_MS) return; + + // FIX 4 (Spam-click safe): abort any running sequence cleanly. + if (sKeyPressed) releaseS(); + + // Skip chance. + if (Math.random() * 100 < skipChance.get()) return; + + // Roll timing using weighted distributions (see helpers below). + currentPreDelay = rollPreDelay(); + currentSHold = rollSHold(); + preDelayTicks = 0; + sHoldTicks = 0; + releaseAtMs = -1L; + waitingForPreDelay = true; + waitingForSRelease = false; + lastResetTimeMs = now; + + // Pre-delay of 1 tick means: press S on the NEXT tick, not this one. + // currentPreDelay is always >= 1 (enforced by rollPreDelay), + // so we never call pressS() here — we always go through the tick counter. + } + + // ── Helpers ─────────────────────────────────────────────────────────────── + + /** + * Weighted pre-delay distribution: + * 1 tick (50ms) → 40% — fast but plausible reaction + * 2 ticks (100ms)→ 40% — average human reaction + * 3 ticks (150ms)→ 20% — slightly slow / distracted hit + * + * Settings shift the anchor: if preDelayMin > 1, the distribution + * is clamped upward. If preDelayMax < 3, the top bucket collapses. + * Never returns 0 — a 0ms reaction is inhuman and directly detectable. + */ + private int rollPreDelay() { + int min = preDelayMin.get(); // always >= 1 + int max = preDelayMax.get(); + + double r = Math.random(); + int rolled; + if (r < 0.40) rolled = 1; + else if (r < 0.80) rolled = 2; + else rolled = 3; + + return Math.max(min, Math.min(max, rolled)); + } + + /** + * Weighted S-hold distribution: + * 1 tick (50ms) → 30% — quick tap + * 2 ticks (100ms)→ 50% — normal hold + * 3 ticks (150ms)→ 20% — slightly long hold + * + * Combined with the 0–20ms sub-tick release jitter, the actual + * hold duration is continuously distributed, not discretely bucketed. + */ + private int rollSHold() { + int min = sHoldMin.get(); + int max = sHoldMax.get(); + + double r = Math.random(); + int rolled; + if (r < 0.30) rolled = 1; + else if (r < 0.80) rolled = 2; + else rolled = 3; + + return Math.max(min, Math.min(max, rolled)); + } + + private void pressS() { + MovementKeys.back(true); + sKeyPressed = true; + + waitingForPreDelay = false; + waitingForSRelease = true; + sHoldTicks = 0; + releaseAtMs = -1L; + } + + private void releaseS() { + MovementKeys.back(false); + sKeyPressed = false; + resetState(); + } + + /** + * Resets all state fields. Never touches the S key directly — + * always call releaseS() first if sKeyPressed is true. + */ + private void resetState() { + waitingForPreDelay = false; + waitingForSRelease = false; + preDelayTicks = 0; + sHoldTicks = 0; + currentPreDelay = 0; + currentSHold = 0; + releaseAtMs = -1L; + } +} \ No newline at end of file diff --git a/src/main/java/com/nnpg/glazed/modules/pvp/TriggerBot.java b/src/main/java/com/nnpg/glazed/modules/pvp/TriggerBot.java new file mode 100644 index 00000000..d8a2833e --- /dev/null +++ b/src/main/java/com/nnpg/glazed/modules/pvp/TriggerBot.java @@ -0,0 +1,323 @@ +package com.nnpg.glazed.modules.pvp; + +import meteordevelopment.meteorclient.events.render.Render3DEvent; +import meteordevelopment.meteorclient.events.world.TickEvent; +import meteordevelopment.meteorclient.settings.*; +import meteordevelopment.meteorclient.systems.friends.Friends; +import meteordevelopment.meteorclient.systems.modules.Module; +import meteordevelopment.orbit.EventHandler; +import net.minecraft.entity.Entity; +import net.minecraft.entity.LivingEntity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.util.Hand; +import net.minecraft.util.hit.EntityHitResult; +import net.minecraft.util.hit.HitResult; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.Vec3d; +import com.nnpg.glazed.GlazedAddon; + +import java.util.Optional; + +public class TriggerBot extends Module { + + // ── Groups ──────────────────────────────────────────────────────────────── + + private final SettingGroup sgFilter = settings.createGroup("Filter"); + private final SettingGroup sgAttack = settings.createGroup("Attack"); + + // ── Filter ──────────────────────────────────────────────────────────────── + + private final Setting target = sgFilter.add(new EnumSetting.Builder() + .name("target") + .description("Which entities to attack.") + .defaultValue(Target.Players) + .build() + ); + + private final Setting range = sgFilter.add(new DoubleSetting.Builder() + .name("range") + .description("Maximum attack range. Warning: high range is easily detectable!") + .defaultValue(3.0) + .min(0.1) + .sliderMax(4.5) + .build() + ); + + private final Setting ignoreFriends = sgFilter.add(new BoolSetting.Builder() + .name("ignore-friends") + .description("Won't attack players on your friends list.") + .defaultValue(true) + .build() + ); + + private final Setting ignoreWalls = sgFilter.add(new BoolSetting.Builder() + .name("ignore-walls") + .description("Attack entities through walls.") + .defaultValue(false) + .build() + ); + + // ── Attack ──────────────────────────────────────────────────────────────── + + private final Setting hitWindowMs = sgAttack.add(new IntSetting.Builder() + .name("hit-window-ms") + .description("How long (ms) after the crosshair last touched a target the attack " + + "is still allowed. Catches fast flick-overs between ticks. " + + "50ms = 1 tick. 0 = disabled.") + .defaultValue(50) + .min(0) + .sliderRange(0, 150) + .build() + ); + + private final Setting onFallMode = sgAttack.add(new EnumSetting.Builder() + .name("on-fall-mode") + .description("Only attack while falling (for critical hits). " + + "None = always attack, Value = fixed velocity threshold, " + + "RandomValue = randomised threshold.") + .defaultValue(OnFallMode.None) + .build() + ); + + private final Setting onFallValue = sgAttack.add(new DoubleSetting.Builder() + .name("on-fall-velocity") + .description("Minimum downward velocity required to attack. " + + "0.1 = just past the jump peak (recommended), " + + "0.3 = deeper into the fall.") + .min(0.0) + .defaultValue(0.1) + .sliderRange(0.0, 1.0) + .visible(() -> onFallMode.get() == OnFallMode.Value) + .build() + ); + + private final Setting onFallMinRandomValue = sgAttack.add(new DoubleSetting.Builder() + .name("on-fall-min-random-velocity") + .description("Minimum of the randomised downward velocity threshold.") + .min(0.0) + .defaultValue(0.1) + .sliderRange(0.0, 1.0) + .visible(() -> onFallMode.get() == OnFallMode.RandomValue) + .build() + ); + + private final Setting onFallMaxRandomValue = sgAttack.add(new DoubleSetting.Builder() + .name("on-fall-max-random-velocity") + .description("Maximum of the randomised downward velocity threshold.") + .min(0.0) + .defaultValue(0.3) + .sliderRange(0.0, 1.0) + .visible(() -> onFallMode.get() == OnFallMode.RandomValue) + .build() + ); + + private final Setting hitSpeedMode = sgAttack.add(new EnumSetting.Builder() + .name("hit-speed-mode") + .description("Minimum attack cooldown required before attacking.") + .defaultValue(HitSpeedMode.RandomValue) + .build() + ); + + private final Setting hitSpeedValue = sgAttack.add(new DoubleSetting.Builder() + .name("hit-speed-value") + .description("Cooldown offset passed to getAttackCooldownProgress. 0 = full cooldown required.") + .defaultValue(0.0) + .sliderRange(-10, 10) + .visible(() -> hitSpeedMode.get() == HitSpeedMode.Value) + .build() + ); + + private final Setting hitSpeedMinRandomValue = sgAttack.add(new DoubleSetting.Builder() + .name("hit-speed-min-random-value") + .description("Minimum randomised cooldown offset value.") + .defaultValue(-0.1) + .sliderRange(-10, 10) + .visible(() -> hitSpeedMode.get() == HitSpeedMode.RandomValue) + .build() + ); + + private final Setting hitSpeedMaxRandomValue = sgAttack.add(new DoubleSetting.Builder() + .name("hit-speed-max-random-value") + .description("Maximum randomised cooldown offset value.") + .defaultValue(0.05) + .sliderRange(-10, 10) + .visible(() -> hitSpeedMode.get() == HitSpeedMode.RandomValue) + .build() + ); + + // ── State ───────────────────────────────────────────────────────────────── + + private float randomOnFallFloat = 0; + private float randomHitSpeedFloat = 0; + + // Sub-tick buffer: Render3DEvent fires every frame (~7ms at 144fps). + // Stores the last valid entity the crosshair touched so fast flick-overs + // between two ticks are not missed. Both Render3DEvent and TickEvent fire + // on the MC main thread – no synchronisation needed. + private Entity bufferedTarget = null; + private long lastSeenNano = 0L; + + // ── Constructor ─────────────────────────────────────────────────────────── + + public TriggerBot() { + super(GlazedAddon.pvp, "triggerbot", + "Attacks the entity you are looking at, optionally only when critting."); + } + + // ── Lifecycle ───────────────────────────────────────────────────────────── + + @Override + public void onActivate() { + randomOnFallFloat = 0; + randomHitSpeedFloat = 0; + bufferedTarget = null; + lastSeenNano = 0L; + } + + // ── Render frame: update sub-tick buffer ────────────────────────────────── + + // Runs every rendered frame. Only updates the buffer – no attack logic here. + @EventHandler + private void onRender(Render3DEvent event) { + if (mc.player == null || mc.world == null) return; + Entity found = getTarget(); + if (found != null) { + bufferedTarget = found; + lastSeenNano = System.nanoTime(); + } + } + + // ── Tick: attack ────────────────────────────────────────────────────────── + + @EventHandler + private void onTick(TickEvent.Pre event) { + if (mc.player == null || mc.player.isDead() + || mc.player.getHealth() <= 0 || mc.world == null) return; + + // ── Resolve target ──────────────────────────────────────────────────── + // Prefer live crosshair, fall back to render-frame buffer (hit window). + Entity entity = getTarget(); + + if (entity == null) { + long elapsedMs = (System.nanoTime() - lastSeenNano) / 1_000_000L; + if (bufferedTarget == null || elapsedMs > hitWindowMs.get()) return; + entity = bufferedTarget; + // Re-validate: entity may have moved/died since last render frame + if (!entity.isAlive()) { bufferedTarget = null; return; } + if (mc.player.squaredDistanceTo(entity) > range.get() * range.get()) { bufferedTarget = null; return; } + if (!entityCheck(entity)) { bufferedTarget = null; return; } + } + + // Clear buffer – attack fires this tick, fresh data needed next tick + bufferedTarget = null; + + // ── On-fall / crit check ────────────────────────────────────────────── + // velocity.y is negative while falling; threshold is stored as positive. + // Jitter of ±0.016 ≈ ±10ms so the exact trigger moment varies slightly. + OnFallMode currOnFallMode = onFallMode.get(); + if (currOnFallMode != OnFallMode.None) { + float threshold = (currOnFallMode == OnFallMode.Value) + ? onFallValue.get().floatValue() + : randomOnFallFloat; + + double vy = mc.player.getVelocity().y; + float jitter = (mc.world.random.nextFloat() * 0.032f) - 0.016f; + + if (vy >= -(threshold + jitter)) return; // not falling fast enough + if (mc.player.isOnGround()) return; // on ground = no crit + if (mc.player.isTouchingWater()) return; // vanilla crit blocker + if (mc.player.isClimbing()) return; // vanilla crit blocker + if (mc.player.hasVehicle()) return; // vanilla crit blocker + } + + // ── Hit-speed / cooldown check ──────────────────────────────────────── + HitSpeedMode currHitSpeedMode = hitSpeedMode.get(); + if (currHitSpeedMode != HitSpeedMode.None) { + float hitSpeed = (currHitSpeedMode == HitSpeedMode.Value) + ? hitSpeedValue.get().floatValue() + : randomHitSpeedFloat; + // Vanilla: (scale * 17) >= 16 → scale >= ~0.941 + if ((mc.player.getAttackCooldownProgress(hitSpeed) * 17.0F) < 16) return; + } + + // ── Attack ──────────────────────────────────────────────────────────── + mc.interactionManager.attackEntity(mc.player, entity); + mc.player.swingHand(Hand.MAIN_HAND); + + // ── Randomise next thresholds ───────────────────────────────────────── + if (currOnFallMode == OnFallMode.RandomValue) { + float min = Math.min(onFallMinRandomValue.get().floatValue(), onFallMaxRandomValue.get().floatValue()); + float max = Math.max(onFallMinRandomValue.get().floatValue(), onFallMaxRandomValue.get().floatValue()); + randomOnFallFloat = min + mc.world.random.nextFloat() * (max - min); + } + + if (currHitSpeedMode == HitSpeedMode.RandomValue) { + float min = Math.min(hitSpeedMinRandomValue.get().floatValue(), hitSpeedMaxRandomValue.get().floatValue()); + float max = Math.max(hitSpeedMinRandomValue.get().floatValue(), hitSpeedMaxRandomValue.get().floatValue()); + randomHitSpeedFloat = min + mc.world.random.nextFloat() * (max - min); + } + } + + // ── Target resolution ───────────────────────────────────────────────────── + + private Entity getTarget() { + if (ignoreWalls.get()) return getTargetThroughWalls(); + + if (mc.crosshairTarget == null + || mc.crosshairTarget.getType() != HitResult.Type.ENTITY) return null; + + Entity entity = ((EntityHitResult) mc.crosshairTarget).getEntity(); + if (mc.player.squaredDistanceTo(entity) > range.get() * range.get()) return null; + if (!entityCheck(entity)) return null; + return entity; + } + + private Entity getTargetThroughWalls() { + Vec3d eye = mc.player.getEyePos(); + Vec3d look = mc.player.getRotationVec(1.0F); + Vec3d end = eye.add(look.multiply(range.get())); + + Box searchBox = mc.player.getBoundingBox() + .stretch(look.multiply(range.get())) + .expand(1.0); + + Entity best = null; + double bestD = Double.MAX_VALUE; + + for (Entity candidate : mc.world.getEntitiesByClass( + LivingEntity.class, searchBox, this::entityCheck)) { + Optional hit = candidate.getBoundingBox().raycast(eye, end); + if (hit.isPresent()) { + double d = eye.squaredDistanceTo(hit.get()); + if (d < bestD) { bestD = d; best = candidate; } + } + } + return best; + } + + // ── Entity filter ───────────────────────────────────────────────────────── + + private boolean entityCheck(Entity entity) { + if (entity == mc.player || entity == mc.getCameraEntity()) return false; + if (!entity.isAlive()) return false; + if (entity instanceof LivingEntity le && (le.isDead() || le.getHealth() <= 0)) return false; + + switch (target.get()) { + case Players -> { if (!(entity instanceof PlayerEntity)) return false; } + case Entities -> { if ( entity instanceof PlayerEntity) return false; } + case All -> {} + } + + if (entity instanceof PlayerEntity player) { + if (ignoreFriends.get() && !Friends.get().shouldAttack(player)) return false; + } + + return true; + } + + // ── Enums ───────────────────────────────────────────────────────────────── + + public enum Target { Players, Entities, All } + public enum OnFallMode { None, Value, RandomValue } + public enum HitSpeedMode { None, Value, RandomValue } +} \ No newline at end of file diff --git a/src/main/java/com/nnpg/glazed/utils/glazed/MovementKeys.java b/src/main/java/com/nnpg/glazed/utils/glazed/MovementKeys.java new file mode 100644 index 00000000..7c46494e --- /dev/null +++ b/src/main/java/com/nnpg/glazed/utils/glazed/MovementKeys.java @@ -0,0 +1,138 @@ +package com.nnpg.glazed.utils.glazed; + +import meteordevelopment.meteorclient.utils.misc.input.Input; +import net.minecraft.client.option.KeyBinding; + +import static meteordevelopment.meteorclient.MeteorClient.mc; + +/** + * MovementKeys Utility - Vereinfachte Steuerung von Movement-Keys + * + * Verwendung in Modulen: + * MovementKeys.forward(true); // Vorwärts laufen + * MovementKeys.jump(true); // Springen + * MovementKeys.sneak(true); // Schleichen + * MovementKeys.releaseAll(); // Alle Keys loslassen + */ +public class MovementKeys { + + /** + * Setzt den Forward-Key (W) + */ + public static void forward(boolean pressed) { + setKey(mc.options.forwardKey, pressed); + } + + /** + * Setzt den Back-Key (S) + */ + public static void back(boolean pressed) { + setKey(mc.options.backKey, pressed); + } + + /** + * Setzt den Left-Key (A) + */ + public static void left(boolean pressed) { + setKey(mc.options.leftKey, pressed); + } + + /** + * Setzt den Right-Key (D) + */ + public static void right(boolean pressed) { + setKey(mc.options.rightKey, pressed); + } + + /** + * Setzt den Jump-Key (Space) + */ + public static void jump(boolean pressed) { + setKey(mc.options.jumpKey, pressed); + } + + /** + * Setzt den Sneak-Key (Shift) + */ + public static void sneak(boolean pressed) { + setKey(mc.options.sneakKey, pressed); + } + + /** + * Setzt den Sprint-Key (Ctrl) + */ + public static void sprint(boolean pressed) { + setKey(mc.options.sprintKey, pressed); + } + + /** + * Lässt alle Movement-Keys los + */ + public static void releaseAll() { + forward(false); + back(false); + left(false); + right(false); + jump(false); + sneak(false); + sprint(false); + } + + /** + * Prüft ob Forward-Key gedrückt ist + */ + public static boolean isForwardPressed() { + return mc.options.forwardKey.isPressed(); + } + + /** + * Prüft ob Back-Key gedrückt ist + */ + public static boolean isBackPressed() { + return mc.options.backKey.isPressed(); + } + + /** + * Prüft ob Left-Key gedrückt ist + */ + public static boolean isLeftPressed() { + return mc.options.leftKey.isPressed(); + } + + /** + * Prüft ob Right-Key gedrückt ist + */ + public static boolean isRightPressed() { + return mc.options.rightKey.isPressed(); + } + + /** + * Prüft ob Jump-Key gedrückt ist + */ + public static boolean isJumpPressed() { + return mc.options.jumpKey.isPressed(); + } + + /** + * Prüft ob Sneak-Key gedrückt ist + */ + public static boolean isSneakPressed() { + return mc.options.sneakKey.isPressed(); + } + + /** + * Prüft ob Sprint-Key gedrückt ist + */ + public static boolean isSprintPressed() { + return mc.options.sprintKey.isPressed(); + } + + /** + * Zentrale Methode zum Setzen eines Movement-Keys. + * Setzt sowohl KeyBinding als auch Meteor's Input-System. + */ + private static void setKey(KeyBinding key, boolean pressed) { + key.setPressed(pressed); + Input.setKeyState(key, pressed); + } +} From 1663a0db5e4da677216707584862b1a7977fc9dc Mon Sep 17 00:00:00 2001 From: HelixCraft Date: Tue, 17 Mar 2026 15:07:12 +0100 Subject: [PATCH 2/5] Add neues Feature --- CrystalTweaks_README.md | 85 ++++ .../java/com/nnpg/glazed/GlazedAddon.java | 10 +- .../glazed/mixins/CrystalTweaksMixin.java | 81 ++++ .../glazed/modules/pvp/CrystalDeathLock.java | 233 ++++++++++ .../glazed/modules/pvp/CrystalTweaks.java | 418 ++++++++++++++++++ src/main/resources/mixins.json | 3 +- 6 files changed, 822 insertions(+), 8 deletions(-) create mode 100644 CrystalTweaks_README.md create mode 100644 src/main/java/com/nnpg/glazed/mixins/CrystalTweaksMixin.java create mode 100644 src/main/java/com/nnpg/glazed/modules/pvp/CrystalDeathLock.java create mode 100644 src/main/java/com/nnpg/glazed/modules/pvp/CrystalTweaks.java diff --git a/CrystalTweaks_README.md b/CrystalTweaks_README.md new file mode 100644 index 00000000..14f72a6f --- /dev/null +++ b/CrystalTweaks_README.md @@ -0,0 +1,85 @@ +# CrystalTweaks + +Quality-of-life safety tweaks for Crystal PvP. Prevents the small inventory and input mistakes that cost fights. + +Requires `CrystalTweaksMixin` to be registered in `glazed.mixins.json`: +```json +"mixins": [ "CrystalTweaksMixin", ... ] +``` + +--- + +## Features + +### Totem Slot Protection +Prevents totems from being accidentally removed from the offhand or your configured backup hotbar slot. + +**Always active (when enabled):** +- Pressing F while the offhand already holds a totem is blocked — the totem cannot be ejected +- Number-key swapping a totem out of the backup hotbar slot is blocked + +**After a totem pop (for `pop-lock-ticks`):** +- Direct clicks on the offhand slot and the backup slot are also blocked, preventing mis-clicks during the restock panic + +| Setting | Default | Description | +|---|---|---| +| `backup-hotbar-slot` | `1` | Hotbar slot (1–9) to protect. Set to `0` to only protect the offhand. | +| `pop-lock-ticks` | `10` | How long to lock after a pop (10 ticks ≈ 500 ms) | + +--- + +### Anti Drop +Blocks the drop key (`Q`) whenever no inventory or container screen is open. As soon as any screen is opened, `Q` works normally again. + +Prevents accidentally dropping crystals, totems, or obsidian mid-fight by hitting `Q` out of reflex. + +--- + +### Anti Interrupt +Requires a double-tap of the chat key (`T`) within a configurable time window to actually open chat. A single accidental press is silently swallowed. + +Single press in the middle of a fight will never open chat — only an intentional double-tap will. + +| Setting | Default | Description | +|---|---|---| +| `double-tap-window-ms` | `300` | Time window (ms) in which the second press counts as a double-tap | + +--- + +### Cursor Guard +Disables left-click and right-click item pickup (drag & drop) in any open inventory screen. Items can only be moved via hotkey binds (number keys, `F`). Shift-click still works normally. + +Prevents the cursor from accidentally picking up an item when pressing a hotkey bind inside the inventory, which would mess up totem restocking. + +--- + +### Hotbar Lock +Locks all hotbar slots against changes. You can whitelist specific slots that are allowed to update freely (e.g. your totem slot managed by AutoInvTotem). + +| Setting | Default | Description | +|---|---|---| +| `whitelist-slots` | `"1"` | Comma-separated slot numbers (1–9) that are exempt from the lock. Leave empty to lock all slots. Example: `"1,2"` | + +--- + +### Glowstone Block +While holding Glowstone, cancels any right-click on a block that is **not** a Respawn Anchor. Glowstone can only be right-clicked into an anchor. + +Prevents accidentally placing Glowstone on the floor instead of into an anchor. + +--- + +### Anchor Max Fill +While holding Glowstone, blocks charging a Respawn Anchor that already has 1 or more charges. The first charge (0 → 1) is always allowed through. + +Since you explode the anchor immediately after the first charge, additional charges are pure Glowstone waste. This also prevents accidentally overcharging when spamming right-click. + +--- + +## Implementation Notes + +All slot-click interception (`CursorGuard`, `HotbarLock`, `TotemSlotProtection`) runs through the `CrystalTweaksMixin` injection into `ClientPlayerInteractionManager#clickSlot` at `HEAD`. This fires **before** Minecraft applies any client-side inventory change and **before** the `ClickSlotC2SPacket` is constructed — zero visual desync, zero packet traces. + +Block interaction interception (`GlowstoneBlock`, `AnchorMaxFill`) runs through the `CrystalTweaksMixin` injection into `ClientPlayerInteractionManager#interactBlock` at `HEAD`. This fires before the interaction is processed client-side and before `PlayerInteractBlockC2SPacket` is sent. + +`AntiDrop` and `AntiInterrupt` use Meteor's `KeyEvent`, which fires before Minecraft processes any key bind — no action is triggered, no packet is ever sent. diff --git a/src/main/java/com/nnpg/glazed/GlazedAddon.java b/src/main/java/com/nnpg/glazed/GlazedAddon.java index 5229837c..88868888 100644 --- a/src/main/java/com/nnpg/glazed/GlazedAddon.java +++ b/src/main/java/com/nnpg/glazed/GlazedAddon.java @@ -101,14 +101,10 @@ public void onInitialize() { Modules.get().add(new PremiumTunnelBaseFinder()); Modules.get().add(new AdminList()); Modules.get().add(new AutoTreeFarmer()); - // Modules.get().add(new MovementTest()); - Modules.get().add(new STabSprintReset()); - // Modules.get().add(new JumpReset()); - Modules.get().add(new TriggerBot()); - // Modules.get().add(new AttributeSwapper()); - + 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..72e12dab --- /dev/null +++ b/src/main/java/com/nnpg/glazed/mixins/CrystalTweaksMixin.java @@ -0,0 +1,81 @@ +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; + +/** + * Two injections into ClientPlayerInteractionManager: + * + * 1. clickSlot — HEAD, cancellable + * Handles: Cursor Guard, Hotbar Lock, Totem Slot Protection + * Fires before Minecraft applies the inventory change client-side + * AND before the ClickSlotC2SPacket is sent. + * + * 2. interactBlock — HEAD, cancellable + * Handles: Glowstone Block, Anchor Max Fill + * Fires before the block interaction is processed client-side + * AND before PlayerInteractBlockC2SPacket is sent. + * Returns ActionResult.FAIL to abort cleanly. + * + * ⚠ IMPORTANT — Register in glazed.mixins.json: + * "mixins": [ "CrystalTweaksMixin", ... ] + */ +@Mixin(ClientPlayerInteractionManager.class) +public class CrystalTweaksMixin { + + // ------------------------------------------------------------------------- + // 1. clickSlot + // ------------------------------------------------------------------------- + + @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(); + } + } + + // ------------------------------------------------------------------------- + // 2. interactBlock + // ------------------------------------------------------------------------- + + @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..9493930f --- /dev/null +++ b/src/main/java/com/nnpg/glazed/modules/pvp/CrystalDeathLock.java @@ -0,0 +1,233 @@ +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; + +/** + * CrystalDeathLock + * + * After detecting a nearby player's death, temporarily blocks: + * 1. End crystal placement — cancels PlayerInteractBlockC2SPacket + * when END_CRYSTAL is in the active hand + * 2. End crystal attacks — cancels AttackEntityEvent for EndCrystalEntity + * 3. Respawn anchor interact — cancels PlayerInteractBlockC2SPacket + * when the target block is RESPAWN_ANCHOR + * + * Death detection uses PacketEvent.Receive on the Netty IO-thread, + * which fires before Minecraft's main thread processes the packet. + * The lock timestamp is written as a volatile long, making it + * immediately visible to the main thread without synchronisation overhead. + * + * Blocking is done via PacketEvent.Send (main thread) and AttackEntityEvent. + * These are the same cancellation hooks used by NoBlockInteract.java. + */ +public class CrystalDeathLock extends Module { + + // ── Settings ────────────────────────────────────────────────────────────── + + 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() + ); + + // ── State ───────────────────────────────────────────────────────────────── + + /** + * Timestamp (System.nanoTime()) until which inputs are locked. + * Written on the Netty IO-thread, read on the main thread. + * volatile ensures the write is immediately visible across threads + * without any additional synchronisation. + * 0L = no active lock. + */ + private volatile long lockUntilNano = 0L; + + // ── Constructor ─────────────────────────────────────────────────────────── + + public CrystalDeathLock() { + super(GlazedAddon.pvp, "crystal-death-lock", + "Blocks end crystal and respawn anchor inputs for a configurable window after a nearby player dies."); + } + + // ── Lifecycle ───────────────────────────────────────────────────────────── + + @Override + public void onDeactivate() { + lockUntilNano = 0L; + } + + // ── Death detection (Netty IO-thread) ───────────────────────────────────── + + /** + * Fires on the Netty IO-thread, before the main thread sees the packet. + * EntityStatusS2CPacket status 3 = living entity death. + * + * We only write lockUntilNano here — no MC state mutation, which keeps + * this handler safe to run off the main thread. + */ + @EventHandler(priority = EventPriority.HIGHEST) + private void onPacketReceive(PacketEvent.Receive event) { + if (!(event.packet instanceof EntityStatusS2CPacket statusPacket)) return; + if (statusPacket.getStatus() != 3) return; // 3 = death + + if (mc.player == null || mc.world == null) return; + + // Resolve entity — reading the entity map from the Netty thread is safe + // (read-only access to a ConcurrentHashMap-backed structure). + Entity entity = statusPacket.getEntity(mc.world); + if (!(entity instanceof PlayerEntity dead)) return; + if (dead == mc.player) return; // don't lock on self-death + + // Distance check — reading position fields is safe (volatile in Entity) + double dist = mc.player.getPos().distanceTo(dead.getPos()); + if (dist > detectionRange.get()) return; + + // Activate the lock. + // nanoTime gives higher precision than currentTimeMillis and is + // not affected by system clock adjustments. + lockUntilNano = System.nanoTime() + (lockDurationMs.get() * 1_000_000L); + + if (notifications.get()) { + // mc.execute() schedules on the main thread — safe from Netty thread + 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()))); + } + } + + // ── Block crystal placement + anchor interact (main thread) ─────────────── + + /** + * Intercepts outgoing PlayerInteractBlockC2SPacket before it reaches the server. + * + * Cancels the packet (and thus the action) when: + * - Lock is active AND + * - Player is holding END_CRYSTAL (would place a crystal) → blockCrystalPlace + * - OR the target block is RESPAWN_ANCHOR → blockAnchorInteract + */ + @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(); + + // Crystal placement: player right-clicks a block holding END_CRYSTAL + if (blockCrystalPlace.get()) { + boolean holdsCrystal = + mc.player.getMainHandStack().getItem() == Items.END_CRYSTAL + || mc.player.getOffHandStack().getItem() == Items.END_CRYSTAL; + + if (holdsCrystal) { + event.cancel(); + return; + } + } + + // Respawn anchor: right-clicking to charge or interact + if (blockAnchorInteract.get() && block == Blocks.RESPAWN_ANCHOR) { + event.cancel(); + } + } + + // ── Block crystal attacks (main thread) ─────────────────────────────────── + + /** + * AttackEntityEvent fires on the main thread before the attack packet is sent. + * Cancelling it prevents both the client-side animation and the server packet. + */ + @EventHandler(priority = EventPriority.HIGHEST) + private void onAttackEntity(AttackEntityEvent event) { + if (!blockCrystalAttack.get()) return; + if (!isLocked()) return; + if (event.entity instanceof EndCrystalEntity) { + event.cancel(); + } + } + + // ── Helper ──────────────────────────────────────────────────────────────── + + /** + * Returns true if the lock is currently active. + * Reads lockUntilNano (volatile) — always sees the latest value + * written by the Netty thread. + * + * Also handles lock expiry notification: the first call after the lock + * expires logs the "unlocked" message exactly once. + */ + private boolean isLocked() { + long now = System.nanoTime(); + if (lockUntilNano == 0L) return false; + + if (now < lockUntilNano) return true; + + // Lock just expired — reset and notify + 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..76f7e2db --- /dev/null +++ b/src/main/java/com/nnpg/glazed/modules/pvp/CrystalTweaks.java @@ -0,0 +1,418 @@ +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; + +/** + * CrystalTweaks -- Crystal PvP inventory & combat safety tweaks. + * + * ALL interception (slot clicks + block interactions) runs through + * CrystalTweaksMixin, which hooks ClientPlayerInteractionManager at HEAD + * BEFORE any client-side changes are applied and BEFORE any packet is sent. + * Zero visual desync, zero suspicious packet traces. + * + * KeyEvent handles Anti-Drop and Anti-Interrupt independently, as those + * don't touch inventory or block state. + * + * Features: + * 1. Totem Slot Protection -- prevent totem from being removed from offhand + * or configured hotbar slots (always + brief lock after pop) + * 2. Anti Drop -- block Q when no screen is open + * 3. Anti Interrupt -- require double-tap of T to open chat + * 4. Cursor Guard -- disable PICKUP / drag in inventory + * 5. Hotbar Lock -- lock hotbar, comma-separated whitelist slots + * 6. Glowstone Block -- block right-clicking glowstone on non-anchor blocks + * 7. Anchor Max Fill -- prevent charging an anchor past 1 level + */ +public class CrystalTweaks extends Module { + + // ========================================================================= + // Setting Groups + // ========================================================================= + + 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"); + + // ========================================================================= + // 1. Totem Slot Protection + // Merges old "Offhand Lock" + "Anti-F Reverse" into one coherent feature. + // + // Always active (when enabled): + // - F-key (button 40) cannot remove a totem from the offhand + // - Number-key cannot swap a totem out of the configured backup slot + // + // After a pop (for popLockTicks): + // - Direct clicks on offhand slot (45) and the backup slot are also blocked + // to prevent accidental mis-clicks during restock panic + // ========================================================================= + + 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 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()); + + // ========================================================================= + // 2. Anti Drop + // ========================================================================= + + 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()); + + // ========================================================================= + // 3. Anti Interrupt + // ========================================================================= + + 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()); + + // ========================================================================= + // 4. Cursor Guard + // ========================================================================= + + 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()); + + // ========================================================================= + // 5. Hotbar Lock + // ========================================================================= + + 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()); + + // ========================================================================= + // 6. Glowstone Block + // ========================================================================= + + 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()); + + // ========================================================================= + // 7. Anchor Max Fill + // ========================================================================= + + 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()); + + // ========================================================================= + // State + // ========================================================================= + + /** Ticks remaining in the post-pop lock window. Decremented every tick. */ + private int popLockTimer = 0; + + /** Timestamp of the last T press, for double-tap detection. */ + private long lastChatKeyPressMs = 0L; + + // ========================================================================= + // Constructor + // ========================================================================= + + public CrystalTweaks() { + super(GlazedAddon.pvp, "crystal-tweaks", "Crystal PvP inventory & combat safety toolkit. Prevents accidental drops, misclicks, interrupted inputs and inventory mistakes during fights."); + } + + // ========================================================================= + // Lifecycle + // ========================================================================= + + @Override + public void onActivate() { + popLockTimer = 0; + lastChatKeyPressMs = 0L; + } + + // ========================================================================= + // Tick -- decrement pop-lock timer + // ========================================================================= + + @EventHandler + private void onTick(TickEvent.Pre event) { + if (popLockTimer > 0) popLockTimer--; + } + + // ========================================================================= + // Packet Receive -- detect totem pop via EntityStatus 35 + // ========================================================================= + + @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(); + } + } + + // ========================================================================= + // Key Event -- Anti Drop + Anti Interrupt + // Fires before Minecraft processes the bind -- fully clean, no packet sent. + // ========================================================================= + + @EventHandler + private void onKey(KeyEvent event) { + if (event.action != KeyAction.Press || mc.player == null) return; + + // --- Anti Drop --- + if (antiDropEnabled.get() && mc.currentScreen == null) { + if (mc.options.dropKey.matchesKey(event.key, 0)) { + event.cancel(); + return; + } + } + + // --- Anti Interrupt (double-tap chat) --- + 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; // valid double-tap, let through + } else { + lastChatKeyPressMs = now; // first press, absorb + event.cancel(); + } + } + } + } + + // ========================================================================= + // Public API for CrystalTweaksMixin -- shouldBlockSlotClick + // + // Called from the clickSlot injection, BEFORE Minecraft applies any + // client-side change and BEFORE the packet is constructed. + // ========================================================================= + + public boolean shouldBlockSlotClick(int syncId, int slot, int button, SlotActionType actionType) { + if (mc.player == null) return false; + + // --- Totem Slot Protection --- + if (totemProtectEnabled.get()) { + int backupSlot0 = totemBackupSlot.get() - 1; // 0-indexed; -1 if disabled + + // ALWAYS block: F-key swap (button=40) when offhand has totem (if enabled) + if (alwaysProtectOffhandTotem.get() && actionType == SlotActionType.SWAP && button == 40) { + if (mc.player.getOffHandStack().isOf(Items.TOTEM_OF_UNDYING)) { + return true; + } + } + + // ALWAYS block: number-key swap OUT of backup slot when it has totem (if enabled) + if (alwaysProtectHotbarTotem.get() && backupSlot0 >= 0 && actionType == SlotActionType.SWAP && button == backupSlot0) { + if (mc.player.getInventory().getStack(backupSlot0).isOf(Items.TOTEM_OF_UNDYING)) { + return true; + } + } + + // ALWAYS block direct clicks on offhand if it has totem (if enabled) + if (alwaysProtectOffhandTotem.get() && syncId == 0 && slot == 45) { + if (mc.player.getOffHandStack().isOf(Items.TOTEM_OF_UNDYING)) { + return true; + } + } + + // ALWAYS block direct clicks on backup slot if it has totem (if enabled) + if (alwaysProtectHotbarTotem.get() && backupSlot0 >= 0 && syncId == 0 && slot == 36 + backupSlot0) { + if (mc.player.getInventory().getStack(backupSlot0).isOf(Items.TOTEM_OF_UNDYING)) { + return true; + } + } + + // POST-POP LOCK: also block direct clicks on offhand (45) and backup + // container slot (36 + backupSlot0) for the lock window duration. + // syncId == 0 = player inventory screen. + if (popLockTimer > 0 && syncId == 0) { + if (slot == 45) return true; + if (backupSlot0 >= 0 && slot == 36 + backupSlot0) return true; + } + } + + // --- Cursor Guard --- + // Block PICKUP (grab onto cursor) and QUICK_CRAFT (drag) in any open screen. + // SWAP (hotkey / F) and QUICK_MOVE (shift-click) are intentionally allowed. + if (cursorGuardEnabled.get() && mc.currentScreen != null) { + if (actionType == SlotActionType.PICKUP || actionType == SlotActionType.QUICK_CRAFT) { + return true; + } + } + + // --- Hotbar Lock --- + // Scoped to player inventory (syncId == 0). + if (hotbarLockEnabled.get() && syncId == 0) { + int affected = resolveAffectedHotbarSlot(slot, button, actionType); + if (affected >= 0) { + Set whitelist = parseWhitelistSlots(); + if (!whitelist.contains(affected)) { + return true; + } + } + } + + return false; + } + + // ========================================================================= + // Public API for CrystalTweaksMixin -- shouldBlockInteractBlock + // + // Called from the interactBlock injection, BEFORE the interaction is + // processed client-side and BEFORE any packet is sent. + // Returning true causes the mixin to return ActionResult.FAIL. + // ========================================================================= + + public boolean shouldBlockInteractBlock(ClientPlayerEntity player, Hand hand, BlockHitResult hitResult) { + if (mc.world == null) return false; + + // Only act when the player is holding Glowstone in their main hand + 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); + + // --- Glowstone Block --- + if (glowstoneBlockEnabled.get() && !state.isOf(Blocks.RESPAWN_ANCHOR)) { + return true; + } + + // --- Anchor Max Fill --- + if (anchorFillEnabled.get() && state.isOf(Blocks.RESPAWN_ANCHOR)) { + if (state.get(RespawnAnchorBlock.CHARGES) >= 1) { + return true; + } + } + + return false; + } + + // ========================================================================= + // Helpers + // ========================================================================= + + /** + * Returns the 0-indexed hotbar slot that a given click action would modify, + * or -1 if no hotbar slot is affected. + */ + private int resolveAffectedHotbarSlot(int slot, int button, SlotActionType actionType) { + // Number-key swap: button 0-8 directly maps to hotbar slot + if (actionType == SlotActionType.SWAP && button >= 0 && button <= 8) { + return button; + } + // Direct click on hotbar slots in player inventory: container slots 36-44 + if ((actionType == SlotActionType.PICKUP || actionType == SlotActionType.QUICK_MOVE) + && slot >= 36 && slot <= 44) { + return slot - 36; + } + return -1; + } + + /** + * Parses the whitelist-slots setting ("1,2,5") into a set of 0-indexed slot indices. + * Invalid entries are silently ignored. + */ + 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); // convert to 0-indexed + } 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 From 95ec43e1dc9e18003c3350b638f9d4eab4c9d7df Mon Sep 17 00:00:00 2001 From: HelixCraft Date: Tue, 17 Mar 2026 15:53:10 +0100 Subject: [PATCH 3/5] gitignore --- .gitignore | 1 + .../glazed/modules/pvp/STabSprintReset.java | 332 ------------------ .../nnpg/glazed/modules/pvp/TriggerBot.java | 323 ----------------- .../glazed/utils/glazed/MovementKeys.java | 138 -------- 4 files changed, 1 insertion(+), 793 deletions(-) delete mode 100644 src/main/java/com/nnpg/glazed/modules/pvp/STabSprintReset.java delete mode 100644 src/main/java/com/nnpg/glazed/modules/pvp/TriggerBot.java delete mode 100644 src/main/java/com/nnpg/glazed/utils/glazed/MovementKeys.java diff --git a/.gitignore b/.gitignore index 16c658f6..b436a221 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ run/ # temp files temp/ +gradlew \ No newline at end of file diff --git a/src/main/java/com/nnpg/glazed/modules/pvp/STabSprintReset.java b/src/main/java/com/nnpg/glazed/modules/pvp/STabSprintReset.java deleted file mode 100644 index 19ef3a31..00000000 --- a/src/main/java/com/nnpg/glazed/modules/pvp/STabSprintReset.java +++ /dev/null @@ -1,332 +0,0 @@ -package com.nnpg.glazed.modules.pvp; - -import com.nnpg.glazed.utils.glazed.MovementKeys; -import meteordevelopment.meteorclient.events.entity.player.AttackEntityEvent; -import meteordevelopment.meteorclient.events.world.TickEvent; -import meteordevelopment.meteorclient.settings.BoolSetting; -import meteordevelopment.meteorclient.settings.DoubleSetting; -import meteordevelopment.meteorclient.settings.IntSetting; -import meteordevelopment.meteorclient.settings.Setting; -import meteordevelopment.meteorclient.settings.SettingGroup; -import meteordevelopment.meteorclient.systems.modules.Module; -import meteordevelopment.orbit.EventHandler; -import meteordevelopment.orbit.EventPriority; -import net.minecraft.entity.LivingEntity; - -import static com.nnpg.glazed.GlazedAddon.pvp; - -/** - * S-Tab Sprint Reset - * - * Mechanism: - * Attack stops sprint IMMEDIATELY in the same tick. - * Pressing S prevents sprint from restarting (W+S = net ~0 → no sprint). - * After releasing S, sprint restarts immediately if the sprint key is held. - * - * Timing (verified against Minecraft source / mcpk.wiki): - * Pre-Delay : 1–3 Ticks (50–150ms) — weighted toward lower values - * S-Hold : 1–3 Ticks (50–150ms) — weighted toward center - * Sub-Tick : 0–20ms jitter on release — breaks tick-boundary fingerprint - * - * Design goals for undetectability: - * - Pre-delay is never 0ms (no human reacts in the same tick as their click) - * - Weighted non-uniform distributions instead of flat random ranges - * - Sub-tick jitter on release so key-up never aligns exactly to a tick edge - * - Rate-limit guards against superhuman reset frequency at high CPS - * - Skip chance tuned to reflect a skilled-but-human success rate (~75-80%) - */ -public class STabSprintReset extends Module { - - private final SettingGroup sgGeneral = settings.getDefaultGroup(); - private final SettingGroup sgTiming = settings.createGroup("Timing"); - - // ── General Settings ────────────────────────────────────────────────────── - - private final Setting skipChance = sgGeneral.add(new DoubleSetting.Builder() - .name("skip-chance") - .description("Chance to skip the reset entirely (mimics human inconsistency)") - .defaultValue(20.0) // 20% skip → ~80% success rate, realistic for a skilled player - .min(0.0) - .max(100.0) - .sliderMax(100.0) - .build() - ); - - private final Setting advancedSettings = sgGeneral.add(new BoolSetting.Builder() - .name("advanced-settings") - .description("Show advanced timing settings") - .defaultValue(false) - .build() - ); - - // ── Timing Settings ─────────────────────────────────────────────────────── - - // Pre-delay: how many ticks to wait after the attack before pressing S. - // Default weighted distribution: 1T=40%, 2T=40%, 3T=20% (see rollPreDelay()). - // User-exposed min/max shifts the weight anchor, not a flat range. - private final Setting preDelayMin = sgTiming.add(new IntSetting.Builder() - .name("pre-delay-min") - .description("Minimum ticks before pressing S (1 = earliest human-possible)") - .defaultValue(1) - .min(1) // Never 0 — 0ms reaction is inhuman and directly detectable - .max(5) - .sliderMax(5) - .visible(() -> advancedSettings.get()) - .build() - ); - - private final Setting preDelayMax = sgTiming.add(new IntSetting.Builder() - .name("pre-delay-max") - .description("Maximum ticks before pressing S") - .defaultValue(3) - .min(1) - .max(8) - .sliderMax(8) - .visible(() -> advancedSettings.get()) - .build() - ); - - private final Setting sHoldMin = sgTiming.add(new IntSetting.Builder() - .name("s-hold-min") - .description("Minimum ticks to hold S") - .defaultValue(1) - .min(1) - .max(5) - .sliderMax(5) - .visible(() -> advancedSettings.get()) - .build() - ); - - private final Setting sHoldMax = sgTiming.add(new IntSetting.Builder() - .name("s-hold-max") - .description("Maximum ticks to hold S") - .defaultValue(3) - .min(1) - .max(8) - .sliderMax(8) - .visible(() -> advancedSettings.get()) - .build() - ); - - // ── State ───────────────────────────────────────────────────────────────── - - private boolean sKeyPressed = false; - private int preDelayTicks = 0; - private int sHoldTicks = 0; - private int currentPreDelay = 0; - private int currentSHold = 0; - private boolean waitingForPreDelay = false; - private boolean waitingForSRelease = false; - - // Sub-tick release jitter: once the hold tick count expires we don't - // release S immediately. Instead we set a real-time target (ms) and - // release when wall-clock time passes it. This means the key-up event - // is no longer aligned to a tick boundary — breaking the tick-edge - // fingerprint that behavioral anticheats look for. - private long releaseAtMs = -1L; - - // Rate-limiting: track the real-time of the last completed reset. - // A human doing S-Tab consistently faster than ~150ms is unrealistic. - private long lastResetTimeMs = 0L; - private static final int MIN_RESET_INTERVAL_MS = 150; - - // FIX 1 – Sprint-state cache. - // isSprinting() in onAttack() can already be false (sprint stops same - // tick as the attack). We cache it at tick-start, before attacks fire. - // isOnGround() is intentionally excluded: strafing (A/D) can briefly - // flip isSprinting() to false; we still want to catch those hits. - private boolean wasSprinting = false; - - // ── Constructor ─────────────────────────────────────────────────────────── - - public STabSprintReset() { - super(pvp, "s-tab-sprint-reset", "Prevents sprint restart after attack with S-tap"); - } - - // ── Lifecycle ───────────────────────────────────────────────────────────── - - @Override - public void onDeactivate() { - if (sKeyPressed) { - MovementKeys.back(false); - sKeyPressed = false; - } - resetState(); - } - - // ── Event Handlers ──────────────────────────────────────────────────────── - - @EventHandler(priority = EventPriority.HIGH) - private void onTick(TickEvent.Pre event) { - // Guard: dead player / world unload. - // onDeactivate() is not guaranteed to fire on death in Meteor Client. - if (mc.player == null || mc.player.isDead() || mc.world == null) { - if (sKeyPressed) { - MovementKeys.back(false); - sKeyPressed = false; - } - resetState(); - return; - } - - // FIX 1: Update sprint cache at tick-start, before AttackEvent fires. - wasSprinting = mc.player.isSprinting(); - - // FIX 3: Player left the ground during S-hold → release immediately. - // S pressed while airborne still blocks sprint technically, but - // produces a visible backward nudge that looks unnatural. - // Pre-delay phase does NOT abort on airborne – S pressed in the air - // after a ground hit still performs the sprint break correctly. - if (waitingForSRelease && !mc.player.isOnGround()) { - releaseS(); - return; - } - - // ── Pre-Delay Phase ─────────────────────────────────────────────────── - if (waitingForPreDelay) { - preDelayTicks++; - if (preDelayTicks >= currentPreDelay) { - pressS(); - } - return; - } - - // ── S-Hold Phase ────────────────────────────────────────────────────── - if (waitingForSRelease) { - sHoldTicks++; - - // Once the hold-tick threshold is reached, schedule a sub-tick - // release rather than releasing instantly on the tick boundary. - if (sHoldTicks >= currentSHold && releaseAtMs < 0) { - int jitterMs = (int)(Math.random() * 20); // 0–20ms sub-tick jitter - releaseAtMs = System.currentTimeMillis() + jitterMs; - } - - // Release only when real-time wall clock has passed the target. - if (releaseAtMs >= 0 && System.currentTimeMillis() >= releaseAtMs) { - releaseAtMs = -1L; - releaseS(); - } - return; - } - - // Safety-net: S is physically held but state machine is idle. - // Caused by: two attacks in the exact same tick, or external corruption. - if (sKeyPressed) { - releaseS(); - } - } - - @EventHandler(priority = EventPriority.HIGH) - private void onAttack(AttackEntityEvent event) { - if (!(event.entity instanceof LivingEntity)) return; - - // Ground check stays here, separate from the sprint cache. - if (!mc.player.isOnGround()) return; - - // FIX 1: Use cached sprint state. - if (!wasSprinting) return; - - // Rate-limit: prevent superhuman reset frequency at high CPS. - // No human consistently S-tabs faster than once every 150ms. - long now = System.currentTimeMillis(); - if (now - lastResetTimeMs < MIN_RESET_INTERVAL_MS) return; - - // FIX 4 (Spam-click safe): abort any running sequence cleanly. - if (sKeyPressed) releaseS(); - - // Skip chance. - if (Math.random() * 100 < skipChance.get()) return; - - // Roll timing using weighted distributions (see helpers below). - currentPreDelay = rollPreDelay(); - currentSHold = rollSHold(); - preDelayTicks = 0; - sHoldTicks = 0; - releaseAtMs = -1L; - waitingForPreDelay = true; - waitingForSRelease = false; - lastResetTimeMs = now; - - // Pre-delay of 1 tick means: press S on the NEXT tick, not this one. - // currentPreDelay is always >= 1 (enforced by rollPreDelay), - // so we never call pressS() here — we always go through the tick counter. - } - - // ── Helpers ─────────────────────────────────────────────────────────────── - - /** - * Weighted pre-delay distribution: - * 1 tick (50ms) → 40% — fast but plausible reaction - * 2 ticks (100ms)→ 40% — average human reaction - * 3 ticks (150ms)→ 20% — slightly slow / distracted hit - * - * Settings shift the anchor: if preDelayMin > 1, the distribution - * is clamped upward. If preDelayMax < 3, the top bucket collapses. - * Never returns 0 — a 0ms reaction is inhuman and directly detectable. - */ - private int rollPreDelay() { - int min = preDelayMin.get(); // always >= 1 - int max = preDelayMax.get(); - - double r = Math.random(); - int rolled; - if (r < 0.40) rolled = 1; - else if (r < 0.80) rolled = 2; - else rolled = 3; - - return Math.max(min, Math.min(max, rolled)); - } - - /** - * Weighted S-hold distribution: - * 1 tick (50ms) → 30% — quick tap - * 2 ticks (100ms)→ 50% — normal hold - * 3 ticks (150ms)→ 20% — slightly long hold - * - * Combined with the 0–20ms sub-tick release jitter, the actual - * hold duration is continuously distributed, not discretely bucketed. - */ - private int rollSHold() { - int min = sHoldMin.get(); - int max = sHoldMax.get(); - - double r = Math.random(); - int rolled; - if (r < 0.30) rolled = 1; - else if (r < 0.80) rolled = 2; - else rolled = 3; - - return Math.max(min, Math.min(max, rolled)); - } - - private void pressS() { - MovementKeys.back(true); - sKeyPressed = true; - - waitingForPreDelay = false; - waitingForSRelease = true; - sHoldTicks = 0; - releaseAtMs = -1L; - } - - private void releaseS() { - MovementKeys.back(false); - sKeyPressed = false; - resetState(); - } - - /** - * Resets all state fields. Never touches the S key directly — - * always call releaseS() first if sKeyPressed is true. - */ - private void resetState() { - waitingForPreDelay = false; - waitingForSRelease = false; - preDelayTicks = 0; - sHoldTicks = 0; - currentPreDelay = 0; - currentSHold = 0; - releaseAtMs = -1L; - } -} \ No newline at end of file diff --git a/src/main/java/com/nnpg/glazed/modules/pvp/TriggerBot.java b/src/main/java/com/nnpg/glazed/modules/pvp/TriggerBot.java deleted file mode 100644 index d8a2833e..00000000 --- a/src/main/java/com/nnpg/glazed/modules/pvp/TriggerBot.java +++ /dev/null @@ -1,323 +0,0 @@ -package com.nnpg.glazed.modules.pvp; - -import meteordevelopment.meteorclient.events.render.Render3DEvent; -import meteordevelopment.meteorclient.events.world.TickEvent; -import meteordevelopment.meteorclient.settings.*; -import meteordevelopment.meteorclient.systems.friends.Friends; -import meteordevelopment.meteorclient.systems.modules.Module; -import meteordevelopment.orbit.EventHandler; -import net.minecraft.entity.Entity; -import net.minecraft.entity.LivingEntity; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.util.Hand; -import net.minecraft.util.hit.EntityHitResult; -import net.minecraft.util.hit.HitResult; -import net.minecraft.util.math.Box; -import net.minecraft.util.math.Vec3d; -import com.nnpg.glazed.GlazedAddon; - -import java.util.Optional; - -public class TriggerBot extends Module { - - // ── Groups ──────────────────────────────────────────────────────────────── - - private final SettingGroup sgFilter = settings.createGroup("Filter"); - private final SettingGroup sgAttack = settings.createGroup("Attack"); - - // ── Filter ──────────────────────────────────────────────────────────────── - - private final Setting target = sgFilter.add(new EnumSetting.Builder() - .name("target") - .description("Which entities to attack.") - .defaultValue(Target.Players) - .build() - ); - - private final Setting range = sgFilter.add(new DoubleSetting.Builder() - .name("range") - .description("Maximum attack range. Warning: high range is easily detectable!") - .defaultValue(3.0) - .min(0.1) - .sliderMax(4.5) - .build() - ); - - private final Setting ignoreFriends = sgFilter.add(new BoolSetting.Builder() - .name("ignore-friends") - .description("Won't attack players on your friends list.") - .defaultValue(true) - .build() - ); - - private final Setting ignoreWalls = sgFilter.add(new BoolSetting.Builder() - .name("ignore-walls") - .description("Attack entities through walls.") - .defaultValue(false) - .build() - ); - - // ── Attack ──────────────────────────────────────────────────────────────── - - private final Setting hitWindowMs = sgAttack.add(new IntSetting.Builder() - .name("hit-window-ms") - .description("How long (ms) after the crosshair last touched a target the attack " - + "is still allowed. Catches fast flick-overs between ticks. " - + "50ms = 1 tick. 0 = disabled.") - .defaultValue(50) - .min(0) - .sliderRange(0, 150) - .build() - ); - - private final Setting onFallMode = sgAttack.add(new EnumSetting.Builder() - .name("on-fall-mode") - .description("Only attack while falling (for critical hits). " - + "None = always attack, Value = fixed velocity threshold, " - + "RandomValue = randomised threshold.") - .defaultValue(OnFallMode.None) - .build() - ); - - private final Setting onFallValue = sgAttack.add(new DoubleSetting.Builder() - .name("on-fall-velocity") - .description("Minimum downward velocity required to attack. " - + "0.1 = just past the jump peak (recommended), " - + "0.3 = deeper into the fall.") - .min(0.0) - .defaultValue(0.1) - .sliderRange(0.0, 1.0) - .visible(() -> onFallMode.get() == OnFallMode.Value) - .build() - ); - - private final Setting onFallMinRandomValue = sgAttack.add(new DoubleSetting.Builder() - .name("on-fall-min-random-velocity") - .description("Minimum of the randomised downward velocity threshold.") - .min(0.0) - .defaultValue(0.1) - .sliderRange(0.0, 1.0) - .visible(() -> onFallMode.get() == OnFallMode.RandomValue) - .build() - ); - - private final Setting onFallMaxRandomValue = sgAttack.add(new DoubleSetting.Builder() - .name("on-fall-max-random-velocity") - .description("Maximum of the randomised downward velocity threshold.") - .min(0.0) - .defaultValue(0.3) - .sliderRange(0.0, 1.0) - .visible(() -> onFallMode.get() == OnFallMode.RandomValue) - .build() - ); - - private final Setting hitSpeedMode = sgAttack.add(new EnumSetting.Builder() - .name("hit-speed-mode") - .description("Minimum attack cooldown required before attacking.") - .defaultValue(HitSpeedMode.RandomValue) - .build() - ); - - private final Setting hitSpeedValue = sgAttack.add(new DoubleSetting.Builder() - .name("hit-speed-value") - .description("Cooldown offset passed to getAttackCooldownProgress. 0 = full cooldown required.") - .defaultValue(0.0) - .sliderRange(-10, 10) - .visible(() -> hitSpeedMode.get() == HitSpeedMode.Value) - .build() - ); - - private final Setting hitSpeedMinRandomValue = sgAttack.add(new DoubleSetting.Builder() - .name("hit-speed-min-random-value") - .description("Minimum randomised cooldown offset value.") - .defaultValue(-0.1) - .sliderRange(-10, 10) - .visible(() -> hitSpeedMode.get() == HitSpeedMode.RandomValue) - .build() - ); - - private final Setting hitSpeedMaxRandomValue = sgAttack.add(new DoubleSetting.Builder() - .name("hit-speed-max-random-value") - .description("Maximum randomised cooldown offset value.") - .defaultValue(0.05) - .sliderRange(-10, 10) - .visible(() -> hitSpeedMode.get() == HitSpeedMode.RandomValue) - .build() - ); - - // ── State ───────────────────────────────────────────────────────────────── - - private float randomOnFallFloat = 0; - private float randomHitSpeedFloat = 0; - - // Sub-tick buffer: Render3DEvent fires every frame (~7ms at 144fps). - // Stores the last valid entity the crosshair touched so fast flick-overs - // between two ticks are not missed. Both Render3DEvent and TickEvent fire - // on the MC main thread – no synchronisation needed. - private Entity bufferedTarget = null; - private long lastSeenNano = 0L; - - // ── Constructor ─────────────────────────────────────────────────────────── - - public TriggerBot() { - super(GlazedAddon.pvp, "triggerbot", - "Attacks the entity you are looking at, optionally only when critting."); - } - - // ── Lifecycle ───────────────────────────────────────────────────────────── - - @Override - public void onActivate() { - randomOnFallFloat = 0; - randomHitSpeedFloat = 0; - bufferedTarget = null; - lastSeenNano = 0L; - } - - // ── Render frame: update sub-tick buffer ────────────────────────────────── - - // Runs every rendered frame. Only updates the buffer – no attack logic here. - @EventHandler - private void onRender(Render3DEvent event) { - if (mc.player == null || mc.world == null) return; - Entity found = getTarget(); - if (found != null) { - bufferedTarget = found; - lastSeenNano = System.nanoTime(); - } - } - - // ── Tick: attack ────────────────────────────────────────────────────────── - - @EventHandler - private void onTick(TickEvent.Pre event) { - if (mc.player == null || mc.player.isDead() - || mc.player.getHealth() <= 0 || mc.world == null) return; - - // ── Resolve target ──────────────────────────────────────────────────── - // Prefer live crosshair, fall back to render-frame buffer (hit window). - Entity entity = getTarget(); - - if (entity == null) { - long elapsedMs = (System.nanoTime() - lastSeenNano) / 1_000_000L; - if (bufferedTarget == null || elapsedMs > hitWindowMs.get()) return; - entity = bufferedTarget; - // Re-validate: entity may have moved/died since last render frame - if (!entity.isAlive()) { bufferedTarget = null; return; } - if (mc.player.squaredDistanceTo(entity) > range.get() * range.get()) { bufferedTarget = null; return; } - if (!entityCheck(entity)) { bufferedTarget = null; return; } - } - - // Clear buffer – attack fires this tick, fresh data needed next tick - bufferedTarget = null; - - // ── On-fall / crit check ────────────────────────────────────────────── - // velocity.y is negative while falling; threshold is stored as positive. - // Jitter of ±0.016 ≈ ±10ms so the exact trigger moment varies slightly. - OnFallMode currOnFallMode = onFallMode.get(); - if (currOnFallMode != OnFallMode.None) { - float threshold = (currOnFallMode == OnFallMode.Value) - ? onFallValue.get().floatValue() - : randomOnFallFloat; - - double vy = mc.player.getVelocity().y; - float jitter = (mc.world.random.nextFloat() * 0.032f) - 0.016f; - - if (vy >= -(threshold + jitter)) return; // not falling fast enough - if (mc.player.isOnGround()) return; // on ground = no crit - if (mc.player.isTouchingWater()) return; // vanilla crit blocker - if (mc.player.isClimbing()) return; // vanilla crit blocker - if (mc.player.hasVehicle()) return; // vanilla crit blocker - } - - // ── Hit-speed / cooldown check ──────────────────────────────────────── - HitSpeedMode currHitSpeedMode = hitSpeedMode.get(); - if (currHitSpeedMode != HitSpeedMode.None) { - float hitSpeed = (currHitSpeedMode == HitSpeedMode.Value) - ? hitSpeedValue.get().floatValue() - : randomHitSpeedFloat; - // Vanilla: (scale * 17) >= 16 → scale >= ~0.941 - if ((mc.player.getAttackCooldownProgress(hitSpeed) * 17.0F) < 16) return; - } - - // ── Attack ──────────────────────────────────────────────────────────── - mc.interactionManager.attackEntity(mc.player, entity); - mc.player.swingHand(Hand.MAIN_HAND); - - // ── Randomise next thresholds ───────────────────────────────────────── - if (currOnFallMode == OnFallMode.RandomValue) { - float min = Math.min(onFallMinRandomValue.get().floatValue(), onFallMaxRandomValue.get().floatValue()); - float max = Math.max(onFallMinRandomValue.get().floatValue(), onFallMaxRandomValue.get().floatValue()); - randomOnFallFloat = min + mc.world.random.nextFloat() * (max - min); - } - - if (currHitSpeedMode == HitSpeedMode.RandomValue) { - float min = Math.min(hitSpeedMinRandomValue.get().floatValue(), hitSpeedMaxRandomValue.get().floatValue()); - float max = Math.max(hitSpeedMinRandomValue.get().floatValue(), hitSpeedMaxRandomValue.get().floatValue()); - randomHitSpeedFloat = min + mc.world.random.nextFloat() * (max - min); - } - } - - // ── Target resolution ───────────────────────────────────────────────────── - - private Entity getTarget() { - if (ignoreWalls.get()) return getTargetThroughWalls(); - - if (mc.crosshairTarget == null - || mc.crosshairTarget.getType() != HitResult.Type.ENTITY) return null; - - Entity entity = ((EntityHitResult) mc.crosshairTarget).getEntity(); - if (mc.player.squaredDistanceTo(entity) > range.get() * range.get()) return null; - if (!entityCheck(entity)) return null; - return entity; - } - - private Entity getTargetThroughWalls() { - Vec3d eye = mc.player.getEyePos(); - Vec3d look = mc.player.getRotationVec(1.0F); - Vec3d end = eye.add(look.multiply(range.get())); - - Box searchBox = mc.player.getBoundingBox() - .stretch(look.multiply(range.get())) - .expand(1.0); - - Entity best = null; - double bestD = Double.MAX_VALUE; - - for (Entity candidate : mc.world.getEntitiesByClass( - LivingEntity.class, searchBox, this::entityCheck)) { - Optional hit = candidate.getBoundingBox().raycast(eye, end); - if (hit.isPresent()) { - double d = eye.squaredDistanceTo(hit.get()); - if (d < bestD) { bestD = d; best = candidate; } - } - } - return best; - } - - // ── Entity filter ───────────────────────────────────────────────────────── - - private boolean entityCheck(Entity entity) { - if (entity == mc.player || entity == mc.getCameraEntity()) return false; - if (!entity.isAlive()) return false; - if (entity instanceof LivingEntity le && (le.isDead() || le.getHealth() <= 0)) return false; - - switch (target.get()) { - case Players -> { if (!(entity instanceof PlayerEntity)) return false; } - case Entities -> { if ( entity instanceof PlayerEntity) return false; } - case All -> {} - } - - if (entity instanceof PlayerEntity player) { - if (ignoreFriends.get() && !Friends.get().shouldAttack(player)) return false; - } - - return true; - } - - // ── Enums ───────────────────────────────────────────────────────────────── - - public enum Target { Players, Entities, All } - public enum OnFallMode { None, Value, RandomValue } - public enum HitSpeedMode { None, Value, RandomValue } -} \ No newline at end of file diff --git a/src/main/java/com/nnpg/glazed/utils/glazed/MovementKeys.java b/src/main/java/com/nnpg/glazed/utils/glazed/MovementKeys.java deleted file mode 100644 index 7c46494e..00000000 --- a/src/main/java/com/nnpg/glazed/utils/glazed/MovementKeys.java +++ /dev/null @@ -1,138 +0,0 @@ -package com.nnpg.glazed.utils.glazed; - -import meteordevelopment.meteorclient.utils.misc.input.Input; -import net.minecraft.client.option.KeyBinding; - -import static meteordevelopment.meteorclient.MeteorClient.mc; - -/** - * MovementKeys Utility - Vereinfachte Steuerung von Movement-Keys - * - * Verwendung in Modulen: - * MovementKeys.forward(true); // Vorwärts laufen - * MovementKeys.jump(true); // Springen - * MovementKeys.sneak(true); // Schleichen - * MovementKeys.releaseAll(); // Alle Keys loslassen - */ -public class MovementKeys { - - /** - * Setzt den Forward-Key (W) - */ - public static void forward(boolean pressed) { - setKey(mc.options.forwardKey, pressed); - } - - /** - * Setzt den Back-Key (S) - */ - public static void back(boolean pressed) { - setKey(mc.options.backKey, pressed); - } - - /** - * Setzt den Left-Key (A) - */ - public static void left(boolean pressed) { - setKey(mc.options.leftKey, pressed); - } - - /** - * Setzt den Right-Key (D) - */ - public static void right(boolean pressed) { - setKey(mc.options.rightKey, pressed); - } - - /** - * Setzt den Jump-Key (Space) - */ - public static void jump(boolean pressed) { - setKey(mc.options.jumpKey, pressed); - } - - /** - * Setzt den Sneak-Key (Shift) - */ - public static void sneak(boolean pressed) { - setKey(mc.options.sneakKey, pressed); - } - - /** - * Setzt den Sprint-Key (Ctrl) - */ - public static void sprint(boolean pressed) { - setKey(mc.options.sprintKey, pressed); - } - - /** - * Lässt alle Movement-Keys los - */ - public static void releaseAll() { - forward(false); - back(false); - left(false); - right(false); - jump(false); - sneak(false); - sprint(false); - } - - /** - * Prüft ob Forward-Key gedrückt ist - */ - public static boolean isForwardPressed() { - return mc.options.forwardKey.isPressed(); - } - - /** - * Prüft ob Back-Key gedrückt ist - */ - public static boolean isBackPressed() { - return mc.options.backKey.isPressed(); - } - - /** - * Prüft ob Left-Key gedrückt ist - */ - public static boolean isLeftPressed() { - return mc.options.leftKey.isPressed(); - } - - /** - * Prüft ob Right-Key gedrückt ist - */ - public static boolean isRightPressed() { - return mc.options.rightKey.isPressed(); - } - - /** - * Prüft ob Jump-Key gedrückt ist - */ - public static boolean isJumpPressed() { - return mc.options.jumpKey.isPressed(); - } - - /** - * Prüft ob Sneak-Key gedrückt ist - */ - public static boolean isSneakPressed() { - return mc.options.sneakKey.isPressed(); - } - - /** - * Prüft ob Sprint-Key gedrückt ist - */ - public static boolean isSprintPressed() { - return mc.options.sprintKey.isPressed(); - } - - /** - * Zentrale Methode zum Setzen eines Movement-Keys. - * Setzt sowohl KeyBinding als auch Meteor's Input-System. - */ - private static void setKey(KeyBinding key, boolean pressed) { - key.setPressed(pressed); - Input.setKeyState(key, pressed); - } -} From ce3394af6a2b5eb47d282788e818620e405b0640 Mon Sep 17 00:00:00 2001 From: HelixCraft Date: Mon, 23 Mar 2026 19:46:02 +0100 Subject: [PATCH 4/5] Removing pay gorn comments. --- CrystalTweaks_README.md | 85 ---------- .../glazed/mixins/CrystalTweaksMixin.java | 25 --- .../glazed/modules/pvp/CrystalDeathLock.java | 88 +--------- .../glazed/modules/pvp/CrystalTweaks.java | 158 +----------------- 4 files changed, 6 insertions(+), 350 deletions(-) delete mode 100644 CrystalTweaks_README.md diff --git a/CrystalTweaks_README.md b/CrystalTweaks_README.md deleted file mode 100644 index 14f72a6f..00000000 --- a/CrystalTweaks_README.md +++ /dev/null @@ -1,85 +0,0 @@ -# CrystalTweaks - -Quality-of-life safety tweaks for Crystal PvP. Prevents the small inventory and input mistakes that cost fights. - -Requires `CrystalTweaksMixin` to be registered in `glazed.mixins.json`: -```json -"mixins": [ "CrystalTweaksMixin", ... ] -``` - ---- - -## Features - -### Totem Slot Protection -Prevents totems from being accidentally removed from the offhand or your configured backup hotbar slot. - -**Always active (when enabled):** -- Pressing F while the offhand already holds a totem is blocked — the totem cannot be ejected -- Number-key swapping a totem out of the backup hotbar slot is blocked - -**After a totem pop (for `pop-lock-ticks`):** -- Direct clicks on the offhand slot and the backup slot are also blocked, preventing mis-clicks during the restock panic - -| Setting | Default | Description | -|---|---|---| -| `backup-hotbar-slot` | `1` | Hotbar slot (1–9) to protect. Set to `0` to only protect the offhand. | -| `pop-lock-ticks` | `10` | How long to lock after a pop (10 ticks ≈ 500 ms) | - ---- - -### Anti Drop -Blocks the drop key (`Q`) whenever no inventory or container screen is open. As soon as any screen is opened, `Q` works normally again. - -Prevents accidentally dropping crystals, totems, or obsidian mid-fight by hitting `Q` out of reflex. - ---- - -### Anti Interrupt -Requires a double-tap of the chat key (`T`) within a configurable time window to actually open chat. A single accidental press is silently swallowed. - -Single press in the middle of a fight will never open chat — only an intentional double-tap will. - -| Setting | Default | Description | -|---|---|---| -| `double-tap-window-ms` | `300` | Time window (ms) in which the second press counts as a double-tap | - ---- - -### Cursor Guard -Disables left-click and right-click item pickup (drag & drop) in any open inventory screen. Items can only be moved via hotkey binds (number keys, `F`). Shift-click still works normally. - -Prevents the cursor from accidentally picking up an item when pressing a hotkey bind inside the inventory, which would mess up totem restocking. - ---- - -### Hotbar Lock -Locks all hotbar slots against changes. You can whitelist specific slots that are allowed to update freely (e.g. your totem slot managed by AutoInvTotem). - -| Setting | Default | Description | -|---|---|---| -| `whitelist-slots` | `"1"` | Comma-separated slot numbers (1–9) that are exempt from the lock. Leave empty to lock all slots. Example: `"1,2"` | - ---- - -### Glowstone Block -While holding Glowstone, cancels any right-click on a block that is **not** a Respawn Anchor. Glowstone can only be right-clicked into an anchor. - -Prevents accidentally placing Glowstone on the floor instead of into an anchor. - ---- - -### Anchor Max Fill -While holding Glowstone, blocks charging a Respawn Anchor that already has 1 or more charges. The first charge (0 → 1) is always allowed through. - -Since you explode the anchor immediately after the first charge, additional charges are pure Glowstone waste. This also prevents accidentally overcharging when spamming right-click. - ---- - -## Implementation Notes - -All slot-click interception (`CursorGuard`, `HotbarLock`, `TotemSlotProtection`) runs through the `CrystalTweaksMixin` injection into `ClientPlayerInteractionManager#clickSlot` at `HEAD`. This fires **before** Minecraft applies any client-side inventory change and **before** the `ClickSlotC2SPacket` is constructed — zero visual desync, zero packet traces. - -Block interaction interception (`GlowstoneBlock`, `AnchorMaxFill`) runs through the `CrystalTweaksMixin` injection into `ClientPlayerInteractionManager#interactBlock` at `HEAD`. This fires before the interaction is processed client-side and before `PlayerInteractBlockC2SPacket` is sent. - -`AntiDrop` and `AntiInterrupt` use Meteor's `KeyEvent`, which fires before Minecraft processes any key bind — no action is triggered, no packet is ever sent. diff --git a/src/main/java/com/nnpg/glazed/mixins/CrystalTweaksMixin.java b/src/main/java/com/nnpg/glazed/mixins/CrystalTweaksMixin.java index 72e12dab..572a47f7 100644 --- a/src/main/java/com/nnpg/glazed/mixins/CrystalTweaksMixin.java +++ b/src/main/java/com/nnpg/glazed/mixins/CrystalTweaksMixin.java @@ -15,30 +15,9 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -/** - * Two injections into ClientPlayerInteractionManager: - * - * 1. clickSlot — HEAD, cancellable - * Handles: Cursor Guard, Hotbar Lock, Totem Slot Protection - * Fires before Minecraft applies the inventory change client-side - * AND before the ClickSlotC2SPacket is sent. - * - * 2. interactBlock — HEAD, cancellable - * Handles: Glowstone Block, Anchor Max Fill - * Fires before the block interaction is processed client-side - * AND before PlayerInteractBlockC2SPacket is sent. - * Returns ActionResult.FAIL to abort cleanly. - * - * ⚠ IMPORTANT — Register in glazed.mixins.json: - * "mixins": [ "CrystalTweaksMixin", ... ] - */ @Mixin(ClientPlayerInteractionManager.class) public class CrystalTweaksMixin { - // ------------------------------------------------------------------------- - // 1. clickSlot - // ------------------------------------------------------------------------- - @Inject( method = "clickSlot", at = @At("HEAD"), @@ -58,10 +37,6 @@ public class CrystalTweaksMixin { } } - // ------------------------------------------------------------------------- - // 2. interactBlock - // ------------------------------------------------------------------------- - @Inject( method = "interactBlock", at = @At("HEAD"), diff --git a/src/main/java/com/nnpg/glazed/modules/pvp/CrystalDeathLock.java b/src/main/java/com/nnpg/glazed/modules/pvp/CrystalDeathLock.java index 9493930f..6be0a9d7 100644 --- a/src/main/java/com/nnpg/glazed/modules/pvp/CrystalDeathLock.java +++ b/src/main/java/com/nnpg/glazed/modules/pvp/CrystalDeathLock.java @@ -17,28 +17,8 @@ import net.minecraft.network.packet.s2c.play.EntityStatusS2CPacket; import net.minecraft.util.math.BlockPos; -/** - * CrystalDeathLock - * - * After detecting a nearby player's death, temporarily blocks: - * 1. End crystal placement — cancels PlayerInteractBlockC2SPacket - * when END_CRYSTAL is in the active hand - * 2. End crystal attacks — cancels AttackEntityEvent for EndCrystalEntity - * 3. Respawn anchor interact — cancels PlayerInteractBlockC2SPacket - * when the target block is RESPAWN_ANCHOR - * - * Death detection uses PacketEvent.Receive on the Netty IO-thread, - * which fires before Minecraft's main thread processes the packet. - * The lock timestamp is written as a volatile long, making it - * immediately visible to the main thread without synchronisation overhead. - * - * Blocking is done via PacketEvent.Send (main thread) and AttackEntityEvent. - * These are the same cancellation hooks used by NoBlockInteract.java. - */ public class CrystalDeathLock extends Module { - // ── Settings ────────────────────────────────────────────────────────────── - private final SettingGroup sgGeneral = settings.getDefaultGroup(); private final Setting detectionRange = sgGeneral.add(new DoubleSetting.Builder() @@ -87,64 +67,32 @@ public class CrystalDeathLock extends Module { .build() ); - // ── State ───────────────────────────────────────────────────────────────── - - /** - * Timestamp (System.nanoTime()) until which inputs are locked. - * Written on the Netty IO-thread, read on the main thread. - * volatile ensures the write is immediately visible across threads - * without any additional synchronisation. - * 0L = no active lock. - */ private volatile long lockUntilNano = 0L; - // ── Constructor ─────────────────────────────────────────────────────────── - public CrystalDeathLock() { super(GlazedAddon.pvp, "crystal-death-lock", "Blocks end crystal and respawn anchor inputs for a configurable window after a nearby player dies."); } - // ── Lifecycle ───────────────────────────────────────────────────────────── - @Override public void onDeactivate() { lockUntilNano = 0L; } - // ── Death detection (Netty IO-thread) ───────────────────────────────────── - - /** - * Fires on the Netty IO-thread, before the main thread sees the packet. - * EntityStatusS2CPacket status 3 = living entity death. - * - * We only write lockUntilNano here — no MC state mutation, which keeps - * this handler safe to run off the main thread. - */ @EventHandler(priority = EventPriority.HIGHEST) private void onPacketReceive(PacketEvent.Receive event) { if (!(event.packet instanceof EntityStatusS2CPacket statusPacket)) return; - if (statusPacket.getStatus() != 3) return; // 3 = death + if (statusPacket.getStatus() != 3) return; if (mc.player == null || mc.world == null) return; - - // Resolve entity — reading the entity map from the Netty thread is safe - // (read-only access to a ConcurrentHashMap-backed structure). Entity entity = statusPacket.getEntity(mc.world); if (!(entity instanceof PlayerEntity dead)) return; - if (dead == mc.player) return; // don't lock on self-death - - // Distance check — reading position fields is safe (volatile in Entity) + if (dead == mc.player) return; double dist = mc.player.getPos().distanceTo(dead.getPos()); if (dist > detectionRange.get()) return; - - // Activate the lock. - // nanoTime gives higher precision than currentTimeMillis and is - // not affected by system clock adjustments. lockUntilNano = System.nanoTime() + (lockDurationMs.get() * 1_000_000L); if (notifications.get()) { - // mc.execute() schedules on the main thread — safe from Netty thread String playerName = dead.getName().getString(); mc.execute(() -> ChatUtils.info( String.format("[CrystalDeathLock] §cLocked §r— %s died (%.1f blocks away). " @@ -153,16 +101,6 @@ private void onPacketReceive(PacketEvent.Receive event) { } } - // ── Block crystal placement + anchor interact (main thread) ─────────────── - - /** - * Intercepts outgoing PlayerInteractBlockC2SPacket before it reaches the server. - * - * Cancels the packet (and thus the action) when: - * - Lock is active AND - * - Player is holding END_CRYSTAL (would place a crystal) → blockCrystalPlace - * - OR the target block is RESPAWN_ANCHOR → blockAnchorInteract - */ @EventHandler(priority = EventPriority.HIGHEST) private void onPacketSend(PacketEvent.Send event) { if (!isLocked()) return; @@ -171,8 +109,6 @@ private void onPacketSend(PacketEvent.Send event) { BlockPos pos = packet.getBlockHitResult().getBlockPos(); var block = mc.world.getBlockState(pos).getBlock(); - - // Crystal placement: player right-clicks a block holding END_CRYSTAL if (blockCrystalPlace.get()) { boolean holdsCrystal = mc.player.getMainHandStack().getItem() == Items.END_CRYSTAL @@ -183,19 +119,11 @@ private void onPacketSend(PacketEvent.Send event) { return; } } - - // Respawn anchor: right-clicking to charge or interact if (blockAnchorInteract.get() && block == Blocks.RESPAWN_ANCHOR) { event.cancel(); } } - // ── Block crystal attacks (main thread) ─────────────────────────────────── - - /** - * AttackEntityEvent fires on the main thread before the attack packet is sent. - * Cancelling it prevents both the client-side animation and the server packet. - */ @EventHandler(priority = EventPriority.HIGHEST) private void onAttackEntity(AttackEntityEvent event) { if (!blockCrystalAttack.get()) return; @@ -205,23 +133,11 @@ private void onAttackEntity(AttackEntityEvent event) { } } - // ── Helper ──────────────────────────────────────────────────────────────── - - /** - * Returns true if the lock is currently active. - * Reads lockUntilNano (volatile) — always sees the latest value - * written by the Netty thread. - * - * Also handles lock expiry notification: the first call after the lock - * expires logs the "unlocked" message exactly once. - */ private boolean isLocked() { long now = System.nanoTime(); if (lockUntilNano == 0L) return false; if (now < lockUntilNano) return true; - - // Lock just expired — reset and notify if (lockUntilNano != 0L) { lockUntilNano = 0L; if (notifications.get()) { diff --git a/src/main/java/com/nnpg/glazed/modules/pvp/CrystalTweaks.java b/src/main/java/com/nnpg/glazed/modules/pvp/CrystalTweaks.java index 76f7e2db..0fb596d7 100644 --- a/src/main/java/com/nnpg/glazed/modules/pvp/CrystalTweaks.java +++ b/src/main/java/com/nnpg/glazed/modules/pvp/CrystalTweaks.java @@ -19,34 +19,8 @@ import java.util.HashSet; import java.util.Set; - -/** - * CrystalTweaks -- Crystal PvP inventory & combat safety tweaks. - * - * ALL interception (slot clicks + block interactions) runs through - * CrystalTweaksMixin, which hooks ClientPlayerInteractionManager at HEAD - * BEFORE any client-side changes are applied and BEFORE any packet is sent. - * Zero visual desync, zero suspicious packet traces. - * - * KeyEvent handles Anti-Drop and Anti-Interrupt independently, as those - * don't touch inventory or block state. - * - * Features: - * 1. Totem Slot Protection -- prevent totem from being removed from offhand - * or configured hotbar slots (always + brief lock after pop) - * 2. Anti Drop -- block Q when no screen is open - * 3. Anti Interrupt -- require double-tap of T to open chat - * 4. Cursor Guard -- disable PICKUP / drag in inventory - * 5. Hotbar Lock -- lock hotbar, comma-separated whitelist slots - * 6. Glowstone Block -- block right-clicking glowstone on non-anchor blocks - * 7. Anchor Max Fill -- prevent charging an anchor past 1 level - */ public class CrystalTweaks extends Module { - // ========================================================================= - // Setting Groups - // ========================================================================= - 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"); @@ -55,19 +29,6 @@ public class CrystalTweaks extends Module { 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"); - // ========================================================================= - // 1. Totem Slot Protection - // Merges old "Offhand Lock" + "Anti-F Reverse" into one coherent feature. - // - // Always active (when enabled): - // - F-key (button 40) cannot remove a totem from the offhand - // - Number-key cannot swap a totem out of the configured backup slot - // - // After a pop (for popLockTicks): - // - Direct clicks on offhand slot (45) and the backup slot are also blocked - // to prevent accidental mis-clicks during restock panic - // ========================================================================= - 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. " @@ -108,20 +69,12 @@ public class CrystalTweaks extends Module { .visible(totemProtectEnabled::get) .build()); - // ========================================================================= - // 2. Anti Drop - // ========================================================================= - 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()); - // ========================================================================= - // 3. Anti Interrupt - // ========================================================================= - 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.") @@ -136,10 +89,6 @@ public class CrystalTweaks extends Module { .visible(antiInterruptEnabled::get) .build()); - // ========================================================================= - // 4. Cursor Guard - // ========================================================================= - 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. " @@ -148,10 +97,6 @@ public class CrystalTweaks extends Module { .defaultValue(false) .build()); - // ========================================================================= - // 5. Hotbar Lock - // ========================================================================= - 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.") @@ -166,20 +111,12 @@ public class CrystalTweaks extends Module { .visible(hotbarLockEnabled::get) .build()); - // ========================================================================= - // 6. Glowstone Block - // ========================================================================= - 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()); - // ========================================================================= - // 7. Anchor Max Fill - // ========================================================================= - 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. " @@ -187,47 +124,25 @@ public class CrystalTweaks extends Module { .defaultValue(false) .build()); - // ========================================================================= - // State - // ========================================================================= - - /** Ticks remaining in the post-pop lock window. Decremented every tick. */ private int popLockTimer = 0; - /** Timestamp of the last T press, for double-tap detection. */ private long lastChatKeyPressMs = 0L; - // ========================================================================= - // Constructor - // ========================================================================= - public CrystalTweaks() { super(GlazedAddon.pvp, "crystal-tweaks", "Crystal PvP inventory & combat safety toolkit. Prevents accidental drops, misclicks, interrupted inputs and inventory mistakes during fights."); } - // ========================================================================= - // Lifecycle - // ========================================================================= - @Override public void onActivate() { popLockTimer = 0; lastChatKeyPressMs = 0L; } - // ========================================================================= - // Tick -- decrement pop-lock timer - // ========================================================================= - @EventHandler private void onTick(TickEvent.Pre event) { if (popLockTimer > 0) popLockTimer--; } - // ========================================================================= - // Packet Receive -- detect totem pop via EntityStatus 35 - // ========================================================================= - @EventHandler private void onPacketReceive(PacketEvent.Receive event) { if (!totemProtectEnabled.get()) return; @@ -239,99 +154,62 @@ private void onPacketReceive(PacketEvent.Receive event) { } } - // ========================================================================= - // Key Event -- Anti Drop + Anti Interrupt - // Fires before Minecraft processes the bind -- fully clean, no packet sent. - // ========================================================================= - @EventHandler private void onKey(KeyEvent event) { if (event.action != KeyAction.Press || mc.player == null) return; - - // --- Anti Drop --- if (antiDropEnabled.get() && mc.currentScreen == null) { if (mc.options.dropKey.matchesKey(event.key, 0)) { event.cancel(); return; } } - - // --- Anti Interrupt (double-tap chat) --- 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; // valid double-tap, let through + lastChatKeyPressMs = 0L; } else { - lastChatKeyPressMs = now; // first press, absorb + lastChatKeyPressMs = now; event.cancel(); } } } } - // ========================================================================= - // Public API for CrystalTweaksMixin -- shouldBlockSlotClick - // - // Called from the clickSlot injection, BEFORE Minecraft applies any - // client-side change and BEFORE the packet is constructed. - // ========================================================================= - public boolean shouldBlockSlotClick(int syncId, int slot, int button, SlotActionType actionType) { if (mc.player == null) return false; - - // --- Totem Slot Protection --- if (totemProtectEnabled.get()) { - int backupSlot0 = totemBackupSlot.get() - 1; // 0-indexed; -1 if disabled - - // ALWAYS block: F-key swap (button=40) when offhand has totem (if enabled) + int backupSlot0 = totemBackupSlot.get() - 1; if (alwaysProtectOffhandTotem.get() && actionType == SlotActionType.SWAP && button == 40) { if (mc.player.getOffHandStack().isOf(Items.TOTEM_OF_UNDYING)) { return true; } } - - // ALWAYS block: number-key swap OUT of backup slot when it has totem (if enabled) if (alwaysProtectHotbarTotem.get() && backupSlot0 >= 0 && actionType == SlotActionType.SWAP && button == backupSlot0) { if (mc.player.getInventory().getStack(backupSlot0).isOf(Items.TOTEM_OF_UNDYING)) { return true; } } - - // ALWAYS block direct clicks on offhand if it has totem (if enabled) if (alwaysProtectOffhandTotem.get() && syncId == 0 && slot == 45) { if (mc.player.getOffHandStack().isOf(Items.TOTEM_OF_UNDYING)) { return true; } } - - // ALWAYS block direct clicks on backup slot if it has totem (if enabled) if (alwaysProtectHotbarTotem.get() && backupSlot0 >= 0 && syncId == 0 && slot == 36 + backupSlot0) { if (mc.player.getInventory().getStack(backupSlot0).isOf(Items.TOTEM_OF_UNDYING)) { return true; } } - - // POST-POP LOCK: also block direct clicks on offhand (45) and backup - // container slot (36 + backupSlot0) for the lock window duration. - // syncId == 0 = player inventory screen. if (popLockTimer > 0 && syncId == 0) { if (slot == 45) return true; if (backupSlot0 >= 0 && slot == 36 + backupSlot0) return true; } } - - // --- Cursor Guard --- - // Block PICKUP (grab onto cursor) and QUICK_CRAFT (drag) in any open screen. - // SWAP (hotkey / F) and QUICK_MOVE (shift-click) are intentionally allowed. if (cursorGuardEnabled.get() && mc.currentScreen != null) { if (actionType == SlotActionType.PICKUP || actionType == SlotActionType.QUICK_CRAFT) { return true; } } - - // --- Hotbar Lock --- - // Scoped to player inventory (syncId == 0). if (hotbarLockEnabled.get() && syncId == 0) { int affected = resolveAffectedHotbarSlot(slot, button, actionType); if (affected >= 0) { @@ -345,30 +223,16 @@ public boolean shouldBlockSlotClick(int syncId, int slot, int button, SlotAction return false; } - // ========================================================================= - // Public API for CrystalTweaksMixin -- shouldBlockInteractBlock - // - // Called from the interactBlock injection, BEFORE the interaction is - // processed client-side and BEFORE any packet is sent. - // Returning true causes the mixin to return ActionResult.FAIL. - // ========================================================================= - public boolean shouldBlockInteractBlock(ClientPlayerEntity player, Hand hand, BlockHitResult hitResult) { if (mc.world == null) return false; - - // Only act when the player is holding Glowstone in their main hand 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); - - // --- Glowstone Block --- if (glowstoneBlockEnabled.get() && !state.isOf(Blocks.RESPAWN_ANCHOR)) { return true; } - - // --- Anchor Max Fill --- if (anchorFillEnabled.get() && state.isOf(Blocks.RESPAWN_ANCHOR)) { if (state.get(RespawnAnchorBlock.CHARGES) >= 1) { return true; @@ -378,20 +242,10 @@ public boolean shouldBlockInteractBlock(ClientPlayerEntity player, Hand hand, Bl return false; } - // ========================================================================= - // Helpers - // ========================================================================= - - /** - * Returns the 0-indexed hotbar slot that a given click action would modify, - * or -1 if no hotbar slot is affected. - */ private int resolveAffectedHotbarSlot(int slot, int button, SlotActionType actionType) { - // Number-key swap: button 0-8 directly maps to hotbar slot if (actionType == SlotActionType.SWAP && button >= 0 && button <= 8) { return button; } - // Direct click on hotbar slots in player inventory: container slots 36-44 if ((actionType == SlotActionType.PICKUP || actionType == SlotActionType.QUICK_MOVE) && slot >= 36 && slot <= 44) { return slot - 36; @@ -399,10 +253,6 @@ private int resolveAffectedHotbarSlot(int slot, int button, SlotActionType actio return -1; } - /** - * Parses the whitelist-slots setting ("1,2,5") into a set of 0-indexed slot indices. - * Invalid entries are silently ignored. - */ private Set parseWhitelistSlots() { Set result = new HashSet<>(); String raw = hotbarWhitelistSlots.get().trim(); @@ -410,7 +260,7 @@ private Set parseWhitelistSlots() { for (String part : raw.split(",")) { try { int slot = Integer.parseInt(part.trim()); - if (slot >= 1 && slot <= 9) result.add(slot - 1); // convert to 0-indexed + if (slot >= 1 && slot <= 9) result.add(slot - 1); } catch (NumberFormatException ignored) {} } return result; From c9fb7d115fd0efc0f05f96a17c69811d148e6028 Mon Sep 17 00:00:00 2001 From: HelixCraft Date: Thu, 23 Apr 2026 16:09:18 +0200 Subject: [PATCH 5/5] CrystalTweaks inprovements --- .gitignore | 2 +- .../glazed/modules/pvp/CrystalTweaks.java | 121 +++++++++++++----- 2 files changed, 88 insertions(+), 35 deletions(-) diff --git a/.gitignore b/.gitignore index b436a221..1ed94c2b 100644 --- a/.gitignore +++ b/.gitignore @@ -33,5 +33,5 @@ bin/ run/ # temp files -temp/ +DEVNODE/ gradlew \ No newline at end of file diff --git a/src/main/java/com/nnpg/glazed/modules/pvp/CrystalTweaks.java b/src/main/java/com/nnpg/glazed/modules/pvp/CrystalTweaks.java index 0fb596d7..c269e26b 100644 --- a/src/main/java/com/nnpg/glazed/modules/pvp/CrystalTweaks.java +++ b/src/main/java/com/nnpg/glazed/modules/pvp/CrystalTweaks.java @@ -19,6 +19,7 @@ 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"); @@ -61,6 +62,20 @@ public class CrystalTweaks extends Module { .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).") @@ -111,6 +126,13 @@ public class CrystalTweaks extends Module { .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.") @@ -178,43 +200,69 @@ private void onKey(KeyEvent event) { 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; - if (alwaysProtectOffhandTotem.get() && actionType == SlotActionType.SWAP && button == 40) { - if (mc.player.getOffHandStack().isOf(Items.TOTEM_OF_UNDYING)) { - return true; - } - } - if (alwaysProtectHotbarTotem.get() && backupSlot0 >= 0 && actionType == SlotActionType.SWAP && button == backupSlot0) { - if (mc.player.getInventory().getStack(backupSlot0).isOf(Items.TOTEM_OF_UNDYING)) { - return true; - } - } - if (alwaysProtectOffhandTotem.get() && syncId == 0 && slot == 45) { - if (mc.player.getOffHandStack().isOf(Items.TOTEM_OF_UNDYING)) { - return true; - } + + // 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 && syncId == 0 && slot == 36 + backupSlot0) { - if (mc.player.getInventory().getStack(backupSlot0).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; } } - if (hotbarLockEnabled.get() && syncId == 0) { - int affected = resolveAffectedHotbarSlot(slot, button, actionType); - if (affected >= 0) { - Set whitelist = parseWhitelistSlots(); - if (!whitelist.contains(affected)) { + + // --- 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; } } @@ -223,6 +271,22 @@ public boolean shouldBlockSlotClick(int syncId, int slot, int button, SlotAction 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; @@ -242,17 +306,6 @@ public boolean shouldBlockInteractBlock(ClientPlayerEntity player, Hand hand, Bl return false; } - private int resolveAffectedHotbarSlot(int slot, int button, SlotActionType actionType) { - if (actionType == SlotActionType.SWAP && button >= 0 && button <= 8) { - return button; - } - if ((actionType == SlotActionType.PICKUP || actionType == SlotActionType.QUICK_MOVE) - && slot >= 36 && slot <= 44) { - return slot - 36; - } - return -1; - } - private Set parseWhitelistSlots() { Set result = new HashSet<>(); String raw = hotbarWhitelistSlots.get().trim();