Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6dbe208c42 | ||
|
|
a9a6f7205d | ||
|
|
98a959f621 | ||
|
|
a574008472 | ||
|
|
66b4c56eb6 | ||
|
|
a9040e8712 |
No files matched your search
+118
-4
@@ -53,10 +53,11 @@ which already forms its own islands. Cave terrain rejects it. A page-free random
|
||||
Age may also roll Floating Islands as one of its material features.
|
||||
|
||||
An unpaired Block or Fluid page chooses a compatible feature from the Age seed:
|
||||
full blocks can become Rocks or Floating Islands; source fluids can become one
|
||||
of the lake features. Flat and Void terrain can only use Floating Islands from
|
||||
this pool, while Cave terrain has no compatible material feature. A page-free
|
||||
random Age rolls zero, one, or two distinct compatible features, choosing each
|
||||
full blocks can become Rocks, Floating Islands, Tendrils, Spheres, or Spikes; source fluids can become one
|
||||
of the lake features. Flat terrain can use Floating Islands, Tendrils, Spheres,
|
||||
or Spikes; Void can use Floating Islands, Tendrils, or Spheres; Cave can use
|
||||
Spheres or Tendrils. A page-free
|
||||
random Age rolls zero, one, or two distinct compatible material features, choosing each
|
||||
material from the Block or Fluid Pages pool. The choices are saved on the Age
|
||||
definition, so opening the Age again does not reroll them.
|
||||
Repeated Block/Fluid and Feature pages remain separate entries in authored
|
||||
@@ -64,6 +65,65 @@ writing. Each unpaired material gets its own compatible feature, and repeated
|
||||
decorations use separate placement rolls. Repeated unmodified feature pages
|
||||
choose different compatible materials until their material pool is exhausted.
|
||||
|
||||
Structure pages are separate Feature symbols. Legacy's Dungeons, Mineshafts,
|
||||
Nether Fortresses, Strongholds, and Villages have writable pages, as do modern
|
||||
Trial Chambers, Bastion Remnants, Buried Treasure, Shipwrecks, Ocean Ruins,
|
||||
Pillager Outposts, Ruined Portals, Nether Fossils, Ancient Cities, and Trail
|
||||
Ruins. They use Minecraft's 1.21.1 structure templates and placement rules,
|
||||
not a recreation of 1.12.2 structure geometry. A page enables only its own
|
||||
structure set; absent pages do not inherit these structures from a biome.
|
||||
Nether Fortresses and Bastions use separate sets instead of vanilla's shared
|
||||
Nether-complexes set, so writing one does not enable the other. Dungeons use
|
||||
the vanilla monster-room feature with eight attempts per chunk; unpaged Ages
|
||||
do not inherit biome-default monster-room attempts. A page-free random Age
|
||||
may choose zero to two distinct compatible structure types, frozen on creation.
|
||||
An authored structure needs a compatible biome in the Age's selected pool.
|
||||
Fortresses, Bastions, and Nether Fossils require Cave terrain plus a Nether
|
||||
biome; surface-only structures reject Cave terrain. Void rejects all of these
|
||||
structure pages. Villages and Strongholds can also be written into Flat Ages.
|
||||
|
||||
For visual testing, write a Standard/Plains/Single Age with Mineshafts,
|
||||
Villages, and Trial Chambers, then use `/locate structure` in that Age to find
|
||||
each. An unwritten Pillager Outpost should not locate there. Test a Cave Age
|
||||
with Nether Wastes/Single and either the Nether Fortresses or Bastion page:
|
||||
only the selected type should locate. Ocean structures need their matching
|
||||
ocean or beach biome page; Ancient Cities need Deep Dark. Check the new pages
|
||||
in Rivencraft's Feature creative tab and JEI as well.
|
||||
|
||||
**Dense Ores** is an authored-only Feature page. Each copy replays one extra
|
||||
pass of every ore placement registered in the Age's nearby biomes, using that
|
||||
placement's existing height, vein-size, rarity, and biome rules. It therefore
|
||||
also densifies conventional mod-added ores without hard-coded mod IDs. Output
|
||||
blocks tagged `c:ores` qualify; OreConfiguration outputs with `_ore` in their
|
||||
registry name also qualify. A mod with a custom, non-OreConfiguration placed
|
||||
feature can opt in by tagging that feature `rivencraft:dense_ores`. Non-ore
|
||||
stone/dirt blobs are not repeated. Flat and Void reject the page, as does a
|
||||
biome pool without any ore placements. Page-free random Ages never roll Dense
|
||||
Ores. The extra pass applies only as new chunks generate; it does not retrofit
|
||||
existing chunks or discover ore systems that bypass biome placed features.
|
||||
|
||||
For an in-game check, write a Standard/Plains/Single Age with Dense Ores and
|
||||
verify that new underground chunks contain visibly more vanilla ore while
|
||||
stone/dirt blob frequency remains unchanged. With an ore-adding mod installed,
|
||||
write an Age using a biome where that mod registers its ore and verify the ore
|
||||
also appears more often. Check that Flat plus Dense Ores is rejected by the
|
||||
Binder and that a random, Link-Panel-only Age has no Dense Ores feature page.
|
||||
|
||||
**Obelisks** adds Legacy-shaped, rare 4×4 plinths with a 2×2 shaft rising 12
|
||||
blocks, at most one attempt per 128 chunks per page. A preceding full-block
|
||||
Block page chooses the material; otherwise the Age seed chooses from valid
|
||||
block pages. Repeated Obelisks pages make independent attempts. Generation is
|
||||
deterministic and stays within one chunk; it requires a reasonably level,
|
||||
exposed solid surface, so Cave and Void terrain reject the page. Standard,
|
||||
Amplified, Sky Islands, and Flat terrain permit it. Page-free random surface
|
||||
Ages can roll Obelisks as one of their material features.
|
||||
|
||||
For a visual check, write a Standard or Flat Age with an Obelisks page and
|
||||
search newly generated terrain for the 4×4 base and 12-block shaft. Try a
|
||||
Block page such as Obsidian immediately before Obelisks to verify its material
|
||||
changes. The Binder should reject Obelisks in Cave and Void Ages. Placement
|
||||
is deliberately rare, so several hundred fresh chunks may be needed.
|
||||
|
||||
For an in-game visual check, bind Descriptive Books with a Link Panel,
|
||||
one of Standard, Amplified, Sky Islands, Flat, or Void terrain, and the Floating Islands
|
||||
feature page in that order. Fly through each Age at roughly Y=170–220 and
|
||||
@@ -73,6 +133,60 @@ common than in the first test build (one source chunk in 128 instead of 64).
|
||||
Sky Islands should retain its terrain islands and gain occasional additional
|
||||
feature islands. Try Cave terrain too; the Binder should reject that pairing.
|
||||
|
||||
**Skylands** is a separate subtractive feature, not the Sky Islands terrain
|
||||
profile or the additive Floating Islands feature. Following Legacy's primer
|
||||
filter, it removes base terrain below a smooth, seven-octave cutoff around
|
||||
world Y=76 and removes liquid above the cutoff. The selected terrain still
|
||||
generates first; Floating Islands and Tendrils are added afterward, so they
|
||||
can coexist with Skylands. Skylands has no Block-page modifier. It can be
|
||||
authored on every terrain, though Flat loses its low ground and Void has no
|
||||
base terrain to change. Page-free random noise Ages have a seeded one-in-eight
|
||||
chance to gain Skylands; random Flat and Void Ages do not roll this ineffective
|
||||
filter. The cloud-height and horizon changes from Legacy are not included yet.
|
||||
|
||||
For an in-game check, compare Standard terrain with and without a Skylands
|
||||
page. Skylands should leave upper hills suspended over open space, while a
|
||||
Floating Islands page instead adds isolated high islands without removing the
|
||||
ground below. Try Sky Islands terrain plus Skylands to confirm the page filters
|
||||
the terrain rather than replacing its generator. A book with both Skylands and
|
||||
Floating Islands should retain the added feature islands above the cutoff.
|
||||
|
||||
The **Tendrils** feature adapts Legacy's winding, branching cave-node geometry
|
||||
into solid tubes. It is additive on all current terrain families, including
|
||||
Flat, Void, and Cave; it does not change biome selection. A preceding full-
|
||||
collision Block page chooses the tendril material. Otherwise the Age seed
|
||||
chooses a valid Block Pages material, consistent with other authored material
|
||||
features. Page-free random Ages can select Tendrils as a solid-material feature,
|
||||
including on Cave terrain where the surface material features are unavailable.
|
||||
The shape and placement use only the Age seed and source-chunk coordinates, so
|
||||
revisiting an Age recreates the same geometry.
|
||||
|
||||
**Spheres** and **Spikes** are two separate Feature pages. Spheres adds
|
||||
winding chains of solid sphere nodes within terrain generation; like Tendrils,
|
||||
it works on all terrain families, even Void and Cave. Spikes is a later,
|
||||
rarer surface decoration (roughly one attempt per 18 chunks) that rises from
|
||||
solid ground on Standard, Amplified, Sky Islands, or Flat terrain. Cave and
|
||||
Void reject the Spikes page because they do not provide a suitable exposed
|
||||
surface. Each page accepts its own immediately preceding full-collision Block
|
||||
page; without one, the Age seed chooses a compatible material. The pages
|
||||
remain distinct in the catalog, Creative menu, JEI, ordered writing, and
|
||||
saved Age definition. Page-free random Ages can select either where compatible.
|
||||
|
||||
For an in-game check, make separate Void/Spheres and Flat/Spikes books with
|
||||
Glowstone Block pages directly before their respective Feature pages. Spheres
|
||||
should form connected bright globes across chunk boundaries in Void; Spikes
|
||||
should appear as rare upward clusters rooted on the Flat surface. Revisit the
|
||||
same Ages to confirm stable locations. Check that a Void/Spikes book is
|
||||
rejected, and that Spheres and Spikes remain two independently selectable
|
||||
pages in Creative and JEI.
|
||||
|
||||
For a clear visual check, bind a Void Age with a Glowstone Block page followed
|
||||
by Tendrils. Fly around world Y=0–120 across several chunks: branching solid
|
||||
forms should appear in the otherwise empty dimension, continue across chunk
|
||||
borders, and remain in place after leaving and returning. Repeat with a lone
|
||||
Tendrils page to confirm its seed-chosen material, and with Cave terrain to
|
||||
confirm the feature does not require a surface biome.
|
||||
|
||||
## Written biome selection (generation version 16)
|
||||
|
||||
Biome pages follow the terrain page and precede the distribution-control page
|
||||
|
||||
@@ -18,7 +18,7 @@ Legacy does **not** reject a book just because it omits terrain, biome, weather,
|
||||
| `WeatherNorm`, `Fast`, `Slow`, `Off`, `On`, `Cloudy`, `Rain`, `Snow`, `Storm` | `AgeTraits.Weather` has matching names except Legacy `On` maps to `ALWAYS` | Compile weather semantics separately from biome selection. `Snow` must remain deferred until optional season integration; do not silently substitute another weather mode. |
|
||||
| Suns, moons, starfields; angle, phase, period, color/gradient/sunset modifiers | One `celestialPeriod`, `celestialAngle`, `celestialPhase` on `AgeTraits` | Compile only the representable single-cycle subset first. Multiple celestial bodies, independent periods, custom stars, gradients and sunsets need a richer definition and renderer, not flattening into one value. |
|
||||
| Sky/fog/cloud/grass/foliage/water colors, natural variants, horizon, rainbow | Existing sky/dimension visuals and preview | Require an explicit modern client/biome-color design; metadata-only symbol pages must not pretend to change rendering. |
|
||||
| Caves, ores, huge trees, lakes, floating islands, tendrils, skylands, spheres, spikes, crystals; vanilla villages, strongholds, mineshafts, dungeons, fortresses, obelisks, fissures | Current cave profile and vanilla data-pack generation | Compile only features for which registered 1.21.1 placed features/structures exist. Defer bespoke structures and polish to milestone 7. |
|
||||
| Caves, ores, huge trees, lakes, floating islands, tendrils, skylands, spheres, spikes, crystals; vanilla villages, strongholds, mineshafts, dungeons, fortresses, obelisks, fissures | Current cave profile, authored Floating Islands/Tendrils/Skylands/Spheres/Spikes/Obelisks and Dense Ores, and page-gated vanilla Dungeons/Mineshafts/Nether Fortresses/Strongholds/Villages | The five Legacy vanilla structure symbols gate Minecraft 1.21.1 generation. Dungeons use monster-room placement; the others use vanilla structure sets. Obelisks use a bespoke 4×4 plinth and 2×12×2 shaft with safe modern placement. Fissures, crystals, and exact 1.12 structure geometry remain separate work. |
|
||||
| `EnvAccel`, explosions, lightning, meteors, scorching; PvP and dummy suppression pages | No equivalent Age-definition effect | Defer environmental hazards/instability effects to milestone 6, PvP rule to a separately scoped server policy; retain authored IDs without fake behavior. |
|
||||
|
||||
The Legacy symbol list and page names come from [ModSymbols](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/ModSymbols.java#L129-L240) and [SymbolRules](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/SymbolRules.java#L19-L137). Rivencraft's [catalog](../src/main/java/net/dnorberg/rivencraft/writing/SymbolCatalog.java) currently holds display metadata only; its built-in set is just standard terrain, native biomes, normal weather, normal lighting, normal time, and caves. [OrderedWriting](../src/main/java/net/dnorberg/rivencraft/writing/OrderedWriting.java) preserves page order, but no compiler yet connects it to [AgeTraits](../src/main/java/net/dnorberg/rivencraft/book/AgeTraits.java) or [Age profiles](AGE_PROFILES.md).
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ mod_name=Rivencraft
|
||||
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
||||
mod_license=LGPL-3.0-or-later
|
||||
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
||||
mod_version=0.4.11.7
|
||||
mod_version=0.4.17.0
|
||||
immersive_portals_version=6.0.7
|
||||
terrablender_version=4.1.0.8
|
||||
serene_seasons_version_id=B6nTjh4b
|
||||
|
||||
@@ -329,7 +329,8 @@ public final class AgeDimensionService {
|
||||
Registries.NOISE_SETTINGS, profile.noiseSettings());
|
||||
NoiseGeneratorSettings baseSettings = noiseSettings.getOrThrow(settingsKey).value();
|
||||
Holder<NoiseGeneratorSettings> settings = Holder.direct(withBiomeMaterialRules(baseSettings));
|
||||
return features.isEmpty() ? new NoiseBasedChunkGenerator(biomeSource, settings)
|
||||
return age.generationVersion() < AgeDefinition.CURRENT_GENERATION_VERSION
|
||||
? new NoiseBasedChunkGenerator(biomeSource, settings)
|
||||
: new AgeFeatureChunkGenerator(biomeSource, settings, features, age.seed());
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,10 @@ import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.writing.BlockSymbolPages;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.dnorberg.rivencraft.writing.SymbolSemantics;
|
||||
import net.dnorberg.rivencraft.writing.WritingGrammar;
|
||||
import net.dnorberg.rivencraft.world.AgeDenseOres;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
@@ -24,9 +26,12 @@ import net.minecraft.world.level.block.Blocks;
|
||||
|
||||
/** Validates and freezes supported feature pages and their preceding material modifiers. */
|
||||
public final class AgeFeatureSelection {
|
||||
private static final List<ResourceLocation> BLOCK_FEATURES = List.of(id("rocks"), id("floating_islands"));
|
||||
private static final List<ResourceLocation> BLOCK_FEATURES = List.of(
|
||||
id("rocks"), id("floating_islands"), id("tendrils"), id("spheres"), id("spikes"),
|
||||
id("obelisks"));
|
||||
private static final List<ResourceLocation> FLUID_FEATURES = List.of(
|
||||
id("surface_lakes"), id("deep_lakes"), id("lava_lakes"));
|
||||
private static final long RANDOM_SKYLANDS_SALT = 0x534B594C414E4453L;
|
||||
|
||||
private AgeFeatureSelection() {}
|
||||
|
||||
@@ -47,6 +52,10 @@ public final class AgeFeatureSelection {
|
||||
.map(symbol -> symbol.semantics()).toList(), profile, ageSeed);
|
||||
for (Choice choice : choices) {
|
||||
String error = error(choice.id(), profile, registries);
|
||||
if (error == null && AgeStructureSelection.feature(choice.id()))
|
||||
error = AgeStructureSelection.error(choice.id(), profile, biomes, registries);
|
||||
if (error == null && choice.id().equals(AgeDenseOres.ID))
|
||||
error = AgeDenseOres.error(profile, biomes, registries);
|
||||
if (error == null) error = materialError(choice);
|
||||
if (error != null) throw new IllegalArgumentException(error);
|
||||
if (surfaceVegetation(choice.id()) && biomes.biomes().stream()
|
||||
@@ -112,7 +121,11 @@ public final class AgeFeatureSelection {
|
||||
}
|
||||
|
||||
private static boolean compatible(ResourceLocation feature, AgeRuntimeProfile profile) {
|
||||
if (tendrils(feature) || spheres(feature)) return true;
|
||||
if (obelisks(feature)) return !AgeBiomeSelection.isCave(profile)
|
||||
&& profile.terrain() != AgeRuntimeProfile.Terrain.VOID;
|
||||
if (AgeBiomeSelection.isCave(profile)) return false;
|
||||
if (spikes(feature)) return profile.terrain() != AgeRuntimeProfile.Terrain.VOID;
|
||||
return floatingIslands(feature) || profile.terrain() == AgeRuntimeProfile.Terrain.NOISE;
|
||||
}
|
||||
|
||||
@@ -124,7 +137,10 @@ public final class AgeFeatureSelection {
|
||||
for (Choice choice : choices(plan.symbols().stream().map(symbol -> symbol.semantics()).toList(),
|
||||
plan.runtimeProfile(), plan.ageSeed())) {
|
||||
if (choice.material().isPresent()
|
||||
|| !isRock(choice.id()) && !isLake(choice.id()) && !floatingIslands(choice.id())) {
|
||||
|| !isRock(choice.id()) && !isLake(choice.id())
|
||||
&& !floatingIslands(choice.id()) && !tendrils(choice.id())
|
||||
&& !spheres(choice.id()) && !spikes(choice.id())
|
||||
&& !obelisks(choice.id())) {
|
||||
resolved.add(choice);
|
||||
continue;
|
||||
}
|
||||
@@ -136,7 +152,7 @@ public final class AgeFeatureSelection {
|
||||
return List.copyOf(resolved);
|
||||
}
|
||||
|
||||
/** Page-free Ages roll up to two compatible material/feature pairs once at creation. */
|
||||
/** Page-free Ages roll up to two material features, plus rare Skylands, once at creation. */
|
||||
public static List<Choice> randomChoices(long ageSeed, AgeRuntimeProfile profile, RegistryAccess registries) {
|
||||
Random random = new Random(ageSeed ^ 0x4D4154455249414CL);
|
||||
int count = random.nextInt(3);
|
||||
@@ -147,18 +163,24 @@ public final class AgeFeatureSelection {
|
||||
List<Choice> choices = new ArrayList<>();
|
||||
for (ResourceLocation feature : features) {
|
||||
if (choices.size() >= count) break;
|
||||
var category = isRock(feature) || floatingIslands(feature)
|
||||
var category = solidFeature(feature)
|
||||
? BlockSymbolPages.CATEGORY : BlockSymbolPages.FLUID_CATEGORY;
|
||||
List<ResourceLocation> materials = BlockSymbolPages.availableBlocks(category).stream()
|
||||
.filter(id -> materialError(new Choice(feature, Optional.of(id))) == null).toList();
|
||||
if (!materials.isEmpty()) choices.add(new Choice(feature,
|
||||
Optional.of(materials.get(random.nextInt(materials.size())))));
|
||||
}
|
||||
if (profile.terrain() == AgeRuntimeProfile.Terrain.NOISE
|
||||
&& Math.floorMod(mix(ageSeed ^ RANDOM_SKYLANDS_SALT), 8) == 0)
|
||||
choices.add(new Choice(id("skylands"), Optional.empty()));
|
||||
AgeBiomeSelection biomes = AgeBiomeSelection.compile(WritingGrammar.parse(OrderedWriting.EMPTY),
|
||||
AgeTraits.select(ageSeed), profile, ageSeed, registries);
|
||||
choices.addAll(AgeStructureSelection.randomChoices(ageSeed, profile, biomes, registries));
|
||||
return List.copyOf(choices);
|
||||
}
|
||||
|
||||
private static List<ResourceLocation> shuffledMaterials(long ageSeed, ResourceLocation feature) {
|
||||
var category = isRock(feature) || floatingIslands(feature)
|
||||
var category = solidFeature(feature)
|
||||
? BlockSymbolPages.CATEGORY : BlockSymbolPages.FLUID_CATEGORY;
|
||||
List<ResourceLocation> pool = new ArrayList<>(BlockSymbolPages.availableBlocks(category).stream()
|
||||
.filter(id -> materialError(new Choice(feature, Optional.of(id))) == null).toList());
|
||||
@@ -178,6 +200,23 @@ public final class AgeFeatureSelection {
|
||||
}
|
||||
|
||||
public static String error(ResourceLocation choice, AgeRuntimeProfile profile, RegistryAccess registries) {
|
||||
if (AgeStructureSelection.feature(choice))
|
||||
return AgeStructureSelection.error(choice, profile, null, registries);
|
||||
if (choice.equals(AgeDenseOres.ID))
|
||||
return AgeDenseOres.error(profile, null, registries);
|
||||
if (skylands(choice)) return null;
|
||||
if (tendrils(choice)) return null;
|
||||
if (spheres(choice)) return null;
|
||||
if (spikes(choice)) {
|
||||
if (AgeBiomeSelection.isCave(profile) || profile.terrain() == AgeRuntimeProfile.Terrain.VOID)
|
||||
return "Feature " + choice + " needs an exposed solid surface, not Cave or Void terrain";
|
||||
return null;
|
||||
}
|
||||
if (obelisks(choice)) {
|
||||
if (AgeBiomeSelection.isCave(profile) || profile.terrain() == AgeRuntimeProfile.Terrain.VOID)
|
||||
return "Feature " + choice + " needs exposed solid surface terrain, not Cave or Void";
|
||||
return null;
|
||||
}
|
||||
if (floatingIslands(choice)) {
|
||||
if (AgeBiomeSelection.isCave(profile))
|
||||
return "Feature " + choice + " cannot form above Cave terrain";
|
||||
@@ -216,7 +255,7 @@ public final class AgeFeatureSelection {
|
||||
if (block.isEmpty() || block.orElseThrow() == Blocks.AIR) {
|
||||
return "Feature " + choice.id() + " references missing block material " + material;
|
||||
}
|
||||
if (isRock(choice.id()) || floatingIslands(choice.id())) {
|
||||
if (solidFeature(choice.id())) {
|
||||
if (!BlockSymbolPages.isRockMaterial(block.orElseThrow())) {
|
||||
return "Feature " + choice.id() + " needs a full solid block without a block entity: " + material;
|
||||
}
|
||||
@@ -256,6 +295,31 @@ public final class AgeFeatureSelection {
|
||||
return choice.equals(ResourceLocation.fromNamespaceAndPath("rivencraft", "floating_islands"));
|
||||
}
|
||||
|
||||
public static boolean tendrils(ResourceLocation choice) {
|
||||
return choice.equals(id("tendrils"));
|
||||
}
|
||||
|
||||
public static boolean spheres(ResourceLocation choice) {
|
||||
return choice.equals(id("spheres"));
|
||||
}
|
||||
|
||||
public static boolean spikes(ResourceLocation choice) {
|
||||
return choice.equals(id("spikes"));
|
||||
}
|
||||
|
||||
public static boolean obelisks(ResourceLocation choice) {
|
||||
return choice.equals(id("obelisks"));
|
||||
}
|
||||
|
||||
private static boolean solidFeature(ResourceLocation choice) {
|
||||
return isRock(choice) || floatingIslands(choice) || tendrils(choice)
|
||||
|| spheres(choice) || spikes(choice) || obelisks(choice);
|
||||
}
|
||||
|
||||
public static boolean skylands(ResourceLocation choice) {
|
||||
return choice.equals(id("skylands"));
|
||||
}
|
||||
|
||||
public static boolean supportsSurfaceVegetation(ResourceLocation id, RegistryAccess registries) {
|
||||
var biome = registries.lookupOrThrow(Registries.BIOME)
|
||||
.get(ResourceKey.create(Registries.BIOME, id));
|
||||
@@ -275,6 +339,7 @@ public final class AgeFeatureSelection {
|
||||
case "trees" -> ResourceLocation.withDefaultNamespace("trees_plains");
|
||||
case "huge_trees" -> ResourceLocation.withDefaultNamespace("trees_old_growth_spruce_taiga");
|
||||
case "rocks" -> ResourceLocation.withDefaultNamespace("forest_rock");
|
||||
case "dungeons" -> ResourceLocation.fromNamespaceAndPath("rivencraft", "dungeons");
|
||||
case "surface_lakes", "lava_lakes" -> ResourceLocation.withDefaultNamespace("lake_lava_surface");
|
||||
case "deep_lakes" -> ResourceLocation.withDefaultNamespace("lake_lava_underground");
|
||||
default -> null;
|
||||
|
||||
@@ -68,12 +68,22 @@ public final class AgePersistenceGameTests {
|
||||
helper.assertTrue(pageFree.plan() == null && panelOnly.plan() == null,
|
||||
"Page-free and Link-Panel-only books must retain the random Age path");
|
||||
for (AgeDefinition age : List.of(pageFree, panelOnly)) {
|
||||
helper.assertTrue(age.randomFeatures().size() <= 2,
|
||||
helper.assertTrue(age.randomFeatures().stream().filter(choice -> choice.material().isPresent())
|
||||
.count() <= 2,
|
||||
"A random Age may roll at most two material features");
|
||||
helper.assertTrue(age.randomFeatures().stream().filter(choice ->
|
||||
AgeFeatureSelection.skylands(choice.id())).count() <= 1,
|
||||
"A random Age may roll at most one Skylands filter");
|
||||
helper.assertTrue(age.randomFeatures().stream().filter(choice ->
|
||||
AgeStructureSelection.feature(choice.id())).count() <= 2,
|
||||
"A random Age may roll at most two distinct structure features");
|
||||
for (AgeFeatureSelection.Choice choice : age.randomFeatures()) {
|
||||
helper.assertTrue(choice.material().isPresent()
|
||||
helper.assertTrue(AgeFeatureSelection.skylands(choice.id())
|
||||
|| AgeStructureSelection.feature(choice.id())
|
||||
? choice.material().isEmpty()
|
||||
: choice.material().isPresent()
|
||||
&& AgeFeatureSelection.materialError(choice) == null,
|
||||
"Random features must each carry a valid block or fluid material");
|
||||
"Random material features need valid blocks or fluids; Skylands and structures need none");
|
||||
}
|
||||
}
|
||||
AgeRegistrySavedData registry = AgeRegistrySavedData.get(helper.getLevel());
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||
import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.levelgen.structure.BuiltinStructureSets;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
|
||||
/** Authored structure pages gate their own structure sets without enabling unrelated structures. */
|
||||
@GameTestHolder(Rivencraft.MOD_ID)
|
||||
@PrefixGameTestTemplate(false)
|
||||
public final class AgeStructureGameTests {
|
||||
private static final Map<String, ResourceLocation> SETS = Map.ofEntries(
|
||||
Map.entry("mineshafts", BuiltinStructureSets.MINESHAFTS.location()),
|
||||
Map.entry("nether_fortresses", id("fortresses")),
|
||||
Map.entry("strongholds", BuiltinStructureSets.STRONGHOLDS.location()),
|
||||
Map.entry("villages", BuiltinStructureSets.VILLAGES.location()),
|
||||
Map.entry("trial_chambers", BuiltinStructureSets.TRIAL_CHAMBERS.location()),
|
||||
Map.entry("bastions", id("bastions")),
|
||||
Map.entry("buried_treasure", BuiltinStructureSets.BURIED_TREASURES.location()),
|
||||
Map.entry("shipwrecks", BuiltinStructureSets.SHIPWRECKS.location()),
|
||||
Map.entry("ocean_ruins", BuiltinStructureSets.OCEAN_RUINS.location()),
|
||||
Map.entry("pillager_outposts", BuiltinStructureSets.PILLAGER_OUTPOSTS.location()),
|
||||
Map.entry("ruined_portals", BuiltinStructureSets.RUINED_PORTALS.location()),
|
||||
Map.entry("nether_fossils", BuiltinStructureSets.NETHER_FOSSILS.location()),
|
||||
Map.entry("ancient_cities", BuiltinStructureSets.ANCIENT_CITIES.location()),
|
||||
Map.entry("trail_ruins", BuiltinStructureSets.TRAIL_RUINS.location()));
|
||||
|
||||
private AgeStructureGameTests() {}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void structurePagesGateDistinctVanillaSets(GameTestHelper helper) {
|
||||
var lookup = helper.getLevel().registryAccess().lookupOrThrow(Registries.STRUCTURE_SET);
|
||||
for (var entry : SETS.entrySet()) {
|
||||
var setKey = ResourceKey.create(Registries.STRUCTURE_SET, entry.getValue());
|
||||
helper.assertTrue(lookup.get(setKey).isPresent(), entry.getKey() + " needs a registered structure set");
|
||||
var filtered = AgeStructureSelection.filteredLookup(lookup,
|
||||
List.of(new AgeFeatureSelection.Choice(id(entry.getKey()), Optional.empty())));
|
||||
helper.assertTrue(filtered.get(setKey).isPresent(), entry.getKey() + " must enable its structure set");
|
||||
helper.assertTrue(filtered.get(ResourceKey.create(Registries.STRUCTURE_SET,
|
||||
BuiltinStructureSets.NETHER_COMPLEXES.location())).isEmpty(),
|
||||
"Vanilla Nether complexes must stay disabled to keep fortresses and bastions separate");
|
||||
for (var other : SETS.entrySet()) {
|
||||
if (other.getKey().equals(entry.getKey())) continue;
|
||||
helper.assertTrue(filtered.get(ResourceKey.create(Registries.STRUCTURE_SET,
|
||||
other.getValue())).isEmpty(),
|
||||
entry.getKey() + " must not enable " + other.getKey());
|
||||
}
|
||||
}
|
||||
helper.assertTrue(AgeStructureSelection.filteredLookup(lookup, List.of()).listElements()
|
||||
.noneMatch(holder -> SETS.containsValue(holder.key().location())),
|
||||
"An Age without structure pages must not inherit these structures from vanilla biomes");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void authoredPagesPersistAndRejectIncompatibleTerrain(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
OrderedWriting written = new OrderedWriting(List.of(WritingEntry.linkPanel(), page("terrain/standard"),
|
||||
page("biome/plains"), page("biome/single"), page("feature/mineshafts"),
|
||||
page("feature/villages"), page("feature/trial_chambers")));
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(written, 149L, AgeGenerationProfiles.STANDARD,
|
||||
AgeTraits.select(149L), registries);
|
||||
List<AgeFeatureSelection.Choice> expected = AgeFeatureSelection.resolved(plan);
|
||||
helper.assertValueEqual(expected.stream().map(AgeFeatureSelection.Choice::id).toList(),
|
||||
List.of(id("mineshafts"), id("villages"), id("trial_chambers")),
|
||||
"Authored structure pages must retain their order");
|
||||
helper.assertValueEqual(AgeFeatureSelection.resolved(AgeGenerationPlan.load(plan.save())), expected,
|
||||
"Authored structure choices must survive an Age plan reload");
|
||||
var filtered = AgeStructureSelection.filteredLookup(registries.lookupOrThrow(Registries.STRUCTURE_SET), expected);
|
||||
helper.assertTrue(filtered.get(ResourceKey.create(Registries.STRUCTURE_SET,
|
||||
BuiltinStructureSets.VILLAGES.location())).isPresent(), "Village set must be enabled");
|
||||
helper.assertTrue(filtered.get(ResourceKey.create(Registries.STRUCTURE_SET,
|
||||
BuiltinStructureSets.PILLAGER_OUTPOSTS.location())).isEmpty(), "Unwritten outposts must stay disabled");
|
||||
String error = AgeStructureSelection.error(id("bastions"),
|
||||
AgeRuntimeProfiles.profiles().get(id("standard")), null, registries);
|
||||
helper.assertTrue(error != null && error.contains("Cave terrain"),
|
||||
"Bastion page must not silently run on Standard terrain");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
private static ResourceLocation id(String path) {
|
||||
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||
}
|
||||
|
||||
private static WritingEntry page(String path) {
|
||||
return WritingEntry.symbol(new SymbolReference(id(path)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.world.level.levelgen.structure.BuiltinStructureSets;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
|
||||
/** Frozen Age structure-page choices and their vanilla structure-set gate. */
|
||||
public final class AgeStructureSelection {
|
||||
private static final ResourceLocation FORTRESSES = ResourceLocation.fromNamespaceAndPath("rivencraft", "fortresses");
|
||||
private static final ResourceLocation BASTIONS = ResourceLocation.fromNamespaceAndPath("rivencraft", "bastions");
|
||||
private static final List<ResourceLocation> FEATURES = List.of(
|
||||
id("dungeons"), id("mineshafts"), id("nether_fortresses"), id("strongholds"),
|
||||
id("villages"), id("trial_chambers"), id("bastions"), id("buried_treasure"),
|
||||
id("shipwrecks"), id("ocean_ruins"), id("pillager_outposts"), id("ruined_portals"),
|
||||
id("nether_fossils"), id("ancient_cities"), id("trail_ruins"));
|
||||
private static final Map<ResourceLocation, ResourceLocation> STRUCTURE_SETS = Map.ofEntries(
|
||||
Map.entry(id("mineshafts"), BuiltinStructureSets.MINESHAFTS.location()),
|
||||
Map.entry(id("nether_fortresses"), FORTRESSES),
|
||||
Map.entry(id("strongholds"), BuiltinStructureSets.STRONGHOLDS.location()),
|
||||
Map.entry(id("villages"), BuiltinStructureSets.VILLAGES.location()),
|
||||
Map.entry(id("trial_chambers"), BuiltinStructureSets.TRIAL_CHAMBERS.location()),
|
||||
Map.entry(id("bastions"), BASTIONS),
|
||||
Map.entry(id("buried_treasure"), BuiltinStructureSets.BURIED_TREASURES.location()),
|
||||
Map.entry(id("shipwrecks"), BuiltinStructureSets.SHIPWRECKS.location()),
|
||||
Map.entry(id("ocean_ruins"), BuiltinStructureSets.OCEAN_RUINS.location()),
|
||||
Map.entry(id("pillager_outposts"), BuiltinStructureSets.PILLAGER_OUTPOSTS.location()),
|
||||
Map.entry(id("ruined_portals"), BuiltinStructureSets.RUINED_PORTALS.location()),
|
||||
Map.entry(id("nether_fossils"), BuiltinStructureSets.NETHER_FOSSILS.location()),
|
||||
Map.entry(id("ancient_cities"), BuiltinStructureSets.ANCIENT_CITIES.location()),
|
||||
Map.entry(id("trail_ruins"), BuiltinStructureSets.TRAIL_RUINS.location()));
|
||||
private static final Set<ResourceLocation> CONTROLLED_SETS = Set.of(
|
||||
BuiltinStructureSets.MINESHAFTS.location(), BuiltinStructureSets.NETHER_COMPLEXES.location(),
|
||||
BuiltinStructureSets.STRONGHOLDS.location(), BuiltinStructureSets.VILLAGES.location(),
|
||||
BuiltinStructureSets.TRIAL_CHAMBERS.location(), FORTRESSES, BASTIONS,
|
||||
BuiltinStructureSets.BURIED_TREASURES.location(), BuiltinStructureSets.SHIPWRECKS.location(),
|
||||
BuiltinStructureSets.OCEAN_RUINS.location(), BuiltinStructureSets.PILLAGER_OUTPOSTS.location(),
|
||||
BuiltinStructureSets.RUINED_PORTALS.location(), BuiltinStructureSets.NETHER_FOSSILS.location(),
|
||||
BuiltinStructureSets.ANCIENT_CITIES.location(), BuiltinStructureSets.TRAIL_RUINS.location());
|
||||
|
||||
private AgeStructureSelection() {}
|
||||
|
||||
private static ResourceLocation id(String path) {
|
||||
return ResourceLocation.fromNamespaceAndPath("rivencraft", path);
|
||||
}
|
||||
|
||||
public static boolean feature(ResourceLocation id) {
|
||||
return FEATURES.contains(id);
|
||||
}
|
||||
|
||||
public static boolean dungeons(ResourceLocation id) {
|
||||
return id.equals(id("dungeons"));
|
||||
}
|
||||
|
||||
private static ResourceLocation setId(ResourceLocation feature) {
|
||||
return STRUCTURE_SETS.get(feature);
|
||||
}
|
||||
|
||||
public static String error(ResourceLocation feature, AgeRuntimeProfile profile,
|
||||
AgeBiomeSelection biomes, RegistryAccess registries) {
|
||||
if (!feature(feature)) return "Unsupported structure feature " + feature;
|
||||
if (profile.terrain() == AgeRuntimeProfile.Terrain.VOID)
|
||||
return "Feature " + feature + " requires terrain, not Void";
|
||||
if (dungeons(feature)) {
|
||||
if (profile.terrain() != AgeRuntimeProfile.Terrain.NOISE)
|
||||
return "Feature " + feature + " needs enclosed noise terrain, not Flat";
|
||||
return placedExists(registries, id("dungeons")) ? null
|
||||
: "Feature " + feature + " requires missing placed feature rivencraft:dungeons";
|
||||
}
|
||||
if (!feature.equals(id("villages")) && !feature.equals(id("strongholds"))
|
||||
&& profile.terrain() != AgeRuntimeProfile.Terrain.NOISE)
|
||||
return "Feature " + feature + " needs noise terrain, not Flat";
|
||||
if (feature.equals(id("nether_fortresses")) || feature.equals(id("bastions"))
|
||||
|| feature.equals(id("nether_fossils"))) {
|
||||
if (!AgeBiomeSelection.isCave(profile))
|
||||
return "Feature " + feature + " requires Cave terrain with a Nether biome";
|
||||
} else if (feature.equals(id("villages")) || feature.equals(id("strongholds"))
|
||||
|| feature.equals(id("buried_treasure")) || feature.equals(id("shipwrecks"))
|
||||
|| feature.equals(id("ocean_ruins")) || feature.equals(id("pillager_outposts"))
|
||||
|| feature.equals(id("trail_ruins"))) {
|
||||
if (AgeBiomeSelection.isCave(profile))
|
||||
return "Feature " + feature + " requires surface terrain, not Cave";
|
||||
}
|
||||
ResourceLocation setId = setId(feature);
|
||||
var set = registries.lookupOrThrow(Registries.STRUCTURE_SET)
|
||||
.get(ResourceKey.create(Registries.STRUCTURE_SET, setId));
|
||||
if (set.isEmpty()) return "Feature " + feature + " requires missing structure set " + setId;
|
||||
if (biomes == null) return null;
|
||||
Set<ResourceLocation> selected = Set.copyOf(biomes.biomes());
|
||||
boolean compatible = set.orElseThrow().value().structures().stream()
|
||||
.flatMap(entry -> entry.structure().value().biomes().stream())
|
||||
.map(holder -> holder.unwrapKey().map(ResourceKey::location).orElse(null))
|
||||
.anyMatch(selected::contains);
|
||||
return compatible ? null : "Feature " + feature
|
||||
+ " needs at least one biome allowed by structure set " + setId;
|
||||
}
|
||||
|
||||
private static boolean placedExists(RegistryAccess registries, ResourceLocation id) {
|
||||
return registries.lookupOrThrow(Registries.PLACED_FEATURE)
|
||||
.get(ResourceKey.create(Registries.PLACED_FEATURE, id)).isPresent();
|
||||
}
|
||||
|
||||
public static List<AgeFeatureSelection.Choice> randomChoices(long ageSeed, AgeRuntimeProfile profile,
|
||||
AgeBiomeSelection biomes, RegistryAccess registries) {
|
||||
List<ResourceLocation> eligible = new ArrayList<>(FEATURES.stream()
|
||||
.filter(id -> error(id, profile, biomes, registries) == null).toList());
|
||||
Random random = new Random(ageSeed ^ 0x5354525543545552L);
|
||||
Collections.shuffle(eligible, random);
|
||||
int count = random.nextInt(3);
|
||||
return eligible.stream().limit(count)
|
||||
.map(id -> new AgeFeatureSelection.Choice(id, Optional.empty())).toList();
|
||||
}
|
||||
|
||||
private static Set<ResourceLocation> enabledSets(List<AgeFeatureSelection.Choice> choices) {
|
||||
return choices.stream().filter(choice -> feature(choice.id()) && !dungeons(choice.id()))
|
||||
.map(choice -> setId(choice.id())).collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
public static Stream<Holder<StructureSet>> allowedSets(Stream<Holder<StructureSet>> sets,
|
||||
List<AgeFeatureSelection.Choice> choices) {
|
||||
Set<ResourceLocation> enabled = enabledSets(choices);
|
||||
return sets.filter(holder -> holder.unwrapKey()
|
||||
.map(key -> allowed(key.location(), enabled)).orElse(false));
|
||||
}
|
||||
|
||||
public static HolderLookup<StructureSet> filteredLookup(HolderLookup<StructureSet> lookup,
|
||||
List<AgeFeatureSelection.Choice> choices) {
|
||||
Set<ResourceLocation> enabled = enabledSets(choices);
|
||||
return new HolderLookup<>() {
|
||||
@Override
|
||||
public Optional<Holder.Reference<StructureSet>> get(ResourceKey<StructureSet> key) {
|
||||
return allowed(key.location(), enabled) ? lookup.get(key) : Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<HolderSet.Named<StructureSet>> get(TagKey<StructureSet> key) {
|
||||
return lookup.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Holder.Reference<StructureSet>> listElements() {
|
||||
return lookup.listElements().filter(holder -> allowed(holder.key().location(), enabled));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<HolderSet.Named<StructureSet>> listTags() {
|
||||
return lookup.listTags();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean allowed(ResourceLocation set, Set<ResourceLocation> enabled) {
|
||||
return !CONTROLLED_SETS.contains(set) || enabled.contains(set);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import java.util.UUID;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.world.AgeFeatureChunkGenerator;
|
||||
import net.dnorberg.rivencraft.world.AgeDenseOres;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.dnorberg.rivencraft.writing.BlockSymbolPages;
|
||||
import net.dnorberg.rivencraft.writing.SymbolCatalog;
|
||||
@@ -32,7 +33,14 @@ public final class FeatureWritingGameTests {
|
||||
Set<String> pages = Set.of("feature/caves", "feature/flowers", "feature/trees",
|
||||
"feature/huge_trees", "feature/rocks", "feature/lava_lakes",
|
||||
"feature/surface_lakes", "feature/deep_lakes", "feature/ravines",
|
||||
"feature/floating_islands");
|
||||
"feature/floating_islands", "feature/tendrils", "feature/skylands",
|
||||
"feature/spheres", "feature/spikes", "feature/dungeons",
|
||||
"feature/mineshafts", "feature/nether_fortresses",
|
||||
"feature/strongholds", "feature/villages", "feature/trial_chambers",
|
||||
"feature/bastions", "feature/buried_treasure", "feature/shipwrecks",
|
||||
"feature/ocean_ruins", "feature/pillager_outposts", "feature/ruined_portals",
|
||||
"feature/nether_fossils", "feature/ancient_cities", "feature/trail_ruins",
|
||||
"feature/dense_ores", "feature/obelisks");
|
||||
helper.assertTrue(PageItem.bundledCreativeSymbols().containsAll(pages),
|
||||
"Supported feature pages must appear in the creative and JEI symbol feed");
|
||||
for (String path : pages) {
|
||||
@@ -59,7 +67,13 @@ public final class FeatureWritingGameTests {
|
||||
AgeFeatureSelection.resolved(plan), "Features must survive the saved-plan round trip");
|
||||
for (AgeFeatureSelection.Choice choice : AgeFeatureSelection.resolved(plan)) {
|
||||
if (AgeFeatureSelection.caves(choice.id()) || AgeFeatureSelection.ravines(choice.id())
|
||||
|| AgeFeatureSelection.floatingIslands(choice.id())) continue;
|
||||
|| AgeFeatureSelection.floatingIslands(choice.id())
|
||||
|| AgeFeatureSelection.tendrils(choice.id())
|
||||
|| AgeFeatureSelection.skylands(choice.id())
|
||||
|| AgeFeatureSelection.spheres(choice.id())
|
||||
|| AgeFeatureSelection.spikes(choice.id())
|
||||
|| AgeFeatureSelection.obelisks(choice.id())
|
||||
|| choice.id().equals(AgeDenseOres.ID)) continue;
|
||||
var placement = AgeFeatureSelection.placedFeature(choice.id());
|
||||
helper.assertTrue(registries.lookupOrThrow(Registries.PLACED_FEATURE)
|
||||
.get(ResourceKey.create(Registries.PLACED_FEATURE, placement)).isPresent(),
|
||||
@@ -136,6 +150,134 @@ public final class FeatureWritingGameTests {
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void tendrilsAreAuthoredAndCanAppearInRandomAges(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
AgeRuntimeProfiles.profiles().forEach((profileId, profile) ->
|
||||
helper.assertTrue(AgeFeatureSelection.error(id("tendrils"), profile, registries) == null,
|
||||
"Tendrils must support loaded terrain profile " + profileId));
|
||||
for (String terrain : List.of("standard", "amplified", "island", "flat", "void", "cave")) {
|
||||
OrderedWriting writing = new OrderedWriting(List.of(panel(), page("terrain/" + terrain),
|
||||
page("feature/tendrils")));
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 450L, AgeGenerationProfiles.STANDARD,
|
||||
AgeTraits.select(450L), registries);
|
||||
List<AgeFeatureSelection.Choice> choices = AgeFeatureSelection.resolved(plan);
|
||||
helper.assertValueEqual(choices.size(), 1, "Tendrils must compile on " + terrain + " terrain");
|
||||
helper.assertValueEqual(choices.getFirst().id(), id("tendrils"),
|
||||
"Tendrils must not replace the terrain page");
|
||||
helper.assertTrue(choices.getFirst().material().isPresent()
|
||||
&& AgeFeatureSelection.materialError(choices.getFirst()) == null,
|
||||
"Unmodified Tendrils must choose a full-block material");
|
||||
helper.assertValueEqual(AgeFeatureSelection.resolved(AgeGenerationPlan.load(plan.save())), choices,
|
||||
"The Tendrils material must survive a plan reload");
|
||||
}
|
||||
OrderedWriting materialWriting = new OrderedWriting(List.of(panel(), page("terrain/void"),
|
||||
block("glowstone"), page("feature/tendrils")));
|
||||
AgeGenerationPlan materialPlan = AgeGenerationPlan.compile(materialWriting, 450L,
|
||||
AgeGenerationProfiles.STANDARD, AgeTraits.select(450L), registries);
|
||||
helper.assertValueEqual(AgeFeatureSelection.resolved(materialPlan), List.of(
|
||||
new AgeFeatureSelection.Choice(id("tendrils"), Optional.of(
|
||||
ResourceLocation.withDefaultNamespace("glowstone")))),
|
||||
"A preceding Block page must set the Tendrils material");
|
||||
var caveProfile = AgeRuntimeProfiles.resolve(id("cave"));
|
||||
boolean found = false;
|
||||
for (long seed = 0; seed < 12; seed++) {
|
||||
List<AgeFeatureSelection.Choice> random = AgeFeatureSelection.randomChoices(seed, caveProfile, registries);
|
||||
helper.assertTrue(random.stream().allMatch(choice -> AgeFeatureSelection.tendrils(choice.id())
|
||||
|| AgeFeatureSelection.spheres(choice.id())
|
||||
|| AgeFeatureSelection.skylands(choice.id())
|
||||
|| AgeStructureSelection.feature(choice.id())),
|
||||
"Cave random Ages must not choose incompatible surface features");
|
||||
if (random.stream().anyMatch(choice -> AgeFeatureSelection.tendrils(choice.id()))) found = true;
|
||||
}
|
||||
helper.assertTrue(found, "A page-free random Cave Age must be able to roll Tendrils");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void skylandsFiltersBaseTerrainIndependentlyOfIslandFeatures(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
for (String terrain : List.of("standard", "amplified", "island", "flat", "void", "cave")) {
|
||||
OrderedWriting writing = new OrderedWriting(List.of(panel(), page("terrain/" + terrain),
|
||||
page("feature/skylands"), page("feature/floating_islands")));
|
||||
if (terrain.equals("cave")) {
|
||||
writing = new OrderedWriting(List.of(panel(), page("terrain/cave"),
|
||||
page("feature/skylands")));
|
||||
}
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 707L,
|
||||
AgeGenerationProfiles.STANDARD, AgeTraits.select(707L), registries);
|
||||
List<AgeFeatureSelection.Choice> resolved = AgeFeatureSelection.resolved(plan);
|
||||
helper.assertValueEqual(resolved.getFirst(), new AgeFeatureSelection.Choice(
|
||||
id("skylands"), Optional.empty()),
|
||||
"Skylands must be a separate material-free feature on " + terrain);
|
||||
helper.assertValueEqual(AgeFeatureSelection.resolved(AgeGenerationPlan.load(plan.save())), resolved,
|
||||
"Skylands and any Floating Islands page must survive a plan reload");
|
||||
}
|
||||
rejects(helper, new OrderedWriting(List.of(panel(), page("terrain/standard"),
|
||||
block("stone"), page("feature/skylands"))), "cannot consume a material page");
|
||||
List<AgeFeatureSelection.Choice> randomNoise = AgeFeatureSelection.randomChoices(7L,
|
||||
AgeRuntimeProfiles.resolve(id("standard")), registries);
|
||||
helper.assertTrue(randomNoise.stream().anyMatch(choice -> AgeFeatureSelection.skylands(choice.id())
|
||||
&& choice.material().isEmpty()),
|
||||
"A page-free random noise Age must be able to roll material-free Skylands");
|
||||
for (String terrain : List.of("flat", "void")) {
|
||||
helper.assertTrue(AgeFeatureSelection.randomChoices(7L,
|
||||
AgeRuntimeProfiles.resolve(id(terrain)), registries).stream()
|
||||
.noneMatch(choice -> AgeFeatureSelection.skylands(choice.id())),
|
||||
"Random " + terrain + " Ages must not roll a filter without useful upper terrain");
|
||||
}
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void spheresAndSpikesHaveSeparateAuthoredPagesAndMaterials(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
OrderedWriting writing = new OrderedWriting(List.of(panel(), page("terrain/standard"),
|
||||
block("glowstone"), page("feature/spheres"), block("stone"), page("feature/spikes")));
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 1717L,
|
||||
AgeGenerationProfiles.STANDARD, AgeTraits.select(1717L), registries);
|
||||
List<AgeFeatureSelection.Choice> expected = List.of(
|
||||
new AgeFeatureSelection.Choice(id("spheres"), Optional.of(
|
||||
ResourceLocation.withDefaultNamespace("glowstone"))),
|
||||
new AgeFeatureSelection.Choice(id("spikes"), Optional.of(
|
||||
ResourceLocation.withDefaultNamespace("stone"))));
|
||||
helper.assertValueEqual(AgeFeatureSelection.resolved(plan), expected,
|
||||
"Each page must preserve its own preceding Block material and writing order");
|
||||
helper.assertValueEqual(AgeFeatureSelection.resolved(AgeGenerationPlan.load(plan.save())), expected,
|
||||
"Spheres and Spikes must retain their identities and materials after reload");
|
||||
for (String terrain : List.of("standard", "amplified", "island", "flat", "void", "cave")) {
|
||||
var profile = AgeRuntimeProfiles.resolve(id(terrain));
|
||||
helper.assertTrue(AgeFeatureSelection.error(id("spheres"), profile, registries) == null,
|
||||
"Spheres must be additive on " + terrain + " terrain");
|
||||
boolean surface = !terrain.equals("void") && !terrain.equals("cave");
|
||||
helper.assertValueEqual(AgeFeatureSelection.error(id("spikes"), profile, registries) == null,
|
||||
surface, "Spikes require a solid surface on " + terrain + " terrain");
|
||||
}
|
||||
rejects(helper, new OrderedWriting(List.of(panel(), page("terrain/void"),
|
||||
page("feature/spikes"))), "needs an exposed solid surface");
|
||||
rejects(helper, new OrderedWriting(List.of(panel(), page("terrain/cave"),
|
||||
page("feature/spikes"))), "needs an exposed solid surface");
|
||||
boolean randomSpheres = false;
|
||||
boolean randomSpikes = false;
|
||||
for (long seed = 0; seed < 80; seed++) {
|
||||
var random = AgeFeatureSelection.randomChoices(seed,
|
||||
AgeRuntimeProfiles.resolve(id("standard")), registries);
|
||||
randomSpheres |= random.stream().anyMatch(choice -> AgeFeatureSelection.spheres(choice.id()));
|
||||
randomSpikes |= random.stream().anyMatch(choice -> AgeFeatureSelection.spikes(choice.id()));
|
||||
}
|
||||
helper.assertTrue(randomSpheres && randomSpikes,
|
||||
"Page-free surface Ages must be able to select both separate features");
|
||||
for (String terrain : List.of("void", "cave")) {
|
||||
var profile = AgeRuntimeProfiles.resolve(id(terrain));
|
||||
for (long seed = 0; seed < 30; seed++) {
|
||||
helper.assertTrue(AgeFeatureSelection.randomChoices(seed, profile, registries).stream()
|
||||
.noneMatch(choice -> AgeFeatureSelection.spikes(choice.id())),
|
||||
"Random " + terrain + " Ages must never choose unsupported Spikes");
|
||||
}
|
||||
}
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void voidBiomesAreIndependentOfFloatingIslands(GameTestHelper helper) {
|
||||
OrderedWriting voidDefault = new OrderedWriting(List.of(panel(), page("terrain/void")));
|
||||
@@ -275,7 +417,11 @@ public final class FeatureWritingGameTests {
|
||||
helper.assertTrue(material.equals("water")
|
||||
? AgeFeatureSelection.isLake(choices.getFirst().id())
|
||||
: AgeFeatureSelection.isRock(choices.getFirst().id())
|
||||
|| AgeFeatureSelection.floatingIslands(choices.getFirst().id()),
|
||||
|| AgeFeatureSelection.floatingIslands(choices.getFirst().id())
|
||||
|| AgeFeatureSelection.tendrils(choices.getFirst().id())
|
||||
|| AgeFeatureSelection.spheres(choices.getFirst().id())
|
||||
|| AgeFeatureSelection.spikes(choices.getFirst().id())
|
||||
|| AgeFeatureSelection.obelisks(choices.getFirst().id()),
|
||||
"The inferred feature must accept the material type");
|
||||
helper.assertValueEqual(AgeFeatureSelection.resolved(AgeGenerationPlan.load(plan.save())), choices,
|
||||
"The inferred feature must be deterministic across a plan reload");
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import net.dnorberg.rivencraft.world.AgeFeatureChunkGenerator;
|
||||
import net.dnorberg.rivencraft.world.AgeFlatChunkGenerator;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.chunk.ChunkGenerator;
|
||||
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.injection.At;
|
||||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
|
||||
/** Suppresses biome-default monster rooms only in new Rivencraft Ages. */
|
||||
@Mixin(ChunkGenerator.class)
|
||||
public abstract class AgeDungeonPlacementMixin {
|
||||
@Redirect(method = "applyBiomeDecoration", at = @At(value = "INVOKE",
|
||||
target = "Lnet/minecraft/world/level/levelgen/placement/PlacedFeature;placeWithBiomeCheck(Lnet/minecraft/world/level/WorldGenLevel;Lnet/minecraft/world/level/chunk/ChunkGenerator;Lnet/minecraft/util/RandomSource;Lnet/minecraft/core/BlockPos;)Z"))
|
||||
private boolean rivencraft$gateAmbientDungeons(PlacedFeature feature, WorldGenLevel level,
|
||||
ChunkGenerator generator, RandomSource random, BlockPos pos) {
|
||||
if ((Object) this instanceof AgeFeatureChunkGenerator || (Object) this instanceof AgeFlatChunkGenerator) {
|
||||
ResourceLocation id = level.registryAccess().registryOrThrow(Registries.PLACED_FEATURE).getKey(feature);
|
||||
if (ResourceLocation.withDefaultNamespace("monster_room").equals(id)
|
||||
|| ResourceLocation.withDefaultNamespace("monster_room_deep").equals(id)) return false;
|
||||
}
|
||||
return feature.placeWithBiomeCheck(level, generator, random, pos);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.book.AgeBiomeSelection;
|
||||
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
|
||||
import net.dnorberg.rivencraft.book.AgeGenerationPlan;
|
||||
import net.dnorberg.rivencraft.book.AgeGenerationProfiles;
|
||||
import net.dnorberg.rivencraft.book.AgeRuntimeProfiles;
|
||||
import net.dnorberg.rivencraft.book.AgeTraits;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||
import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
import net.minecraft.world.level.levelgen.feature.configurations.OreConfiguration;
|
||||
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||
import net.minecraft.world.level.levelgen.structure.templatesystem.BlockMatchTest;
|
||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
|
||||
/** Dense Ores remains authored-only and discovers ore configurations without a namespace allow-list. */
|
||||
@GameTestHolder(Rivencraft.MOD_ID)
|
||||
@PrefixGameTestTemplate(false)
|
||||
public final class AgeDenseOreGameTests {
|
||||
private AgeDenseOreGameTests() {}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void denseOresPageCompilesAndPersists(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
helper.assertTrue(PageItem.bundledCreativeSymbols().contains("feature/dense_ores"),
|
||||
"Dense Ores must appear in the Creative and JEI page feed");
|
||||
OrderedWriting writing = new OrderedWriting(List.of(WritingEntry.linkPanel(),
|
||||
page("terrain/standard"), page("biome/plains"), page("biome/single"),
|
||||
page("feature/dense_ores")));
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 55L, AgeGenerationProfiles.STANDARD,
|
||||
AgeTraits.select(55L), registries);
|
||||
helper.assertValueEqual(AgeFeatureSelection.resolved(plan).stream()
|
||||
.map(AgeFeatureSelection.Choice::id).toList(), List.of(AgeDenseOres.ID),
|
||||
"Dense Ores must be saved as an authored feature");
|
||||
helper.assertValueEqual(AgeFeatureSelection.resolved(AgeGenerationPlan.load(plan.save())),
|
||||
AgeFeatureSelection.resolved(plan), "Dense Ores must survive Age-plan reload");
|
||||
for (long seed = 0; seed < 32; seed++) {
|
||||
helper.assertTrue(AgeFeatureSelection.randomChoices(seed,
|
||||
AgeRuntimeProfiles.resolve(id("standard")), registries).stream()
|
||||
.noneMatch(choice -> choice.id().equals(AgeDenseOres.ID)),
|
||||
"Random Ages must not roll an authored-only Dense Ores page");
|
||||
}
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void denseOresRejectsUnsuitableTerrainAndBiome(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
helper.assertTrue(AgeDenseOres.error(AgeRuntimeProfiles.resolve(id("flat")), null, registries)
|
||||
.contains("noise terrain"), "Flat terrain must reject Dense Ores");
|
||||
helper.assertTrue(AgeDenseOres.error(AgeRuntimeProfiles.resolve(id("void")), null, registries)
|
||||
.contains("noise terrain"), "Void terrain must reject Dense Ores");
|
||||
helper.assertTrue(AgeDenseOres.error(AgeRuntimeProfiles.resolve(id("standard")),
|
||||
new AgeBiomeSelection(AgeTraits.BiomeDistribution.SINGLE,
|
||||
List.of(ResourceLocation.withDefaultNamespace("the_end"))), registries)
|
||||
.contains("biome with registered ore"),
|
||||
"A biome without ore placements must not accept a no-op Dense Ores page");
|
||||
helper.assertTrue(AgeDenseOres.error(AgeRuntimeProfiles.resolve(id("cave")),
|
||||
new AgeBiomeSelection(AgeTraits.BiomeDistribution.SINGLE,
|
||||
List.of(ResourceLocation.withDefaultNamespace("nether_wastes"))), registries) == null,
|
||||
"Nether ore placements must qualify for a Cave Age");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void recognizesOreTargetsButNotStoneBlobs(GameTestHelper helper) {
|
||||
helper.assertTrue(AgeDenseOres.isOre(ore(Blocks.IRON_ORE.defaultBlockState())),
|
||||
"Any namespace's OreConfiguration yielding an ore block must qualify");
|
||||
helper.assertTrue(!AgeDenseOres.isOre(ore(Blocks.STONE.defaultBlockState())),
|
||||
"Ordinary stone replacement must not be doubled as an ore");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
private static Holder<PlacedFeature> ore(net.minecraft.world.level.block.state.BlockState output) {
|
||||
var config = new OreConfiguration(new BlockMatchTest(Blocks.STONE), output, 8);
|
||||
var feature = new ConfiguredFeature<>(Feature.ORE, config);
|
||||
return Holder.direct(new PlacedFeature(Holder.direct(feature), List.of()));
|
||||
}
|
||||
|
||||
private static ResourceLocation id(String path) {
|
||||
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||
}
|
||||
|
||||
private static WritingEntry page(String path) {
|
||||
return WritingEntry.symbol(new SymbolReference(id(path)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.book.AgeBiomeSelection;
|
||||
import net.dnorberg.rivencraft.book.AgeRuntimeProfile;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.RegistryAccess;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.tags.TagKey;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.biome.Biome;
|
||||
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.levelgen.feature.configurations.OreConfiguration;
|
||||
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.neoforged.neoforge.common.Tags;
|
||||
|
||||
/** Replays eligible biome ore placements once per authored Dense Ores page. */
|
||||
public final class AgeDenseOres {
|
||||
public static final ResourceLocation ID = ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "dense_ores");
|
||||
public static final TagKey<PlacedFeature> EXTRA_ORES = TagKey.create(Registries.PLACED_FEATURE, ID);
|
||||
|
||||
private AgeDenseOres() {}
|
||||
|
||||
public static String error(AgeRuntimeProfile profile, AgeBiomeSelection biomes, RegistryAccess registries) {
|
||||
if (profile.terrain() != AgeRuntimeProfile.Terrain.NOISE)
|
||||
return "Dense Ores needs noise terrain with replaceable underground stone or netherrack";
|
||||
if (biomes == null) return null;
|
||||
var registry = registries.lookupOrThrow(Registries.BIOME);
|
||||
boolean eligible = biomes.biomes().stream()
|
||||
.map(id -> registry.get(ResourceKey.create(Registries.BIOME, id)))
|
||||
.flatMap(java.util.Optional::stream)
|
||||
.anyMatch(holder -> oreFeatures(holder).stream().anyMatch(AgeDenseOres::isOre));
|
||||
return eligible ? null : "Dense Ores needs at least one biome with registered ore placements";
|
||||
}
|
||||
|
||||
/** Vanilla-style and modded OreConfiguration targets, or an explicit opt-in tag. */
|
||||
public static boolean isOre(Holder<PlacedFeature> holder) {
|
||||
if (holder.is(EXTRA_ORES)) return true;
|
||||
if (!(holder.value().feature().value().config() instanceof OreConfiguration ore)) return false;
|
||||
return ore.targetStates.stream().anyMatch(target -> isOreBlock(target.state));
|
||||
}
|
||||
|
||||
private static boolean isOreBlock(BlockState state) {
|
||||
if (state.is(Tags.Blocks.ORES)) return true;
|
||||
// Some third-party ores omit the common tag; require an ore configuration
|
||||
// as well so similarly named decorative features cannot be selected.
|
||||
String path = BuiltInRegistries.BLOCK.getKey(state.getBlock()).getPath();
|
||||
return path.endsWith("_ore") || path.contains("_ore_");
|
||||
}
|
||||
|
||||
private static java.util.List<Holder<PlacedFeature>> oreFeatures(Holder<Biome> biome) {
|
||||
// A mod may register its ore in a non-vanilla decoration step. Its
|
||||
// configuration and output, not its stage or namespace, decide eligibility.
|
||||
return biome.value().getGenerationSettings().features().stream()
|
||||
.flatMap(net.minecraft.core.HolderSet::stream).toList();
|
||||
}
|
||||
|
||||
public static void place(WorldGenLevel level, ChunkAccess chunk, ChunkGenerator generator,
|
||||
long ageSeed, int occurrence) {
|
||||
Set<Holder<Biome>> biomes = new HashSet<>();
|
||||
ChunkPos.rangeClosed(chunk.getPos(), 1).forEach(pos -> {
|
||||
for (var section : level.getChunk(pos.x, pos.z).getSections())
|
||||
section.getBiomes().getAll(biomes::add);
|
||||
});
|
||||
biomes.retainAll(generator.getBiomeSource().possibleBiomes());
|
||||
Map<ResourceLocation, PlacedFeature> ores = new TreeMap<>();
|
||||
for (Holder<Biome> biome : biomes) {
|
||||
for (Holder<PlacedFeature> feature : oreFeatures(biome)) {
|
||||
if (!isOre(feature)) continue;
|
||||
feature.unwrapKey().ifPresent(key -> ores.putIfAbsent(key.location(), feature.value()));
|
||||
}
|
||||
}
|
||||
BlockPos origin = new BlockPos(chunk.getPos().getMinBlockX(), level.getMinBuildHeight(),
|
||||
chunk.getPos().getMinBlockZ());
|
||||
for (var entry : ores.entrySet()) {
|
||||
long seed = mix(ageSeed ^ (long) origin.getX() * 341873128712L
|
||||
^ (long) origin.getZ() * 132897987541L
|
||||
^ (long) entry.getKey().hashCode() * 0x9E3779B97F4A7C15L
|
||||
^ (long) occurrence * 0xD1B54A32D192ED03L);
|
||||
entry.getValue().placeWithBiomeCheck(level, generator, RandomSource.create(seed), origin);
|
||||
}
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
|
||||
import net.dnorberg.rivencraft.book.AgeStructureSelection;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
@@ -25,6 +27,7 @@ import net.minecraft.world.level.biome.BiomeManager;
|
||||
import net.minecraft.world.level.biome.BiomeSource;
|
||||
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.chunk.ProtoChunk;
|
||||
import net.minecraft.world.level.levelgen.GenerationStep;
|
||||
import net.minecraft.world.level.levelgen.LegacyRandomSource;
|
||||
@@ -37,6 +40,7 @@ import net.minecraft.world.level.levelgen.carver.CarvingContext;
|
||||
import net.minecraft.world.level.levelgen.carver.ConfiguredWorldCarver;
|
||||
import net.minecraft.world.level.levelgen.placement.BiomeFilter;
|
||||
import net.minecraft.world.level.levelgen.placement.PlacedFeature;
|
||||
import net.minecraft.world.level.levelgen.structure.StructureSet;
|
||||
import net.minecraft.world.level.levelgen.placement.RarityFilter;
|
||||
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
|
||||
import net.minecraft.world.level.levelgen.feature.Feature;
|
||||
@@ -71,17 +75,37 @@ public final class AgeFeatureChunkGenerator extends NoiseBasedChunkGenerator {
|
||||
@Override
|
||||
protected MapCodec<? extends ChunkGenerator> codec() { return CODEC; }
|
||||
|
||||
@Override
|
||||
public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> sets, RandomState random, long seed) {
|
||||
return ChunkGeneratorStructureState.createForNormal(random, seed, getBiomeSource(),
|
||||
AgeStructureSelection.filteredLookup(sets, features));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<ChunkAccess> fillFromNoise(Blender blender, RandomState random,
|
||||
StructureManager structures, ChunkAccess chunk) {
|
||||
CompletableFuture<ChunkAccess> filled = super.fillFromNoise(blender, random, structures, chunk);
|
||||
if (features.stream().noneMatch(choice -> AgeFeatureSelection.floatingIslands(choice.id()))) return filled;
|
||||
if (features.stream().noneMatch(choice -> AgeFeatureSelection.floatingIslands(choice.id())
|
||||
|| AgeFeatureSelection.tendrils(choice.id())
|
||||
|| AgeFeatureSelection.spheres(choice.id())
|
||||
|| AgeFeatureSelection.skylands(choice.id()))) return filled;
|
||||
return filled.thenApply(result -> {
|
||||
int occurrence = 0;
|
||||
// Legacy's Skylands primer filter removes base terrain before large
|
||||
// features add their own blocks to the chunk.
|
||||
if (features.stream().anyMatch(choice -> AgeFeatureSelection.skylands(choice.id())))
|
||||
SkylandsFeature.filter(result, ageSeed);
|
||||
Map<ResourceLocation, Integer> occurrences = new HashMap<>();
|
||||
for (AgeFeatureSelection.Choice choice : features) {
|
||||
if (!AgeFeatureSelection.floatingIslands(choice.id())) continue;
|
||||
FloatingIslandFeature.generate(result, getBiomeSource(), random,
|
||||
ageSeed ^ (long) occurrence++ * 0x9E3779B97F4A7C15L, choice);
|
||||
if (!AgeFeatureSelection.floatingIslands(choice.id())
|
||||
&& !AgeFeatureSelection.tendrils(choice.id())
|
||||
&& !AgeFeatureSelection.spheres(choice.id())) continue;
|
||||
int occurrence = occurrences.merge(choice.id(), 1, Integer::sum) - 1;
|
||||
long featureSeed = ageSeed ^ (long) occurrence * 0x9E3779B97F4A7C15L;
|
||||
if (AgeFeatureSelection.floatingIslands(choice.id()))
|
||||
FloatingIslandFeature.generate(result, getBiomeSource(), random, featureSeed, choice);
|
||||
else if (AgeFeatureSelection.tendrils(choice.id()))
|
||||
TendrilFeature.generate(result, featureSeed, choice);
|
||||
else SpheresFeature.generate(result, featureSeed, choice);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
@@ -137,9 +161,24 @@ public final class AgeFeatureChunkGenerator extends NoiseBasedChunkGenerator {
|
||||
int z = chunk.getPos().getMinBlockZ();
|
||||
BlockPos origin = new BlockPos(x, level.getMinBuildHeight(), z);
|
||||
Map<AgeFeatureSelection.Choice, Integer> occurrences = new HashMap<>();
|
||||
Map<ResourceLocation, Integer> spikeOccurrences = new HashMap<>();
|
||||
int obeliskOccurrence = 0;
|
||||
for (int index = 0; index < features.size(); index++) {
|
||||
AgeFeatureSelection.Choice choice = features.get(index);
|
||||
int occurrence = occurrences.merge(choice, 1, Integer::sum) - 1;
|
||||
if (AgeFeatureSelection.spikes(choice.id())) {
|
||||
SpikesFeature.generate(level, chunk, ageSeed, choice,
|
||||
spikeOccurrences.merge(choice.id(), 1, Integer::sum) - 1);
|
||||
continue;
|
||||
}
|
||||
if (AgeFeatureSelection.obelisks(choice.id())) {
|
||||
ObelisksFeature.generate(level, chunk, ageSeed, choice, obeliskOccurrence++);
|
||||
continue;
|
||||
}
|
||||
if (choice.id().equals(AgeDenseOres.ID)) {
|
||||
AgeDenseOres.place(level, chunk, this, ageSeed, occurrence);
|
||||
continue;
|
||||
}
|
||||
if (AgeFeatureSelection.surfaceVegetation(choice.id())) {
|
||||
int surface = chunk.getHeight(Heightmap.Types.WORLD_SURFACE_WG, 8, 8);
|
||||
if (!AgeFeatureSelection.supportsSurfaceVegetation(
|
||||
|
||||
@@ -3,19 +3,24 @@ package net.dnorberg.rivencraft.world;
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.MapCodec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.stream.Stream;
|
||||
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
|
||||
import net.dnorberg.rivencraft.book.AgeStructureSelection;
|
||||
import net.minecraft.Util;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.HolderSet;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.server.level.WorldGenRegion;
|
||||
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;
|
||||
@@ -67,7 +72,8 @@ public final class AgeFlatChunkGenerator extends ChunkGenerator {
|
||||
Stream<Holder<StructureSet>> entries = settings.structureOverrides()
|
||||
.map(HolderSet::stream)
|
||||
.orElseGet(() -> structures.listElements().map(holder -> (Holder<StructureSet>) holder));
|
||||
return ChunkGeneratorStructureState.createForFlat(random, seed, biomeSource, entries);
|
||||
return ChunkGeneratorStructureState.createForFlat(random, seed, biomeSource,
|
||||
AgeStructureSelection.allowedSets(entries, features));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -97,15 +103,37 @@ public final class AgeFlatChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
}
|
||||
}
|
||||
int islandOccurrence = 0;
|
||||
if (features.stream().anyMatch(choice -> AgeFeatureSelection.skylands(choice.id())))
|
||||
SkylandsFeature.filter(chunk, ageSeed);
|
||||
Map<ResourceLocation, Integer> occurrences = new HashMap<>();
|
||||
for (AgeFeatureSelection.Choice choice : features) {
|
||||
if (!AgeFeatureSelection.floatingIslands(choice.id())) continue;
|
||||
FloatingIslandFeature.generate(chunk, biomeSource, random,
|
||||
ageSeed ^ (long) islandOccurrence++ * 0x9E3779B97F4A7C15L, choice);
|
||||
if (!AgeFeatureSelection.floatingIslands(choice.id())
|
||||
&& !AgeFeatureSelection.tendrils(choice.id())
|
||||
&& !AgeFeatureSelection.spheres(choice.id())) continue;
|
||||
int occurrence = occurrences.merge(choice.id(), 1, Integer::sum) - 1;
|
||||
long featureSeed = ageSeed ^ (long) occurrence * 0x9E3779B97F4A7C15L;
|
||||
if (AgeFeatureSelection.floatingIslands(choice.id()))
|
||||
FloatingIslandFeature.generate(chunk, biomeSource, random, featureSeed, choice);
|
||||
else if (AgeFeatureSelection.tendrils(choice.id()))
|
||||
TendrilFeature.generate(chunk, featureSeed, choice);
|
||||
else SpheresFeature.generate(chunk, featureSeed, choice);
|
||||
}
|
||||
return CompletableFuture.completedFuture(chunk);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyBiomeDecoration(WorldGenLevel level, ChunkAccess chunk, StructureManager structures) {
|
||||
super.applyBiomeDecoration(level, chunk, structures);
|
||||
int spikeOccurrence = 0;
|
||||
int obeliskOccurrence = 0;
|
||||
for (AgeFeatureSelection.Choice choice : features) {
|
||||
if (AgeFeatureSelection.spikes(choice.id()))
|
||||
SpikesFeature.generate(level, chunk, ageSeed, choice, spikeOccurrence++);
|
||||
if (AgeFeatureSelection.obelisks(choice.id()))
|
||||
ObelisksFeature.generate(level, chunk, ageSeed, choice, obeliskOccurrence++);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBaseHeight(int x, int z, Heightmap.Types type, LevelHeightAccessor level, RandomState random) {
|
||||
List<BlockState> layers = settings.getLayers();
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.Direction;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
import net.minecraft.world.level.block.state.BlockState;
|
||||
import net.minecraft.world.level.chunk.ChunkAccess;
|
||||
import net.minecraft.world.level.levelgen.Heightmap;
|
||||
|
||||
/** Rare one-chunk obelisk adapted from Legacy's 4x4 plinth and 2x12x2 shaft. */
|
||||
final class ObelisksFeature {
|
||||
private static final long SALT = 0x4F42454C49534B53L;
|
||||
private static final int CHANCE = 128;
|
||||
private static final int BASE = 4;
|
||||
private static final int HEIGHT = 12;
|
||||
|
||||
private ObelisksFeature() {}
|
||||
|
||||
record Site(int x, int z) {}
|
||||
|
||||
static Site site(long ageSeed, ChunkPos chunk, int occurrence) {
|
||||
RandomSource random = RandomSource.create(mix(ageSeed ^ SALT
|
||||
^ (long) chunk.x * 341873128712L ^ (long) chunk.z * 132897987541L
|
||||
^ (long) occurrence * 0x9E3779B97F4A7C15L));
|
||||
if (random.nextInt(CHANCE) != 0) return null;
|
||||
return new Site(chunk.getMinBlockX() + 4 + random.nextInt(8),
|
||||
chunk.getMinBlockZ() + 4 + random.nextInt(8));
|
||||
}
|
||||
|
||||
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed,
|
||||
AgeFeatureSelection.Choice choice, int occurrence) {
|
||||
Site site = site(ageSeed, chunk.getPos(), occurrence);
|
||||
if (site == null || choice.material().isEmpty()) return;
|
||||
BlockState material = BuiltInRegistries.BLOCK.get(choice.material().orElseThrow()).defaultBlockState();
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
int minSurface = Integer.MAX_VALUE;
|
||||
int maxSurface = Integer.MIN_VALUE;
|
||||
int[][] surface = new int[BASE][BASE];
|
||||
for (int dx = 0; dx < BASE; dx++) {
|
||||
for (int dz = 0; dz < BASE; dz++) {
|
||||
int x = site.x() + dx;
|
||||
int z = site.z() + dz;
|
||||
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
|
||||
if (y <= level.getMinBuildHeight() + 1) return;
|
||||
BlockState ground = level.getBlockState(pos.set(x, y - 1, z));
|
||||
if (!ground.isFaceSturdy(level, pos, Direction.UP)
|
||||
|| !ground.getFluidState().isEmpty() || ground.hasBlockEntity()) return;
|
||||
surface[dx][dz] = y;
|
||||
minSurface = Math.min(minSurface, y);
|
||||
maxSurface = Math.max(maxSurface, y);
|
||||
}
|
||||
}
|
||||
if (maxSurface - minSurface > 2 || maxSurface + HEIGHT >= level.getMaxBuildHeight()) return;
|
||||
for (int dx = 0; dx < BASE; dx++) {
|
||||
for (int dz = 0; dz < BASE; dz++) {
|
||||
for (int y = surface[dx][dz]; y <= maxSurface; y++) {
|
||||
if (!open(level.getBlockState(pos.set(site.x() + dx, y, site.z() + dz)))) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int dx = 1; dx <= 2; dx++) {
|
||||
for (int dz = 1; dz <= 2; dz++) {
|
||||
for (int y = maxSurface + 1; y <= maxSurface + HEIGHT; y++) {
|
||||
if (!open(level.getBlockState(pos.set(site.x() + dx, y, site.z() + dz)))) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int dx = 0; dx < BASE; dx++) {
|
||||
for (int dz = 0; dz < BASE; dz++) {
|
||||
for (int y = surface[dx][dz]; y <= maxSurface; y++)
|
||||
level.setBlock(pos.set(site.x() + dx, y, site.z() + dz), material, 2);
|
||||
}
|
||||
}
|
||||
for (int dx = 1; dx <= 2; dx++) {
|
||||
for (int dz = 1; dz <= 2; dz++) {
|
||||
for (int y = maxSurface + 1; y <= maxSurface + HEIGHT; y++)
|
||||
level.setBlock(pos.set(site.x() + dx, y, site.z() + dz), material, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean open(BlockState state) {
|
||||
return state.getFluidState().isEmpty() && (state.isAir() || state.canBeReplaced());
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
|
||||
import net.dnorberg.rivencraft.book.AgeGenerationPlan;
|
||||
import net.dnorberg.rivencraft.book.AgeGenerationProfiles;
|
||||
import net.dnorberg.rivencraft.book.AgeRuntimeProfiles;
|
||||
import net.dnorberg.rivencraft.book.AgeTraits;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||
import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
|
||||
/** Obelisks must retain ordered materials and generate a stable Legacy-sized shape. */
|
||||
@GameTestHolder(Rivencraft.MOD_ID)
|
||||
@PrefixGameTestTemplate(false)
|
||||
public final class ObelisksGameTests {
|
||||
private ObelisksGameTests() {}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void authoredObelisksKeepTheirMaterialAndTerrainRules(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
helper.assertTrue(PageItem.bundledCreativeSymbols().contains("feature/obelisks"),
|
||||
"Obelisks must appear in the Creative and JEI symbol feed");
|
||||
OrderedWriting writing = new OrderedWriting(List.of(WritingEntry.linkPanel(), page("terrain/standard"),
|
||||
block("obsidian"), page("feature/obelisks"), page("feature/obelisks")));
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 56L, AgeGenerationProfiles.STANDARD,
|
||||
AgeTraits.select(56L), registries);
|
||||
var choices = AgeFeatureSelection.resolved(plan);
|
||||
helper.assertValueEqual(choices.size(), 2, "Repeated Obelisks pages must remain separate");
|
||||
helper.assertValueEqual(choices.getFirst().material(),
|
||||
Optional.of(ResourceLocation.withDefaultNamespace("obsidian")),
|
||||
"A preceding Block page must set the first obelisk material");
|
||||
helper.assertTrue(choices.getLast().material().isPresent()
|
||||
&& AgeFeatureSelection.materialError(choices.getLast()) == null,
|
||||
"An unmodified Obelisks page must choose a valid material");
|
||||
helper.assertValueEqual(AgeFeatureSelection.resolved(AgeGenerationPlan.load(plan.save())), choices,
|
||||
"Obelisk materials and order must survive Age-plan reload");
|
||||
for (String terrain : List.of("standard", "amplified", "island", "flat"))
|
||||
helper.assertTrue(AgeFeatureSelection.error(id("obelisks"),
|
||||
AgeRuntimeProfiles.resolve(id(terrain)), registries) == null,
|
||||
"Obelisks should be valid on exposed " + terrain + " terrain");
|
||||
for (String terrain : List.of("void", "cave"))
|
||||
helper.assertTrue(AgeFeatureSelection.error(id("obelisks"),
|
||||
AgeRuntimeProfiles.resolve(id(terrain)), registries) != null,
|
||||
"Obelisks must reject " + terrain + " terrain without an exposed surface");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void randomAgesOnlyChooseObelisksOnSupportedTerrain(GameTestHelper helper) {
|
||||
var registries = helper.getLevel().registryAccess();
|
||||
boolean selected = false;
|
||||
for (long seed = 0; seed < 128; seed++) {
|
||||
selected |= AgeFeatureSelection.randomChoices(seed,
|
||||
AgeRuntimeProfiles.resolve(id("standard")), registries).stream()
|
||||
.anyMatch(choice -> AgeFeatureSelection.obelisks(choice.id()));
|
||||
for (String terrain : List.of("void", "cave"))
|
||||
helper.assertTrue(AgeFeatureSelection.randomChoices(seed,
|
||||
AgeRuntimeProfiles.resolve(id(terrain)), registries).stream()
|
||||
.noneMatch(choice -> AgeFeatureSelection.obelisks(choice.id())),
|
||||
"Random " + terrain + " Ages must not gain Obelisks");
|
||||
}
|
||||
helper.assertTrue(selected, "Random surface Ages should be able to select Obelisks");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void obeliskSitesAreSeededAndStayInsideTheirChunk(GameTestHelper helper) {
|
||||
ChunkPos chunk = new ChunkPos(123, -45);
|
||||
int selected = 0;
|
||||
for (long seed = 0; seed < 2048; seed++) {
|
||||
ObelisksFeature.Site site = ObelisksFeature.site(seed, chunk, 0);
|
||||
helper.assertTrue(java.util.Objects.equals(ObelisksFeature.site(seed, chunk, 0), site),
|
||||
"An Age seed must reproduce the same obelisk site");
|
||||
if (site == null) continue;
|
||||
selected++;
|
||||
helper.assertTrue(site.x() >= chunk.getMinBlockX() + 4
|
||||
&& site.x() + 3 < chunk.getMaxBlockX(),
|
||||
"The full 4x4 plinth must remain inside its source chunk");
|
||||
helper.assertTrue(site.z() >= chunk.getMinBlockZ() + 4
|
||||
&& site.z() + 3 < chunk.getMaxBlockZ(),
|
||||
"The full 4x4 plinth must remain inside its source chunk");
|
||||
}
|
||||
helper.assertTrue(selected > 0 && selected < 64,
|
||||
"The 1-in-128 roll should remain rare but reachable");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
private static ResourceLocation id(String path) {
|
||||
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||
}
|
||||
|
||||
private static WritingEntry page(String path) {
|
||||
return WritingEntry.symbol(new SymbolReference(id(path)));
|
||||
}
|
||||
|
||||
private static WritingEntry block(String path) {
|
||||
return WritingEntry.symbol(new SymbolReference(net.dnorberg.rivencraft.writing.BlockSymbolPages.symbolId(
|
||||
ResourceLocation.withDefaultNamespace(path))));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
import net.minecraft.core.BlockPos;
|
||||
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.levelgen.Heightmap;
|
||||
|
||||
/**
|
||||
* Subtractive Skylands terrain filter adapted from Mystcraft Legacy's
|
||||
* SymbolSkylands (LGPL-3.0-or-later). Legacy keeps solid terrain above a
|
||||
* seven-octave cutoff near Y=76 and discards liquid above that cutoff.
|
||||
*/
|
||||
final class SkylandsFeature {
|
||||
private static final long SALT = 0x534B594C414E4453L;
|
||||
private static final int BASE_CUTOFF = 76;
|
||||
private static final BlockState AIR = Blocks.AIR.defaultBlockState();
|
||||
|
||||
private SkylandsFeature() {}
|
||||
|
||||
static void filter(ChunkAccess chunk, long ageSeed) {
|
||||
int originX = chunk.getPos().getMinBlockX();
|
||||
int originZ = chunk.getPos().getMinBlockZ();
|
||||
int minY = chunk.getMinBuildHeight();
|
||||
int maxY = chunk.getMaxBuildHeight();
|
||||
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
for (int localX = 0; localX < 16; localX++) {
|
||||
for (int localZ = 0; localZ < 16; localZ++) {
|
||||
int cutoff = cutoff(ageSeed, originX + localX, originZ + localZ);
|
||||
for (int y = minY; y < maxY; y++) {
|
||||
pos.set(originX + localX, y, originZ + localZ);
|
||||
BlockState state = chunk.getBlockState(pos);
|
||||
if (!removes(state, y, cutoff)) continue;
|
||||
chunk.setBlockState(pos, AIR, false);
|
||||
surface.update(localX, y, localZ, AIR);
|
||||
floor.update(localX, y, localZ, AIR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean removes(BlockState state, int y, int cutoff) {
|
||||
return !state.isAir() && (y <= cutoff || state.liquid());
|
||||
}
|
||||
|
||||
static int cutoff(long ageSeed, int x, int z) {
|
||||
double variation = 0.0D;
|
||||
int cellSize = 128;
|
||||
double amplitude = 12.0D;
|
||||
for (int octave = 0; octave < 7; octave++) {
|
||||
variation += valueNoise(ageSeed ^ SALT ^ (long) octave * 0x9E3779B97F4A7C15L,
|
||||
x, z, cellSize) * amplitude;
|
||||
cellSize /= 2;
|
||||
amplitude /= 2.0D;
|
||||
}
|
||||
return BASE_CUTOFF + (int) Math.round(variation);
|
||||
}
|
||||
|
||||
private static double valueNoise(long seed, int x, int z, int cellSize) {
|
||||
int cellX = Math.floorDiv(x, cellSize);
|
||||
int cellZ = Math.floorDiv(z, cellSize);
|
||||
double u = smooth((x - cellX * cellSize) / (double) cellSize);
|
||||
double v = smooth((z - cellZ * cellSize) / (double) cellSize);
|
||||
double north = lerp(value(seed, cellX, cellZ), value(seed, cellX + 1, cellZ), u);
|
||||
double south = lerp(value(seed, cellX, cellZ + 1), value(seed, cellX + 1, cellZ + 1), u);
|
||||
return lerp(north, south, v);
|
||||
}
|
||||
|
||||
private static double value(long seed, int x, int z) {
|
||||
long hash = mix(seed ^ (long) x * 0xD1B54A32D192ED03L
|
||||
^ (long) z * 0xABC98388FB8FAC03L);
|
||||
return (hash >>> 11) * 0x1.0p-52 - 1.0D;
|
||||
}
|
||||
|
||||
private static double smooth(double value) {
|
||||
return value * value * (3.0D - 2.0D * value);
|
||||
}
|
||||
|
||||
private static double lerp(double first, double second, double amount) {
|
||||
return first + (second - first) * amount;
|
||||
}
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.world.level.block.Blocks;
|
||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
|
||||
/** Checks the Legacy-style filter independently of a slow dimension-generation test. */
|
||||
@GameTestHolder(Rivencraft.MOD_ID)
|
||||
@PrefixGameTestTemplate(false)
|
||||
public final class SkylandsFeatureGameTests {
|
||||
private SkylandsFeatureGameTests() {}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void skylandsOnlyKeepsUpperSolidTerrain(GameTestHelper helper) {
|
||||
int cutoff = SkylandsFeature.cutoff(707L, 32, -48);
|
||||
helper.assertValueEqual(SkylandsFeature.cutoff(707L, 32, -48), cutoff,
|
||||
"Skylands cutoff must be deterministic for an Age seed and world column");
|
||||
helper.assertTrue(SkylandsFeature.removes(Blocks.STONE.defaultBlockState(), cutoff, cutoff),
|
||||
"Solid terrain at the cutoff must be removed");
|
||||
helper.assertFalse(SkylandsFeature.removes(Blocks.STONE.defaultBlockState(), cutoff + 1, cutoff),
|
||||
"Solid terrain above the cutoff must remain");
|
||||
helper.assertTrue(SkylandsFeature.removes(Blocks.WATER.defaultBlockState(), cutoff + 1, cutoff),
|
||||
"Liquids above the cutoff must be removed");
|
||||
helper.assertFalse(SkylandsFeature.removes(Blocks.AIR.defaultBlockState(), cutoff - 1, cutoff),
|
||||
"Air must remain air");
|
||||
helper.succeed();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
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.levelgen.Heightmap;
|
||||
|
||||
/**
|
||||
* Solid, wandering sphere nodes adapted from Mystcraft Legacy's MapGenSpheresMyst
|
||||
* (Mystcraft contributors, LGPL-3.0-or-later, commit 9bc8ddc). NeoForge chunk
|
||||
* writes and seed mixing are Rivencraft-specific.
|
||||
*/
|
||||
final class SpheresFeature {
|
||||
private static final int SOURCE_RANGE = 8;
|
||||
private static final long SALT = 0x53504845524553L;
|
||||
|
||||
private SpheresFeature() {}
|
||||
|
||||
static void generate(ChunkAccess chunk, long ageSeed, AgeFeatureSelection.Choice choice) {
|
||||
BlockState material = choice.material()
|
||||
.map(id -> BuiltInRegistries.BLOCK.get(id).defaultBlockState())
|
||||
.orElse(Blocks.COBBLESTONE.defaultBlockState());
|
||||
ChunkPos target = chunk.getPos();
|
||||
Random master = new Random(ageSeed ^ SALT);
|
||||
long xSeed = master.nextLong();
|
||||
long zSeed = master.nextLong();
|
||||
for (int sx = target.x - SOURCE_RANGE; sx <= target.x + SOURCE_RANGE; sx++) {
|
||||
for (int sz = target.z - SOURCE_RANGE; sz <= target.z + SOURCE_RANGE; sz++) {
|
||||
Random source = new Random((long) sx * xSeed ^ (long) sz * zSeed ^ ageSeed ^ SALT);
|
||||
if (source.nextFloat() > 0.05F) continue;
|
||||
double x = sx * 16 + source.nextInt(16);
|
||||
double y = source.nextInt(source.nextInt(192) + 1) + 32;
|
||||
double z = sz * 16 + source.nextInt(16);
|
||||
trace(chunk, material, source.nextLong(), x, y, z,
|
||||
1.0F + source.nextFloat() * 4.0F, 0, 0, -1, -1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void trace(ChunkAccess chunk, BlockState material, long seed,
|
||||
double x, double y, double z, float radius, float yaw, float pitch,
|
||||
int step, int length) {
|
||||
Random random = new Random(seed);
|
||||
if (length <= 0) {
|
||||
int maximum = SOURCE_RANGE * 16 - 16;
|
||||
length = maximum - random.nextInt(maximum / 4);
|
||||
}
|
||||
boolean singleNode = step == -1;
|
||||
if (singleNode) step = length / 2;
|
||||
int splitAt = random.nextInt(length / 2) + length / 4;
|
||||
boolean slowPitch = random.nextInt(6) == 0;
|
||||
float yawDrift = 0;
|
||||
float pitchDrift = 0;
|
||||
double middleX = chunk.getPos().getMinBlockX() + 8;
|
||||
double middleZ = chunk.getPos().getMinBlockZ() + 8;
|
||||
for (; step < length; step++) {
|
||||
double nodeRadius = 1.5D + Math.sin(step * Math.PI / length) * radius;
|
||||
float horizontal = Mth.cos(pitch);
|
||||
x += Mth.cos(yaw) * horizontal;
|
||||
y += Mth.sin(pitch);
|
||||
z += Mth.sin(yaw) * horizontal;
|
||||
pitch *= slowPitch ? 0.92F : 0.7F;
|
||||
pitch += pitchDrift * 0.1F;
|
||||
yaw += yawDrift * 0.1F;
|
||||
pitchDrift = pitchDrift * 0.9F
|
||||
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 2.0F;
|
||||
yawDrift = yawDrift * 0.75F
|
||||
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 4.0F;
|
||||
if (!singleNode && step == splitAt && radius > 1.0F) {
|
||||
trace(chunk, material, random.nextLong(), x, y, z,
|
||||
random.nextFloat() * 0.5F + 0.5F, yaw - Mth.HALF_PI, pitch / 3, step, length);
|
||||
trace(chunk, material, random.nextLong(), x, y, z,
|
||||
random.nextFloat() * 0.5F + 0.5F, yaw + Mth.HALF_PI, pitch / 3, step, length);
|
||||
return;
|
||||
}
|
||||
if (!singleNode && random.nextInt(4) == 0) continue;
|
||||
double dx = x - middleX;
|
||||
double dz = z - middleZ;
|
||||
double remaining = length - step;
|
||||
double reach = radius + 18;
|
||||
if (dx * dx + dz * dz - remaining * remaining > reach * reach) return;
|
||||
if (Math.abs(dx) <= 16 + nodeRadius * 2 && Math.abs(dz) <= 16 + nodeRadius * 2)
|
||||
fillNode(chunk, material, x, y, z, nodeRadius);
|
||||
if (singleNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void fillNode(ChunkAccess chunk, BlockState material,
|
||||
double centerX, double centerY, double centerZ, double radius) {
|
||||
int originX = chunk.getPos().getMinBlockX();
|
||||
int originZ = chunk.getPos().getMinBlockZ();
|
||||
int minX = Math.max(0, Mth.floor(centerX - radius) - originX - 1);
|
||||
int maxX = Math.min(16, Mth.floor(centerX + radius) - originX + 2);
|
||||
int minZ = Math.max(0, Mth.floor(centerZ - radius) - originZ - 1);
|
||||
int maxZ = Math.min(16, Mth.floor(centerZ + radius) - originZ + 2);
|
||||
int minY = Math.max(chunk.getMinBuildHeight() + 1, Mth.floor(centerY - radius) - 1);
|
||||
int maxY = Math.min(chunk.getMaxBuildHeight(), Mth.floor(centerY + radius) + 2);
|
||||
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
for (int worldY = minY; worldY < maxY; worldY++) {
|
||||
double dy = (worldY + 0.5D - centerY) / radius;
|
||||
for (int localZ = minZ; localZ < maxZ; localZ++) {
|
||||
double dz = (originZ + localZ + 0.5D - centerZ) / radius;
|
||||
for (int localX = minX; localX < maxX; localX++) {
|
||||
double dx = (originX + localX + 0.5D - centerX) / radius;
|
||||
if (!insideSphere(dx, dy, dz)) continue;
|
||||
pos.set(originX + localX, worldY, originZ + localZ);
|
||||
if (chunk.getBlockState(pos).is(Blocks.BEDROCK)) continue;
|
||||
chunk.setBlockState(pos, material, false);
|
||||
surface.update(localX, worldY, localZ, material);
|
||||
floor.update(localX, worldY, localZ, material);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean insideSphere(double dx, double dy, double dz) {
|
||||
return dx * dx + dy * dy + dz * dz < 1.0D;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.util.RandomSource;
|
||||
import net.minecraft.world.level.WorldGenLevel;
|
||||
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.levelgen.Heightmap;
|
||||
|
||||
/**
|
||||
* Rare surface spikes adapted from Mystcraft Legacy's WorldGenSpikesAdv
|
||||
* (Mystcraft contributors, LGPL-3.0-or-later, commit 9bc8ddc). The footprint
|
||||
* is kept in one chunk to avoid generation-order seams.
|
||||
*/
|
||||
final class SpikesFeature {
|
||||
private static final long SALT = 0x5350494B4553L;
|
||||
|
||||
private SpikesFeature() {}
|
||||
|
||||
static void generate(WorldGenLevel level, ChunkAccess chunk, long ageSeed,
|
||||
AgeFeatureSelection.Choice choice, int occurrence) {
|
||||
int originX = chunk.getPos().getMinBlockX();
|
||||
int originZ = chunk.getPos().getMinBlockZ();
|
||||
RandomSource random = RandomSource.create(ageSeed ^ SALT
|
||||
^ (long) originX * 341873128712L ^ (long) originZ * 132897987541L
|
||||
^ (long) occurrence * 0x9E3779B97F4A7C15L);
|
||||
if (random.nextInt(18) != 0) return;
|
||||
BlockState material = choice.material()
|
||||
.map(id -> BuiltInRegistries.BLOCK.get(id).defaultBlockState())
|
||||
.orElse(Blocks.STONE.defaultBlockState());
|
||||
// Keep the entire footprint in this chunk; neighboring decoration order
|
||||
// must not decide whether a spike exists or cut it at a chunk border.
|
||||
int x = originX + 4 + random.nextInt(8);
|
||||
int z = originZ + 4 + random.nextInt(8);
|
||||
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE_WG, x, z);
|
||||
int height = 6 + random.nextInt(32);
|
||||
int width = 1 + random.nextInt(4);
|
||||
if (y < level.getMinBuildHeight() + 1 || y + height >= level.getMaxBuildHeight()) return;
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
if (!level.isEmptyBlock(pos.set(x, y, z))) return;
|
||||
for (int px = x - width; px <= x + width; px++) {
|
||||
for (int pz = z - width; pz <= z + width; pz++) {
|
||||
if (!insideFootprint(px - x, pz - z, width)) continue;
|
||||
if (!level.getBlockState(pos.set(px, y - 1, pz)).isSolid()) return;
|
||||
}
|
||||
}
|
||||
for (int px = x - width; px <= x + width; px++) {
|
||||
for (int pz = z - width; pz <= z + width; pz++) {
|
||||
if (!insideFootprint(px - x, pz - z, width)) continue;
|
||||
int top = y + 1 + random.nextInt(random.nextInt(height) + 1);
|
||||
for (int py = y; py < top; py++) {
|
||||
pos.set(px, py, pz);
|
||||
if (level.isEmptyBlock(pos)) level.setBlock(pos, material, 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static boolean insideFootprint(int dx, int dz, int width) {
|
||||
return dx * dx + dz * dz <= width * width + 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package net.dnorberg.rivencraft.world;
|
||||
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.book.AgeFeatureSelection;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.core.registries.BuiltInRegistries;
|
||||
import net.minecraft.util.Mth;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
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.levelgen.Heightmap;
|
||||
|
||||
/**
|
||||
* Winding solid volumes adapted from Mystcraft Legacy's SymbolTendrils and
|
||||
* MapGenCavesMyst (LGPL-3.0-or-later). Generation stays inside the target chunk,
|
||||
* while source-chunk seeds make the same tendril meet seamlessly at its borders.
|
||||
*/
|
||||
final class TendrilFeature {
|
||||
private static final long SALT = 0x54454E4452494C53L;
|
||||
private static final int SOURCE_RANGE = 8;
|
||||
private static final int SOURCE_RATE = 15;
|
||||
private static final int SOURCE_SIZE = 18;
|
||||
|
||||
private TendrilFeature() {}
|
||||
|
||||
static void generate(ChunkAccess chunk, long ageSeed, AgeFeatureSelection.Choice choice) {
|
||||
BlockState material = choice.material()
|
||||
.map(id -> BuiltInRegistries.BLOCK.get(id).defaultBlockState())
|
||||
.orElse(Blocks.OAK_LOG.defaultBlockState());
|
||||
ChunkPos target = chunk.getPos();
|
||||
Random master = new Random(ageSeed ^ SALT);
|
||||
long xSeed = master.nextLong();
|
||||
long zSeed = master.nextLong();
|
||||
for (int sourceX = target.x - SOURCE_RANGE; sourceX <= target.x + SOURCE_RANGE; sourceX++) {
|
||||
for (int sourceZ = target.z - SOURCE_RANGE; sourceZ <= target.z + SOURCE_RANGE; sourceZ++) {
|
||||
Random source = new Random((long) sourceX * xSeed ^ (long) sourceZ * zSeed ^ ageSeed ^ SALT);
|
||||
int nodes = source.nextInt(source.nextInt(source.nextInt(SOURCE_SIZE) + 1) + 1);
|
||||
if (source.nextInt(SOURCE_RATE) != 0) continue;
|
||||
for (int node = 0; node < nodes; node++) {
|
||||
double x = sourceX * 16 + source.nextInt(16);
|
||||
double y = source.nextInt(source.nextInt(120) + 8);
|
||||
double z = sourceZ * 16 + source.nextInt(16);
|
||||
int branches = 1;
|
||||
if (source.nextInt(4) == 0) {
|
||||
trace(chunk, material, source.nextLong(), x, y, z,
|
||||
1.0F + source.nextFloat() * 6.0F, 0.0F, 0.0F, -1, -1, 0.5D);
|
||||
branches += source.nextInt(4);
|
||||
}
|
||||
for (int branch = 0; branch < branches; branch++) {
|
||||
float yaw = source.nextFloat() * Mth.TWO_PI;
|
||||
float pitch = (source.nextFloat() - 0.5F) / 4.0F;
|
||||
float radius = source.nextFloat() * 2.0F + source.nextFloat();
|
||||
if (source.nextInt(10) == 0)
|
||||
radius *= source.nextFloat() * source.nextFloat() * 3.0F + 1.0F;
|
||||
trace(chunk, material, source.nextLong(), x, y, z,
|
||||
radius, yaw, pitch, 0, 0, 1.0D);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void trace(ChunkAccess chunk, BlockState material, long seed,
|
||||
double x, double y, double z, float radius, float yaw, float pitch,
|
||||
int step, int length, double verticalScale) {
|
||||
Random random = new Random(seed);
|
||||
if (length <= 0) {
|
||||
int maximum = SOURCE_RANGE * 16 - 16;
|
||||
length = maximum - random.nextInt(maximum / 4);
|
||||
}
|
||||
boolean singleNode = step == -1;
|
||||
if (singleNode) step = length / 2;
|
||||
int splitAt = random.nextInt(length / 2) + length / 4;
|
||||
boolean slowPitch = random.nextInt(6) == 0;
|
||||
float yawDrift = 0.0F;
|
||||
float pitchDrift = 0.0F;
|
||||
double middleX = chunk.getPos().getMinBlockX() + 8;
|
||||
double middleZ = chunk.getPos().getMinBlockZ() + 8;
|
||||
|
||||
for (; step < length; step++) {
|
||||
double horizontalRadius = 1.5D + Math.sin(step * Math.PI / length) * radius;
|
||||
double verticalRadius = horizontalRadius * verticalScale;
|
||||
float horizontalMotion = Mth.cos(pitch);
|
||||
x += Mth.cos(yaw) * horizontalMotion;
|
||||
y += Mth.sin(pitch);
|
||||
z += Mth.sin(yaw) * horizontalMotion;
|
||||
pitch *= slowPitch ? 0.92F : 0.7F;
|
||||
pitch += pitchDrift * 0.1F;
|
||||
yaw += yawDrift * 0.1F;
|
||||
pitchDrift = pitchDrift * 0.9F
|
||||
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 2.0F;
|
||||
yawDrift = yawDrift * 0.75F
|
||||
+ (random.nextFloat() - random.nextFloat()) * random.nextFloat() * 4.0F;
|
||||
|
||||
if (!singleNode && step == splitAt && radius > 1.0F) {
|
||||
trace(chunk, material, random.nextLong(), x, y, z, random.nextFloat() * 0.5F + 0.5F,
|
||||
yaw - Mth.HALF_PI, pitch / 3.0F, step, length, 1.0D);
|
||||
trace(chunk, material, random.nextLong(), x, y, z, random.nextFloat() * 0.5F + 0.5F,
|
||||
yaw + Mth.HALF_PI, pitch / 3.0F, step, length, 1.0D);
|
||||
return;
|
||||
}
|
||||
if (!singleNode && random.nextInt(4) == 0) continue;
|
||||
double distanceX = x - middleX;
|
||||
double distanceZ = z - middleZ;
|
||||
double remaining = length - step;
|
||||
double reach = radius + 18.0D;
|
||||
if (distanceX * distanceX + distanceZ * distanceZ - remaining * remaining > reach * reach)
|
||||
return;
|
||||
if (x >= middleX - 16.0D - horizontalRadius * 2.0D
|
||||
&& x <= middleX + 16.0D + horizontalRadius * 2.0D
|
||||
&& z >= middleZ - 16.0D - horizontalRadius * 2.0D
|
||||
&& z <= middleZ + 16.0D + horizontalRadius * 2.0D) {
|
||||
fillNode(chunk, material, x, y, z, horizontalRadius, verticalRadius);
|
||||
}
|
||||
if (singleNode) break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void fillNode(ChunkAccess chunk, BlockState material, double centerX, double centerY,
|
||||
double centerZ, double horizontalRadius, double verticalRadius) {
|
||||
int originX = chunk.getPos().getMinBlockX();
|
||||
int originZ = chunk.getPos().getMinBlockZ();
|
||||
int minX = Math.max(0, Mth.floor(centerX - horizontalRadius) - originX - 1);
|
||||
int maxX = Math.min(16, Mth.floor(centerX + horizontalRadius) - originX + 2);
|
||||
int minZ = Math.max(0, Mth.floor(centerZ - horizontalRadius) - originZ - 1);
|
||||
int maxZ = Math.min(16, Mth.floor(centerZ + horizontalRadius) - originZ + 2);
|
||||
int minY = Math.max(chunk.getMinBuildHeight() + 1, Mth.floor(centerY - verticalRadius) - 1);
|
||||
int maxY = Math.min(chunk.getMaxBuildHeight(), Mth.floor(centerY + verticalRadius) + 2);
|
||||
Heightmap surface = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.WORLD_SURFACE_WG);
|
||||
Heightmap floor = chunk.getOrCreateHeightmapUnprimed(Heightmap.Types.OCEAN_FLOOR_WG);
|
||||
BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
|
||||
for (int worldY = minY; worldY < maxY; worldY++) {
|
||||
double dy = (worldY + 0.5D - centerY) / verticalRadius;
|
||||
if (dy <= -0.7D || dy * dy >= 1.0D) continue;
|
||||
for (int localZ = minZ; localZ < maxZ; localZ++) {
|
||||
double dz = (originZ + localZ + 0.5D - centerZ) / horizontalRadius;
|
||||
for (int localX = minX; localX < maxX; localX++) {
|
||||
double dx = (originX + localX + 0.5D - centerX) / horizontalRadius;
|
||||
if (dx * dx + dy * dy + dz * dz >= 1.0D) continue;
|
||||
pos.set(originX + localX, worldY, originZ + localZ);
|
||||
if (chunk.getBlockState(pos).is(Blocks.BEDROCK)) continue;
|
||||
chunk.setBlockState(pos, material, false);
|
||||
surface.update(localX, worldY, localZ, material);
|
||||
floor.update(localX, worldY, localZ, material);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,8 @@ public final class WritingGrammar {
|
||||
pendingBlock = parsed;
|
||||
} else if (role == Role.FEATURE && pendingBlock != null) {
|
||||
ResourceLocation feature = ((SymbolSemantics.ResourceValue) parsed.semantics().value()).id();
|
||||
if (!Set.of("rocks", "surface_lakes", "deep_lakes", "lava_lakes", "floating_islands")
|
||||
if (!Set.of("rocks", "surface_lakes", "deep_lakes", "lava_lakes",
|
||||
"floating_islands", "tendrils", "spheres", "spikes", "obelisks")
|
||||
.contains(feature.getPath()) || !feature.getNamespace().equals("rivencraft")) {
|
||||
diagnostics.add(new Diagnostic(Code.MISPLACED_MODIFIER, pendingBlock.index(),
|
||||
"This feature cannot consume a material page"));
|
||||
|
||||
@@ -245,6 +245,48 @@
|
||||
"symbol.rivencraft.feature.ravines.description": "Add vanilla canyon carving to noise terrain.",
|
||||
"symbol.rivencraft.feature.floating_islands": "Floating Islands",
|
||||
"symbol.rivencraft.feature.floating_islands.description": "Add high floating islands above the chosen surface terrain.",
|
||||
"symbol.rivencraft.feature.tendrils": "Tendrils",
|
||||
"symbol.rivencraft.feature.tendrils.description": "Add winding solid tendrils through the Age, using a random block or a preceding Block page.",
|
||||
"symbol.rivencraft.feature.skylands": "Skylands",
|
||||
"symbol.rivencraft.feature.skylands.description": "Remove terrain below a shifting sky cutoff; unlike Floating Islands, this does not add land.",
|
||||
"symbol.rivencraft.feature.spheres": "Spheres",
|
||||
"symbol.rivencraft.feature.spheres.description": "Add wandering chains of solid spheres through the Age, with a random or selected block material.",
|
||||
"symbol.rivencraft.feature.spikes": "Spikes",
|
||||
"symbol.rivencraft.feature.spikes.description": "Add rare rising spikes on exposed solid ground, with a random or selected block material.",
|
||||
"symbol.rivencraft.feature.dungeons": "Dungeons",
|
||||
"symbol.rivencraft.feature.dungeons.description": "Add monster-room dungeons below ground in this Age.",
|
||||
"symbol.rivencraft.feature.mineshafts": "Mineshafts",
|
||||
"symbol.rivencraft.feature.mineshafts.description": "Allow abandoned mineshafts in compatible terrain and biomes.",
|
||||
"symbol.rivencraft.feature.nether_fortresses": "Nether Fortresses",
|
||||
"symbol.rivencraft.feature.nether_fortresses.description": "Allow Nether fortresses in a Cave Age with a compatible Nether biome.",
|
||||
"symbol.rivencraft.feature.strongholds": "Strongholds",
|
||||
"symbol.rivencraft.feature.strongholds.description": "Allow strongholds with End portal rooms in a compatible Age.",
|
||||
"symbol.rivencraft.feature.villages": "Villages",
|
||||
"symbol.rivencraft.feature.villages.description": "Allow biome-appropriate villages on surface terrain.",
|
||||
"symbol.rivencraft.feature.trial_chambers": "Trial Chambers",
|
||||
"symbol.rivencraft.feature.trial_chambers.description": "Allow underground Trial Chambers in compatible Minecraft 1.21.1 biomes.",
|
||||
"symbol.rivencraft.feature.bastions": "Bastion Remnants",
|
||||
"symbol.rivencraft.feature.bastions.description": "Allow Bastion Remnants in a Cave Age with a compatible Nether biome.",
|
||||
"symbol.rivencraft.feature.buried_treasure": "Buried Treasure",
|
||||
"symbol.rivencraft.feature.buried_treasure.description": "Allow buried treasure beneath compatible beaches.",
|
||||
"symbol.rivencraft.feature.shipwrecks": "Shipwrecks",
|
||||
"symbol.rivencraft.feature.shipwrecks.description": "Allow shipwrecks in compatible ocean and beach biomes.",
|
||||
"symbol.rivencraft.feature.ocean_ruins": "Ocean Ruins",
|
||||
"symbol.rivencraft.feature.ocean_ruins.description": "Allow warm and cold ocean ruins in compatible biomes.",
|
||||
"symbol.rivencraft.feature.pillager_outposts": "Pillager Outposts",
|
||||
"symbol.rivencraft.feature.pillager_outposts.description": "Allow pillager outposts in compatible surface biomes.",
|
||||
"symbol.rivencraft.feature.ruined_portals": "Ruined Portals",
|
||||
"symbol.rivencraft.feature.ruined_portals.description": "Allow ruined portals in compatible surface or Nether biomes.",
|
||||
"symbol.rivencraft.feature.nether_fossils": "Nether Fossils",
|
||||
"symbol.rivencraft.feature.nether_fossils.description": "Allow Nether fossils in a Cave Age with a compatible Nether biome.",
|
||||
"symbol.rivencraft.feature.ancient_cities": "Ancient Cities",
|
||||
"symbol.rivencraft.feature.ancient_cities.description": "Allow ancient cities in compatible Deep Dark biomes.",
|
||||
"symbol.rivencraft.feature.trail_ruins": "Trail Ruins",
|
||||
"symbol.rivencraft.feature.trail_ruins.description": "Allow buried trail ruins in compatible surface biomes.",
|
||||
"symbol.rivencraft.feature.dense_ores": "Dense Ores",
|
||||
"symbol.rivencraft.feature.dense_ores.description": "Repeat compatible vanilla and modded ore placements once per page in this Age.",
|
||||
"symbol.rivencraft.feature.obelisks": "Obelisks",
|
||||
"symbol.rivencraft.feature.obelisks.description": "Add rare tall obelisks on exposed solid ground, with a random or selected block material.",
|
||||
"symbol.rivencraft.feature.lava_lakes": "Small Lakes",
|
||||
"symbol.rivencraft.feature.lava_lakes.description": "Add small surface lakes using a random fluid, or specify one with a fluid page.",
|
||||
"generator.rivencraft.standard": "Standard",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.ancient_cities",
|
||||
"description": "symbol.rivencraft.feature.ancient_cities.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:ancient_cities" },
|
||||
"poem": "Civilization Darkness Silence Ancient"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.bastions",
|
||||
"description": "symbol.rivencraft.feature.bastions.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:bastions" },
|
||||
"poem": "Civilization Ruin Greed Nether"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.buried_treasure",
|
||||
"description": "symbol.rivencraft.feature.buried_treasure.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:buried_treasure" },
|
||||
"poem": "Treasure Sand Ocean Memory"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.dense_ores",
|
||||
"description": "symbol.rivencraft.feature.dense_ores.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:dense_ores" },
|
||||
"poem": "Nature Material Treasure Abundance"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.dungeons",
|
||||
"description": "symbol.rivencraft.feature.dungeons.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:dungeons" },
|
||||
"poem": "Civilization Constraint Chain Resurrect"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.mineshafts",
|
||||
"description": "symbol.rivencraft.feature.mineshafts.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:mineshafts" },
|
||||
"poem": "Civilization Machine Motion Tradition"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.nether_fortresses",
|
||||
"description": "symbol.rivencraft.feature.nether_fortresses.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:nether_fortresses" },
|
||||
"poem": "Civilization Machine Power Entropy"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.nether_fossils",
|
||||
"description": "symbol.rivencraft.feature.nether_fossils.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:nether_fossils" },
|
||||
"poem": "Bone Death Nether Time"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.obelisks",
|
||||
"description": "symbol.rivencraft.feature.obelisks.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:obelisks" },
|
||||
"poem": "Civilization Structure Power Obelisk"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.ocean_ruins",
|
||||
"description": "symbol.rivencraft.feature.ocean_ruins.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:ocean_ruins" },
|
||||
"poem": "Ocean Stone Ruin Time"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.pillager_outposts",
|
||||
"description": "symbol.rivencraft.feature.pillager_outposts.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:pillager_outposts" },
|
||||
"poem": "Civilization Conflict Watchtower Danger"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.ruined_portals",
|
||||
"description": "symbol.rivencraft.feature.ruined_portals.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:ruined_portals" },
|
||||
"poem": "Portal Ruin Flame Memory"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.shipwrecks",
|
||||
"description": "symbol.rivencraft.feature.shipwrecks.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:shipwrecks" },
|
||||
"poem": "Ocean Journey Ruin Timber"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.skylands",
|
||||
"description": "symbol.rivencraft.feature.skylands.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:skylands" },
|
||||
"poem": "Terrain Sky Void Suspension"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.spheres",
|
||||
"description": "symbol.rivencraft.feature.spheres.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:spheres" },
|
||||
"poem": "Terrain Transform Form Cycle"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.spikes",
|
||||
"description": "symbol.rivencraft.feature.spikes.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:spikes" },
|
||||
"poem": "Nature Encourage Entropy Structure"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.strongholds",
|
||||
"description": "symbol.rivencraft.feature.strongholds.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:strongholds" },
|
||||
"poem": "Civilization Wisdom Future Honor"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.tendrils",
|
||||
"description": "symbol.rivencraft.feature.tendrils.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:tendrils" },
|
||||
"poem": "Terrain Thread Structure Winding"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.trail_ruins",
|
||||
"description": "symbol.rivencraft.feature.trail_ruins.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:trail_ruins" },
|
||||
"poem": "Civilization Buried Path Memory"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.trial_chambers",
|
||||
"description": "symbol.rivencraft.feature.trial_chambers.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:trial_chambers" },
|
||||
"poem": "Civilization Trial Structure Challenge"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schema_version": 2,
|
||||
"name": "symbol.rivencraft.feature.villages",
|
||||
"description": "symbol.rivencraft.feature.villages.description",
|
||||
"category": "rivencraft:feature",
|
||||
"semantics": { "role": "feature", "value": "rivencraft:villages" },
|
||||
"poem": "Civilization Society Harmony Nurture"
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"replace": false,
|
||||
"values": []
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"feature": "minecraft:monster_room",
|
||||
"placement": [
|
||||
{ "type": "minecraft:count", "count": 8 },
|
||||
{ "type": "minecraft:in_square" },
|
||||
{
|
||||
"type": "minecraft:height_range",
|
||||
"height": {
|
||||
"type": "minecraft:uniform",
|
||||
"min_inclusive": { "above_bottom": 1 },
|
||||
"max_inclusive": { "below_top": 1 }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"structures": [
|
||||
{ "structure": "minecraft:bastion_remnant", "weight": 1 }
|
||||
],
|
||||
"placement": {
|
||||
"type": "minecraft:random_spread",
|
||||
"spacing": 27,
|
||||
"separation": 4,
|
||||
"salt": 30084233
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"structures": [
|
||||
{ "structure": "minecraft:fortress", "weight": 1 }
|
||||
],
|
||||
"placement": {
|
||||
"type": "minecraft:random_spread",
|
||||
"spacing": 27,
|
||||
"separation": 4,
|
||||
"salt": 30084232
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@
|
||||
"minVersion": "0.8",
|
||||
"package": "net.dnorberg.rivencraft.mixin",
|
||||
"compatibilityLevel": "JAVA_21",
|
||||
"mixins": [
|
||||
"AgeDungeonPlacementMixin"
|
||||
],
|
||||
"client": [
|
||||
"client.ClientLevelSoundMixin",
|
||||
"client.ImmersivePortalsGuiRenderingMixin",
|
||||
|
||||
Reference in new issue
Block a user