diff --git a/src/main/java/com/altnoir/poopsky/mixin/CreateWorldScreenWorldTabMixin.java b/src/main/java/com/altnoir/poopsky/mixin/CreateWorldScreenWorldTabMixin.java new file mode 100644 index 00000000..473b6866 --- /dev/null +++ b/src/main/java/com/altnoir/poopsky/mixin/CreateWorldScreenWorldTabMixin.java @@ -0,0 +1,30 @@ +package com.altnoir.poopsky.mixin; + +import com.altnoir.poopsky.util.ClientUtil; +import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArg; + +import java.util.function.BooleanSupplier; + +@Mixin(targets = "net.minecraft.client.gui.screens.worldselection.CreateWorldScreen$WorldTab") +public class CreateWorldScreenWorldTabMixin { + @Shadow + @Final + private CreateWorldScreen this$0; + + @ModifyArg( + method = "", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/client/gui/screens/worldselection/SwitchGrid$SwitchBuilder;withIsActiveCondition(Ljava/util/function/BooleanSupplier;)Lnet/minecraft/client/gui/screens/worldselection/SwitchGrid$SwitchBuilder;", + ordinal = 1 + ) + ) + private BooleanSupplier poopsky$disableBonusChestForPoopSky(BooleanSupplier original) { + return () -> original.getAsBoolean() && !ClientUtil.isPoopSkyWorldType(this.this$0.getUiState()); + } +} diff --git a/src/main/java/com/altnoir/poopsky/mixin/WorldCreationUiStateMixin.java b/src/main/java/com/altnoir/poopsky/mixin/WorldCreationUiStateMixin.java new file mode 100644 index 00000000..3fb092c5 --- /dev/null +++ b/src/main/java/com/altnoir/poopsky/mixin/WorldCreationUiStateMixin.java @@ -0,0 +1,38 @@ +package com.altnoir.poopsky.mixin; + +import com.altnoir.poopsky.util.ClientUtil; +import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState; +import org.spongepowered.asm.mixin.Mixin; +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 org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(WorldCreationUiState.class) +public abstract class WorldCreationUiStateMixin { + @Shadow + private boolean bonusChest; + + @Inject(method = "setBonusChest", at = @At("HEAD"), cancellable = true) + private void poopsky$preventBonusChestForPoopSky(boolean bonusChest, CallbackInfo ci) { + if (bonusChest && ClientUtil.isPoopSkyWorldType((WorldCreationUiState)(Object)this)) { + this.bonusChest = false; + ci.cancel(); + } + } + + @Inject(method = "setWorldType", at = @At("HEAD")) + private void poopsky$clearBonusChestForPoopSky(WorldCreationUiState.WorldTypeEntry worldType, CallbackInfo ci) { + if (ClientUtil.isPoopSkyWorldType(worldType)) { + this.bonusChest = false; + } + } + + @Inject(method = "isBonusChest", at = @At("HEAD"), cancellable = true) + private void poopsky$hideBonusChestForPoopSky(CallbackInfoReturnable cir) { + if (ClientUtil.isPoopSkyWorldType((WorldCreationUiState)(Object)this)) { + cir.setReturnValue(false); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/altnoir/poopsky/util/ClientUtil.java b/src/main/java/com/altnoir/poopsky/util/ClientUtil.java index 00613f39..a2723015 100644 --- a/src/main/java/com/altnoir/poopsky/util/ClientUtil.java +++ b/src/main/java/com/altnoir/poopsky/util/ClientUtil.java @@ -1,10 +1,12 @@ package com.altnoir.poopsky.util; +import com.altnoir.poopsky.util.asm.ASMHooks; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.vertex.*; import com.mojang.math.Axis; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState; import net.minecraft.client.renderer.ItemBlockRenderTypes; import net.minecraft.client.renderer.MultiBufferSource; import net.minecraft.client.renderer.RenderType; @@ -25,6 +27,8 @@ import org.joml.Matrix4fStack; import org.joml.Vector3f; +import java.util.Optional; + public class ClientUtil { private static final FluidState EMPTY = Fluids.EMPTY.defaultFluidState(); private static final BlockState AIR = Blocks.AIR.defaultBlockState(); @@ -143,4 +147,15 @@ public int getMinBuildHeight() { return 0; } } + + public static boolean isPoopSkyWorldType(WorldCreationUiState uiState) { + return isPoopSkyWorldType(uiState.getWorldType()); + } + + public static boolean isPoopSkyWorldType(WorldCreationUiState.WorldTypeEntry worldType) { + return Optional.ofNullable(worldType.preset()) + .flatMap(holder -> holder.unwrapKey()) + .filter(ASMHooks.POOPSKY::equals) + .isPresent(); + } } diff --git a/src/main/java/com/altnoir/poopsky/worldgen/PSVoidChunkGenerator.java b/src/main/java/com/altnoir/poopsky/worldgen/PSVoidChunkGenerator.java index f289ca30..7d90dac1 100644 --- a/src/main/java/com/altnoir/poopsky/worldgen/PSVoidChunkGenerator.java +++ b/src/main/java/com/altnoir/poopsky/worldgen/PSVoidChunkGenerator.java @@ -1,52 +1,113 @@ package com.altnoir.poopsky.worldgen; import com.altnoir.poopsky.Config; +import com.mojang.datafixers.util.Either; +import com.mojang.serialization.Codec; import com.mojang.serialization.MapCodec; import com.mojang.serialization.codecs.RecordCodecBuilder; -import net.minecraft.core.*; +import net.minecraft.CrashReport; +import net.minecraft.ReportedException; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.SectionPos; import net.minecraft.core.registries.Registries; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.server.level.WorldGenRegion; import net.minecraft.tags.TagKey; +import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.LevelHeightAccessor; import net.minecraft.world.level.NoiseColumn; import net.minecraft.world.level.StructureManager; import net.minecraft.world.level.WorldGenLevel; import net.minecraft.world.level.biome.BiomeManager; import net.minecraft.world.level.biome.BiomeSource; +import net.minecraft.world.level.block.Blocks; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.ChunkGenerator; import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; -import net.minecraft.world.level.levelgen.*; +import net.minecraft.world.level.levelgen.GenerationStep; +import net.minecraft.world.level.levelgen.Heightmap; +import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator; +import net.minecraft.world.level.levelgen.NoiseGeneratorSettings; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.RandomSupport; +import net.minecraft.world.level.levelgen.WorldgenRandom; +import net.minecraft.world.level.levelgen.XoroshiroRandomSource; import net.minecraft.world.level.levelgen.blending.Blender; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; import net.minecraft.world.level.levelgen.structure.StructureSet; +import net.minecraft.world.level.levelgen.structure.StructureStart; import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.CompletableFuture; -import java.util.stream.Stream; +import java.util.function.Supplier; +import java.util.stream.Collectors; public class PSVoidChunkGenerator extends NoiseBasedChunkGenerator { - public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( - BiomeSource.CODEC.fieldOf("biome_source").forGetter(generator -> generator.biomeSource), - NoiseGeneratorSettings.CODEC.fieldOf("settings").forGetter(generator -> generator.settings), - TagKey.codec(Registries.STRUCTURE_SET).fieldOf("allowed_structure_sets").forGetter(generator -> generator.allowedStructureSets) - ).apply(instance, instance.stable(PSVoidChunkGenerator::new))); + private static final int VIRTUAL_SURFACE_Y = 64; + + private static final Codec>> STRUCTURE_SET_KEYS_CODEC = + Codec.either(ResourceLocation.CODEC.listOf(), ResourceLocation.CODEC).xmap( + either -> either.map( + locations -> locations.stream() + .map(location -> ResourceKey.create(Registries.STRUCTURE_SET, location)) + .toList(), + location -> List.of(ResourceKey.create(Registries.STRUCTURE_SET, location)) + ), + keys -> keys.size() == 1 + ? Either.right(keys.getFirst().location()) + : Either.left(keys.stream().map(ResourceKey::location).toList()) + ); + + private static final Codec ALLOWED_STRUCTURE_SETS_CODEC = + Codec.either( + TagKey.hashedCodec(Registries.STRUCTURE_SET), + STRUCTURE_SET_KEYS_CODEC + ).xmap( + either -> either.map( + AllowedStructureSets::ofTag, + AllowedStructureSets::ofKeys + ), + allowed -> allowed.tag() + ., List>>>map(Either::left) + .orElseGet(() -> Either.right(allowed.keys())) + ); + + public static final MapCodec CODEC = + RecordCodecBuilder.mapCodec(instance -> instance.group( + BiomeSource.CODEC.fieldOf("biome_source") + .forGetter(generator -> generator.biomeSource), + NoiseGeneratorSettings.CODEC.fieldOf("settings") + .forGetter(generator -> generator.settings), + ALLOWED_STRUCTURE_SETS_CODEC.optionalFieldOf("allowed_structure_sets") + .forGetter(generator -> generator.allowedStructureSets) + ).apply(instance, instance.stable(PSVoidChunkGenerator::new))); private final Holder settings; - private final TagKey allowedStructureSets; + private final Optional allowedStructureSets; private final boolean generateNormal; - private final boolean allowBiomeDecoration; - public PSVoidChunkGenerator(BiomeSource biomeSource, Holder settings, TagKey allowedStructureSets) { + public PSVoidChunkGenerator(BiomeSource biomeSource, Holder settings, Optional allowedStructureSets) { super(biomeSource, settings); this.settings = settings; this.allowedStructureSets = allowedStructureSets; - this.generateNormal = (settings.is(ResourceLocation.parse("minecraft:nether")) && !Config.voidNetherGeneration); - this.allowBiomeDecoration = !settings.is(ResourceLocation.parse("minecraft:overworld")); + this.generateNormal = settings.is(ResourceLocation.parse("minecraft:nether")) && !Config.voidNetherGeneration; + } + + public PSVoidChunkGenerator(BiomeSource biomeSource, Holder settings) { + this(biomeSource, settings, Optional.empty()); } @Override @@ -54,103 +115,238 @@ protected MapCodec codec() { return CODEC; } - @Override - public void applyCarvers(WorldGenRegion level, long seed, RandomState randomState, BiomeManager biomeManager, StructureManager structureManager, ChunkAccess chunk, GenerationStep.Carving step) { - if (this.generateNormal) { - super.applyCarvers(level, seed, randomState, biomeManager, structureManager, chunk, step); + private record AllowedStructureSets(Optional> tag, List> keys) { + private static AllowedStructureSets ofTag(TagKey tag) { + return new AllowedStructureSets(Optional.of(tag), List.of()); } - } - @Override - public ChunkGeneratorStructureState createState(HolderLookup lookup, RandomState randomState, long seed) { - return this.generateNormal - ? super.createState(lookup, randomState, seed) - : super.createState(new FilteredLookup(lookup, this.allowedStructureSets), randomState, seed); + private static AllowedStructureSets ofKeys(List> keys) { + return new AllowedStructureSets(Optional.empty(), List.copyOf(keys)); + } + + private boolean contains(Holder holder) { + return tag.map(holder::is) + .orElseGet(() -> holder.unwrapKey().filter(keys::contains).isPresent()); + } } @Override - public void buildSurface(WorldGenRegion level, StructureManager structureManager, RandomState randomState, ChunkAccess chunk) { - if (this.generateNormal) { - super.buildSurface(level, structureManager, randomState, chunk); + public void applyCarvers( + WorldGenRegion level, + long seed, + RandomState randomState, + BiomeManager biomeManager, + StructureManager structureManager, + ChunkAccess chunk, + GenerationStep.Carving step + ) { + if (generateNormal) { + super.applyCarvers( level, seed, randomState, biomeManager, structureManager, chunk, step); } } @Override - public void spawnOriginalMobs(WorldGenRegion level) { - if (this.generateNormal) { - super.spawnOriginalMobs(level); + public void buildSurface(WorldGenRegion level, StructureManager structureManager, RandomState randomState, ChunkAccess chunk) { + if (generateNormal) { + super.buildSurface(level, structureManager, randomState, chunk); } } @Override public CompletableFuture fillFromNoise(Blender blender, RandomState randomState, StructureManager structureManager, ChunkAccess chunk) { - return this.generateNormal + return generateNormal ? super.fillFromNoise(blender, randomState, structureManager, chunk) : CompletableFuture.completedFuture(chunk); } @Override public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor level, RandomState randomState) { - return this.generateNormal ? super.getBaseHeight(x, z, type, level, randomState) : getMinY(); + return generateNormal + ? super.getBaseHeight(x, z, type, level, randomState) + : virtualSurfaceY(level); } @Override - public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor heightAccessor, RandomState randomState) { - return this.generateNormal ? super.getBaseColumn(x, z, heightAccessor, randomState) : new NoiseColumn(0, new BlockState[0]); + public NoiseColumn getBaseColumn(int x, int z, LevelHeightAccessor level, RandomState randomState) { + if (generateNormal) { + return super.getBaseColumn(x, z, level, randomState); + } + + int minY = level.getMinBuildHeight(); + int surfaceY = virtualSurfaceY(level); + BlockState[] states = new BlockState[Math.max(0, surfaceY - minY)]; + + for (int index = 0; index < states.length; index++) { + int y = minY + index; + states[index] = y == minY + ? Blocks.BEDROCK.defaultBlockState() + : y == surfaceY - 1 + ? Blocks.GRASS_BLOCK.defaultBlockState() + : Blocks.DIRT.defaultBlockState(); + } + + return new NoiseColumn(minY, states); } @Override - public void addDebugScreenInfo(List info, RandomState randomState, BlockPos pos) { - if (this.generateNormal) { - super.addDebugScreenInfo(info, randomState, pos); + public void createStructures( + RegistryAccess registries, + ChunkGeneratorStructureState structureState, + StructureManager structureManager, + ChunkAccess chunk, + StructureTemplateManager templateManager + ) { + super.createStructures(registries, structureState, structureManager, chunk, templateManager); + + if (allowedStructureSets.isEmpty()) { + return; } + + Set allowedStructures = resolveAllowedStructures(registries); + Map filteredStarts = new HashMap<>(); + + chunk.getAllStarts().forEach((structure, start) -> { + if (allowedStructures.contains(structure)) { + filteredStarts.put(structure, start); + } + }); + + chunk.setAllStarts(filteredStarts); } @Override - public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) { - if (this.generateNormal || this.allowBiomeDecoration) { + public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager + ) { + if (generateNormal) { super.applyBiomeDecoration(level, chunk, structureManager); + } else { + placeStructuresOnly(level, chunk, structureManager); } } @Override - public void createReferences(WorldGenLevel level, StructureManager structureManager, ChunkAccess chunk) { - if (this.generateNormal || hasStructures(level.registryAccess())) { - super.createReferences(level, structureManager, chunk); + public void spawnOriginalMobs(WorldGenRegion level) { + if (generateNormal) { + super.spawnOriginalMobs(level); } } @Override - public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState structureState, StructureManager structureManager, ChunkAccess chunk, StructureTemplateManager templateManager) { - if (this.generateNormal || hasStructures(registries)) { - super.createStructures(registries, structureState, structureManager, chunk, templateManager); + public void addDebugScreenInfo(List info, RandomState randomState, BlockPos pos) { + if (generateNormal) { + super.addDebugScreenInfo(info, randomState, pos); } } - private boolean hasStructures(RegistryAccess registries) { - return registries.registryOrThrow(Registries.STRUCTURE_SET).getTagOrEmpty(this.allowedStructureSets).iterator().hasNext(); + private Set resolveAllowedStructures(RegistryAccess registries) { + if (allowedStructureSets.isEmpty()) { + return Set.of(); + } + + AllowedStructureSets allowed = allowedStructureSets.get(); + Registry registry = registries.registryOrThrow(Registries.STRUCTURE_SET); + + return registry.holders() + .filter(allowed::contains) + .flatMap(holder -> holder.value().structures().stream()) + .map(StructureSet.StructureSelectionEntry::structure) + .map(Holder::value) + .collect(Collectors.toUnmodifiableSet()); } - private record FilteredLookup(HolderLookup parent, - TagKey allowedValues) implements HolderLookup { - @Override - public Optional> get(ResourceKey key) { - return this.parent.get(key).filter(holder -> holder.is(this.allowedValues)); - } + private void placeStructuresOnly( WorldGenLevel level, ChunkAccess chunk, StructureManager structureManager) { + ChunkPos chunkPos = chunk.getPos(); - @Override - public Optional> get(TagKey tagKey) { - return this.parent.get(tagKey); + if (SharedConstants.debugVoidTerrain(chunkPos) || !structureManager.shouldGenerateStructures()) { + return; } - @Override - public Stream> listElements() { - return this.parent.listElements().filter(holder -> holder.is(this.allowedValues)); - } + SectionPos sectionPos = SectionPos.bottomOf(chunk); + BlockPos origin = sectionPos.origin(); + RegistryAccess registries = level.registryAccess(); + Registry structureRegistry = registries.registryOrThrow(Registries.STRUCTURE); + + Set allowedStructures = allowedStructureSets.isPresent() + ? resolveAllowedStructures(registries) + : null; + + Map> structuresByStep = + structureRegistry.stream() + .filter(structure -> allowedStructures == null || allowedStructures.contains(structure)) + .collect(Collectors.groupingBy(structure -> structure.step().ordinal())); + + WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); + + long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ()); - @Override - public Stream> listTags() { - return this.parent.listTags(); + try { + for (int step = 0; step < GenerationStep.Decoration.values().length; step++) { + List structures = structuresByStep.getOrDefault(step, Collections.emptyList()); + + for (int index = 0; index < structures.size(); index++) { + Structure structure = structures.get(index); + random.setFeatureSeed(decorationSeed, index, step); + + Supplier name = () -> + structureRegistry.getResourceKey(structure) + .map(Object::toString) + .orElseGet(structure::toString); + + try { + level.setCurrentlyGenerating(name); + + structureManager.startsForStructure(sectionPos, structure) + .stream() + .filter(StructureStart::isValid) + .forEach(start -> start.placeInChunk( + level, + structureManager, + this, + random, + writableArea(chunk), + chunkPos + )); + } catch (Exception exception) { + CrashReport report = CrashReport.forThrowable(exception, "Structure placement"); + + report.addCategory("Structure") + .setDetail("Description", name::get); + + throw new ReportedException(report); + } + } + } + } catch (ReportedException exception) { + throw exception; + } catch (Exception exception) { + CrashReport report = CrashReport.forThrowable(exception,"Void biome decoration"); + + report.addCategory("Generation") + .setDetail("CenterX", chunkPos.x) + .setDetail("CenterZ", chunkPos.z) + .setDetail("Decoration Seed", decorationSeed); + + throw new ReportedException(report); + } finally { + level.setCurrentlyGenerating(null); } } -} + + private static BoundingBox writableArea(ChunkAccess chunk) { + ChunkPos pos = chunk.getPos(); + LevelHeightAccessor height = chunk.getHeightAccessorForGeneration(); + + return new BoundingBox( + pos.getMinBlockX(), + height.getMinBuildHeight() + 1, + pos.getMinBlockZ(), + pos.getMaxBlockX(), + height.getMaxBuildHeight() - 1, + pos.getMaxBlockZ() + ); + } + + private static int virtualSurfaceY(LevelHeightAccessor level) { + return Math.clamp(VIRTUAL_SURFACE_Y, level.getMinBuildHeight() + 1, level.getMaxBuildHeight()); + } +} \ No newline at end of file diff --git a/src/main/resources/data/poopsky/worldgen/world_preset/poopsky.json b/src/main/resources/data/poopsky/worldgen/world_preset/poopsky.json index 9d025cfa..60c53725 100644 --- a/src/main/resources/data/poopsky/worldgen/world_preset/poopsky.json +++ b/src/main/resources/data/poopsky/worldgen/world_preset/poopsky.json @@ -9,7 +9,10 @@ "preset": "minecraft:overworld" }, "settings": "minecraft:overworld", - "allowed_structure_sets": "poopsky:overworld_void_structure_sets" + "allowed_structure_sets": [ + "minecraft:villages", + "minecraft:strongholds" + ] } }, "minecraft:the_end": { @@ -30,8 +33,7 @@ "type": "minecraft:multi_noise", "preset": "minecraft:nether" }, - "settings": "minecraft:nether", - "allowed_structure_sets": "poopsky:the_nether_void_structure_sets" + "settings": "minecraft:nether" } } } diff --git a/src/main/resources/poopsky.mixins.json b/src/main/resources/poopsky.mixins.json index 28a6b942..3c391dfa 100644 --- a/src/main/resources/poopsky.mixins.json +++ b/src/main/resources/poopsky.mixins.json @@ -12,7 +12,9 @@ "VillagerMixin" ], "client": [ - "ClientPacketListenerMixin" + "ClientPacketListenerMixin", + "CreateWorldScreenWorldTabMixin", + "WorldCreationUiStateMixin" ], "injectors": { "defaultRequire": 1