Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,3 @@ jobs:
with:
name: logs for ${{ matrix.os }}
path: '**/*.log'

Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,16 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.Futures;
import com.mojang.serialization.Codec;
import com.mojang.serialization.Lifecycle;
import com.sk89q.worldedit.EditSession;
import com.sk89q.worldedit.WorldEditException;
import com.sk89q.worldedit.blocks.BaseItem;
import com.sk89q.worldedit.blocks.BaseItemStack;
import com.sk89q.worldedit.bukkit.BukkitAdapter;
import com.sk89q.worldedit.bukkit.WorldEditPlugin;
import com.sk89q.worldedit.bukkit.adapter.BukkitImplAdapter;
import com.sk89q.worldedit.bukkit.folia.FoliaScheduler;
import com.sk89q.worldedit.entity.BaseEntity;
import com.sk89q.worldedit.extension.platform.Watchdog;
import com.sk89q.worldedit.extent.Extent;
Expand Down Expand Up @@ -699,6 +700,10 @@ public boolean canPlaceAt(World world, BlockVector3 position, BlockState blockSt

@Override
public boolean regenerate(World bukkitWorld, Region region, Extent extent, RegenOptions options) {
if (FoliaScheduler.isFolia()) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO the regen code here is substantially more complicated than it makes sense to support (given the very low Folia user counts); I'd personally rather at least for now have this throw an unsupported error, and then possibly in the future looking into adding this in a simpler way with less maintenance burden.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appreciate the kind responses here! We've actually created a much more simplified approach that'd likely also reduce the # of code changes needed. Once we properly implement everything on FAWE, I'll go ahead and shift all of these changes to WE in an appropriate way.

return regenerateFolia(bukkitWorld, region, extent, options);
}

try {
doRegen(bukkitWorld, region, extent, options);
} catch (Exception e) {
Expand All @@ -708,6 +713,14 @@ public boolean regenerate(World bukkitWorld, Region region, Extent extent, Regen
return true;
}

private boolean regenerateFolia(World bukkitWorld, Region region, Extent extent, RegenOptions options) {
try {
return doRegenFolia(bukkitWorld, region, extent, options);
} catch (Exception e) {
throw new IllegalStateException("Regen failed on Folia.", e);
}
}

private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception {
Environment env = bukkitWorld.getEnvironment();
ChunkGenerator gen = bukkitWorld.getGenerator();
Expand Down Expand Up @@ -781,6 +794,244 @@ private void doRegen(World bukkitWorld, Region region, Extent extent, RegenOptio
}
}

private boolean doRegenFolia(World bukkitWorld, Region region, Extent extent, RegenOptions options) throws Exception {
Environment env = bukkitWorld.getEnvironment();
ChunkGenerator gen = bukkitWorld.getGenerator();

Path tempDir = Files.createTempDirectory("WorldEditWorldGen");
LevelStorageSource levelStorage = LevelStorageSource.createDefault(tempDir);
ResourceKey<LevelStem> worldDimKey = getWorldDimKey(env);
try (LevelStorageSource.LevelStorageAccess session = levelStorage.createAccess("worldeditregentempworld", worldDimKey)) {
ServerLevel originalWorld = ((CraftWorld) bukkitWorld).getHandle();
PrimaryLevelData levelProperties = (PrimaryLevelData) originalWorld.getServer()
.getWorldData().overworldData();
WorldOptions originalOpts = levelProperties.worldGenOptions();

long seed = options.getSeed().orElse(originalWorld.getSeed());
WorldOptions newOpts = options.getSeed().isPresent()
? originalOpts.withSeed(OptionalLong.of(seed))
: originalOpts;

LevelSettings newWorldSettings = new LevelSettings(
"worldeditregentempworld",
levelProperties.settings.gameType(),
levelProperties.settings.hardcore(),
levelProperties.settings.difficulty(),
levelProperties.settings.allowCommands(),
levelProperties.settings.gameRules(),
levelProperties.settings.getDataConfiguration()
);

@SuppressWarnings("deprecation")
PrimaryLevelData.SpecialWorldProperty specialWorldProperty =
levelProperties.isFlatWorld()
? PrimaryLevelData.SpecialWorldProperty.FLAT
: levelProperties.isDebugWorld()
? PrimaryLevelData.SpecialWorldProperty.DEBUG
: PrimaryLevelData.SpecialWorldProperty.NONE;

PrimaryLevelData newWorldData = new PrimaryLevelData(newWorldSettings, newOpts, specialWorldProperty, Lifecycle.stable());

ServerLevel freshWorld = new ServerLevel(
originalWorld.getServer(),
originalWorld.getServer().executor,
session, newWorldData,
originalWorld.dimension(),
new LevelStem(
originalWorld.dimensionTypeRegistration(),
originalWorld.getChunkSource().getGenerator()
),
new NoOpWorldLoadListener(),
originalWorld.isDebug(),
seed,
ImmutableList.of(),
false,
originalWorld.getRandomSequences(),
env,
gen,
bukkitWorld.getBiomeProvider()
);

try {
ChunkPos spawnChunk = new ChunkPos(
freshWorld.getChunkSource().randomState().sampler().findSpawnPosition()
);

try {
Field randomSpawnField = ServerLevel.class.getDeclaredField("randomSpawnSelection");
randomSpawnField.setAccessible(true);
randomSpawnField.set(freshWorld, spawnChunk);
} catch (ReflectiveOperationException e) {
throw new RuntimeException("Failed to set spawn chunk for Folia", e);
}

MinecraftServer console = originalWorld.getServer();
CompletableFuture<Void> initFuture = new CompletableFuture<>();

FoliaScheduler.getRegionScheduler().run(
WorldEditPlugin.getInstance(),
freshWorld.getWorld(),
spawnChunk.x, spawnChunk.z,
o -> {
try {
console.initWorld(freshWorld, newWorldData, newWorldData, newWorldData.worldGenOptions());
initFuture.complete(null);
} catch (Exception e) {
initFuture.completeExceptionally(e);
}
}
);

initFuture.get();

regenForWorldFolia(region, extent, freshWorld, options);
} finally {
freshWorld.getChunkSource().close(false);
}
} finally {
try {
@SuppressWarnings("unchecked")
Map<String, World> map = (Map<String, World>) serverWorldsField.get(Bukkit.getServer());
map.remove("worldeditregentempworld");
} catch (IllegalAccessException ignored) {
}
SafeFiles.tryHardToDeleteDir(tempDir);
}

return true;
}

@SuppressWarnings("unchecked")
private void regenForWorldFolia(Region region, Extent extent, ServerLevel serverWorld, RegenOptions options) throws WorldEditException {
Map<BlockVector3, BlockStateHolder<?>> blockStates = new HashMap<>();
Map<BlockVector3, BiomeType> biomes = new HashMap<>();
Map<ChunkPos, List<BlockVector3>> blocksByChunk = new HashMap<>();

for (BlockVector3 vec : region) {
ChunkPos chunkPos = new ChunkPos(vec.x() >> 4, vec.z() >> 4);
blocksByChunk.computeIfAbsent(chunkPos, k -> new ArrayList<>()).add(vec);
}

World bukkitWorld = serverWorld.getWorld();
List<CompletableFuture<Void>> extractionFutures = new ArrayList<>();

for (Map.Entry<ChunkPos, List<BlockVector3>> entry : blocksByChunk.entrySet()) {
ChunkPos chunkPos = entry.getKey();
List<BlockVector3> blocks = entry.getValue();
CompletableFuture<Void> future = new CompletableFuture<>();

FoliaScheduler.getRegionScheduler().execute(
WorldEditPlugin.getInstance(),
bukkitWorld,
chunkPos.x,
chunkPos.z,
() -> {
try {
ServerChunkCache chunkManager = serverWorld.getChunkSource();
CompletableFuture<ChunkResult<ChunkAccess>> chunkFuture =
((CompletableFuture<ChunkResult<ChunkAccess>>)
getChunkFutureMethod.invoke(chunkManager, chunkPos.x, chunkPos.z, ChunkStatus.FEATURES, true));
chunkFuture.thenApply(either -> either.orElse(null))
.whenComplete((chunkAccess, throwable) -> {
if (throwable != null) {
future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", throwable));
} else if (chunkAccess != null) {
try {
for (BlockVector3 vec : blocks) {
BlockPos pos = new BlockPos(vec.x(), vec.y(), vec.z());
final net.minecraft.world.level.block.state.BlockState blockData = chunkAccess.getBlockState(pos);
int internalId = Block.getId(blockData);
BlockStateHolder<?> state = BlockStateIdAccess.getBlockStateById(internalId);
Objects.requireNonNull(state);
BlockEntity blockEntity = chunkAccess.getBlockEntity(pos);
if (blockEntity != null) {
net.minecraft.nbt.CompoundTag tag = blockEntity.saveWithId(serverWorld.registryAccess());
state = state.toBaseBlock(LazyReference.from(() -> (LinCompoundTag) toNative(tag)));
}
synchronized (blockStates) {
blockStates.put(vec, state);
}
if (options.shouldRegenBiomes()) {
Biome origBiome = chunkAccess.getNoiseBiome(vec.x(), vec.y(), vec.z()).value();
BiomeType adaptedBiome = adapt(serverWorld, origBiome);
if (adaptedBiome != null) {
synchronized (biomes) {
biomes.put(vec, adaptedBiome);
}
}
}
}
future.complete(null);
} catch (Exception e) {
future.completeExceptionally(e);
}
} else {
future.completeExceptionally(new IllegalStateException("Failed to generate a chunk, regen failed."));
}
});
} catch (IllegalAccessException | InvocationTargetException e) {
future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", e));
}
}
);
extractionFutures.add(future);
}

CompletableFuture.allOf(extractionFutures.toArray(new CompletableFuture<?>[0])).join();

for (BlockVector3 vec : region) {
BlockStateHolder<?> state = blockStates.get(vec);
if (state != null) {
extent.setBlock(vec, state.toBaseBlock());
if (options.shouldRegenBiomes()) {
BiomeType biome = biomes.get(vec);
if (biome != null) {
extent.setBiome(vec, biome);
}
}
}
}
}

@SuppressWarnings("unchecked")
private List<CompletableFuture<ChunkAccess>> submitChunkLoadTasksFolia(Region region, ServerLevel serverWorld) {
ServerChunkCache chunkManager = serverWorld.getChunkSource();
List<CompletableFuture<ChunkAccess>> chunkLoadings = new ArrayList<>();
World bukkitWorld = serverWorld.getWorld();

for (BlockVector2 chunk : region.getChunks()) {
CompletableFuture<ChunkAccess> future = new CompletableFuture<>();
final int chunkX = chunk.x();
final int chunkZ = chunk.z();

FoliaScheduler.getRegionScheduler().execute(
WorldEditPlugin.getInstance(),
bukkitWorld,
chunkX,
chunkZ,
() -> {
try {
CompletableFuture<ChunkResult<ChunkAccess>> chunkFuture =
((CompletableFuture<ChunkResult<ChunkAccess>>)
getChunkFutureMethod.invoke(chunkManager, chunkX, chunkZ, ChunkStatus.FEATURES, true));
chunkFuture.thenApply(either -> either.orElse(null))
.whenComplete((chunkAccess, throwable) -> {
if (throwable != null) {
future.completeExceptionally(throwable);
} else {
future.complete(chunkAccess);
}
});
} catch (IllegalAccessException | InvocationTargetException e) {
future.completeExceptionally(new IllegalStateException("Couldn't load chunk for regen.", e));
}
}
);
chunkLoadings.add(future);
}
return chunkLoadings;
}

private BiomeType adapt(ServerLevel serverWorld, Biome origBiome) {
ResourceLocation key = serverWorld.registryAccess().lookupOrThrow(Registries.BIOME).getKey(origBiome);
if (key == null) {
Expand All @@ -801,7 +1052,7 @@ private void regenForWorld(Region region, Extent extent, ServerLevel serverWorld
executor.managedBlock(() -> {
// bail out early if a future fails
if (chunkLoadings.stream().anyMatch(ftr ->
ftr.isDone() && Futures.getUnchecked(ftr) == null
ftr.isDone() && ftr.getNow(null) == null
)) {
return false;
}
Expand Down
Loading