From a1ac2746f6d080cf24dbe715592ededa46ea9e6e Mon Sep 17 00:00:00 2001 From: MagmaGuy Date: Sun, 24 May 2026 16:50:52 +0100 Subject: [PATCH 01/20] docs: barrel loot implementation plan Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/plans/2026-05-24-barrel-loot.md | 588 +++++++++++++++++++++++++++ 1 file changed, 588 insertions(+) create mode 100644 docs/plans/2026-05-24-barrel-loot.md diff --git a/docs/plans/2026-05-24-barrel-loot.md b/docs/plans/2026-05-24-barrel-loot.md new file mode 100644 index 0000000..c9e140a --- /dev/null +++ b/docs/plans/2026-05-24-barrel-loot.md @@ -0,0 +1,588 @@ +# Barrel Loot Generation Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add barrel loot generation to BetterStructures (Lootify system). Barrels found in generated structures fill with food-by-default loot, tuned to 1-3 items per barrel. Server owners can disable barrel fills, swap the loot table, or override per-schematic. + +**Architecture:** Mirror the existing chest pipeline. `ChestContents` already operates on `org.bukkit.block.Container`, which `Barrel` implements — no changes needed to the loot-rolling itself. The work is (1) detect barrels inherently in both pasting pipelines (modules + schematics) — no sign marker; (2) add a second `ChestContents` slot ("barrelContents") on generators/schematics pointing to a separate treasure file; (3) route the fill to chest- or barrel-contents based on the placed block type; (4) ship a `treasure_barrel_food.yml` premade with food-only defaults and `mean: 1`, `standardDeviation: 0.7` so each barrel rolls 1-3 items; (5) add a `generateLootInBarrels` flag (default `true`) that lets users opt out per-generator. + +**Tech Stack:** Java 17, Paper/Spigot API (`org.bukkit.block.Container`, `Barrel`, `Material.BARREL`), WorldEdit, Lombok, Gradle. Built via `./gradlew build`. + +**Design decisions (locked in):** +- **Inherent detection** — any `BARREL` block in a schematic or module is detected and filled. No `[barrel]` sign marker. +- **`generateLootInBarrels: true`** (default) on `GeneratorConfigFields` and `ModulesConfigFields`. Setting `false` skips barrel fills for that generator. No schematic-level override of this flag (YAGNI). +- **Separate `barrelTreasureFilename`** on `GeneratorConfigFields` / `SchematicConfigField` / `ModulesConfigFields`. Defaults to `"treasure_barrel_food"`. +- **Food-only default** with three rarity tiers (common / rare / epic, weights 60 / 30 / 10) mirroring the chest table shape. +- **1-3 items per barrel** via `mean: 1`, `standardDeviation: 0.7` on the barrel treasure config (the existing `ceil(gaussian) + 1` formula in `ChestContents.java:166-168` puts the distribution at 1-3 with a short tail). +- Reuse `ChestContents` class as-is — it's already container-agnostic. (Renaming would break public-API users.) +- Keep the existing `ChestFillEvent` for barrels too — it already accepts `Container`. No new event class. + +**Out of scope:** trapped barrels (don't exist in MC), per-instance barrel orientation handling beyond what vanilla barrel `BlockData` already does, MMOItems-only barrel tables (users can author those via the generic treasure config). + +--- + +## Files touched (overview) + +- **Create:** `src/main/java/com/magmaguy/betterstructures/config/treasures/premade/BarrelFoodTreasureConfig.java` +- **Modify:** `src/main/java/com/magmaguy/betterstructures/util/DefaultChestContents.java` — add `barrelFoodContents()` +- **Modify:** `src/main/java/com/magmaguy/betterstructures/config/treasures/TreasureConfigFields.java` — make `mean` / `standardDeviation` defaults respect the current field value so subclasses can tune (tiny refactor) +- **Modify:** `src/main/java/com/magmaguy/betterstructures/config/generators/GeneratorConfigFields.java` — add `barrelTreasureFilename`, `barrelContents`, `generateLootInBarrels` +- **Modify:** `src/main/java/com/magmaguy/betterstructures/config/modules/ModulesConfigFields.java` — add `barrelTreasureFilename`, `barrelContents`, `generateLootInBarrels` +- **Modify:** `src/main/java/com/magmaguy/betterstructures/config/schematics/SchematicConfigField.java` — add `barrelTreasureFilename` + `barrelContents` (per-schematic override of the treasure file only) +- **Modify:** `src/main/java/com/magmaguy/betterstructures/schematics/SchematicContainer.java` — add `Material.BARREL` to the location-collection check +- **Modify:** `src/main/java/com/magmaguy/betterstructures/buildingfitter/FitAnything.java` — route chest vs barrel in `fillChests()`; respect `generateLootInBarrels` +- **Modify:** `src/main/java/com/magmaguy/betterstructures/modules/ModulePasting.java` — track barrel blocks during paste, fill them after paste, respect `generateLootInBarrels`; widen `isNbtRichMaterial` to exclude `BARREL` + +--- + +## Task 1: Add the tiered food loot map + +**Files:** +- Modify: `src/main/java/com/magmaguy/betterstructures/util/DefaultChestContents.java` + +**Step 1: Add `barrelFoodContents()`** + +Append this method to `DefaultChestContents`, next to `overworldContents()`. Three tiers, weights 60 / 30 / 10, mirroring the chest tables. + +```java +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; +} +``` + +**Step 2: Build check** + +Run: `./gradlew compileJava` +Expected: BUILD SUCCESSFUL. + +**Step 3: Commit** + +```bash +git add src/main/java/com/magmaguy/betterstructures/util/DefaultChestContents.java +git commit -m "feat(lootify): add tiered barrel food loot map" +``` + +--- + +## Task 2: Let `TreasureConfigFields` subclasses override `mean` / `standardDeviation` defaults + +**Files:** +- Modify: `src/main/java/com/magmaguy/betterstructures/config/treasures/TreasureConfigFields.java` + +**Why this is needed:** `processConfigFields()` currently hardcodes the defaults — `processDouble("mean", mean, 4, true)`. If `BarrelFoodTreasureConfig` sets `setMean(1)` in its constructor, that call runs *before* `processConfigFields`, and the literal `4` in `processDouble` would still win as the on-disk default. By passing `mean` itself as the default, a subclass-set value becomes the default written to YAML. + +**Step 1: Edit `processConfigFields()`** + +In `TreasureConfigFields.java` around lines 54-55, change: + +```java +this.mean = processDouble("mean", mean, 4, true); +this.standardDeviation = processDouble("standardDeviation", standardDeviation, 3, true); +``` + +to: + +```java +this.mean = processDouble("mean", mean, mean, true); +this.standardDeviation = processDouble("standardDeviation", standardDeviation, standardDeviation, true); +``` + +Existing chest treasure configs keep working because their field initial values (declared at lines 37-40) are still `4` and `3` — same defaults, just sourced from the field instead of a literal. + +**Step 2: Build check** + +Run: `./gradlew compileJava` +Expected: BUILD SUCCESSFUL. + +**Step 3: Commit** + +```bash +git add src/main/java/com/magmaguy/betterstructures/config/treasures/TreasureConfigFields.java +git commit -m "refactor(lootify): source mean/stddev defaults from field, not literals" +``` + +--- + +## Task 3: Ship the `treasure_barrel_food` premade with tuned mean/stddev + +**Files:** +- Create: `src/main/java/com/magmaguy/betterstructures/config/treasures/premade/BarrelFoodTreasureConfig.java` + +**Step 1: Create the premade class** + +`TreasureConfig.java:13` auto-discovers everything in the `premade` package — no registration needed. + +```java +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); + } +} +``` + +**Step 2: Deploy + verify the YAML file is written** + +Run `./gradlew build` then deploy to a testbed ([reference_testbed_setup.md](../../../../../.claude/projects/C--Users-tiago-Documents-MineCraftProjects/memory/reference_testbed_setup.md)). Start the server once cleanly. + +Expected: `plugins/BetterStructures/treasures/treasure_barrel_food.yml` exists with: +- `items.common` / `items.rare` / `items.epic` populated +- `mean: 1.0` +- `standardDeviation: 0.7` + +If `mean` or `standardDeviation` come out as `4.0` / `3.0`, Task 2's refactor wasn't applied correctly. + +**Step 3: Commit** + +```bash +git add src/main/java/com/magmaguy/betterstructures/config/treasures/premade/BarrelFoodTreasureConfig.java +git commit -m "feat(lootify): ship treasure_barrel_food premade (mean=1, stddev=0.7)" +``` + +--- + +## Task 4: Add barrel fields to `GeneratorConfigFields` + +**Files:** +- Modify: `src/main/java/com/magmaguy/betterstructures/config/generators/GeneratorConfigFields.java` + +**Step 1: Add the fields** + +After the existing `treasureFilename` / `chestContents` declarations (around lines 41-43), add: + +```java +@Getter +@Setter +private String barrelTreasureFilename = "treasure_barrel_food"; +@Getter +private ChestContents barrelContents = null; +@Getter +@Setter +private boolean generateLootInBarrels = true; +``` + +**Step 2: Load them during `processConfigFields()`** + +After the existing chest-treasure load (around lines 80-86), append: + +```java +// 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", 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."); + } +} +``` + +**Step 3: Build check** + +Run: `./gradlew compileJava` +Expected: BUILD SUCCESSFUL. + +**Step 4: Commit** + +```bash +git add src/main/java/com/magmaguy/betterstructures/config/generators/GeneratorConfigFields.java +git commit -m "feat(lootify): generator-level barrel loot config (default on)" +``` + +--- + +## Task 5: Add barrel fields to `ModulesConfigFields` + +**Files:** +- Modify: `src/main/java/com/magmaguy/betterstructures/config/modules/ModulesConfigFields.java` + +**Step 1: Mirror Task 4's field additions** + +Same three fields, same defaults. Add Lombok getters (and setter for the string + boolean), wire through `processConfigFields()` the same way. + +**Step 2: Add a getter to make the ModulePasting flow read it** + +Confirm `ModulesConfigFields` exposes `getBarrelTreasureFilename()`, `getBarrelContents()`, and `isGenerateLootInBarrels()` — Lombok `@Getter` produces these. ModulePasting will read these in Task 8. + +**Step 3: Build check** + +Run: `./gradlew compileJava` +Expected: BUILD SUCCESSFUL. + +**Step 4: Commit** + +```bash +git add src/main/java/com/magmaguy/betterstructures/config/modules/ModulesConfigFields.java +git commit -m "feat(lootify): module-level barrel loot config (default on)" +``` + +--- + +## Task 6: Add per-schematic barrel treasure override to `SchematicConfigField` + +**Files:** +- Modify: `src/main/java/com/magmaguy/betterstructures/config/schematics/SchematicConfigField.java` + +**Step 1: Read the file** + +Find the `chestContents` field (line ~35) and the treasure-file load block (lines ~60-67). + +**Step 2: Add mirrored barrel fields** + +Right after `private ChestContents chestContents = null;`, add: + +```java +@Getter +private ChestContents barrelContents = null; +@Getter +@Setter +private String barrelTreasureFilename = null; +``` + +No `generateLootInBarrels` here — that decision is generator-/module-level. Per-schematic override is treasure-file only, mirroring the existing chest-side override. + +**Step 3: Wire the load path** + +Where `this.chestContents = generatorConfigFields.getChestContents();` lives (line ~60), also inherit: + +```java +this.barrelContents = generatorConfigFields.getBarrelContents(); +``` + +Inside the `treasureConfigFields != null` block (line ~67), parallel to the chest override, look for `barrelTreasureFilename` in the YAML. If present and it resolves to a valid `TreasureConfigFields`, replace `barrelContents` with `new ChestContents(thatConfig)`. + +**Step 4: Build check** + +Run: `./gradlew compileJava` +Expected: BUILD SUCCESSFUL. + +**Step 5: Commit** + +```bash +git add src/main/java/com/magmaguy/betterstructures/config/schematics/SchematicConfigField.java +git commit -m "feat(lootify): per-schematic barrelTreasureFilename override" +``` + +--- + +## Task 7: Inherent barrel detection + chest/barrel routing in the schematic pipeline + +**Files:** +- Modify: `src/main/java/com/magmaguy/betterstructures/schematics/SchematicContainer.java` +- Modify: `src/main/java/com/magmaguy/betterstructures/buildingfitter/FitAnything.java` + +**Step 1: Widen the chest-location collector in `SchematicContainer.java`** + +At line 74-77, change: + +```java +if (minecraftMaterial.equals(Material.CHEST) || + minecraftMaterial.equals(Material.TRAPPED_CHEST) || + minecraftMaterial.equals(Material.SHULKER_BOX)) { + chestLocations.add(new Vector(x, y, z)); +} +``` + +to: + +```java +if (minecraftMaterial.equals(Material.CHEST) || + minecraftMaterial.equals(Material.TRAPPED_CHEST) || + minecraftMaterial.equals(Material.SHULKER_BOX) || + minecraftMaterial.equals(Material.BARREL)) { + chestLocations.add(new Vector(x, y, z)); +} +``` + +(`chestLocations` now technically means "loot-bearing container locations." Don't rename — too much surface area for what should be a small change.) + +**Step 2: Route chest vs barrel in `FitAnything.fillChests()`** + +Replace the existing `fillChests()` (lines ~290-315) with: + +```java +private void fillChests() { + 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; + } + + boolean isBarrel = container.getBlock().getType() == Material.BARREL; + if (isBarrel && !barrelsEnabled) continue; + if (!isBarrel && !chestsEnabled) continue; + + ChestContents contents; + String treasureFilename; + if (isBarrel) { + contents = schematicContainer.getBarrelContents() != null + ? schematicContainer.getBarrelContents() + : gen.getBarrelContents(); + treasureFilename = schematicContainer.getSchematicConfigField().getBarrelTreasureFilename() != null + ? schematicContainer.getSchematicConfigField().getBarrelTreasureFilename() + : gen.getBarrelTreasureFilename(); + } else { + contents = schematicContainer.getChestContents() != null + ? schematicContainer.getChestContents() + : gen.getChestContents(); + treasureFilename = schematicContainer.getChestContents() != null + ? schematicContainer.getSchematicConfigField().getTreasureFile() + : gen.getTreasureFilename(); + } + + if (contents == null) continue; + contents.rollChestContents(container); + + ChestFillEvent chestFillEvent = new ChestFillEvent(container, treasureFilename); + Bukkit.getServer().getPluginManager().callEvent(chestFillEvent); + if (!chestFillEvent.isCancelled()) { + container.update(true); + } + } +} +``` + +**Step 3: Build check** + +Run: `./gradlew compileJava` +Expected: BUILD SUCCESSFUL. If `getBarrelContents()` / `isGenerateLootInBarrels()` don't resolve, double-check Task 4 declared those fields with Lombok `@Getter` (note: `boolean` getters are `isX()` not `getX()`). + +**Step 4: Commit** + +```bash +git add src/main/java/com/magmaguy/betterstructures/schematics/SchematicContainer.java src/main/java/com/magmaguy/betterstructures/buildingfitter/FitAnything.java +git commit -m "feat(lootify): inherent barrel detection + routing in schematic pipeline" +``` + +--- + +## Task 8: Inherent barrel detection + fill in `ModulePasting` + +**Files:** +- Modify: `src/main/java/com/magmaguy/betterstructures/modules/ModulePasting.java` + +**Step 1: Allow `BARREL` to bypass NBT-rich deferral** + +Around line 91: + +```java +if (m == Material.CHEST || m == Material.TRAPPED_CHEST) return false; +``` + +becomes: + +```java +if (m == Material.CHEST || m == Material.TRAPPED_CHEST || m == Material.BARREL) return false; +``` + +**Step 2: Track barrel placements during paste** + +Inside the same `pasteableList.forEach(...)` loop that already special-cases signs (around lines 190-262), add a check: when the block being placed is a barrel, record its world location in a new `List barrelsToFill` (declare it alongside `chestsToPlace` at the top of `batchPaste`). + +Locate where `pasteableList.add(new Pasteable(pasteLocation, blockData))` is called (around line 261). Just before that line, add: + +```java +if (blockData.getMaterial() == Material.BARREL) { + barrelsToFill.add(pasteLocation); +} +``` + +Do NOT skip the normal paste path — the barrel still needs to be placed via `pasteableList` so its block data (orientation, etc.) is set correctly. The list just remembers where it ended up for the post-paste fill step. + +**Step 3: Fill barrels after paste** + +Find the existing chest-fill loop (lines ~386-405). After that loop, add a parallel loop for barrels: + +```java +if (moduleGeneratorsConfigFields.isGenerateLootInBarrels()) { + String barrelTreasureFilename = moduleGeneratorsConfigFields.getBarrelTreasureFilename(); + TreasureConfigFields barrelTreasureFields = TreasureConfig.getConfigFields(barrelTreasureFilename); + if (barrelTreasureFields != null) { + ChestContents barrelContents = new ChestContents(barrelTreasureFields); + for (Location barrelLocation : barrelsToFill) { + Block block = barrelLocation.getBlock(); + if (block.getType() != Material.BARREL) continue; // got overwritten somehow + if (!(block.getState() instanceof Container container)) continue; + + barrelContents.rollChestContents(container); + ChestFillEvent chestFillEvent = new ChestFillEvent(container, barrelTreasureFilename); + Bukkit.getServer().getPluginManager().callEvent(chestFillEvent); + if (!chestFillEvent.isCancelled()) { + container.update(true); + } + } + } else if (!barrelsToFill.isEmpty()) { + Logger.warn("Module generator " + moduleGeneratorsConfigFields.getFilename() + " has barrels in its modules but barrelTreasureFilename '" + barrelTreasureFilename + "' did not resolve to a valid treasure config. Barrels will be empty."); + } +} +``` + +(Same `new ChestContents(treasureFields)` per-paste construction as the existing chest path on line 396 — not lazy-load-violating because it's a batch paste operation, not per-tick.) + +**Step 4: Build check** + +Run: `./gradlew compileJava` +Expected: BUILD SUCCESSFUL. + +**Step 5: Commit** + +```bash +git add src/main/java/com/magmaguy/betterstructures/modules/ModulePasting.java +git commit -m "feat(lootify): inherent barrel detection + fill in module pipeline" +``` + +--- + +## Task 9: Full build + testbed verification + +**Files:** none modified — verification only. + +**Step 1: Full plugin build** + +Per [feedback_full_builds.md](../../../../../.claude/projects/C--Users-tiago-Documents-MineCraftProjects/memory/feedback_full_builds.md), produce a usable jar. + +Run: `./gradlew clean build` +Expected: BUILD SUCCESSFUL, jar at `build/libs/BetterStructures-*.jar`. + +**Step 2: Deploy + start the server once** + +Use the testbed setup ([reference_testbed_setup.md](../../../../../.claude/projects/C--Users-tiago-Documents-MineCraftProjects/memory/reference_testbed_setup.md)). Start the server once so the new `treasure_barrel_food.yml` writes, then stop it and confirm: +- File exists at `plugins/BetterStructures/treasures/treasure_barrel_food.yml` +- Has three rarity tiers +- `mean: 1.0`, `standardDeviation: 0.7` + +**Step 3: Verify the schematic pathway** + +1. Place a barrel inside a test schematic used by a known generator. +2. Trigger a structure paste. +3. Open the placed barrel. + +Expected: 1-3 items, all food, drawn from the table in Task 1. Over ~10 paste runs you should see mostly common-tier items, occasional cooked food, rare epic items. + +**Step 4: Verify the module pathway** + +1. Place a barrel inside a module schematic (no sign needed). +2. Trigger module pasting. +3. Open the placed barrel. + +Expected: same behavior as Step 3. + +**Step 5: Verify the per-generator opt-out** + +1. In `plugins/BetterStructures/generators/.yml`, set `generateLootInBarrels: false`. +2. Reload (or restart). +3. Trigger a paste with a barrel. + +Expected: barrel is placed but empty. + +**Step 6: Verify the per-generator treasure override** + +1. Reset that generator's `generateLootInBarrels` to default (or remove the key). +2. Set `barrelTreasureFilename: treasure_overworld_surface`. +3. Trigger a paste with a barrel. + +Expected: barrel now contains overworld-chest loot (gear, etc.) — proving the override path works end-to-end. + +**Step 7: Verify the per-schematic treasure override** + +1. Restore `barrelTreasureFilename` to default in the generator. +2. In the schematic config, set `barrelTreasureFilename: treasure_overworld_surface`. +3. Trigger a paste. + +Expected: that schematic's barrels carry overworld loot, other schematics in the same generator still carry food. + +**Step 8: Verify `ChestFillEvent` fires for barrels** + +Optional sanity check: add a temporary `Logger.info` to a `ChestFillEvent` consumer (or write a tiny listener plugin), confirm the event fires with `container.getBlock().getType() == BARREL` and the right `getTreasureConfigFilename()`. + +**Step 9: Commit any tweaks** + +If verification surfaces real bugs, fix them with focused commits. Don't bundle into the earlier feature commits. + +--- + +## Notes for the executing engineer + +- **DRY:** `ChestContents` is reused, not duplicated. Resist the urge to make a `BarrelContents` class. +- **YAGNI:** No `BarrelFillEvent`. `ChestFillEvent` is already container-generic; consumers can branch on `getContainer().getBlock().getType()`. No `[barrel]` sign marker — barrels are inherent. +- **TDD:** This codebase doesn't have unit-test coverage for the chest/loot pipeline (verify: `grep -r "rollChestContents" src/test`). Verification is manual on the testbed — own that explicitly per [feedback_full_builds.md](../../../../../.claude/projects/C--Users-tiago-Documents-MineCraftProjects/memory/feedback_full_builds.md). If you want one unit test, the cheapest valuable one: instantiate `ChestContents` with a synthetic `TreasureConfigFields`, hand it a mock `Container`, assert `rollChestContents` populates the inventory with 1-3 items when `mean=1`/`stddev=0.7`. Don't gate this PR on it. +- **Lazy loading:** Per [feedback_lazy_loading.md](../../../../../.claude/projects/C--Users-tiago-Documents-MineCraftProjects/memory/feedback_lazy_loading.md), the cached path uses one `ChestContents` per generator (built at config load in Task 4). The modules path constructs per-batch (Task 8 step 3) — that matches the existing chest behavior at `ModulePasting:396`, so it's not a regression. +- **Magmacore:** This plan touches only BetterStructures internals. No Magmacore changes, so [reference_magmacore_publish_workflow.md](../../../../../.claude/projects/C--Users-tiago-Documents-MineCraftProjects/memory/reference_magmacore_publish_workflow.md) does not apply. +- **Commits:** One per task. Branch name suggestion: `feat/barrel-loot`. From dc7ceda7ceebcfe6343696dd4e15e7b1acfcea8d Mon Sep 17 00:00:00 2001 From: MagmaGuy Date: Sun, 24 May 2026 16:52:56 +0100 Subject: [PATCH 02/20] feat(lootify): add tiered barrel food loot map Co-Authored-By: Claude Opus 4.7 (1M context) --- .../util/DefaultChestContents.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/main/java/com/magmaguy/betterstructures/util/DefaultChestContents.java b/src/main/java/com/magmaguy/betterstructures/util/DefaultChestContents.java index 234ccb6..0018df2 100644 --- a/src/main/java/com/magmaguy/betterstructures/util/DefaultChestContents.java +++ b/src/main/java/com/magmaguy/betterstructures/util/DefaultChestContents.java @@ -222,6 +222,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()); From 14d3bf466b40725a21cfab52e55b23370671f612 Mon Sep 17 00:00:00 2001 From: MagmaGuy Date: Sun, 24 May 2026 16:56:45 +0100 Subject: [PATCH 03/20] refactor(lootify): source mean/stddev defaults from field, not literals Co-Authored-By: Claude Opus 4.7 (1M context) --- .../config/treasures/TreasureConfigFields.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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..34408ff 100644 --- a/src/main/java/com/magmaguy/betterstructures/config/treasures/TreasureConfigFields.java +++ b/src/main/java/com/magmaguy/betterstructures/config/treasures/TreasureConfigFields.java @@ -51,8 +51,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(); From 82e8b6e0ef619be41e527040592a9fae67ff73cc Mon Sep 17 00:00:00 2001 From: MagmaGuy Date: Sun, 24 May 2026 17:02:52 +0100 Subject: [PATCH 04/20] feat(lootify): ship treasure_barrel_food premade (mean=1, stddev=0.7) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../treasures/premade/BarrelFoodTreasureConfig.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/main/java/com/magmaguy/betterstructures/config/treasures/premade/BarrelFoodTreasureConfig.java 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); + } +} From ce3a438f5b226b7f8ee08672f196a34ec98d62bc Mon Sep 17 00:00:00 2001 From: MagmaGuy Date: Sun, 24 May 2026 17:05:40 +0100 Subject: [PATCH 05/20] feat(lootify): generator-level barrel loot config (default on) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../generators/GeneratorConfigFields.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) 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..864c056 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"; + @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", 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."); + } + } } /** From 5f5d699c749eb4a87b3629709c7f493dafd03fc6 Mon Sep 17 00:00:00 2001 From: MagmaGuy Date: Sun, 24 May 2026 17:10:01 +0100 Subject: [PATCH 06/20] feat(lootify): module-level barrel loot config (default on) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../config/modules/ModulesConfigFields.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) 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..6ec13cd 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,12 @@ public class ModulesConfigFields extends CustomConfigFields { private String treasureFile = null; @Setter private ChestContents chestContents = null; + @Setter + private String barrelTreasureFilename = "treasure_barrel_food"; + @Setter + private ChestContents barrelContents = null; + @Setter + private boolean generateLootInBarrels = true; private Map borderMap = new HashMap<>(); private Integer minY = -4; private Integer maxY = 20; @@ -73,6 +79,18 @@ public ChestContents getChestContents() { return clonedConfig == null ? chestContents : clonedConfig.getChestContents(); } + public String getBarrelTreasureFilename() { + return clonedConfig == null ? barrelTreasureFilename : clonedConfig.getBarrelTreasureFilename(); + } + + public ChestContents getBarrelContents() { + return clonedConfig == null ? barrelContents : clonedConfig.getBarrelContents(); + } + + public boolean isGenerateLootInBarrels() { + return clonedConfig == null ? generateLootInBarrels : clonedConfig.isGenerateLootInBarrels(); + } + public Map getBorderMap() { return clonedConfig == null ? borderMap : clonedConfig.getBorderMap(); } @@ -145,6 +163,16 @@ public void processConfigFields() { } this.chestContents = treasureConfigFields.getChestContents(); } + this.generateLootInBarrels = processBoolean("generateLootInBarrels", generateLootInBarrels, true, true); + this.barrelTreasureFilename = processString("barrelTreasureFilename", barrelTreasureFilename, "treasure_barrel_food", true); + if (generateLootInBarrels && barrelTreasureFilename != null && !barrelTreasureFilename.isEmpty()) { + TreasureConfigFields barrelTreasureConfigFields = TreasureConfig.getConfigFields(barrelTreasureFilename); + if (barrelTreasureConfigFields == null) { + Logger.warn("Failed to get barrel treasure config file " + barrelTreasureFilename + " for module configuration " + filename + " ! Barrels will be empty."); + } else { + this.barrelContents = barrelTreasureConfigFields.getChestContents(); + } + } this.borderMap = processMap("borders", new HashMap<>()); this.minY = processInt("minY", minY, minY, true); this.maxY = processInt("maxY", maxY, maxY, true); @@ -175,6 +203,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 +212,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); From 7fc741618c0697a10b77aa9b93341f2b6046d233 Mon Sep 17 00:00:00 2001 From: MagmaGuy Date: Sun, 24 May 2026 17:14:59 +0100 Subject: [PATCH 07/20] feat(lootify): per-schematic barrelTreasureFilename override Co-Authored-By: Claude Opus 4.7 (1M context) --- .../schematics/SchematicConfigField.java | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) 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(); } } From 3fb04bfe6674d4ca5189fc70a09a03648ca2e731 Mon Sep 17 00:00:00 2001 From: MagmaGuy Date: Sun, 24 May 2026 17:20:02 +0100 Subject: [PATCH 08/20] feat(lootify): inherent barrel detection + routing in schematic pipeline Co-Authored-By: Claude Opus 4.7 (1M context) --- .../buildingfitter/FitAnything.java | 60 ++++++++++++------- .../schematics/SchematicContainer.java | 14 ++++- 2 files changed, 53 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/magmaguy/betterstructures/buildingfitter/FitAnything.java b/src/main/java/com/magmaguy/betterstructures/buildingfitter/FitAnything.java index deb105a..828cc7a 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,49 @@ 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() != null + ? schematicContainer.getBarrelContents() + : gen.getBarrelContents(); + treasureFilename = schematicContainer.getSchematicConfigField().getBarrelTreasureFilename() != null + ? schematicContainer.getSchematicConfigField().getBarrelTreasureFilename() + : gen.getBarrelTreasureFilename(); + } else { + contents = schematicContainer.getChestContents() != null + ? schematicContainer.getChestContents() + : gen.getChestContents(); + treasureFilename = schematicContainer.getChestContents() != null + ? schematicContainer.getSchematicConfigField().getTreasureFile() + : 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/schematics/SchematicContainer.java b/src/main/java/com/magmaguy/betterstructures/schematics/SchematicContainer.java index 0443057..e9ea70c 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) || @@ -143,6 +146,15 @@ public SchematicContainer(Clipboard clipboard, String clipboardFilename, Schemat } chestContents = schematicConfigField.getChestContents(); } + barrelContents = generatorConfigFields.getBarrelContents(); + 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()); + return; + } + barrelContents = schematicConfigField.getBarrelContents(); + } if (valid) generatorConfigFields.getStructureTypes().forEach(structureType -> schematics.put(structureType, this)); } From 5abca349944480f094d8591c401aaeb935eac498 Mon Sep 17 00:00:00 2001 From: MagmaGuy Date: Sun, 24 May 2026 17:26:12 +0100 Subject: [PATCH 09/20] fix(lootify): correct schematic treasure-file resolution in fillChests Previously the treasureFilename ternary in fillChests keyed off schematicContainer.getChestContents() != null, which is now always true after SchematicContainer unconditionally seeds chestContents from the generator. That caused ChestFillEvent.getTreasureFilename() to be null when the schematic had no per-schematic treasureFile. Now keys off the source field directly. Also simplifies the dead fallback ternaries for contents (both chest and barrel branches). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../buildingfitter/FitAnything.java | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/magmaguy/betterstructures/buildingfitter/FitAnything.java b/src/main/java/com/magmaguy/betterstructures/buildingfitter/FitAnything.java index 828cc7a..54fa5bc 100644 --- a/src/main/java/com/magmaguy/betterstructures/buildingfitter/FitAnything.java +++ b/src/main/java/com/magmaguy/betterstructures/buildingfitter/FitAnything.java @@ -308,18 +308,16 @@ private void fillChests() { ChestContents contents; String treasureFilename; if (isBarrel) { - contents = schematicContainer.getBarrelContents() != null - ? schematicContainer.getBarrelContents() - : gen.getBarrelContents(); - treasureFilename = schematicContainer.getSchematicConfigField().getBarrelTreasureFilename() != null - ? schematicContainer.getSchematicConfigField().getBarrelTreasureFilename() + contents = schematicContainer.getBarrelContents(); + String schematicBarrelFile = schematicContainer.getSchematicConfigField().getBarrelTreasureFilename(); + treasureFilename = (schematicBarrelFile != null && !schematicBarrelFile.isEmpty()) + ? schematicBarrelFile : gen.getBarrelTreasureFilename(); } else { - contents = schematicContainer.getChestContents() != null - ? schematicContainer.getChestContents() - : gen.getChestContents(); - treasureFilename = schematicContainer.getChestContents() != null - ? schematicContainer.getSchematicConfigField().getTreasureFile() + contents = schematicContainer.getChestContents(); + String schematicTreasureFile = schematicContainer.getSchematicConfigField().getTreasureFile(); + treasureFilename = (schematicTreasureFile != null && !schematicTreasureFile.isEmpty()) + ? schematicTreasureFile : gen.getTreasureFilename(); } From dd5e2b69828b04df2b9308cb3ec29fd573c65094 Mon Sep 17 00:00:00 2001 From: MagmaGuy Date: Sun, 24 May 2026 17:30:28 +0100 Subject: [PATCH 10/20] feat(lootify): inherent barrel detection + fill in module pipeline Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ModuleGeneratorsConfigFields.java | 6 ++++ .../modules/ModulePasting.java | 29 ++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) 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..1d5e4ae 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"; + @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", 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/modules/ModulePasting.java b/src/main/java/com/magmaguy/betterstructures/modules/ModulePasting.java index 5521bde..18c15c7 100644 --- a/src/main/java/com/magmaguy/betterstructures/modules/ModulePasting.java +++ b/src/main/java/com/magmaguy/betterstructures/modules/ModulePasting.java @@ -44,6 +44,7 @@ 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 +89,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; @@ -257,6 +258,10 @@ private List generatePasteMeList(Clipboard clipboard, return; // do NOT add to normal paste list } + if (blockData.getMaterial() == Material.BARREL) { + barrelsToFill.add(pasteLocation); + } + // Normal placement path pasteableList.add(new Pasteable(pasteLocation, blockData)); }); @@ -404,6 +409,28 @@ private void postPasteProcessing(List entityPasteInfos) { } } + if (moduleGeneratorsConfigFields.isGenerateLootInBarrels()) { + String barrelTreasureFilename = moduleGeneratorsConfigFields.getBarrelTreasureFilename(); + TreasureConfigFields barrelTreasureFields = TreasureConfig.getConfigFields(barrelTreasureFilename); + if (barrelTreasureFields != null) { + ChestContents barrelContents = new ChestContents(barrelTreasureFields); + for (Location barrelLocation : barrelsToFill) { + Block block = barrelLocation.getBlock(); + if (block.getType() != Material.BARREL) continue; + if (!(block.getState() instanceof Container container)) continue; + + barrelContents.rollChestContents(container); + ChestFillEvent chestFillEvent = new ChestFillEvent(container, barrelTreasureFilename); + Bukkit.getServer().getPluginManager().callEvent(chestFillEvent); + if (!chestFillEvent.isCancelled()) { + container.update(true); + } + } + } else if (!barrelsToFill.isEmpty()) { + Logger.warn("Module generator " + moduleGeneratorsConfigFields.getFilename() + " has barrels in its modules but barrelTreasureFilename '" + barrelTreasureFilename + "' did not resolve to a valid treasure config. Barrels will be empty."); + } + } + // 4) Spawn entities last for (EntitySpawn entitySpawn : entitiesToSpawn) { try { From e906bc76bade8d484e313b9bdc3c82ad1992c46e Mon Sep 17 00:00:00 2001 From: MagmaGuy Date: Sun, 24 May 2026 17:48:38 +0100 Subject: [PATCH 11/20] fix(lootify): correct treasure lookup + soften failure handling in SchematicContainer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pre-existing bug: chest override passed schematicConfigField.getFilename() to TreasureConfig.getConfigFields() instead of getTreasureFile(), so the defensive verification in SchematicContainer never resolved correctly. Also soften the early returns on treasure-resolution failure for both chest and barrel paths — a typo in treasureFile / barrelTreasureFilename should not exclude the schematic from generation. We fall back to the generator-level defaults that were already assigned. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../schematics/SchematicContainer.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/magmaguy/betterstructures/schematics/SchematicContainer.java b/src/main/java/com/magmaguy/betterstructures/schematics/SchematicContainer.java index e9ea70c..72a3458 100644 --- a/src/main/java/com/magmaguy/betterstructures/schematics/SchematicContainer.java +++ b/src/main/java/com/magmaguy/betterstructures/schematics/SchematicContainer.java @@ -138,22 +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(); } - chestContents = schematicConfigField.getChestContents(); } - barrelContents = generatorConfigFields.getBarrelContents(); 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()); - return; + Logger.warn("Failed to get barrel treasure configuration " + schematicConfigField.getBarrelTreasureFilename() + " for schematic " + schematicConfigField.getFilename() + " ! Defaulting to the generator barrel treasure."); + } else { + barrelContents = schematicConfigField.getBarrelContents(); } - barrelContents = schematicConfigField.getBarrelContents(); } if (valid) generatorConfigFields.getStructureTypes().forEach(structureType -> schematics.put(structureType, this)); From 461a3520b4cb6792f0f81ec45f42b02db1cc291a Mon Sep 17 00:00:00 2001 From: MagmaGuy Date: Sun, 24 May 2026 17:52:16 +0100 Subject: [PATCH 12/20] feat(lootify): per-module barrel treasure + opt-out in module pipeline Track each placed barrel with its source ModulesConfigFields so per-module barrelTreasureFilename and generateLootInBarrels overrides take effect. The module-generator-level toggle remains a hard kill-switch. Treasure configs are cached per unique name to avoid rebuilding ChestContents across many barrels. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../modules/ModulePasting.java | 66 +++++++++++++------ 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/magmaguy/betterstructures/modules/ModulePasting.java b/src/main/java/com/magmaguy/betterstructures/modules/ModulePasting.java index 18c15c7..3a4aa8c 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,12 +40,14 @@ import java.io.File; import java.util.ArrayList; import java.util.Deque; +import java.util.HashMap; import java.util.List; +import java.util.Map; 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 barrelsToFill = new ArrayList<>(); private final List entitiesToSpawn = new ArrayList<>(); private final String spawnPoolSuffix; private final Location startLocation; @@ -186,7 +189,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 @@ -259,7 +263,7 @@ private List generatePasteMeList(Clipboard clipboard, } if (blockData.getMaterial() == Material.BARREL) { - barrelsToFill.add(pasteLocation); + barrelsToFill.add(new BarrelPlacement(pasteLocation, modulesConfigFields)); } // Normal placement path @@ -292,8 +296,9 @@ public List batchPaste(Deque WFCNodeDeque, List entityPasteInfos) { } } - if (moduleGeneratorsConfigFields.isGenerateLootInBarrels()) { - String barrelTreasureFilename = moduleGeneratorsConfigFields.getBarrelTreasureFilename(); - TreasureConfigFields barrelTreasureFields = TreasureConfig.getConfigFields(barrelTreasureFilename); - if (barrelTreasureFields != null) { - ChestContents barrelContents = new ChestContents(barrelTreasureFields); - for (Location barrelLocation : barrelsToFill) { - Block block = barrelLocation.getBlock(); - if (block.getType() != Material.BARREL) continue; - if (!(block.getState() instanceof Container container)) continue; - - barrelContents.rollChestContents(container); - ChestFillEvent chestFillEvent = new ChestFillEvent(container, barrelTreasureFilename); - Bukkit.getServer().getPluginManager().callEvent(chestFillEvent); - if (!chestFillEvent.isCancelled()) { - container.update(true); + if (moduleGeneratorsConfigFields.isGenerateLootInBarrels() && !barrelsToFill.isEmpty()) { + Map contentsByTreasure = new HashMap<>(); + java.util.Set warnedMissingTreasures = new java.util.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); } - } else if (!barrelsToFill.isEmpty()) { - Logger.warn("Module generator " + moduleGeneratorsConfigFields.getFilename() + " has barrels in its modules but barrelTreasureFilename '" + barrelTreasureFilename + "' did not resolve to a valid treasure config. Barrels will be empty."); } } @@ -468,6 +489,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) { } From 04e7eede0d1963b7a00e09047c46e58643621818 Mon Sep 17 00:00:00 2001 From: MagmaGuy Date: Sun, 24 May 2026 17:56:56 +0100 Subject: [PATCH 13/20] style(lootify): import Set/HashSet instead of fully-qualifying Cosmetic cleanup flagged in the follow-up review. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../com/magmaguy/betterstructures/modules/ModulePasting.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/magmaguy/betterstructures/modules/ModulePasting.java b/src/main/java/com/magmaguy/betterstructures/modules/ModulePasting.java index 3a4aa8c..52577ce 100644 --- a/src/main/java/com/magmaguy/betterstructures/modules/ModulePasting.java +++ b/src/main/java/com/magmaguy/betterstructures/modules/ModulePasting.java @@ -41,8 +41,10 @@ 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<>(); @@ -416,7 +418,7 @@ private void postPasteProcessing(List entityPasteInfos) { if (moduleGeneratorsConfigFields.isGenerateLootInBarrels() && !barrelsToFill.isEmpty()) { Map contentsByTreasure = new HashMap<>(); - java.util.Set warnedMissingTreasures = new java.util.HashSet<>(); + Set warnedMissingTreasures = new HashSet<>(); for (BarrelPlacement bp : barrelsToFill) { ModulesConfigFields modConfig = bp.modulesConfigFields(); if (modConfig != null && !modConfig.isGenerateLootInBarrels()) continue; From dafb84915dec1275adacc9bed4873f35ac8dff76 Mon Sep 17 00:00:00 2001 From: MagmaGuy Date: Sun, 24 May 2026 18:16:48 +0100 Subject: [PATCH 14/20] fix(lootify): drop spurious eager barrel-treasure lookup in ModulesConfigFields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cached barrelContents field on ModulesConfigFields was never read anywhere — ModulePasting resolves the treasure by filename at fill time. The eager lookup in processConfigFields was emitting a "Failed to get barrel treasure config file" warning per module on servers where the TreasureConfig had not yet finished registering the BarrelFoodTreasureConfig premade (older builds, partial deploys). Removing the dead field, dead getter, and dead lookup. The per-module override still works via the barrelTreasureFilename string, which ModulePasting reads at fill time. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../config/modules/ModulesConfigFields.java | 14 -------------- 1 file changed, 14 deletions(-) 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 6ec13cd..2960537 100644 --- a/src/main/java/com/magmaguy/betterstructures/config/modules/ModulesConfigFields.java +++ b/src/main/java/com/magmaguy/betterstructures/config/modules/ModulesConfigFields.java @@ -23,8 +23,6 @@ public class ModulesConfigFields extends CustomConfigFields { @Setter private String barrelTreasureFilename = "treasure_barrel_food"; @Setter - private ChestContents barrelContents = null; - @Setter private boolean generateLootInBarrels = true; private Map borderMap = new HashMap<>(); private Integer minY = -4; @@ -83,10 +81,6 @@ public String getBarrelTreasureFilename() { return clonedConfig == null ? barrelTreasureFilename : clonedConfig.getBarrelTreasureFilename(); } - public ChestContents getBarrelContents() { - return clonedConfig == null ? barrelContents : clonedConfig.getBarrelContents(); - } - public boolean isGenerateLootInBarrels() { return clonedConfig == null ? generateLootInBarrels : clonedConfig.isGenerateLootInBarrels(); } @@ -165,14 +159,6 @@ public void processConfigFields() { } this.generateLootInBarrels = processBoolean("generateLootInBarrels", generateLootInBarrels, true, true); this.barrelTreasureFilename = processString("barrelTreasureFilename", barrelTreasureFilename, "treasure_barrel_food", true); - if (generateLootInBarrels && barrelTreasureFilename != null && !barrelTreasureFilename.isEmpty()) { - TreasureConfigFields barrelTreasureConfigFields = TreasureConfig.getConfigFields(barrelTreasureFilename); - if (barrelTreasureConfigFields == null) { - Logger.warn("Failed to get barrel treasure config file " + barrelTreasureFilename + " for module configuration " + filename + " ! Barrels will be empty."); - } else { - this.barrelContents = barrelTreasureConfigFields.getChestContents(); - } - } this.borderMap = processMap("borders", new HashMap<>()); this.minY = processInt("minY", minY, minY, true); this.maxY = processInt("maxY", maxY, maxY, true); From a97639c7dc1d9fa1df49e012d1fdd330956b3485 Mon Sep 17 00:00:00 2001 From: MagmaGuy Date: Sun, 24 May 2026 18:28:08 +0100 Subject: [PATCH 15/20] fix(lootify): use treasure_barrel_food.yml in references; normalize lookup Root cause: CustomConfigFields auto-appends .yml to the filename when constructed (CustomConfigFields:41), so the premade is stored under the map key "treasure_barrel_food.yml". My defaults referenced the value without .yml ("treasure_barrel_food"), so TreasureConfig.getConfigFields() returned null for every generator and module on user servers. Matches the existing convention used by premade generator configs (GeneratorLiquidNether.setTreasureFilename("treasure_nether.yml") etc). Also normalize TreasureConfig.getConfigFields() to auto-append .yml when missing, so existing user YAMLs that already have the old default baked in continue to resolve correctly without manual fixup. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../config/generators/GeneratorConfigFields.java | 4 ++-- .../config/modulegenerators/ModuleGeneratorsConfigFields.java | 4 ++-- .../betterstructures/config/modules/ModulesConfigFields.java | 4 ++-- .../betterstructures/config/treasures/TreasureConfig.java | 4 +++- 4 files changed, 9 insertions(+), 7 deletions(-) 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 864c056..9cb68c5 100644 --- a/src/main/java/com/magmaguy/betterstructures/config/generators/GeneratorConfigFields.java +++ b/src/main/java/com/magmaguy/betterstructures/config/generators/GeneratorConfigFields.java @@ -43,7 +43,7 @@ public class GeneratorConfigFields extends CustomConfigFields { private ChestContents chestContents = null; @Getter @Setter - private String barrelTreasureFilename = "treasure_barrel_food"; + private String barrelTreasureFilename = "treasure_barrel_food.yml"; @Getter private ChestContents barrelContents = null; @Getter @@ -97,7 +97,7 @@ public void processConfigFields() { this.generateLootInBarrels = processBoolean("generateLootInBarrels", generateLootInBarrels, true, false); // Load barrel treasure config (defaults to the food premade) - this.barrelTreasureFilename = processString("barrelTreasureFilename", barrelTreasureFilename, "treasure_barrel_food", false); + this.barrelTreasureFilename = processString("barrelTreasureFilename", barrelTreasureFilename, "treasure_barrel_food.yml", false); if (generateLootInBarrels) { TreasureConfigFields barrelTreasureConfig = TreasureConfig.getConfigFields(barrelTreasureFilename); if (barrelTreasureConfig != null) { 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 1d5e4ae..0d0399f 100644 --- a/src/main/java/com/magmaguy/betterstructures/config/modulegenerators/ModuleGeneratorsConfigFields.java +++ b/src/main/java/com/magmaguy/betterstructures/config/modulegenerators/ModuleGeneratorsConfigFields.java @@ -37,7 +37,7 @@ public class ModuleGeneratorsConfigFields extends CustomConfigFields { @Getter protected boolean generateLootInBarrels = true; @Getter - protected String barrelTreasureFilename = "treasure_barrel_food"; + protected String barrelTreasureFilename = "treasure_barrel_food.yml"; @Getter @Setter private List validWorlds = null; @@ -85,7 +85,7 @@ public void processConfigFields() { 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", 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 2960537..e7d0486 100644 --- a/src/main/java/com/magmaguy/betterstructures/config/modules/ModulesConfigFields.java +++ b/src/main/java/com/magmaguy/betterstructures/config/modules/ModulesConfigFields.java @@ -21,7 +21,7 @@ public class ModulesConfigFields extends CustomConfigFields { @Setter private ChestContents chestContents = null; @Setter - private String barrelTreasureFilename = "treasure_barrel_food"; + private String barrelTreasureFilename = "treasure_barrel_food.yml"; @Setter private boolean generateLootInBarrels = true; private Map borderMap = new HashMap<>(); @@ -158,7 +158,7 @@ public void processConfigFields() { this.chestContents = treasureConfigFields.getChestContents(); } this.generateLootInBarrels = processBoolean("generateLootInBarrels", generateLootInBarrels, true, true); - this.barrelTreasureFilename = processString("barrelTreasureFilename", barrelTreasureFilename, "treasure_barrel_food", 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); 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); } } From 7086cf7b4823dd37276fca4786cfad2ed9f2b0c6 Mon Sep 17 00:00:00 2001 From: MagmaGuy Date: Thu, 28 May 2026 12:42:04 +0100 Subject: [PATCH 16/20] =?UTF-8?q?BetterStructures=202.4.0:=20-=20[New]=20B?= =?UTF-8?q?arrel=20loot=20generation=20=E2=80=94=20barrels=20in=20schemati?= =?UTF-8?q?cs=20and=20modules=20now=20get=20treasure=20rolled=20into=20the?= =?UTF-8?q?m=20on=20placement,=20gated=20by=20new=20`generateLootInBarrels?= =?UTF-8?q?`=20toggles=20on=20both=20the=20generator=20config=20(top-level?= =?UTF-8?q?=20kill-switch)=20and=20per-module=20config=20(defaults=20true?= =?UTF-8?q?=20on=20both).=20-=20[New]=20Inherent=20barrel=20detection=20?= =?UTF-8?q?=E2=80=94=20barrels=20baked=20into=20schematics=20and=20modules?= =?UTF-8?q?=20are=20now=20picked=20up=20automatically=20with=20no=20specia?= =?UTF-8?q?l=20markup=20required;=20routed=20through=20the=20same=20fill?= =?UTF-8?q?=20pipeline=20as=20chests.=20-=20[New]=20`barrelTreasureFilenam?= =?UTF-8?q?e`=20override=20=E2=80=94=20per-schematic=20and=20per-module=20?= =?UTF-8?q?field=20for=20pointing=20a=20placement=20at=20a=20specific=20tr?= =?UTF-8?q?easure=20config;=20falls=20back=20to=20the=20generator-level=20?= =?UTF-8?q?default=20on=20typo=20or=20missing=20rather=20than=20skipping?= =?UTF-8?q?=20the=20schematic.=20-=20[New]=20`treasure=5Fbarrel=5Ffood.yml?= =?UTF-8?q?`=20premade=20=E2=80=94=20ships=20a=20tiered=20food=20loot=20ma?= =?UTF-8?q?p=20(mean=3D1,=20stddev=3D0.7)=20as=20the=20default=20barrel=20?= =?UTF-8?q?treasure,=20matching=20the=20`treasure=5F*.yml`=20convention.?= =?UTF-8?q?=20-=20[New]=20Tiered=20barrel=20food=20loot=20map=20=E2=80=94?= =?UTF-8?q?=20drop-in=20food=20tier=20definitions=20backing=20the=20new=20?= =?UTF-8?q?premade.=20-=20[Fix]=20`treasure=5Fbarrel=5Ffood`=20lookup=20no?= =?UTF-8?q?w=20resolves=20correctly=20=E2=80=94=20the=20premade=20key=20wa?= =?UTF-8?q?s=20missing=20`.yml`,=20breaking=20every=20generator=20and=20mo?= =?UTF-8?q?dule=20default;=20`TreasureConfig.getConfigFields()`=20now=20al?= =?UTF-8?q?so=20auto-appends=20`.yml`=20so=20existing=20user=20YAMLs=20kee?= =?UTF-8?q?p=20working=20without=20manual=20edits.=20-=20[Fix]=20Schematic?= =?UTF-8?q?=20chest=20override=20now=20resolves=20via=20`getTreasureFile()?= =?UTF-8?q?`=20instead=20of=20`getFilename()`=20=E2=80=94=20a=20typo=20in?= =?UTF-8?q?=20`treasureFile`=20or=20`barrelTreasureFilename`=20no=20longer?= =?UTF-8?q?=20excludes=20the=20schematic=20from=20generation.=20-=20[Fix]?= =?UTF-8?q?=20`ChestFillEvent.getTreasureFilename()`=20now=20reports=20the?= =?UTF-8?q?=20per-schematic=20treasure=20file=20when=20one=20is=20set,=20i?= =?UTF-8?q?nstead=20of=20always=20reporting=20null.=20-=20[Fix]=20Removed?= =?UTF-8?q?=20a=20spurious=20"Failed=20to=20get=20barrel=20treasure=20conf?= =?UTF-8?q?ig=20file"=20warning=20logged=20per=20module=20on=20partial=20d?= =?UTF-8?q?eploys.=20-=20[Tweak]=20Setup=20menu=20rebuilt=20on=20MagmaCore?= =?UTF-8?q?'s=20`SetupMenuBuilder`=20shared=20UI=20=E2=80=94=20same=20Stru?= =?UTF-8?q?cture=20Packs=20/=20Module=20Packs=20filters,=20cleaner=20filte?= =?UTF-8?q?r=20wiring.=20-=20[Tweak]=20MagmaCore=20shared=20assets=20now?= =?UTF-8?q?=20exported=20on=20enable=20via=20`MagmaCore.exportSharedAssets?= =?UTF-8?q?(this)`.=20-=20[Tweak]=20Various=20MagmaCore=20library=20improv?= =?UTF-8?q?ements=20(NMS=20v26=20/=20Paper=2026.1=20support,=20type-based?= =?UTF-8?q?=20`EntityDimensions`=20reflection=20with=20cached=20lookup,=20?= =?UTF-8?q?`WorldFolderResolver`=20and=20Paper-migration=20debris=20quaran?= =?UTF-8?q?tine,=20`TemporaryBlockManager`,=20`SetupMenuBuilder`=20+=20`In?= =?UTF-8?q?foButtonFactory`=20+=20`NightbreakSetupIcons`=20shared=20UI,=20?= =?UTF-8?q?`ContainerAllowlist`=20/=20`WorldProtectionRules`=20instance=20?= =?UTF-8?q?protection,=20`MagmaCore.exportSharedAssets`=20/=20`enableWorld?= =?UTF-8?q?Protections`=20/=20`isShutdownRequested`,=20`AdvancedCommand`?= =?UTF-8?q?=20optional-argument=20support,=20`LocationAPI`=20with=20WorldG?= =?UTF-8?q?uard=20+=20GriefPrevention=20providers,=20Nightbreak=20token-ch?= =?UTF-8?q?ange=20subscribers,=20`ClientboundEntityPositionSyncPacket`=20a?= =?UTF-8?q?doption=20on=201.21.2+=20with=20Bedrock=20`ClientboundBundlePac?= =?UTF-8?q?ket`=20skip,=20ArmorStand=20HeadPose=20force-send=20every=20tic?= =?UTF-8?q?k=20for=20Bedrock=20attachable=20rebind,=20`DAMAGE=5FINDICATOR`?= =?UTF-8?q?=20outbound=20particle-count=20clamp,=20per-plugin=20Netty=20ha?= =?UTF-8?q?ndler=20name=20in=20`PacketInteractionListener`,=20lazy=20Lua?= =?UTF-8?q?=20field=20support,=20`ZipFile`=20STORED-with-EXT-descriptor=20?= =?UTF-8?q?fix,=20dedup=20/=20rate-limited=20Nightbreak=20auth-failure=20l?= =?UTF-8?q?ogs,=20three-tier=20BedrockChecker=20detection=20=E2=80=94=20na?= =?UTF-8?q?me=20pattern=20`^\..*\d{4}$`=20+=20UUID-MSB=20=3D=3D=200=20+=20?= =?UTF-8?q?case-insensitive=20plugin=20lookup=20=E2=80=94=20so=20Bedrock?= =?UTF-8?q?=20viewers=20are=20detected=20even=20when=20Floodgate=20hasn't?= =?UTF-8?q?=20registered=20them=20yet).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- build.gradle | 2 +- .../betterstructures/BetterStructures.java | 1 + .../menus/BetterStructuresSetupMenu.java | 45 ++++++++++--------- src/main/resources/plugin.yml | 2 +- 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/build.gradle b/build.gradle index e74b8ee..376a006 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { } group = 'com.magmaguy' -version = '2.3.1' +version = '2.4.0' repositories { mavenCentral() diff --git a/src/main/java/com/magmaguy/betterstructures/BetterStructures.java b/src/main/java/com/magmaguy/betterstructures/BetterStructures.java index c65dbaa..3b3c37c 100644 --- a/src/main/java/com/magmaguy/betterstructures/BetterStructures.java +++ b/src/main/java/com/magmaguy/betterstructures/BetterStructures.java @@ -51,6 +51,7 @@ public void onEnable() { throw new RuntimeException(e); } MagmaCore.onEnable(this); + MagmaCore.exportSharedAssets(this); MagmaCore.startInitialization(this, new PluginInitializationConfig("BetterStructures", "betterstructures.*", 16), this::asyncInitialization, diff --git a/src/main/java/com/magmaguy/betterstructures/menus/BetterStructuresSetupMenu.java b/src/main/java/com/magmaguy/betterstructures/menus/BetterStructuresSetupMenu.java index 20685b1..c8c3772 100644 --- a/src/main/java/com/magmaguy/betterstructures/menus/BetterStructuresSetupMenu.java +++ b/src/main/java/com/magmaguy/betterstructures/menus/BetterStructuresSetupMenu.java @@ -1,12 +1,13 @@ package com.magmaguy.betterstructures.menus; import com.magmaguy.betterstructures.MetadataHandler; +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.util.ChatColorConverter; import com.magmaguy.magmacore.util.ItemStackGenerator; import com.magmaguy.magmacore.util.Logger; @@ -82,27 +83,29 @@ public void onClick(Player p) { } }; - List allPackages = new ArrayList<>(bsPackages); - allPackages.add(new DownloadAllContentPackage<>(() -> new ArrayList<>(BSPackage.getBsPackages().values()), - "BetterStructures", - "https://nightbreak.io/plugin/betterstructures/", - "bs downloadall")); + new SetupMenuBuilder((JavaPlugin) MetadataHandler.PLUGIN, player) + .title("Setup menu") + .titleIconPrefix(null) + .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) + .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/resources/plugin.yml b/src/main/resources/plugin.yml index e4306d3..e5c7026 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.4.0' main: com.magmaguy.betterstructures.BetterStructures api-version: '1.21.4' depend: [ WorldEdit ] From 99d77a777ed1afac17443d9052b6599f980a6bb7 Mon Sep 17 00:00:00 2001 From: MagmaGuy Date: Sat, 30 May 2026 21:55:19 +0100 Subject: [PATCH 17/20] Remove completed planning docs --- docs/plans/2026-05-24-barrel-loot.md | 588 --------------------------- 1 file changed, 588 deletions(-) delete mode 100644 docs/plans/2026-05-24-barrel-loot.md diff --git a/docs/plans/2026-05-24-barrel-loot.md b/docs/plans/2026-05-24-barrel-loot.md deleted file mode 100644 index c9e140a..0000000 --- a/docs/plans/2026-05-24-barrel-loot.md +++ /dev/null @@ -1,588 +0,0 @@ -# Barrel Loot Generation Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add barrel loot generation to BetterStructures (Lootify system). Barrels found in generated structures fill with food-by-default loot, tuned to 1-3 items per barrel. Server owners can disable barrel fills, swap the loot table, or override per-schematic. - -**Architecture:** Mirror the existing chest pipeline. `ChestContents` already operates on `org.bukkit.block.Container`, which `Barrel` implements — no changes needed to the loot-rolling itself. The work is (1) detect barrels inherently in both pasting pipelines (modules + schematics) — no sign marker; (2) add a second `ChestContents` slot ("barrelContents") on generators/schematics pointing to a separate treasure file; (3) route the fill to chest- or barrel-contents based on the placed block type; (4) ship a `treasure_barrel_food.yml` premade with food-only defaults and `mean: 1`, `standardDeviation: 0.7` so each barrel rolls 1-3 items; (5) add a `generateLootInBarrels` flag (default `true`) that lets users opt out per-generator. - -**Tech Stack:** Java 17, Paper/Spigot API (`org.bukkit.block.Container`, `Barrel`, `Material.BARREL`), WorldEdit, Lombok, Gradle. Built via `./gradlew build`. - -**Design decisions (locked in):** -- **Inherent detection** — any `BARREL` block in a schematic or module is detected and filled. No `[barrel]` sign marker. -- **`generateLootInBarrels: true`** (default) on `GeneratorConfigFields` and `ModulesConfigFields`. Setting `false` skips barrel fills for that generator. No schematic-level override of this flag (YAGNI). -- **Separate `barrelTreasureFilename`** on `GeneratorConfigFields` / `SchematicConfigField` / `ModulesConfigFields`. Defaults to `"treasure_barrel_food"`. -- **Food-only default** with three rarity tiers (common / rare / epic, weights 60 / 30 / 10) mirroring the chest table shape. -- **1-3 items per barrel** via `mean: 1`, `standardDeviation: 0.7` on the barrel treasure config (the existing `ceil(gaussian) + 1` formula in `ChestContents.java:166-168` puts the distribution at 1-3 with a short tail). -- Reuse `ChestContents` class as-is — it's already container-agnostic. (Renaming would break public-API users.) -- Keep the existing `ChestFillEvent` for barrels too — it already accepts `Container`. No new event class. - -**Out of scope:** trapped barrels (don't exist in MC), per-instance barrel orientation handling beyond what vanilla barrel `BlockData` already does, MMOItems-only barrel tables (users can author those via the generic treasure config). - ---- - -## Files touched (overview) - -- **Create:** `src/main/java/com/magmaguy/betterstructures/config/treasures/premade/BarrelFoodTreasureConfig.java` -- **Modify:** `src/main/java/com/magmaguy/betterstructures/util/DefaultChestContents.java` — add `barrelFoodContents()` -- **Modify:** `src/main/java/com/magmaguy/betterstructures/config/treasures/TreasureConfigFields.java` — make `mean` / `standardDeviation` defaults respect the current field value so subclasses can tune (tiny refactor) -- **Modify:** `src/main/java/com/magmaguy/betterstructures/config/generators/GeneratorConfigFields.java` — add `barrelTreasureFilename`, `barrelContents`, `generateLootInBarrels` -- **Modify:** `src/main/java/com/magmaguy/betterstructures/config/modules/ModulesConfigFields.java` — add `barrelTreasureFilename`, `barrelContents`, `generateLootInBarrels` -- **Modify:** `src/main/java/com/magmaguy/betterstructures/config/schematics/SchematicConfigField.java` — add `barrelTreasureFilename` + `barrelContents` (per-schematic override of the treasure file only) -- **Modify:** `src/main/java/com/magmaguy/betterstructures/schematics/SchematicContainer.java` — add `Material.BARREL` to the location-collection check -- **Modify:** `src/main/java/com/magmaguy/betterstructures/buildingfitter/FitAnything.java` — route chest vs barrel in `fillChests()`; respect `generateLootInBarrels` -- **Modify:** `src/main/java/com/magmaguy/betterstructures/modules/ModulePasting.java` — track barrel blocks during paste, fill them after paste, respect `generateLootInBarrels`; widen `isNbtRichMaterial` to exclude `BARREL` - ---- - -## Task 1: Add the tiered food loot map - -**Files:** -- Modify: `src/main/java/com/magmaguy/betterstructures/util/DefaultChestContents.java` - -**Step 1: Add `barrelFoodContents()`** - -Append this method to `DefaultChestContents`, next to `overworldContents()`. Three tiers, weights 60 / 30 / 10, mirroring the chest tables. - -```java -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; -} -``` - -**Step 2: Build check** - -Run: `./gradlew compileJava` -Expected: BUILD SUCCESSFUL. - -**Step 3: Commit** - -```bash -git add src/main/java/com/magmaguy/betterstructures/util/DefaultChestContents.java -git commit -m "feat(lootify): add tiered barrel food loot map" -``` - ---- - -## Task 2: Let `TreasureConfigFields` subclasses override `mean` / `standardDeviation` defaults - -**Files:** -- Modify: `src/main/java/com/magmaguy/betterstructures/config/treasures/TreasureConfigFields.java` - -**Why this is needed:** `processConfigFields()` currently hardcodes the defaults — `processDouble("mean", mean, 4, true)`. If `BarrelFoodTreasureConfig` sets `setMean(1)` in its constructor, that call runs *before* `processConfigFields`, and the literal `4` in `processDouble` would still win as the on-disk default. By passing `mean` itself as the default, a subclass-set value becomes the default written to YAML. - -**Step 1: Edit `processConfigFields()`** - -In `TreasureConfigFields.java` around lines 54-55, change: - -```java -this.mean = processDouble("mean", mean, 4, true); -this.standardDeviation = processDouble("standardDeviation", standardDeviation, 3, true); -``` - -to: - -```java -this.mean = processDouble("mean", mean, mean, true); -this.standardDeviation = processDouble("standardDeviation", standardDeviation, standardDeviation, true); -``` - -Existing chest treasure configs keep working because their field initial values (declared at lines 37-40) are still `4` and `3` — same defaults, just sourced from the field instead of a literal. - -**Step 2: Build check** - -Run: `./gradlew compileJava` -Expected: BUILD SUCCESSFUL. - -**Step 3: Commit** - -```bash -git add src/main/java/com/magmaguy/betterstructures/config/treasures/TreasureConfigFields.java -git commit -m "refactor(lootify): source mean/stddev defaults from field, not literals" -``` - ---- - -## Task 3: Ship the `treasure_barrel_food` premade with tuned mean/stddev - -**Files:** -- Create: `src/main/java/com/magmaguy/betterstructures/config/treasures/premade/BarrelFoodTreasureConfig.java` - -**Step 1: Create the premade class** - -`TreasureConfig.java:13` auto-discovers everything in the `premade` package — no registration needed. - -```java -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); - } -} -``` - -**Step 2: Deploy + verify the YAML file is written** - -Run `./gradlew build` then deploy to a testbed ([reference_testbed_setup.md](../../../../../.claude/projects/C--Users-tiago-Documents-MineCraftProjects/memory/reference_testbed_setup.md)). Start the server once cleanly. - -Expected: `plugins/BetterStructures/treasures/treasure_barrel_food.yml` exists with: -- `items.common` / `items.rare` / `items.epic` populated -- `mean: 1.0` -- `standardDeviation: 0.7` - -If `mean` or `standardDeviation` come out as `4.0` / `3.0`, Task 2's refactor wasn't applied correctly. - -**Step 3: Commit** - -```bash -git add src/main/java/com/magmaguy/betterstructures/config/treasures/premade/BarrelFoodTreasureConfig.java -git commit -m "feat(lootify): ship treasure_barrel_food premade (mean=1, stddev=0.7)" -``` - ---- - -## Task 4: Add barrel fields to `GeneratorConfigFields` - -**Files:** -- Modify: `src/main/java/com/magmaguy/betterstructures/config/generators/GeneratorConfigFields.java` - -**Step 1: Add the fields** - -After the existing `treasureFilename` / `chestContents` declarations (around lines 41-43), add: - -```java -@Getter -@Setter -private String barrelTreasureFilename = "treasure_barrel_food"; -@Getter -private ChestContents barrelContents = null; -@Getter -@Setter -private boolean generateLootInBarrels = true; -``` - -**Step 2: Load them during `processConfigFields()`** - -After the existing chest-treasure load (around lines 80-86), append: - -```java -// 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", 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."); - } -} -``` - -**Step 3: Build check** - -Run: `./gradlew compileJava` -Expected: BUILD SUCCESSFUL. - -**Step 4: Commit** - -```bash -git add src/main/java/com/magmaguy/betterstructures/config/generators/GeneratorConfigFields.java -git commit -m "feat(lootify): generator-level barrel loot config (default on)" -``` - ---- - -## Task 5: Add barrel fields to `ModulesConfigFields` - -**Files:** -- Modify: `src/main/java/com/magmaguy/betterstructures/config/modules/ModulesConfigFields.java` - -**Step 1: Mirror Task 4's field additions** - -Same three fields, same defaults. Add Lombok getters (and setter for the string + boolean), wire through `processConfigFields()` the same way. - -**Step 2: Add a getter to make the ModulePasting flow read it** - -Confirm `ModulesConfigFields` exposes `getBarrelTreasureFilename()`, `getBarrelContents()`, and `isGenerateLootInBarrels()` — Lombok `@Getter` produces these. ModulePasting will read these in Task 8. - -**Step 3: Build check** - -Run: `./gradlew compileJava` -Expected: BUILD SUCCESSFUL. - -**Step 4: Commit** - -```bash -git add src/main/java/com/magmaguy/betterstructures/config/modules/ModulesConfigFields.java -git commit -m "feat(lootify): module-level barrel loot config (default on)" -``` - ---- - -## Task 6: Add per-schematic barrel treasure override to `SchematicConfigField` - -**Files:** -- Modify: `src/main/java/com/magmaguy/betterstructures/config/schematics/SchematicConfigField.java` - -**Step 1: Read the file** - -Find the `chestContents` field (line ~35) and the treasure-file load block (lines ~60-67). - -**Step 2: Add mirrored barrel fields** - -Right after `private ChestContents chestContents = null;`, add: - -```java -@Getter -private ChestContents barrelContents = null; -@Getter -@Setter -private String barrelTreasureFilename = null; -``` - -No `generateLootInBarrels` here — that decision is generator-/module-level. Per-schematic override is treasure-file only, mirroring the existing chest-side override. - -**Step 3: Wire the load path** - -Where `this.chestContents = generatorConfigFields.getChestContents();` lives (line ~60), also inherit: - -```java -this.barrelContents = generatorConfigFields.getBarrelContents(); -``` - -Inside the `treasureConfigFields != null` block (line ~67), parallel to the chest override, look for `barrelTreasureFilename` in the YAML. If present and it resolves to a valid `TreasureConfigFields`, replace `barrelContents` with `new ChestContents(thatConfig)`. - -**Step 4: Build check** - -Run: `./gradlew compileJava` -Expected: BUILD SUCCESSFUL. - -**Step 5: Commit** - -```bash -git add src/main/java/com/magmaguy/betterstructures/config/schematics/SchematicConfigField.java -git commit -m "feat(lootify): per-schematic barrelTreasureFilename override" -``` - ---- - -## Task 7: Inherent barrel detection + chest/barrel routing in the schematic pipeline - -**Files:** -- Modify: `src/main/java/com/magmaguy/betterstructures/schematics/SchematicContainer.java` -- Modify: `src/main/java/com/magmaguy/betterstructures/buildingfitter/FitAnything.java` - -**Step 1: Widen the chest-location collector in `SchematicContainer.java`** - -At line 74-77, change: - -```java -if (minecraftMaterial.equals(Material.CHEST) || - minecraftMaterial.equals(Material.TRAPPED_CHEST) || - minecraftMaterial.equals(Material.SHULKER_BOX)) { - chestLocations.add(new Vector(x, y, z)); -} -``` - -to: - -```java -if (minecraftMaterial.equals(Material.CHEST) || - minecraftMaterial.equals(Material.TRAPPED_CHEST) || - minecraftMaterial.equals(Material.SHULKER_BOX) || - minecraftMaterial.equals(Material.BARREL)) { - chestLocations.add(new Vector(x, y, z)); -} -``` - -(`chestLocations` now technically means "loot-bearing container locations." Don't rename — too much surface area for what should be a small change.) - -**Step 2: Route chest vs barrel in `FitAnything.fillChests()`** - -Replace the existing `fillChests()` (lines ~290-315) with: - -```java -private void fillChests() { - 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; - } - - boolean isBarrel = container.getBlock().getType() == Material.BARREL; - if (isBarrel && !barrelsEnabled) continue; - if (!isBarrel && !chestsEnabled) continue; - - ChestContents contents; - String treasureFilename; - if (isBarrel) { - contents = schematicContainer.getBarrelContents() != null - ? schematicContainer.getBarrelContents() - : gen.getBarrelContents(); - treasureFilename = schematicContainer.getSchematicConfigField().getBarrelTreasureFilename() != null - ? schematicContainer.getSchematicConfigField().getBarrelTreasureFilename() - : gen.getBarrelTreasureFilename(); - } else { - contents = schematicContainer.getChestContents() != null - ? schematicContainer.getChestContents() - : gen.getChestContents(); - treasureFilename = schematicContainer.getChestContents() != null - ? schematicContainer.getSchematicConfigField().getTreasureFile() - : gen.getTreasureFilename(); - } - - if (contents == null) continue; - contents.rollChestContents(container); - - ChestFillEvent chestFillEvent = new ChestFillEvent(container, treasureFilename); - Bukkit.getServer().getPluginManager().callEvent(chestFillEvent); - if (!chestFillEvent.isCancelled()) { - container.update(true); - } - } -} -``` - -**Step 3: Build check** - -Run: `./gradlew compileJava` -Expected: BUILD SUCCESSFUL. If `getBarrelContents()` / `isGenerateLootInBarrels()` don't resolve, double-check Task 4 declared those fields with Lombok `@Getter` (note: `boolean` getters are `isX()` not `getX()`). - -**Step 4: Commit** - -```bash -git add src/main/java/com/magmaguy/betterstructures/schematics/SchematicContainer.java src/main/java/com/magmaguy/betterstructures/buildingfitter/FitAnything.java -git commit -m "feat(lootify): inherent barrel detection + routing in schematic pipeline" -``` - ---- - -## Task 8: Inherent barrel detection + fill in `ModulePasting` - -**Files:** -- Modify: `src/main/java/com/magmaguy/betterstructures/modules/ModulePasting.java` - -**Step 1: Allow `BARREL` to bypass NBT-rich deferral** - -Around line 91: - -```java -if (m == Material.CHEST || m == Material.TRAPPED_CHEST) return false; -``` - -becomes: - -```java -if (m == Material.CHEST || m == Material.TRAPPED_CHEST || m == Material.BARREL) return false; -``` - -**Step 2: Track barrel placements during paste** - -Inside the same `pasteableList.forEach(...)` loop that already special-cases signs (around lines 190-262), add a check: when the block being placed is a barrel, record its world location in a new `List barrelsToFill` (declare it alongside `chestsToPlace` at the top of `batchPaste`). - -Locate where `pasteableList.add(new Pasteable(pasteLocation, blockData))` is called (around line 261). Just before that line, add: - -```java -if (blockData.getMaterial() == Material.BARREL) { - barrelsToFill.add(pasteLocation); -} -``` - -Do NOT skip the normal paste path — the barrel still needs to be placed via `pasteableList` so its block data (orientation, etc.) is set correctly. The list just remembers where it ended up for the post-paste fill step. - -**Step 3: Fill barrels after paste** - -Find the existing chest-fill loop (lines ~386-405). After that loop, add a parallel loop for barrels: - -```java -if (moduleGeneratorsConfigFields.isGenerateLootInBarrels()) { - String barrelTreasureFilename = moduleGeneratorsConfigFields.getBarrelTreasureFilename(); - TreasureConfigFields barrelTreasureFields = TreasureConfig.getConfigFields(barrelTreasureFilename); - if (barrelTreasureFields != null) { - ChestContents barrelContents = new ChestContents(barrelTreasureFields); - for (Location barrelLocation : barrelsToFill) { - Block block = barrelLocation.getBlock(); - if (block.getType() != Material.BARREL) continue; // got overwritten somehow - if (!(block.getState() instanceof Container container)) continue; - - barrelContents.rollChestContents(container); - ChestFillEvent chestFillEvent = new ChestFillEvent(container, barrelTreasureFilename); - Bukkit.getServer().getPluginManager().callEvent(chestFillEvent); - if (!chestFillEvent.isCancelled()) { - container.update(true); - } - } - } else if (!barrelsToFill.isEmpty()) { - Logger.warn("Module generator " + moduleGeneratorsConfigFields.getFilename() + " has barrels in its modules but barrelTreasureFilename '" + barrelTreasureFilename + "' did not resolve to a valid treasure config. Barrels will be empty."); - } -} -``` - -(Same `new ChestContents(treasureFields)` per-paste construction as the existing chest path on line 396 — not lazy-load-violating because it's a batch paste operation, not per-tick.) - -**Step 4: Build check** - -Run: `./gradlew compileJava` -Expected: BUILD SUCCESSFUL. - -**Step 5: Commit** - -```bash -git add src/main/java/com/magmaguy/betterstructures/modules/ModulePasting.java -git commit -m "feat(lootify): inherent barrel detection + fill in module pipeline" -``` - ---- - -## Task 9: Full build + testbed verification - -**Files:** none modified — verification only. - -**Step 1: Full plugin build** - -Per [feedback_full_builds.md](../../../../../.claude/projects/C--Users-tiago-Documents-MineCraftProjects/memory/feedback_full_builds.md), produce a usable jar. - -Run: `./gradlew clean build` -Expected: BUILD SUCCESSFUL, jar at `build/libs/BetterStructures-*.jar`. - -**Step 2: Deploy + start the server once** - -Use the testbed setup ([reference_testbed_setup.md](../../../../../.claude/projects/C--Users-tiago-Documents-MineCraftProjects/memory/reference_testbed_setup.md)). Start the server once so the new `treasure_barrel_food.yml` writes, then stop it and confirm: -- File exists at `plugins/BetterStructures/treasures/treasure_barrel_food.yml` -- Has three rarity tiers -- `mean: 1.0`, `standardDeviation: 0.7` - -**Step 3: Verify the schematic pathway** - -1. Place a barrel inside a test schematic used by a known generator. -2. Trigger a structure paste. -3. Open the placed barrel. - -Expected: 1-3 items, all food, drawn from the table in Task 1. Over ~10 paste runs you should see mostly common-tier items, occasional cooked food, rare epic items. - -**Step 4: Verify the module pathway** - -1. Place a barrel inside a module schematic (no sign needed). -2. Trigger module pasting. -3. Open the placed barrel. - -Expected: same behavior as Step 3. - -**Step 5: Verify the per-generator opt-out** - -1. In `plugins/BetterStructures/generators/.yml`, set `generateLootInBarrels: false`. -2. Reload (or restart). -3. Trigger a paste with a barrel. - -Expected: barrel is placed but empty. - -**Step 6: Verify the per-generator treasure override** - -1. Reset that generator's `generateLootInBarrels` to default (or remove the key). -2. Set `barrelTreasureFilename: treasure_overworld_surface`. -3. Trigger a paste with a barrel. - -Expected: barrel now contains overworld-chest loot (gear, etc.) — proving the override path works end-to-end. - -**Step 7: Verify the per-schematic treasure override** - -1. Restore `barrelTreasureFilename` to default in the generator. -2. In the schematic config, set `barrelTreasureFilename: treasure_overworld_surface`. -3. Trigger a paste. - -Expected: that schematic's barrels carry overworld loot, other schematics in the same generator still carry food. - -**Step 8: Verify `ChestFillEvent` fires for barrels** - -Optional sanity check: add a temporary `Logger.info` to a `ChestFillEvent` consumer (or write a tiny listener plugin), confirm the event fires with `container.getBlock().getType() == BARREL` and the right `getTreasureConfigFilename()`. - -**Step 9: Commit any tweaks** - -If verification surfaces real bugs, fix them with focused commits. Don't bundle into the earlier feature commits. - ---- - -## Notes for the executing engineer - -- **DRY:** `ChestContents` is reused, not duplicated. Resist the urge to make a `BarrelContents` class. -- **YAGNI:** No `BarrelFillEvent`. `ChestFillEvent` is already container-generic; consumers can branch on `getContainer().getBlock().getType()`. No `[barrel]` sign marker — barrels are inherent. -- **TDD:** This codebase doesn't have unit-test coverage for the chest/loot pipeline (verify: `grep -r "rollChestContents" src/test`). Verification is manual on the testbed — own that explicitly per [feedback_full_builds.md](../../../../../.claude/projects/C--Users-tiago-Documents-MineCraftProjects/memory/feedback_full_builds.md). If you want one unit test, the cheapest valuable one: instantiate `ChestContents` with a synthetic `TreasureConfigFields`, hand it a mock `Container`, assert `rollChestContents` populates the inventory with 1-3 items when `mean=1`/`stddev=0.7`. Don't gate this PR on it. -- **Lazy loading:** Per [feedback_lazy_loading.md](../../../../../.claude/projects/C--Users-tiago-Documents-MineCraftProjects/memory/feedback_lazy_loading.md), the cached path uses one `ChestContents` per generator (built at config load in Task 4). The modules path constructs per-batch (Task 8 step 3) — that matches the existing chest behavior at `ModulePasting:396`, so it's not a regression. -- **Magmacore:** This plan touches only BetterStructures internals. No Magmacore changes, so [reference_magmacore_publish_workflow.md](../../../../../.claude/projects/C--Users-tiago-Documents-MineCraftProjects/memory/reference_magmacore_publish_workflow.md) does not apply. -- **Commits:** One per task. Branch name suggestion: `feat/barrel-loot`. From ac820554915694a55aae496ba18d3e0c6154d064 Mon Sep 17 00:00:00 2001 From: MagmaGuy Date: Wed, 3 Jun 2026 15:23:17 +0100 Subject: [PATCH 18/20] Add docs/ to .gitignore Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 805aaf9..c6bc6f3 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ Thumbs.db codex-build.log .claude/ Get +# Local design docs and plan notes - not tracked +docs/ From 3b60757929865fce5528eacb9b940259e9c66d0d Mon Sep 17 00:00:00 2001 From: MagmaGuy Date: Sun, 21 Jun 2026 13:00:30 +0100 Subject: [PATCH 19/20] BetterStructures 2.5.0: in-game plugin updates, setup overhaul, world-tracking fix - [New] Check for and one-click download BetterStructures updates in-game from the setup menu (the server checks on boot and refreshes about hourly; restart to apply) - [New] Automatic Updates toggle (off by default) that downloads updates on startup, also exposed as autoDownloadPluginUpdates in config.yml - [New] Recommended Plugins view in the setup menu (plus a recommendedplugins command) and a /nightbreak plugins catalog to browse MagmaGuy's other plugins - [Tweak] Reworked the setup and first-time setup menus with clearer, clickable actions, including a prompt to renew your account token when it expires - [Tweak] Renamed bulk commands (downloadallcontent / updateallcontent); downloadall now checks for plugin updates alongside content - [Fix] Reworked valid-world tracking to key worlds by name and prune them when worlds unload --- .gitignore | 10 ++ build.gradle | 17 ++- .../betterstructures/BetterStructures.java | 43 ++++++- .../commands/BetterStructuresCommand.java | 5 +- .../commands/DownloadAllContentCommand.java | 25 ++++- .../commands/UpdateContentCommand.java | 4 +- .../config/DefaultConfig.java | 6 +- .../config/ValidWorldsConfig.java | 106 ++++++++++++++---- .../treasures/TreasureConfigFields.java | 11 +- .../BetterStructuresFirstTimeSetupMenu.java | 36 +++--- .../menus/BetterStructuresSetupMenu.java | 58 ++-------- .../util/DefaultChestContents.java | 2 + src/main/resources/plugin.yml | 4 +- 13 files changed, 214 insertions(+), 113 deletions(-) diff --git a/.gitignore b/.gitignore index c6bc6f3..c28e484 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build/ # IDE .idea/ +.run/ *.iml # Test server @@ -19,3 +20,12 @@ codex-build.log 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 376a006..7e812c0 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { } group = 'com.magmaguy' -version = '2.4.0' +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 3b3c37c..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() { @@ -58,8 +74,13 @@ public void onEnable() { 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; } }, @@ -151,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/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/treasures/TreasureConfigFields.java b/src/main/java/com/magmaguy/betterstructures/config/treasures/TreasureConfigFields.java index 34408ff..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(); @@ -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/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 c8c3772..7ad7770 100644 --- a/src/main/java/com/magmaguy/betterstructures/menus/BetterStructuresSetupMenu.java +++ b/src/main/java/com/magmaguy/betterstructures/menus/BetterStructuresSetupMenu.java @@ -1,6 +1,7 @@ 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; @@ -8,6 +9,7 @@ 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; @@ -35,57 +37,12 @@ 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"); - new SetupMenuBuilder((JavaPlugin) MetadataHandler.PLUGIN, player) + SetupMenuBuilder builder = new SetupMenuBuilder((JavaPlugin) MetadataHandler.PLUGIN, player) .title("Setup menu") - .titleIconPrefix(null) .infoButton(infoButton) .packages(bsPackages) .appendPackage(new DownloadAllContentPackage<>(() -> new ArrayList<>(BSPackage.getBsPackages().values()), @@ -95,7 +52,8 @@ public void onClick(Player p) { .addFilter(Material.GRASS_BLOCK, "Structure Packs", (Predicate) BetterStructuresSetupMenu::filterStructures) .addFilter(Material.DEEPSLATE_BRICKS, "Module Packs", - (Predicate) BetterStructuresSetupMenu::filterModules) + (Predicate) BetterStructuresSetupMenu::filterModules); + NightbreakSetupControls.prependStandardControls(builder, (JavaPlugin) MetadataHandler.PLUGIN, BetterStructures.NIGHTBREAK_PLUGIN_SPEC) .open(); } diff --git a/src/main/java/com/magmaguy/betterstructures/util/DefaultChestContents.java b/src/main/java/com/magmaguy/betterstructures/util/DefaultChestContents.java index 0018df2..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; @@ -886,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/resources/plugin.yml b/src/main/resources/plugin.yml index e5c7026..0d23006 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,5 +1,5 @@ name: BetterStructures -version: '2.4.0' +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 From c7669396961fec8e010806296999fbc9fcf80c54 Mon Sep 17 00:00:00 2001 From: MaXoNeRYT <123590496+MaXoNeRYT@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:53:11 +0300 Subject: [PATCH 20/20] Add CraftEngine & Other Plugins CustomBlock Support --- .../betterstructures/worldedit/Schematic.java | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) 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