release: v3.5.0.2 - fix treasure map leak, end-city/spike idempotency bugs, remove redundant work

Fixed:
- MapSeedInterceptor filtered map icons against "TREASURE"/"PILLAGER"/"BASTION",
  none of which are real vanilla map decoration IDs (verified against
  org.bukkit.map.MapCursor.Type directly - no such constants exist). Buried
  treasure maps actually use TARGET_X, which was missing entirely, so treasure
  locations were never actually scrambled despite being a named threat-model
  signal. Now matches TARGET_X; the two dead checks were removed.
- EndCityProtector and EndSpikeProtector both marked a chunk/world "already
  processed" as soon as it was scanned, even when modify_world was off and
  nothing was changed. Turning modify_world on later (including via
  /asc toggle) would silently do nothing for anything already loaded once
  while it was off. Both now only mark "done" when the modification actually
  ran, or (End Cities) when a chunk provably has no End City at all.
- All four dragon-respawn end crystals independently started their own
  300-attempt polling loop to detect the end of the respawn animation - 4x
  the necessary scheduled work per fight. Now guarded to one loop per world.

Optimized:
- Replaced reflection-based scheduled-task cancellation in FoliaSchedulerUtil
  with direct typed ScheduledTask#cancel() calls (verified against the real
  Paper API); a genuine cancel failure is no longer silently swallowed.
- Removed a pointless seconds->ticks->seconds round-trip in the seed rotation
  interval config.
- Simplified the End Spike pillar-angle formula to a plain decagon spacing
  expression (behaviorally identical, verified via cos/sin periodicity).
- Trimmed default config.yml to essential settings; fuller explanations live
  in the README instead of long inline comments.
This commit is contained in:
SunsetRQ7 committed 2026-07-24 16:08:17 +03:30
1 parent e6f73e3f05
commit ab0ba5362e
13 files changed
+106 -45

No files matched your search

