diff --git a/.gitignore b/.gitignore index 805aaf9..c28e484 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build/ # IDE .idea/ +.run/ *.iml # Test server @@ -17,3 +18,14 @@ Thumbs.db codex-build.log .claude/ Get +# Local design docs and plan notes - not tracked +docs/ + +# Local IDE/tooling and generated debris +.vscode/ +.classpath +.project +.settings/ +target/ +out/ +dependency-reduced-pom.xml diff --git a/build.gradle b/build.gradle index e74b8ee..7e812c0 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { } group = 'com.magmaguy' -version = '2.3.1' +version = '2.5.0' repositories { mavenCentral() @@ -65,7 +65,20 @@ shadowJar { duplicatesStrategy = DuplicatesStrategy.EXCLUDE archiveClassifier.set(null) archiveFileName.set(project.name + ".jar") - destinationDirectory.set(new File("testbed/plugins")) +} + +// --- Shared dist mirror ----------------------------------------------------- +// Copy the finished shaded jar into the top-level dist/ folder so the testbeds +// and the release tool pick every build up from one place. Gated on MC_DIST_DIR +// (or -PmcDistDir) so clones/CI that don't set it just build to build/libs and +// no machine-specific path is ever committed. +def mcDistDir = System.getenv('MC_DIST_DIR') ?: project.findProperty('mcDistDir') +if (mcDistDir) { + tasks.register('mirrorToDist', Copy) { + from tasks.named('shadowJar') + into mcDistDir + } + tasks.named('shadowJar') { finalizedBy('mirrorToDist') } } tasks.withType(JavaCompile) { diff --git a/src/main/java/com/magmaguy/betterstructures/BetterStructures.java b/src/main/java/com/magmaguy/betterstructures/BetterStructures.java index c65dbaa..8f8d71d 100644 --- a/src/main/java/com/magmaguy/betterstructures/BetterStructures.java +++ b/src/main/java/com/magmaguy/betterstructures/BetterStructures.java @@ -24,6 +24,13 @@ import com.magmaguy.magmacore.initialization.PluginInitializationConfig; import com.magmaguy.magmacore.initialization.PluginInitializationContext; import com.magmaguy.magmacore.initialization.PluginInitializationState; +import com.magmaguy.magmacore.nightbreak.NightbreakDownloadContentCommand; +import com.magmaguy.magmacore.nightbreak.NightbreakDownloadEverythingCommand; +import com.magmaguy.magmacore.nightbreak.NightbreakDownloadPluginUpdateCommand; +import com.magmaguy.magmacore.nightbreak.NightbreakPluginSpec; +import com.magmaguy.magmacore.nightbreak.NightbreakPluginUpdater; +import com.magmaguy.magmacore.nightbreak.NightbreakPluginStateRegistry; +import com.magmaguy.magmacore.nightbreak.NightbreakRecommendedPluginsCommand; import com.magmaguy.magmacore.util.Logger; import org.bstats.bukkit.Metrics; import org.bukkit.Bukkit; @@ -32,8 +39,17 @@ import org.bukkit.plugin.java.JavaPlugin; import java.io.IOException; +import java.util.ArrayList; public final class BetterStructures extends JavaPlugin { + public static final NightbreakPluginSpec NIGHTBREAK_PLUGIN_SPEC = new NightbreakPluginSpec( + "BetterStructures", + "bs", + "betterstructures.*", + "betterstructures.setup", + "betterstructures.initialize", + "https://nightbreak.io/plugin/betterstructures/", + "Reloaded BetterStructures."); @Override public void onEnable() { @@ -51,14 +67,20 @@ public void onEnable() { throw new RuntimeException(e); } MagmaCore.onEnable(this); + MagmaCore.exportSharedAssets(this); MagmaCore.startInitialization(this, new PluginInitializationConfig("BetterStructures", "betterstructures.*", 16), this::asyncInitialization, this::syncInitialization, () -> { Logger.info("BetterStructures fully initialized!"); - if (MetadataHandler.pendingReloadSender != null) { - Logger.sendMessage(MetadataHandler.pendingReloadSender, "Reloaded BetterStructures."); + NightbreakPluginUpdater.autoDownloadPluginUpdateIfEnabled(this, NIGHTBREAK_PLUGIN_SPEC); + CommandSender pendingReloadSender = NightbreakPluginStateRegistry.consumePendingReloadSender(this); + if (pendingReloadSender == null) { + pendingReloadSender = MetadataHandler.pendingReloadSender; + } + if (pendingReloadSender != null) { + Logger.sendMessage(pendingReloadSender, NIGHTBREAK_PLUGIN_SPEC.reloadSuccessMessage()); MetadataHandler.pendingReloadSender = null; } }, @@ -150,8 +172,22 @@ private void syncInitialization(PluginInitializationContext initializationContex commandManager.registerCommand(new VersionCommand()); commandManager.registerCommand(new SetupCommand()); commandManager.registerCommand(new FirstTimeSetupCommand()); - commandManager.registerCommand(new DownloadAllContentCommand()); - commandManager.registerCommand(new UpdateContentCommand()); + commandManager.registerCommand(new NightbreakRecommendedPluginsCommand(this, NIGHTBREAK_PLUGIN_SPEC)); + commandManager.registerCommand(new NightbreakDownloadPluginUpdateCommand(this, NIGHTBREAK_PLUGIN_SPEC)); + commandManager.registerCommand(new NightbreakDownloadEverythingCommand<>(this, + NIGHTBREAK_PLUGIN_SPEC, + () -> new ArrayList<>(BSPackage.getBsPackages().values()), + ReloadCommand::reload)); + commandManager.registerCommand(new NightbreakDownloadContentCommand<>(this, + NIGHTBREAK_PLUGIN_SPEC, + () -> new ArrayList<>(BSPackage.getBsPackages().values()), + ReloadCommand::reload, + false)); + commandManager.registerCommand(new NightbreakDownloadContentCommand<>(this, + NIGHTBREAK_PLUGIN_SPEC, + () -> new ArrayList<>(BSPackage.getBsPackages().values()), + ReloadCommand::reload, + true)); commandManager.registerCommand(new GenerateModulesCommand()); commandManager.registerCommand(new BetterStructuresCommand()); diff --git a/src/main/java/com/magmaguy/betterstructures/buildingfitter/FitAnything.java b/src/main/java/com/magmaguy/betterstructures/buildingfitter/FitAnything.java index deb105a..54fa5bc 100644 --- a/src/main/java/com/magmaguy/betterstructures/buildingfitter/FitAnything.java +++ b/src/main/java/com/magmaguy/betterstructures/buildingfitter/FitAnything.java @@ -5,6 +5,7 @@ import com.magmaguy.betterstructures.buildingfitter.util.FitUndergroundDeepBuilding; import com.magmaguy.betterstructures.buildingfitter.util.LocationProjector; import com.magmaguy.betterstructures.buildingfitter.util.SchematicPicker; +import com.magmaguy.betterstructures.chests.ChestContents; import com.magmaguy.betterstructures.config.DefaultConfig; import com.magmaguy.betterstructures.config.generators.GeneratorConfigFields; import com.magmaguy.betterstructures.schematics.SchematicContainer; @@ -288,30 +289,47 @@ private void clearTrees(Location location) { } private void fillChests() { - if (schematicContainer.getGeneratorConfigFields().getChestContents() != null) - for (Vector chestPosition : schematicContainer.getChestLocations()) { - Location chestLocation = LocationProjector.project(location, schematicOffset, chestPosition); - if (!(chestLocation.getBlock().getState() instanceof Container container)) { - Logger.warn("Expected a container for " + chestLocation.getBlock().getType() + " but didn't get it. Skipping this loot!"); - continue; - } + GeneratorConfigFields gen = schematicContainer.getGeneratorConfigFields(); + boolean barrelsEnabled = gen.isGenerateLootInBarrels() && gen.getBarrelContents() != null; + boolean chestsEnabled = gen.getChestContents() != null; + if (!barrelsEnabled && !chestsEnabled) return; + + for (Vector chestPosition : schematicContainer.getChestLocations()) { + Location chestLocation = LocationProjector.project(location, schematicOffset, chestPosition); + if (!(chestLocation.getBlock().getState() instanceof Container container)) { + Logger.warn("Expected a container for " + chestLocation.getBlock().getType() + " but didn't get it. Skipping this loot!"); + continue; + } - String treasureFilename; - if (schematicContainer.getChestContents() != null) { - schematicContainer.getChestContents().rollChestContents(container); - treasureFilename = schematicContainer.getSchematicConfigField().getTreasureFile(); - } else { - schematicContainer.getGeneratorConfigFields().getChestContents().rollChestContents(container); - treasureFilename = schematicContainer.getGeneratorConfigFields().getTreasureFilename(); - } + boolean isBarrel = container.getBlock().getType() == Material.BARREL; + if (isBarrel && !barrelsEnabled) continue; + if (!isBarrel && !chestsEnabled) continue; + + ChestContents contents; + String treasureFilename; + if (isBarrel) { + contents = schematicContainer.getBarrelContents(); + String schematicBarrelFile = schematicContainer.getSchematicConfigField().getBarrelTreasureFilename(); + treasureFilename = (schematicBarrelFile != null && !schematicBarrelFile.isEmpty()) + ? schematicBarrelFile + : gen.getBarrelTreasureFilename(); + } else { + contents = schematicContainer.getChestContents(); + String schematicTreasureFile = schematicContainer.getSchematicConfigField().getTreasureFile(); + treasureFilename = (schematicTreasureFile != null && !schematicTreasureFile.isEmpty()) + ? schematicTreasureFile + : gen.getTreasureFilename(); + } - ChestFillEvent chestFillEvent = new ChestFillEvent(container, treasureFilename); - Bukkit.getServer().getPluginManager().callEvent(chestFillEvent); - if (!chestFillEvent.isCancelled()) { - container.update(true); + if (contents == null) continue; + contents.rollChestContents(container); - } + ChestFillEvent chestFillEvent = new ChestFillEvent(container, treasureFilename); + Bukkit.getServer().getPluginManager().callEvent(chestFillEvent); + if (!chestFillEvent.isCancelled()) { + container.update(true); } + } } private void spawnEntities() { diff --git a/src/main/java/com/magmaguy/betterstructures/commands/BetterStructuresCommand.java b/src/main/java/com/magmaguy/betterstructures/commands/BetterStructuresCommand.java index 6ff5823..a8ac5c5 100644 --- a/src/main/java/com/magmaguy/betterstructures/commands/BetterStructuresCommand.java +++ b/src/main/java/com/magmaguy/betterstructures/commands/BetterStructuresCommand.java @@ -19,8 +19,9 @@ public BetterStructuresCommand() { @Override public void execute(CommandData commandData) { Logger.sendMessage(commandData.getCommandSender(), "BetterStructures is a plugin that adds random structures to your Minecraft world!"); - Logger.sendMessage(commandData.getCommandSender(), "You can check installed content and download structure packs in the &2/betterstructures setup &fcommand."); - Logger.sendMessage(commandData.getCommandSender(), "If your Nightbreak account is linked, you can install everything with &2/betterstructures downloadall&f."); + Logger.sendMessage(commandData.getCommandSender(), "Use &2/bs setup &fto manage structure packs, plugin updates, and update settings."); + Logger.sendMessage(commandData.getCommandSender(), "Use &2/bs recommendedplugins &fto see plugins that work well with BetterStructures."); + Logger.sendMessage(commandData.getCommandSender(), "Use &2/bs downloadall &fto check plugin and content updates from one command."); Logger.sendMessage(commandData.getCommandSender(), "Once a pack is installed, structures will automatically generate in freshly generated chunks. You do not have to run any commands for this to happen."); Logger.sendMessage(commandData.getCommandSender(), "By default, OPs will get notified about new structures generating until they disable these messages."); } diff --git a/src/main/java/com/magmaguy/betterstructures/commands/DownloadAllContentCommand.java b/src/main/java/com/magmaguy/betterstructures/commands/DownloadAllContentCommand.java index c7ebe54..ca396e8 100644 --- a/src/main/java/com/magmaguy/betterstructures/commands/DownloadAllContentCommand.java +++ b/src/main/java/com/magmaguy/betterstructures/commands/DownloadAllContentCommand.java @@ -7,6 +7,7 @@ import com.magmaguy.magmacore.command.SenderType; import com.magmaguy.magmacore.nightbreak.NightbreakAccount; import com.magmaguy.magmacore.nightbreak.NightbreakContentManager; +import com.magmaguy.magmacore.nightbreak.NightbreakSetupMenuHelper; import com.magmaguy.magmacore.util.Logger; import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; @@ -25,11 +26,11 @@ public class DownloadAllContentCommand extends AdvancedCommand { static final AtomicBoolean IS_BULK_DOWNLOADING = new AtomicBoolean(false); public DownloadAllContentCommand() { - super(List.of("downloadall")); + super(List.of("downloadallcontent")); setPermission("betterstructures.setup"); setSenderType(SenderType.ANY); - setDescription("Downloads all BetterStructures content available through Nightbreak."); - setUsage("/bs downloadall"); + setDescription("Downloads all available BetterStructures content."); + setUsage("/bs downloadallcontent"); } @Override @@ -39,7 +40,11 @@ public void execute(CommandData commandData) { public static void execute(CommandSender sender, boolean updatesOnly) { if (!NightbreakAccount.hasToken()) { - Logger.sendSimpleMessage(sender, "&cLink your Nightbreak account first with &a/nightbreaklogin &c."); + Logger.sendSimpleMessage(sender, "&cConnect this server first with &a/nightbreaklogin &c."); + return; + } + if (NightbreakAccount.hasAuthFailure()) { + NightbreakSetupMenuHelper.sendTokenUpdatePrompt(sender, "BetterStructures"); return; } @@ -54,7 +59,7 @@ public static void execute(CommandSender sender, boolean updatesOnly) { IS_BULK_DOWNLOADING.set(false); Logger.sendSimpleMessage(sender, updatesOnly ? "&aAll BetterStructures content is already up to date." - : "&aAll BetterStructures Nightbreak content is already downloaded and up to date."); + : "&aAll BetterStructures content is already downloaded and up to date."); return; } @@ -112,6 +117,7 @@ private static void downloadNext(JavaPlugin plugin, if (success) { completed.incrementAndGet(); } else { + if (abortIfAuthFailure(sender, player)) return; failed.incrementAndGet(); failedNames.add(bsPackage.getDisplayName()); } @@ -149,6 +155,7 @@ private static void downloadNext(JavaPlugin plugin, : "&aDownloaded " + bsPackage.getDisplayName() + "&a."); } } else { + if (abortIfAuthFailure(sender, player)) return; failed.incrementAndGet(); failedNames.add(bsPackage.getDisplayName()); if (player == null || player.isOnline()) { @@ -158,4 +165,12 @@ private static void downloadNext(JavaPlugin plugin, downloadNext(plugin, packages, index + 1, importsFolder, sender, player, completed, failed, failedNames, updatesOnly); }); } + + private static boolean abortIfAuthFailure(CommandSender sender, Player player) { + if (!NightbreakAccount.hasAuthFailure()) return false; + IS_BULK_DOWNLOADING.set(false); + CommandSender target = player != null && !player.isOnline() ? Bukkit.getConsoleSender() : sender; + NightbreakSetupMenuHelper.sendTokenUpdatePrompt(target, "BetterStructures"); + return true; + } } diff --git a/src/main/java/com/magmaguy/betterstructures/commands/UpdateContentCommand.java b/src/main/java/com/magmaguy/betterstructures/commands/UpdateContentCommand.java index e44e907..5ff44e2 100644 --- a/src/main/java/com/magmaguy/betterstructures/commands/UpdateContentCommand.java +++ b/src/main/java/com/magmaguy/betterstructures/commands/UpdateContentCommand.java @@ -7,10 +7,10 @@ public class UpdateContentCommand extends AdvancedCommand { public UpdateContentCommand() { - super(List.of("updatecontent", "updateall")); + super(List.of("updatecontent", "updateallcontent")); setPermission("betterstructures.setup"); setSenderType(SenderType.ANY); - setDescription("Downloads updates for outdated BetterStructures Nightbreak content."); + setDescription("Downloads updates for outdated BetterStructures content."); setUsage("/bs updatecontent"); } diff --git a/src/main/java/com/magmaguy/betterstructures/config/DefaultConfig.java b/src/main/java/com/magmaguy/betterstructures/config/DefaultConfig.java index 4558acd..91fc4bc 100644 --- a/src/main/java/com/magmaguy/betterstructures/config/DefaultConfig.java +++ b/src/main/java/com/magmaguy/betterstructures/config/DefaultConfig.java @@ -2,6 +2,7 @@ import com.magmaguy.magmacore.config.ConfigurationEngine; import com.magmaguy.magmacore.config.ConfigurationFile; +import com.magmaguy.magmacore.nightbreak.NightbreakPluginUpdater; import lombok.Getter; import java.util.List; @@ -76,6 +77,8 @@ public class DefaultConfig extends ConfigurationFile { @Getter private static int spawnProtectionRadius; + @Getter + private static boolean autoDownloadPluginUpdates; public DefaultConfig() { super("config.yml"); @@ -120,6 +123,7 @@ public void initializeValues() { percentageOfTickUsedForPregeneration = ConfigurationEngine.setDouble(List.of("Sets the maximum percentage of a tick that BetterStructures will use for world pregeneration when using the pregenerate command.", "Ranges from 0.01 to 1, where 0.01 is 1% and 1 is 100%.", "This controls how much of each server tick is dedicated to generating chunks, allowing you to balance generation speed with server performance.", "Lower values will generate chunks more slowly but reduce server lag, while higher values will generate faster but may impact server performance."), fileConfiguration, "percentageOfTickUsedForPregeneration", 0.1); pregenerationTPSPauseThreshold = ConfigurationEngine.setDouble(List.of("The TPS threshold at which chunk pregeneration will pause to protect server performance.", "When server TPS drops below this value, pregeneration will pause until TPS recovers.", "Default: 12.0"), fileConfiguration, "pregenerationTPSPauseThreshold", 12.0); pregenerationTPSResumeThreshold = ConfigurationEngine.setDouble(List.of("The TPS threshold at which chunk pregeneration will resume after being paused.", "Pregeneration will only resume when server TPS is at or above this value.", "Should be higher than the pause threshold to prevent rapid pause/resume cycles.", "Default: 14.0"), fileConfiguration, "pregenerationTPSResumeThreshold", 14.0); + autoDownloadPluginUpdates = NightbreakPluginUpdater.setAutoDownloadConfigDefault(fileConfiguration); // Initialize the distances from configuration distanceSurface = ConfigurationEngine.setInt( @@ -193,4 +197,4 @@ public void initializeValues() { ConfigurationEngine.fileSaverOnlyDefaults(fileConfiguration, file); } -} \ No newline at end of file +} diff --git a/src/main/java/com/magmaguy/betterstructures/config/ValidWorldsConfig.java b/src/main/java/com/magmaguy/betterstructures/config/ValidWorldsConfig.java index 4251436..eb9b18c 100644 --- a/src/main/java/com/magmaguy/betterstructures/config/ValidWorldsConfig.java +++ b/src/main/java/com/magmaguy/betterstructures/config/ValidWorldsConfig.java @@ -1,7 +1,9 @@ package com.magmaguy.betterstructures.config; +import com.magmaguy.betterstructures.MetadataHandler; import com.magmaguy.magmacore.config.ConfigurationEngine; import com.magmaguy.magmacore.config.ConfigurationFile; +import com.magmaguy.magmacore.util.WorldFolderResolver; import lombok.Getter; import org.bukkit.Bukkit; import org.bukkit.World; @@ -10,14 +12,17 @@ import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; import org.bukkit.event.world.WorldLoadEvent; +import org.bukkit.event.world.WorldUnloadEvent; +import org.bukkit.scheduler.BukkitRunnable; import java.util.ArrayList; import java.util.HashMap; -import java.util.List; public class ValidWorldsConfig extends ConfigurationFile { + private static final String VALID_WORLDS_KEY = "Valid worlds"; + private static final long UNLOAD_PRUNE_DELAY_TICKS = 20L * 10L; @Getter - private static HashMap validWorlds = new HashMap<>(); + private static HashMap validWorlds = new HashMap<>(); @Getter private static boolean whitelistNewWorlds; private static ValidWorldsConfig instance; @@ -28,41 +33,88 @@ public ValidWorldsConfig() { } public static void registerNewWorld(World world) { - if (instance.fileConfiguration.getKeys(true).contains("Valid worlds." + world.getName())) { - validWorlds.put(world, instance.fileConfiguration.getBoolean("Valid worlds." + world.getName())); - return; + if (world == null || instance == null) return; + registerWorldName(world.getName(), whitelistNewWorlds, true); + } + + private static void registerWorldName(String worldName, boolean defaultValue, boolean save) { + ConfigurationSection validWorldsSection = getOrCreateValidWorldsSection(); + if (!validWorldsSection.contains(worldName)) { + instance.fileConfiguration.set(validWorldsPath(worldName), defaultValue); + if (save) + ConfigurationEngine.fileSaverCustomValues(instance.fileConfiguration, instance.file); } - ConfigurationEngine.setBoolean(instance.fileConfiguration, "Valid worlds." + world.getName(), whitelistNewWorlds); - ConfigurationEngine.fileSaverOnlyDefaults(instance.fileConfiguration, instance.file); - validWorlds.put(world, whitelistNewWorlds); + validWorlds.put(worldName, instance.fileConfiguration.getBoolean(validWorldsPath(worldName))); + } + + private static ConfigurationSection getOrCreateValidWorldsSection() { + ConfigurationSection validWorldsSection = instance.fileConfiguration.getConfigurationSection(VALID_WORLDS_KEY); + if (validWorldsSection != null) return validWorldsSection; + return instance.fileConfiguration.createSection(VALID_WORLDS_KEY); + } + + private static String validWorldsPath(String worldName) { + return VALID_WORLDS_KEY + "." + worldName; + } + + public static void unregisterWorld(World world) { + if (world == null) return; + validWorlds.remove(world.getName()); + } + + private static void pruneMissingWorldEntry(String worldName) { + if (instance == null || worldName == null) return; + if (Bukkit.getWorld(worldName) != null || WorldFolderResolver.folderExists(worldName)) return; + + ConfigurationSection validWorldsSection = instance.fileConfiguration.getConfigurationSection(VALID_WORLDS_KEY); + if (validWorldsSection == null || !validWorldsSection.contains(worldName)) return; + + instance.fileConfiguration.set(validWorldsPath(worldName), null); + validWorlds.remove(worldName); + ConfigurationEngine.fileSaverCustomValues(instance.fileConfiguration, instance.file); + } + + private void pruneMissingWorldEntries() { + ConfigurationSection validWorldsSection = fileConfiguration.getConfigurationSection(VALID_WORLDS_KEY); + if (validWorldsSection == null) return; + + for (String worldName : new ArrayList<>(validWorldsSection.getKeys(false))) { + if (Bukkit.getWorld(worldName) != null || WorldFolderResolver.folderExists(worldName)) continue; + fileConfiguration.set(validWorldsPath(worldName), null); + validWorlds.remove(worldName); + } } public static boolean isValidWorld(World world) { - if (validWorlds.get(world) != null) - return validWorlds.get(world); + if (world == null) return false; + if (validWorlds.get(world.getName()) != null) + return validWorlds.get(world.getName()); + registerNewWorld(world); + if (validWorlds.get(world.getName()) != null) + return validWorlds.get(world.getName()); return false; } @Override public void initializeValues() { + instance = this; + validWorlds.clear(); whitelistNewWorlds = ConfigurationEngine.setBoolean(fileConfiguration, "New worlds spawn structures", true); + fileConfiguration.addDefault(VALID_WORLDS_KEY, new HashMap()); - for (World world : Bukkit.getWorlds()) - ConfigurationEngine.setBoolean(fileConfiguration, "Valid worlds." + world.getName(), true); + pruneMissingWorldEntries(); - ConfigurationSection validWorldsSection = fileConfiguration.getConfigurationSection("Valid worlds"); + for (World world : Bukkit.getWorlds()) + registerWorldName(world.getName(), true, false); - List enabledWorlds = new ArrayList<>(); + ConfigurationSection validWorldsSection = fileConfiguration.getConfigurationSection(VALID_WORLDS_KEY); + if (validWorldsSection == null) return; for (String key : validWorldsSection.getKeys(false)) - if (validWorldsSection.getBoolean(key)) - enabledWorlds.add(key); - - for (World world : Bukkit.getWorlds()) - validWorlds.put(world, enabledWorlds.contains(world.getName())); + validWorlds.put(key, validWorldsSection.getBoolean(key)); - ConfigurationEngine.fileSaverOnlyDefaults(fileConfiguration, file); + ConfigurationEngine.fileSaverCustomValues(fileConfiguration, file); } public static class ValidWorldsConfigEvents implements Listener { @@ -70,5 +122,19 @@ public static class ValidWorldsConfigEvents implements Listener { public void onWorldLoad(WorldLoadEvent event) { registerNewWorld(event.getWorld()); } + + @EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true) + public void onWorldUnload(WorldUnloadEvent event) { + String worldName = event.getWorld().getName(); + unregisterWorld(event.getWorld()); + if (MetadataHandler.PLUGIN == null || !MetadataHandler.PLUGIN.isEnabled()) + return; + new BukkitRunnable() { + @Override + public void run() { + pruneMissingWorldEntry(worldName); + } + }.runTaskLater(MetadataHandler.PLUGIN, UNLOAD_PRUNE_DELAY_TICKS); + } } } diff --git a/src/main/java/com/magmaguy/betterstructures/config/generators/GeneratorConfigFields.java b/src/main/java/com/magmaguy/betterstructures/config/generators/GeneratorConfigFields.java index f2afbe9..9cb68c5 100644 --- a/src/main/java/com/magmaguy/betterstructures/config/generators/GeneratorConfigFields.java +++ b/src/main/java/com/magmaguy/betterstructures/config/generators/GeneratorConfigFields.java @@ -41,6 +41,14 @@ public class GeneratorConfigFields extends CustomConfigFields { private String treasureFilename = null; @Getter private ChestContents chestContents = null; + @Getter + @Setter + private String barrelTreasureFilename = "treasure_barrel_food.yml"; + @Getter + private ChestContents barrelContents = null; + @Getter + @Setter + private boolean generateLootInBarrels = true; /** * Used by plugin-generated files (defaults) @@ -84,6 +92,20 @@ public void processConfigFields() { } else { Logger.warn("No valid treasure config file found for generator " + filename + " ! This will not spawn loot in chests until fixed."); } + + // Per-generator barrel loot toggle (default ON) + this.generateLootInBarrels = processBoolean("generateLootInBarrels", generateLootInBarrels, true, false); + + // Load barrel treasure config (defaults to the food premade) + this.barrelTreasureFilename = processString("barrelTreasureFilename", barrelTreasureFilename, "treasure_barrel_food.yml", false); + if (generateLootInBarrels) { + TreasureConfigFields barrelTreasureConfig = TreasureConfig.getConfigFields(barrelTreasureFilename); + if (barrelTreasureConfig != null) { + this.barrelContents = new ChestContents(barrelTreasureConfig); + } else { + Logger.warn("No valid barrel treasure config found for generator " + filename + " (looked for: " + barrelTreasureFilename + "). Barrels in this generator will be left empty until fixed."); + } + } } /** diff --git a/src/main/java/com/magmaguy/betterstructures/config/modulegenerators/ModuleGeneratorsConfigFields.java b/src/main/java/com/magmaguy/betterstructures/config/modulegenerators/ModuleGeneratorsConfigFields.java index fb04daf..0d0399f 100644 --- a/src/main/java/com/magmaguy/betterstructures/config/modulegenerators/ModuleGeneratorsConfigFields.java +++ b/src/main/java/com/magmaguy/betterstructures/config/modulegenerators/ModuleGeneratorsConfigFields.java @@ -35,6 +35,10 @@ public class ModuleGeneratorsConfigFields extends CustomConfigFields { @Getter protected String treasureFile; @Getter + protected boolean generateLootInBarrels = true; + @Getter + protected String barrelTreasureFilename = "treasure_barrel_food.yml"; + @Getter @Setter private List validWorlds = null; @Getter @@ -80,6 +84,8 @@ public void processConfigFields() { this.spawnPoolSuffix = processString("spawnPoolSuffix", spawnPoolSuffix, spawnPoolSuffix, true); this.isWorldGeneration = processBoolean("isWorldGeneration", isWorldGeneration, isWorldGeneration, true); this.treasureFile = processString("treasureFile", treasureFile, null, false); + this.generateLootInBarrels = processBoolean("generateLootInBarrels", generateLootInBarrels, true, false); + this.barrelTreasureFilename = processString("barrelTreasureFilename", barrelTreasureFilename, "treasure_barrel_food.yml", false); this.validWorlds = processStringList("validWorlds", validWorlds, new ArrayList<>(), false); this.validWorldEnvironments = processEnumList("validWorldEnvironments", validWorldEnvironments, null, World.Environment.class, false); this.centerModuleAltitude = processInt("centerModuleAltitude", centerModuleAltitude, 0, false); diff --git a/src/main/java/com/magmaguy/betterstructures/config/modules/ModulesConfigFields.java b/src/main/java/com/magmaguy/betterstructures/config/modules/ModulesConfigFields.java index f916cfd..e7d0486 100644 --- a/src/main/java/com/magmaguy/betterstructures/config/modules/ModulesConfigFields.java +++ b/src/main/java/com/magmaguy/betterstructures/config/modules/ModulesConfigFields.java @@ -20,6 +20,10 @@ public class ModulesConfigFields extends CustomConfigFields { private String treasureFile = null; @Setter private ChestContents chestContents = null; + @Setter + private String barrelTreasureFilename = "treasure_barrel_food.yml"; + @Setter + private boolean generateLootInBarrels = true; private Map borderMap = new HashMap<>(); private Integer minY = -4; private Integer maxY = 20; @@ -73,6 +77,14 @@ public ChestContents getChestContents() { return clonedConfig == null ? chestContents : clonedConfig.getChestContents(); } + public String getBarrelTreasureFilename() { + return clonedConfig == null ? barrelTreasureFilename : clonedConfig.getBarrelTreasureFilename(); + } + + public boolean isGenerateLootInBarrels() { + return clonedConfig == null ? generateLootInBarrels : clonedConfig.isGenerateLootInBarrels(); + } + public Map getBorderMap() { return clonedConfig == null ? borderMap : clonedConfig.getBorderMap(); } @@ -145,6 +157,8 @@ public void processConfigFields() { } this.chestContents = treasureConfigFields.getChestContents(); } + this.generateLootInBarrels = processBoolean("generateLootInBarrels", generateLootInBarrels, true, true); + this.barrelTreasureFilename = processString("barrelTreasureFilename", barrelTreasureFilename, "treasure_barrel_food.yml", true); this.borderMap = processMap("borders", new HashMap<>()); this.minY = processInt("minY", minY, minY, true); this.maxY = processInt("maxY", maxY, maxY, true); @@ -175,6 +189,7 @@ public void validateClones() { } // else Logger.info("Cloned " + filename + " into " + clonedConfig.getFilename()); fileConfiguration.set("treasureFile", null); + fileConfiguration.set("barrelTreasureFilename", null); fileConfiguration.set("borders", null); fileConfiguration.set("minY", null); fileConfiguration.set("maxY", null); @@ -183,6 +198,7 @@ public void validateClones() { fileConfiguration.set("weight", null); fileConfiguration.set("repetitionPenalty", null); fileConfiguration.set("enforceHorizontalRotation", null); + fileConfiguration.set("generateLootInBarrels", null); fileConfiguration.set("northIsPassable", null); fileConfiguration.set("southIsPassable", null); fileConfiguration.set("eastIsPassable", null); diff --git a/src/main/java/com/magmaguy/betterstructures/config/schematics/SchematicConfigField.java b/src/main/java/com/magmaguy/betterstructures/config/schematics/SchematicConfigField.java index 96fddb8..743676c 100644 --- a/src/main/java/com/magmaguy/betterstructures/config/schematics/SchematicConfigField.java +++ b/src/main/java/com/magmaguy/betterstructures/config/schematics/SchematicConfigField.java @@ -33,6 +33,12 @@ public class SchematicConfigField extends CustomConfigFields { @Getter @Setter private ChestContents chestContents = null; + @Getter + @Setter + private String barrelTreasureFilename = null; + @Getter + @Setter + private ChestContents barrelContents = null; /** @@ -53,18 +59,31 @@ public void processConfigFields() { this.generatorConfigFilename = processString("generatorConfigFilename", generatorConfigFilename, generatorConfigFilename, true); this.generatorConfigFields = GeneratorConfig.getConfigFields(generatorConfigFilename); this.treasureFile = processString("treasureFile", treasureFile, null, false); + this.barrelTreasureFilename = processString("barrelTreasureFilename", barrelTreasureFilename, null, false); if (generatorConfigFields == null) { Logger.warn("Failed to assign a valid generator to " + filename + "! This will not spawn. Generator config name: " + generatorConfigFilename); return; } + // Inherit defaults from the generator this.chestContents = generatorConfigFields.getChestContents(); + this.barrelContents = generatorConfigFields.getBarrelContents(); + // Per-schematic chest treasure override if (treasureFile != null && !treasureFile.isEmpty()) { TreasureConfigFields treasureConfigFields = TreasureConfig.getConfigFields(treasureFile); if (treasureConfigFields == null) { Logger.warn("Failed to get treasure config file " + treasureFile + " for schematic configuration " + filename + " ! Defaulting to the generator treasure."); - return; + } else { + this.chestContents = treasureConfigFields.getChestContents(); + } + } + // Per-schematic barrel treasure override + if (barrelTreasureFilename != null && !barrelTreasureFilename.isEmpty()) { + TreasureConfigFields barrelTreasureConfigFields = TreasureConfig.getConfigFields(barrelTreasureFilename); + if (barrelTreasureConfigFields == null) { + Logger.warn("Failed to get barrel treasure config file " + barrelTreasureFilename + " for schematic configuration " + filename + " ! Defaulting to the generator barrel treasure."); + } else { + this.barrelContents = barrelTreasureConfigFields.getChestContents(); } - this.chestContents = treasureConfigFields.getChestContents(); } } diff --git a/src/main/java/com/magmaguy/betterstructures/config/treasures/TreasureConfig.java b/src/main/java/com/magmaguy/betterstructures/config/treasures/TreasureConfig.java index 4210de7..ec32f05 100644 --- a/src/main/java/com/magmaguy/betterstructures/config/treasures/TreasureConfig.java +++ b/src/main/java/com/magmaguy/betterstructures/config/treasures/TreasureConfig.java @@ -18,6 +18,8 @@ public TreasureConfig() { } public static TreasureConfigFields getConfigFields(String configurationFilename) { - return treasureConfigurations.get(configurationFilename); + if (configurationFilename == null) return null; + String key = configurationFilename.endsWith(".yml") ? configurationFilename : configurationFilename + ".yml"; + return treasureConfigurations.get(key); } } diff --git a/src/main/java/com/magmaguy/betterstructures/config/treasures/TreasureConfigFields.java b/src/main/java/com/magmaguy/betterstructures/config/treasures/TreasureConfigFields.java index 5b68dc2..1bac500 100644 --- a/src/main/java/com/magmaguy/betterstructures/config/treasures/TreasureConfigFields.java +++ b/src/main/java/com/magmaguy/betterstructures/config/treasures/TreasureConfigFields.java @@ -23,7 +23,6 @@ public class TreasureConfigFields extends CustomConfigFields { @Getter private final Map> enchantmentSettings = new HashMap<>(); - private final List seenInvalidKeys = new ArrayList<>(); @Getter @Setter private Map rawLoot = new HashMap(); @@ -51,8 +50,8 @@ public void processConfigFields() { this.isEnabled = processBoolean("isEnabled", isEnabled, true, true); this.rawLoot = processMapWithKey("items", rawLoot); this.rawEnchantmentSettings = processMapWithKey("procedurallyGeneratedItemSettings", DefaultChestContents.generateProcedurallyGeneratedItems()); - this.mean = processDouble("mean", mean, 4, true); - this.standardDeviation = processDouble("standardDeviation", standardDeviation, 3, true); + this.mean = processDouble("mean", mean, mean, true); + this.standardDeviation = processDouble("standardDeviation", standardDeviation, standardDeviation, true); this.vanillaTreasure = parseVanillaTreasure(processString("vanillaTreasure", null, null, false)); chestContents = new ChestContents(this); parseEnchantmentSettings(); @@ -79,12 +78,10 @@ private void parseEnchantmentSettings() { List configurationEnchantments = new ArrayList<>(); Map enchantments = ((MemorySection) stringObjectEntry.getValue()).getValues(false); for (Map.Entry enchantmentsEntry : enchantments.entrySet()) { - Enchantment enchantment = Enchantment.getByKey(NamespacedKey.minecraft(enchantmentsEntry.getKey())); - if (enchantment == null && !seenInvalidKeys.contains(enchantmentsEntry.getKey())) { - Logger.info("Failed to get valid enchantment from key " + enchantmentsEntry.getKey() + " in configuration file " + filename + " ! This is almost certainly because another plugin " + "is using enchantments that are pretending to be vanilla Minecraft enchantments, when they aren't, " + "and doing so in a way that doesn't allow items to be enchanted via normal means. This enchantment " + "will be ignored for generating items, you can ignore this warning if you didn't plan to use this " + "enchantment in the first place. Warnings about this specific enchantment will now be suppressed."); - seenInvalidKeys.add(enchantmentsEntry.getKey()); - continue; - } + NamespacedKey enchantmentKey = NamespacedKey.fromString(enchantmentsEntry.getKey()); + if (enchantmentKey == null || !NamespacedKey.MINECRAFT.equals(enchantmentKey.getNamespace())) continue; + Enchantment enchantment = Enchantment.getByKey(enchantmentKey); + if (enchantment == null) continue; int minLevel = 1; int maxLevel = 1; double chance = 0; diff --git a/src/main/java/com/magmaguy/betterstructures/config/treasures/premade/BarrelFoodTreasureConfig.java b/src/main/java/com/magmaguy/betterstructures/config/treasures/premade/BarrelFoodTreasureConfig.java new file mode 100644 index 0000000..8690476 --- /dev/null +++ b/src/main/java/com/magmaguy/betterstructures/config/treasures/premade/BarrelFoodTreasureConfig.java @@ -0,0 +1,13 @@ +package com.magmaguy.betterstructures.config.treasures.premade; + +import com.magmaguy.betterstructures.config.treasures.TreasureConfigFields; +import com.magmaguy.betterstructures.util.DefaultChestContents; + +public class BarrelFoodTreasureConfig extends TreasureConfigFields { + public BarrelFoodTreasureConfig() { + super("treasure_barrel_food", true); + super.setRawLoot(DefaultChestContents.barrelFoodContents()); + super.setMean(1); + super.setStandardDeviation(0.7); + } +} diff --git a/src/main/java/com/magmaguy/betterstructures/menus/BetterStructuresFirstTimeSetupMenu.java b/src/main/java/com/magmaguy/betterstructures/menus/BetterStructuresFirstTimeSetupMenu.java index 69418e6..d8b1060 100644 --- a/src/main/java/com/magmaguy/betterstructures/menus/BetterStructuresFirstTimeSetupMenu.java +++ b/src/main/java/com/magmaguy/betterstructures/menus/BetterStructuresFirstTimeSetupMenu.java @@ -22,7 +22,7 @@ public static void createMenu(Player player) { (JavaPlugin) MetadataHandler.PLUGIN, player, "&2BetterStructures", - "&6Nightbreak-powered content setup", + "&6Guided content setup", createInfoItem(), List.of(createRecommendedItem(), createManualItem(), createSkipItem())); } @@ -32,8 +32,8 @@ private static MenuButton createInfoItem() { "magmaguy", "&2Welcome to BetterStructures!", List.of( - "&7Link your Nightbreak account,", - "&7download content in-game,", + "&7Connect this server,", + "&7open the setup menu,", "&7and start generating structures quickly."))) { @Override public void onClick(Player player) { @@ -42,15 +42,15 @@ public void onClick(Player player) { sendLink(player, "&2Setup guide: ", "&9&nhttps://nightbreak.io/plugin/betterstructures/#setup", "&7Click to open the BetterStructures setup guide.", "https://nightbreak.io/plugin/betterstructures/#setup"); - sendLink(player, "&2Nightbreak account: ", "&9&nhttps://nightbreak.io/account/", - "&7Click to open your Nightbreak account page.", + sendLink(player, "&2Account token: ", "&9&nhttps://nightbreak.io/account/", + "&7Click to open the account token page.", "https://nightbreak.io/account/"); - sendCommand(player, "&2Content browser: ", "&a/bs setup", + sendCommand(player, "&2Setup menu: ", "&a/bs setup", "&7Click to open the BetterStructures setup menu.", "/bs setup"); - sendCommand(player, "&2Bulk download: ", "&a/bs downloadall", - "&7Click to download all available BetterStructures content.", - "/bs downloadall"); + sendCommand(player, "&2Recommended plugins: ", "&a/bs recommendedplugins", + "&7Click to see plugins that work well with BetterStructures.", + "/bs recommendedplugins"); sendLink(player, "&2Support Discord: ", "&9&nhttps://discord.gg/eSxvPbWYy4", "&7Click to open the BetterStructures support Discord.", "https://discord.gg/eSxvPbWYy4"); @@ -63,29 +63,29 @@ private static MenuButton createRecommendedItem() { return new MenuButton(ItemStackGenerator.generateItemStack( Material.GREEN_STAINED_GLASS_PANE, "&2Recommended Setup", - List.of("&aMarks setup complete.", "&aGuides you to Nightbreak login and content install."))) { + List.of("&aMarks setup complete.", "&aGuides you to setup and recommended plugins."))) { @Override public void onClick(Player player) { player.closeInventory(); DefaultConfig.toggleSetupDone(true); Logger.sendSimpleMessage(player, "&8&m-----------------------------------------------------"); Logger.sendSimpleMessage(player, "&aBetterStructures setup is now marked as complete."); - sendLink(player, "&7Step 1: get your Nightbreak token at ", + Logger.sendSimpleMessage(player, "&7Connect this server so BetterStructures can install content and download plugin updates from in-game."); + sendLink(player, "&7Step 1: get your account token at ", "&9&nhttps://nightbreak.io/account/", - "&7Click to open your Nightbreak account page.", + "&7Click to open the account token page.", "https://nightbreak.io/account/"); sendCommand(player, "&7Step 2: link it in-game with ", "&a/nightbreaklogin ", - "&7Click to run the Nightbreak login command.", + "&7Click to prepare the token command.", "/nightbreaklogin "); player.spigot().sendMessage( - SpigotMessage.simpleMessage("&7Step 3: install content with "), - SpigotMessage.commandHoverMessage("&a/bs downloadall", - "&7Click to download all available BetterStructures content.", - "/bs downloadall"), - SpigotMessage.simpleMessage(" &7or browse it with "), + SpigotMessage.simpleMessage("&7Step 3: open the setup menu with "), SpigotMessage.commandHoverMessage("&a/bs setup", "&7Click to open the BetterStructures setup menu.", "/bs setup")); + sendCommand(player, "&7Recommended plugins: ", "&a/bs recommendedplugins", + "&7Click to see plugins that work well with BetterStructures.", + "/bs recommendedplugins"); Logger.sendSimpleMessage(player, "&8&m-----------------------------------------------------"); } }; diff --git a/src/main/java/com/magmaguy/betterstructures/menus/BetterStructuresSetupMenu.java b/src/main/java/com/magmaguy/betterstructures/menus/BetterStructuresSetupMenu.java index 20685b1..7ad7770 100644 --- a/src/main/java/com/magmaguy/betterstructures/menus/BetterStructuresSetupMenu.java +++ b/src/main/java/com/magmaguy/betterstructures/menus/BetterStructuresSetupMenu.java @@ -1,12 +1,15 @@ package com.magmaguy.betterstructures.menus; import com.magmaguy.betterstructures.MetadataHandler; +import com.magmaguy.betterstructures.BetterStructures; +import com.magmaguy.betterstructures.config.contentpackages.ContentPackageConfigFields; import com.magmaguy.betterstructures.content.BSPackage; import com.magmaguy.betterstructures.content.BSPackageRefresher; import com.magmaguy.magmacore.menus.MenuButton; -import com.magmaguy.magmacore.menus.SetupMenu; -import com.magmaguy.magmacore.nightbreak.NightbreakAccount; +import com.magmaguy.magmacore.menus.SetupMenuBuilder; import com.magmaguy.magmacore.nightbreak.DownloadAllContentPackage; +import com.magmaguy.magmacore.nightbreak.NightbreakAccount; +import com.magmaguy.magmacore.nightbreak.NightbreakSetupControls; import com.magmaguy.magmacore.util.ChatColorConverter; import com.magmaguy.magmacore.util.ItemStackGenerator; import com.magmaguy.magmacore.util.Logger; @@ -34,75 +37,33 @@ public static void createMenu(Player player) { .collect(Collectors.toList()); BSPackageRefresher.refreshContentAndAccess(); - MenuButton infoButton = new MenuButton(ItemStackGenerator.generateSkullItemStack("magmaguy", - "&2Installation instructions:", - List.of( - "&2To setup the optional/recommended content for BetterStructures:", - "&61) &fLink your Nightbreak account: &a/nightbreaklogin", - "&62) &fDownload all content: &a/bs downloadall", - "&63) &fOr browse and manage content: &a/bs setup", - "&2That's it!", - "&6Click for more info and links!"))) { - @Override - public void onClick(Player p) { - p.closeInventory(); - Logger.sendSimpleMessage(p, "▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"); - Logger.sendSimpleMessage(p, "&6&lBetterStructures installation resources:"); - p.spigot().sendMessage( - SpigotMessage.simpleMessage("&2&lNightbreak account: "), - SpigotMessage.hoverLinkMessage("&ahttps://nightbreak.io/account/", - "&7Click to open the Nightbreak account page.", - "https://nightbreak.io/account/")); - p.spigot().sendMessage( - SpigotMessage.simpleMessage("&2&lWiki page: "), - SpigotMessage.hoverLinkMessage("&ahttps://nightbreak.io/plugin/betterstructures/#setup", - "&7Click to open the BetterStructures setup page.", - "https://nightbreak.io/plugin/betterstructures/#setup")); - p.spigot().sendMessage( - SpigotMessage.simpleMessage("&2&lContent: "), - SpigotMessage.hoverLinkMessage("&ahttps://nightbreak.io/plugin/betterstructures/", - "&7Click to browse BetterStructures content.", - "https://nightbreak.io/plugin/betterstructures/")); - p.spigot().sendMessage( - SpigotMessage.simpleMessage("&2&lDiscord support: "), - SpigotMessage.hoverLinkMessage("&ahttps://discord.gg/9f5QSka", - "&7Click to open Discord.", - "https://discord.gg/9f5QSka")); - if (NightbreakAccount.hasToken()) { - p.spigot().sendMessage( - SpigotMessage.commandHoverMessage("&2&lQuick install: &a/bs downloadall", - "&7Click to run the bulk BetterStructures download.", - "/bs downloadall")); - p.spigot().sendMessage( - SpigotMessage.commandHoverMessage("&2&lQuick update: &a/bs updatecontent", - "&7Click to update all outdated BetterStructures content.", - "/bs updatecontent")); - } - Logger.sendSimpleMessage(p, "▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬"); - } - }; + MenuButton infoButton = NightbreakSetupControls.setupInfoButton( + BetterStructures.NIGHTBREAK_PLUGIN_SPEC, + "https://nightbreak.io/plugin/betterstructures/#setup"); - List allPackages = new ArrayList<>(bsPackages); - allPackages.add(new DownloadAllContentPackage<>(() -> new ArrayList<>(BSPackage.getBsPackages().values()), - "BetterStructures", - "https://nightbreak.io/plugin/betterstructures/", - "bs downloadall")); + SetupMenuBuilder builder = new SetupMenuBuilder((JavaPlugin) MetadataHandler.PLUGIN, player) + .title("Setup menu") + .infoButton(infoButton) + .packages(bsPackages) + .appendPackage(new DownloadAllContentPackage<>(() -> new ArrayList<>(BSPackage.getBsPackages().values()), + "BetterStructures", + "https://nightbreak.io/plugin/betterstructures/", + "bs downloadall")) + .addFilter(Material.GRASS_BLOCK, "Structure Packs", + (Predicate) BetterStructuresSetupMenu::filterStructures) + .addFilter(Material.DEEPSLATE_BRICKS, "Module Packs", + (Predicate) BetterStructuresSetupMenu::filterModules); + NightbreakSetupControls.prependStandardControls(builder, (JavaPlugin) MetadataHandler.PLUGIN, BetterStructures.NIGHTBREAK_PLUGIN_SPEC) + .open(); + } - new SetupMenu((JavaPlugin) MetadataHandler.PLUGIN, player, infoButton, allPackages, - List.of( - createFilter(bsPackages, Material.GRASS_BLOCK, "Structure Packs", bspPackage -> - bspPackage.getContentPackageConfigFields().getContentPackageType() == com.magmaguy.betterstructures.config.contentpackages.ContentPackageConfigFields.ContentPackageType.STRUCTURE), - createFilter(bsPackages, Material.DEEPSLATE_BRICKS, "Module Packs", bspPackage -> - bspPackage.getContentPackageConfigFields().getContentPackageType() == com.magmaguy.betterstructures.config.contentpackages.ContentPackageConfigFields.ContentPackageType.MODULAR)), - "Setup menu"); + private static boolean filterStructures(BSPackage bsPackage) { + return bsPackage.getContentPackageConfigFields().getContentPackageType() == + ContentPackageConfigFields.ContentPackageType.STRUCTURE; } - private static SetupMenu.SetupMenuFilter createFilter(List orderedPackages, - Material material, - String name, - Predicate predicate) { - return new SetupMenu.SetupMenuFilter( - ItemStackGenerator.generateItemStack(material, name), - orderedPackages.stream().filter(predicate).toList()); + private static boolean filterModules(BSPackage bsPackage) { + return bsPackage.getContentPackageConfigFields().getContentPackageType() == + ContentPackageConfigFields.ContentPackageType.MODULAR; } } diff --git a/src/main/java/com/magmaguy/betterstructures/modules/ModulePasting.java b/src/main/java/com/magmaguy/betterstructures/modules/ModulePasting.java index 5521bde..52577ce 100644 --- a/src/main/java/com/magmaguy/betterstructures/modules/ModulePasting.java +++ b/src/main/java/com/magmaguy/betterstructures/modules/ModulePasting.java @@ -5,6 +5,7 @@ import com.magmaguy.betterstructures.config.DefaultConfig; import com.magmaguy.betterstructures.chests.ChestContents; import com.magmaguy.betterstructures.config.modulegenerators.ModuleGeneratorsConfigFields; +import com.magmaguy.betterstructures.config.modules.ModulesConfigFields; import com.magmaguy.betterstructures.config.treasures.TreasureConfig; import com.magmaguy.betterstructures.config.treasures.TreasureConfigFields; import com.magmaguy.betterstructures.util.WorldEditUtils; @@ -39,11 +40,16 @@ import java.io.File; import java.util.ArrayList; import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; public final class ModulePasting { private final List interpretedSigns = new ArrayList<>(); private final List chestsToPlace = new ArrayList<>(); + private final List barrelsToFill = new ArrayList<>(); private final List entitiesToSpawn = new ArrayList<>(); private final String spawnPoolSuffix; private final Location startLocation; @@ -88,7 +94,7 @@ public ModulePasting(World world, File worldFolder, Deque WFCNodeDeque, } private static boolean isNbtRichMaterial(Material m) { - if (m == Material.CHEST || m == Material.TRAPPED_CHEST) return false; + if (m == Material.CHEST || m == Material.TRAPPED_CHEST || m == Material.BARREL) return false; if (m.name().endsWith("_SIGN") || m.name().endsWith("_WALL_SIGN") || m.name().endsWith("_HANGING_SIGN")) return false; @@ -185,7 +191,8 @@ public static void pasteArmorStands(Clipboard clipboard, Location location, Inte private List generatePasteMeList(Clipboard clipboard, Location worldPasteOriginLocation, Integer rotation, - List interpretedSigns) { + List interpretedSigns, + ModulesConfigFields modulesConfigFields) { List pasteableList = new ArrayList<>(); // Apply rotation transformation @@ -257,6 +264,10 @@ private List generatePasteMeList(Clipboard clipboard, return; // do NOT add to normal paste list } + if (blockData.getMaterial() == Material.BARREL) { + barrelsToFill.add(new BarrelPlacement(pasteLocation, modulesConfigFields)); + } + // Normal placement path pasteableList.add(new Pasteable(pasteLocation, blockData)); }); @@ -287,8 +298,9 @@ public List batchPaste(Deque WFCNodeDeque, List entityPasteInfos) { } } + if (moduleGeneratorsConfigFields.isGenerateLootInBarrels() && !barrelsToFill.isEmpty()) { + Map contentsByTreasure = new HashMap<>(); + Set warnedMissingTreasures = new HashSet<>(); + for (BarrelPlacement bp : barrelsToFill) { + ModulesConfigFields modConfig = bp.modulesConfigFields(); + if (modConfig != null && !modConfig.isGenerateLootInBarrels()) continue; + + String treasureFilename = (modConfig != null && modConfig.getBarrelTreasureFilename() != null && !modConfig.getBarrelTreasureFilename().isEmpty()) + ? modConfig.getBarrelTreasureFilename() + : moduleGeneratorsConfigFields.getBarrelTreasureFilename(); + if (treasureFilename == null || treasureFilename.isEmpty()) continue; + + ChestContents barrelContents = contentsByTreasure.get(treasureFilename); + if (barrelContents == null && !contentsByTreasure.containsKey(treasureFilename)) { + TreasureConfigFields barrelTreasureFields = TreasureConfig.getConfigFields(treasureFilename); + barrelContents = barrelTreasureFields != null ? new ChestContents(barrelTreasureFields) : null; + contentsByTreasure.put(treasureFilename, barrelContents); + } + if (barrelContents == null) { + if (warnedMissingTreasures.add(treasureFilename)) { + Logger.warn("Module generator " + moduleGeneratorsConfigFields.getFilename() + " has barrels referencing barrelTreasureFilename '" + treasureFilename + "' but it did not resolve to a valid treasure config. Affected barrels will be empty."); + } + continue; + } + + Block block = bp.location().getBlock(); + if (block.getType() != Material.BARREL) continue; + if (!(block.getState() instanceof Container container)) continue; + + barrelContents.rollChestContents(container); + ChestFillEvent chestFillEvent = new ChestFillEvent(container, treasureFilename); + Bukkit.getServer().getPluginManager().callEvent(chestFillEvent); + if (!chestFillEvent.isCancelled()) { + container.update(true); + } + } + } + // 4) Spawn entities last for (EntitySpawn entitySpawn : entitiesToSpawn) { try { @@ -441,6 +491,9 @@ private record EntityPasteInfo(Clipboard clipboard, Location location, Integer r private record ChestPlacement(Location location, Material material, Integer rotation) { } + private record BarrelPlacement(Location location, ModulesConfigFields modulesConfigFields) { + } + private record EntitySpawn(Location location, EntityType entityType) { } diff --git a/src/main/java/com/magmaguy/betterstructures/schematics/SchematicContainer.java b/src/main/java/com/magmaguy/betterstructures/schematics/SchematicContainer.java index 0443057..72a3458 100644 --- a/src/main/java/com/magmaguy/betterstructures/schematics/SchematicContainer.java +++ b/src/main/java/com/magmaguy/betterstructures/schematics/SchematicContainer.java @@ -51,6 +51,8 @@ public class SchematicContainer { @Getter private ChestContents chestContents = null; @Getter + private ChestContents barrelContents = null; + @Getter private boolean valid = true; public SchematicContainer(Clipboard clipboard, String clipboardFilename, SchematicConfigField schematicConfigField, String configFilename) { @@ -73,7 +75,8 @@ public SchematicContainer(Clipboard clipboard, String clipboardFilename, Schemat //register chest location if (minecraftMaterial.equals(Material.CHEST) || minecraftMaterial.equals(Material.TRAPPED_CHEST) || - minecraftMaterial.equals(Material.SHULKER_BOX)) { + minecraftMaterial.equals(Material.SHULKER_BOX) || + minecraftMaterial.equals(Material.BARREL)) { chestLocations.add(new Vector(x, y, z)); } if (minecraftMaterial.equals(Material.ACACIA_SIGN) || @@ -135,13 +138,22 @@ public SchematicContainer(Clipboard clipboard, String clipboardFilename, Schemat } } chestContents = generatorConfigFields.getChestContents(); + barrelContents = generatorConfigFields.getBarrelContents(); if (schematicConfigField.getTreasureFile() != null && !schematicConfigField.getTreasureFile().isEmpty()) { - TreasureConfigFields treasureConfigFields = TreasureConfig.getConfigFields(schematicConfigField.getFilename()); + TreasureConfigFields treasureConfigFields = TreasureConfig.getConfigFields(schematicConfigField.getTreasureFile()); if (treasureConfigFields == null) { - Logger.warn("Failed to get treasure configuration " + schematicConfigField.getTreasureFile()); - return; + Logger.warn("Failed to get treasure configuration " + schematicConfigField.getTreasureFile() + " for schematic " + schematicConfigField.getFilename() + " ! Defaulting to the generator chest treasure."); + } else { + chestContents = schematicConfigField.getChestContents(); + } + } + if (schematicConfigField.getBarrelTreasureFilename() != null && !schematicConfigField.getBarrelTreasureFilename().isEmpty()) { + TreasureConfigFields barrelTreasureConfigFields = TreasureConfig.getConfigFields(schematicConfigField.getBarrelTreasureFilename()); + if (barrelTreasureConfigFields == null) { + Logger.warn("Failed to get barrel treasure configuration " + schematicConfigField.getBarrelTreasureFilename() + " for schematic " + schematicConfigField.getFilename() + " ! Defaulting to the generator barrel treasure."); + } else { + barrelContents = schematicConfigField.getBarrelContents(); } - chestContents = schematicConfigField.getChestContents(); } if (valid) generatorConfigFields.getStructureTypes().forEach(structureType -> schematics.put(structureType, this)); diff --git a/src/main/java/com/magmaguy/betterstructures/util/DefaultChestContents.java b/src/main/java/com/magmaguy/betterstructures/util/DefaultChestContents.java index 234ccb6..3ea1233 100644 --- a/src/main/java/com/magmaguy/betterstructures/util/DefaultChestContents.java +++ b/src/main/java/com/magmaguy/betterstructures/util/DefaultChestContents.java @@ -2,6 +2,7 @@ import com.magmaguy.magmacore.util.VersionChecker; import org.bukkit.Material; +import org.bukkit.NamespacedKey; import org.bukkit.enchantments.Enchantment; import org.bukkit.inventory.ItemStack; @@ -222,6 +223,70 @@ public static Map overworldContents() { return items; } + public static Map barrelFoodContents() { + Map items = new HashMap<>(); + Map commonItems = new HashMap<>(); + Map rareItems = new HashMap<>(); + Map epicItems = new HashMap<>(); + List> commonList = new ArrayList<>(); + List> rareList = new ArrayList<>(); + List> epicList = new ArrayList<>(); + + // Common — staples and raw foods (peasant's pantry) + commonList.add(generateEntry(Material.BREAD, 1, 3, normalWeight)); + commonList.add(generateEntry(Material.APPLE, 1, 3, normalWeight)); + commonList.add(generateEntry(Material.CARROT, 1, 3, normalWeight)); + commonList.add(generateEntry(Material.POTATO, 1, 3, normalWeight)); + commonList.add(generateEntry(Material.BEETROOT, 1, 3, normalWeight)); + commonList.add(generateEntry(Material.SWEET_BERRIES, 1, 4, normalWeight)); + commonList.add(generateEntry(Material.GLOW_BERRIES, 1, 4, normalWeight)); + commonList.add(generateEntry(Material.MELON_SLICE, 1, 4, normalWeight)); + commonList.add(generateEntry(Material.DRIED_KELP, 2, 6, normalWeight)); + commonList.add(generateEntry(Material.COOKIE, 2, 6, normalWeight)); + commonList.add(generateEntry(Material.BEEF, 1, 3, normalWeight)); + commonList.add(generateEntry(Material.PORKCHOP, 1, 3, normalWeight)); + commonList.add(generateEntry(Material.MUTTON, 1, 3, normalWeight)); + commonList.add(generateEntry(Material.COD, 1, 4, normalWeight)); + commonList.add(generateEntry(Material.SALMON, 1, 4, normalWeight)); + commonList.add(generateEntry(Material.CHICKEN, 1, 3, rareWeight)); + commonList.add(generateEntry(Material.RABBIT, 1, 3, rareWeight)); + commonList.add(generateEntry(Material.TROPICAL_FISH, 1, 2, extraRareWeight)); + commonList.add(generateEntry(Material.CHORUS_FRUIT, 1, 3, extraRareWeight)); + + // Rare — cooked / processed (someone actually fed the fire) + rareList.add(generateEntry(Material.COOKED_BEEF, 1, 3, normalWeight)); + rareList.add(generateEntry(Material.COOKED_PORKCHOP, 1, 3, normalWeight)); + rareList.add(generateEntry(Material.COOKED_MUTTON, 1, 3, normalWeight)); + rareList.add(generateEntry(Material.COOKED_CHICKEN, 1, 3, normalWeight)); + rareList.add(generateEntry(Material.COOKED_COD, 1, 3, normalWeight)); + rareList.add(generateEntry(Material.COOKED_SALMON, 1, 3, normalWeight)); + rareList.add(generateEntry(Material.COOKED_RABBIT, 1, 2, normalWeight)); + rareList.add(generateEntry(Material.BAKED_POTATO, 1, 3, normalWeight)); + rareList.add(generateEntry(Material.PUMPKIN_PIE, 1, 2, rareWeight)); + rareList.add(generateEntry(Material.HONEY_BOTTLE, 1, 2, rareWeight)); + rareList.add(generateEntry(Material.MUSHROOM_STEW, 1, 1, rareWeight)); + rareList.add(generateEntry(Material.BEETROOT_SOUP, 1, 1, rareWeight)); + rareList.add(generateEntry(Material.SUSPICIOUS_STEW, 1, 1, extraRareWeight)); + rareList.add(generateEntry(Material.RABBIT_STEW, 1, 1, extraRareWeight)); + + // Epic — premium (the lord's larder) + epicList.add(generateEntry(Material.GOLDEN_CARROT, 1, 3, normalWeight)); + epicList.add(generateEntry(Material.GOLDEN_APPLE, 1, 2, rareWeight)); + epicList.add(generateEntry(Material.ENCHANTED_GOLDEN_APPLE, 1, 1, extraRareWeight)); + epicList.add(generateEntry(Material.CAKE, 1, 1, extraRareWeight)); + + commonItems.put("weight", 60); + commonItems.put("items", commonList); + rareItems.put("weight", 30); + rareItems.put("items", rareList); + epicItems.put("weight", 10); + epicItems.put("items", epicList); + items.put("common", commonItems); + items.put("rare", rareItems); + items.put("epic", epicItems); + return items; + } + public static Map overworldUndergroundContents() { //Clones the list from above ground Map items = new HashMap<>(overworldContents()); @@ -822,6 +887,7 @@ public static Map generateProcedurallyGeneratedItems() { for (Material enchantableItem : enchantableItems) { Map> enchantmentMap = new HashMap<>(); for (Enchantment enchantment : Enchantment.values()) { + if (!NamespacedKey.MINECRAFT.equals(enchantment.getKey().getNamespace())) continue; if (!enchantment.canEnchantItem(new ItemStack(enchantableItem))) continue; Map enchantmentSettingsMap = new HashMap<>(); int minLevel = enchantment.getStartLevel(); diff --git a/src/main/java/com/magmaguy/betterstructures/worldedit/Schematic.java b/src/main/java/com/magmaguy/betterstructures/worldedit/Schematic.java index 5d1760b..b907d24 100644 --- a/src/main/java/com/magmaguy/betterstructures/worldedit/Schematic.java +++ b/src/main/java/com/magmaguy/betterstructures/worldedit/Schematic.java @@ -93,6 +93,25 @@ public static void paste(Clipboard clipboard, Location location) { } } + /** + * Determines whether a block at the given clipboard position is solid. + * Tries to resolve the Bukkit material first; if WorldEdit's BukkitAdapter + * can't map the block to a Bukkit Material (returns null - e.g. for newer + * or otherwise unmapped block types), falls back to asking WorldEdit's + * own BlockType/BlockState whether it considers the block solid. + * + * @param schematicClipboard The clipboard containing the schematic + * @param clipboardPosition The position within the clipboard to check + * @return true if the block at that position is solid + */ + private static boolean isSolidBlock(Clipboard schematicClipboard, BlockVector3 clipboardPosition) { + BaseBlock baseBlock = schematicClipboard.getFullBlock(clipboardPosition); + BlockState blockState = baseBlock.toImmutableState(); + Material material = BukkitAdapter.adapt(blockState.getBlockType()); + if (material == null) return blockState.getBlockType().getMaterial().isSolid(); + return material.isSolid(); + } + /** * Creates a list of paste blocks from a schematic * @@ -124,11 +143,24 @@ private static List createPasteBlocks( BlockData blockData = Bukkit.createBlockData(baseBlock.toImmutableState().getAsString()); Material material = BukkitAdapter.adapt(baseBlock.getBlockType()); Block worldBlock = adjustedLocation.clone().add(new Vector(x, y, z)).getBlock(); + boolean isGround = !isSolidBlock(schematicClipboard, BlockVector3.at( + adjustedClipboardLocation.x(), + adjustedClipboardLocation.y() + 1, + adjustedClipboardLocation.z())); + + if (material == null) { + // WorldEdit couldn't map this block to a Bukkit Material (e.g. unmapped/newer block type). + // Fall back to WorldEdit's own BlockType to decide if it's solid, and handle it via + // WorldEdit's paste path instead of relying on Bukkit's Material/BlockData APIs. + boolean solid = blockState.getBlockType().getMaterial().isSolid(); + if (solid) { + pasteBlocks.add(new PasteBlock(worldBlock, null, + WorldEditUtils.createSingleBlockClipboard(adjustedLocation, baseBlock, blockState))); + } + continue; + } + String materialString = material.toString().toUpperCase(Locale.ROOT); - boolean isGround = !BukkitAdapter.adapt(schematicClipboard.getBlock( - BlockVector3.at(adjustedClipboardLocation.x(), - adjustedClipboardLocation.y() + 1, - adjustedClipboardLocation.z())).getBlockType()).isSolid(); if (material == Material.BARRIER) { // special behavior: do not replace barriers, so do nothing diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index e4306d3..0d23006 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: BetterStructures -version: '2.3.1' +version: '2.5.0' main: com.magmaguy.betterstructures.BetterStructures api-version: '1.21.4' depend: [ WorldEdit ] @@ -34,4 +34,4 @@ permissions: default: op betterstructures.generatemodules: description: Gives admins access to the /betterstructures generateModules command - default: op \ No newline at end of file + default: op