Initial commit: SeedProtector plugin for Paper/Folia

This commit is contained in:
Professor Doom 2026-05-22 20:28:01 -05:00
commit 2858dc8786
10 changed files with 368 additions and 0 deletions

44
.gitignore vendored Normal file
View file

@ -0,0 +1,44 @@
# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
build.log
.incremental/
.polyglot.build.properties
# IDE - VSCode
.vscode/
*.code-workspace
# OS
.DS_Store
Thumbs.db
# Test Server Files
foila test/
logs/
plugins/
world/
world_nether/
world_the_end/
usercache.json
whitelist.json
ops.json
banned-ips.json
banned-players.json
server.properties
bukkit.yml
spigot.yml
paper-global.yml
eula.txt
help.yml
commands.yml
permissions.yml
version_history.json
cache/
libraries/
versions/

49
pom.xml Normal file
View file

@ -0,0 +1,49 @@
<?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://www.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>seedprotector</groupId>
<artifactId>SeedProtector</artifactId>
<version>1.0</version>
<name>Seed Protector</name>
<url>https://example.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>papermc</id>
<url>https://repo.papermc.io/repository/maven-public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.papermc.paper</groupId>
<artifactId>paper-api</artifactId>
<version>1.21-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
<resources>
<resource>
<directory>${project.basedir}/src/main/resources</directory>
<includes>
<include>plugin.yml</include>
</includes>
</resource>
</resources>
<plugins>
</plugins>
</build>
</project>

View file

@ -0,0 +1,38 @@
package seedprotector;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.world.WorldInitEvent;
import org.bukkit.plugin.java.JavaPlugin;
import seedprotector.listeners.StructureListener;
import seedprotector.managers.PluginManager;
import seedprotector.utils.ScramblePopulator;
public class SeedProtector extends JavaPlugin implements Listener {
@Override
public void onEnable() {
// Initialize managers
PluginManager.getInstance().initialize(this);
// Register structure listener
getServer().getPluginManager().registerEvents(new StructureListener(), this);
// Register world init listener to inject populator
getServer().getPluginManager().registerEvents(this, this);
getLogger().info("Seed Protector has been enabled!");
}
@EventHandler
public void onWorldInit(WorldInitEvent event) {
// Add the scramble populator to every world as it initializes
event.getWorld().getPopulators().add(new ScramblePopulator());
getLogger().info("Injected ScramblePopulator into world: " + event.getWorld().getName());
}
@Override
public void onDisable() {
getLogger().info("Seed Protector has been disabled!");
}
}

View file

@ -0,0 +1,13 @@
package seedprotector.listeners;
import org.bukkit.event.Listener;
import org.bukkit.event.EventHandler;
import org.bukkit.event.player.PlayerJoinEvent;
public class PlayerListener implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
// Handle player join event
}
}

View file

@ -0,0 +1,49 @@
package seedprotector.listeners;
import org.bukkit.Bukkit;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.world.AsyncStructureSpawnEvent;
import org.bukkit.generator.structure.Structure;
import seedprotector.managers.PluginManager;
import java.util.Random;
public class StructureListener implements Listener {
@EventHandler
public void onStructureSpawn(AsyncStructureSpawnEvent event) {
Structure structure = event.getStructure();
String structureKey = structure.getKey().toString();
PluginManager config = PluginManager.getInstance();
// 1. Check if structure is exempt
if (config.getExemptStructures().contains(structureKey)) {
return;
}
// 2. Check if structure is included (if list is not empty)
if (!config.getIncludedStructures().isEmpty() && !config.getIncludedStructures().contains(structureKey)) {
return;
}
// 3. Prevent /locate infinite loops by only cancelling during actual generation
// If the chunk is already loaded, it's likely a scan/locate command
if (event.getWorld().isChunkLoaded(event.getChunkX(), event.getChunkZ())) {
return;
}
// 4. Deterministic cancellation based on salt and percentage
long salt = config.getSecretSalt();
int chunkX = event.getChunkX();
int chunkZ = event.getChunkZ();
long combined = salt ^ ((long) chunkX << 32) ^ chunkZ ^ structure.hashCode();
Random random = new Random(combined);
if (random.nextDouble() < config.getObscurePercentage()) {
event.setCancelled(true);
Bukkit.getLogger().info("[SeedProtector] Obscured " + structureKey + " at " + chunkX + "," + chunkZ);
}
}
}

View file

