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:
1 parent
e6f73e3f05
commit
ab0ba5362e
13 files changed
+106
-45
No files matched your search
@@ -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
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
[](LICENSE)
|
||||
[](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
@@ -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`.
|
||||
|
||||
|
||||
@@ -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**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
Reference in new issue
Block a user