Skip to content

Commit ef4ab36

Browse files
authored
Merge pull request #109 from BentoBoxWorld/108_Add_undo_to_structure_placing
Add undo command for undoing placement of structures by admins
2 parents 8adbce4 + 859edc3 commit ef4ab36

File tree

6 files changed

+173
-12
lines changed

6 files changed

+173
-12
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Publish
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
jobs:
8+
publish:
9+
name: Publish
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout repository
13+
uses: actions/checkout@v4
14+
15+
- name: Set up JDK 21
16+
uses: actions/setup-java@v4
17+
with:
18+
java-version: 21
19+
distribution: adopt
20+
cache: maven
21+
22+
# This step will take the version tag from the release and replace it in `pom.xml` before building.
23+
#- name: Set version from release tag
24+
# run: mvn -B versions:set -DnewVersion=${{ github.event.release.tag_name }} -DgenerateBackupPoms=false
25+
26+
- name: Build and package with Maven
27+
run: mvn -B clean package -DskipTests -DgenerateBackupPoms=false -Pmaster --file pom.xml
28+
29+
- name: Upload to Modrinth
30+
uses: cloudnode-pro/modrinth-publish@v2
31+
with:
32+
token: ${{ secrets.MODRINTH_TOKEN }}
33+
project: F4I5miJX
34+
name: ${{ github.event.release.name }}
35+
version: ${{ github.event.release.tag_name }}
36+
changelog: ${{ github.event.release.body }}
37+
loaders: |-
38+
paper
39+
spigot
40+
game-versions: |-
41+
1.21.4
42+
1.21.5
43+
files: /home/runner/work/Boxed/Boxed/target/Boxed-${{ github.event.release.tag_name }}.jar

src/main/java/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/.DS_Store

src/main/java/world/bentobox/boxed/commands/AdminPlaceStructureCommand.java

Lines changed: 119 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,27 @@
44
import java.io.IOException;
55
import java.util.Arrays;
66
import java.util.Collections;
7+
import java.util.HashMap;
78
import java.util.List;
89
import java.util.Locale;
10+
import java.util.Map;
911
import java.util.Optional;
1012
import java.util.Random;
13+
import java.util.Stack;
1114

1215
import org.bukkit.Bukkit;
1316
import org.bukkit.Location;
17+
import org.bukkit.Material;
1418
import org.bukkit.NamespacedKey;
19+
import org.bukkit.block.BlockState;
20+
import org.bukkit.block.data.BlockData;
1521
import org.bukkit.block.structure.Mirror;
1622
import org.bukkit.block.structure.StructureRotation;
1723
import org.bukkit.configuration.InvalidConfigurationException;
1824
import org.bukkit.configuration.file.YamlConfiguration;
1925
import org.bukkit.structure.Structure;
26+
import org.bukkit.util.BlockTransformer;
27+
import org.bukkit.util.Vector;
2028

2129
import com.google.common.base.Enums;
2230

