Skip to content

Commit 69ab331

Browse files
authored
Merge pull request #3213 from Multiverse/feat/keep-files
Add option to keep files during world regen
2 parents dcb8a35 + e625e47 commit 69ab331

File tree

8 files changed

+159
-34
lines changed

8 files changed

+159
-34
lines changed

src/main/java/org/mvplugins/multiverse/core/commands/DeleteCommand.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.mvplugins.multiverse.core.world.MultiverseWorld;
3030
import org.mvplugins.multiverse.core.world.WorldManager;
3131
import org.mvplugins.multiverse.core.world.helpers.PlayerWorldTeleporter;
32+
import org.mvplugins.multiverse.core.world.options.DeleteWorldOptions;
3233

3334
@Service
3435
class DeleteCommand extends CoreCommand {
@@ -94,7 +95,7 @@ private void runDeleteCommand(MVCommandIssuer issuer, MultiverseWorld world, Par
9495
}
9596

9697
private void doWorldDeleting(MVCommandIssuer issuer, MultiverseWorld world) {
97-
worldManager.deleteWorld(world)
98+
worldManager.deleteWorld(DeleteWorldOptions.world(world))
9899
.onSuccess(deletedWorldName -> {
99100
Logging.fine("World delete success: " + deletedWorldName);
100101
issuer.sendInfo(MVCorei18n.DELETE_SUCCESS, Replace.WORLD.with(deletedWorldName));

src/main/java/org/mvplugins/multiverse/core/commands/RegenCommand.java

+10-2
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
import org.mvplugins.multiverse.core.command.LegacyAliasCommand;
2121
import org.mvplugins.multiverse.core.command.MVCommandIssuer;
22-
import org.mvplugins.multiverse.core.command.MVCommandManager;
2322
import org.mvplugins.multiverse.core.command.flag.CommandFlag;
2423
import org.mvplugins.multiverse.core.command.flag.CommandFlagsManager;
2524
import org.mvplugins.multiverse.core.command.flag.CommandValueFlag;
@@ -30,6 +29,7 @@
3029
import org.mvplugins.multiverse.core.locale.MVCorei18n;
3130
import org.mvplugins.multiverse.core.locale.message.Message;
3231
import org.mvplugins.multiverse.core.locale.message.MessageReplacement.Replace;
32+
import org.mvplugins.multiverse.core.utils.REPatterns;
3333
import org.mvplugins.multiverse.core.utils.WorldTickDeferrer;
3434
import org.mvplugins.multiverse.core.utils.result.AsyncAttemptsAggregate;
3535
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
@@ -112,7 +112,8 @@ private void doWorldRegening(
112112
.seed(parsedFlags.flagValue(flags.seed))
113113
.keepWorldConfig(!parsedFlags.hasFlag(flags.resetWorldConfig))
114114
.keepGameRule(!parsedFlags.hasFlag(flags.resetGamerules))
115-
.keepWorldBorder(!parsedFlags.hasFlag(flags.resetWorldBorder));
115+
.keepWorldBorder(!parsedFlags.hasFlag(flags.resetWorldBorder))
116+
.keepFiles(parsedFlags.flagValue(flags.keepFiles));
116117

117118
worldManager.regenWorld(regenWorldOptions).onSuccess(newWorld -> {
118119
Logging.fine("World regen success: " + newWorld);
@@ -153,6 +154,13 @@ private Flags(@NotNull CommandFlagsManager flagsManager) {
153154
private final CommandFlag resetWorldBorder = flag(CommandFlag.builder("--reset-world-border")
154155
.addAlias("-wb")
155156
.build());
157+
158+
private final CommandValueFlag<List> keepFiles = flag(CommandValueFlag.builder("--keep-files", List.class)
159+
.addAlias("-f")
160+
.completion(input -> List.of("paper-world.yml"))
161+
.defaultValue(Collections.emptyList())
162+
.context(input -> List.of(REPatterns.COMMA.split(input)))
163+
.build());
156164
}
157165

158166
@Service

src/main/java/org/mvplugins/multiverse/core/utils/FileUtils.java

+31-6
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@
99

1010
import java.io.File;
1111
import java.io.IOException;
12-
import java.lang.management.ManagementFactory;
13-
import java.nio.file.*;
12+
import java.nio.file.FileVisitResult;
13+
import java.nio.file.Files;
14+
import java.nio.file.Path;
15+
import java.nio.file.SimpleFileVisitor;
1416
import java.nio.file.attribute.BasicFileAttributes;
17+
import java.util.Collection;
1518
import java.util.Collections;
1619
import java.util.Comparator;
1720
import java.util.List;
@@ -20,7 +23,6 @@
2023
import com.dumptruckman.minecraft.util.Logging;
2124
import io.vavr.control.Try;
2225
import jakarta.inject.Inject;
23-
import org.bukkit.Bukkit;
2426
import org.jetbrains.annotations.NotNull;
2527
import org.jetbrains.annotations.Nullable;
2628
import org.jvnet.hk2.annotations.Service;
@@ -102,7 +104,11 @@ public File getServerFolder() {
102104
* the folder could not be deleted.
103105
*/
104106
public Try<Void> deleteFolder(File file) {
105-
return deleteFolder(file.toPath());
107+
return deleteFolder(file, Collections.emptyList());
108+
}
109+
110+
public Try<Void> deleteFolder(File file, Collection<String> keepFiles) {
111+
return deleteFolder(file.toPath(), keepFiles);
106112
}
107113

108114
/**
@@ -112,11 +118,19 @@ public Try<Void> deleteFolder(File file) {
112118
* @return A {@link Try} that will contain {@code null} if the folder was deleted successfully, or an exception if
113119
* the folder could not be deleted.
114120
*/
115-
public Try<Void> deleteFolder(Path path) {
121+
public Try<Void> deleteFolder(Path path, Collection<String> keepFiles) {
116122
try (Stream<Path> files = Files.walk(path)) {
117123
files.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(file -> {
124+
if (!isDirectoryEmpty(file)) {
125+
Logging.finest("Cannot delete folder as it is not empty: " + file);
126+
return;
127+
}
128+
if (file.isFile() && keepFiles.contains(file.getName())) {
129+
Logging.finest("Keeping file: " + file);
130+
return;
131+
}
118132
if (!file.delete()) {
119-
Logging.warning("Failed to delete file: " + file);
133+
throw new IllegalStateException("Failed to delete file: " + file);
120134
}
121135
});
122136
return Try.success(null);
@@ -127,6 +141,17 @@ public Try<Void> deleteFolder(Path path) {
127141
}
128142
}
129143

144+
private boolean isDirectoryEmpty(File file) {
145+
if (!file.isDirectory()) {
146+
return true;
147+
}
148+
try (Stream<Path> entries = Files.list(file.toPath())) {
149+
return entries.findFirst().isEmpty();
150+
} catch (IOException e) {
151+
return true;
152+
}
153+
}
154+
130155
/**
131156
* Copies all the content of the given folder to the given target folder.
132157
*

src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java

+13-24
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import org.mvplugins.multiverse.core.world.helpers.WorldNameChecker;
5656
import org.mvplugins.multiverse.core.world.options.CloneWorldOptions;
5757
import org.mvplugins.multiverse.core.world.options.CreateWorldOptions;
58+
import org.mvplugins.multiverse.core.world.options.DeleteWorldOptions;
5859
import org.mvplugins.multiverse.core.world.options.ImportWorldOptions;
5960
import org.mvplugins.multiverse.core.world.options.KeepWorldSettingsOptions;
6061
import org.mvplugins.multiverse.core.world.options.RegenWorldOptions;
@@ -211,7 +212,7 @@ private Attempt<CreateWorldOptions, CreateFailureReason> validateCreateWorldOpti
211212
return worldActionResult(CreateFailureReason.WORLD_EXIST_LOADED, options.worldName());
212213
} else if (getWorld(options.worldName()).isDefined()) {
213214
return worldActionResult(CreateFailureReason.WORLD_EXIST_UNLOADED, options.worldName());
214-
} else if (worldNameChecker.hasWorldFolder(options.worldName())) {
215+
} else if (options.doFolderCheck() && worldNameChecker.hasWorldFolder(options.worldName())) {
215216
return worldActionResult(CreateFailureReason.WORLD_EXIST_FOLDER, options.worldName());
216217
}
217218
return worldActionResult(options);
@@ -506,28 +507,15 @@ private Attempt<String, RemoveFailureReason> removeWorldFromConfig(@NotNull Mult
506507
* Deletes an existing multiverse world entirely. World will be loaded if it is not already loaded.
507508
* Warning: This will delete all world files.
508509
*
509-
* @param worldName The name of the world to delete.
510+
* @param options The options for customizing the deletion of a world.
510511
* @return The result of the delete action.
511512
*/
512-
public Attempt<String, DeleteFailureReason> deleteWorld(@NotNull String worldName) {
513-
return getWorld(worldName)
514-
.map(this::deleteWorld)
515-
.getOrElse(() -> worldActionResult(DeleteFailureReason.WORLD_NON_EXISTENT, worldName));
516-
}
517-
518-
/**
519-
* Deletes an existing multiverse world entirely. World will be loaded if it is not already loaded.
520-
* Warning: This will delete all world files.
521-
*
522-
* @param world The world to delete.
523-
* @return The result of the delete action.
524-
*/
525-
public Attempt<String, DeleteFailureReason> deleteWorld(@NotNull MultiverseWorld world) {
526-
return getLoadedWorld(world).fold(
527-
() -> loadWorld(world)
513+
public Attempt<String, DeleteFailureReason> deleteWorld(@NotNull DeleteWorldOptions options) {
514+
return getLoadedWorld(options.world()).fold(
515+
() -> loadWorld(options.world())
528516
.transform(DeleteFailureReason.LOAD_FAILED)
529-
.mapAttempt(this::deleteWorld),
530-
this::deleteWorld);
517+
.mapAttempt(world -> doDeleteWorld(world, options)),
518+
world -> doDeleteWorld(world, options));
531519
}
532520

533521
/**
@@ -536,7 +524,7 @@ public Attempt<String, DeleteFailureReason> deleteWorld(@NotNull MultiverseWorld
536524
* @param world The multiverse world to delete.
537525
* @return The result of the delete action.
538526
*/
539-
public Attempt<String, DeleteFailureReason> deleteWorld(@NotNull LoadedMultiverseWorld world) {
527+
private Attempt<String, DeleteFailureReason> doDeleteWorld(@NotNull LoadedMultiverseWorld world, @NotNull DeleteWorldOptions options) {
540528
AtomicReference<File> worldFolder = new AtomicReference<>();
541529
return validateWorldToDelete(world)
542530
.peek(worldFolder::set)
@@ -548,7 +536,7 @@ public Attempt<String, DeleteFailureReason> deleteWorld(@NotNull LoadedMultivers
548536
: Attempt.success(null);
549537
})
550538
.mapAttempt(() -> removeWorld(world).transform(DeleteFailureReason.REMOVE_FAILED))
551-
.mapAttempt(() -> fileUtils.deleteFolder(worldFolder.get()).fold(
539+
.mapAttempt(() -> fileUtils.deleteFolder(worldFolder.get(), options.keepFiles()).fold(
552540
exception -> worldActionResult(DeleteFailureReason.FAILED_TO_DELETE_FOLDER,
553541
world.getName(), exception),
554542
success -> worldActionResult(world.getName())));
@@ -665,9 +653,10 @@ public Attempt<LoadedMultiverseWorld, RegenFailureReason> regenWorld(@NotNull Re
665653
.generator(world.getGenerator())
666654
.seed(options.seed())
667655
.useSpawnAdjust(!shouldKeepSpawnLocation && world.getAdjustSpawn())
668-
.worldType(world.getWorldType().getOrElse(WorldType.NORMAL));
656+
.worldType(world.getWorldType().getOrElse(WorldType.NORMAL))
657+
.doFolderCheck(options.keepFiles().isEmpty());
669658

670-
return deleteWorld(world)
659+
return deleteWorld(DeleteWorldOptions.world(world).keepFiles(options.keepFiles()))
671660
.transform(RegenFailureReason.DELETE_FAILED)
672661
.mapAttempt(() -> createWorld(createWorldOptions).transform(RegenFailureReason.CREATE_FAILED))
673662
.onSuccess(newWorld -> {

src/main/java/org/mvplugins/multiverse/core/world/options/CreateWorldOptions.java

+21
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public final class CreateWorldOptions {
3030
private long seed;
3131
private boolean useSpawnAdjust = true;
3232
private WorldType worldType = WorldType.NORMAL;
33+
private boolean doFolderCheck = true;
3334

3435
/**
3536
* Creates a new {@link CreateWorldOptions} instance with the given world name.
@@ -239,4 +240,24 @@ public boolean useSpawnAdjust() {
239240
public @NotNull WorldType worldType() {
240241
return worldType;
241242
}
243+
244+
/**
245+
* Sets whether to ensure folder does not exist before creating the world.
246+
*
247+
* @param doFolderCheckInput Whether to do the folder check
248+
* @return This {@link CreateWorldOptions} instance
249+
*/
250+
public @NotNull CreateWorldOptions doFolderCheck(boolean doFolderCheckInput) {
251+
this.doFolderCheck = doFolderCheckInput;
252+
return this;
253+
}
254+
255+
/**
256+
* Gets whether to ensure folder does not exist before creating the world.
257+
*
258+
* @return Whether to do the folder check
259+
*/
260+
public boolean doFolderCheck() {
261+
return doFolderCheck;
262+
}
242263
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package org.mvplugins.multiverse.core.world.options;
2+
3+
import org.jetbrains.annotations.NotNull;
4+
import org.mvplugins.multiverse.core.world.MultiverseWorld;
5+
6+
import java.util.Collections;
7+
import java.util.List;
8+
9+
/**
10+
* Options for customizing the deletion of a world.
11+
*/
12+
public final class DeleteWorldOptions {
13+
14+
/**
15+
* Creates a new {@link DeleteWorldOptions} instance with the given world.
16+
*
17+
* @param world The world to delete.
18+
* @return A new {@link DeleteWorldOptions} instance.
19+
*/
20+
public static @NotNull DeleteWorldOptions world(@NotNull MultiverseWorld world) {
21+
return new DeleteWorldOptions(world);
22+
}
23+
24+
private final MultiverseWorld world;
25+
private List<String> keepFiles = Collections.emptyList();
26+
27+
DeleteWorldOptions(MultiverseWorld world) {
28+
this.world = world;
29+
}
30+
31+
public MultiverseWorld world() {
32+
return world;
33+
}
34+
35+
/**
36+
* Sets the files to keep during deletion.
37+
*
38+
* @param keepFilesInput The files to keep during deletion.
39+
* @return This {@link DeleteWorldOptions} instance.
40+
*/
41+
public @NotNull DeleteWorldOptions keepFiles(List<String> keepFilesInput) {
42+
this.keepFiles = keepFilesInput == null ? Collections.emptyList() : keepFilesInput.stream().toList();
43+
return this;
44+
}
45+
46+
/**
47+
* Gets the files to keep during deletion. Note: The list is unmodifiable.
48+
*
49+
* @return The files to keep during deletion.
50+
*/
51+
public List<String> keepFiles() {
52+
return keepFiles;
53+
}
54+
}

src/main/java/org/mvplugins/multiverse/core/world/options/RegenWorldOptions.java

+24
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld;
99

10+
import java.util.Collections;
11+
import java.util.List;
12+
1013
/**
1114
* Options for customizing the regeneration of a world.
1215
*/
@@ -31,6 +34,7 @@ public final class RegenWorldOptions implements KeepWorldSettingsOptions {
3134
private boolean keepWorldBorder = true;
3235
private boolean randomSeed = false;
3336
private long seed = UNINITIALIZED_SEED_VALUE;
37+
private List<String> keepFiles = Collections.emptyList();
3438

3539
RegenWorldOptions(@NotNull LoadedMultiverseWorld world) {
3640
this.world = world;
@@ -211,4 +215,24 @@ public long seed() {
211215
}
212216
return world.getSeed();
213217
}
218+
219+
/**
220+
* Sets the files to keep during regeneration.
221+
*
222+
* @param keepFilesInput The files to keep during regeneration.
223+
* @return This {@link RegenWorldOptions} instance.
224+
*/
225+
public @NotNull RegenWorldOptions keepFiles(@Nullable List<String> keepFilesInput) {
226+
this.keepFiles = keepFilesInput == null ? Collections.emptyList() : keepFilesInput.stream().toList();
227+
return this;
228+
}
229+
230+
/**
231+
* Gets the files to keep during regeneration. Note: The list is unmodifiable.
232+
*
233+
* @return The files to keep during regeneration.
234+
*/
235+
public @NotNull List<String> keepFiles() {
236+
return keepFiles;
237+
}
214238
}

src/test/java/org/mvplugins/multiverse/core/world/WorldManagerTest.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import org.mvplugins.multiverse.core.TestWithMockBukkit
99
import org.mvplugins.multiverse.core.event.world.*
1010
import org.mvplugins.multiverse.core.world.options.CloneWorldOptions
1111
import org.mvplugins.multiverse.core.world.options.CreateWorldOptions
12+
import org.mvplugins.multiverse.core.world.options.DeleteWorldOptions
1213
import org.mvplugins.multiverse.core.world.options.RegenWorldOptions
1314
import org.mvplugins.multiverse.core.world.options.UnloadWorldOptions
1415
import org.mvplugins.multiverse.core.world.reasons.CloneFailureReason
@@ -112,8 +113,10 @@ class WorldManagerTest : TestWithMockBukkit() {
112113

113114
@Test
114115
fun `Delete world`() {
115-
assertTrue(worldManager.deleteWorld(world).isSuccess)
116+
assertTrue(File(Bukkit.getWorldContainer(), "world").isDirectory)
117+
assertTrue(worldManager.deleteWorld(DeleteWorldOptions.world(world)).isSuccess)
116118
assertFalse(worldManager.getLoadedWorld("world").isDefined)
119+
assertFalse(File(Bukkit.getWorldContainer(), "world").isDirectory)
117120

118121
assertThat(server.pluginManager, hasFiredEventInstance(MVWorldDeleteEvent::class.java))
119122
assertThat(server.pluginManager, hasFiredEventInstance(MVWorldUnloadedEvent::class.java))

0 commit comments

Comments
 (0)