+37
View File
@@ -3,6 +3,43 @@
All notable changes to AntiSeedCracker are documented here. Format loosely follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [3.5.0.2] - 2026-07-24
### Fixed
- **Buried treasure map locations were never actually scrambled**: `MapSeedInterceptor`
filtered map decorations by name, checking for `"TREASURE"`, `"PILLAGER"`, and
`"BASTION"` — none of which are real vanilla map decoration type IDs (verified against
`org.bukkit.map.MapCursor.Type` directly: no such constants exist, so those three checks
never matched anything, ever). Buried treasure maps actually use `TARGET_X`, which was
missing entirely. Buried treasure is one of this plugin's own named SeedCrackerX
structure signals, so its map marker was leaking the real, unscrambled location on every
treasure map handed to a player. Now correctly matches `TARGET_X`; the two other
never-matching checks were removed.
- **`end_cities`/`end_spikes` `modify_world: false` could permanently disable itself**:
both `EndCityProtector` and `EndSpikeProtector` marked a chunk/world as "already
processed" as soon as it was scanned, even when `modify_world` was off and nothing was
actually changed. Turning `modify_world` on later via `/asc toggle` or a config edit
would find that flag already set and skip the modification forever, for any chunk/world
already loaded once while it was off. Both now only mark a chunk/world "done" when the
modification actually ran (or, for End Cities, when a chunk provably has no End City at
all and therefore never will need modifying).
- **Redundant respawn-cycle polling in `EndSpikeProtector`**: all four dragon-respawn End
Crystals fire their placement event independently, each previously starting its own
300-attempt polling loop to detect when the respawn animation finished — four times the
necessary scheduled work per dragon fight. Now guarded so only one poll loop runs per
world at a time.
- Replaced reflection-based scheduled-task cancellation (`getMethod("cancel").invoke(...)`,
silently swallowing any failure) in `FoliaSchedulerUtil` with direct, typed
`ScheduledTask#cancel()` calls — verified against the real Paper API class. Faster, and
a genuine cancellation failure is no longer silently discarded.
- Removed a pointless seconds→ticks→seconds round-trip conversion in the seed rotation
interval config.
- Simplified the End Spike pillar-angle formula to a plain decagon spacing expression;
behaviorally identical (verified via the periodicity of `cos`/`sin`), just no longer
needlessly obscure.
- Trimmed default `config.yml` down to essential settings, with fuller explanations kept
in the README instead of long inline comments.
## [3.5.0.1] - 2026-07-22
### Fixed
+3 -3
View File
@@ -5,7 +5,7 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
[![Java](https://img.shields.io/badge/Java-25-orange)](https://adoptium.net/)
**Version:** 3.5.0.1  |  **Target:** Minecraft 26.1.2  |  **Author:** SunsetRQ7_
**Version:** 3.5.0.2  |  **Target:** Minecraft 26.1.2  |  **Author:** SunsetRQ7_
AntiSeedCracker is a professional, multi-layer world seed protection plugin for Paper, Purpur, and Folia. It blocks every known technique a player can use to recover your world seed, protecting your server from ESP clients, ghost clients, and seed-cracking tools such as SeedCrackerX.
@@ -51,7 +51,7 @@ Anonymous usage statistics are collected via [bStats](https://bstats.org/plugin/
## Installation
1. Download `AntiSeedCracker-3.5.0.1.jar` from [Releases](../../releases/latest).
1. Download `AntiSeedCracker-3.5.0.2.jar` from [Releases](../../releases/latest).
2. Place it in your server's `plugins/` folder.
3. Start or restart the server.
4. Edit `plugins/AntiSeedCracker/config.yml` to your preference.
@@ -237,7 +237,7 @@ export JAVA_HOME=/path/to/jdk-25
mvn clean package
```
The shaded jar is produced at `target/AntiSeedCracker-3.5.0.1.jar`. PacketEvents 2.13.0 and bStats 3.2.1 are bundled and relocated under `me.sunsetrqsv.antiseedcracker.lib`.
The shaded jar is produced at `target/AntiSeedCracker-3.5.0.2.jar`. PacketEvents 2.13.0 and bStats 3.2.1 are bundled and relocated under `me.sunsetrqsv.antiseedcracker.lib`.
---
+2 -2
View File
@@ -29,7 +29,7 @@ Once your seed is out, it doesn't matter how good your build-battle event or you
- **`/locate` spoofing** — returns convincing fake coordinates, stable per player and per structure so repeat queries can't be used to spot the fake.
- **End spike randomization** — shuffles the bedrock cap heights of the ten End pillars, destroying the vanilla height fingerprint that uniquely identifies a seed.
- **End City fingerprint removal** — replaces the distinctive magenta glass pattern in End Cities (scoped to the structure's bounding box, not the whole chunk).
- **Treasure & explorer map scrambling** — structure icons on maps are randomized in the `MAP_DATA` packet, stable per map so icons don't visibly jitter around.
- **Treasure & explorer map scrambling** — buried treasure, mansion, monument, and other structure icons on maps are randomized in the `MAP_DATA` packet, stable per map so icons don't visibly jitter around.
- **Slime chunk obfuscation** *(opt-in)* — denies slime-chunk-sampling-based cracking at the cost of disabling slime farms where enabled.
**Command & detection** *(all logging-only where noted — never punishes players)*
@@ -71,7 +71,7 @@ Spigot/CraftBukkit and legacy Minecraft versions (1.16.5 – 25.x) are not suppo
## Quick start
1. Drop `AntiSeedCracker-3.5.0.1.jar` into your `plugins/` folder.
1. Drop `AntiSeedCracker-3.5.0.2.jar` into your `plugins/` folder.
2. Restart the server. Protection is active immediately with sensible defaults.
3. Optionally tune `plugins/AntiSeedCracker/config.yml` — every layer, world list, message, and interval is configurable — and apply with `/asc reload`, or flip a single feature live with `/asc toggle`.
+10
View File
@@ -1,3 +1,13 @@
## 3.5.0.2 — 2026-07-24
**Fixed**
- Fixed buried treasure map locations never actually being scrambled: the treasure map protection layer was filtering map icons by the wrong internal name, so a real buried treasure map still pointed straight at the true, unmodified location. It now correctly detects and scrambles the treasure marker.
- Fixed `end_cities`/`end_spikes` protection being able to permanently disable itself: if `modify_world` was left off for a world or chunk, turning it on later (including via `/asc toggle`) would silently do nothing for anything already loaded once while it was off. It now correctly applies the moment it's turned on.
- Fixed the End obsidian pillar protection starting up to four times more background work than needed during a dragon respawn.
- General code cleanup and minor performance tightening (task scheduling, config parsing) with no behavior changes.
---
## 3.5.0.1 — 2026-07-22
**Fixed**
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>me.sunsetrqsv</groupId>
<artifactId>AntiSeedCracker</artifactId>
<version>3.5.0.1</version>
<version>3.5.0.2</version>
<packaging>jar</packaging>
<name>AntiSeedCracker</name>
@@ -25,7 +25,7 @@ public final class PluginConfig {
private final boolean interceptRespawn;
private final boolean seedRotationEnabled;
private final long seedRotationIntervalTicks;
private final long seedRotationIntervalSeconds;
private final boolean endSpikesEnabled;
private final List<String> endSpikeWorlds;
@@ -76,8 +76,8 @@ public final class PluginConfig {
this.interceptRespawn = cfg.getBoolean("seed_obfuscation.intercept_respawn", true);
this.seedRotationEnabled = cfg.getBoolean("seed_rotation.enabled", true);
long rawSecs = Math.max(30L, cfg.getLong("seed_rotation.interval_seconds", 60L));
this.seedRotationIntervalTicks = rawSecs * 20L;
this.seedRotationIntervalSeconds = Math.max(30L,
cfg.getLong("seed_rotation.interval_seconds", 60L));
this.endSpikesEnabled = cfg.getBoolean("structure_protection.end_spikes.enabled", true);
this.endSpikeWorlds = Collections.unmodifiableList(
@@ -154,8 +154,8 @@ public final class PluginConfig {
public boolean isInterceptLogin() { return interceptLogin; }
public boolean isInterceptRespawn() { return interceptRespawn; }
public boolean isSeedRotationEnabled() { return seedRotationEnabled; }
public long getSeedRotationIntervalTicks() { return seedRotationIntervalTicks; }
public boolean isSeedRotationEnabled() { return seedRotationEnabled; }
public long getSeedRotationIntervalSeconds() { return seedRotationIntervalSeconds; }
public boolean isEndSpikesEnabled() { return endSpikesEnabled; }
public List<String> getEndSpikeWorlds() { return endSpikeWorlds; }
@@ -49,16 +49,25 @@ public final class EndCityProtector implements Listener {
Collection<GeneratedStructure> structures =
chunk.getStructures(Structure.END_CITY);
if (structures.isEmpty()) return;
if (structures.isEmpty()) {
// Nothing in this chunk ever needs checking again - structures don't
// appear after generation, so this is permanently safe to skip.
chunk.getPersistentDataContainer()
.set(modifiedKey, PersistentDataType.BYTE, (byte) 1);
return;
}
if (plugin.getPluginConfig().isEndCitiesModifyWorld()) {
for (GeneratedStructure structure : structures) {
replaceGlassInBounds(chunk, structure.getBoundingBox());
}
chunk.getPersistentDataContainer()
.set(modifiedKey, PersistentDataType.BYTE, (byte) 1);
}
chunk.getPersistentDataContainer()
.set(modifiedKey, PersistentDataType.BYTE, (byte) 1);
// else: an End City is here but modify_world is off - leave unmarked so a
// future load re-checks this chunk in case modify_world gets enabled later.
// Marking it "done" here without having done anything would permanently
// skip this chunk even after the admin turns modify_world on.
} catch (Exception e) {
plugin.getLogger().warning("[AntiSeedCracker] EndCityProtector scan error"
@@ -20,6 +20,9 @@ import org.bukkit.persistence.PersistentDataType;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public final class EndSpikeProtector implements Listener {
@@ -31,7 +34,7 @@ public final class EndSpikeProtector implements Listener {
static {
PILLAR_ANGLES = new double[10];
for (int i = 0; i < 10; i++) {
PILLAR_ANGLES[i] = 2.0 * (-Math.PI + Math.PI * 0.1 * i);
PILLAR_ANGLES[i] = i * (2.0 * Math.PI / 10.0);
}
}
@@ -40,6 +43,10 @@ public final class EndSpikeProtector implements Listener {
private final AntiSeedCrackerPlugin plugin;
private final NamespacedKey modifiedKey;
/** Worlds currently running a respawn-cycle poll loop, to avoid starting one per crystal
* (all 4 respawn crystals fire {@link EntityPlaceEvent} independently). */
private final Set<UUID> pollingWorlds = ConcurrentHashMap.newKeySet();
public EndSpikeProtector(AntiSeedCrackerPlugin plugin) {
this.plugin = plugin;
this.modifiedKey = new NamespacedKey(plugin, "asc_spike_modified");
@@ -62,11 +69,16 @@ public final class EndSpikeProtector implements Listener {
if (event.getEntityType() != EntityType.END_CRYSTAL) return;
World world = event.getEntity().getWorld();
if (!shouldProcess(world)) return;
if (!plugin.getPluginConfig().isEndSpikesModifyWorld()) return;
Block placedOn = event.getBlock();
if (placedOn == null || placedOn.getType() != Material.BEDROCK) return;
if (Math.abs(placedOn.getX()) > 3 || Math.abs(placedOn.getZ()) > 3) return;
// All 4 respawn crystals fire this event independently within the same tick or two;
// without this guard each one would start its own redundant 300-attempt poll loop.
if (!pollingWorlds.add(world.getUID())) return;
world.getPersistentDataContainer().set(modifiedKey, PersistentDataType.BOOLEAN, false);
schedulePollForRespawnEnd(world, 0);
}
@@ -78,13 +90,16 @@ public final class EndSpikeProtector implements Listener {
}
private void scheduleModification(World world) {
// Checked before touching the PDC at all: if this flag were set while modify_world
// was off, turning modify_world on later would find "already modified" already true
// and permanently skip the shuffle for this world.
if (!plugin.getPluginConfig().isEndSpikesModifyWorld()) return;
Boolean alreadyModified = world.getPersistentDataContainer()
.getOrDefault(modifiedKey, PersistentDataType.BOOLEAN, false);
if (alreadyModified) return;
world.getPersistentDataContainer().set(modifiedKey, PersistentDataType.BOOLEAN, true);
if (!plugin.getPluginConfig().isEndSpikesModifyWorld()) return;
int[] heights = Arrays.copyOf(VANILLA_HEIGHTS, VANILLA_HEIGHTS.length);
for (int i = heights.length - 1; i > 0; i--) {
int j = RNG.nextInt(i + 1);
@@ -121,6 +136,7 @@ public final class EndSpikeProtector implements Listener {
private void schedulePollForRespawnEnd(World world, int attemptCount) {
if (attemptCount > 300) {
pollingWorlds.remove(world.getUID());
scheduleModification(world);
return;
}
@@ -133,6 +149,7 @@ public final class EndSpikeProtector implements Listener {
if (summoning) {
schedulePollForRespawnEnd(world, attemptCount + 1);
} else {
pollingWorlds.remove(world.getUID());
scheduleModification(world);
}
}, 20L);
@@ -68,17 +68,22 @@ public final class MapSeedInterceptor extends PacketListenerAbstract {
}
}
/**
* Matches against the real vanilla map decoration type IDs (verified against
* {@code org.bukkit.map.MapCursor.Type}: no "treasure", "pillager", or "bastion" type
* exists at all — those three never matched anything. Buried treasure maps actually use
* {@code TARGET_X}, which was previously missing here, so buried treasure locations
* (one of this plugin's own named SeedCrackerX signals) were never actually scrambled.
*/
private static boolean isStructureDecoration(MapDecorationType type) {
try {
String name = type.getName().toString().toUpperCase();
return name.contains("MANSION")
|| name.contains("MONUMENT")
|| name.contains("TARGET_X")
|| name.contains("RED_X")
|| name.contains("TREASURE")
|| name.contains("TRIAL_CHAMBERS")
|| name.contains("VILLAGE")
|| name.contains("PILLAGER")
|| name.contains("BASTION")
|| name.contains("JUNGLE_TEMPLE")
|| name.contains("SWAMP_HUT");
} catch (Exception ignored) {
@@ -1,5 +1,6 @@
package me.sunsetrqsv.antiseedcracker.scheduler;
import io.papermc.paper.threadedregions.scheduler.ScheduledTask;
import me.sunsetrqsv.antiseedcracker.util.PlatformUtil;
import org.bukkit.Location;
import org.bukkit.World;
@@ -15,13 +16,9 @@ public final class FoliaSchedulerUtil {
public static CancelableTask scheduleAsyncRepeating(Plugin plugin, Runnable task,
long initialDelayMs, long periodMs) {
if (PlatformUtil.IS_PAPER) {
Object handle = plugin.getServer().getAsyncScheduler()
ScheduledTask handle = plugin.getServer().getAsyncScheduler()
.runAtFixedRate(plugin, t -> task.run(), initialDelayMs, periodMs, TimeUnit.MILLISECONDS);
return () -> {
try {
handle.getClass().getMethod("cancel").invoke(handle);
} catch (Exception ignored) {}
};
return handle::cancel;
}
long delayTicks = Math.max(1L, initialDelayMs / 50L);
long periodTicks = Math.max(1L, periodMs / 50L);
@@ -113,13 +110,9 @@ public final class FoliaSchedulerUtil {
long initialDelayTicks,
long periodTicks) {
if (PlatformUtil.IS_PAPER) {
Object handle = plugin.getServer().getGlobalRegionScheduler()
ScheduledTask handle = plugin.getServer().getGlobalRegionScheduler()
.runAtFixedRate(plugin, t -> task.run(), initialDelayTicks, periodTicks);
return () -> {
try {
handle.getClass().getMethod("cancel").invoke(handle);
} catch (Exception ignored) {}
};
return handle::cancel;
}
org.bukkit.scheduler.BukkitTask bt = plugin.getServer().getScheduler()
.runTaskTimer(plugin, task, initialDelayTicks, periodTicks);
@@ -17,7 +17,7 @@ public final class SeedRotationTask implements Runnable {
if (handle != null) {
handle.cancel();
}
long periodMs = (plugin.getPluginConfig().getSeedRotationIntervalTicks() / 20L) * 1000L;
long periodMs = plugin.getPluginConfig().getSeedRotationIntervalSeconds() * 1000L;
handle = FoliaSchedulerUtil.scheduleAsyncRepeating(plugin, this, periodMs, periodMs);
}
-10
View File
@@ -35,11 +35,6 @@ structure_recon_monitor:
enabled: true
threshold: 6
window_seconds: 900
# Leave empty to use the built-in default list (shipwrecks, desert/jungle temples,
# swamp huts, pillager outposts, ocean monuments, igloos, buried treasure, End
# Cities, ancient cities — the structures SeedCrackerX-style tools accumulate bits
# from). Deliberately excludes villages/mineshafts/ruined portals since those are
# far too common to be a useful signal and would just generate noise.
tracked_structures: []
eye_of_ender_protection:
@@ -70,11 +65,6 @@ extra_blocked_commands:
- "seedfind"
- "worldseedinfo"
# Logs (never blocks — same non-punishing philosophy as structure_recon_monitor) when
# a player triggers several blocked /seed-family commands in a short window: a pattern
# consistent with a script or macro probing for a command-blocker bypass (trying many
# command variants back to back) rather than a player who just typed /seed once out of
# curiosity.
command_probing_monitor:
enabled: true
threshold: 5
+1 -1
View File
@@ -1,5 +1,5 @@
name: AntiSeedCracker
version: '3.5.0.1'
version: '3.5.0.2'
main: me.sunsetrqsv.antiseedcracker.AntiSeedCrackerPlugin
api-version: '26.1'
folia-supported: true