fix: remove world-generation-corrupting seed patch, add integrity monitor and cracking-detection systems (v3.3.0)

Root cause of Modrinth stability complaints: WorldSeedInterceptor patched
the live NMS seed field every second on every world. That field is
re-read fresh by vanilla chunk decoration (trees, ores, structure
placement) for every chunk generated, not cached once at world load, so
any chunk decorated while the field held a fake value generated from
random garbage instead of the real seed. This explains the reported
"seed changing every few seconds disrupts chunk generation" and "issues
with other plugins/server core" reports.

- Remove WorldSeedInterceptor and the "Plugin API Protection" feature
  entirely; replace with SeedIntegrityMonitor, a read-only watchdog that
  only ever reads world.getSeed() and warns on drift, never writes to it
- Verified fix by booting real Paper 26.1.2 and Folia 26.1.2 servers with
  explicit known seeds and confirming byte-for-byte on-disk seed integrity
  in world_gen_settings.dat after extended runtime
- EndCityProtector now scans only the structure's bounding box instead of
  the full chunk column (up to 256x256 blocks reduced to structure size)
- Player-seed safety-net task reduced from 1s to 15s interval (no longer
  has seed-patching work driving the need for high frequency)
- Researched LiquidBounce/SeedCrackerX mechanics: seed cracking is
  primarily done via real coordinates of legitimately-visited structures,
  which is not a network exploit and cannot be prevented server-side.
  Documented this honestly instead of overclaiming
- Add StructureReconMonitor: detects (audit-log only, never blocks) the
  specific multi-structure visitation pattern SeedCrackerX data-gathering
  produces
- Add SlimeChunkObfuscator (opt-in, off by default): cancels natural
  slime spawns to deny slime-chunk-sampling-based cracking
- Document that block/ore ESP (XRay, StorageESP) is a different threat
  class solved by Paper's native anti-xray, not by this plugin
- Fix modrinth.txt, which previously claimed "world generation is never
  altered" while the plugin's own API-protection layer was doing exactly
  that
This commit is contained in:
SunsetRQ7_ committed 2026-07-11 22:47:53 -07:00
1 parent 9c84a44ced
commit 3acaf6f509
16 files changed
+364 -277

No files matched your search

