feat: AntiSeedCracker v3.1.0 — professional multi-layer seed protection

This commit is contained in:
SunsetRQ7_ committed 2026-06-13 01:35:50 -07:00
commit 61418c43c2
32 files changed
+2769

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
# Normalize line endings to LF in the repository; checkout as-is on all platforms
* text=auto eol=lf
# Force binary for compiled artifacts
*.jar binary
*.class binary
*.png binary
*.gif binary
+64
View File
@@ -0,0 +1,64 @@
name: Bug Report
description: Report a bug or unexpected behavior in AntiSeedCracker
labels: ["bug", "needs-triage"]
body:
- type: markdown
attributes:
value: |
Thank you for reporting a bug. Fill in every field to help us reproduce and fix the issue.
- type: input
id: plugin-version
attributes:
label: Plugin Version
placeholder: "3.1.0"
validations:
required: true
- type: input
id: server-software
attributes:
label: Server Software & Version
placeholder: "Paper 26.1.2 build 58 / Folia 26.1.2 build 8"
validations:
required: true
- type: input
id: java-version
attributes:
label: Java Version
placeholder: "Java 21.0.5 / Java 25.0.3"
validations:
required: true
- type: textarea
id: description
attributes:
label: Describe the bug
description: What happened? What did you expect to happen?
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Steps to reproduce
placeholder: |
1. Install plugin
2. Join the server
3. Run command ...
validations:
required: true
- type: textarea
id: config
attributes:
label: Your config.yml (redact any sensitive values)
render: yaml
- type: textarea
id: logs
attributes:
label: Relevant startup log or error output
render: shell
description: Paste the full startup section or the stacktrace, not just one line.
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Discussions (Questions & Ideas)
url: https://github.com/SunsetRQ7_/AntiSeedCracker/discussions
about: Ask questions, share configs, or discuss ideas here before opening an issue.
@@ -0,0 +1,42 @@
name: Feature Request
description: Suggest a new protection layer, configuration option, or improvement
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Before submitting, check whether a similar request already exists.
- type: textarea
id: bypass-vector
attributes:
label: What seed-leaking method or attack vector would this address?
description: Describe the bypass technique, exploit, or gap in current protection.
validations:
required: true
- type: textarea
id: proposal
attributes:
label: Proposed solution
description: How should AntiSeedCracker handle it? Be as specific as possible.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives you considered
description: Any other approaches you thought of, and why you prefer the one above.
- type: dropdown
id: platforms
attributes:
label: Affected platforms
multiple: true
options:
- Paper
- Purpur
- Folia
- Spigot
- All platforms
+31
View File
@@ -0,0 +1,31 @@
name: Build
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn clean package -q
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: AntiSeedCracker-${{ github.sha }}
path: target/AntiSeedCracker-*.jar
retention-days: 30
+29
View File
@@ -0,0 +1,29 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
java-version: '25'
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn clean package -q
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: target/AntiSeedCracker-*.jar
generate_release_notes: true
+14
View File
@@ -0,0 +1,14 @@
target/
*.class
*.log
*.mdmp
replay_pid*.log
hs_err_pid*.log
.idea/
*.iml
*.iws
*.ipr
.DS_Store
Thumbs.db
dependency-reduced-pom.xml
META-INF/
+37
View File
@@ -0,0 +1,37 @@
# Contributing to AntiSeedCracker
## Reporting bugs
Use the **Bug Report** issue template. Include your server software, version, Java version, full config.yml, and the complete startup log section — not just one line.
## Suggesting features
Use the **Feature Request** template. Describe the specific bypass vector or seed-leaking attack you want to address, not just the general idea.
## Questions
Post in [Discussions](../../discussions) rather than opening an issue. Issues are for confirmed bugs and accepted feature work.
## Pull requests
1. Fork the repository and create a branch: `git checkout -b feat/my-change`
2. Build and verify locally:
```sh
# Requires JDK 25
mvn clean package
```
3. Ensure zero compilation warnings (`-Xlint:deprecation` passes clean)
4. Keep the same code style as the existing sources:
- No comments in source files
- No unused imports
- Thread safety: async tasks go through `FoliaSchedulerUtil`; any block mutations run on the chunk's region thread
- All world-modifying operations must be idempotent and guarded by a Persistent Data Container marker
5. Open a PR against `main` with a clear description of what bypass vector is addressed or what bug is fixed
## Versioning
| Change | Version bump |
|---|---|
| New protection layer | Minor: 3.x.0 |
| Bug fix | Patch: 3.1.x |
| Breaking config change | Major: x.0.0 |
+176
View File
@@ -0,0 +1,176 @@
# AntiSeedCracker
**Version:** 3.1.0  |  **Target:** Minecraft 26.1.2  |  **Author:** SunsetRQ7_
AntiSeedCracker is a professional, multi-layer world seed protection plugin for Paper, Purpur, Folia, and Spigot. It blocks every known technique a player can use to recover your world seed, protecting your server from ESP clients and seed-cracking tools.
---
## Features
| 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. |
| **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. |
| **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. |
---
## Compatibility
| Platform | Supported |
|---|---|
| Paper 26.1.2 | ✔ |
| Purpur 26.1.2 | ✔ |
| Folia 26.1.2 | ✔ (region-threaded scheduling) |
| Spigot 26.1.2 | ✔ (BukkitScheduler fallback) |
**Runtime:** Java 25 or higher is required.
---
## Installation
1. Download `AntiSeedCracker-3.1.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.
5. Run `/asc reload` to apply changes without restarting.
---
## Commands
| Command | Description | Permission |
|---|---|---|
| `/asc reload` | Reloads config and restarts all protection tasks | `antiseedcracker.admin` |
| `/asc status` | Shows which protection layers are active | `antiseedcracker.admin` |
| `/asc stats` | Displays aggregate event counts from the audit log | `antiseedcracker.admin` |
| `/asc info` | Shows version, author, platform, and update status | `antiseedcracker.admin` |
The `antiseedcracker.admin` permission defaults to server operators.
---
## Configuration
```yaml
seed_obfuscation:
intercept_login: true
intercept_respawn: true
seed_rotation:
enabled: true
interval_seconds: 60
structure_protection:
end_spikes:
enabled: true
worlds:
- world_the_end
modify_world: true
end_cities:
enabled: true
worlds:
- world_the_end
modify_world: true
spoof_locate_command:
enabled: true
max_offset: 2000
plugin_api_protection:
enabled: true
eye_of_ender_protection:
enabled: true
worlds:
- world
fake_stronghold_min_distance: 800
fake_stronghold_max_distance: 4000
update_checker:
enabled: true
modrinth_id: "YOUR_MODRINTH_PROJECT_ID"
database:
enabled: true
log_events: true
max_event_age_days: 30
treasure_map_protection:
enabled: true
extra_blocked_commands:
- "seedcracker"
- "seedfind"
- "worldseedinfo"
```
### `modify_world` Warning
Setting `modify_world: true` for End spikes or End cities **permanently changes world data**. Spike bedrock caps are moved to shuffled positions and End City magenta glass is replaced with purpur blocks. These changes persist across restarts. Set to `false` to disable physical world modifications while keeping all other protection layers active.
---
## World Safety
AntiSeedCracker never reads, stores, or logs the real world seed. The only world modifications performed are:
- Moving bedrock cap blocks on End obsidian pillars (when `end_spikes.modify_world: true`).
- Replacing magenta stained glass in End City chunks with purpur blocks (when `end_cities.modify_world: true`).
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.
---
## 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).
---
## Known Bypass Mitigations
| Attack Vector | Mitigation |
|---|---|
| Login/Respawn packet interception | PacketEvents HIGHEST-priority intercept |
| `/seed`, `/getseed`, `/worldseed` | `PlayerCommandPreprocessEvent` + `ServerCommandEvent` + `RemoteServerCommandEvent` blocking |
| Eye of Ender triangulation | Per-player fake stronghold + ±8° jitter |
| `/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 |
| Tab-complete exposure | `TabCompleteEvent` filtering |
| RCON seed commands | `RemoteServerCommandEvent` blocking |
---
## Building from Source
Requires Maven 3.6+ and **JDK 25**. The project compiles against folia-api 26.1.2 (Java 25 class files) and produces a Java 25 output jar.
```sh
# Windows
set JAVA_HOME=C:\Program Files\Java\jdk-25.0.3
mvn clean package
# Linux / macOS
export JAVA_HOME=/path/to/jdk-25
mvn clean package
```
The shaded jar is produced at `target/AntiSeedCracker-3.1.0.jar`. PacketEvents is bundled and relocated to `me.legendcraft.antiseedcracker.lib.packetevents`.
---
## License
All rights reserved. Redistribution without permission is prohibited.
+159
View File
@@ -0,0 +1,159 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>me.legendcraft</groupId>
<artifactId>AntiSeedCracker</artifactId>
<version>3.1.0</version>
<packaging>jar</packaging>
<name>AntiSeedCracker</name>
<description>Professional multi-layer world seed protection plugin for Paper, Purpur, Folia, and Spigot 26.1.2+</description>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
<repository>
<id>codemc-repo</id>
<url>https://repo.codemc.io/repository/maven-releases/</url>
</repository>
<repository>
<id>spigotmc-repo</id>
<url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>dev.folia</groupId>
<artifactId>folia-api</artifactId>
<version>26.1.2.build.8-stable</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.github.retrooper</groupId>
<artifactId>packetevents-spigot</artifactId>
<version>2.12.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>33.3.1-jre</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-chat</artifactId>
<version>1.20-R0.2</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}-${project.version}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>25</release>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.2</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<minimizeJar>false</minimizeJar>
<relocations>
<relocation>
<pattern>com.github.retrooper.packetevents</pattern>
<shadedPattern>me.legendcraft.antiseedcracker.lib.packetevents</shadedPattern>
</relocation>
<relocation>
<pattern>io.github.retrooper.packetevents</pattern>
<shadedPattern>me.legendcraft.antiseedcracker.lib.packetevents.impl</shadedPattern>
</relocation>
</relocations>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
<exclude>META-INF/MANIFEST.MF</exclude>
<exclude>**/module-info.class</exclude>
</excludes>
</filter>
<filter>
<artifact>net.kyori:adventure-api</artifact>
<excludes><exclude>**</exclude></excludes>
</filter>
<filter>
<artifact>net.kyori:adventure-key</artifact>
<excludes><exclude>**</exclude></excludes>
</filter>
<filter>
<artifact>net.kyori:adventure-nbt</artifact>
<excludes><exclude>**</exclude></excludes>
</filter>
<filter>
<artifact>net.kyori:examination-api</artifact>
<excludes><exclude>**</exclude></excludes>
</filter>
<filter>
<artifact>net.kyori:examination-string</artifact>
<excludes><exclude>**</exclude></excludes>
</filter>
<filter>
<artifact>org.jetbrains:annotations</artifact>
<excludes><exclude>**</exclude></excludes>
</filter>
<filter>
<artifact>com.github.retrooper:packetevents-spigot</artifact>
<excludes>
<exclude>plugin.yml</exclude>
<exclude>META-INF/maven/**</exclude>
</excludes>
</filter>
</filters>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,182 @@
package me.legendcraft.antiseedcracker;
import com.github.retrooper.packetevents.PacketEvents;
import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;
import me.legendcraft.antiseedcracker.commands.AdminCommand;
import me.legendcraft.antiseedcracker.config.PluginConfig;
import me.legendcraft.antiseedcracker.database.DatabaseManager;
import me.legendcraft.antiseedcracker.listeners.CommandProtectionListener;
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.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.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;
import me.legendcraft.antiseedcracker.util.UpdateChecker;
import org.bukkit.entity.Player;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.Objects;
public final class AntiSeedCrackerPlugin extends JavaPlugin {
private PluginConfig pluginConfig;
private SeedManager seedManager;
private DatabaseManager databaseManager;
private UpdateChecker updateChecker;
private SeedBroadcastTask seedBroadcastTask;
private SeedRotationTask seedRotationTask;
private WorldSeedInterceptor worldSeedInterceptor;
private PlayerSessionListener playerSessionListener;
private CommandProtectionListener commandProtectionListener;
private EyeOfEnderProtector eyeOfEnderProtector;
private EndSpikeProtector endSpikeProtector;
private EndCityProtector endCityProtector;
@Override
public void onLoad() {
PacketEvents.setAPI(SpigotPacketEventsBuilder.build(this));
PacketEvents.getAPI().getSettings()
.checkForUpdates(false)
.reEncodeByDefault(false);
PacketEvents.getAPI().load();
}
@Override
public void onEnable() {
getLogger().info("[AntiSeedCracker] Platform detected: " + PlatformUtil.name());
saveDefaultConfig();
pluginConfig = new PluginConfig(getConfig());
seedManager = new SeedManager(getLogger());
if (pluginConfig.isDatabaseEnabled()) {
try {
databaseManager = new DatabaseManager(getDataFolder(), getLogger());
databaseManager.init();
final int maxDays = pluginConfig.getDatabaseMaxEventAgeDays();
FoliaSchedulerUtil.runAsync(this, () -> databaseManager.pruneOldEvents(maxDays));
} catch (Exception e) {
getLogger().warning(
"[AntiSeedCracker] Audit log init failed ("
+ e.getMessage()
+ ") — event logging disabled.");
databaseManager = null;
}
}
if (pluginConfig.isUpdateCheckerEnabled()) {
updateChecker = new UpdateChecker(this, pluginConfig.getModrinthProjectId());
updateChecker.checkAsync();
}
worldSeedInterceptor = new WorldSeedInterceptor(getLogger());
seedBroadcastTask = new SeedBroadcastTask(this);
seedBroadcastTask.start();
PacketEvents.getAPI().getEventManager().registerListeners(
new LoginPacketInterceptor(this),
new RespawnPacketInterceptor(this),
new MapSeedInterceptor(this)
);
PacketEvents.getAPI().init();
registerListeners();
seedRotationTask = new SeedRotationTask(this);
if (pluginConfig.isSeedRotationEnabled()) seedRotationTask.start();
AdminCommand adminCmd = new AdminCommand(this);
Objects.requireNonNull(getCommand("asc")).setExecutor(adminCmd);
Objects.requireNonNull(getCommand("asc")).setTabCompleter(adminCmd);
for (Player player : getServer().getOnlinePlayers()) {
playerSessionListener.initPlayer(player);
}
getLogger().info("[AntiSeedCracker] Enabled — real seed is protected.");
}
@Override
public void onDisable() {
if (seedBroadcastTask != null) seedBroadcastTask.stop();
if (seedRotationTask != null) seedRotationTask.stop();
if (databaseManager != null) databaseManager.close();
try {
PacketEvents.getAPI().terminate();
} catch (Exception ignored) { }
getLogger().info("[AntiSeedCracker] Disabled.");
}
public void reload() {
if (seedBroadcastTask != null) seedBroadcastTask.stop();
if (seedRotationTask != null) seedRotationTask.stop();
unregisterListeners();
reloadConfig();
pluginConfig = new PluginConfig(getConfig());
registerListeners();
if (pluginConfig.isSeedRotationEnabled()) seedRotationTask.start();
seedBroadcastTask.start();
for (Player player : getServer().getOnlinePlayers()) {
playerSessionListener.initPlayer(player);
}
getLogger().info("[AntiSeedCracker] Reloaded successfully.");
}
private void registerListeners() {
PluginManager pm = getServer().getPluginManager();
playerSessionListener = new PlayerSessionListener(this);
commandProtectionListener = new CommandProtectionListener(this);
pm.registerEvents(playerSessionListener, this);
pm.registerEvents(commandProtectionListener, this);
if (pluginConfig.isEyeOfEnderEnabled()) {
eyeOfEnderProtector = new EyeOfEnderProtector(this);
pm.registerEvents(eyeOfEnderProtector, this);
}
if (pluginConfig.isEndSpikesEnabled()) {
endSpikeProtector = new EndSpikeProtector(this);
pm.registerEvents(endSpikeProtector, this);
getServer().getWorlds().forEach(endSpikeProtector::triggerModification);
}
if (pluginConfig.isEndCitiesEnabled()) {
endCityProtector = new EndCityProtector(this);
pm.registerEvents(endCityProtector, this);
}
}
private void unregisterListeners() {
if (playerSessionListener != null) { playerSessionListener.unregister(); playerSessionListener = null; }
if (commandProtectionListener != null) { commandProtectionListener.unregister(); commandProtectionListener = null; }
if (eyeOfEnderProtector != null) { eyeOfEnderProtector.unregister(); eyeOfEnderProtector = null; }
if (endSpikeProtector != null) { endSpikeProtector.unregister(); endSpikeProtector = null; }
if (endCityProtector != null) { endCityProtector.unregister(); endCityProtector = 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; }
}
@@ -0,0 +1,196 @@
package me.legendcraft.antiseedcracker.commands;
import me.legendcraft.antiseedcracker.AntiSeedCrackerPlugin;
import me.legendcraft.antiseedcracker.scheduler.FoliaSchedulerUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public final class AdminCommand implements CommandExecutor, TabCompleter {
private static final String PERM = "antiseedcracker.admin";
private static final Component PREFIX =
Component.text("[", NamedTextColor.DARK_GRAY)
.append(Component.text("ASC", NamedTextColor.AQUA))
.append(Component.text("] ", NamedTextColor.DARK_GRAY));
private static final List<String> SUBCOMMANDS = Arrays.asList("reload", "status", "stats", "info");
private final AntiSeedCrackerPlugin plugin;
public AdminCommand(AntiSeedCrackerPlugin plugin) {
this.plugin = plugin;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!sender.hasPermission(PERM)) {
sender.sendMessage(msg(Component.text(
"You do not have permission to use this command.", NamedTextColor.RED)));
return true;
}
if (args.length == 0) {
sendUsage(sender);
return true;
}
switch (args[0].toLowerCase()) {
case "reload" -> handleReload(sender);
case "status" -> handleStatus(sender);
case "stats" -> handleStats(sender);
case "info" -> handleInfo(sender);
default -> sendUsage(sender);
}
return true;
}
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
if (!sender.hasPermission(PERM)) return Collections.emptyList();
if (args.length == 1) {
String partial = args[0].toLowerCase();
return SUBCOMMANDS.stream()
.filter(s -> s.startsWith(partial))
.collect(Collectors.toList());
}
return Collections.emptyList();
}
private void handleReload(CommandSender sender) {
sender.sendMessage(msg(Component.text("Reloading AntiSeedCracker…", NamedTextColor.YELLOW)));
try {
plugin.reload();
sender.sendMessage(msg(Component.text("Reload successful.", NamedTextColor.GREEN)));
} catch (Exception e) {
sender.sendMessage(msg(Component.text("Reload failed: " + e.getMessage(), NamedTextColor.RED)));
plugin.getLogger().severe("[AntiSeedCracker] Reload exception: " + e.getMessage());
}
}
private void handleStatus(CommandSender sender) {
boolean loginIntercepted = plugin.getPluginConfig().isInterceptLogin();
boolean respawnIntercepted = plugin.getPluginConfig().isInterceptRespawn();
boolean rotationEnabled = plugin.getPluginConfig().isSeedRotationEnabled();
boolean spikesEnabled = plugin.getPluginConfig().isEndSpikesEnabled();
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 mapProtEnabled = plugin.getPluginConfig().isTreasureMapProtectionEnabled();
boolean dbEnabled = plugin.getDatabaseManager() != null;
int trackedSeeds = plugin.getSeedManager().trackedCount();
sender.sendMessage(Component.empty());
sender.sendMessage(msg(Component.text("━━ AntiSeedCracker v"
+ plugin.getPluginMeta().getVersion() + " by SunsetRQ7_ ━━",
NamedTextColor.GOLD)));
sender.sendMessage(statusLine("Login Packet Intercept", loginIntercepted));
sender.sendMessage(statusLine("Respawn Packet Intercept", respawnIntercepted));
sender.sendMessage(statusLine("Seed Rotation", rotationEnabled));
sender.sendMessage(statusLine("End Spike Protection", spikesEnabled));
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("Treasure Map Scramble", mapProtEnabled));
sender.sendMessage(statusLine("Database / Audit Log", dbEnabled));
sender.sendMessage(msg(
Component.text("Tracked players: ", NamedTextColor.AQUA)
.append(Component.text(String.valueOf(trackedSeeds), NamedTextColor.WHITE))));
sender.sendMessage(Component.empty());
}
private void handleStats(CommandSender sender) {
me.legendcraft.antiseedcracker.database.DatabaseManager db =
plugin.getDatabaseManager();
if (db == null) {
sender.sendMessage(msg(Component.text(
"Database is disabled or failed to initialise.", NamedTextColor.RED)));
return;
}
sender.sendMessage(msg(Component.text("Loading stats…", NamedTextColor.YELLOW)));
FoliaSchedulerUtil.runAsync(plugin, () -> {
java.util.Map<String, Long> stats = db.getStats();
FoliaSchedulerUtil.runGlobal(plugin, () -> {
sender.sendMessage(Component.empty());
sender.sendMessage(msg(Component.text(
"━━ Protection Statistics ━━", NamedTextColor.GOLD)));
if (stats.isEmpty()) {
sender.sendMessage(msg(Component.text(
"No events recorded yet.", NamedTextColor.GRAY)));
} else {
for (java.util.Map.Entry<String, Long> e : stats.entrySet()) {
String label = e.getKey().replace('_', ' ').toLowerCase();
label = Character.toUpperCase(label.charAt(0)) + label.substring(1);
sender.sendMessage(
Component.text(" ", NamedTextColor.DARK_GRAY)
.append(Component.text(label + ": ", NamedTextColor.GRAY))
.append(Component.text(String.valueOf(e.getValue()),
NamedTextColor.WHITE)));
}
}
sender.sendMessage(Component.empty());
});
});
}
private void handleInfo(CommandSender sender) {
me.legendcraft.antiseedcracker.util.UpdateChecker uc = plugin.getUpdateChecker();
sender.sendMessage(Component.empty());
sender.sendMessage(msg(Component.text(
"━━ AntiSeedCracker Info ━━", NamedTextColor.GOLD)));
sender.sendMessage(msg(Component.text("Version: ", NamedTextColor.GRAY)
.append(Component.text(plugin.getPluginMeta().getVersion(),
NamedTextColor.WHITE))));
sender.sendMessage(msg(Component.text("Author: ", NamedTextColor.GRAY)
.append(Component.text("SunsetRQ7_", NamedTextColor.AQUA))));
sender.sendMessage(msg(Component.text("Platform: ", NamedTextColor.GRAY)
.append(Component.text(
me.legendcraft.antiseedcracker.util.PlatformUtil.name(),
NamedTextColor.WHITE))));
if (uc != null) {
if (uc.isUpToDate()) {
sender.sendMessage(msg(Component.text(
"Update: ✔ Up to date", NamedTextColor.GREEN)));
} else if (uc.getLatestVersion() != null) {
sender.sendMessage(msg(Component.text(
"Update: ✘ v" + uc.getLatestVersion()
+ " available on Modrinth!", NamedTextColor.YELLOW)));
} else {
sender.sendMessage(msg(Component.text(
"Update: not yet checked", NamedTextColor.GRAY)));
}
}
sender.sendMessage(Component.empty());
}
private static Component msg(Component body) {
return PREFIX.append(body);
}
private static Component statusLine(String label, boolean enabled) {
Component status = enabled
? Component.text("✔ enabled", NamedTextColor.GREEN)
: Component.text("✘ disabled", NamedTextColor.RED);
return Component.text(" ")
.append(Component.text(label, NamedTextColor.GRAY))
.append(Component.text(": ", NamedTextColor.DARK_GRAY))
.append(status);
}
private void sendUsage(CommandSender sender) {
sender.sendMessage(msg(
Component.text("Usage: ", NamedTextColor.YELLOW)
.append(Component.text("/asc <reload|status|stats|info>",
NamedTextColor.WHITE))));
}
}
@@ -0,0 +1,127 @@
package me.legendcraft.antiseedcracker.config;
import org.bukkit.configuration.file.FileConfiguration;
import java.util.Collections;
import java.util.List;
public final class PluginConfig {
private final boolean interceptLogin;
private final boolean interceptRespawn;
private final boolean seedRotationEnabled;
private final long seedRotationIntervalTicks;
private final boolean endSpikesEnabled;
private final List<String> endSpikeWorlds;
private final boolean endSpikesModifyWorld;
private final boolean endCitiesEnabled;
private final List<String> endCityWorlds;
private final boolean endCitiesModifyWorld;
private final boolean spoofLocateEnabled;
private final int spoofLocateMaxOffset;
private final boolean pluginApiProtectionEnabled;
private final boolean eyeOfEnderEnabled;
private final List<String> eyeOfEnderWorlds;
private final int fakeStrongholdMinDist;
private final int fakeStrongholdMaxDist;
private final boolean updateCheckerEnabled;
private final String modrinthProjectId;
private final boolean databaseEnabled;
private final boolean databaseLogEvents;
private final int databaseMaxEventAgeDays;
private final boolean treasureMapProtectionEnabled;
private final List<String> extraBlockedCommands;
public PluginConfig(FileConfiguration cfg) {
this.interceptLogin = cfg.getBoolean("seed_obfuscation.intercept_login", true);
this.interceptRespawn = cfg.getBoolean("seed_obfuscation.intercept_respawn", true);
this.seedRotationEnabled = cfg.getBoolean("seed_rotation.enabled", true);
long rawSecs = Math.max(30L, cfg.getLong("seed_rotation.interval_seconds", 60L));
this.seedRotationIntervalTicks = rawSecs * 20L;
this.endSpikesEnabled = cfg.getBoolean("structure_protection.end_spikes.enabled", true);
this.endSpikeWorlds = Collections.unmodifiableList(
cfg.getStringList("structure_protection.end_spikes.worlds"));
this.endSpikesModifyWorld = cfg.getBoolean("structure_protection.end_spikes.modify_world", true);
this.endCitiesEnabled = cfg.getBoolean("structure_protection.end_cities.enabled", true);
this.endCityWorlds = Collections.unmodifiableList(
cfg.getStringList("structure_protection.end_cities.worlds"));
this.endCitiesModifyWorld = cfg.getBoolean("structure_protection.end_cities.modify_world", true);
this.spoofLocateEnabled = cfg.getBoolean("structure_protection.spoof_locate_command.enabled", true);
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.eyeOfEnderEnabled = cfg.getBoolean("eye_of_ender_protection.enabled", true);
this.eyeOfEnderWorlds = Collections.unmodifiableList(
cfg.getStringList("eye_of_ender_protection.worlds"));
this.fakeStrongholdMinDist = Math.max(100,
cfg.getInt("eye_of_ender_protection.fake_stronghold_min_distance", 800));
this.fakeStrongholdMaxDist = Math.max(this.fakeStrongholdMinDist + 100,
cfg.getInt("eye_of_ender_protection.fake_stronghold_max_distance", 4000));
this.updateCheckerEnabled = cfg.getBoolean("update_checker.enabled", true);
this.modrinthProjectId = cfg.getString("update_checker.modrinth_id",
"YOUR_MODRINTH_PROJECT_ID");
this.databaseEnabled = cfg.getBoolean("database.enabled", true);
this.databaseLogEvents = cfg.getBoolean("database.log_events", true);
this.databaseMaxEventAgeDays = Math.max(1,
cfg.getInt("database.max_event_age_days", 30));
this.treasureMapProtectionEnabled =
cfg.getBoolean("treasure_map_protection.enabled", true);
this.extraBlockedCommands = Collections.unmodifiableList(
cfg.getStringList("extra_blocked_commands"));
}
public boolean isInterceptLogin() { return interceptLogin; }
public boolean isInterceptRespawn() { return interceptRespawn; }
public boolean isSeedRotationEnabled() { return seedRotationEnabled; }
public long getSeedRotationIntervalTicks() { return seedRotationIntervalTicks; }
public boolean isEndSpikesEnabled() { return endSpikesEnabled; }
public List<String> getEndSpikeWorlds() { return endSpikeWorlds; }
public boolean isEndSpikesModifyWorld() { return endSpikesModifyWorld; }
public boolean isEndCitiesEnabled() { return endCitiesEnabled; }
public List<String> getEndCityWorlds() { return endCityWorlds; }
public boolean isEndCitiesModifyWorld() { return endCitiesModifyWorld; }
public boolean isSpoofLocateEnabled() { return spoofLocateEnabled; }
public int getSpoofLocateMaxOffset() { return spoofLocateMaxOffset; }
public boolean isPluginApiProtectionEnabled() { return pluginApiProtectionEnabled; }
public boolean isEyeOfEnderEnabled() { return eyeOfEnderEnabled; }
public List<String> getEyeOfEnderWorlds() { return eyeOfEnderWorlds; }
public int getFakeStrongholdMinDist() { return fakeStrongholdMinDist; }
public int getFakeStrongholdMaxDist() { return fakeStrongholdMaxDist; }
public boolean isUpdateCheckerEnabled() { return updateCheckerEnabled; }
public String getModrinthProjectId() { return modrinthProjectId; }
public boolean isDatabaseEnabled() { return databaseEnabled; }
public boolean isDatabaseLogEvents() { return databaseLogEvents; }
public int getDatabaseMaxEventAgeDays(){ return databaseMaxEventAgeDays; }
public boolean isTreasureMapProtectionEnabled() { return treasureMapProtectionEnabled; }
public List<String> getExtraBlockedCommands() { return extraBlockedCommands; }
}
@@ -0,0 +1,171 @@
package me.legendcraft.antiseedcracker.database;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.logging.Logger;
import java.util.stream.Collectors;
public final class DatabaseManager {
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ISO_LOCAL_DATE;
private static final String SEP = "\t";
private static final int QUEUE_CAP = 2_000;
private final Path logDir;
private final Logger log;
private final LinkedBlockingQueue<Runnable> writeQueue =
new LinkedBlockingQueue<>(QUEUE_CAP);
private Thread writerThread;
private volatile boolean running = false;
public DatabaseManager(File dataFolder, Logger log) {
this.logDir = dataFolder.toPath().resolve("logs");
this.log = log;
}
public void init() throws IOException {
Files.createDirectories(logDir);
startWriterThread();
log.info("[AntiSeedCracker] Audit log initialised at " + logDir.toAbsolutePath());
}
public void close() {
running = false;
if (writerThread != null) {
writerThread.interrupt();
try {
writerThread.join(5_000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public void logEvent(EventType type,
String playerUuid,
String playerName,
String worldName,
String detail) {
final long ts = System.currentTimeMillis();
final String date = LocalDate.now(ZoneOffset.UTC).format(DATE_FMT);
boolean offered = writeQueue.offer(
() -> doAppend(type, playerUuid, playerName, worldName, detail, ts, date));
if (!offered) {
log.fine("[ASC-Log] Write queue full — event dropped: " + type);
}
}
public Map<String, Long> getStats() {
Map<String, Long> counts = new HashMap<>();
try (DirectoryStream<Path> ds =
Files.newDirectoryStream(logDir, "events-*.log")) {
for (Path file : ds) {
try (BufferedReader br =
Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
String line;
while ((line = br.readLine()) != null) {
if (line.isBlank()) continue;
int first = line.indexOf(SEP);
int second = first >= 0 ? line.indexOf(SEP, first + 1) : -1;
if (first >= 0 && second > first) {
counts.merge(line.substring(first + 1, second), 1L, Long::sum);
}
}
} catch (IOException ignored) { }
}
} catch (IOException e) {
log.warning("[ASC-Log] Stats scan failed: " + e.getMessage());
}
return counts.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.collect(Collectors.toMap(
Map.Entry::getKey, Map.Entry::getValue,
(a, b) -> a, LinkedHashMap::new));
}
public int pruneOldEvents(int maxAgeDays) {
long cutoffMs = System.currentTimeMillis() - ((long) maxAgeDays * 86_400_000L);
int removed = 0;
try (DirectoryStream<Path> ds =
Files.newDirectoryStream(logDir, "events-*.log")) {
for (Path file : ds) {
try {
if (Files.getLastModifiedTime(file).toMillis() < cutoffMs) {
Files.deleteIfExists(file);
removed++;
}
} catch (IOException ignored) { }
}
} catch (IOException e) {
log.warning("[ASC-Log] Prune failed: " + e.getMessage());
}
if (removed > 0) {
log.info("[AntiSeedCracker] Pruned " + removed + " old audit log file(s).");
}
return removed;
}
private void startWriterThread() {
running = true;
writerThread = new Thread(() -> {
while (running || !writeQueue.isEmpty()) {
try {
Runnable task = writeQueue.take();
task.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
Runnable remaining;
while ((remaining = writeQueue.poll()) != null) {
remaining.run();
}
break;
}
}
}, "ASC-AuditLog-Writer");
writerThread.setDaemon(true);
writerThread.start();
}
private void doAppend(EventType type,
String uuid, String name, String world, String detail,
long ts, String date) {
Path file = logDir.resolve("events-" + date + ".log");
String line = ts + SEP
+ type.name() + SEP
+ sanitise(uuid) + SEP
+ sanitise(name) + SEP
+ sanitise(world) + SEP
+ sanitise(detail);
try (BufferedWriter bw = Files.newBufferedWriter(
file, StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
bw.write(line);
bw.newLine();
} catch (IOException e) {
log.warning("[ASC-Log] Write failed: " + e.getMessage());
}
}
private static String sanitise(String s) {
if (s == null) return "";
return s.replace("\t", " ").replace("\r", "").replace("\n", " ");
}
public enum EventType {
SEED_COMMAND_BLOCKED,
LOCATE_SPOOFED,
EYE_REDIRECTED,
TREASURE_MAP_SCRAMBLED,
SEED_PACKET_SPOOFED,
END_SPIKE_PROTECTED,
END_CITY_PROTECTED
}
}
@@ -0,0 +1,110 @@
package me.legendcraft.antiseedcracker.listeners;
import me.legendcraft.antiseedcracker.AntiSeedCrackerPlugin;
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.PlayerCommandPreprocessEvent;
import org.bukkit.event.server.RemoteServerCommandEvent;
import org.bukkit.event.server.ServerCommandEvent;
import org.bukkit.event.server.TabCompleteEvent;
public final class CommandProtectionListener implements Listener {
private final AntiSeedCrackerPlugin plugin;
public CommandProtectionListener(AntiSeedCrackerPlugin plugin) {
this.plugin = plugin;
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = false)
public void onPlayerCommand(PlayerCommandPreprocessEvent event) {
String raw = event.getMessage().trim();
if (!isSeedCommand(raw) && !isExtraBlocked(raw)) return;
event.setCancelled(true);
event.getPlayer().sendMessage(
"§c[AntiSeedCracker] Access to world seed information is restricted.");
plugin.getLogger().warning(
"[AntiSeedCracker] Seed command blocked for player "
+ event.getPlayer().getName() + ": " + raw);
me.legendcraft.antiseedcracker.database.DatabaseManager db =
plugin.getDatabaseManager();
if (db != null && plugin.getPluginConfig().isDatabaseLogEvents()) {
db.logEvent(
me.legendcraft.antiseedcracker.database.DatabaseManager.EventType
.SEED_COMMAND_BLOCKED,
event.getPlayer().getUniqueId().toString(),
event.getPlayer().getName(),
event.getPlayer().getWorld().getName(),
raw);
}
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = false)
public void onServerCommand(ServerCommandEvent event) {
String raw = event.getCommand().trim();
if (!isSeedCommand("/" + raw) && !isExtraBlocked("/" + raw)) return;
event.setCancelled(true);
event.getSender().sendMessage(
"§c[AntiSeedCracker] Seed access is restricted. The real seed is never exposed.");
plugin.getLogger().warning(
"[AntiSeedCracker] Console seed command blocked: " + raw);
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = false)
public void onRemoteServerCommand(RemoteServerCommandEvent event) {
String raw = event.getCommand().trim();
if (!isSeedCommand("/" + raw) && !isExtraBlocked("/" + raw)) return;
event.setCancelled(true);
event.getSender().sendMessage(
"§c[AntiSeedCracker] Seed access is restricted. The real seed is never exposed.");
plugin.getLogger().warning(
"[AntiSeedCracker] RCON seed command blocked: " + raw);
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onTabComplete(TabCompleteEvent event) {
java.util.List<String> extra = plugin.getPluginConfig().getExtraBlockedCommands();
event.getCompletions().removeIf(c -> {
String lower = c.toLowerCase();
if (lower.startsWith("/")) lower = lower.substring(1);
int colon = lower.indexOf(':');
if (colon >= 0) lower = lower.substring(colon + 1);
return lower.equals("seed")
|| lower.equals("getseed")
|| lower.equals("worldseed")
|| extra.contains(lower);
});
}
private static boolean isSeedCommand(String raw) {
String lower = raw.toLowerCase();
if (lower.startsWith("/")) lower = lower.substring(1);
int colon = lower.indexOf(':');
if (colon >= 0) lower = lower.substring(colon + 1);
return lower.equals("seed")
|| lower.startsWith("seed ")
|| lower.equals("getseed")
|| lower.startsWith("getseed ")
|| lower.equals("worldseed")
|| lower.startsWith("worldseed ");
}
private boolean isExtraBlocked(String raw) {
String lower = raw.toLowerCase();
if (lower.startsWith("/")) lower = lower.substring(1);
int colon = lower.indexOf(':');
if (colon >= 0) lower = lower.substring(colon + 1);
String root = lower.split("\\s+")[0];
return plugin.getPluginConfig().getExtraBlockedCommands().contains(root);
}
public void unregister() {
HandlerList.unregisterAll(this);
}
}
@@ -0,0 +1,89 @@
package me.legendcraft.antiseedcracker.listeners;
import me.legendcraft.antiseedcracker.AntiSeedCrackerPlugin;
import me.legendcraft.antiseedcracker.scheduler.FoliaSchedulerUtil;
import org.bukkit.Chunk;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.world.ChunkLoadEvent;
import org.bukkit.generator.structure.GeneratedStructure;
import org.bukkit.generator.structure.Structure;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import java.util.Collection;
public final class EndCityProtector implements Listener {
private final NamespacedKey modifiedKey;
private final AntiSeedCrackerPlugin plugin;
public EndCityProtector(AntiSeedCrackerPlugin plugin) {
this.plugin = plugin;
this.modifiedKey = new NamespacedKey(plugin, "asc_city_modified");
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onChunkLoad(ChunkLoadEvent event) {
if (!plugin.getPluginConfig().isEndCitiesEnabled()) return;
String worldName = event.getWorld().getName();
if (!plugin.getPluginConfig().getEndCityWorlds().contains(worldName)) return;
Chunk chunk = event.getChunk();
PersistentDataContainer pdc = chunk.getPersistentDataContainer();
if (pdc.has(modifiedKey, PersistentDataType.BYTE)) return;
FoliaSchedulerUtil.runAtChunk(plugin, chunk.getWorld(), chunk.getX(), chunk.getZ(), () -> {
if (!chunk.isLoaded()) return;
try {
Collection<GeneratedStructure> structures =
chunk.getStructures(Structure.END_CITY);
if (structures.isEmpty()) return;
if (plugin.getPluginConfig().isEndCitiesModifyWorld()) {
replaceGlassInChunk(chunk);
}
chunk.getPersistentDataContainer()
.set(modifiedKey, PersistentDataType.BYTE, (byte) 1);
} catch (Exception e) {
plugin.getLogger().warning("[AntiSeedCracker] EndCityProtector scan error"
+ " at chunk (" + chunk.getX() + "," + chunk.getZ() + "): " + e.getMessage());
}
});
}
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;
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);
if (block.getType() == Material.MAGENTA_STAINED_GLASS) {
block.setType(Material.PURPUR_BLOCK, false);
}
}
}
}
}
public void unregister() {
HandlerList.unregisterAll(this);
}
}
@@ -0,0 +1,144 @@
package me.legendcraft.antiseedcracker.listeners;
import me.legendcraft.antiseedcracker.AntiSeedCrackerPlugin;
import me.legendcraft.antiseedcracker.scheduler.FoliaSchedulerUtil;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.World.Environment;
import org.bukkit.block.Block;
import org.bukkit.boss.DragonBattle;
import org.bukkit.boss.DragonBattle.RespawnPhase;
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.EntityPlaceEvent;
import org.bukkit.event.world.WorldLoadEvent;
import org.bukkit.persistence.PersistentDataType;
import java.security.SecureRandom;
import java.util.Arrays;
public final class EndSpikeProtector implements Listener {
private static final int[] VANILLA_HEIGHTS = {76, 79, 82, 85, 88, 91, 94, 97, 100, 103};
private static final int CAP_MIN = 65;
private static final int CAP_MAX = 120;
private static final double[] PILLAR_ANGLES;
static {
PILLAR_ANGLES = new double[10];
for (int i = 0; i < 10; i++) {
PILLAR_ANGLES[i] = 2.0 * (-Math.PI + Math.PI * 0.1 * i);
}
}
private static final SecureRandom RNG = new SecureRandom();
private final AntiSeedCrackerPlugin plugin;
private final NamespacedKey modifiedKey;
public EndSpikeProtector(AntiSeedCrackerPlugin plugin) {
this.plugin = plugin;
this.modifiedKey = new NamespacedKey(plugin, "asc_spike_modified");
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onWorldLoad(WorldLoadEvent event) {
World world = event.getWorld();
if (!shouldProcess(world)) return;
triggerModification(world);
}
public void triggerModification(World world) {
if (!shouldProcess(world)) return;
scheduleModification(world);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onCrystalPlace(EntityPlaceEvent event) {
if (event.getEntityType() != EntityType.END_CRYSTAL) return;
World world = event.getEntity().getWorld();
if (!shouldProcess(world)) return;
Block placedOn = event.getBlock();
if (placedOn == null || placedOn.getType() != Material.BEDROCK) return;
if (Math.abs(placedOn.getX()) > 3 || Math.abs(placedOn.getZ()) > 3) return;
world.getPersistentDataContainer().set(modifiedKey, PersistentDataType.BOOLEAN, false);
schedulePollForRespawnEnd(world, 0);
}
private boolean shouldProcess(World world) {
if (world.getEnvironment() != Environment.THE_END) return false;
if (!plugin.getPluginConfig().isEndSpikesEnabled()) return false;
return plugin.getPluginConfig().getEndSpikeWorlds().contains(world.getName());
}
private void scheduleModification(World world) {
Boolean alreadyModified = world.getPersistentDataContainer()
.getOrDefault(modifiedKey, PersistentDataType.BOOLEAN, false);
if (alreadyModified) return;
world.getPersistentDataContainer().set(modifiedKey, PersistentDataType.BOOLEAN, true);
if (!plugin.getPluginConfig().isEndSpikesModifyWorld()) return;
int[] heights = Arrays.copyOf(VANILLA_HEIGHTS, VANILLA_HEIGHTS.length);
for (int i = heights.length - 1; i > 0; i--) {
int j = RNG.nextInt(i + 1);
int tmp = heights[i]; heights[i] = heights[j]; heights[j] = tmp;
}
for (int i = 0; i < VANILLA_HEIGHTS.length; i++) {
modifyPillarCap(world, i, heights[i]);
}
}
private void modifyPillarCap(World world, int index, int newHeight) {
double x = 42.0 * Math.cos(PILLAR_ANGLES[index]);
double z = 42.0 * Math.sin(PILLAR_ANGLES[index]);
int cx = (int) Math.floor(x) >> 4;
int cz = (int) Math.floor(z) >> 4;
FoliaSchedulerUtil.runAtChunk(plugin, world, cx, cz, () -> {
Block cap = null;
for (int y = CAP_MAX; y >= CAP_MIN; y--) {
Block b = world.getBlockAt((int) Math.round(x), y, (int) Math.round(z));
if (b.getType() == Material.BEDROCK) {
cap = b;
break;
}
}
if (cap == null) return;
cap.setType(Material.OBSIDIAN, false);
Block newCap = world.getBlockAt(cap.getX(), newHeight, cap.getZ());
newCap.setType(Material.BEDROCK, false);
});
}
private void schedulePollForRespawnEnd(World world, int attemptCount) {
if (attemptCount > 300) {
scheduleModification(world);
return;
}
FoliaSchedulerUtil.runGlobalDelayed(plugin, () -> {
DragonBattle battle = world.getEnderDragonBattle();
RespawnPhase phase = (battle == null) ? null : battle.getRespawnPhase();
boolean summoning = phase == RespawnPhase.START
|| phase == RespawnPhase.PREPARING_TO_SUMMON_PILLARS
|| phase == RespawnPhase.SUMMONING_PILLARS;
if (summoning) {
schedulePollForRespawnEnd(world, attemptCount + 1);
} else {
scheduleModification(world);
}
}, 20L);
}
public void unregister() {
HandlerList.unregisterAll(this);
}
}
@@ -0,0 +1,131 @@
package me.legendcraft.antiseedcracker.listeners;
import me.legendcraft.antiseedcracker.AntiSeedCrackerPlugin;
import me.legendcraft.antiseedcracker.database.DatabaseManager;
import me.legendcraft.antiseedcracker.scheduler.FoliaSchedulerUtil;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.EnderSignal;
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.ProjectileLaunchEvent;
import java.security.SecureRandom;
import java.util.UUID;
public final class EyeOfEnderProtector implements Listener {
private static final double EYE_JITTER_RADIANS = Math.toRadians(8.0);
private static final SecureRandom RNG = new SecureRandom();
private final AntiSeedCrackerPlugin plugin;
public EyeOfEnderProtector(AntiSeedCrackerPlugin plugin) {
this.plugin = plugin;
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onEyeLaunch(ProjectileLaunchEvent event) {
if (!plugin.getPluginConfig().isEyeOfEnderEnabled()) return;
if (!(event.getEntity() instanceof EnderSignal eye)) return;
World world = eye.getWorld();
if (!plugin.getPluginConfig().getEyeOfEnderWorlds().contains(world.getName())) return;
final org.bukkit.entity.Player shooterPlayer =
(event.getEntity().getShooter() instanceof org.bukkit.entity.Player p) ? p : null;
UUID shooterUUID = shooterPlayer != null ? shooterPlayer.getUniqueId() : null;
final int[] fakeTarget;
if (shooterUUID != null) {
int[] stored = plugin.getSeedManager().getFakeStronghold(shooterUUID);
if (stored != null) {
fakeTarget = stored;
} else {
fakeTarget = plugin.getSeedManager().assignFakeStronghold(
(org.bukkit.entity.Player) event.getEntity().getShooter(),
plugin.getPluginConfig().getFakeStrongholdMinDist(),
plugin.getPluginConfig().getFakeStrongholdMaxDist()
);
}
} else {
int min = plugin.getPluginConfig().getFakeStrongholdMinDist();
int max = plugin.getPluginConfig().getFakeStrongholdMaxDist();
double angle = RNG.nextDouble() * 2.0 * Math.PI;
int dist = min + RNG.nextInt(Math.max(1, max - min + 1));
fakeTarget = new int[]{
(int) Math.round(Math.cos(angle) * dist),
(int) Math.round(Math.sin(angle) * dist)
};
}
final int targetX = fakeTarget[0];
final int targetZ = fakeTarget[1];
try {
Location eyeLoc = eye.getLocation();
double dx = targetX - eyeLoc.getX();
double dz = targetZ - eyeLoc.getZ();
double distTarget = Math.sqrt(dx * dx + dz * dz);
if (distTarget < 1.0) return;
double speed = eye.getVelocity().length();
if (speed < 0.01) speed = 0.3;
double ndx = dx / distTarget;
double ndz = dz / distTarget;
double jitter = (RNG.nextDouble() * 2.0 - 1.0) * EYE_JITTER_RADIANS;
double cos = Math.cos(jitter);
double sin = Math.sin(jitter);
double jx = ndx * cos - ndz * sin;
double jz = ndx * sin + ndz * cos;
eye.setVelocity(new org.bukkit.util.Vector(
jx * speed,
eye.getVelocity().getY(),
jz * speed
));
DatabaseManager db = plugin.getDatabaseManager();
if (db != null && plugin.getPluginConfig().isDatabaseLogEvents()
&& shooterPlayer != null) {
db.logEvent(DatabaseManager.EventType.EYE_REDIRECTED,
shooterUUID.toString(),
shooterPlayer.getName(),
eye.getWorld().getName(),
"Redirected to fake stronghold [" + targetX + ", " + targetZ + "]");
}
} catch (Exception e) {
FoliaSchedulerUtil.runForEntity(plugin, eye, () -> {
if (!eye.isValid()) return;
try {
Location eyeLoc = eye.getLocation();
double dx = targetX - eyeLoc.getX();
double dz = targetZ - eyeLoc.getZ();
double distTarget = Math.sqrt(dx * dx + dz * dz);
if (distTarget < 1.0) return;
double speed = eye.getVelocity().length();
if (speed < 0.01) speed = 0.3;
double ndx = dx / distTarget;
double ndz = dz / distTarget;
double jitter = (RNG.nextDouble() * 2.0 - 1.0) * EYE_JITTER_RADIANS;
double cos = Math.cos(jitter);
double sin = Math.sin(jitter);
eye.setVelocity(new org.bukkit.util.Vector(
(ndx * cos - ndz * sin) * speed,
eye.getVelocity().getY(),
(ndx * sin + ndz * cos) * speed
));
} catch (Exception ignored) {}
});
}
}
public void unregister() {
HandlerList.unregisterAll(this);
}
}
@@ -0,0 +1,129 @@
package me.legendcraft.antiseedcracker.listeners;
import me.legendcraft.antiseedcracker.AntiSeedCrackerPlugin;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
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.PlayerCommandPreprocessEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.world.WorldUnloadEvent;
import java.security.SecureRandom;
import java.util.regex.Pattern;
public final class PlayerSessionListener implements Listener {
private static final Pattern LOCATE_PATTERN =
Pattern.compile("^/?(?:minecraft:)?locate(?:\\s+(\\w+))?(?:\\s+([\\w:]+))?\\s*$",
Pattern.CASE_INSENSITIVE);
private static final SecureRandom RNG = new SecureRandom();
private final AntiSeedCrackerPlugin plugin;
public PlayerSessionListener(AntiSeedCrackerPlugin plugin) {
this.plugin = plugin;
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerJoin(PlayerJoinEvent event) {
initPlayer(event.getPlayer());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onPlayerQuit(PlayerQuitEvent event) {
plugin.getSeedManager().removePlayer(event.getPlayer().getUniqueId());
}
@EventHandler(priority = EventPriority.MONITOR)
public void onWorldUnload(WorldUnloadEvent event) {
plugin.getSeedManager().forgetWorld(event.getWorld().getUID());
}
public void initPlayer(Player player) {
plugin.getSeedManager().assignSeed(player);
plugin.getSeedManager().assignFakeStronghold(
player,
plugin.getPluginConfig().getFakeStrongholdMinDist(),
plugin.getPluginConfig().getFakeStrongholdMaxDist()
);
}
@EventHandler(priority = EventPriority.LOWEST, ignoreCancelled = true)
public void onPlayerCommand(PlayerCommandPreprocessEvent event) {
if (!plugin.getPluginConfig().isSpoofLocateEnabled()) return;
String message = event.getMessage().trim();
if (!LOCATE_PATTERN.matcher(message).matches()) return;
event.setCancelled(true);
Player player = event.getPlayer();
String structureArg = extractStructureArg(message);
int[] fakeCoords = buildFakeLocateCoords(player, structureArg);
int fakeX = fakeCoords[0];
int fakeZ = fakeCoords[1];
double dx = fakeX - player.getLocation().getX();
double dz = fakeZ - player.getLocation().getZ();
int dist = (int) Math.sqrt(dx * dx + dz * dz);
String displayName = formatStructureName(structureArg);
player.sendMessage(
Component.text("The nearest " + displayName + " is at ", NamedTextColor.WHITE)
.append(Component.text("[" + fakeX + ", ~, " + fakeZ + "]", NamedTextColor.GREEN))
.append(Component.text(" (approximately ", NamedTextColor.WHITE))
.append(Component.text(String.valueOf(dist), NamedTextColor.GREEN))
.append(Component.text(" blocks away)", NamedTextColor.WHITE)));
}
private String extractStructureArg(String command) {
String[] tokens = command.replaceFirst("^/", "").split("\\s+");
int startIdx = 1;
if (tokens.length > startIdx) {
String t1 = tokens[startIdx].toLowerCase();
if (t1.equals("structure") || t1.equals("biome")) startIdx = 2;
}
if (tokens.length > startIdx) return tokens[startIdx].toLowerCase();
return "minecraft:stronghold";
}
private int[] buildFakeLocateCoords(Player player, String structureArg) {
if (structureArg.contains("stronghold")) {
int[] fakeStronghold = plugin.getSeedManager().getFakeStronghold(player.getUniqueId());
if (fakeStronghold != null) return fakeStronghold;
}
int maxOffset = plugin.getPluginConfig().getSpoofLocateMaxOffset();
int minOffset = maxOffset / 4;
double angle = RNG.nextDouble() * 2.0 * Math.PI;
int dist = minOffset + RNG.nextInt(Math.max(1, maxOffset - minOffset + 1));
int baseX = (int) player.getLocation().getX();
int baseZ = (int) player.getLocation().getZ();
int x = ((baseX + (int) Math.round(Math.cos(angle) * dist)) >> 4) << 4;
int z = ((baseZ + (int) Math.round(Math.sin(angle) * dist)) >> 4) << 4;
return new int[]{x, z};
}
private String formatStructureName(String id) {
String name = id.contains(":") ? id.substring(id.indexOf(':') + 1) : id;
String[] parts = name.split("_");
StringBuilder sb = new StringBuilder();
for (String part : parts) {
if (!part.isEmpty()) {
if (sb.length() > 0) sb.append(' ');
sb.append(Character.toUpperCase(part.charAt(0)));
sb.append(part.substring(1).toLowerCase());
}
}
return sb.toString();
}
public void unregister() {
HandlerList.unregisterAll(this);
}
}
@@ -0,0 +1,38 @@
package me.legendcraft.antiseedcracker.packets;
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
import com.github.retrooper.packetevents.event.PacketListenerPriority;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerJoinGame;
import me.legendcraft.antiseedcracker.AntiSeedCrackerPlugin;
import org.bukkit.entity.Player;
public final class LoginPacketInterceptor extends PacketListenerAbstract {
private final AntiSeedCrackerPlugin plugin;
public LoginPacketInterceptor(AntiSeedCrackerPlugin plugin) {
super(PacketListenerPriority.HIGHEST);
this.plugin = plugin;
}
@Override
public void onPacketSend(PacketSendEvent event) {
if (event.getPacketType() != PacketType.Play.Server.JOIN_GAME) return;
if (!plugin.getPluginConfig().isInterceptLogin()) return;
Object raw = event.getPlayer();
if (!(raw instanceof Player player)) return;
try {
WrapperPlayServerJoinGame wrapper = new WrapperPlayServerJoinGame(event);
wrapper.setHashedSeed(plugin.getSeedManager().getFakeSeed(player));
event.markForReEncode(true);
} catch (Exception e) {
plugin.getLogger().warning(
"[AntiSeedCracker] Failed to spoof login seed for "
+ player.getName() + ": " + e.getMessage());
}
}
}
@@ -0,0 +1,88 @@
package me.legendcraft.antiseedcracker.packets;
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
import com.github.retrooper.packetevents.event.PacketListenerPriority;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.item.mapdecoration.MapDecorationType;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerMapData;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerMapData.MapDecoration;
import me.legendcraft.antiseedcracker.AntiSeedCrackerPlugin;
import me.legendcraft.antiseedcracker.database.DatabaseManager;
import org.bukkit.entity.Player;
import java.security.SecureRandom;
import java.util.List;
public final class MapSeedInterceptor extends PacketListenerAbstract {
private static final SecureRandom RNG = new SecureRandom();
private final AntiSeedCrackerPlugin plugin;
public MapSeedInterceptor(AntiSeedCrackerPlugin plugin) {
super(PacketListenerPriority.HIGHEST);
this.plugin = plugin;
}
@Override
public void onPacketSend(PacketSendEvent event) {
if (event.getPacketType() != PacketType.Play.Server.MAP_DATA) return;
if (!plugin.getPluginConfig().isTreasureMapProtectionEnabled()) return;
try {
WrapperPlayServerMapData wrapper = new WrapperPlayServerMapData(event);
List<MapDecoration> decorations = wrapper.getDecorations();
if (decorations == null || decorations.isEmpty()) return;
boolean modified = false;
for (MapDecoration deco : decorations) {
if (isStructureDecoration(deco.getType())) {
deco.setX((byte) (RNG.nextInt(256) - 128));
deco.setY((byte) (RNG.nextInt(256) - 128));
modified = true;
}
}
if (modified) {
wrapper.setDecorations(decorations);
event.markForReEncode(true);
DatabaseManager db = plugin.getDatabaseManager();
if (db != null && plugin.getPluginConfig().isDatabaseLogEvents()) {
Object raw = event.getPlayer();
if (raw instanceof Player player) {
db.logEvent(DatabaseManager.EventType.TREASURE_MAP_SCRAMBLED,
player.getUniqueId().toString(),
player.getName(),
player.getWorld().getName(),
"Structure map marker scrambled");
}
}
}
} catch (Exception e) {
plugin.getLogger().fine(
"[AntiSeedCracker] MapSeedInterceptor skipped: " + e.getMessage());
}
}
private static boolean isStructureDecoration(MapDecorationType type) {
try {
String name = type.getName().toString().toUpperCase();
return name.contains("MANSION")
|| name.contains("MONUMENT")
|| name.contains("RED_X")
|| name.contains("TREASURE")
|| name.contains("TRIAL_CHAMBERS")
|| name.contains("VILLAGE")
|| name.contains("PILLAGER")
|| name.contains("BASTION")
|| name.contains("JUNGLE_TEMPLE")
|| name.contains("SWAMP_HUT");
} catch (Exception ignored) {
return false;
}
}
}
@@ -0,0 +1,38 @@
package me.legendcraft.antiseedcracker.packets;
import com.github.retrooper.packetevents.event.PacketListenerAbstract;
import com.github.retrooper.packetevents.event.PacketListenerPriority;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerRespawn;
import me.legendcraft.antiseedcracker.AntiSeedCrackerPlugin;
import org.bukkit.entity.Player;
public final class RespawnPacketInterceptor extends PacketListenerAbstract {
private final AntiSeedCrackerPlugin plugin;
public RespawnPacketInterceptor(AntiSeedCrackerPlugin plugin) {
super(PacketListenerPriority.HIGHEST);
this.plugin = plugin;
}
@Override
public void onPacketSend(PacketSendEvent event) {
if (event.getPacketType() != PacketType.Play.Server.RESPAWN) return;
if (!plugin.getPluginConfig().isInterceptRespawn()) return;
Object raw = event.getPlayer();
if (!(raw instanceof Player player)) return;
try {
WrapperPlayServerRespawn wrapper = new WrapperPlayServerRespawn(event);
wrapper.setHashedSeed(plugin.getSeedManager().getFakeSeed(player));
event.markForReEncode(true);
} catch (Exception e) {
plugin.getLogger().warning(
"[AntiSeedCracker] Failed to spoof respawn seed for "
+ player.getName() + ": " + e.getMessage());
}
}
}
@@ -0,0 +1,5 @@
package me.legendcraft.antiseedcracker.scheduler;
public interface CancelableTask {
void cancel();
}
@@ -0,0 +1,128 @@
package me.legendcraft.antiseedcracker.scheduler;
import me.legendcraft.antiseedcracker.util.PlatformUtil;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Entity;
import org.bukkit.plugin.Plugin;
import java.util.concurrent.TimeUnit;
public final class FoliaSchedulerUtil {
private FoliaSchedulerUtil() {}
public static CancelableTask scheduleAsyncRepeating(Plugin plugin, Runnable task,
long initialDelayMs, long periodMs) {
if (PlatformUtil.IS_PAPER) {
Object handle = plugin.getServer().getAsyncScheduler()
.runAtFixedRate(plugin, t -> task.run(), initialDelayMs, periodMs, TimeUnit.MILLISECONDS);
return () -> {
try {
handle.getClass().getMethod("cancel").invoke(handle);
} catch (Exception ignored) {}
};
}
long delayTicks = Math.max(1L, initialDelayMs / 50L);
long periodTicks = Math.max(1L, periodMs / 50L);
org.bukkit.scheduler.BukkitTask bt = plugin.getServer().getScheduler()
.runTaskTimerAsynchronously(plugin, task, delayTicks, periodTicks);
return bt::cancel;
}
public static void runAsync(Plugin plugin, Runnable task) {
if (PlatformUtil.IS_PAPER) {
plugin.getServer().getAsyncScheduler().runNow(plugin, t -> task.run());
return;
}
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, task);
}
public static void runAtChunk(Plugin plugin, World world, int chunkX, int chunkZ,
Runnable task) {
if (PlatformUtil.IS_FOLIA) {
plugin.getServer().getRegionScheduler()
.run(plugin, world, chunkX, chunkZ, t -> task.run());
return;
}
if (PlatformUtil.IS_PAPER) {
plugin.getServer().getGlobalRegionScheduler().run(plugin, t -> task.run());
return;
}
plugin.getServer().getScheduler().runTask(plugin, task);
}
public static void runAtLocation(Plugin plugin, Location location, Runnable task) {
if (PlatformUtil.IS_FOLIA) {
plugin.getServer().getRegionScheduler()
.run(plugin, location, t -> task.run());
return;
}
if (PlatformUtil.IS_PAPER) {
plugin.getServer().getGlobalRegionScheduler().run(plugin, t -> task.run());
return;
}
plugin.getServer().getScheduler().runTask(plugin, task);
}
public static void runAtLocationDelayed(Plugin plugin, Location location,
Runnable task, long delayTicks) {
if (PlatformUtil.IS_FOLIA) {
plugin.getServer().getRegionScheduler()
.runDelayed(plugin, location, t -> task.run(), delayTicks);
return;
}
if (PlatformUtil.IS_PAPER) {
plugin.getServer().getGlobalRegionScheduler()
.runDelayed(plugin, t -> task.run(), delayTicks);
return;
}
plugin.getServer().getScheduler().runTaskLater(plugin, task, delayTicks);
}
public static void runForEntity(Plugin plugin, Entity entity, Runnable task) {
if (PlatformUtil.IS_FOLIA) {
entity.getScheduler().run(plugin, t -> task.run(), null);
return;
}
if (PlatformUtil.IS_PAPER) {
plugin.getServer().getGlobalRegionScheduler().run(plugin, t -> task.run());
return;
}
plugin.getServer().getScheduler().runTask(plugin, task);
}
public static void runGlobal(Plugin plugin, Runnable task) {
if (PlatformUtil.IS_PAPER) {
plugin.getServer().getGlobalRegionScheduler().run(plugin, t -> task.run());
return;
}
plugin.getServer().getScheduler().runTask(plugin, task);
}
public static void runGlobalDelayed(Plugin plugin, Runnable task, long delayTicks) {
if (PlatformUtil.IS_PAPER) {
plugin.getServer().getGlobalRegionScheduler()
.runDelayed(plugin, t -> task.run(), delayTicks);
return;
}
plugin.getServer().getScheduler().runTaskLater(plugin, task, delayTicks);
}
public static CancelableTask scheduleGlobalRepeating(Plugin plugin, Runnable task,
long initialDelayTicks,
long periodTicks) {
if (PlatformUtil.IS_PAPER) {
Object handle = plugin.getServer().getGlobalRegionScheduler()
.runAtFixedRate(plugin, t -> task.run(), initialDelayTicks, periodTicks);
return () -> {
try {
handle.getClass().getMethod("cancel").invoke(handle);
} catch (Exception ignored) {}
};
}
org.bukkit.scheduler.BukkitTask bt = plugin.getServer().getScheduler()
.runTaskTimer(plugin, task, initialDelayTicks, periodTicks);
return bt::cancel;
}
}
@@ -0,0 +1,103 @@
package me.legendcraft.antiseedcracker.seed;
import org.bukkit.World;
import org.bukkit.entity.Player;
import java.security.SecureRandom;
import java.util.Collection;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Logger;
public final class SeedManager {
private final ConcurrentHashMap<UUID, Long> playerSeeds = new ConcurrentHashMap<>();
private final ConcurrentHashMap<UUID, long[]> fakeStrongholds = new ConcurrentHashMap<>();
private final ConcurrentHashMap<UUID, Long> worldSeeds = new ConcurrentHashMap<>();
private final SecureRandom rng = new SecureRandom();
private final Logger logger;
public SeedManager(Logger logger) {
this.logger = logger;
}
public long assignSeed(Player player) {
long seed = generateSeed();
playerSeeds.put(player.getUniqueId(), seed);
return seed;
}
public long getFakeSeed(Player player) {
return playerSeeds.computeIfAbsent(player.getUniqueId(), k -> generateSeed());
}
public void removePlayer(UUID uuid) {
playerSeeds.remove(uuid);
fakeStrongholds.remove(uuid);
}
public int rotateAll() {
int count = 0;
for (UUID uuid : playerSeeds.keySet()) {
playerSeeds.put(uuid, generateSeed());
count++;
}
return count;
}
public void forgetWorld(UUID worldUID) {
worldSeeds.remove(worldUID);
}
public long assignWorldFakeSeed(World world) {
return worldSeeds.computeIfAbsent(world.getUID(), k -> generateSeed());
}
public long getWorldFakeSeed(World world) {
return worldSeeds.computeIfAbsent(world.getUID(), k -> generateSeed());
}
public void ensureAllPlayersHaveSeeds(Collection<? extends Player> players) {
for (Player player : players) {
playerSeeds.computeIfAbsent(player.getUniqueId(), k -> generateSeed());
}
}
public int[] assignFakeStronghold(Player player, int minDist, int maxDist) {
double angle = rng.nextDouble() * 2.0 * Math.PI;
int dist = minDist + rng.nextInt(Math.max(1, maxDist - minDist + 1));
int x = (int) Math.round(Math.cos(angle) * dist);
int z = (int) Math.round(Math.sin(angle) * dist);
fakeStrongholds.put(player.getUniqueId(), new long[]{x, z});
return new int[]{x, z};
}
public int[] getFakeStronghold(UUID uuid) {
long[] packed = fakeStrongholds.get(uuid);
if (packed == null) return null;
return new int[]{(int) packed[0], (int) packed[1]};
}
public void purgeOfflinePlayers(java.util.Set<UUID> onlineUUIDs) {
int removed = 0;
for (UUID uuid : playerSeeds.keySet()) {
if (!onlineUUIDs.contains(uuid)) {
playerSeeds.remove(uuid);
fakeStrongholds.remove(uuid);
removed++;
}
}
if (removed > 0) {
logger.info("[AntiSeedCracker] Audit removed " + removed + " stale seed entries.");
}
}
public int trackedCount() {
return playerSeeds.size();
}
private long generateSeed() {
return rng.nextLong();
}
}
@@ -0,0 +1,214 @@
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) { }
}
@@ -0,0 +1,51 @@
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;
private final AntiSeedCrackerPlugin plugin;
private volatile CancelableTask handle;
public SeedBroadcastTask(AntiSeedCrackerPlugin plugin) {
this.plugin = plugin;
}
public void start() {
if (handle != null) {
handle.cancel();
}
handle = FoliaSchedulerUtil.scheduleAsyncRepeating(
plugin, this, PERIOD_MS, PERIOD_MS);
}
public void stop() {
if (handle != null) {
handle.cancel();
handle = null;
}
}
@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);
}
}
plugin.getSeedManager().ensureAllPlayersHaveSeeds(
plugin.getServer().getOnlinePlayers());
}
}
@@ -0,0 +1,39 @@
package me.legendcraft.antiseedcracker.tasks;
import me.legendcraft.antiseedcracker.AntiSeedCrackerPlugin;
import me.legendcraft.antiseedcracker.scheduler.CancelableTask;
import me.legendcraft.antiseedcracker.scheduler.FoliaSchedulerUtil;
public final class SeedRotationTask implements Runnable {
private final AntiSeedCrackerPlugin plugin;
private volatile CancelableTask handle;
public SeedRotationTask(AntiSeedCrackerPlugin plugin) {
this.plugin = plugin;
}
public void start() {
if (handle != null) {
handle.cancel();
}
long periodMs = (plugin.getPluginConfig().getSeedRotationIntervalTicks() / 20L) * 1000L;
handle = FoliaSchedulerUtil.scheduleAsyncRepeating(plugin, this, periodMs, periodMs);
}
public void stop() {
if (handle != null) {
handle.cancel();
handle = null;
}
}
@Override
public void run() {
int count = plugin.getSeedManager().rotateAll();
if (count > 0) {
plugin.getLogger().info(
"[AntiSeedCracker] Rotated fake seeds for " + count + " player(s).");
}
}
}
@@ -0,0 +1,37 @@
package me.legendcraft.antiseedcracker.util;
public final class PlatformUtil {
public static final boolean IS_FOLIA;
public static final boolean IS_PURPUR;
public static final boolean IS_PAPER;
static {
IS_FOLIA = classExists("io.papermc.paper.threadedregions.RegionizedServer");
IS_PURPUR = classExists("net.pl3x.purpur.PurpurConfig")
|| classExists("org.purpurmc.purpur.PurpurConfig");
IS_PAPER = IS_FOLIA
|| IS_PURPUR
|| classExists("io.papermc.paper.ServerBuildInfo")
|| classExists("io.papermc.paper.configuration.GlobalConfiguration")
|| classExists("com.destroystokyo.paper.PaperConfig");
}
private PlatformUtil() {}
public static String name() {
if (IS_FOLIA) return "Folia";
if (IS_PURPUR) return "Purpur";
if (IS_PAPER) return "Paper";
return "Spigot/CraftBukkit";
}
private static boolean classExists(String className) {
try {
Class.forName(className);
return true;
} catch (ClassNotFoundException ignored) {
return false;
}
}
}
@@ -0,0 +1,109 @@
package me.legendcraft.antiseedcracker.util;
import com.google.gson.JsonArray;
import com.google.gson.JsonParser;
import me.legendcraft.antiseedcracker.AntiSeedCrackerPlugin;
import me.legendcraft.antiseedcracker.scheduler.FoliaSchedulerUtil;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.logging.Logger;
public final class UpdateChecker {
private static final String API_URL = "https://api.modrinth.com/v2/project/%s/version";
private static final String USER_AGENT = "AntiSeedCracker/%s (Modrinth; contact SunsetRQ7_)";
private static final String UNSET_PLACEHOLDER = "YOUR_MODRINTH_PROJECT_ID";
private final AntiSeedCrackerPlugin plugin;
private final Logger log;
private final String currentVersion;
private final String projectId;
private volatile String latestVersion = null;
private volatile boolean upToDate = true;
public UpdateChecker(AntiSeedCrackerPlugin plugin, String projectId) {
this.plugin = plugin;
this.log = plugin.getLogger();
this.currentVersion = plugin.getPluginMeta().getVersion();
this.projectId = projectId;
}
public void checkAsync() {
if (projectId == null || projectId.isBlank()
|| projectId.equalsIgnoreCase(UNSET_PLACEHOLDER)) {
log.info("[AntiSeedCracker] Update checker skipped — set "
+ "'update_checker.modrinth_id' in config.yml after publishing.");
return;
}
FoliaSchedulerUtil.runAsync(plugin, this::perform);
}
public String getLatestVersion() { return latestVersion; }
public boolean isUpToDate() { return upToDate; }
public String getCurrentVersion() { return currentVersion; }
private void perform() {
try {
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(String.format(API_URL, projectId)))
.header("User-Agent", String.format(USER_AGENT, currentVersion))
.timeout(Duration.ofSeconds(10))
.GET()
.build();
HttpResponse<String> response =
client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 404) {
log.warning("[AntiSeedCracker] Update checker: project '"
+ projectId + "' not found on Modrinth. "
+ "Verify 'update_checker.modrinth_id' in config.yml.");
return;
}
if (response.statusCode() != 200) {
log.fine("[AntiSeedCracker] Update checker: HTTP " + response.statusCode());
return;
}
JsonArray versions = JsonParser.parseString(response.body()).getAsJsonArray();
if (versions.isEmpty()) return;
latestVersion = versions.get(0).getAsJsonObject()
.get("version_number").getAsString();
if (latestVersion.equalsIgnoreCase(currentVersion)) {
upToDate = true;
log.info("[AntiSeedCracker] Running latest version (v" + currentVersion + ").");
} else {
upToDate = false;
log.warning("╔════════════════════════════════════════════╗");
log.warning("║ AntiSeedCracker — Update Available! ║");
log.warning("║ Current : v" + pad(currentVersion, 30) + "║");
log.warning("║ Latest : v" + pad(latestVersion, 30) + "║");
log.warning("║ https://modrinth.com/plugin/"
+ pad(projectId, 14) + "║");
log.warning("╚════════════════════════════════════════════╝");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Exception e) {
log.fine("[AntiSeedCracker] Update check failed: " + e.getMessage());
}
}
private static String pad(String s, int width) {
if (s == null) s = "unknown";
return s.length() >= width ? s.substring(0, width)
: s + " ".repeat(width - s.length());
}
}
+52
View File
@@ -0,0 +1,52 @@
seed_obfuscation:
intercept_login: true
intercept_respawn: true
seed_rotation:
enabled: true
interval_seconds: 60
structure_protection:
end_spikes:
enabled: true
worlds:
- world_the_end
modify_world: true
end_cities:
enabled: true
worlds:
- world_the_end
modify_world: true
spoof_locate_command:
enabled: true
max_offset: 2000
plugin_api_protection:
enabled: true
eye_of_ender_protection:
enabled: true
worlds:
- world
fake_stronghold_min_distance: 800
fake_stronghold_max_distance: 4000
update_checker:
enabled: true
modrinth_id: "YOUR_MODRINTH_PROJECT_ID"
database:
enabled: true
log_events: true
max_event_age_days: 30
treasure_map_protection:
enabled: true
extra_blocked_commands:
- "seedcracker"
- "seedfind"
- "worldseedinfo"
+23
View File
@@ -0,0 +1,23 @@
name: AntiSeedCracker
version: '3.1.0'
main: me.legendcraft.antiseedcracker.AntiSeedCrackerPlugin
api-version: '26.1'
folia-supported: true
author: SunsetRQ7_
description: >
Professional world seed protection plugin for Paper, Purpur, Folia, and Spigot 26.1.2+.
Blocks every known seed-cracking technique: packet-level spoofing, structure
triangulation, Eye of Ender tracking, /locate exploitation, treasure maps,
End spike fingerprinting, End City fingerprinting, and plugin-API interception.
commands:
asc:
aliases: [antiseedcracker]
description: AntiSeedCracker administration command.
usage: /asc <reload|status|stats|info>
permission: antiseedcracker.admin
permissions:
antiseedcracker.admin:
description: Access to AntiSeedCracker administration commands.
default: op