Skip to content

Commit 1352ca8

Browse files
authored
Folia Support
This is a thoroughly tested version of WorldEdit against Folia. Not only does it introduce the fundamentals that other forks have offered, but it also provides the ability to utilize `-e` tags, butchering entities, and even the `//regen` command on Spigot, Paper, and Folia alike. That said, this pull request is ready for review, so critical changes (if needed) can be made to ensure Folia is sufficient and prepared for WorldEdit upstream. This was also done for FastAsyncWorldEdit: IntellectualSites/FastAsyncWorldEdit#3363
1 parent e6e82dd commit 1352ca8

21 files changed

Lines changed: 2590 additions & 112 deletions

File tree

.github/workflows/gradle.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,3 @@ jobs:
4040
with:
4141
name: logs for ${{ matrix.os }}
4242
path: '**/*.log'
43-

worldedit-bukkit/adapters/adapter-1.21.3/src/main/java/com/sk89q/worldedit/bukkit/adapter/impl/v1_21_3/PaperweightAdapter.java

Lines changed: 253 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,16 @@
2525
import com.google.common.collect.ImmutableList;
2626
import com.google.common.collect.Lists;
2727
import com.google.common.collect.Sets;
28-
import com.google.common.util.concurrent.Futures;
2928
import com.mojang.serialization.Codec;
3029
import com.mojang.serialization.Lifecycle;
3130
import com.sk89q.worldedit.EditSession;
3231
import com.sk89q.worldedit.WorldEditException;
3332
import com.sk89q.worldedit.blocks.BaseItem;
3433
import com.sk89q.worldedit.blocks.BaseItemStack;
3534
import com.sk89q.worldedit.bukkit.BukkitAdapter;
35+
import com.sk89q.worldedit.bukkit.WorldEditPlugin;
3636
import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter;
37+
import com.sk89q.worldedit.bukkit.folia.FoliaScheduler;
3738
import com.sk89q.worldedit.entity.BaseEntity;
3839
import com.sk89q.worldedit.extension.platform.Watchdog;
3940
import com.sk89q.worldedit.extent.Extent;
@@ -699,6 +700,10 @@ public boolean canPlaceAt(World world, BlockVector3 position, BlockState blockSt
699700

700701
@Override
701702
public boolean regenerate(World bukkitWorld, Region region, Extent extent, RegenOptions options) {
703+
if (FoliaScheduler.isFolia()) {
704+
return regenerateFolia(bukkitWorld, region, extent, options);
705+
}
706+
702707
try {
703708
doRegen(bukkitWorld, region, extent, options);
704709
} catch (Exception e) {
@@ -708,6 +713,14 @@ public boolean regenerate(World bukkitWorld, Region region, Extent extent, Regen
708713
return true;
709714
}
710715

716+
private boolean regenerateFolia(World bukkitWorld, Region region, Extent extent, RegenOptions options) {
717+
try {
718+
return doRegenFolia(bukkitWorld, region, extent, options);
719+
} catch (Exception e) {
720+
throw new IllegalStateException("Regen failed on Folia.", e);
721+
}
722+
}
723+
711724
private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception {
712725
Environment env = bukkitWorld.getEnvironment();
713726
ChunkGenerator gen = bukkitWorld.getGenerator();
@@ -781,6 +794,244 @@ private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptio
781794
}
782795
}
783796

797+
private boolean doRegenFolia(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception {
798+
Environment env = bukkitWorld.getEnvironment();
799+
ChunkGenerator gen = bukkitWorld.getGenerator();
800+
801+
Path tempDir = Files.createTempDirectory("WorldEditWorldGen");
802+
LevelStorageSource levelStorage = LevelStorageSource.createDefault(tempDir);
803+
ResourceKey<LevelStem> worldDimKey = getWorldDimKey(env);
804+
try (LevelStorageSource.LevelStorageAccess session = levelStorage.createAccess("worldeditregentempworld", worldDimKey)) {
805+
ServerLevel originalWorld = ((CraftWorld) bukkitWorld).getHandle();
806+
PrimaryLevelData levelProperties = (PrimaryLevelData) originalWorld.getServer()
807+
.getWorldData().overworldData();
808+
WorldOptions originalOpts = levelProperties.worldGenOptions();
809+
810+
long seed = options.getSeed().orElse(originalWorld.getSeed());
811+
WorldOptions newOpts = options.getSeed().isPresent()
812+
? originalOpts.withSeed(OptionalLong.of(seed))
813+
: originalOpts;
814+
815+
LevelSettings newWorldSettings = new LevelSettings(
816+
"worldeditregentempworld",
817+
levelProperties.settings.gameType(),
818+
levelProperties.settings.hardcore(),
819+
levelProperties.settings.difficulty(),
820+
levelProperties.settings.allowCommands(),
821+
levelProperties.settings.gameRules(),
822+
levelProperties.settings.getDataConfiguration()
823+
);
824+
825+
@SuppressWarnings("deprecation")
826+
PrimaryLevelData.SpecialWorldProperty specialWorldProperty =
827+
levelProperties.isFlatWorld()
828+
? PrimaryLevelData.SpecialWorldProperty.FLAT
829+
: levelProperties.isDebugWorld()
830+
? PrimaryLevelData.SpecialWorldProperty.DEBUG
831+
: PrimaryLevelData.SpecialWorldProperty.NONE;
832+
833+
PrimaryLevelData newWorldData = new PrimaryLevelData(newWorldSettings, newOpts, specialWorldProperty, Lifecycle.stable());
834+
835+
ServerLevel freshWorld = new ServerLevel(
836+
originalWorld.getServer(),
837+
originalWorld.getServer().executor,
838+
session, newWorldData,
839+
originalWorld.dimension(),
840+
new LevelStem(
841+
originalWorld.dimensionTypeRegistration(),
842+
originalWorld.getChunkSource().getGenerator()
843+
),
844+
new NoOpWorldLoadListener(),
845+
originalWorld.isDebug(),
846+
seed,
847+
ImmutableList.of(),
848+
false,
849+
originalWorld.getRandomSequences(),
850+
env,
851+
gen,
852+
bukkitWorld.getBiomeProvider()
853+
);
854+
855+
try {
856+
ChunkPos spawnChunk = new ChunkPos(
857+
freshWorld.getChunkSource().randomState().sampler().findSpawnPosition()
858+
);
859+
860+
try {
861+
Field randomSpawnField = ServerLevel.class.getDeclaredField("randomSpawnSelection");
862+
randomSpawnField.setAccessible(true);
863+
randomSpawnField.set(freshWorld, spawnChunk);
864+
} catch (ReflectiveOperationException e) {
865+
throw new RuntimeException("Failed to set spawn chunk for Folia", e);
866+
}
867+
868+
MinecraftServer console = originalWorld.getServer();
869+
CompletableFuture<Void> initFuture = new CompletableFuture<>();
870+
871+
FoliaScheduler.getRegionScheduler().run(
872+
WorldEditPlugin.getInstance(),
873+
freshWorld.getWorld(),
874+
spawnChunk.x, spawnChunk.z,
875+
o -> {
876+
try {
877+
console.initWorld(freshWorld, newWorldData, newWorldData, newWorldData.worldGenOptions());
878+
initFuture.complete(null);
879+
} catch (Exception e) {
880+
initFuture.completeExceptionally(e);
881+
}
882+
}
883+
);
884+
885+
initFuture.get();
886+
887+
regenForWorldFolia(region, extent, freshWorld, options);
888+
} finally {
889+
freshWorld.getChunkSource().close(false);
890+
}
891+
} finally {
892+
try {
893+
@SuppressWarnings("unchecked")
894+
Map<String, World> map = (Map<String, World>) serverWorldsField.get(Bukkit.getServer());
895+
map.remove("worldeditregentempworld");
896+
} catch (IllegalAccessException ignored) {
897+
}
898+
SafeFiles.tryHardToDeleteDir(tempDir);
899+
}
900+
901+
return true;
902+
}
903+
904+
@SuppressWarnings("unchecked")
905+
private void regenForWorldFolia(Region region, Extent extent, ServerLevel serverWorld, RegenOptions options) throws WorldEditException {
906+
Map<BlockVector3, BlockStateHolder<?>> blockStates = new HashMap<>();
907+
Map<BlockVector3, BiomeType> biomes = new HashMap<>();
908+
Map<ChunkPos, List<BlockVector3>> blocksByChunk = new HashMap<>();
909+
910+
for (BlockVector3 vec : region) {
911+
ChunkPos chunkPos = new ChunkPos(vec.x() >> 4, vec.z() >> 4);
912+
blocksByChunk.computeIfAbsent(chunkPos, k -> new ArrayList<>()).add(vec);
913+
}
914+
915+
World bukkitWorld = serverWorld.getWorld();
916+
List<CompletableFuture<Void>> extractionFutures = new ArrayList<>();
917+
918+
for (Map.Entry<ChunkPos, List<BlockVector3>> entry : blocksByChunk.entrySet()) {
919+
ChunkPos chunkPos = entry.getKey();
920+
List<BlockVector3> blocks = entry.getValue();
921+
CompletableFuture<Void> future = new CompletableFuture<>();
922+
923+
FoliaScheduler.getRegionScheduler().execute(
924+
WorldEditPlugin.getInstance(),
925+
bukkitWorld,
926+
chunkPos.x,
927+
chunkPos.z,
928+
() -> {
929+
try {
930+
ServerChunkCache chunkManager = serverWorld.getChunkSource();
931+
CompletableFuture<ChunkResult<ChunkAccess>> chunkFuture =
932+
((CompletableFuture<ChunkResult<ChunkAccess>>)
933+
getChunkFutureMethod.invoke(chunkManager, chunkPos.x, chunkPos.z, ChunkStatus.FEATURES, true));
934+
chunkFuture.thenApply(either -> either.orElse(null))
935+
.whenComplete((chunkAccess, throwable) -> {
936+
if (throwable != null) {
937+
future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", throwable));
938+
} else if (chunkAccess != null) {
939+
try {
940+
for (BlockVector3 vec : blocks) {
941+
BlockPos pos = new BlockPos(vec.x(), vec.y(), vec.z());
942+
final net.minecraft.world.level.block.state.BlockState blockData = chunkAccess.getBlockState(pos);
943+
int internalId = Block.getId(blockData);
944+
BlockStateHolder<?> state = BlockStateIdAccess.getBlockStateById(internalId);
945+
Objects.requireNonNull(state);
946+
BlockEntity blockEntity = chunkAccess.getBlockEntity(pos);
947+
if (blockEntity != null) {
948+
net.minecraft.nbt.CompoundTag tag = blockEntity.saveWithId(serverWorld.registryAccess());
949+
state = state.toBaseBlock(LazyReference.from(() -> (LinCompoundTag) toNative(tag)));
950+
}
951+
synchronized (blockStates) {
952+
blockStates.put(vec, state);
953+
}
954+
if (options.shouldRegenBiomes()) {
955+
Biome origBiome = chunkAccess.getNoiseBiome(vec.x(), vec.y(), vec.z()).value();
956+
BiomeType adaptedBiome = adapt(serverWorld, origBiome);
957+
if (adaptedBiome != null) {
958+
synchronized (biomes) {
959+
biomes.put(vec, adaptedBiome);
960+
}
961+
}
962+
}
963+
}
964+
future.complete(null);
965+
} catch (Exception e) {
966+
future.completeExceptionally(e);
967+
}
968+
} else {
969+
future.completeExceptionally(new IllegalStateException("Failed to generate a chunk, regen failed."));
970+
}
971+
});
972+
} catch (IllegalAccessException | InvocationTargetException e) {
973+
future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", e));
974+
}
975+
}
976+
);
977+
extractionFutures.add(future);
978+
}
979+
980+
CompletableFuture.allOf(extractionFutures.toArray(new CompletableFuture<?>[0])).join();
981+
982+
for (BlockVector3 vec : region) {
983+
BlockStateHolder<?> state = blockStates.get(vec);
984+
if (state != null) {
985+
extent.setBlock(vec, state.toBaseBlock());
986+
if (options.shouldRegenBiomes()) {
987+
BiomeType biome = biomes.get(vec);
988+
if (biome != null) {
989+
extent.setBiome(vec, biome);
990+
}
991+
}
992+
}
993+
}
994+
}
995+
996+
@SuppressWarnings("unchecked")
997+
private List<CompletableFuture<ChunkAccess>> submitChunkLoadTasksFolia(Region region, ServerLevel serverWorld) {
998+
ServerChunkCache chunkManager = serverWorld.getChunkSource();
999+
List<CompletableFuture<ChunkAccess>> chunkLoadings = new ArrayList<>();
1000+
World bukkitWorld = serverWorld.getWorld();
1001+
1002+
for (BlockVector2 chunk : region.getChunks()) {
1003+
CompletableFuture<ChunkAccess> future = new CompletableFuture<>();
1004+
final int chunkX = chunk.x();
1005+
final int chunkZ = chunk.z();
1006+
1007+
FoliaScheduler.getRegionScheduler().execute(
1008+
WorldEditPlugin.getInstance(),
1009+
bukkitWorld,
1010+
chunkX,
1011+
chunkZ,
1012+
() -> {
1013+
try {
1014+
CompletableFuture<ChunkResult<ChunkAccess>> chunkFuture =
1015+
((CompletableFuture<ChunkResult<ChunkAccess>>)
1016+
getChunkFutureMethod.invoke(chunkManager, chunkX, chunkZ, ChunkStatus.FEATURES, true));
1017+
chunkFuture.thenApply(either -> either.orElse(null))
1018+
.whenComplete((chunkAccess, throwable) -> {
1019+
if (throwable != null) {
1020+
future.completeExceptionally(throwable);
1021+
} else {
1022+
future.complete(chunkAccess);
1023+
}
1024+
});
1025+
} catch (IllegalAccessException | InvocationTargetException e) {
1026+
future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", e));
1027+
}
1028+
}
1029+
);
1030+
chunkLoadings.add(future);
1031+
}
1032+
return chunkLoadings;
1033+
}
1034+
7841035
private BiomeType adapt(ServerLevel serverWorld, Biome origBiome) {
7851036
ResourceLocation key = serverWorld.registryAccess().lookupOrThrow(Registries.BIOME).getKey(origBiome);
7861037
if (key == null) {
@@ -801,7 +1052,7 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld
8011052
executor.managedBlock(() -> {
8021053
// bail out early if a future fails
8031054
if (chunkLoadings.stream().anyMatch(ftr ->
804-
ftr.isDone() && Futures.getUnchecked(ftr) == null
1055+
ftr.isDone() && ftr.getNow(null) == null
8051056
)) {
8061057
return false;
8071058
}

0 commit comments

Comments
 (0)