From 2858dc878653f35d562d80321d14e8a994fe161c Mon Sep 17 00:00:00 2001 From: bunchy7s Date: Fri, 22 May 2026 20:28:01 -0500 Subject: [PATCH] Initial commit: SeedProtector plugin for Paper/Folia --- .gitignore | 44 ++++++++++++ pom.xml | 49 +++++++++++++ .../java/seedprotector/SeedProtector.java | 38 ++++++++++ .../listeners/PlayerListener.java | 13 ++++ .../listeners/StructureListener.java | 49 +++++++++++++ .../seedprotector/managers/PluginManager.java | 55 +++++++++++++++ .../utils/ScramblePopulator.java | 70 +++++++++++++++++++ src/main/java/seedprotector/utils/Utils.java | 21 ++++++ src/main/resources/config.json | 23 ++++++ src/main/resources/plugin.yml | 6 ++ 10 files changed, 368 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/seedprotector/SeedProtector.java create mode 100644 src/main/java/seedprotector/listeners/PlayerListener.java create mode 100644 src/main/java/seedprotector/listeners/StructureListener.java create mode 100644 src/main/java/seedprotector/managers/PluginManager.java create mode 100644 src/main/java/seedprotector/utils/ScramblePopulator.java create mode 100644 src/main/java/seedprotector/utils/Utils.java create mode 100644 src/main/resources/config.json create mode 100644 src/main/resources/plugin.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4e7165b --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..950a57e --- /dev/null +++ b/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + seedprotector + SeedProtector + 1.0 + + Seed Protector + https://example.com + + + UTF-8 + 21 + 21 + + + + + papermc + https://repo.papermc.io/repository/maven-public/ + + + + + + io.papermc.paper + paper-api + 1.21-R0.1-SNAPSHOT + provided + + + + + ${project.basedir}/src/main/java + + + ${project.basedir}/src/main/resources + + plugin.yml + + + + + + + \ No newline at end of file diff --git a/src/main/java/seedprotector/SeedProtector.java b/src/main/java/seedprotector/SeedProtector.java new file mode 100644 index 0000000..a94df15 --- /dev/null +++ b/src/main/java/seedprotector/SeedProtector.java @@ -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!"); + } +} diff --git a/src/main/java/seedprotector/listeners/PlayerListener.java b/src/main/java/seedprotector/listeners/PlayerListener.java new file mode 100644 index 0000000..8fffb63 --- /dev/null +++ b/src/main/java/seedprotector/listeners/PlayerListener.java @@ -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 + } +} \ No newline at end of file diff --git a/src/main/java/seedprotector/listeners/StructureListener.java b/src/main/java/seedprotector/listeners/StructureListener.java new file mode 100644 index 0000000..6e01da4 --- /dev/null +++ b/src/main/java/seedprotector/listeners/StructureListener.java @@ -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); + } + } +} diff --git a/src/main/java/seedprotector/managers/PluginManager.java b/src/main/java/seedprotector/managers/PluginManager.java new file mode 100644 index 0000000..2317b15 --- /dev/null +++ b/src/main/java/seedprotector/managers/PluginManager.java @@ -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 exemptStructures = new HashSet<>(); + private final Set 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 getExemptStructures() { return exemptStructures; } + public Set getIncludedStructures() { return includedStructures; } +} \ No newline at end of file diff --git a/src/main/java/seedprotector/utils/ScramblePopulator.java b/src/main/java/seedprotector/utils/ScramblePopulator.java new file mode 100644 index 0000000..e733bad --- /dev/null +++ b/src/main/java/seedprotector/utils/ScramblePopulator.java @@ -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; + } +} diff --git a/src/main/java/seedprotector/utils/Utils.java b/src/main/java/seedprotector/utils/Utils.java new file mode 100644 index 0000000..851a69e --- /dev/null +++ b/src/main/java/seedprotector/utils/Utils.java @@ -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) + ); + } +} \ No newline at end of file diff --git a/src/main/resources/config.json b/src/main/resources/config.json new file mode 100644 index 0000000..f2f0c27 --- /dev/null +++ b/src/main/resources/config.json @@ -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" + ] +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..ddcdb72 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,6 @@ +main: seedprotector.SeedProtector +version: 1.0 +name: SeedProtector +author: bunchy7s +api-version: '1.21' +folia-supported: true