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:
1 parent
9c84a44ced
commit
3acaf6f509
16 files changed
+364
-277
No files matched your search
@@ -5,7 +5,7 @@
|
||||
[](LICENSE)
|
||||
[](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
@@ -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`.
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,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
|
||||
|
||||
Reference in new issue
Block a user