From c55772811b1cf47d6a6cafae81cc00dc127cd1d6 Mon Sep 17 00:00:00 2001 From: zljtt <52527069+zljtt@users.noreply.github.com> Date: Mon, 24 Aug 2026 04:34:43 +0800 Subject: [PATCH 1/3] feat: generate abyss wall windmills --- .../resources/assets/mia/lang/en_us.json | 1 + .../worldgen/biome/has_abyss_windmill.json | 6 + .../worldgen/structure/abyss_windmill.json | 20 ++ .../structure_set/abyss_windmills.json | 23 ++ .../abyss_windmill/straight.json | 16 ++ .../template_pool/abyss_windmill/tilt.json | 16 ++ src/main/java/com/altnoir/mia/MIA.java | 1 + .../java/com/altnoir/mia/MiaGameTests.java | 175 +++++++++++++ .../mia/datagen/MiaBiomeTagsProvider.java | 3 + .../altnoir/mia/datagen/MiaLangProvider.java | 2 + .../java/com/altnoir/mia/init/MiaTags.java | 1 + .../worldgen/MiaStructurePlacementTypes.java | 24 ++ .../mia/init/worldgen/MiaStructureTypes.java | 3 + .../worldgen/structure/MiaStructureSets.java | 10 + .../mia/worldgen/structure/MiaStructures.java | 17 ++ .../structure/pools/AbyssWindmillPools.java | 35 +++ .../worldgen/structure/pools/MiaPools.java | 1 + .../structure/wall/AbyssWallCandidate.java | 23 ++ .../structure/wall/AbyssWallPlanConfig.java | 49 ++++ .../structure/wall/AbyssWallPlanner.java | 238 ++++++++++++++++++ .../wall/AbyssWallStructurePlacement.java | 57 +++++ .../wall/AbyssWindmillStructure.java | 152 +++++++++++ .../mia/structure/abyss_windmill_straight.nbt | Bin 0 -> 43544 bytes .../mia/structure/abyss_windmill_tilt.nbt | Bin 0 -> 32389 bytes 24 files changed, 873 insertions(+) create mode 100644 src/generated/resources/data/mia/tags/worldgen/biome/has_abyss_windmill.json create mode 100644 src/generated/resources/data/mia/worldgen/structure/abyss_windmill.json create mode 100644 src/generated/resources/data/mia/worldgen/structure_set/abyss_windmills.json create mode 100644 src/generated/resources/data/mia/worldgen/template_pool/abyss_windmill/straight.json create mode 100644 src/generated/resources/data/mia/worldgen/template_pool/abyss_windmill/tilt.json create mode 100644 src/main/java/com/altnoir/mia/init/worldgen/MiaStructurePlacementTypes.java create mode 100644 src/main/java/com/altnoir/mia/worldgen/structure/pools/AbyssWindmillPools.java create mode 100644 src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallCandidate.java create mode 100644 src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanConfig.java create mode 100644 src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanner.java create mode 100644 src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallStructurePlacement.java create mode 100644 src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWindmillStructure.java create mode 100644 src/main/resources/data/mia/structure/abyss_windmill_straight.nbt create mode 100644 src/main/resources/data/mia/structure/abyss_windmill_tilt.nbt diff --git a/src/generated/resources/assets/mia/lang/en_us.json b/src/generated/resources/assets/mia/lang/en_us.json index 2b51f3e6..c4b11cee 100644 --- a/src/generated/resources/assets/mia/lang/en_us.json +++ b/src/generated/resources/assets/mia/lang/en_us.json @@ -174,6 +174,7 @@ "block.mia.verdant_stairs": "Verdant Stairs", "block.mia.verdant_stem": "Verdant Stem", "block.mia.verdant_trapdoor": "Verdant Trapdoor", + "config.jade.plugin_mia.spawner_cooldown": "Spawner Cooldown", "container.mia.artifact.smithing.title": "Artifact Smithing Table", "curios.identifier.whistle": "Whistle", "curios.modifiers.whistle": "Whistle Attributes", diff --git a/src/generated/resources/data/mia/tags/worldgen/biome/has_abyss_windmill.json b/src/generated/resources/data/mia/tags/worldgen/biome/has_abyss_windmill.json new file mode 100644 index 00000000..0149a88e --- /dev/null +++ b/src/generated/resources/data/mia/tags/worldgen/biome/has_abyss_windmill.json @@ -0,0 +1,6 @@ +{ + "values": [ + "mia:the_abyss/the_abyss", + "mia:the_abyss/inverted_forest" + ] +} \ No newline at end of file diff --git a/src/generated/resources/data/mia/worldgen/structure/abyss_windmill.json b/src/generated/resources/data/mia/worldgen/structure/abyss_windmill.json new file mode 100644 index 00000000..77521188 --- /dev/null +++ b/src/generated/resources/data/mia/worldgen/structure/abyss_windmill.json @@ -0,0 +1,20 @@ +{ + "type": "mia:abyss_wall_template", + "anchor_name": "mia:abyss_wall_anchor", + "biomes": "#mia:has_abyss_windmill", + "plan": { + "attempts_per_band": 64, + "candidate_count": 20, + "contract_version": 2, + "embed_depth": 3, + "max_correction": 16, + "max_y": 125, + "min_anchor_distance": 32, + "min_y": -96, + "salt": 1296646487 + }, + "spawn_overrides": {}, + "step": "underground_structures", + "straight_pool": "mia:abyss_windmill/straight", + "tilt_pool": "mia:abyss_windmill/tilt" +} diff --git a/src/generated/resources/data/mia/worldgen/structure_set/abyss_windmills.json b/src/generated/resources/data/mia/worldgen/structure_set/abyss_windmills.json new file mode 100644 index 00000000..fab5ff85 --- /dev/null +++ b/src/generated/resources/data/mia/worldgen/structure_set/abyss_windmills.json @@ -0,0 +1,23 @@ +{ + "placement": { + "type": "mia:abyss_wall", + "plan": { + "attempts_per_band": 64, + "candidate_count": 20, + "contract_version": 2, + "embed_depth": 3, + "max_correction": 16, + "max_y": 125, + "min_anchor_distance": 32, + "min_y": -96, + "salt": 1296646487 + }, + "salt": 1296646487 + }, + "structures": [ + { + "structure": "mia:abyss_windmill", + "weight": 1 + } + ] +} diff --git a/src/generated/resources/data/mia/worldgen/template_pool/abyss_windmill/straight.json b/src/generated/resources/data/mia/worldgen/template_pool/abyss_windmill/straight.json new file mode 100644 index 00000000..385d061c --- /dev/null +++ b/src/generated/resources/data/mia/worldgen/template_pool/abyss_windmill/straight.json @@ -0,0 +1,16 @@ +{ + "elements": [ + { + "element": { + "element_type": "minecraft:single_pool_element", + "location": "mia:abyss_windmill_straight", + "processors": { + "processors": [] + }, + "projection": "rigid" + }, + "weight": 1 + } + ], + "fallback": "minecraft:empty" +} \ No newline at end of file diff --git a/src/generated/resources/data/mia/worldgen/template_pool/abyss_windmill/tilt.json b/src/generated/resources/data/mia/worldgen/template_pool/abyss_windmill/tilt.json new file mode 100644 index 00000000..e97f46fd --- /dev/null +++ b/src/generated/resources/data/mia/worldgen/template_pool/abyss_windmill/tilt.json @@ -0,0 +1,16 @@ +{ + "elements": [ + { + "element": { + "element_type": "minecraft:single_pool_element", + "location": "mia:abyss_windmill_tilt", + "processors": { + "processors": [] + }, + "projection": "rigid" + }, + "weight": 1 + } + ], + "fallback": "minecraft:empty" +} \ No newline at end of file diff --git a/src/main/java/com/altnoir/mia/MIA.java b/src/main/java/com/altnoir/mia/MIA.java index b34be87a..1311ce97 100644 --- a/src/main/java/com/altnoir/mia/MIA.java +++ b/src/main/java/com/altnoir/mia/MIA.java @@ -45,6 +45,7 @@ public MIA(IEventBus modEventBus, ModContainer modContainer) { MiaDensityFunctionTypes.register(modEventBus); MiaFeatures.register(modEventBus); MiaPlacements.register(modEventBus); + MiaStructurePlacementTypes.register(modEventBus); MiaStructureTypes.register(modEventBus); MiaFoliagePlacerTypes.register(modEventBus); MiaTrunkPlacerTypes.register(modEventBus); diff --git a/src/main/java/com/altnoir/mia/MiaGameTests.java b/src/main/java/com/altnoir/mia/MiaGameTests.java index 84852b36..b16f5766 100644 --- a/src/main/java/com/altnoir/mia/MiaGameTests.java +++ b/src/main/java/com/altnoir/mia/MiaGameTests.java @@ -1,10 +1,32 @@ package com.altnoir.mia; +import com.altnoir.mia.util.MiaUtil; +import com.altnoir.mia.worldgen.noise_setting.MiaNoiseGeneratorSettings; +import com.altnoir.mia.worldgen.structure.wall.AbyssWallCandidate; +import com.altnoir.mia.worldgen.structure.wall.AbyssWallPlanConfig; +import com.altnoir.mia.worldgen.structure.wall.AbyssWallPlanner; +import net.minecraft.core.BlockPos; +import net.minecraft.core.FrontAndTop; +import net.minecraft.core.Vec3i; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.JigsawBlock; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; import net.minecraft.gametest.framework.*; import net.neoforged.neoforge.event.RegisterGameTestsEvent; import net.neoforged.neoforge.gametest.GameTestHolder; import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; +import java.util.List; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.Set; +import java.util.stream.IntStream; +import java.util.stream.Collectors; + @GameTestHolder(MIA.MOD_ID) public class MiaGameTests { @@ -16,6 +38,159 @@ public static void exampleTest(GameTestHelper helper) { }); } + @PrefixGameTestTemplate(false) + @GameTest(template = "gametest/amethyst_lamptube") + public static void abyssWallPlanIsDeterministicSeparatedAndInRange(GameTestHelper helper) { + AbyssWallPlanConfig config = AbyssWallPlanConfig.DEFAULT; + AbyssWallPlanner.RadiusPredictor cylinder = (angle, y) -> OptionalDouble.of(200.0); + + List first = AbyssWallPlanner.createPlan(123456789L, config, cylinder); + List repeated = AbyssWallPlanner.createPlan(123456789L, config, cylinder); + List otherSeed = AbyssWallPlanner.createPlan(-987654321L, config, cylinder); + + helper.assertValueEqual(first, repeated, "same seed must reproduce the same wall plan"); + helper.assertFalse(first.equals(otherSeed), "different seeds must change the wall plan"); + helper.assertTrue(first.size() >= 16 && first.size() <= 20, + "default plan must retain approximately 16-20 candidates across its twenty height bands"); + long bufferedMinimumDistance = AbyssWallPlanConfig.DEFAULT.planningAnchorDistance(); + helper.assertValueEqual(AbyssWallPlanConfig.DEFAULT.maxWallAnchorOffset(), 3, + "the configured embedding value is the maximum radial offset"); + helper.assertValueEqual(bufferedMinimumDistance, 74L, + "planning distance reserves correction, embedding, and block rounding on both anchors"); + + for (int i = 0; i < first.size(); i++) { + AbyssWallCandidate candidate = first.get(i); + helper.assertTrue(candidate.y() >= -96 && candidate.y() <= 125, + "candidate Y must stay in the allowed windmill range"); + helper.assertTrue(candidate.y() < -3 || candidate.y() > 25, + "candidate Y must avoid the horizontal transition shelf from -3 through 25"); + for (int j = 0; j < i; j++) { + helper.assertTrue(candidate.anchor().distSqr(first.get(j).anchor()) + >= bufferedMinimumDistance * bufferedMinimumDistance, + "predicted anchors must reserve correction space around the final minimum separation"); + helper.assertFalse(candidate.startChunk().equals(first.get(j).startChunk()), + "one structure cannot own two candidates in the same start chunk"); + } + } + Set embedOffsets = first.stream() + .map(candidate -> AbyssWallPlanner.wallAnchorOffset(123456789L, candidate, config)) + .collect(Collectors.toSet()); + helper.assertTrue(embedOffsets.size() > 1, "deterministic embedding must vary between candidates"); + for (int offset : embedOffsets) { + helper.assertTrue(offset >= 0 && offset <= 3, "embedding must stay in the inclusive 0-3 range"); + } + for (AbyssWallCandidate candidate : first) { + helper.assertValueEqual( + AbyssWallPlanner.wallAnchorOffset(123456789L, candidate, config), + AbyssWallPlanner.wallAnchorOffset(123456789L, candidate, config), + "embedding must be deterministic for a seed and candidate" + ); + } + helper.succeed(); + } + + @PrefixGameTestTemplate(false) + @GameTest(template = "gametest/amethyst_lamptube") + public static void abyssWallDirectionsSelectStraightAndTiltRotations(GameTestHelper helper) { + helper.assertValueEqual(AbyssWallPlanner.orientationFor(Math.toRadians(90)), + new AbyssWallCandidate.Orientation(AbyssWallCandidate.TemplateKind.STRAIGHT, Rotation.NONE), + "south wall uses the authored straight orientation"); + helper.assertValueEqual(AbyssWallPlanner.orientationFor(Math.toRadians(45)), + new AbyssWallCandidate.Orientation(AbyssWallCandidate.TemplateKind.TILT, Rotation.NONE), + "south-east wall uses the authored tilt orientation"); + helper.assertValueEqual(AbyssWallPlanner.orientationFor(Math.toRadians(180)), + new AbyssWallCandidate.Orientation(AbyssWallCandidate.TemplateKind.STRAIGHT, Rotation.CLOCKWISE_90), + "west wall rotates the straight template clockwise"); + helper.assertValueEqual(AbyssWallPlanner.orientationFor(Math.toRadians(22.49)), + new AbyssWallCandidate.Orientation(AbyssWallCandidate.TemplateKind.STRAIGHT, Rotation.COUNTERCLOCKWISE_90), + "angle below the sector boundary remains straight"); + helper.assertValueEqual(AbyssWallPlanner.orientationFor(Math.toRadians(22.51)), + new AbyssWallCandidate.Orientation(AbyssWallCandidate.TemplateKind.TILT, Rotation.NONE), + "angle above the sector boundary switches to tilt"); + helper.succeed(); + } + + @PrefixGameTestTemplate(false) + @GameTest(template = "gametest/amethyst_lamptube") + public static void abyssWallRefinementRequiresStableAirToSolidBoundary(GameTestHelper helper) { + OptionalInt boundary = AbyssWallPlanner.findFirstStableBoundary(180, 230, radius -> radius >= 205); + OptionalInt missing = AbyssWallPlanner.findFirstStableBoundary(180, 230, radius -> false); + OptionalInt lowerWall = AbyssWallPlanner.findRefinedBoundary(96.0, 16, radius -> radius >= 94); + OptionalInt unrelatedOuterWall = AbyssWallPlanner.findRefinedBoundary(96.0, 16, radius -> radius >= 125); + + helper.assertValueEqual(boundary, OptionalInt.of(205), "first stable wall radius"); + helper.assertValueEqual(missing, OptionalInt.empty(), "missing wall must reject the candidate"); + helper.assertValueEqual(lowerWall, OptionalInt.of(94), "nearby lower-layer wall must refine the macro radius"); + helper.assertValueEqual(unrelatedOuterWall, OptionalInt.empty(), + "a perforated main wall must not fall through to a farther cave boundary"); + helper.succeed(); + } + + @PrefixGameTestTemplate(false) + @GameTest(template = "gametest/amethyst_lamptube", timeoutTicks = 200) + public static void abyssWallPlanFindsTheRealAbyssDensityWall(GameTestHelper helper) { + long seed = 8675309L; + var settings = helper.getLevel().registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS) + .getOrThrow(MiaNoiseGeneratorSettings.THE_ABYSS).value(); + var noises = helper.getLevel().registryAccess().lookupOrThrow(Registries.NOISE); + RandomState randomState = RandomState.create(settings, noises, seed); + List candidates = AbyssWallPlanner.planFor(seed, randomState, AbyssWallPlanConfig.DEFAULT); + + helper.assertTrue(candidates.size() >= 16 && candidates.size() <= 20, + "real Abyss wall plan must yield approximately 16-20 candidates"); + for (AbyssWallCandidate candidate : candidates) { + helper.assertTrue(candidate.y() <= 125, "real-density candidates above Y=125 must be rejected"); + helper.assertTrue(candidate.y() < -3 || candidate.y() > 25, + "real-density candidates must avoid the horizontal transition shelf"); + if (candidate.y() < -3) { + helper.assertValueEqual(candidate.predictedRadius(), 96.0, + "lower-layer candidates must target the macro wall instead of a farther final-density boundary"); + } + } + helper.assertValueEqual(candidates, AbyssWallPlanner.planFor(seed, randomState, AbyssWallPlanConfig.DEFAULT), + "cached real-density plan must be stable"); + + long concurrentSeed = -8675309L; + RandomState concurrentRandomState = RandomState.create(settings, noises, concurrentSeed); + List> concurrentPlans = IntStream.range(0, 16) + .parallel() + .mapToObj(ignored -> AbyssWallPlanner.planFor( + concurrentSeed, concurrentRandomState, AbyssWallPlanConfig.DEFAULT + )) + .toList(); + for (List concurrentPlan : concurrentPlans) { + helper.assertValueEqual(concurrentPlan, concurrentPlans.getFirst(), + "concurrent chunk queries must share one immutable plan"); + } + helper.succeed(); + } + + @PrefixGameTestTemplate(false) + @GameTest(template = "gametest/amethyst_lamptube") + public static void abyssWindmillTemplatesHaveOneReplacingAnchor(GameTestHelper helper) { + assertWindmillTemplate(helper, "abyss_windmill_straight", new Vec3i(27, 26, 36)); + assertWindmillTemplate(helper, "abyss_windmill_tilt", new Vec3i(26, 23, 25)); + helper.succeed(); + } + + private static void assertWindmillTemplate(GameTestHelper helper, String path, Vec3i expectedSize) { + StructureTemplate template = helper.getLevel().getStructureManager().get(MiaUtil.miaId(path)) + .orElseThrow(() -> new AssertionError("missing structure template mia:" + path)); + helper.assertValueEqual(template.getSize(), expectedSize, path + " template size"); + + List anchors = template + .filterBlocks(BlockPos.ZERO, new StructurePlaceSettings(), Blocks.JIGSAW) + .stream() + .filter(info -> info.nbt() != null + && "mia:abyss_wall_anchor".equals(info.nbt().getString("name"))) + .toList(); + helper.assertValueEqual(anchors.size(), 1, path + " must contain exactly one wall anchor"); + helper.assertValueEqual(anchors.getFirst().state().getValue(JigsawBlock.ORIENTATION), FrontAndTop.SOUTH_UP, + path + " anchor orientation"); + helper.assertValueEqual(anchors.getFirst().nbt().getString("final_state"), "mia:abyss_andesite", + path + " anchor final state"); + } + public static void register(RegisterGameTestsEvent event) { event.register(MiaGameTests.class); } diff --git a/src/main/java/com/altnoir/mia/datagen/MiaBiomeTagsProvider.java b/src/main/java/com/altnoir/mia/datagen/MiaBiomeTagsProvider.java index db870d36..a8a71eb3 100644 --- a/src/main/java/com/altnoir/mia/datagen/MiaBiomeTagsProvider.java +++ b/src/main/java/com/altnoir/mia/datagen/MiaBiomeTagsProvider.java @@ -32,5 +32,8 @@ protected void addTags(@NotNull HolderLookup.Provider provider) { .add(Biomes.SPARSE_JUNGLE) .add(Biomes.BAMBOO_JUNGLE) .add(Biomes.MANGROVE_SWAMP); + tag(MiaTags.Biomes.HAS_ABYSS_WINDMILL) + .add(MiaBiomes.THE_ABYSS) + .add(MiaBiomes.INVERTED_FOREST); } } diff --git a/src/main/java/com/altnoir/mia/datagen/MiaLangProvider.java b/src/main/java/com/altnoir/mia/datagen/MiaLangProvider.java index 0832936a..f36ab20d 100644 --- a/src/main/java/com/altnoir/mia/datagen/MiaLangProvider.java +++ b/src/main/java/com/altnoir/mia/datagen/MiaLangProvider.java @@ -28,6 +28,8 @@ protected void addTranslations() { addDefault("tooltip"); addDefault("ponder"); + add("config.jade.plugin_mia.spawner_cooldown", "Spawner Cooldown"); + MiaBiomes.BIOMES.forEach(biome -> { add("biome.mia." + biome.location().getPath(), formatName(biome)); }); diff --git a/src/main/java/com/altnoir/mia/init/MiaTags.java b/src/main/java/com/altnoir/mia/init/MiaTags.java index 6def4508..20c57295 100644 --- a/src/main/java/com/altnoir/mia/init/MiaTags.java +++ b/src/main/java/com/altnoir/mia/init/MiaTags.java @@ -52,6 +52,7 @@ public static class Biomes { public static final TagKey HAS_ISLAND = create("has_island"); public static final TagKey THE_ABYSS_CLEAR = create("the_abyss_clear"); public static final TagKey HAS_STAR_COMPASS_TEMPLE = create("has_star_compass_temple"); + public static final TagKey HAS_ABYSS_WINDMILL = create("has_abyss_windmill"); private static TagKey create(String name) { return TagKey.create(Registries.BIOME, MiaUtil.miaId(name)); diff --git a/src/main/java/com/altnoir/mia/init/worldgen/MiaStructurePlacementTypes.java b/src/main/java/com/altnoir/mia/init/worldgen/MiaStructurePlacementTypes.java new file mode 100644 index 00000000..29e23535 --- /dev/null +++ b/src/main/java/com/altnoir/mia/init/worldgen/MiaStructurePlacementTypes.java @@ -0,0 +1,24 @@ +package com.altnoir.mia.init.worldgen; + +import com.altnoir.mia.MIA; +import com.altnoir.mia.worldgen.structure.wall.AbyssWallStructurePlacement; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacementType; +import net.neoforged.bus.api.IEventBus; +import net.neoforged.neoforge.registries.DeferredHolder; +import net.neoforged.neoforge.registries.DeferredRegister; + +public final class MiaStructurePlacementTypes { + public static final DeferredRegister> STRUCTURE_PLACEMENT_TYPES = + DeferredRegister.create(BuiltInRegistries.STRUCTURE_PLACEMENT, MIA.MOD_ID); + + public static final DeferredHolder, StructurePlacementType> ABYSS_WALL = + STRUCTURE_PLACEMENT_TYPES.register("abyss_wall", () -> () -> AbyssWallStructurePlacement.CODEC); + + private MiaStructurePlacementTypes() { + } + + public static void register(IEventBus eventBus) { + STRUCTURE_PLACEMENT_TYPES.register(eventBus); + } +} diff --git a/src/main/java/com/altnoir/mia/init/worldgen/MiaStructureTypes.java b/src/main/java/com/altnoir/mia/init/worldgen/MiaStructureTypes.java index 00e2c657..6b27d8e5 100644 --- a/src/main/java/com/altnoir/mia/init/worldgen/MiaStructureTypes.java +++ b/src/main/java/com/altnoir/mia/init/worldgen/MiaStructureTypes.java @@ -2,6 +2,7 @@ import com.altnoir.mia.MIA; import com.altnoir.mia.worldgen.structure.MiaJigsawStructure; +import com.altnoir.mia.worldgen.structure.wall.AbyssWindmillStructure; import net.minecraft.core.registries.BuiltInRegistries; import net.minecraft.world.level.levelgen.structure.StructureType; import net.neoforged.bus.api.IEventBus; @@ -13,6 +14,8 @@ public class MiaStructureTypes { public static final DeferredHolder, StructureType> MIA_JIGSAW = STRUCTURE_TYPE.register("jigsaw", () -> () -> MiaJigsawStructure.CODEC); + public static final DeferredHolder, StructureType> ABYSS_WALL_TEMPLATE = + STRUCTURE_TYPE.register("abyss_wall_template", () -> () -> AbyssWindmillStructure.CODEC); public static void register(IEventBus eventBus) { STRUCTURE_TYPE.register(eventBus); diff --git a/src/main/java/com/altnoir/mia/worldgen/structure/MiaStructureSets.java b/src/main/java/com/altnoir/mia/worldgen/structure/MiaStructureSets.java index 1a1ae61a..1ee4cc6a 100644 --- a/src/main/java/com/altnoir/mia/worldgen/structure/MiaStructureSets.java +++ b/src/main/java/com/altnoir/mia/worldgen/structure/MiaStructureSets.java @@ -1,6 +1,8 @@ package com.altnoir.mia.worldgen.structure; import com.altnoir.mia.util.MiaUtil; +import com.altnoir.mia.worldgen.structure.wall.AbyssWallPlanConfig; +import com.altnoir.mia.worldgen.structure.wall.AbyssWallStructurePlacement; import net.minecraft.core.HolderGetter; import net.minecraft.core.registries.Registries; import net.minecraft.data.worldgen.BootstrapContext; @@ -18,6 +20,7 @@ public interface MiaStructureSets { ResourceKey STAR_COMPASS_RUINS = register("star_compass_ruins"); ResourceKey ABYSS_STRONGHOLDS = register("abyss_strongholds"); + ResourceKey ABYSS_WINDMILLS = register("abyss_windmills"); static void bootstrap(BootstrapContext context) { HolderGetter structure = context.lookup(Registries.STRUCTURE); @@ -37,6 +40,13 @@ static void bootstrap(BootstrapContext context) { new ConcentricRingsStructurePlacement(32, 11, 64, biome.getOrThrow(BiomeTags.IS_OVERWORLD)) ) ); + context.register( + ABYSS_WINDMILLS, + new StructureSet( + structure.getOrThrow(MiaStructures.ABYSS_WINDMILL), + new AbyssWallStructurePlacement(AbyssWallPlanConfig.DEFAULT) + ) + ); } private static ResourceKey register(String name) { diff --git a/src/main/java/com/altnoir/mia/worldgen/structure/MiaStructures.java b/src/main/java/com/altnoir/mia/worldgen/structure/MiaStructures.java index f6ac6173..267bfe33 100644 --- a/src/main/java/com/altnoir/mia/worldgen/structure/MiaStructures.java +++ b/src/main/java/com/altnoir/mia/worldgen/structure/MiaStructures.java @@ -4,7 +4,10 @@ import com.altnoir.mia.init.MiaTags; import com.altnoir.mia.util.MiaUtil; import com.altnoir.mia.worldgen.structure.pools.AbyssStrongholdPools; +import com.altnoir.mia.worldgen.structure.pools.AbyssWindmillPools; import com.altnoir.mia.worldgen.structure.pools.StarCompassRuinsPools; +import com.altnoir.mia.worldgen.structure.wall.AbyssWallPlanConfig; +import com.altnoir.mia.worldgen.structure.wall.AbyssWindmillStructure; import net.minecraft.core.HolderGetter; import net.minecraft.core.registries.Registries; import net.minecraft.data.worldgen.BootstrapContext; @@ -36,6 +39,7 @@ public class MiaStructures { public static final ResourceKey STAR_COMPASS_RUINS = createKey("star_compass_ruins"); public static final ResourceKey ABYSS_STRONGHOLD = createKey("abyss_stronghold"); + public static final ResourceKey ABYSS_WINDMILL = createKey("abyss_windmill"); public static void bootstrap(BootstrapContext context) { HolderGetter biome = context.lookup(Registries.BIOME); @@ -93,6 +97,19 @@ public static void bootstrap(BootstrapContext context) { LiquidSettings.IGNORE_WATERLOGGING ) ); + context.register( + ABYSS_WINDMILL, + new AbyssWindmillStructure( + new Structure.StructureSettings.Builder(biome.getOrThrow(MiaTags.Biomes.HAS_ABYSS_WINDMILL)) + .generationStep(GenerationStep.Decoration.UNDERGROUND_STRUCTURES) + .terrainAdapation(TerrainAdjustment.NONE) + .build(), + templatePool.getOrThrow(AbyssWindmillPools.STRAIGHT), + templatePool.getOrThrow(AbyssWindmillPools.TILT), + MiaUtil.miaId("abyss_wall_anchor"), + AbyssWallPlanConfig.DEFAULT + ) + ); } private static ResourceKey createKey(String name) { diff --git a/src/main/java/com/altnoir/mia/worldgen/structure/pools/AbyssWindmillPools.java b/src/main/java/com/altnoir/mia/worldgen/structure/pools/AbyssWindmillPools.java new file mode 100644 index 00000000..4653a1f0 --- /dev/null +++ b/src/main/java/com/altnoir/mia/worldgen/structure/pools/AbyssWindmillPools.java @@ -0,0 +1,35 @@ +package com.altnoir.mia.worldgen.structure.pools; + +import com.altnoir.mia.worldgen.MiaSructurePoolUtils; +import com.google.common.collect.ImmutableList; +import com.mojang.datafixers.util.Pair; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderGetter; +import net.minecraft.core.registries.Registries; +import net.minecraft.data.worldgen.BootstrapContext; +import net.minecraft.data.worldgen.Pools; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; + +public final class AbyssWindmillPools { + public static final ResourceKey STRAIGHT = MiaPools.createKey("abyss_windmill/straight"); + public static final ResourceKey TILT = MiaPools.createKey("abyss_windmill/tilt"); + + private AbyssWindmillPools() { + } + + public static void bootstrap(BootstrapContext context) { + HolderGetter pools = context.lookup(Registries.TEMPLATE_POOL); + Holder empty = pools.getOrThrow(Pools.EMPTY); + context.register(STRAIGHT, new StructureTemplatePool( + empty, + ImmutableList.of(Pair.of(MiaSructurePoolUtils.single("abyss_windmill_straight"), 1)), + StructureTemplatePool.Projection.RIGID + )); + context.register(TILT, new StructureTemplatePool( + empty, + ImmutableList.of(Pair.of(MiaSructurePoolUtils.single("abyss_windmill_tilt"), 1)), + StructureTemplatePool.Projection.RIGID + )); + } +} diff --git a/src/main/java/com/altnoir/mia/worldgen/structure/pools/MiaPools.java b/src/main/java/com/altnoir/mia/worldgen/structure/pools/MiaPools.java index 2e74dcff..8144706a 100644 --- a/src/main/java/com/altnoir/mia/worldgen/structure/pools/MiaPools.java +++ b/src/main/java/com/altnoir/mia/worldgen/structure/pools/MiaPools.java @@ -18,5 +18,6 @@ public static void register(BootstrapContext context, Str public static void bootstrap(BootstrapContext context) { StarCompassRuinsPools.bootstrap(context); AbyssStrongholdPools.bootstrap(context); + AbyssWindmillPools.bootstrap(context); } } diff --git a/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallCandidate.java b/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallCandidate.java new file mode 100644 index 00000000..b0569c13 --- /dev/null +++ b/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallCandidate.java @@ -0,0 +1,23 @@ +package com.altnoir.mia.worldgen.structure.wall; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.Rotation; + +public record AbyssWallCandidate( + int id, + double angle, + int y, + double predictedRadius, + BlockPos anchor, + ChunkPos startChunk, + Orientation orientation +) { + public enum TemplateKind { + STRAIGHT, + TILT + } + + public record Orientation(TemplateKind templateKind, Rotation rotation) { + } +} diff --git a/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanConfig.java b/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanConfig.java new file mode 100644 index 00000000..8c2b8cf2 --- /dev/null +++ b/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanConfig.java @@ -0,0 +1,49 @@ +package com.altnoir.mia.worldgen.structure.wall; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +public record AbyssWallPlanConfig( + int minY, + int maxY, + int candidateCount, + int minAnchorDistance, + int maxCorrection, + int embedDepth, + int attemptsPerBand, + int salt, + int contractVersion +) { + public static final AbyssWallPlanConfig DEFAULT = new AbyssWallPlanConfig( + -96, 125, 20, 32, 16, 3, 64, 0x4D494157, 2 + ); + + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.intRange(-2048, 2048).fieldOf("min_y").forGetter(AbyssWallPlanConfig::minY), + Codec.intRange(-2048, 2048).fieldOf("max_y").forGetter(AbyssWallPlanConfig::maxY), + Codec.intRange(1, 64).fieldOf("candidate_count").forGetter(AbyssWallPlanConfig::candidateCount), + Codec.intRange(1, 1024).fieldOf("min_anchor_distance").forGetter(AbyssWallPlanConfig::minAnchorDistance), + Codec.intRange(1, 256).fieldOf("max_correction").forGetter(AbyssWallPlanConfig::maxCorrection), + Codec.intRange(0, 32).fieldOf("embed_depth").forGetter(AbyssWallPlanConfig::embedDepth), + Codec.intRange(1, 256).fieldOf("attempts_per_band").forGetter(AbyssWallPlanConfig::attemptsPerBand), + Codec.intRange(0, Integer.MAX_VALUE).fieldOf("salt").forGetter(AbyssWallPlanConfig::salt), + Codec.intRange(1, Integer.MAX_VALUE).fieldOf("contract_version").forGetter(AbyssWallPlanConfig::contractVersion) + ).apply(instance, AbyssWallPlanConfig::new)); + + public AbyssWallPlanConfig { + if (maxY < minY) { + throw new IllegalArgumentException("maxY must be greater than or equal to minY"); + } + } + + public int maxWallAnchorOffset() { + return this.embedDepth; + } + + public int planningAnchorDistance() { + int maximumAnchorShift = (int) Math.ceil( + this.maxCorrection + this.maxWallAnchorOffset() + Math.sqrt(2.0) + ); + return this.minAnchorDistance + 2 * maximumAnchorShift; + } +} diff --git a/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanner.java b/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanner.java new file mode 100644 index 00000000..198f5f47 --- /dev/null +++ b/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanner.java @@ -0,0 +1,238 @@ +package com.altnoir.mia.worldgen.structure.wall; + +import com.altnoir.mia.MIA; +import com.altnoir.mia.MiaConfig; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.levelgen.LegacyRandomSource; +import net.minecraft.world.level.levelgen.RandomState; +import net.minecraft.world.level.levelgen.WorldgenRandom; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.IntPredicate; + +public final class AbyssWallPlanner { + private static final int FORBIDDEN_MIN_Y = -3; + private static final int FORBIDDEN_MAX_Y = 25; + private static final int EMBED_SALT = 0x57414C4C; + private static final double FULL_CIRCLE = Math.PI * 2.0; + private static final double SECTOR_SIZE = Math.PI / 4.0; + private static final double HALF_SECTOR = SECTOR_SIZE / 2.0; + private static final Cache> WORLD_PLANS = CacheBuilder.newBuilder() + .weakKeys() + .maximumSize(16) + .build(); + + private AbyssWallPlanner() { + } + + @FunctionalInterface + public interface RadiusPredictor { + OptionalDouble radiusAt(double angle, int y); + } + + public static List createPlan(long seed, AbyssWallPlanConfig config, RadiusPredictor predictor) { + List candidates = new ArrayList<>(config.candidateCount()); + Set claimedChunks = new HashSet<>(); + long bufferedMinimumDistance = config.planningAnchorDistance(); + long minimumDistanceSquared = bufferedMinimumDistance * bufferedMinimumDistance; + int allowedYCount = allowedYCount(config); + if (allowedYCount == 0) { + return List.of(); + } + + for (int band = 0; band < config.candidateCount(); band++) { + int bandMinIndex = band * allowedYCount / config.candidateCount(); + int bandMaxIndex = (band + 1) * allowedYCount / config.candidateCount() - 1; + if (bandMaxIndex < bandMinIndex) { + continue; + } + WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(0L)); + random.setLargeFeatureWithSalt(seed, band, config.contractVersion(), config.salt()); + + for (int attempt = 0; attempt < config.attemptsPerBand(); attempt++) { + int y = allowedYAt(config, Mth.nextInt(random, bandMinIndex, bandMaxIndex)); + double angle = random.nextDouble() * FULL_CIRCLE; + OptionalDouble predictedRadius = predictor.radiusAt(angle, y); + if (predictedRadius.isEmpty() || !Double.isFinite(predictedRadius.getAsDouble()) || predictedRadius.getAsDouble() <= 0.0) { + continue; + } + + BlockPos anchor = blockPos(angle, y, predictedRadius.getAsDouble()); + ChunkPos startChunk = new ChunkPos(anchor); + if (claimedChunks.contains(startChunk) || isTooClose(anchor, candidates, minimumDistanceSquared)) { + continue; + } + + candidates.add(new AbyssWallCandidate( + band, + angle, + y, + predictedRadius.getAsDouble(), + anchor, + startChunk, + orientationFor(angle) + )); + claimedChunks.add(startChunk); + break; + } + } + + return List.copyOf(candidates); + } + + public static List planFor(long seed, RandomState randomState, AbyssWallPlanConfig config) { + return cachedPlanFor(seed, randomState, config).candidates(); + } + + private static CachedPlan cachedPlanFor(long seed, RandomState randomState, AbyssWallPlanConfig config) { + int abyssRadius = currentAbyssRadius(); + Map plans = WORLD_PLANS.asMap() + .computeIfAbsent(randomState, ignored -> new ConcurrentHashMap<>()); + return plans.computeIfAbsent(new PlanKey(seed, config, abyssRadius), ignored -> { + List plan = createPlan(seed, config, + (angle, y) -> OptionalDouble.of(nominalRadius(y, abyssRadius))); + MIA.LOGGER.debug("Abyss wall candidates for seed {}: {}", seed, plan); + return CachedPlan.create(plan); + }); + } + + public static AbyssWallCandidate candidateForChunk( + long seed, + RandomState randomState, + AbyssWallPlanConfig config, + ChunkPos chunkPos + ) { + return cachedPlanFor(seed, randomState, config).byChunk().get(chunkPos); + } + + public static AbyssWallCandidate.Orientation orientationFor(double angle) { + double normalized = Mth.positiveModulo(angle, FULL_CIRCLE); + int sector = Mth.floor((normalized + HALF_SECTOR) / SECTOR_SIZE) & 7; + if ((sector & 1) == 0) { + return new AbyssWallCandidate.Orientation(AbyssWallCandidate.TemplateKind.STRAIGHT, rotationFromSouth(sector)); + } + return new AbyssWallCandidate.Orientation(AbyssWallCandidate.TemplateKind.TILT, rotationFromSouthEast(sector)); + } + + public static BlockPos blockPos(double angle, int y, double radius) { + return new BlockPos( + Mth.floor(Math.cos(angle) * radius + 0.5), + y, + Mth.floor(Math.sin(angle) * radius + 0.5) + ); + } + + public static OptionalInt findFirstStableBoundary(int minRadius, int maxRadius, IntPredicate isSolid) { + for (int radius = minRadius + 3; radius <= maxRadius - 3; radius++) { + boolean stableAir = !isSolid.test(radius - 3) + && !isSolid.test(radius - 2) + && !isSolid.test(radius - 1); + boolean stableRock = isSolid.test(radius) + && isSolid.test(radius + 1) + && isSolid.test(radius + 2) + && isSolid.test(radius + 3); + if (stableAir && stableRock) { + return OptionalInt.of(radius); + } + } + return OptionalInt.empty(); + } + + public static OptionalInt findRefinedBoundary(double predictedRadius, int maxCorrection, IntPredicate isSolid) { + int allowedMinimum = Math.max(1, (int) Math.floor(predictedRadius) - maxCorrection); + int allowedMaximum = (int) Math.ceil(predictedRadius) + maxCorrection; + return findFirstStableBoundary( + Math.max(1, allowedMinimum - 3), + allowedMaximum + 3, + isSolid + ); + } + + public static int wallAnchorOffset(long seed, AbyssWallCandidate candidate, AbyssWallPlanConfig config) { + if (config.embedDepth() == 0) { + return 0; + } + WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(0L)); + random.setLargeFeatureWithSalt(seed, candidate.id(), config.contractVersion(), config.salt() ^ EMBED_SALT); + return random.nextInt(config.embedDepth() + 1); + } + + private static boolean isTooClose(BlockPos anchor, List candidates, long minimumDistanceSquared) { + for (AbyssWallCandidate candidate : candidates) { + if (anchor.distSqr(candidate.anchor()) < minimumDistanceSquared) { + return true; + } + } + return false; + } + + private static Rotation rotationFromSouth(int sector) { + return switch (sector) { + case 0 -> Rotation.COUNTERCLOCKWISE_90; + case 2 -> Rotation.NONE; + case 4 -> Rotation.CLOCKWISE_90; + case 6 -> Rotation.CLOCKWISE_180; + default -> throw new IllegalArgumentException("Not a cardinal sector: " + sector); + }; + } + + private static Rotation rotationFromSouthEast(int sector) { + return switch (sector) { + case 1 -> Rotation.NONE; + case 3 -> Rotation.CLOCKWISE_90; + case 5 -> Rotation.CLOCKWISE_180; + case 7 -> Rotation.COUNTERCLOCKWISE_90; + default -> throw new IllegalArgumentException("Not a diagonal sector: " + sector); + }; + } + + private static int currentAbyssRadius() { + return MiaConfig.abyssRadius > 0 ? MiaConfig.abyssRadius : 160; + } + + private static double nominalRadius(int y, int abyssRadius) { + double radius = abyssRadius; + return y < FORBIDDEN_MIN_Y + ? radius * 0.6 + : radius * Mth.clamp(1.0 + y / 512.0, 1.0, 2.0); + } + + private static int allowedYCount(AbyssWallPlanConfig config) { + int total = config.maxY() - config.minY() + 1; + int excludedMin = Math.max(config.minY(), FORBIDDEN_MIN_Y); + int excludedMax = Math.min(config.maxY(), FORBIDDEN_MAX_Y); + return total - Math.max(0, excludedMax - excludedMin + 1); + } + + private static int allowedYAt(AbyssWallPlanConfig config, int index) { + int lowerAllowedCount = Math.max(0, Math.min(config.maxY(), FORBIDDEN_MIN_Y - 1) - config.minY() + 1); + return index < lowerAllowedCount + ? config.minY() + index + : Math.max(config.minY(), FORBIDDEN_MAX_Y + 1) + index - lowerAllowedCount; + } + + private record PlanKey(long seed, AbyssWallPlanConfig config, int abyssRadius) { + } + + private record CachedPlan(List candidates, Map byChunk) { + private static CachedPlan create(List candidates) { + Map byChunk = new ConcurrentHashMap<>(); + for (AbyssWallCandidate candidate : candidates) { + byChunk.put(candidate.startChunk(), candidate); + } + return new CachedPlan(candidates, Map.copyOf(byChunk)); + } + } +} diff --git a/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallStructurePlacement.java b/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallStructurePlacement.java new file mode 100644 index 00000000..0ee5c452 --- /dev/null +++ b/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallStructurePlacement.java @@ -0,0 +1,57 @@ +package com.altnoir.mia.worldgen.structure.wall; + +import com.altnoir.mia.init.worldgen.MiaStructurePlacementTypes; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.Vec3i; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.ChunkGeneratorStructureState; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacement; +import net.minecraft.world.level.levelgen.structure.placement.StructurePlacementType; + +import java.util.Optional; + +public final class AbyssWallStructurePlacement extends StructurePlacement { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> + placementCodec(instance) + .and(AbyssWallPlanConfig.CODEC.fieldOf("plan").forGetter(AbyssWallStructurePlacement::plan)) + .apply(instance, AbyssWallStructurePlacement::new) + ); + + private final AbyssWallPlanConfig plan; + + private AbyssWallStructurePlacement( + Vec3i locateOffset, + FrequencyReductionMethod frequencyReductionMethod, + float frequency, + int salt, + Optional exclusionZone, + AbyssWallPlanConfig plan + ) { + super(locateOffset, frequencyReductionMethod, frequency, salt, exclusionZone); + this.plan = plan; + } + + public AbyssWallStructurePlacement(AbyssWallPlanConfig plan) { + this(Vec3i.ZERO, FrequencyReductionMethod.DEFAULT, 1.0F, plan.salt(), Optional.empty(), plan); + } + + public AbyssWallPlanConfig plan() { + return this.plan; + } + + @Override + protected boolean isPlacementChunk(ChunkGeneratorStructureState structureState, int x, int z) { + return AbyssWallPlanner.candidateForChunk( + structureState.getLevelSeed(), + structureState.randomState(), + this.plan, + new ChunkPos(x, z) + ) != null; + } + + @Override + public StructurePlacementType type() { + return MiaStructurePlacementTypes.ABYSS_WALL.get(); + } +} diff --git a/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWindmillStructure.java b/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWindmillStructure.java new file mode 100644 index 00000000..5a8b5e78 --- /dev/null +++ b/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWindmillStructure.java @@ -0,0 +1,152 @@ +package com.altnoir.mia.worldgen.structure.wall; + +import com.altnoir.mia.MIA; +import com.altnoir.mia.init.worldgen.MiaStructureTypes; +import com.mojang.serialization.MapCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.NoiseColumn; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.PoolElementStructurePiece; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureType; +import net.minecraft.world.level.levelgen.structure.pools.EmptyPoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructurePoolElement; +import net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool; +import net.minecraft.world.level.levelgen.structure.templatesystem.LiquidSettings; +import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; + +public final class AbyssWindmillStructure extends Structure { + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( + settingsCodec(instance), + StructureTemplatePool.CODEC.fieldOf("straight_pool").forGetter(AbyssWindmillStructure::straightPool), + StructureTemplatePool.CODEC.fieldOf("tilt_pool").forGetter(AbyssWindmillStructure::tiltPool), + ResourceLocation.CODEC.fieldOf("anchor_name").forGetter(AbyssWindmillStructure::anchorName), + AbyssWallPlanConfig.CODEC.fieldOf("plan").forGetter(AbyssWindmillStructure::plan) + ).apply(instance, AbyssWindmillStructure::new)); + + private final Holder straightPool; + private final Holder tiltPool; + private final ResourceLocation anchorName; + private final AbyssWallPlanConfig plan; + + public AbyssWindmillStructure( + StructureSettings settings, + Holder straightPool, + Holder tiltPool, + ResourceLocation anchorName, + AbyssWallPlanConfig plan + ) { + super(settings); + this.straightPool = straightPool; + this.tiltPool = tiltPool; + this.anchorName = anchorName; + this.plan = plan; + } + + private Holder straightPool() { + return this.straightPool; + } + + private Holder tiltPool() { + return this.tiltPool; + } + + private ResourceLocation anchorName() { + return this.anchorName; + } + + private AbyssWallPlanConfig plan() { + return this.plan; + } + + @Override + protected Optional findGenerationPoint(GenerationContext context) { + if (!(context.chunkGenerator() instanceof NoiseBasedChunkGenerator noiseGenerator)) { + return Optional.empty(); + } + + AbyssWallCandidate candidate = AbyssWallPlanner.candidateForChunk( + context.seed(), context.randomState(), this.plan, context.chunkPos() + ); + if (candidate == null) { + MIA.LOGGER.warn("Abyss windmill placement/structure plans disagree at start chunk {}", context.chunkPos()); + return Optional.empty(); + } + + OptionalInt refinedRadius = refineRadius(context, noiseGenerator, candidate); + if (refinedRadius.isEmpty()) { + return Optional.empty(); + } + + double anchorRadius = refinedRadius.getAsInt() + + AbyssWallPlanner.wallAnchorOffset(context.seed(), candidate, this.plan); + BlockPos wallAnchor = AbyssWallPlanner.blockPos(candidate.angle(), candidate.y(), anchorRadius); + Holder selectedPool = candidate.orientation().templateKind() == AbyssWallCandidate.TemplateKind.STRAIGHT + ? this.straightPool + : this.tiltPool; + StructurePoolElement element = selectedPool.value().getRandomTemplate(context.random()); + if (element == EmptyPoolElement.INSTANCE) { + MIA.LOGGER.warn("Abyss windmill pool {} is empty", selectedPool.unwrapKey().orElse(null)); + return Optional.empty(); + } + + List anchors = element.getShuffledJigsawBlocks( + context.structureTemplateManager(), BlockPos.ZERO, candidate.orientation().rotation(), context.random()) + .stream() + .filter(info -> info.nbt() != null && this.anchorName.toString().equals(info.nbt().getString("name"))) + .toList(); + if (anchors.size() != 1) { + MIA.LOGGER.warn("Abyss windmill template must contain exactly one anchor named {}, found {}", this.anchorName, anchors.size()); + return Optional.empty(); + } + + BlockPos templateOrigin = wallAnchor.subtract(anchors.getFirst().pos()); + BoundingBox boundingBox = element.getBoundingBox( + context.structureTemplateManager(), templateOrigin, candidate.orientation().rotation() + ); + PoolElementStructurePiece piece = new PoolElementStructurePiece( + context.structureTemplateManager(), + element, + templateOrigin, + element.getGroundLevelDelta(), + candidate.orientation().rotation(), + boundingBox, + LiquidSettings.IGNORE_WATERLOGGING + ); + return Optional.of(new GenerationStub(wallAnchor, builder -> builder.addPiece(piece))); + } + + private OptionalInt refineRadius( + GenerationContext context, + NoiseBasedChunkGenerator generator, + AbyssWallCandidate candidate + ) { + Map solidity = new HashMap<>(); + return AbyssWallPlanner.findRefinedBoundary(candidate.predictedRadius(), this.plan.maxCorrection(), radius -> + solidity.computeIfAbsent(radius, ignored -> { + BlockPos pos = AbyssWallPlanner.blockPos(candidate.angle(), candidate.y(), radius); + NoiseColumn column = generator.getBaseColumn( + pos.getX(), pos.getZ(), context.heightAccessor(), context.randomState() + ); + BlockState state = column.getBlock(candidate.y()); + return !state.isAir() && state.getFluidState().isEmpty(); + }) + ); + } + + @Override + public StructureType type() { + return MiaStructureTypes.ABYSS_WALL_TEMPLATE.get(); + } +} diff --git a/src/main/resources/data/mia/structure/abyss_windmill_straight.nbt b/src/main/resources/data/mia/structure/abyss_windmill_straight.nbt new file mode 100644 index 0000000000000000000000000000000000000000..55ba4973e2d43ff5dc96127b53728353331b4f52 GIT binary patch literal 43544 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 F{|{Z3n9TqH literal 0 HcmV?d00001 diff --git a/src/main/resources/data/mia/structure/abyss_windmill_tilt.nbt b/src/main/resources/data/mia/structure/abyss_windmill_tilt.nbt new file mode 100644 index 0000000000000000000000000000000000000000..a594bce81b01fb3928a566c811318ea665e7abb6 GIT binary patch literal 32389 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 RUENcTe(@VyoTmNi{{ktG_OSo} literal 0 HcmV?d00001 From 64f6e80fa0073b125425e9a77e7ff7cdc505d156 Mon Sep 17 00:00:00 2001 From: zljtt <52527069+zljtt@users.noreply.github.com> Date: Mon, 24 Aug 2026 05:09:53 +0800 Subject: [PATCH 2/3] fix: constrain windmills to inward abyss walls --- .../item/artifact_modifiers_material.json | 2 +- .../worldgen/structure/abyss_windmill.json | 6 +- .../structure_set/abyss_windmills.json | 6 +- .../java/com/altnoir/mia/MiaGameTests.java | 17 +++-- .../structure/wall/AbyssWallPlanConfig.java | 2 +- .../structure/wall/AbyssWallPlanner.java | 62 +++++++++++++++++-- 6 files changed, 79 insertions(+), 16 deletions(-) diff --git a/src/generated/resources/data/mia/tags/item/artifact_modifiers_material.json b/src/generated/resources/data/mia/tags/item/artifact_modifiers_material.json index e7947ebc..d0c922b1 100644 --- a/src/generated/resources/data/mia/tags/item/artifact_modifiers_material.json +++ b/src/generated/resources/data/mia/tags/item/artifact_modifiers_material.json @@ -9,4 +9,4 @@ "minecraft:diamond", "minecraft:iron_ingot" ] -} \ No newline at end of file +} diff --git a/src/generated/resources/data/mia/worldgen/structure/abyss_windmill.json b/src/generated/resources/data/mia/worldgen/structure/abyss_windmill.json index 77521188..4c4fcdff 100644 --- a/src/generated/resources/data/mia/worldgen/structure/abyss_windmill.json +++ b/src/generated/resources/data/mia/worldgen/structure/abyss_windmill.json @@ -5,10 +5,10 @@ "plan": { "attempts_per_band": 64, "candidate_count": 20, - "contract_version": 2, + "contract_version": 3, "embed_depth": 3, "max_correction": 16, - "max_y": 125, + "max_y": 288, "min_anchor_distance": 32, "min_y": -96, "salt": 1296646487 @@ -17,4 +17,4 @@ "step": "underground_structures", "straight_pool": "mia:abyss_windmill/straight", "tilt_pool": "mia:abyss_windmill/tilt" -} +} \ No newline at end of file diff --git a/src/generated/resources/data/mia/worldgen/structure_set/abyss_windmills.json b/src/generated/resources/data/mia/worldgen/structure_set/abyss_windmills.json index fab5ff85..ed99ac82 100644 --- a/src/generated/resources/data/mia/worldgen/structure_set/abyss_windmills.json +++ b/src/generated/resources/data/mia/worldgen/structure_set/abyss_windmills.json @@ -4,10 +4,10 @@ "plan": { "attempts_per_band": 64, "candidate_count": 20, - "contract_version": 2, + "contract_version": 3, "embed_depth": 3, "max_correction": 16, - "max_y": 125, + "max_y": 288, "min_anchor_distance": 32, "min_y": -96, "salt": 1296646487 @@ -20,4 +20,4 @@ "weight": 1 } ] -} +} \ No newline at end of file diff --git a/src/main/java/com/altnoir/mia/MiaGameTests.java b/src/main/java/com/altnoir/mia/MiaGameTests.java index b16f5766..061a61bb 100644 --- a/src/main/java/com/altnoir/mia/MiaGameTests.java +++ b/src/main/java/com/altnoir/mia/MiaGameTests.java @@ -60,7 +60,7 @@ public static void abyssWallPlanIsDeterministicSeparatedAndInRange(GameTestHelpe for (int i = 0; i < first.size(); i++) { AbyssWallCandidate candidate = first.get(i); - helper.assertTrue(candidate.y() >= -96 && candidate.y() <= 125, + helper.assertTrue(candidate.y() >= -96 && candidate.y() <= 288, "candidate Y must stay in the allowed windmill range"); helper.assertTrue(candidate.y() < -3 || candidate.y() > 25, "candidate Y must avoid the horizontal transition shelf from -3 through 25"); @@ -117,12 +117,22 @@ public static void abyssWallRefinementRequiresStableAirToSolidBoundary(GameTestH OptionalInt missing = AbyssWallPlanner.findFirstStableBoundary(180, 230, radius -> false); OptionalInt lowerWall = AbyssWallPlanner.findRefinedBoundary(96.0, 16, radius -> radius >= 94); OptionalInt unrelatedOuterWall = AbyssWallPlanner.findRefinedBoundary(96.0, 16, radius -> radius >= 125); + double inwardPrediction = AbyssWallPlanner.preferredRadius(96.0, 3, radius -> radius >= 90); + double toleratedPrediction = AbyssWallPlanner.preferredRadius(96.0, 3, radius -> radius >= 99); + double justOutsideTolerance = AbyssWallPlanner.preferredRadius(96.0, 3, radius -> radius >= 100); + double distantOuterPrediction = AbyssWallPlanner.preferredRadius(96.0, 3, radius -> radius >= 125); helper.assertValueEqual(boundary, OptionalInt.of(205), "first stable wall radius"); helper.assertValueEqual(missing, OptionalInt.empty(), "missing wall must reject the candidate"); helper.assertValueEqual(lowerWall, OptionalInt.of(94), "nearby lower-layer wall must refine the macro radius"); helper.assertValueEqual(unrelatedOuterWall, OptionalInt.empty(), "a perforated main wall must not fall through to a farther cave boundary"); + helper.assertValueEqual(inwardPrediction, 90.0, "an inward final-density wall must override the macro radius"); + helper.assertValueEqual(toleratedPrediction, 99.0, "the tolerance boundary must remain eligible"); + helper.assertValueEqual(justOutsideTolerance, 96.0, + "a final-density wall beyond tolerance must fall back to the macro radius"); + helper.assertValueEqual(distantOuterPrediction, 96.0, + "a distant outer wall must fall back to the macro radius"); helper.succeed(); } @@ -139,12 +149,11 @@ public static void abyssWallPlanFindsTheRealAbyssDensityWall(GameTestHelper help helper.assertTrue(candidates.size() >= 16 && candidates.size() <= 20, "real Abyss wall plan must yield approximately 16-20 candidates"); for (AbyssWallCandidate candidate : candidates) { - helper.assertTrue(candidate.y() <= 125, "real-density candidates above Y=125 must be rejected"); helper.assertTrue(candidate.y() < -3 || candidate.y() > 25, "real-density candidates must avoid the horizontal transition shelf"); if (candidate.y() < -3) { - helper.assertValueEqual(candidate.predictedRadius(), 96.0, - "lower-layer candidates must target the macro wall instead of a farther final-density boundary"); + helper.assertTrue(candidate.predictedRadius() <= 99.0, + "lower-layer prediction must not select a wall beyond macro radius plus tolerance"); } } helper.assertValueEqual(candidates, AbyssWallPlanner.planFor(seed, randomState, AbyssWallPlanConfig.DEFAULT), diff --git a/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanConfig.java b/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanConfig.java index 8c2b8cf2..8e4c9c42 100644 --- a/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanConfig.java +++ b/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanConfig.java @@ -15,7 +15,7 @@ public record AbyssWallPlanConfig( int contractVersion ) { public static final AbyssWallPlanConfig DEFAULT = new AbyssWallPlanConfig( - -96, 125, 20, 32, 16, 3, 64, 0x4D494157, 2 + -96, 288, 20, 32, 16, 3, 64, 0x4D494157, 3 ); public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( diff --git a/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanner.java b/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanner.java index 198f5f47..67f15f78 100644 --- a/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanner.java +++ b/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanner.java @@ -8,11 +8,13 @@ import net.minecraft.util.Mth; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.levelgen.DensityFunction; import net.minecraft.world.level.levelgen.LegacyRandomSource; import net.minecraft.world.level.levelgen.RandomState; import net.minecraft.world.level.levelgen.WorldgenRandom; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -25,6 +27,7 @@ public final class AbyssWallPlanner { private static final int FORBIDDEN_MIN_Y = -3; private static final int FORBIDDEN_MAX_Y = 25; + private static final int FINAL_DENSITY_TOLERANCE = 3; private static final int EMBED_SALT = 0x57414C4C; private static final double FULL_CIRCLE = Math.PI * 2.0; private static final double SECTOR_SIZE = Math.PI / 4.0; @@ -43,6 +46,15 @@ public interface RadiusPredictor { } public static List createPlan(long seed, AbyssWallPlanConfig config, RadiusPredictor predictor) { + return createPlan(seed, config, predictor, predictor); + } + + private static List createPlan( + long seed, + AbyssWallPlanConfig config, + RadiusPredictor preliminaryPredictor, + RadiusPredictor preferredPredictor + ) { List candidates = new ArrayList<>(config.candidateCount()); Set claimedChunks = new HashSet<>(); long bufferedMinimumDistance = config.planningAnchorDistance(); @@ -64,8 +76,19 @@ public static List createPlan(long seed, AbyssWallPlanConfig for (int attempt = 0; attempt < config.attemptsPerBand(); attempt++) { int y = allowedYAt(config, Mth.nextInt(random, bandMinIndex, bandMaxIndex)); double angle = random.nextDouble() * FULL_CIRCLE; - OptionalDouble predictedRadius = predictor.radiusAt(angle, y); - if (predictedRadius.isEmpty() || !Double.isFinite(predictedRadius.getAsDouble()) || predictedRadius.getAsDouble() <= 0.0) { + OptionalDouble preliminaryRadius = preliminaryPredictor.radiusAt(angle, y); + if (!isUsableRadius(preliminaryRadius)) { + continue; + } + + BlockPos preliminaryAnchor = blockPos(angle, y, preliminaryRadius.getAsDouble()); + if (claimedChunks.contains(new ChunkPos(preliminaryAnchor)) + || isTooClose(preliminaryAnchor, candidates, minimumDistanceSquared)) { + continue; + } + + OptionalDouble predictedRadius = preferredPredictor.radiusAt(angle, y); + if (!isUsableRadius(predictedRadius)) { continue; } @@ -101,8 +124,19 @@ private static CachedPlan cachedPlanFor(long seed, RandomState randomState, Abys Map plans = WORLD_PLANS.asMap() .computeIfAbsent(randomState, ignored -> new ConcurrentHashMap<>()); return plans.computeIfAbsent(new PlanKey(seed, config, abyssRadius), ignored -> { - List plan = createPlan(seed, config, - (angle, y) -> OptionalDouble.of(nominalRadius(y, abyssRadius))); + DensityFunction finalDensity = randomState.router().finalDensity(); + RadiusPredictor macroPredictor = (angle, y) -> OptionalDouble.of(nominalRadius(y, abyssRadius)); + RadiusPredictor finalDensityPredictor = (angle, y) -> { + double macroRadius = nominalRadius(y, abyssRadius); + return OptionalDouble.of(preferredRadius( + macroRadius, + FINAL_DENSITY_TOLERANCE, + radius -> sampleDensity(finalDensity, angle, y, radius) > 0.0 + )); + }; + List plan = createPlan( + seed, config, macroPredictor, finalDensityPredictor + ); MIA.LOGGER.debug("Abyss wall candidates for seed {}: {}", seed, plan); return CachedPlan.create(plan); }); @@ -160,6 +194,17 @@ public static OptionalInt findRefinedBoundary(double predictedRadius, int maxCor ); } + public static double preferredRadius(double macroRadius, int tolerance, IntPredicate isSolid) { + int maximumBoundary = Math.max(1, (int) Math.floor(macroRadius + tolerance)); + Map solidity = new HashMap<>(); + OptionalInt boundary = findFirstStableBoundary( + 1, + maximumBoundary + 3, + radius -> solidity.computeIfAbsent(radius, ignored -> isSolid.test(radius)) + ); + return boundary.isPresent() ? boundary.getAsInt() : macroRadius; + } + public static int wallAnchorOffset(long seed, AbyssWallCandidate candidate, AbyssWallPlanConfig config) { if (config.embedDepth() == 0) { return 0; @@ -178,6 +223,10 @@ private static boolean isTooClose(BlockPos anchor, List cand return false; } + private static boolean isUsableRadius(OptionalDouble radius) { + return radius.isPresent() && Double.isFinite(radius.getAsDouble()) && radius.getAsDouble() > 0.0; + } + private static Rotation rotationFromSouth(int sector) { return switch (sector) { case 0 -> Rotation.COUNTERCLOCKWISE_90; @@ -223,6 +272,11 @@ private static int allowedYAt(AbyssWallPlanConfig config, int index) { : Math.max(config.minY(), FORBIDDEN_MAX_Y + 1) + index - lowerAllowedCount; } + private static double sampleDensity(DensityFunction density, double angle, int y, int radius) { + BlockPos pos = blockPos(angle, y, radius); + return density.compute(new DensityFunction.SinglePointContext(pos.getX(), pos.getY(), pos.getZ())); + } + private record PlanKey(long seed, AbyssWallPlanConfig config, int abyssRadius) { } From cb4d869212fc9ed67524a20cf92e97becb8e355a Mon Sep 17 00:00:00 2001 From: zljtt <52527069+zljtt@users.noreply.github.com> Date: Mon, 24 Aug 2026 16:52:51 +0800 Subject: [PATCH 3/3] fix: make abyss windmill bounds data driven --- .../worldgen/structure/abyss_windmill.json | 8 +- .../structure_set/abyss_windmills.json | 8 +- .../java/com/altnoir/mia/MiaGameTests.java | 80 ++++++++++++++++++- .../structure/wall/AbyssWallPlanConfig.java | 18 ++++- .../structure/wall/AbyssWallPlanner.java | 48 +++++++---- 5 files changed, 140 insertions(+), 22 deletions(-) diff --git a/src/generated/resources/data/mia/worldgen/structure/abyss_windmill.json b/src/generated/resources/data/mia/worldgen/structure/abyss_windmill.json index 4c4fcdff..210658a3 100644 --- a/src/generated/resources/data/mia/worldgen/structure/abyss_windmill.json +++ b/src/generated/resources/data/mia/worldgen/structure/abyss_windmill.json @@ -5,10 +5,14 @@ "plan": { "attempts_per_band": 64, "candidate_count": 20, - "contract_version": 3, + "contract_version": 4, "embed_depth": 3, + "final_density_inward_search": 32, + "final_density_tolerance": 3, + "forbidden_max_y": 25, + "forbidden_min_y": -3, "max_correction": 16, - "max_y": 288, + "max_y": 125, "min_anchor_distance": 32, "min_y": -96, "salt": 1296646487 diff --git a/src/generated/resources/data/mia/worldgen/structure_set/abyss_windmills.json b/src/generated/resources/data/mia/worldgen/structure_set/abyss_windmills.json index ed99ac82..4c81e690 100644 --- a/src/generated/resources/data/mia/worldgen/structure_set/abyss_windmills.json +++ b/src/generated/resources/data/mia/worldgen/structure_set/abyss_windmills.json @@ -4,10 +4,14 @@ "plan": { "attempts_per_band": 64, "candidate_count": 20, - "contract_version": 3, + "contract_version": 4, "embed_depth": 3, + "final_density_inward_search": 32, + "final_density_tolerance": 3, + "forbidden_max_y": 25, + "forbidden_min_y": -3, "max_correction": 16, - "max_y": 288, + "max_y": 125, "min_anchor_distance": 32, "min_y": -96, "salt": 1296646487 diff --git a/src/main/java/com/altnoir/mia/MiaGameTests.java b/src/main/java/com/altnoir/mia/MiaGameTests.java index 061a61bb..06a7bb86 100644 --- a/src/main/java/com/altnoir/mia/MiaGameTests.java +++ b/src/main/java/com/altnoir/mia/MiaGameTests.java @@ -5,6 +5,8 @@ import com.altnoir.mia.worldgen.structure.wall.AbyssWallCandidate; import com.altnoir.mia.worldgen.structure.wall.AbyssWallPlanConfig; import com.altnoir.mia.worldgen.structure.wall.AbyssWallPlanner; +import com.google.gson.JsonObject; +import com.mojang.serialization.JsonOps; import net.minecraft.core.BlockPos; import net.minecraft.core.FrontAndTop; import net.minecraft.core.Vec3i; @@ -24,6 +26,7 @@ import java.util.OptionalDouble; import java.util.OptionalInt; import java.util.Set; +import java.util.function.IntPredicate; import java.util.stream.IntStream; import java.util.stream.Collectors; @@ -60,7 +63,7 @@ public static void abyssWallPlanIsDeterministicSeparatedAndInRange(GameTestHelpe for (int i = 0; i < first.size(); i++) { AbyssWallCandidate candidate = first.get(i); - helper.assertTrue(candidate.y() >= -96 && candidate.y() <= 288, + helper.assertTrue(candidate.y() >= -96 && candidate.y() <= 125, "candidate Y must stay in the allowed windmill range"); helper.assertTrue(candidate.y() < -3 || candidate.y() > 25, "candidate Y must avoid the horizontal transition shelf from -3 through 25"); @@ -89,6 +92,69 @@ public static void abyssWallPlanIsDeterministicSeparatedAndInRange(GameTestHelpe helper.succeed(); } + @PrefixGameTestTemplate(false) + @GameTest(template = "gametest/amethyst_lamptube") + public static void abyssWallPlanCodecSerializesTuningKnobs(GameTestHelper helper) { + JsonObject encoded = AbyssWallPlanConfig.CODEC.encodeStart(JsonOps.INSTANCE, AbyssWallPlanConfig.DEFAULT) + .getOrThrow() + .getAsJsonObject(); + + helper.assertValueEqual(encoded.get("max_y").getAsInt(), 125, + "the default data contract must temporarily disable windmills above Y=125"); + helper.assertValueEqual(encoded.get("forbidden_min_y").getAsInt(), -3, + "the transition shelf lower bound must be serialized"); + helper.assertValueEqual(encoded.get("forbidden_max_y").getAsInt(), 25, + "the transition shelf upper bound must be serialized"); + helper.assertValueEqual(encoded.get("final_density_inward_search").getAsInt(), 32, + "the inward final-density search distance must be serialized"); + helper.assertValueEqual(encoded.get("final_density_tolerance").getAsInt(), 3, + "the outward final-density tolerance must be serialized"); + helper.assertValueEqual( + AbyssWallPlanConfig.CODEC.parse(JsonOps.INSTANCE, encoded).getOrThrow(), + AbyssWallPlanConfig.DEFAULT, + "the complete wall plan data contract must round-trip" + ); + helper.succeed(); + } + + @PrefixGameTestTemplate(false) + @GameTest(template = "gametest/amethyst_lamptube") + public static void abyssWallPlanConfigControlsForbiddenYRange(GameTestHelper helper) { + JsonObject encoded = AbyssWallPlanConfig.CODEC.encodeStart(JsonOps.INSTANCE, AbyssWallPlanConfig.DEFAULT) + .getOrThrow() + .getAsJsonObject(); + encoded.addProperty("min_y", -20); + encoded.addProperty("max_y", 20); + encoded.addProperty("candidate_count", 4); + encoded.addProperty("forbidden_min_y", -20); + encoded.addProperty("forbidden_max_y", 20); + AbyssWallPlanConfig config = AbyssWallPlanConfig.CODEC.parse(JsonOps.INSTANCE, encoded).getOrThrow(); + + helper.assertValueEqual( + AbyssWallPlanner.createPlan(42L, config, (angle, y) -> OptionalDouble.of(96.0)), + List.of(), + "a data-configured forbidden range covering the full height range must produce no candidates" + ); + helper.succeed(); + } + + @PrefixGameTestTemplate(false) + @GameTest(template = "gametest/amethyst_lamptube") + public static void abyssWallPlanConfigControlsDensitySearch(GameTestHelper helper) { + int[] minimumSampledRadius = {Integer.MAX_VALUE}; + IntPredicate wallAtNinetyTwo = radius -> { + minimumSampledRadius[0] = Math.min(minimumSampledRadius[0], radius); + return radius >= 92; + }; + double configuredPrediction = AbyssWallPlanner.preferredRadius(96.0, 8, 3, wallAtNinetyTwo); + + helper.assertValueEqual(configuredPrediction, 92.0, + "a wall within the configured inward search must be selected"); + helper.assertValueEqual(minimumSampledRadius[0], 88, + "an inward search of 8 must begin at macroRadius - 8"); + helper.succeed(); + } + @PrefixGameTestTemplate(false) @GameTest(template = "gametest/amethyst_lamptube") public static void abyssWallDirectionsSelectStraightAndTiltRotations(GameTestHelper helper) { @@ -121,6 +187,12 @@ public static void abyssWallRefinementRequiresStableAirToSolidBoundary(GameTestH double toleratedPrediction = AbyssWallPlanner.preferredRadius(96.0, 3, radius -> radius >= 99); double justOutsideTolerance = AbyssWallPlanner.preferredRadius(96.0, 3, radius -> radius >= 100); double distantOuterPrediction = AbyssWallPlanner.preferredRadius(96.0, 3, radius -> radius >= 125); + double solidAtSearchStart = AbyssWallPlanner.preferredRadius(96.0, 3, radius -> radius >= 64); + int[] minimumSampledRadius = {Integer.MAX_VALUE}; + double boundedPrediction = AbyssWallPlanner.preferredRadius(96.0, 3, radius -> { + minimumSampledRadius[0] = Math.min(minimumSampledRadius[0], radius); + return radius >= 90; + }); helper.assertValueEqual(boundary, OptionalInt.of(205), "first stable wall radius"); helper.assertValueEqual(missing, OptionalInt.empty(), "missing wall must reject the candidate"); @@ -133,6 +205,12 @@ public static void abyssWallRefinementRequiresStableAirToSolidBoundary(GameTestH "a final-density wall beyond tolerance must fall back to the macro radius"); helper.assertValueEqual(distantOuterPrediction, 96.0, "a distant outer wall must fall back to the macro radius"); + helper.assertTrue(Double.isNaN(solidAtSearchStart), + "a candidate whose inward search limit is already solid must be rejected"); + helper.assertValueEqual(boundedPrediction, 90.0, + "a stable wall within the bounded inward search must still be selected"); + helper.assertValueEqual(minimumSampledRadius[0], 64, + "final density must not be sampled more than 32 blocks inward from the macro radius"); helper.succeed(); } diff --git a/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanConfig.java b/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanConfig.java index 8e4c9c42..e22ae5e0 100644 --- a/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanConfig.java +++ b/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanConfig.java @@ -12,10 +12,15 @@ public record AbyssWallPlanConfig( int embedDepth, int attemptsPerBand, int salt, - int contractVersion + int contractVersion, + int forbiddenMinY, + int forbiddenMaxY, + int finalDensityInwardSearch, + int finalDensityTolerance ) { public static final AbyssWallPlanConfig DEFAULT = new AbyssWallPlanConfig( - -96, 288, 20, 32, 16, 3, 64, 0x4D494157, 3 + -96, 125, 20, 32, 16, 3, 64, 0x4D494157, 4, + -3, 25, 32, 3 ); public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( @@ -27,13 +32,20 @@ public record AbyssWallPlanConfig( Codec.intRange(0, 32).fieldOf("embed_depth").forGetter(AbyssWallPlanConfig::embedDepth), Codec.intRange(1, 256).fieldOf("attempts_per_band").forGetter(AbyssWallPlanConfig::attemptsPerBand), Codec.intRange(0, Integer.MAX_VALUE).fieldOf("salt").forGetter(AbyssWallPlanConfig::salt), - Codec.intRange(1, Integer.MAX_VALUE).fieldOf("contract_version").forGetter(AbyssWallPlanConfig::contractVersion) + Codec.intRange(1, Integer.MAX_VALUE).fieldOf("contract_version").forGetter(AbyssWallPlanConfig::contractVersion), + Codec.intRange(-2048, 2048).fieldOf("forbidden_min_y").forGetter(AbyssWallPlanConfig::forbiddenMinY), + Codec.intRange(-2048, 2048).fieldOf("forbidden_max_y").forGetter(AbyssWallPlanConfig::forbiddenMaxY), + Codec.intRange(0, 256).fieldOf("final_density_inward_search").forGetter(AbyssWallPlanConfig::finalDensityInwardSearch), + Codec.intRange(0, 256).fieldOf("final_density_tolerance").forGetter(AbyssWallPlanConfig::finalDensityTolerance) ).apply(instance, AbyssWallPlanConfig::new)); public AbyssWallPlanConfig { if (maxY < minY) { throw new IllegalArgumentException("maxY must be greater than or equal to minY"); } + if (forbiddenMaxY < forbiddenMinY) { + throw new IllegalArgumentException("forbiddenMaxY must be greater than or equal to forbiddenMinY"); + } } public int maxWallAnchorOffset() { diff --git a/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanner.java b/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanner.java index 67f15f78..8557088d 100644 --- a/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanner.java +++ b/src/main/java/com/altnoir/mia/worldgen/structure/wall/AbyssWallPlanner.java @@ -25,9 +25,6 @@ import java.util.function.IntPredicate; public final class AbyssWallPlanner { - private static final int FORBIDDEN_MIN_Y = -3; - private static final int FORBIDDEN_MAX_Y = 25; - private static final int FINAL_DENSITY_TOLERANCE = 3; private static final int EMBED_SALT = 0x57414C4C; private static final double FULL_CIRCLE = Math.PI * 2.0; private static final double SECTOR_SIZE = Math.PI / 4.0; @@ -125,12 +122,13 @@ private static CachedPlan cachedPlanFor(long seed, RandomState randomState, Abys .computeIfAbsent(randomState, ignored -> new ConcurrentHashMap<>()); return plans.computeIfAbsent(new PlanKey(seed, config, abyssRadius), ignored -> { DensityFunction finalDensity = randomState.router().finalDensity(); - RadiusPredictor macroPredictor = (angle, y) -> OptionalDouble.of(nominalRadius(y, abyssRadius)); + RadiusPredictor macroPredictor = (angle, y) -> OptionalDouble.of(nominalRadius(y, abyssRadius, config)); RadiusPredictor finalDensityPredictor = (angle, y) -> { - double macroRadius = nominalRadius(y, abyssRadius); + double macroRadius = nominalRadius(y, abyssRadius, config); return OptionalDouble.of(preferredRadius( macroRadius, - FINAL_DENSITY_TOLERANCE, + config.finalDensityInwardSearch(), + config.finalDensityTolerance(), radius -> sampleDensity(finalDensity, angle, y, radius) > 0.0 )); }; @@ -195,12 +193,31 @@ public static OptionalInt findRefinedBoundary(double predictedRadius, int maxCor } public static double preferredRadius(double macroRadius, int tolerance, IntPredicate isSolid) { + return preferredRadius( + macroRadius, + AbyssWallPlanConfig.DEFAULT.finalDensityInwardSearch(), + tolerance, + isSolid + ); + } + + public static double preferredRadius( + double macroRadius, + int inwardSearch, + int tolerance, + IntPredicate isSolid + ) { + int minimumSampleRadius = Math.max(1, (int) Math.floor(macroRadius) - inwardSearch); int maximumBoundary = Math.max(1, (int) Math.floor(macroRadius + tolerance)); Map solidity = new HashMap<>(); + IntPredicate cachedSolidity = radius -> solidity.computeIfAbsent(radius, ignored -> isSolid.test(radius)); + if (cachedSolidity.test(minimumSampleRadius)) { + return Double.NaN; + } OptionalInt boundary = findFirstStableBoundary( - 1, + minimumSampleRadius, maximumBoundary + 3, - radius -> solidity.computeIfAbsent(radius, ignored -> isSolid.test(radius)) + cachedSolidity ); return boundary.isPresent() ? boundary.getAsInt() : macroRadius; } @@ -251,25 +268,28 @@ private static int currentAbyssRadius() { return MiaConfig.abyssRadius > 0 ? MiaConfig.abyssRadius : 160; } - private static double nominalRadius(int y, int abyssRadius) { + private static double nominalRadius(int y, int abyssRadius, AbyssWallPlanConfig config) { double radius = abyssRadius; - return y < FORBIDDEN_MIN_Y + return y < config.forbiddenMinY() ? radius * 0.6 : radius * Mth.clamp(1.0 + y / 512.0, 1.0, 2.0); } private static int allowedYCount(AbyssWallPlanConfig config) { int total = config.maxY() - config.minY() + 1; - int excludedMin = Math.max(config.minY(), FORBIDDEN_MIN_Y); - int excludedMax = Math.min(config.maxY(), FORBIDDEN_MAX_Y); + int excludedMin = Math.max(config.minY(), config.forbiddenMinY()); + int excludedMax = Math.min(config.maxY(), config.forbiddenMaxY()); return total - Math.max(0, excludedMax - excludedMin + 1); } private static int allowedYAt(AbyssWallPlanConfig config, int index) { - int lowerAllowedCount = Math.max(0, Math.min(config.maxY(), FORBIDDEN_MIN_Y - 1) - config.minY() + 1); + int lowerAllowedCount = Math.max( + 0, + Math.min(config.maxY(), config.forbiddenMinY() - 1) - config.minY() + 1 + ); return index < lowerAllowedCount ? config.minY() + index - : Math.max(config.minY(), FORBIDDEN_MAX_Y + 1) + index - lowerAllowedCount; + : Math.max(config.minY(), config.forbiddenMaxY() + 1) + index - lowerAllowedCount; } private static double sampleDensity(DensityFunction density, double angle, int y, int radius) {