Skip to content

Commit

Permalink
fix: Fix feature generation
Browse files Browse the repository at this point in the history
Uses a workaround that returns a dummy `ProtoChunk` in `ChunkRegion#getChunk` to prevent issues due to missing `null` checks.
This means there are issues at chunk borders, where features will get cut off. A potential solution for this is to store features' block modifications in the save data, and apply these block modifications on top of world generation chunk data.
  • Loading branch information
Steveplays28 committed Jun 23, 2024
1 parent 1fad880 commit a537247
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

// TODO: Disable vanilla's ServerEntityManager's save method
@Mixin(value = ServerEntityManager.class, priority = 500)
public class ServerEntityManagerMixin<T extends EntityLike> {
@Inject(method = "stopTracking", at = @At(value = "HEAD"), cancellable = true)
Expand All @@ -21,4 +20,10 @@ public class ServerEntityManagerMixin<T extends EntityLike> {
}
}
}

@Inject(method = "save", at = @At(value = "HEAD"), cancellable = true)
private void noisium$cancelSave(CallbackInfo ci) {
// TODO: Invoke an entity manager save event that's used in NoisiumServerWorldEntityTracker
ci.cancel();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,13 @@ public abstract class ServerWorldMixin implements NoisiumServerWorldExtension {
private NoisiumServerWorldEntityTracker noisium$serverWorldEntityManager;

@Inject(method = "<init>", at = @At(value = "TAIL"))
private void noisium$constructorCreateServerWorldChunkManager(MinecraftServer server, Executor workerExecutor, LevelStorage.Session session, ServerWorldProperties properties, RegistryKey<World> worldKey, DimensionOptions dimensionOptions, WorldGenerationProgressListener worldGenerationProgressListener, boolean debugWorld, long seed, List<?> spawners, boolean shouldTickTime, RandomSequencesState randomSequencesState, CallbackInfo ci, @Local DataFixer dataFixer) {
private void noisium$constructorCreateServerWorldChunkManager(MinecraftServer server, Executor workerExecutor, LevelStorage.Session session, ServerWorldProperties serverWorldProperties, RegistryKey<World> worldKey, DimensionOptions dimensionOptions, WorldGenerationProgressListener worldGenerationProgressListener, boolean debugWorld, long seed, List<?> spawners, boolean shouldTickTime, RandomSequencesState randomSequencesState, CallbackInfo ci, @Local DataFixer dataFixer) {
@SuppressWarnings("DataFlowIssue")
var serverWorld = ((ServerWorld) (Object) this);
// DEBUG
if (serverWorld.getRegistryKey() != World.OVERWORLD) {
return;
}

this.noisium$serverWorldChunkManager = new NoisiumServerWorldChunkManager(
serverWorld, dimensionOptions.chunkGenerator(), session.getWorldDirectory(worldKey), dataFixer);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.github.steveplays28.noisium.mixin.world;

import net.minecraft.registry.RegistryKeys;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.ChunkPos;
import net.minecraft.world.ChunkRegion;
import net.minecraft.world.StructureWorldAccess;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.ChunkStatus;
import net.minecraft.world.chunk.ProtoChunk;
import net.minecraft.world.chunk.UpgradeData;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;

import java.util.List;

@Mixin(ChunkRegion.class)
public abstract class ChunkRegionMixin implements StructureWorldAccess {

@Shadow
@Final
private ServerWorld world;

@Shadow
@Final
private ChunkPos lowerCorner;

@Shadow
@Final
private List<Chunk> chunks;

@Shadow
@Final
private int width;

/**
* @return The loaded {@link Chunk} at the specified chunk position, or a dummy {@link ProtoChunk} if the specified chunk position isn't loaded.
* @author Steveplays28
* @reason Workaround for there not being {@code null} checks when the specified chunk position isn't in this {@link ChunkRegion}'s bounds.
*/
@Overwrite
public @Nullable Chunk getChunk(int chunkPosX, int chunkPosZ, ChunkStatus leastChunkStatus, boolean create) {
if (!this.isChunkLoaded(chunkPosX, chunkPosZ)) {
// TODO: Replace the ProtoChunk instances with null and add null checks where needed using ASM
var protoChunk = new ProtoChunk(
new ChunkPos(chunkPosX, chunkPosZ), UpgradeData.NO_UPGRADE_DATA, this.world, this.world.getRegistryManager().get(
RegistryKeys.BIOME), null);
protoChunk.setLightingProvider(world.getLightingProvider());
protoChunk.setStatus(ChunkStatus.FULL);
return protoChunk;
}

int i = chunkPosX - this.lowerCorner.x;
int j = chunkPosZ - this.lowerCorner.z;
var chunk = (Chunk) this.chunks.get(i + j * this.width);
if (chunk.getStatus().isAtLeast(leastChunkStatus)) {
return chunk;
}

// TODO: Replace the ProtoChunk instances with null and add null checks where needed using ASM
var protoChunk = new ProtoChunk(
new ChunkPos(chunkPosX, chunkPosZ), UpgradeData.NO_UPGRADE_DATA, this.world, this.world.getRegistryManager().get(
RegistryKeys.BIOME), null);
protoChunk.setLightingProvider(world.getLightingProvider());
protoChunk.setStatus(ChunkStatus.FULL);
return protoChunk;
}
}
Original file line number Diff line number Diff line change
@@ -1,46 +1,69 @@
package io.github.steveplays28.noisium.mixin.world.gen.chunk;

import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import com.llamalad7.mixinextras.sugar.Local;
import it.unimi.dsi.fastutil.ints.IntArraySet;
import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.objects.ObjectArraySet;
import net.minecraft.SharedConstants;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.entry.RegistryEntry;
import net.minecraft.registry.entry.RegistryEntryList;
import net.minecraft.server.network.DebugInfoSender;
import net.minecraft.structure.StructureStart;
import net.minecraft.util.crash.CrashException;
import net.minecraft.util.crash.CrashReport;
import net.minecraft.util.crash.CrashReportSection;
import net.minecraft.util.math.BlockBox;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.ChunkPos;
import net.minecraft.util.math.ChunkSectionPos;
import net.minecraft.util.math.random.ChunkRandom;
import net.minecraft.util.math.random.RandomSeed;
import net.minecraft.util.math.random.Xoroshiro128PlusPlusRandom;
import net.minecraft.world.StructureWorldAccess;
import net.minecraft.world.biome.Biome;
import net.minecraft.world.biome.GenerationSettings;
import net.minecraft.world.biome.source.BiomeSource;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.ChunkSection;
import net.minecraft.world.gen.GenerationStep;
import net.minecraft.world.gen.StructureAccessor;
import net.minecraft.world.gen.chunk.ChunkGenerator;
import net.minecraft.world.gen.feature.PlacedFeature;
import net.minecraft.world.gen.feature.util.PlacedFeatureIndexer;
import net.minecraft.world.gen.structure.Structure;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;

import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

@Mixin(ChunkGenerator.class)
public class ChunkGeneratorMixin {
@WrapWithCondition(method = "generateFeatures", at = @At(value = "INVOKE", target = "Ljava/util/stream/Stream;forEach(Ljava/util/function/Consumer;)V"))
private static boolean noisium$cancelChunkPosStreamToFixAnInfiniteLoop(Stream<ChunkPos> stream, Consumer<?> consumer) {
return false;
}
public abstract class ChunkGeneratorMixin {
@Shadow
@Final
private Supplier<List<PlacedFeatureIndexer.IndexedFeatures>> indexedFeaturesListSupplier;

@Inject(method = "generateFeatures", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/math/ChunkPos;stream(Lnet/minecraft/util/math/ChunkPos;I)Ljava/util/stream/Stream;", shift = At.Shift.BEFORE))
private void noisium$addChunkSectionBiomeContainersToSet(StructureWorldAccess world, Chunk chunk, StructureAccessor structureAccessor, CallbackInfo ci, @Local Set<RegistryEntry<Biome>> set) {
for (ChunkSection chunkSection : chunk.getSectionArray()) {
chunkSection.getBiomeContainer().forEachValue(set::add);
}
@Shadow
private static BlockBox getBlockBoxForChunk(Chunk chunk) {
throw new RuntimeException();
}

@Shadow
@Final
private Function<RegistryEntry<Biome>, GenerationSettings> generationSettingsGetter;

@Shadow
public abstract BiomeSource getBiomeSource();

/**
* Replaces {@link ChunkGenerator#addStructureReferences} with a simpler one, that only checks the center chunk, instead of iterating outwards.
* This fixes an infinite loop with {@link io.github.steveplays28.noisium.server.world.NoisiumServerWorldChunkManager}.
Expand Down Expand Up @@ -95,4 +118,119 @@ public class ChunkGeneratorMixin {

ci.cancel();
}

/**
* @author Steveplays28
* @reason TODO
*/
@Overwrite
public void generateFeatures(StructureWorldAccess world, Chunk chunk, StructureAccessor structureAccessor) {
ChunkPos chunkPos = chunk.getPos();
if (SharedConstants.isOutsideGenerationArea(chunkPos)) {
return;
}

ChunkSectionPos chunkSectionPos = ChunkSectionPos.from(chunkPos, world.getBottomSectionCoord());
BlockPos blockPos = chunkSectionPos.getMinPos();
Registry<Structure> registry = world.getRegistryManager().get(RegistryKeys.STRUCTURE);
Map<Integer, List<Structure>> map = registry.stream().collect(
Collectors.groupingBy((structureType) -> structureType.getFeatureGenerationStep().ordinal()));
List<PlacedFeatureIndexer.IndexedFeatures> indexedFeatures = this.indexedFeaturesListSupplier.get();
ChunkRandom chunkRandom = new ChunkRandom(new Xoroshiro128PlusPlusRandom(RandomSeed.getSeed()));
long l = chunkRandom.setPopulationSeed(world.getSeed(), blockPos.getX(), blockPos.getZ());
Set<RegistryEntry<Biome>> biomeRegistryEntries = new ObjectArraySet<>();
for (ChunkSection chunkSection : chunk.getSectionArray()) {
chunkSection.getBiomeContainer().forEachValue(biomeRegistryEntries::add);
}
biomeRegistryEntries.retainAll(this.getBiomeSource().getBiomes());
int indexedFeaturesSize = indexedFeatures.size();

try {
Registry<PlacedFeature> placedFeatureRegistry = world.getRegistryManager().get(RegistryKeys.PLACED_FEATURE);
int j = Math.max(GenerationStep.Feature.values().length, indexedFeaturesSize);

for (int indexedFeatureIndex = 0; indexedFeatureIndex < j; ++indexedFeatureIndex) {
int m = 0;
CrashReportSection crashReportSection;
if (structureAccessor.shouldGenerateStructures()) {
List<Structure> list2 = map.getOrDefault(indexedFeatureIndex, Collections.emptyList());

for (var biomeRegistryEntriesIterator = list2.iterator(); biomeRegistryEntriesIterator.hasNext(); ++m) {
Structure structure = biomeRegistryEntriesIterator.next();
chunkRandom.setDecoratorSeed(l, m, indexedFeatureIndex);
Supplier<String> currentlyGeneratingStructureNameSupplier = () -> {
var placedFeatureNameOptional = registry.getKey(structure).map(Object::toString);
Objects.requireNonNull(structure);
return placedFeatureNameOptional.orElseGet(structure::toString);
};

try {
world.setCurrentlyGeneratingStructureName(currentlyGeneratingStructureNameSupplier);
//noinspection DataFlowIssue
structureAccessor.getStructureStarts(chunkSectionPos, structure).forEach((start) -> start.place(
world, structureAccessor, (ChunkGenerator) (Object) this, chunkRandom,
getBlockBoxForChunk(chunk), chunkPos
));
} catch (Exception e) {
CrashReport crashReport = CrashReport.create(e, "Feature placement");
crashReportSection = crashReport.addElement("Feature");
Objects.requireNonNull(currentlyGeneratingStructureNameSupplier);
crashReportSection.add("Description", currentlyGeneratingStructureNameSupplier::get);
throw new CrashException(crashReport);
}
}
}

if (indexedFeatureIndex < indexedFeaturesSize) {
IntSet placedFeatureIndexMappings = new IntArraySet();

for (RegistryEntry<Biome> biomeRegistryEntry : biomeRegistryEntries) {
List<RegistryEntryList<PlacedFeature>> placedFeatureRegistryEntries = this.generationSettingsGetter.apply(
biomeRegistryEntry).getFeatures();
if (indexedFeatureIndex < placedFeatureRegistryEntries.size()) {
RegistryEntryList<PlacedFeature> registryEntryList = placedFeatureRegistryEntries.get(indexedFeatureIndex);
PlacedFeatureIndexer.IndexedFeatures indexedFeature = indexedFeatures.get(indexedFeatureIndex);
registryEntryList.stream().map(RegistryEntry::value).forEach(
(placedFeature) -> placedFeatureIndexMappings.add(
indexedFeature.indexMapping().applyAsInt(placedFeature)));
}
}

int placedFeatureIndexMappingsSize = placedFeatureIndexMappings.size();
int[] placedFeatureIndexMappingsArray = placedFeatureIndexMappings.toIntArray();
Arrays.sort(placedFeatureIndexMappingsArray);
PlacedFeatureIndexer.IndexedFeatures indexedFeature = indexedFeatures.get(indexedFeatureIndex);

for (int o = 0; o < placedFeatureIndexMappingsSize; ++o) {
int p = placedFeatureIndexMappingsArray[o];
PlacedFeature placedFeature = indexedFeature.features().get(p);
Supplier<String> currentlyGeneratingStructureNameSupplier = () -> {
var placedFeatureNameOptional = placedFeatureRegistry.getKey(placedFeature).map(Object::toString);
Objects.requireNonNull(placedFeature);
return placedFeatureNameOptional.orElseGet(placedFeature::toString);
};
chunkRandom.setDecoratorSeed(l, p, indexedFeatureIndex);

try {
world.setCurrentlyGeneratingStructureName(currentlyGeneratingStructureNameSupplier);
//noinspection DataFlowIssue
placedFeature.generate(world, (ChunkGenerator) (Object) this, chunkRandom, blockPos);
} catch (Exception e) {
CrashReport crashReport = CrashReport.create(e, "Feature placement");
crashReportSection = crashReport.addElement("Feature");
Objects.requireNonNull(currentlyGeneratingStructureNameSupplier);
crashReportSection.add("Description", currentlyGeneratingStructureNameSupplier::get);
throw new CrashException(crashReport);
}
}
}
}

world.setCurrentlyGeneratingStructureName(null);
} catch (Exception var31) {
CrashReport crashReport3 = CrashReport.create(var31, "Biome decoration");
crashReport3.addElement("Generation").add("CenterX", chunkPos.x).add("CenterZ", chunkPos.z).add("Seed", l);
throw new CrashException(crashReport3);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import net.minecraft.util.math.ChunkPos;
import net.minecraft.util.math.ChunkSectionPos;
import net.minecraft.util.math.Vec3d;
import net.minecraft.world.World;
import net.minecraft.world.chunk.WorldChunk;
import org.jetbrains.annotations.NotNull;

Expand All @@ -35,8 +36,15 @@ public NoisiumServerPlayerChunkLoader() {
PlayerEvent.PLAYER_JOIN.register(player -> previousPlayerPositions.put(player.getId(), player.getPos()));
PlayerEvent.PLAYER_QUIT.register(player -> previousPlayerPositions.remove(player.getId()));
TickEvent.ServerLevelTick.SERVER_LEVEL_POST.register(
instance -> tick(
instance, ((NoisiumServerWorldExtension) instance).noisium$getServerWorldChunkManager()::getChunksInRadiusAsync));
instance -> {
// DEBUG
if (instance.getRegistryKey() != World.OVERWORLD) {
return;
}

tick(
instance, ((NoisiumServerWorldExtension) instance).noisium$getServerWorldChunkManager()::getChunksInRadiusAsync);
});
}

// TODO: Enable ticking/update chunk tracking in ServerEntityManager
Expand All @@ -51,7 +59,7 @@ private void tick(@NotNull ServerWorld serverWorld, @NotNull BiFunction<ChunkPos
var player = players.get(i);
var playerBlockPos = player.getBlockPos();
if (!playerBlockPos.isWithinDistance(previousPlayerPositions.get(player.getId()), 16d)) {
var worldChunks = worldChunksSupplier.apply(new ChunkPos(playerBlockPos), 2);
var worldChunks = worldChunksSupplier.apply(new ChunkPos(playerBlockPos), 6);

ChunkUtil.sendWorldChunksToPlayerAsync(serverWorld, new ArrayList<>(worldChunks.values()));
CompletableFuture.runAsync(() -> player.networkHandler.sendPacket(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,13 @@ public NoisiumServerWorldChunkManager(@NotNull ServerWorld serverWorld, @NotNull

NoisiumServerChunkEvent.LIGHT_UPDATE.register(this::onLightUpdateAsync);
NoisiumServerChunkEvent.BLOCK_CHANGE.register(this::onBlockChange);
TickEvent.SERVER_LEVEL_POST.register(instance -> ((ServerLightingProvider) serverWorld.getLightingProvider()).tick());
TickEvent.SERVER_LEVEL_POST.register(instance -> {
if (!instance.equals(serverWorld)) {
return;
}

((ServerLightingProvider) serverWorld.getLightingProvider()).tick();
});
}

/**
Expand Down Expand Up @@ -317,7 +323,7 @@ private void onBlockChange(@NotNull BlockPos blockPos, @NotNull BlockState oldBl

protoChunk.setStatus(ChunkStatus.CARVERS);
chunkGenerator.carve(
chunkRegion, serverWorld.getSeed(), noiseConfig, chunkRegion.getBiomeAccess(), chunkRegionStructureAccessor, protoChunk,
chunkRegion, chunkRegion.getSeed(), noiseConfig, chunkRegion.getBiomeAccess(), chunkRegionStructureAccessor, protoChunk,
GenerationStep.Carver.AIR
);

Expand Down
1 change: 1 addition & 0 deletions common/src/main/resources/noisium-common.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"server.world.ServerEntityManagerMixin",
"server.world.ServerWorldMixin",
"server.world.ThreadedAnvilChunkStorageMixin",
"world.ChunkRegionMixin",
"world.WorldMixin",
"world.chunk.WorldChunkMixin",
"world.gen.chunk.ChunkGeneratorMixin",
Expand Down

0 comments on commit a537247

Please sign in to comment.