diff --git a/.github/workflows/modrinth-publish.yml b/.github/workflows/modrinth-publish.yml new file mode 100644 index 0000000..72b93e6 --- /dev/null +++ b/.github/workflows/modrinth-publish.yml @@ -0,0 +1,43 @@ +name: Publish + +on: + release: + types: [published] + +jobs: + publish: + name: Publish + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: 21 + distribution: adopt + cache: maven + + # This step will take the version tag from the release and replace it in `pom.xml` before building. + #- name: Set version from release tag + # run: mvn -B versions:set -DnewVersion=${{ github.event.release.tag_name }} -DgenerateBackupPoms=false + + - name: Build and package with Maven + run: mvn -B clean package -DskipTests -DgenerateBackupPoms=false -Pmaster --file pom.xml + + - name: Upload to Modrinth + uses: cloudnode-pro/modrinth-publish@v2 + with: + token: ${{ secrets.MODRINTH_TOKEN }} + project: F4I5miJX + name: ${{ github.event.release.name }} + version: ${{ github.event.release.tag_name }} + changelog: ${{ github.event.release.body }} + loaders: |- + paper + spigot + game-versions: |- + 1.21.4 + 1.21.5 + files: /home/runner/work/Boxed/Boxed/target/Boxed-${{ github.event.release.tag_name }}.jar diff --git a/src/main/java/.gitignore b/src/main/java/.gitignore new file mode 100644 index 0000000..9bb88d3 --- /dev/null +++ b/src/main/java/.gitignore @@ -0,0 +1 @@ +/.DS_Store diff --git a/src/main/java/world/bentobox/boxed/commands/AdminPlaceStructureCommand.java b/src/main/java/world/bentobox/boxed/commands/AdminPlaceStructureCommand.java index 82415d4..4e4d7b2 100644 --- a/src/main/java/world/bentobox/boxed/commands/AdminPlaceStructureCommand.java +++ b/src/main/java/world/bentobox/boxed/commands/AdminPlaceStructureCommand.java @@ -4,19 +4,27 @@ import java.io.IOException; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Optional; import java.util.Random; +import java.util.Stack; import org.bukkit.Bukkit; import org.bukkit.Location; +import org.bukkit.Material; import org.bukkit.NamespacedKey; +import org.bukkit.block.BlockState; +import org.bukkit.block.data.BlockData; import org.bukkit.block.structure.Mirror; import org.bukkit.block.structure.StructureRotation; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.YamlConfiguration; import org.bukkit.structure.Structure; +import org.bukkit.util.BlockTransformer; +import org.bukkit.util.Vector; import com.google.common.base.Enums; @@ -52,6 +60,8 @@ public class AdminPlaceStructureCommand extends CompositeCommand { private Mirror mirror = Mirror.NONE; private boolean noMobs; + private final Stack placedStructures = new Stack<>(); + public AdminPlaceStructureCommand(CompositeCommand parent) { super(parent, "place"); } @@ -68,15 +78,20 @@ public void setup() { @Override public boolean canExecute(User user, String label, List args) { + if (args.size() == 1 && args.get(0).equalsIgnoreCase("undo")) { + return true; // Allow "undo" command without additional checks + } + // Initialize sr = StructureRotation.NONE; mirror = Mirror.NONE; // Check world - if (!((Boxed)getAddon()).inWorld(getWorld())) { + if (!((Boxed) getAddon()).inWorld(getWorld())) { user.sendMessage("boxed.commands.boxadmin.place.wrong-world"); return false; } + /* * Acceptable syntax with number of args: * 1. place @@ -85,21 +100,23 @@ public boolean canExecute(User user, String label, List args) { * 6. place ~ ~ ~ ROTATION MIRROR * 7. place ~ ~ ~ ROTATION MIRROR NO_MOBS */ - // Format is place ~ ~ ~ or coords if (args.isEmpty() || args.size() == 2 || args.size() == 3 || args.size() > 6) { this.showHelp(this, user); return false; } + // First arg must always be the structure name List options = Bukkit.getStructureManager().getStructures().keySet().stream().map(NamespacedKey::getKey).toList(); if (!options.contains(args.get(0).toLowerCase(Locale.ENGLISH))) { user.sendMessage("boxed.commands.boxadmin.place.unknown-structure"); return false; } + // If that is all we have, we're done if (args.size() == 1) { return true; } + // Next come the coordinates - there must be at least 3 of them if ((!args.get(1).equals("~") && !Util.isInteger(args.get(1), true)) || (!args.get(2).equals("~") && !Util.isInteger(args.get(2), true)) @@ -107,27 +124,32 @@ public boolean canExecute(User user, String label, List args) { user.sendMessage("boxed.commands.boxadmin.place.use-integers"); return false; } + // If that is all we have, we're done if (args.size() == 4) { return true; } - // But there is more! + + // Handle rotation sr = Enums.getIfPresent(StructureRotation.class, args.get(4).toUpperCase(Locale.ENGLISH)).orNull(); if (sr == null) { user.sendMessage("boxed.commands.boxadmin.place.unknown-rotation"); Arrays.stream(StructureRotation.values()).map(StructureRotation::name).forEach(user::sendRawMessage); return false; } + if (args.size() == 5) { return true; } - // But there is more! + + // Handle mirror mirror = Enums.getIfPresent(Mirror.class, args.get(5).toUpperCase(Locale.ENGLISH)).orNull(); if (mirror == null) { user.sendMessage("boxed.commands.boxadmin.place.unknown-mirror"); Arrays.stream(Mirror.values()).map(Mirror::name).forEach(user::sendRawMessage); return false; } + if (args.size() == 7) { if (args.get(6).toUpperCase(Locale.ENGLISH).equals("NO_MOBS")) { noMobs = true; @@ -136,20 +158,40 @@ public boolean canExecute(User user, String label, List args) { return false; } } + // Syntax is okay return true; } @Override public boolean execute(User user, String label, List args) { + if (args.size() == 1 && args.get(0).equalsIgnoreCase("undo")) { + return undoLastPlacement(user); + } + NamespacedKey tag = NamespacedKey.fromString(args.get(0).toLowerCase(Locale.ENGLISH)); Structure s = Bukkit.getStructureManager().loadStructure(tag); - int x = args.size() == 1 || args.get(1).equals("~") ? user.getLocation().getBlockX() : Integer.parseInt(args.get(1).trim()); - int y = args.size() == 1 || args.get(2).equals("~") ? user.getLocation().getBlockY() : Integer.parseInt(args.get(2).trim()); - int z = args.size() == 1 || args.get(3).equals("~") ? user.getLocation().getBlockZ() : Integer.parseInt(args.get(3).trim()); + int x = args.size() == 1 || args.get(1).equals("~") ? user.getLocation().getBlockX() + : Integer.parseInt(args.get(1).trim()); + int y = args.size() == 1 || args.get(2).equals("~") ? user.getLocation().getBlockY() + : Integer.parseInt(args.get(2).trim()); + int z = args.size() == 1 || args.get(3).equals("~") ? user.getLocation().getBlockZ() + : Integer.parseInt(args.get(3).trim()); Location spot = new Location(user.getWorld(), x, y, z); - s.place(spot, true, sr, mirror, PALETTE, INTEGRITY, new Random()); - NewAreaListener.removeJigsaw(new StructureRecord(tag.getKey(), tag.getKey(), spot, sr, mirror, noMobs)); + Map removedBlocks = new HashMap<>(); + BlockTransformer store = (region, xx, yy, zz, current, state) -> { + // Store the state + removedBlocks.put(new Vector(xx, yy, zz), region.getBlockData(xx, yy, zz)); + return state.getOriginal(); + }; + + s.place(spot, true, sr, mirror, PALETTE, INTEGRITY, new Random(), Collections.singleton(store), // Transformer to store blocks + Collections.emptyList() // No entity transformers + ); + NewAreaListener + .removeJigsaw(new StructureRecord(tag.getKey(), tag.getKey(), spot, sr, mirror, noMobs, removedBlocks)); + placedStructures.push(new StructureRecord(tag.getKey(), tag.getKey(), spot, sr, mirror, noMobs, removedBlocks)); // Track the placement + boolean result = saveStructure(spot, tag, user, sr, mirror); if (result) { user.sendMessage("boxed.commands.boxadmin.place.saved"); @@ -184,9 +226,74 @@ private boolean saveStructure(Location spot, NamespacedKey tag, User user, Struc } + private boolean undoLastPlacement(User user) { + if (placedStructures.isEmpty()) { + user.sendMessage("boxed.commands.boxadmin.place.no-undo"); + return false; + } + + StructureRecord lastRecord = placedStructures.pop(); + NamespacedKey tag = NamespacedKey.fromString(lastRecord.name()); + Structure s = Bukkit.getStructureManager().loadStructure(tag); + + if (s == null) { + user.sendMessage("boxed.commands.boxadmin.place.undo-failed"); + return false; + } + + BlockTransformer erase = (region, x, y, z, current, state) -> { + Vector v = new Vector(x, y, z); + if (lastRecord.removedBlocks().containsKey(v)) { + return lastRecord.removedBlocks().get(v).createBlockState(); + } + BlockState airState = Material.AIR.createBlockData().createBlockState(); + return airState; + }; + + s.place( + lastRecord.location(), + false, // Don't respawn entities + lastRecord.rot(), lastRecord.mirror(), + PALETTE, + 1.0f, // Integrity = 1 means "place everything" + new Random(), + Collections.singleton(erase), // Transformer to erase blocks + Collections.emptyList() // No entity transformers + ); + lastRecord.removedBlocks().clear(); + removeStructure(lastRecord.location(), tag, user); // Remove from config + + user.sendMessage("boxed.commands.boxadmin.place.undo-success"); + return true; + } + + private boolean removeStructure(Location spot, NamespacedKey tag, User user) { + return getAddon().getIslands().getIslandAt(spot).map(i -> { + int xx = spot.getBlockX() - i.getCenter().getBlockX(); + int zz = spot.getBlockZ() - i.getCenter().getBlockZ(); + File structures = new File(getAddon().getDataFolder(), STRUCTURE_FILE); + YamlConfiguration config = new YamlConfiguration(); + try { + config.load(structures); + String key = spot.getWorld().getEnvironment().name().toLowerCase(Locale.ENGLISH) + "." + xx + "," + spot.getBlockY() + "," + zz; + if (config.contains(key)) { + config.set(key, null); // Remove the entry + config.save(structures); + return true; + } + } catch (IOException | InvalidConfigurationException e) { + e.printStackTrace(); + } + return false; + }).orElse(false); + } + @Override public Optional> tabComplete(User user, String alias, List args) { + if (args.size() == 1) { + return Optional.of(Util.tabLimit(Arrays.asList("undo"), args.get(0))); + } String lastArg = !args.isEmpty() ? args.get(args.size() - 1) : ""; if (args.size() == 2) { return Optional.of(Util.tabLimit(Bukkit.getStructureManager().getStructures().keySet().stream().map(NamespacedKey::getKey).toList(), lastArg)); @@ -205,4 +312,7 @@ public Optional> tabComplete(User user, String alias, List } return Optional.of(Collections.emptyList()); } + + + } \ No newline at end of file diff --git a/src/main/java/world/bentobox/boxed/listeners/NewAreaListener.java b/src/main/java/world/bentobox/boxed/listeners/NewAreaListener.java index 2ac8399..fd95d38 100644 --- a/src/main/java/world/bentobox/boxed/listeners/NewAreaListener.java +++ b/src/main/java/world/bentobox/boxed/listeners/NewAreaListener.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; @@ -388,9 +389,11 @@ private void place(ConfigurationSection section, Location center, Environment en Location location = new Location(world, x, y, z); // Structure will be placed at location readyToBuild.computeIfAbsent(new Pair<>(x >> 4, z >> 4), k -> new ArrayList<>()) - .add(new StructureRecord(name, "minecraft:" + name, location, rotation, mirror, noMobs)); + .add(new StructureRecord(name, "minecraft:" + name, location, rotation, mirror, noMobs, + Collections.emptyMap())); this.itemsToBuild - .add(new StructureRecord(name, "minecraft:" + name, location, rotation, mirror, noMobs)); + .add(new StructureRecord(name, "minecraft:" + name, location, rotation, mirror, noMobs, + Collections.emptyMap())); } else { addon.logError("Structure file syntax error: " + vector + ": " + Arrays.toString(coords)); } diff --git a/src/main/java/world/bentobox/boxed/objects/ToBePlacedStructures.java b/src/main/java/world/bentobox/boxed/objects/ToBePlacedStructures.java index 5d8078c..ef21b0e 100644 --- a/src/main/java/world/bentobox/boxed/objects/ToBePlacedStructures.java +++ b/src/main/java/world/bentobox/boxed/objects/ToBePlacedStructures.java @@ -5,8 +5,10 @@ import java.util.Map; import org.bukkit.Location; +import org.bukkit.block.data.BlockData; import org.bukkit.block.structure.Mirror; import org.bukkit.block.structure.StructureRotation; +import org.bukkit.util.Vector; import com.google.gson.annotations.Expose; @@ -36,7 +38,8 @@ public class ToBePlacedStructures implements DataObject { * @param noMobs - if false, mobs not pasted */ public record StructureRecord(@Expose String name, @Expose String structure, @Expose Location location, - @Expose StructureRotation rot, @Expose Mirror mirror, @Expose Boolean noMobs) { + @Expose StructureRotation rot, @Expose Mirror mirror, @Expose Boolean noMobs, + Map removedBlocks) { } @Expose diff --git a/src/main/resources/locales/en-US.yml b/src/main/resources/locales/en-US.yml index dd558fe..437a1f5 100755 --- a/src/main/resources/locales/en-US.yml +++ b/src/main/resources/locales/en-US.yml @@ -898,6 +898,7 @@ boxed: saved: '&a Placed and saved to structures.yml' failed: '&c Could not be saved to structures.yml. Check console for error' unknown: '&c Unknown parameter: [label]' + undo-success: '&a Undone. Some changes cannot be undone.'' island: go: parameters: '[home number]'