@ -0,0 +1,55 @@
package seedprotector.managers;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import org.bukkit.plugin.Plugin;
import java.io.File;
import java.io.FileReader;
import java.io.InputStream;
import java.nio.file.Files;
import java.util.HashSet;
import java.util.Set;
public class PluginManager {
private static PluginManager instance;
private long secretSalt;
private double obscurePercentage;
private final Set<String> exemptStructures = new HashSet<>();
private final Set<String> includedStructures = new HashSet<>();
public static PluginManager getInstance() {
if (instance == null) {
instance = new PluginManager();
}
return instance;
}
public void initialize(Plugin plugin) {
File configFile = new File(plugin.getDataFolder(), "config.json");
if (!configFile.exists()) {
plugin.getDataFolder().mkdirs();
try (InputStream in = plugin.getResource("config.json")) {
Files.copy(in, configFile.toPath());
} catch (Exception e) {
plugin.getLogger().severe("Could not save default config.json");
}
}
try (FileReader reader = new FileReader(configFile)) {
JsonObject json = new Gson().fromJson(reader, JsonObject.class);
this.secretSalt = json.get("secret_salt").getAsLong();
this.obscurePercentage = json.get("obscure_percentage").getAsDouble();
json.get("exempt_structures").getAsJsonArray().forEach(e -> exemptStructures.add(e.getAsString()));
json.get("included_structures").getAsJsonArray().forEach(e -> includedStructures.add(e.getAsString()));
} catch (Exception e) {
plugin.getLogger().severe("Could not load config.json: " + e.getMessage());
}
}
public long getSecretSalt() { return secretSalt; }
public double getObscurePercentage() { return obscurePercentage; }
public Set<String> getExemptStructures() { return exemptStructures; }
public Set<String> getIncludedStructures() { return includedStructures; }
}

View file

@ -0,0 +1,70 @@
package seedprotector.utils;
import org.bukkit.Material;
import org.bukkit.generator.BlockPopulator;
import org.bukkit.generator.LimitedRegion;
import org.bukkit.generator.WorldInfo;
import seedprotector.managers.PluginManager;
import java.util.Random;
public class ScramblePopulator extends BlockPopulator {
@Override
public void populate(WorldInfo worldInfo, Random random, int chunkX, int chunkZ, LimitedRegion limitedRegion) {
long salt = PluginManager.getInstance().getSecretSalt();
// Create a deterministic random for this chunk based on salt
long chunkSeed = salt ^ ((long) chunkX << 32) ^ chunkZ ^ worldInfo.getSeed();
Random chunkRandom = new Random(chunkSeed);
// Iterate through the chunk to find and "scramble" decorators
// We focus on Y levels where ores and flowers are common
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
int realX = (chunkX << 4) + x;
int realZ = (chunkZ << 4) + z;
for (int y = worldInfo.getMinHeight(); y < worldInfo.getMaxHeight(); y++) {
Material type = limitedRegion.getType(realX, y, realZ);
// Scramble Ores
if (isOre(type)) {
// 10% chance to move the ore slightly
if (chunkRandom.nextDouble() < 0.10) {
int offsetX = chunkRandom.nextInt(3) - 1; // -1, 0, 1
int offsetY = chunkRandom.nextInt(3) - 1;
int offsetZ = chunkRandom.nextInt(3) - 1;
int targetX = realX + offsetX;
int targetY = y + offsetY;
int targetZ = realZ + offsetZ;
if (limitedRegion.isInRegion(targetX, targetY, targetZ) &&
limitedRegion.getType(targetX, targetY, targetZ) == Material.STONE) {
limitedRegion.setType(realX, y, realZ, Material.STONE);
limitedRegion.setType(targetX, targetY, targetZ, type);
}
}
}
// Scramble Flowers/Vegetation on surface
if (isVegetation(type)) {
// 20% chance to remove or swap vegetation
if (chunkRandom.nextDouble() < 0.20) {
limitedRegion.setType(realX, y, realZ, Material.AIR);
}
}
}
}
}
}
private boolean isOre(Material type) {
return type.name().endsWith("_ORE");
}
private boolean isVegetation(Material type) {
return type == Material.TALL_GRASS || type == Material.POPPY ||
type == Material.DANDELION || type == Material.SUGAR_CANE;
}
}

View file

@ -0,0 +1,21 @@
package seedprotector.utils;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
public class Utils {
public static Component colorize(String msg) {
if (msg.contains("<") && msg.contains(">")) {
return MiniMessage.miniMessage().deserialize(msg);
}
return LegacyComponentSerializer.legacyAmpersand().deserialize(msg);
}
public static String legacyColorize(String msg) {
return LegacyComponentSerializer.legacyAmpersand().serialize(
LegacyComponentSerializer.legacyAmpersand().deserialize(msg)
);
}
}

View file

@ -0,0 +1,23 @@
{
"secret_salt": 123456789,
"obscure_percentage": 0.5,
"exempt_structures": [
"minecraft:stronghold",
"minecraft:fortress",
"minecraft:end_city"
],
"included_structures": [
"minecraft:village_savanna",
"minecraft:village_desert",
"minecraft:village_plains",
"minecraft:village_snowy",
"minecraft:village_taiga",
"minecraft:desert_pyramid",
"minecraft:jungle_pyramid",
"minecraft:swamp_hut",
"minecraft:pillager_outpost",
"minecraft:ocean_monument",
"minecraft:shipwreck",
"minecraft:buried_treasure"
]
}

View file

@ -0,0 +1,6 @@
main: seedprotector.SeedProtector
version: 1.0
name: SeedProtector
author: bunchy7s
api-version: '1.21'
folia-supported: true