feat: AntiSeedCracker v3.1.0 — professional multi-layer seed protection
This commit is contained in:
commit
61418c43c2
32 files changed
+2769
No files matched your search
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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/
|
||||
@@ -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 |
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
+110
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
Reference in new issue
Block a user