@@ -52,6 +60,8 @@ public class AdminPlaceStructureCommand extends CompositeCommand {
5260
private Mirror mirror = Mirror.NONE;
5361
private boolean noMobs;
5462

63+
private final Stack<StructureRecord> placedStructures = new Stack<>();
64+
5565
public AdminPlaceStructureCommand(CompositeCommand parent) {
5666
super(parent, "place");
5767
}
@@ -68,15 +78,20 @@ public void setup() {
6878

6979
@Override
7080
public boolean canExecute(User user, String label, List<String> args) {
81+
if (args.size() == 1 && args.get(0).equalsIgnoreCase("undo")) {
82+
return true; // Allow "undo" command without additional checks
83+
}
84+
7185
// Initialize
7286
sr = StructureRotation.NONE;
7387
mirror = Mirror.NONE;
7488

7589
// Check world
76-
if (!((Boxed)getAddon()).inWorld(getWorld())) {
90+
if (!((Boxed) getAddon()).inWorld(getWorld())) {
7791
user.sendMessage("boxed.commands.boxadmin.place.wrong-world");
7892
return false;
7993
}
94+
8095
/*
8196
* Acceptable syntax with number of args:
8297
* 1. place <structure>
@@ -85,49 +100,56 @@ public boolean canExecute(User user, String label, List<String> args) {
85100
* 6. place <structure> ~ ~ ~ ROTATION MIRROR
86101
* 7. place <structure> ~ ~ ~ ROTATION MIRROR NO_MOBS
87102
*/
88-
// Format is place <structure> ~ ~ ~ or coords
89103
if (args.isEmpty() || args.size() == 2 || args.size() == 3 || args.size() > 6) {
90104
this.showHelp(this, user);
91105
return false;
92106
}
107+
93108
// First arg must always be the structure name
94109
List<String> options = Bukkit.getStructureManager().getStructures().keySet().stream().map(NamespacedKey::getKey).toList();
95110
if (!options.contains(args.get(0).toLowerCase(Locale.ENGLISH))) {
96111
user.sendMessage("boxed.commands.boxadmin.place.unknown-structure");
97112
return false;
98113
}
114+
99115
// If that is all we have, we're done
100116
if (args.size() == 1) {
101117
return true;
102118
}
119+
103120
// Next come the coordinates - there must be at least 3 of them
104121
if ((!args.get(1).equals("~") && !Util.isInteger(args.get(1), true))
105122
|| (!args.get(2).equals("~") && !Util.isInteger(args.get(2), true))
106123
|| (!args.get(3).equals("~") && !Util.isInteger(args.get(3), true))) {
107124
user.sendMessage("boxed.commands.boxadmin.place.use-integers");
108125
return false;
109126
}
127+
110128
// If that is all we have, we're done
111129
if (args.size() == 4) {
112130
return true;
113131
}
114-
// But there is more!
132+
133+
// Handle rotation
115134
sr = Enums.getIfPresent(StructureRotation.class, args.get(4).toUpperCase(Locale.ENGLISH)).orNull();
116135
if (sr == null) {
117136
user.sendMessage("boxed.commands.boxadmin.place.unknown-rotation");
118137
Arrays.stream(StructureRotation.values()).map(StructureRotation::name).forEach(user::sendRawMessage);
119138
return false;
120139
}
140+
121141
if (args.size() == 5) {
122142
return true;
123143
}
124-
// But there is more!
144+
145+
// Handle mirror
125146
mirror = Enums.getIfPresent(Mirror.class, args.get(5).toUpperCase(Locale.ENGLISH)).orNull();
126147
if (mirror == null) {
127148
user.sendMessage("boxed.commands.boxadmin.place.unknown-mirror");
128149
Arrays.stream(Mirror.values()).map(Mirror::name).forEach(user::sendRawMessage);
129150
return false;
130151
}
152+
131153
if (args.size() == 7) {
132154
if (args.get(6).toUpperCase(Locale.ENGLISH).equals("NO_MOBS")) {
133155
noMobs = true;
@@ -136,20 +158,40 @@ public boolean canExecute(User user, String label, List<String> args) {
136158
return false;
137159
}
138160
}
161+
139162
// Syntax is okay
140163
return true;
141164
}
142165

143166
@Override
144167
public boolean execute(User user, String label, List<String> args) {
168+
if (args.size() == 1 && args.get(0).equalsIgnoreCase("undo")) {
169+
return undoLastPlacement(user);
170+
}
171+
145172
NamespacedKey tag = NamespacedKey.fromString(args.get(0).toLowerCase(Locale.ENGLISH));
146173
Structure s = Bukkit.getStructureManager().loadStructure(tag);
147-
int x = args.size() == 1 || args.get(1).equals("~") ? user.getLocation().getBlockX() : Integer.parseInt(args.get(1).trim());
148-
int y = args.size() == 1 || args.get(2).equals("~") ? user.getLocation().getBlockY() : Integer.parseInt(args.get(2).trim());
149-
int z = args.size() == 1 || args.get(3).equals("~") ? user.getLocation().getBlockZ() : Integer.parseInt(args.get(3).trim());
174+
int x = args.size() == 1 || args.get(1).equals("~") ? user.getLocation().getBlockX()
175+
: Integer.parseInt(args.get(1).trim());
176+
int y = args.size() == 1 || args.get(2).equals("~") ? user.getLocation().getBlockY()
177+
: Integer.parseInt(args.get(2).trim());
178+
int z = args.size() == 1 || args.get(3).equals("~") ? user.getLocation().getBlockZ()
179+
: Integer.parseInt(args.get(3).trim());
150180
Location spot = new Location(user.getWorld(), x, y, z);
151-
s.place(spot, true, sr, mirror, PALETTE, INTEGRITY, new Random());
152-
NewAreaListener.removeJigsaw(new StructureRecord(tag.getKey(), tag.getKey(), spot, sr, mirror, noMobs));
181+
Map<Vector, BlockData> removedBlocks = new HashMap<>();
182+
BlockTransformer store = (region, xx, yy, zz, current, state) -> {
183+
// Store the state
184+
removedBlocks.put(new Vector(xx, yy, zz), region.getBlockData(xx, yy, zz));
185+
return state.getOriginal();
186+
};
187+
188+
s.place(spot, true, sr, mirror, PALETTE, INTEGRITY, new Random(), Collections.singleton(store), // Transformer to store blocks
189+
Collections.emptyList() // No entity transformers
190+
);
191+
NewAreaListener
192+
.removeJigsaw(new StructureRecord(tag.getKey(), tag.getKey(), spot, sr, mirror, noMobs, removedBlocks));
193+
placedStructures.push(new StructureRecord(tag.getKey(), tag.getKey(), spot, sr, mirror, noMobs, removedBlocks)); // Track the placement
194+
153195
boolean result = saveStructure(spot, tag, user, sr, mirror);
154196
if (result) {
155197
user.sendMessage("boxed.commands.boxadmin.place.saved");
@@ -184,9 +226,74 @@ private boolean saveStructure(Location spot, NamespacedKey tag, User user, Struc
184226

185227
}
186228

229+
private boolean undoLastPlacement(User user) {
230+
if (placedStructures.isEmpty()) {
231+
user.sendMessage("boxed.commands.boxadmin.place.no-undo");
232+
return false;
233+
}
234+
235+
StructureRecord lastRecord = placedStructures.pop();
236+
NamespacedKey tag = NamespacedKey.fromString(lastRecord.name());
237+
Structure s = Bukkit.getStructureManager().loadStructure(tag);
238+
239+
if (s == null) {
240+
user.sendMessage("boxed.commands.boxadmin.place.undo-failed");
241+
return false;
242+
}
243+
244+
BlockTransformer erase = (region, x, y, z, current, state) -> {
245+
Vector v = new Vector(x, y, z);
246+
if (lastRecord.removedBlocks().containsKey(v)) {
247+
return lastRecord.removedBlocks().get(v).createBlockState();
248+
}
249+
BlockState airState = Material.AIR.createBlockData().createBlockState();
250+
return airState;
251+
};
252+
253+
s.place(
254+
lastRecord.location(),
255+
false, // Don't respawn entities
256+
lastRecord.rot(), lastRecord.mirror(),
257+
PALETTE,
258+
1.0f, // Integrity = 1 means "place everything"
259+
new Random(),
260+
Collections.singleton(erase), // Transformer to erase blocks
261+
Collections.emptyList() // No entity transformers
262+
);
263+
lastRecord.removedBlocks().clear();
264+
removeStructure(lastRecord.location(), tag, user); // Remove from config
265+
266+
user.sendMessage("boxed.commands.boxadmin.place.undo-success");
267+
return true;
268+
}
269+
270+
private boolean removeStructure(Location spot, NamespacedKey tag, User user) {
271+
return getAddon().getIslands().getIslandAt(spot).map(i -> {
272+
int xx = spot.getBlockX() - i.getCenter().getBlockX();
273+
int zz = spot.getBlockZ() - i.getCenter().getBlockZ();
274+
File structures = new File(getAddon().getDataFolder(), STRUCTURE_FILE);
275+
YamlConfiguration config = new YamlConfiguration();
276+
try {
277+
config.load(structures);
278+
String key = spot.getWorld().getEnvironment().name().toLowerCase(Locale.ENGLISH) + "." + xx + "," + spot.getBlockY() + "," + zz;
279+
if (config.contains(key)) {
280+
config.set(key, null); // Remove the entry
281+
config.save(structures);
282+
return true;
283+
}
284+
} catch (IOException | InvalidConfigurationException e) {
285+
e.printStackTrace();
286+
}
287+
return false;
288+
}).orElse(false);
289+
}
290+
187291
@Override
188292
public Optional<List<String>> tabComplete(User user, String alias, List<String> args)
189293
{
294+
if (args.size() == 1) {
295+
return Optional.of(Util.tabLimit(Arrays.asList("undo"), args.get(0)));
296+
}
190297
String lastArg = !args.isEmpty() ? args.get(args.size() - 1) : "";
191298
if (args.size() == 2) {
192299
return Optional.of(Util.tabLimit(Bukkit.getStructureManager().getStructures().keySet().stream().map(NamespacedKey::getKey).toList(), lastArg));
@@ -205,4 +312,7 @@ public Optional<List<String>> tabComplete(User user, String alias, List<String>
205312
}
206313
return Optional.of(Collections.emptyList());
207314
}
315+
316+
317+
208318
}

src/main/java/world/bentobox/boxed/listeners/NewAreaListener.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import java.io.IOException;
55
import java.util.ArrayList;
66
import java.util.Arrays;
7+
import java.util.Collections;
78
import java.util.HashMap;
89
import java.util.Iterator;
910
import java.util.LinkedList;
@@ -388,9 +389,11 @@ private void place(ConfigurationSection section, Location center, Environment en
388389
Location location = new Location(world, x, y, z);
389390
// Structure will be placed at location
390391
readyToBuild.computeIfAbsent(new Pair<>(x >> 4, z >> 4), k -> new ArrayList<>())
391-
.add(new StructureRecord(name, "minecraft:" + name, location, rotation, mirror, noMobs));
392+
.add(new StructureRecord(name, "minecraft:" + name, location, rotation, mirror, noMobs,
393+
Collections.emptyMap()));
392394
this.itemsToBuild
393-
.add(new StructureRecord(name, "minecraft:" + name, location, rotation, mirror, noMobs));
395+
.add(new StructureRecord(name, "minecraft:" + name, location, rotation, mirror, noMobs,
396+
Collections.emptyMap()));
394397
} else {
395398
addon.logError("Structure file syntax error: " + vector + ": " + Arrays.toString(coords));
396399
}

src/main/java/world/bentobox/boxed/objects/ToBePlacedStructures.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
import java.util.Map;
66

77
import org.bukkit.Location;
8+
import org.bukkit.block.data.BlockData;
89
import org.bukkit.block.structure.Mirror;
910
import org.bukkit.block.structure.StructureRotation;
11+
import org.bukkit.util.Vector;
1012

1113
import com.google.gson.annotations.Expose;
1214

@@ -36,7 +38,8 @@ public class ToBePlacedStructures implements DataObject {
3638
* @param noMobs - if false, mobs not pasted
3739
*/
3840
public record StructureRecord(@Expose String name, @Expose String structure, @Expose Location location,
39-
@Expose StructureRotation rot, @Expose Mirror mirror, @Expose Boolean noMobs) {
41+
@Expose StructureRotation rot, @Expose Mirror mirror, @Expose Boolean noMobs,
42+
Map<Vector, BlockData> removedBlocks) {
4043
}
4144

4245
@Expose

src/main/resources/locales/en-US.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,7 @@ boxed:
898898
saved: '&a Placed and saved to structures.yml'
899899
failed: '&c Could not be saved to structures.yml. Check console for error'
900900
unknown: '&c Unknown parameter: [label]'
901+
undo-success: '&a Undone. Some changes cannot be undone.''
901902
island:
902903
go:
903904
parameters: '[home number]'

0 commit comments

Comments
 (0)