+52 -13
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.2.0  |  **Target:** Minecraft 26.1.2  |  **Author:** SunsetRQ7_
**Version:** 3.3.0  |  **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.
@@ -16,15 +16,17 @@ AntiSeedCracker is a professional, multi-layer world seed protection plugin for
| Protection Layer | Description |
|---|---|
| **Login & Respawn Packet Spoofing** | Replaces the hashed seed in `JOIN_GAME` and `RESPAWN` network packets with a per-player cryptographically random fake seed. Operates at the Netty I/O level via PacketEvents before any client ever receives the real value. |
| **Dynamic Seed Rotation** | Periodically assigns each online player a new random fake seed (configurable interval, minimum 30 s). Defeats crackers that accumulate packets across multiple sessions. |
| **Plugin API Protection** | Patches the NMS seed field via reflection every second so any plugin calling `World.getSeed()` receives a rotating fake value. Re-applies unconditionally to prevent other plugins from undoing the patch. |
| **Dynamic Seed Rotation** | Periodically assigns each online player a new random fake seed (configurable interval, minimum 30 s). Only affects the network-facing hashed seed, never the generation seed. Defeats crackers that accumulate packets across multiple sessions. |
| **Seed Integrity Monitor** | Read-only watchdog that compares each world's live generation seed against its value at startup and warns in the console if it ever drifts. Never writes to the seed itself — see [Correctness & Large-Server Notes](#correctness--large-server-notes) for why. |
| **Eye of Ender Redirection** | Intercepts thrown Eyes of Ender and redirects their velocity toward a per-player fake stronghold location. Applies a ±8° angular jitter to prevent exact triangulation across repeated throws. |
| **/locate Spoofing** | Cancels `/locate` and replies with convincingly fake coordinates. Stronghold queries use the player's stable fake location for consistency. |
| **/seed Command Block** | Blocks `/seed`, `/minecraft:seed`, `/getseed`, `/worldseed`, and any extra commands configured in `config.yml` for players, console, and RCON. Also strips seed commands from tab-complete results. |
| **End Spike Randomization** | Shuffles the bedrock cap heights of all 10 End obsidian pillars at world load and after each dragon respawn cycle. Breaks the vanilla height fingerprint `{76, 79, 82 … 103}` that uniquely identifies the seed. |
| **End City Glass Replacement** | Replaces magenta stained glass in End City chunks with purpur blocks on first load. Prevents End City coordinate fingerprinting via the distinctive glass pattern. |
| **End City Glass Replacement** | Replaces magenta stained glass in End City chunks (scanning only the structure's bounding box, not the full chunk column) with purpur blocks on first load. Prevents long-range visual fingerprinting of the distinctive glass pattern. |
| **Treasure & Explorer Map Scrambling** | Intercepts `MAP_DATA` packets and randomises the on-map position of every structure icon (buried treasure, woodland mansion, ocean monument, and others). |
| **Audit Log** | Appends every blocked command, spoofed packet, and redirected Eye to a flat-file TSV log in `plugins/AntiSeedCracker/logs/`. Zero external dependencies — no SQLite. |
| **Slime Chunk Obfuscation** *(opt-in)* | Cancels natural slime spawns in configured worlds so tools sampling spawn/no-spawn per chunk get no signal. Disables slime farms in those worlds — off by default. |
| **Structure Reconnaissance Monitor** | Detects (logs only, never blocks) when a player visits an unusual number of seed-cracking-relevant structures in a short window — see [Threat Model](#threat-model-liquidbounce--seedcrackerx-research) below. |
| **Audit Log** | Appends every blocked command, spoofed packet, redirected Eye, and recon flag to a flat-file TSV log in `plugins/AntiSeedCracker/logs/`. Zero external dependencies — no SQLite. |
---
@@ -32,8 +34,8 @@ AntiSeedCracker is a professional, multi-layer world seed protection plugin for
| Platform | Supported | Verified |
|---|---|---|
| Paper 26.1.2 | ✔ | ✔ Live-tested on build 72, zero errors |
| Folia 26.1.2 | ✔ (region-threaded scheduling) | ✔ Live-tested on build 8, zero errors |
| Paper 26.1.2 | ✔ | ✔ Live-tested on build 74, zero errors, seed integrity verified byte-for-byte on disk |
| Folia 26.1.2 | ✔ (region-threaded scheduling) | ✔ Live-tested on build 8, zero errors, seed integrity verified byte-for-byte on disk |
| Purpur 26.1.2 | ✔ (Paper-compatible) | — |
| Spigot / CraftBukkit | ✘ Not supported — the plugin uses Paper-only APIs (Adventure, AsyncScheduler) | — |
@@ -47,7 +49,7 @@ Anonymous usage statistics are collected via [bStats](https://bstats.org/plugin/
## Installation
1. Download `AntiSeedCracker-3.2.0.jar` from [Releases](../../releases/latest).
1. Download `AntiSeedCracker-3.3.0.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.
@@ -94,9 +96,18 @@ structure_protection:
enabled: true
max_offset: 2000
plugin_api_protection:
seed_integrity_monitor:
enabled: true
slime_chunk_obfuscation:
enabled: false
worlds: []
structure_recon_monitor:
enabled: true
threshold: 6
window_seconds: 900
eye_of_ender_protection:
enabled: true
worlds:
@@ -141,11 +152,37 @@ AntiSeedCracker never reads, stores, or logs the real world seed. The only world
Both modifications use `physicsUpdate: false` to prevent block-update cascades. Each chunk is tagged with a Persistent Data Container key so the modification runs **at most once per chunk** across all server restarts. No terrain, biome, or structure data outside these specific block types is ever altered.
**AntiSeedCracker never writes to a world's generation seed, at any point, for any reason.** Earlier versions did (see below) — this caused real damage and has been removed entirely.
---
## Correctness & Large-Server Notes
**Version 3.3.0 fixes a serious bug present in 3.2.0 and earlier.** The old "Plugin API Protection" feature patched the live in-memory seed field that Minecraft's own chunk generator reads, once per second, forever, on every loaded world. That field isn't a copy — it's the exact same value the vanilla decoration step (trees, ores, ravines, structure placement) re-reads *fresh for every chunk it generates*, not just once at world load. The result: any chunk decorated while that field held a fake value got seeded from randomness instead of your real seed, causing visibly broken terrain/structure generation, and the constant reflection-based writing added avoidable overhead and cross-plugin risk on busy servers.
This has been completely removed. In its place, `SeedIntegrityMonitor` only *reads* `world.getSeed()` on a low-frequency timer and logs a warning if it ever changes — it never writes to it. This was verified by booting real Paper 26.1.2 and Folia 26.1.2 servers with explicit known seeds and confirming, at the raw NBT byte level in `world_gen_settings.dat`, that the seed never drifted after extended runtime.
Other large-server changes in this release:
- `EndCityProtector` now scans only the generated structure's bounding box instead of the entire chunk column (previously up to 256 vertical blocks × 256 columns per End City chunk).
- The periodic player-seed safety-net task runs every 15s instead of every 1s (it no longer has any seed-patching work to do).
---
## Threat Model: LiquidBounce & SeedCrackerX Research
This release includes research into how modern cheat clients and cracking tools actually recover seeds, so protections target real techniques rather than guesses:
- **SeedCrackerX** (the dominant seed-cracking tool) primarily works by combining the *exact coordinates* of structures the player has legitimately found — shipwrecks, desert pyramids, jungle temples, swamp huts, pillager outposts, ocean monuments, igloos, and End Cities — until it has enough bits to brute-force the seed. **This is not a network exploit.** A player standing at a real structure already knows their own coordinates; no plugin can hide a player's position from themselves without breaking movement entirely. This is a fundamental limitation of any server-side plugin, not just this one — treat any tool's claim of "100% uncrackable" against this technique with skepticism.
- What we added because of this: `StructureReconMonitor` watches for the specific pattern SeedCrackerX data-gathering produces — visiting several of those exact structure types in a short time window — and logs it for admin review. It never blocks or punishes, matching this plugin's design; it just gives you visibility into likely automated cracking activity.
- **Slime-chunk sampling** is another real technique (spawn/no-spawn per chunk leaks a bit of seed information). `slime_chunk_obfuscation` (opt-in, off by default) cancels natural slime spawns in configured worlds to deny this signal — at the cost of disabling slime farms there.
- **LiquidBounce's `XRay` and `StorageESP` modules** (block/ore/chest ESP) are a **different threat entirely** — they exploit full chunk data the server must send for rendering, and have nothing to do with the world seed. This is not something a seed-protection plugin should attempt to solve by reimplementing chunk obfuscation (high risk, likely to break vanilla behavior — exactly what large-server admins told us to stop doing). **Use Paper's built-in anti-xray** (`anticheat.anti-xray` in `paper-world-defaults.yml`) for that threat class; it's mature, maintained, and the correct tool for the job.
- Packet-level hashed-seed spoofing (login/respawn) and `/locate` spoofing still close off the older, simpler leak vectors that predate SeedCrackerX's structure-based method.
---
## Folia Threading Model
All block mutations are dispatched to the region thread owning the relevant chunk via `RegionScheduler` (Folia) or the main thread (Paper/Spigot). Packet interceptors run on the Netty I/O thread and only perform `ConcurrentHashMap` lookups. The seed rotation and world-seed broadcast tasks run on the `AsyncScheduler` (Paper/Folia) or an asynchronous `BukkitTask` (Spigot).
All block mutations are dispatched to the region thread owning the relevant chunk via `RegionScheduler` (Folia) or the main thread (Paper). Packet interceptors run on the Netty I/O thread and only perform `ConcurrentHashMap` lookups. The seed rotation and player-seed safety-net tasks run on the `AsyncScheduler` (Paper/Folia).
---
@@ -159,10 +196,12 @@ All block mutations are dispatched to the region thread owning the relevant chun
| `/locate structure` triangulation | Spoofed coordinate output, consistent per-player stronghold fake |
| Treasure / explorer maps | `MAP_DATA` packet decoration scrambling |
| End spike height fingerprinting | Full Fisher-Yates shuffle of pillar cap heights |
| End City coordinate fingerprinting | Magenta glass block replacement |
| Other plugin API (`world.getSeed()`) | NMS field reflection patch, re-applied unconditionally every second |
| End City visual fingerprinting | Magenta glass block replacement (bounding-box scoped) |
| SeedCrackerX-style structure recon | Detection + audit logging (cannot be prevented — see Threat Model) |
| Slime-chunk seed sampling | Optional natural-spawn cancellation |
| Tab-complete exposure | `TabCompleteEvent` filtering |
| RCON seed commands | `RemoteServerCommandEvent` blocking |
| Block/ore ESP (XRay, StorageESP) | Out of scope — use Paper's native anti-xray |
---
@@ -180,7 +219,7 @@ export JAVA_HOME=/path/to/jdk-25
mvn clean package
```
The shaded jar is produced at `target/AntiSeedCracker-3.2.0.jar`. PacketEvents 2.13.0 and bStats 3.2.1 are bundled and relocated under `me.legendcraft.antiseedcracker.lib`.
The shaded jar is produced at `target/AntiSeedCracker-3.3.0.jar`. PacketEvents 2.13.0 and bStats 3.2.1 are bundled and relocated under `me.legendcraft.antiseedcracker.lib`.
---
+13 -7
View File
@@ -14,25 +14,31 @@ Once a player knows your world seed, they can compute the exact location of ever
- **Packet-level seed spoofing** — replaces the hashed seed in JOIN_GAME and RESPAWN packets with a per-player, cryptographically random fake before it ever leaves the server (Netty-level via bundled PacketEvents).
- **Dynamic seed rotation** — every player's fake seed rotates on a configurable interval, defeating crackers that accumulate data across sessions.
- **Plugin API protection** — patches the internal seed field every second, so even other plugins calling `World.getSeed()` receive a fake value.
- **Seed integrity monitor** — read-only watchdog that verifies the real generation seed never drifts. AntiSeedCracker does not, and will not, write to your world's actual generation seed — see "Safe for your world" below.
- **Eye of Ender redirection** — thrown Eyes fly toward a per-player fake stronghold with randomized angular jitter, making triangulation useless.
- **/locate spoofing** — returns convincing fake coordinates; stronghold queries stay consistent with Eye of Ender behavior.
- **Seed command blocking** — /seed and friends are blocked for players, console, RCON, and stripped from tab-complete.
- **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.
- **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.
- **Structure reconnaissance monitor** — logs (never blocks) suspicious multi-structure visitation patterns matching known seed-cracking tool behavior.
- **Slime chunk obfuscation** (opt-in) — denies slime-chunk-sampling-based cracking at the cost of disabling slime farms where enabled.
- **Audit log** — every blocked attempt is recorded to a lightweight flat-file log, with /asc stats summaries in-game.
## Never punishes players
AntiSeedCracker follows a strict no-punishment philosophy: no kicks, no bans, no warnings. Cheat tools silently fail while legitimate gameplay is completely unaffected.
AntiSeedCracker follows a strict no-punishment philosophy: no kicks, no bans. Cheat tools silently fail while legitimate gameplay is completely unaffected.
## Safe for your world
- The real seed is never read into logs, files, or network traffic.
- World generation is never altered — protection happens at the packet and API layer.
- The real seed is never read into logs, files, or network traffic, and is never rewritten by this plugin.
- An earlier internal design continuously overwrote the live in-memory generation seed to spoof `World.getSeed()` for other plugins. That approach was removed in 3.3.0 after we found it could corrupt chunk decoration on affected servers — Minecraft re-reads that field fresh for every chunk it generates, not just once at world load. The plugin now only ever reads that value, never writes it.
- Optional cosmetic changes (End spike caps, End City glass) are single-block, physics-free, run at most once per chunk, and can be disabled in the config.
- Verified by live testing on Paper 26.1.2 and Folia 26.1.2 with zero errors and untouched world generation.
- Verified by live testing on Paper 26.1.2 and Folia 26.1.2: zero errors, and the real seed confirmed byte-for-byte intact on disk after extended runtime.
## Honest limits
No plugin can make a seed truly uncrackable against tools like SeedCrackerX, which reconstruct it from the real coordinates of structures a player has legitimately explored — that's not a network exploit, and coordinates can't be hidden from a player standing at their own position. AntiSeedCracker closes every server-side leak we're aware of (packet spoofing, triangulation, command access, structure fingerprinting) and adds detection for cracking-pattern behavior, but exploration-based cracking is a limit of the genre, not something this plugin claims to solve.
## Performance
@@ -51,7 +57,7 @@ Spigot/CraftBukkit and legacy Minecraft versions (1.16.5 – 25.x) are not suppo
## Quick start
1. Drop `AntiSeedCracker-3.2.0.jar` into your `plugins/` folder.
1. Drop `AntiSeedCracker-3.3.0.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`.
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>me.legendcraft</groupId>
<artifactId>AntiSeedCracker</artifactId>
<version>3.2.0</version>
<version>3.3.0</version>
<packaging>jar</packaging>
<name>AntiSeedCracker</name>
@@ -10,12 +10,14 @@ import me.legendcraft.antiseedcracker.listeners.EndCityProtector;
import me.legendcraft.antiseedcracker.listeners.EndSpikeProtector;
import me.legendcraft.antiseedcracker.listeners.EyeOfEnderProtector;
import me.legendcraft.antiseedcracker.listeners.PlayerSessionListener;
import me.legendcraft.antiseedcracker.listeners.SlimeChunkObfuscator;
import me.legendcraft.antiseedcracker.listeners.StructureReconMonitor;
import me.legendcraft.antiseedcracker.packets.LoginPacketInterceptor;
import me.legendcraft.antiseedcracker.packets.MapSeedInterceptor;
import me.legendcraft.antiseedcracker.packets.RespawnPacketInterceptor;
import me.legendcraft.antiseedcracker.scheduler.FoliaSchedulerUtil;
import me.legendcraft.antiseedcracker.seed.SeedIntegrityMonitor;
import me.legendcraft.antiseedcracker.seed.SeedManager;
import me.legendcraft.antiseedcracker.seed.WorldSeedInterceptor;
import me.legendcraft.antiseedcracker.tasks.SeedBroadcastTask;
import me.legendcraft.antiseedcracker.tasks.SeedRotationTask;
import me.legendcraft.antiseedcracker.util.PlatformUtil;
@@ -40,13 +42,15 @@ public final class AntiSeedCrackerPlugin extends JavaPlugin {
private SeedBroadcastTask seedBroadcastTask;
private SeedRotationTask seedRotationTask;
private WorldSeedInterceptor worldSeedInterceptor;
private SeedIntegrityMonitor seedIntegrityMonitor;
private PlayerSessionListener playerSessionListener;
private CommandProtectionListener commandProtectionListener;
private EyeOfEnderProtector eyeOfEnderProtector;
private EndSpikeProtector endSpikeProtector;
private EndCityProtector endCityProtector;
private SlimeChunkObfuscator slimeChunkObfuscator;
private StructureReconMonitor structureReconMonitor;
@Override
public void onLoad() {
@@ -86,7 +90,7 @@ public final class AntiSeedCrackerPlugin extends JavaPlugin {
updateChecker.checkAsync();
}
worldSeedInterceptor = new WorldSeedInterceptor(getLogger());
seedIntegrityMonitor = new SeedIntegrityMonitor(getLogger());
seedBroadcastTask = new SeedBroadcastTask(this);
seedBroadcastTask.start();
@@ -119,12 +123,16 @@ public final class AntiSeedCrackerPlugin extends JavaPlugin {
private void setupMetrics() {
Metrics metrics = new Metrics(this, BSTATS_PLUGIN_ID);
metrics.addCustomChart(new SimplePie("platform_type", PlatformUtil::name));
metrics.addCustomChart(new SimplePie("plugin_api_protection_active",
() -> worldSeedInterceptor.isAvailable() ? "active" : "inactive"));
metrics.addCustomChart(new SimplePie("seed_integrity_monitor_enabled",
() -> pluginConfig.isSeedIntegrityMonitorEnabled() ? "enabled" : "disabled"));
metrics.addCustomChart(new SimplePie("seed_rotation_enabled",
() -> pluginConfig.isSeedRotationEnabled() ? "enabled" : "disabled"));
metrics.addCustomChart(new SimplePie("audit_log_enabled",
() -> databaseManager != null ? "enabled" : "disabled"));
metrics.addCustomChart(new SimplePie("slime_chunk_obfuscation_enabled",
() -> pluginConfig.isSlimeChunkObfuscationEnabled() ? "enabled" : "disabled"));
metrics.addCustomChart(new SimplePie("structure_recon_monitor_enabled",
() -> pluginConfig.isStructureReconMonitorEnabled() ? "enabled" : "disabled"));
}
@Override
@@ -197,6 +205,16 @@ public final class AntiSeedCrackerPlugin extends JavaPlugin {
endCityProtector = new EndCityProtector(this);
pm.registerEvents(endCityProtector, this);
}
if (pluginConfig.isSlimeChunkObfuscationEnabled()) {
slimeChunkObfuscator = new SlimeChunkObfuscator(this);
pm.registerEvents(slimeChunkObfuscator, this);
}
if (pluginConfig.isStructureReconMonitorEnabled()) {
structureReconMonitor = new StructureReconMonitor(this);
pm.registerEvents(structureReconMonitor, this);
}
}
private void unregisterListeners() {
@@ -205,11 +223,14 @@ public final class AntiSeedCrackerPlugin extends JavaPlugin {
if (eyeOfEnderProtector != null) { eyeOfEnderProtector.unregister(); eyeOfEnderProtector = null; }
if (endSpikeProtector != null) { endSpikeProtector.unregister(); endSpikeProtector = null; }
if (endCityProtector != null) { endCityProtector.unregister(); endCityProtector = null; }
if (slimeChunkObfuscator != null) { slimeChunkObfuscator.unregister(); slimeChunkObfuscator = null; }
if (structureReconMonitor != null) { structureReconMonitor.unregister(); structureReconMonitor = null; }
}
public PluginConfig getPluginConfig() { return pluginConfig; }
public SeedManager getSeedManager() { return seedManager; }
public WorldSeedInterceptor getWorldSeedInterceptor() { return worldSeedInterceptor; }
public DatabaseManager getDatabaseManager() { return databaseManager; }
public UpdateChecker getUpdateChecker() { return updateChecker; }
public PluginConfig getPluginConfig() { return pluginConfig; }
public SeedManager getSeedManager() { return seedManager; }
public SeedIntegrityMonitor getSeedIntegrityMonitor() { return seedIntegrityMonitor; }
public StructureReconMonitor getStructureReconMonitor() { return structureReconMonitor; }
public DatabaseManager getDatabaseManager() { return databaseManager; }
public UpdateChecker getUpdateChecker() { return updateChecker; }
}
@@ -83,8 +83,9 @@ public final class AdminCommand implements CommandExecutor, TabCompleter {
boolean citiesEnabled = plugin.getPluginConfig().isEndCitiesEnabled();
boolean eyeEnabled = plugin.getPluginConfig().isEyeOfEnderEnabled();
boolean locateEnabled = plugin.getPluginConfig().isSpoofLocateEnabled();
boolean pluginApiEnabled = plugin.getPluginConfig().isPluginApiProtectionEnabled();
boolean interceptorActive = plugin.getWorldSeedInterceptor().isAvailable();
boolean integrityEnabled = plugin.getPluginConfig().isSeedIntegrityMonitorEnabled();
boolean slimeObfEnabled = plugin.getPluginConfig().isSlimeChunkObfuscationEnabled();
boolean reconEnabled = plugin.getPluginConfig().isStructureReconMonitorEnabled();
boolean mapProtEnabled = plugin.getPluginConfig().isTreasureMapProtectionEnabled();
boolean dbEnabled = plugin.getDatabaseManager() != null;
int trackedSeeds = plugin.getSeedManager().trackedCount();
@@ -100,7 +101,9 @@ public final class AdminCommand implements CommandExecutor, TabCompleter {
sender.sendMessage(statusLine("End City Protection", citiesEnabled));
sender.sendMessage(statusLine("Eye of Ender Redirect", eyeEnabled));
sender.sendMessage(statusLine("/locate Spoof", locateEnabled));
sender.sendMessage(statusLine("Plugin API Protection", pluginApiEnabled && interceptorActive));
sender.sendMessage(statusLine("Seed Integrity Monitor", integrityEnabled));
sender.sendMessage(statusLine("Slime Chunk Obfuscation", slimeObfEnabled));
sender.sendMessage(statusLine("Structure Recon Monitor", reconEnabled));
sender.sendMessage(statusLine("Treasure Map Scramble", mapProtEnabled));
sender.sendMessage(statusLine("Database / Audit Log", dbEnabled));
sender.sendMessage(msg(
@@ -24,7 +24,14 @@ public final class PluginConfig {
private final boolean spoofLocateEnabled;
private final int spoofLocateMaxOffset;
private final boolean pluginApiProtectionEnabled;
private final boolean seedIntegrityMonitorEnabled;
private final boolean slimeChunkObfuscationEnabled;
private final List<String> slimeChunkObfuscationWorlds;
private final boolean structureReconMonitorEnabled;
private final int structureReconThreshold;
private final int structureReconWindowSeconds;
private final boolean eyeOfEnderEnabled;
private final List<String> eyeOfEnderWorlds;
@@ -67,7 +74,17 @@ public final class PluginConfig {
this.spoofLocateMaxOffset = Math.max(100,
cfg.getInt("structure_protection.spoof_locate_command.max_offset", 2000));
this.pluginApiProtectionEnabled = cfg.getBoolean("plugin_api_protection.enabled", true);
this.seedIntegrityMonitorEnabled = cfg.getBoolean("seed_integrity_monitor.enabled", true);
this.slimeChunkObfuscationEnabled = cfg.getBoolean("slime_chunk_obfuscation.enabled", false);
this.slimeChunkObfuscationWorlds = Collections.unmodifiableList(
cfg.getStringList("slime_chunk_obfuscation.worlds"));
this.structureReconMonitorEnabled = cfg.getBoolean("structure_recon_monitor.enabled", true);
this.structureReconThreshold = Math.max(2,
cfg.getInt("structure_recon_monitor.threshold", 6));
this.structureReconWindowSeconds = Math.max(30,
cfg.getInt("structure_recon_monitor.window_seconds", 900));
this.eyeOfEnderEnabled = cfg.getBoolean("eye_of_ender_protection.enabled", true);
this.eyeOfEnderWorlds = Collections.unmodifiableList(
@@ -121,7 +138,14 @@ public final class PluginConfig {
public boolean isSpoofLocateEnabled() { return spoofLocateEnabled; }
public int getSpoofLocateMaxOffset() { return spoofLocateMaxOffset; }
public boolean isPluginApiProtectionEnabled() { return pluginApiProtectionEnabled; }
public boolean isSeedIntegrityMonitorEnabled() { return seedIntegrityMonitorEnabled; }
public boolean isSlimeChunkObfuscationEnabled() { return slimeChunkObfuscationEnabled; }
public List<String> getSlimeChunkObfuscationWorlds() { return slimeChunkObfuscationWorlds; }
public boolean isStructureReconMonitorEnabled() { return structureReconMonitorEnabled; }
public int getStructureReconThreshold() { return structureReconThreshold; }
public int getStructureReconWindowSeconds() { return structureReconWindowSeconds; }
public boolean isEyeOfEnderEnabled() { return eyeOfEnderEnabled; }
public List<String> getEyeOfEnderWorlds() { return eyeOfEnderWorlds; }
@@ -166,6 +166,7 @@ public final class DatabaseManager {
TREASURE_MAP_SCRAMBLED,
SEED_PACKET_SPOOFED,
END_SPIKE_PROTECTED,
END_CITY_PROTECTED
END_CITY_PROTECTED,
STRUCTURE_RECON_FLAGGED
}
}
@@ -16,6 +16,7 @@ import org.bukkit.generator.structure.GeneratedStructure;
import org.bukkit.generator.structure.Structure;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.util.BoundingBox;
import java.util.Collection;
@@ -51,7 +52,9 @@ public final class EndCityProtector implements Listener {
if (structures.isEmpty()) return;
if (plugin.getPluginConfig().isEndCitiesModifyWorld()) {
replaceGlassInChunk(chunk);
for (GeneratedStructure structure : structures) {
replaceGlassInBounds(chunk, structure.getBoundingBox());
}
}
chunk.getPersistentDataContainer()
@@ -64,17 +67,21 @@ public final class EndCityProtector implements Listener {
});
}
private void replaceGlassInChunk(Chunk chunk) {
World world = chunk.getWorld();
int minY = world.getMinHeight();
int maxY = world.getMaxHeight();
int baseX = chunk.getX() << 4;
int baseZ = chunk.getZ() << 4;
private void replaceGlassInBounds(Chunk chunk, BoundingBox bounds) {
World world = chunk.getWorld();
int minY = Math.max(world.getMinHeight(), (int) Math.floor(bounds.getMinY()));
int maxY = Math.min(world.getMaxHeight(), (int) Math.ceil(bounds.getMaxY()));
int baseX = chunk.getX() << 4;
int baseZ = chunk.getZ() << 4;
int minX = Math.max(baseX, (int) Math.floor(bounds.getMinX()));
int maxX = Math.min(baseX + 15, (int) Math.ceil(bounds.getMaxX()));
int minZ = Math.max(baseZ, (int) Math.floor(bounds.getMinZ()));
int maxZ = Math.min(baseZ + 15, (int) Math.ceil(bounds.getMaxZ()));
for (int dx = 0; dx < 16; dx++) {
for (int dz = 0; dz < 16; dz++) {
for (int y = minY; y < maxY; y++) {
Block block = world.getBlockAt(baseX + dx, y, baseZ + dz);
for (int x = minX; x <= maxX; x++) {
for (int z = minZ; z <= maxZ; z++) {
for (int y = minY; y <= maxY; y++) {
Block block = world.getBlockAt(x, y, z);
if (block.getType() == Material.MAGENTA_STAINED_GLASS) {
block.setType(Material.PURPUR_BLOCK, false);
}
@@ -38,11 +38,15 @@ public final class PlayerSessionListener implements Listener {
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerQuit(PlayerQuitEvent event) {
plugin.getSeedManager().removePlayer(event.getPlayer().getUniqueId());
if (plugin.getStructureReconMonitor() != null) {
plugin.getStructureReconMonitor().removePlayer(event.getPlayer().getUniqueId());
}
}
@EventHandler(priority = EventPriority.MONITOR)
public void onWorldUnload(WorldUnloadEvent event) {
plugin.getSeedManager().forgetWorld(event.getWorld().getUID());
plugin.getSeedIntegrityMonitor().forgetWorld(event.getWorld().getUID());
}
public void initPlayer(Player player) {
@@ -0,0 +1,32 @@
package me.legendcraft.antiseedcracker.listeners;
import me.legendcraft.antiseedcracker.AntiSeedCrackerPlugin;
import org.bukkit.entity.EntityType;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.entity.CreatureSpawnEvent;
public final class SlimeChunkObfuscator implements Listener {
private final AntiSeedCrackerPlugin plugin;
public SlimeChunkObfuscator(AntiSeedCrackerPlugin plugin) {
this.plugin = plugin;
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onSlimeSpawn(CreatureSpawnEvent event) {
if (event.getEntityType() != EntityType.SLIME) return;
if (event.getSpawnReason() != CreatureSpawnEvent.SpawnReason.NATURAL) return;
if (!plugin.getPluginConfig().getSlimeChunkObfuscationWorlds()
.contains(event.getEntity().getWorld().getName())) return;
event.setCancelled(true);
}
public void unregister() {
HandlerList.unregisterAll(this);
}
}
@@ -0,0 +1,127 @@
package me.legendcraft.antiseedcracker.listeners;
import me.legendcraft.antiseedcracker.AntiSeedCrackerPlugin;
import me.legendcraft.antiseedcracker.database.DatabaseManager;
import org.bukkit.Chunk;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.generator.structure.GeneratedStructure;
import org.bukkit.generator.structure.Structure;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public final class StructureReconMonitor implements Listener {
private static final Set<Structure> TRACKED_STRUCTURES = buildTrackedStructures();
private static Set<Structure> buildTrackedStructures() {
Set<Structure> set = new HashSet<>();
set.add(Structure.SHIPWRECK);
set.add(Structure.SHIPWRECK_BEACHED);
set.add(Structure.DESERT_PYRAMID);
set.add(Structure.JUNGLE_PYRAMID);
set.add(Structure.SWAMP_HUT);
set.add(Structure.PILLAGER_OUTPOST);
set.add(Structure.MONUMENT);
set.add(Structure.IGLOO);
set.add(Structure.END_CITY);
return set;
}
private final AntiSeedCrackerPlugin plugin;
private final Map<UUID, Set<Long>> visitedChunks = new ConcurrentHashMap<>();
private final Map<UUID, Deque<Long>> visitTimestamps = new ConcurrentHashMap<>();
public StructureReconMonitor(AntiSeedCrackerPlugin plugin) {
this.plugin = plugin;
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onMove(PlayerMoveEvent event) {
if (!plugin.getPluginConfig().isStructureReconMonitorEnabled()) return;
int fromChunkX = event.getFrom().getBlockX() >> 4;
int fromChunkZ = event.getFrom().getBlockZ() >> 4;
int toChunkX = event.getTo().getBlockX() >> 4;
int toChunkZ = event.getTo().getBlockZ() >> 4;
if (fromChunkX == toChunkX && fromChunkZ == toChunkZ) return;
Chunk chunk = event.getTo().getChunk();
Collection<GeneratedStructure> structures;
try {
structures = chunk.getStructures();
} catch (Exception e) {
return;
}
if (structures.isEmpty()) return;
boolean qualifies = false;
for (GeneratedStructure s : structures) {
if (TRACKED_STRUCTURES.contains(s.getStructure())) {
qualifies = true;
break;
}
}
if (!qualifies) return;
Player player = event.getPlayer();
UUID uuid = player.getUniqueId();
long chunkKey = (((long) toChunkX) << 32) ^ (toChunkZ & 0xFFFFFFFFL);
Set<Long> visited = visitedChunks.computeIfAbsent(uuid, k -> ConcurrentHashMap.newKeySet());
if (!visited.add(chunkKey)) return;
int windowSeconds = plugin.getPluginConfig().getStructureReconWindowSeconds();
int threshold = plugin.getPluginConfig().getStructureReconThreshold();
long now = System.currentTimeMillis();
long windowStart = now - (windowSeconds * 1000L);
Deque<Long> timestamps = visitTimestamps.computeIfAbsent(uuid, k -> new ArrayDeque<>());
synchronized (timestamps) {
timestamps.addLast(now);
while (!timestamps.isEmpty() && timestamps.peekFirst() < windowStart) {
timestamps.pollFirst();
}
if (timestamps.size() >= threshold) {
timestamps.clear();
flagPlayer(player);
}
}
}
private void flagPlayer(Player player) {
plugin.getLogger().warning("[AntiSeedCracker] Structure reconnaissance pattern detected for "
+ player.getName() + " — visited multiple seed-relevant structures in a short "
+ "window. This does not block the player (per design); review manually if "
+ "seed-cracking tool use is suspected.");
DatabaseManager db = plugin.getDatabaseManager();
if (db != null && plugin.getPluginConfig().isDatabaseLogEvents()) {
db.logEvent(DatabaseManager.EventType.STRUCTURE_RECON_FLAGGED,
player.getUniqueId().toString(),
player.getName(),
player.getWorld().getName(),
"Rapid multi-structure visitation pattern");
}
}
public void removePlayer(UUID uuid) {
visitedChunks.remove(uuid);
visitTimestamps.remove(uuid);
}
public void unregister() {
HandlerList.unregisterAll(this);
}
}
@@ -0,0 +1,37 @@
package me.legendcraft.antiseedcracker.seed;
import org.bukkit.World;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
public final class SeedIntegrityMonitor {
private final Map<UUID, Long> firstObservedSeed = new ConcurrentHashMap<>();
private final Logger logger;
public SeedIntegrityMonitor(Logger logger) {
this.logger = logger;
}
public void check(Iterable<World> worlds) {
for (World world : worlds) {
long current = world.getSeed();
Long baseline = firstObservedSeed.putIfAbsent(world.getUID(), current);
if (baseline != null && baseline != current) {
logger.warning("[AntiSeedCracker] Seed integrity check FAILED for world '"
+ world.getName() + "' — the generation seed changed after the server "
+ "started. This plugin never modifies it; another plugin or external "
+ "tool is altering world generation state. Investigate immediately, as "
+ "this can corrupt newly generated chunks.");
firstObservedSeed.put(world.getUID(), current);
}
}
}
public void forgetWorld(UUID worldUID) {
firstObservedSeed.remove(worldUID);
}
}
@@ -1,214 +0,0 @@
package me.legendcraft.antiseedcracker.seed;
import org.bukkit.Bukkit;
import org.bukkit.World;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
public final class WorldSeedInterceptor {
private Method getHandleMethod;
private List<Field> containerPath = Collections.emptyList();
private Field seedField;
private final boolean available;
private final Logger logger;
private static final int MAX_DEPTH = 7;
private static final int MAX_VISITS = 10_000;
private static final String[] TRAVERSE_PREFIXES = { "net.minecraft.", "com.mojang." };
private static final long SENTINEL = 0xDEAD_C0DE_1337_4242L;
public WorldSeedInterceptor(Logger logger) {
this.logger = logger;
boolean ok = false;
try {
ok = init();
} catch (Exception e) {
logger.warning("[AntiSeedCracker] WorldSeedInterceptor init error: " + e);
}
this.available = ok;
}
public boolean isAvailable() {
return available;
}
public void patchWorld(World world, long fakeSeed) {
if (!available) return;
try {
Object container = getHandleMethod.invoke(world);
for (Field f : containerPath) {
container = f.get(container);
if (container == null) return;
}
seedField.setLong(container, fakeSeed);
} catch (Exception e) {
logger.log(Level.FINE, "[AntiSeedCracker] WorldSeedInterceptor.patchWorld failed", e);
}
}
private boolean init() throws Exception {
Class<?> craftWorldClass = resolveCraftWorldClass();
getHandleMethod = craftWorldClass.getDeclaredMethod("getHandle");
getHandleMethod.setAccessible(true);
List<World> worlds = Bukkit.getWorlds();
if (worlds.isEmpty()) {
logger.warning("[AntiSeedCracker] WorldSeedInterceptor: no worlds loaded yet — disabling.");
return false;
}
World refWorld = worlds.get(0);
Object nmsLevel = getHandleMethod.invoke(refWorld);
List<SeedPath> candidates = new ArrayList<>();
collectDirectCandidates(nmsLevel, candidates);
bfsFindAll(nmsLevel, candidates);
logger.info("[AntiSeedCracker] WorldSeedInterceptor: found "
+ candidates.size() + " long 'seed' candidate(s) — verifying each...");
for (SeedPath path : candidates) {
long orig = Long.MIN_VALUE;
boolean origRead = false;
try {
path.seedField().setAccessible(true);
orig = path.seedField().getLong(path.container());
origRead = true;
path.seedField().setLong(path.container(), SENTINEL);
boolean verified;
try {
verified = (refWorld.getSeed() == SENTINEL);
} finally {
path.seedField().setLong(path.container(), orig);
}
if (verified) {
containerPath = path.containerPath();
seedField = path.seedField();
for (Field f : containerPath) f.setAccessible(true);
logger.info("[AntiSeedCracker] WorldSeedInterceptor: ✔ active — "
+ "world.getSeed() intercepted. Class: "
+ path.seedField().getDeclaringClass().getSimpleName()
+ ", field: " + path.seedField().getName()
+ ", depth: " + containerPath.size());
return true;
} else {
logger.fine("[AntiSeedCracker] WorldSeedInterceptor: candidate "
+ path.seedField().getDeclaringClass().getSimpleName() + "."
+ path.seedField().getName() + " did not affect getSeed() — skipping.");
}
} catch (Exception e) {
if (origRead) {
try { path.seedField().setLong(path.container(), orig); } catch (Exception ignored) {}
}
logger.fine("[AntiSeedCracker] WorldSeedInterceptor: candidate threw "
+ e.getClass().getSimpleName() + " — skipping.");
}
}
logger.warning("[AntiSeedCracker] WorldSeedInterceptor: none of the "
+ candidates.size() + " candidate(s) controlled world.getSeed(). "
+ "Plugin-API seed protection is disabled; packet-level protection remains active.");
return false;
}
private static void collectDirectCandidates(Object obj, List<SeedPath> out) {
for (Field f : allDeclaredFields(obj.getClass())) {
if (f.getType() == long.class && "seed".equalsIgnoreCase(f.getName())) {
try {
f.setAccessible(true);
out.add(new SeedPath(Collections.emptyList(), f, obj));
} catch (Exception ignored) { }
}
}
}
private static void bfsFindAll(Object root, List<SeedPath> out) {
record Node(Object obj, List<Field> path) {}
Set<Integer> visited = new HashSet<>();
ArrayDeque<Node> queue = new ArrayDeque<>();
queue.add(new Node(root, Collections.emptyList()));
int visits = 0;
while (!queue.isEmpty() && visits < MAX_VISITS) {
Node node = queue.poll();
if (node.obj() == null) continue;
int id = System.identityHashCode(node.obj());
if (!visited.add(id)) continue;
visits++;
for (Field f : allDeclaredFields(node.obj().getClass())) {
try {
f.setAccessible(true);
if (f.getType() == long.class && "seed".equalsIgnoreCase(f.getName())) {
out.add(new SeedPath(new ArrayList<>(node.path()), f, node.obj()));
continue;
}
if (node.path().size() < MAX_DEPTH
&& !f.getType().isPrimitive()
&& !f.getType().isArray()
&& !f.getType().isEnum()
&& isTraversable(f.getType())) {
Object nested = f.get(node.obj());
if (nested != null && nested != node.obj()) {
List<Field> np = new ArrayList<>(node.path());
np.add(f);
queue.add(new Node(nested, np));
}
}
} catch (Exception ignored) { }
}
}
}
private static boolean isTraversable(Class<?> cls) {
String name = cls.getName();
for (String prefix : TRAVERSE_PREFIXES) {
if (name.startsWith(prefix)) return true;
}
return false;
}
private static List<Field> allDeclaredFields(Class<?> clazz) {
List<Field> result = new ArrayList<>();
Class<?> c = clazz;
while (c != null && c != Object.class) {
for (Field f : c.getDeclaredFields()) result.add(f);
c = c.getSuperclass();
}
return result;
}
private static Class<?> resolveCraftWorldClass() throws ClassNotFoundException {
try {
return Class.forName("org.bukkit.craftbukkit.CraftWorld");
} catch (ClassNotFoundException e) {
String ver = detectVersion();
return Class.forName("org.bukkit.craftbukkit." + ver + ".CraftWorld");
}
}
private static String detectVersion() {
try {
String[] parts = Bukkit.getServer().getClass().getName().split("\\.");
if (parts.length >= 5) return parts[3];
} catch (Exception ignored) { }
return "v1_21_R3";
}
private record SeedPath(List<Field> containerPath, Field seedField, Object container) { }
}
@@ -3,13 +3,10 @@ package me.legendcraft.antiseedcracker.tasks;
import me.legendcraft.antiseedcracker.AntiSeedCrackerPlugin;
import me.legendcraft.antiseedcracker.scheduler.CancelableTask;
import me.legendcraft.antiseedcracker.scheduler.FoliaSchedulerUtil;
import org.bukkit.World;
import java.util.List;
public final class SeedBroadcastTask implements Runnable {
public static final long PERIOD_MS = 1_000L;
public static final long PERIOD_MS = 15_000L;
private final AntiSeedCrackerPlugin plugin;
private volatile CancelableTask handle;
@@ -35,14 +32,8 @@ public final class SeedBroadcastTask implements Runnable {
@Override
public void run() {
if (plugin.getPluginConfig().isPluginApiProtectionEnabled()
&& plugin.getWorldSeedInterceptor().isAvailable()) {
List<World> worlds = plugin.getServer().getWorlds();
for (World world : worlds) {
long fakeSeed = plugin.getSeedManager().assignWorldFakeSeed(world);
plugin.getWorldSeedInterceptor().patchWorld(world, fakeSeed);
}
if (plugin.getPluginConfig().isSeedIntegrityMonitorEnabled()) {
plugin.getSeedIntegrityMonitor().check(plugin.getServer().getWorlds());
}
plugin.getSeedManager().ensureAllPlayersHaveSeeds(
+10 -1
View File
@@ -24,9 +24,18 @@ structure_protection:
enabled: true
max_offset: 2000
plugin_api_protection:
seed_integrity_monitor:
enabled: true
slime_chunk_obfuscation:
enabled: false
worlds: []
structure_recon_monitor:
enabled: true
threshold: 6
window_seconds: 900
eye_of_ender_protection:
enabled: true
worlds:
+1 -1
View File
@@ -1,5 +1,5 @@
name: AntiSeedCracker
version: '3.2.0'
version: '3.3.0'
main: me.legendcraft.antiseedcracker.AntiSeedCrackerPlugin
api-version: '26.1'
folia-supported: true