Public Access
Port celestial systems and seeded Age completion to 26.3 beta
Build / build (push) Canceled after 0s
Build / build (push) Canceled after 0s
This commit is contained in:
1 parent
38e40ddba3
commit
40770841ee
32 files changed
+2792
-99
No files matched your search
@@ -21,7 +21,7 @@ public final class AgeCatalogSync {
|
||||
}
|
||||
|
||||
public static void registerPayloads(RegisterPayloadHandlersEvent event) {
|
||||
var registrar = event.registrar("6");
|
||||
var registrar = event.registrar("10");
|
||||
registrar.playToClient(AgeCatalogPayload.TYPE, AgeCatalogPayload.STREAM_CODEC);
|
||||
registrar.playToServer(AgeCatalogAckPayload.TYPE, AgeCatalogAckPayload.STREAM_CODEC,
|
||||
(payload, context) -> {
|
||||
|
||||
@@ -14,22 +14,27 @@ import org.joml.Vector3f;
|
||||
import org.joml.Vector3fc;
|
||||
|
||||
/** Frozen shared-color writing, adapted from 1.21.1 AgeSkyProfile. -1 means native. */
|
||||
public record PortAgeColors(int sky, int ambient) {
|
||||
public record PortAgeColors(int sky, int ambient, int celestial) {
|
||||
public static final PortAgeColors NATIVE = new PortAgeColors(-1, -1);
|
||||
public static final Codec<PortAgeColors> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.intRange(-1, 0xffffff).fieldOf("sky").forGetter(PortAgeColors::sky),
|
||||
Codec.intRange(-1, 0xffffff).fieldOf("ambient").forGetter(PortAgeColors::ambient)
|
||||
Codec.intRange(-1, 0xffffff).fieldOf("ambient").forGetter(PortAgeColors::ambient),
|
||||
Codec.intRange(-1, 0xffffff).optionalFieldOf("celestial", -1).forGetter(PortAgeColors::celestial)
|
||||
).apply(instance, PortAgeColors::new));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, PortAgeColors> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.INT, PortAgeColors::sky, ByteBufCodecs.INT, PortAgeColors::ambient, PortAgeColors::new);
|
||||
ByteBufCodecs.INT, PortAgeColors::sky, ByteBufCodecs.INT, PortAgeColors::ambient,
|
||||
ByteBufCodecs.INT, PortAgeColors::celestial, PortAgeColors::new);
|
||||
public static final Identifier AMBIENT = Identifier.fromNamespaceAndPath("rivencraft", "lighting/ambient_color");
|
||||
private static volatile Map<Identifier, PortAgeColors> clientColors = Map.of();
|
||||
|
||||
public PortAgeColors {
|
||||
if (sky < -1 || sky > 0xffffff || ambient < -1 || ambient > 0xffffff)
|
||||
if (sky < -1 || sky > 0xffffff || ambient < -1 || ambient > 0xffffff
|
||||
|| celestial < -1 || celestial > 0xffffff)
|
||||
throw new IllegalArgumentException("Invalid Age color");
|
||||
}
|
||||
|
||||
public PortAgeColors(int sky, int ambient) { this(sky, ambient, -1); }
|
||||
|
||||
public static void updateClient(Map<Identifier, PortAgeColors> colors) { clientColors = Map.copyOf(colors); }
|
||||
|
||||
public static PortAgeColors forLevel(Level level) {
|
||||
@@ -65,7 +70,7 @@ public record PortAgeColors(int sky, int ambient) {
|
||||
|
||||
public static PortAgeColors compile(List<Identifier> pages, long seed) {
|
||||
if (!validNumbers(pages)) throw new IllegalArgumentException("Number pages must precede a supported Color, Length, Phase, Angle, or Size modifier");
|
||||
int red = 0, green = 0, blue = 0, sky = -1, ambient = -1;
|
||||
int red = 0, green = 0, blue = 0, sky = -1, ambient = -1, celestial = -1;
|
||||
boolean written = false;
|
||||
for (int i = 0; i < pages.size(); i++) {
|
||||
Identifier page = pages.get(i);
|
||||
@@ -81,17 +86,43 @@ public record PortAgeColors(int sky, int ambient) {
|
||||
case "color/white" -> { red += amount; green += amount; blue += amount; }
|
||||
case "color/black" -> { red -= amount; green -= amount; blue -= amount; }
|
||||
}
|
||||
} else if (PortSkyStyle.forPage(page) != null || page.equals(AMBIENT)) {
|
||||
} else if (PortSkyStyle.forPage(page) != null || page.equals(AMBIENT)
|
||||
|| PortDayCycle.isController(page)) {
|
||||
if (PortDayCycle.isController(page)) celestial = -1;
|
||||
if (written) {
|
||||
int rgb = clamp(red) << 16 | clamp(green) << 8 | clamp(blue);
|
||||
if (page.equals(AMBIENT)) ambient = rgb; else sky = rgb;
|
||||
if (page.equals(AMBIENT)) ambient = rgb;
|
||||
else if (PortDayCycle.isController(page)) celestial = rgb;
|
||||
else sky = rgb;
|
||||
}
|
||||
red = green = blue = 0;
|
||||
written = false;
|
||||
}
|
||||
}
|
||||
if (written) sky = clamp(red) << 16 | clamp(green) << 8 | clamp(blue);
|
||||
return new PortAgeColors(sky, ambient);
|
||||
return new PortAgeColors(sky, ambient, celestial);
|
||||
}
|
||||
|
||||
/** Only pageless random Ages receive a random sky tint, as in 1.21.1. */
|
||||
public static PortAgeColors resolve(List<Identifier> pages, long seed) {
|
||||
if (!pages.isEmpty()) return compile(pages, seed);
|
||||
var random = new Random(seed ^ 0x534B5950524F464CL);
|
||||
random.nextInt(5); // Root sky-style draw precedes the sky-color draw.
|
||||
return new PortAgeColors(randomColor(random, true), -1);
|
||||
}
|
||||
|
||||
/** Shared palette; retain .85's truncation for already-defined body defaults. */
|
||||
static int randomColor(Random random, boolean rounded) {
|
||||
int[] palette = {0xB74A50, 0xD9834B, 0xDBC574, 0x5AAB80, 0x6FA8DF, 0x9870B9, 0xD5DBE1, 0x252531};
|
||||
int count = 1 + random.nextInt(3), red = 0, green = 0, blue = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
int color = palette[random.nextInt(palette.length)];
|
||||
red += color >> 16 & 255;
|
||||
green += color >> 8 & 255;
|
||||
blue += color & 255;
|
||||
}
|
||||
int offset = rounded ? count / 2 : 0;
|
||||
return (red + offset) / count << 16 | (green + offset) / count << 8 | (blue + offset) / count;
|
||||
}
|
||||
|
||||
private static int clamp(int value) { return Math.max(0, Math.min(255, value)); }
|
||||
|
||||
@@ -8,6 +8,7 @@ import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.item.LinkBookTarget;
|
||||
import net.minecraft.commands.Commands;
|
||||
import net.minecraft.commands.CommandSourceStack;
|
||||
import net.minecraft.commands.arguments.IdentifierArgument;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.core.component.DataComponents;
|
||||
import net.minecraft.network.chat.Component;
|
||||
@@ -68,6 +69,85 @@ public final class PortAgeCommands {
|
||||
return 1;
|
||||
})))
|
||||
.then(Commands.literal("age")
|
||||
.then(Commands.literal("create_biome_surface")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||
.then(Commands.argument("biome", IdentifierArgument.id())
|
||||
.executes(PortAgeCommands::createBiomeSurface))))))
|
||||
.then(Commands.literal("material_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(context -> probeBiomeMaterials(context, 0, 0))
|
||||
.then(Commands.argument("chunk_x", IntegerArgumentType.integer())
|
||||
.then(Commands.argument("chunk_z", IntegerArgumentType.integer())
|
||||
.executes(context -> probeBiomeMaterials(context,
|
||||
IntegerArgumentType.getInteger(context, "chunk_x"),
|
||||
IntegerArgumentType.getInteger(context, "chunk_z")))))))
|
||||
.then(Commands.literal("create_random_structures")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||
.then(Commands.argument("biome", IdentifierArgument.id())
|
||||
.executes(PortAgeCommands::createRandomStructures))))))
|
||||
.then(Commands.literal("create_random_features")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::createRandomFeatures)))))
|
||||
.then(Commands.literal("create_implied_material")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||
.then(Commands.argument("material", IdentifierArgument.id())
|
||||
.executes(PortAgeCommands::createImpliedMaterial))))))
|
||||
.then(Commands.literal("feature_plan")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeFeaturePlan)))
|
||||
.then(Commands.literal("create_completed_biomes")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||
.then(Commands.argument("distribution", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::createCompletedBiomes))))))
|
||||
.then(Commands.literal("biome_pool")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeBiomePool)))
|
||||
.then(Commands.literal("create_completed_sky")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> createCompletedSky(context, null))
|
||||
.then(Commands.argument("sky", StringArgumentType.word())
|
||||
.executes(context -> createCompletedSky(context,
|
||||
StringArgumentType.getString(context, "sky")))))))
|
||||
.then(Commands.literal("create_random_celestials")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(PortAgeCommands::createRandomCelestialsAge))))
|
||||
.then(Commands.literal("create_eclipse")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("mode", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::createEclipseAge)
|
||||
.then(Commands.argument("sky", StringArgumentType.word())
|
||||
.executes(context -> createEclipseAge(context,
|
||||
StringArgumentType.getString(context, "sky"))))))))
|
||||
.then(Commands.literal("create_celestials")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(PortAgeCommands::createMultiBodyAge))))
|
||||
.then(Commands.literal("create_body")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("body", StringArgumentType.word())
|
||||
.then(Commands.argument("phase", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::createBodyAge))))))
|
||||
.then(Commands.literal("create_celestial_color")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.then(Commands.argument("phase", StringArgumentType.word())
|
||||
.then(Commands.argument("color", StringArgumentType.word())
|
||||
.then(Commands.argument("number", IntegerArgumentType.integer(0, 24))
|
||||
.executes(PortAgeCommands::createCelestialColorAge)))))))
|
||||
.then(Commands.literal("create_size")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
@@ -209,8 +289,13 @@ public final class PortAgeCommands {
|
||||
PortRandomAgeSelection.Selection selected = PortRandomAgeSelection.select(
|
||||
context.getSource().getServer(), seed);
|
||||
var environment = PortEnvironmentSelection.resolve(java.util.List.of(), seed, selected.terrain());
|
||||
var cycle = PortDayCycle.resolve(java.util.List.of(), seed,
|
||||
PortDayCycle.nativeTicks(context.getSource().getServer().overworld()));
|
||||
var features = PortRandomFeatureSelection.select(context.getSource().getServer(),
|
||||
seed, selected.terrain(), selected.biomes());
|
||||
int created = createAge(context, selected.terrain(), selected.biomes(),
|
||||
null, null, 0, null, null, environment);
|
||||
features.rocks(), null, features.dungeons(), features.lakes(), features.terrain(), environment, cycle,
|
||||
PortAgeColors.resolve(java.util.List.of(), seed));
|
||||
if (created != 0) context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Random Age plan: terrain=" + selected.terrain()
|
||||
+ " biomes=" + selected.biomes()
|
||||
@@ -244,7 +329,9 @@ public final class PortAgeCommands {
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
.executes(context -> createAge(context, PortAgeTerrain.STANDARD,
|
||||
null, null, null, testDungeons())))))
|
||||
null, null, null, testDungeons()))
|
||||
.then(Commands.argument("terrain", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::createDungeonTerrain)))))
|
||||
.then(Commands.literal("create_lakes")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.then(Commands.argument("seed", LongArgumentType.longArg())
|
||||
@@ -378,7 +465,10 @@ public final class PortAgeCommands {
|
||||
.executes(PortAgeCommands::probeVegetation)))
|
||||
.then(Commands.literal("dungeons_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeDungeons)))
|
||||
.executes(context -> probeDungeons(context, 4))
|
||||
.then(Commands.argument("chunks", IntegerArgumentType.integer(1, 4))
|
||||
.executes(context -> probeDungeons(context,
|
||||
IntegerArgumentType.getInteger(context, "chunks"))))))
|
||||
.then(Commands.literal("lakes_probe")
|
||||
.then(Commands.argument("name", StringArgumentType.word())
|
||||
.executes(PortAgeCommands::probeLakes)))
|
||||
@@ -586,12 +676,659 @@ public final class PortAgeCommands {
|
||||
var pos = new net.minecraft.core.BlockPos(0, 80, 0);
|
||||
var sun = level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.SUN_ANGLE, pos);
|
||||
var light = level.environmentAttributes().getDimensionValue(net.minecraft.world.attribute.EnvironmentAttributes.SKY_LIGHT_LEVEL);
|
||||
var moon = level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.MOON_ANGLE, pos);
|
||||
if (cycle.customLight()) {
|
||||
float expectedAngle = cycle.bodyAngle(source.totalTicks());
|
||||
float weather = (1F - level.getRainLevel(1F) * 5F / 16F)
|
||||
* (1F - level.getThunderLevel(1F) * 5F / 16F);
|
||||
float expectedLight = 15F - (int) ((1F - cycle.bodyLight(source.totalTicks()) * weather) * 11F);
|
||||
if (cycle.singleBody() && (Math.abs(sun - expectedAngle) > 0.01F || Math.abs(moon - expectedAngle) > 0.01F)
|
||||
|| Math.abs(light - expectedLight) > 0.01F)
|
||||
throw new IllegalStateException("Single-body attribute mismatch");
|
||||
}
|
||||
context.getSource().sendSuccess(() -> Component.literal("Cycle probe " + ageId(context)
|
||||
+ " saved=" + cycle + " nativeTicks=" + source.totalTicks() + " ageTicks=" + sample.totalTicks()
|
||||
+ " sunAngle=" + sun + " skyLight=" + light), false);
|
||||
+ " sunAngle=" + sun + " moonAngle=" + moon + " skyLight=" + light), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int createBiomeSurface(CommandContext<CommandSourceStack> context) {
|
||||
try {
|
||||
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
||||
var biome = IdentifierArgument.getId(context, "biome");
|
||||
var plan = new PortBiomePlan(PortBiomePlan.Distribution.SINGLE, java.util.List.of(biome));
|
||||
var server = context.getSource().getServer();
|
||||
var registryOps = net.minecraft.resources.RegistryOps.create(com.mojang.serialization.JsonOps.INSTANCE, server.registryAccess());
|
||||
for (var key : java.util.List.of(net.minecraft.world.level.levelgen.NoiseGeneratorSettings.OVERWORLD,
|
||||
net.minecraft.world.level.levelgen.NoiseGeneratorSettings.AMPLIFIED,
|
||||
net.minecraft.world.level.levelgen.NoiseGeneratorSettings.CAVES,
|
||||
net.minecraft.world.level.levelgen.NoiseGeneratorSettings.FLOATING_ISLANDS)) {
|
||||
var base = server.registryAccess().lookupOrThrow(Registries.NOISE_SETTINGS).getOrThrow(key).value();
|
||||
var wrapped = PortBiomeMaterialRules.apply(server.registryAccess(), base).value();
|
||||
if (!wrapped.noiseSettings().equals(base.noiseSettings()) || wrapped.noiseRouter() != base.noiseRouter()
|
||||
|| wrapped.defaultBlock() != base.defaultBlock() || wrapped.defaultFluid() != base.defaultFluid()
|
||||
|| wrapped.seaLevel() != base.seaLevel() || !wrapped.aquifers().equals(base.aquifers()))
|
||||
throw new IllegalStateException("Biome material rules changed terrain/aquifer settings");
|
||||
var codec = net.minecraft.world.level.levelgen.material.rule.MaterialRule.HOLDER_CODEC;
|
||||
var encoded = codec.encodeStart(registryOps, wrapped.materialRule()).getOrThrow();
|
||||
var decoded = codec.parse(registryOps, encoded).getOrThrow();
|
||||
if (!encoded.equals(codec.encodeStart(registryOps, decoded).getOrThrow()))
|
||||
throw new IllegalStateException("Biome material rule codec round trip failed");
|
||||
}
|
||||
return createAge(context, terrain, plan);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** One terrain-stage chunk only: no feature/lighting/entity generation. */
|
||||
private static int probeBiomeMaterials(CommandContext<CommandSourceStack> context, int chunkX, int chunkZ) {
|
||||
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||
if (level == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age not loaded"));
|
||||
return 0;
|
||||
}
|
||||
var chunk = level.getChunk(chunkX, chunkZ, net.minecraft.world.level.chunk.status.ChunkStatus.TERRAIN);
|
||||
var counts = new TreeMap<String, Integer>();
|
||||
var pos = new net.minecraft.core.BlockPos.MutableBlockPos();
|
||||
for (int x = 0; x < 16; x++) for (int z = 0; z < 16; z++)
|
||||
for (int y = chunk.getMinY(); y < chunk.getMaxY(); y++) {
|
||||
var state = chunk.getBlockState(pos.set(chunk.getPos().getMinBlockX() + x, y, chunk.getPos().getMinBlockZ() + z));
|
||||
if (!state.isAir()) counts.merge(net.minecraft.core.registries.BuiltInRegistries.BLOCK
|
||||
.getKey(state.getBlock()).toString(), 1, Integer::sum);
|
||||
}
|
||||
context.getSource().sendSuccess(() -> Component.literal("Terrain materials " + ageId(context)
|
||||
+ " chunk=" + chunkX + "," + chunkZ + " " + counts), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int createRandomStructures(CommandContext<CommandSourceStack> context) {
|
||||
try {
|
||||
var server = context.getSource().getServer();
|
||||
verifyRandomStructures(server);
|
||||
long seed = LongArgumentType.getLong(context, "seed");
|
||||
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
||||
Identifier biome = IdentifierArgument.getId(context, "biome");
|
||||
var plan = biome.equals(Identifier.withDefaultNamespace("native")) ? null : new PortBiomePlan(PortBiomePlan.Distribution.SINGLE,
|
||||
java.util.List.of(biome));
|
||||
var selected = PortRandomFeatureSelection.select(server, seed, terrain, plan);
|
||||
int result = createAge(context, terrain, plan, selected.rocks(), null, selected.dungeons(), selected.lakes(), selected.terrain());
|
||||
return result == 0 ? 0 : probeFeaturePlan(context);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void verifyRandomStructures(net.minecraft.server.MinecraftServer server) {
|
||||
boolean empty = false, one = false, two = false, dungeons = false, caveDungeons = false, modern = false;
|
||||
for (var terrain : PortAgeTerrain.values()) for (long seed = 0; seed < 64; seed++) {
|
||||
var selected = PortRandomStructureSelection.select(server, seed, terrain, null);
|
||||
int count = selected.structures().size() + selected.dungeons();
|
||||
if (count > 2 || selected.dungeons() > 1
|
||||
|| new java.util.HashSet<>(selected.structures()).size() != selected.structures().size()
|
||||
|| !selected.equals(PortRandomStructureSelection.select(server, seed, terrain, null))
|
||||
|| terrain == PortAgeTerrain.VOID && count != 0)
|
||||
throw new IllegalStateException("Random structure count/terrain/determinism failed");
|
||||
empty |= count == 0;
|
||||
one |= count == 1;
|
||||
two |= count == 2;
|
||||
dungeons |= selected.dungeons() != 0;
|
||||
caveDungeons |= terrain == PortAgeTerrain.CAVE && selected.dungeons() != 0;
|
||||
modern |= selected.structures().stream().anyMatch(page -> page.getPath().equals("feature/trial_chambers"));
|
||||
for (var feature : selected.structures())
|
||||
if (!PortStructurePages.supportsTerrain(feature, terrain))
|
||||
throw new IllegalStateException("Random structure violated terrain eligibility");
|
||||
for (String biome : java.util.List.of("plains", "the_end", "basalt_deltas")) {
|
||||
var biomeId = Identifier.withDefaultNamespace(biome);
|
||||
var fixed = new PortBiomePlan(PortBiomePlan.Distribution.SINGLE, java.util.List.of(biomeId));
|
||||
var chosen = PortRandomStructureSelection.select(server, seed, terrain, fixed);
|
||||
for (var feature : chosen.structures())
|
||||
if (!PortStructurePages.randomEligible(feature, terrain, java.util.Set.of(biomeId), server.registryAccess()))
|
||||
throw new IllegalStateException("Random structure violated biome eligibility");
|
||||
}
|
||||
}
|
||||
if (!empty || !one || !two || !dungeons || !caveDungeons || !modern)
|
||||
throw new IllegalStateException("Random structure fixture coverage failed");
|
||||
var sets = server.registryAccess().lookupOrThrow(Registries.STRUCTURE_SET);
|
||||
var source = server.overworld().getChunkSource().getGenerator().getBiomeSource();
|
||||
var villages = ResourceKey.create(Registries.STRUCTURE_SET, Identifier.withDefaultNamespace("villages"));
|
||||
var page = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/villages");
|
||||
if (PortStructurePages.filtered(sets, java.util.List.of(), source).get(villages).isPresent()
|
||||
|| PortStructurePages.filtered(sets, java.util.List.of(page), source).get(villages).isEmpty()
|
||||
|| PortStructurePages.randomEligible(page, PortAgeTerrain.STANDARD,
|
||||
java.util.Set.of(Identifier.withDefaultNamespace("the_end")), server.registryAccess()))
|
||||
throw new IllegalStateException("Random empty-selection/biome gate failed");
|
||||
var merged = PortRandomFeatureSelection.select(server, 91, PortAgeTerrain.STANDARD, null);
|
||||
var base = PortRandomFeatureSelection.select(91, PortAgeTerrain.STANDARD);
|
||||
var encoded = PortTerrainFeaturePlan.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, merged.terrain()).getOrThrow();
|
||||
if (!merged.terrain().structureControl()
|
||||
|| !java.util.Objects.equals(merged.rocks(), base.rocks()) || !java.util.Objects.equals(merged.lakes(), base.lakes())
|
||||
|| !merged.terrain().equals(PortTerrainFeaturePlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow()))
|
||||
throw new IllegalStateException("Random structure merge/persistence failed");
|
||||
var older = PortTerrainFeaturePlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE,
|
||||
com.google.gson.JsonParser.parseString("{\"choices\":[],\"skylands\":true}")).getOrThrow();
|
||||
if (older.structureControl()) throw new IllegalStateException("Old Age acquired structure control");
|
||||
}
|
||||
|
||||
private static int createRandomFeatures(CommandContext<CommandSourceStack> context) {
|
||||
try {
|
||||
verifyRandomFeatures();
|
||||
long seed = LongArgumentType.getLong(context, "seed");
|
||||
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
||||
var features = PortRandomFeatureSelection.select(seed, terrain);
|
||||
int result = createAge(context, terrain, null, features.rocks(), null, 0, features.lakes(), features.terrain());
|
||||
return result == 0 ? 0 : probeFeaturePlan(context);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void verifyRandomFeatures() {
|
||||
var seen = new java.util.HashSet<Identifier>();
|
||||
boolean empty = false, single = false, doubleFeature = false, skylands = false, crystals = false, fissures = false;
|
||||
for (var terrain : PortAgeTerrain.values()) for (long seed = 0; seed < 64; seed++) {
|
||||
var selected = PortRandomFeatureSelection.select(seed, terrain);
|
||||
if (!selected.equals(PortRandomFeatureSelection.select(seed, terrain)))
|
||||
throw new IllegalStateException("Random feature completion is not deterministic");
|
||||
var materialPages = new java.util.ArrayList<Identifier>();
|
||||
if (selected.rocks() != null) {
|
||||
materialPages.add(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/rocks"));
|
||||
var encoded = PortRockPlan.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, selected.rocks()).getOrThrow();
|
||||
if (!selected.rocks().equals(PortRockPlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow()))
|
||||
throw new IllegalStateException("Random Rocks codec mismatch");
|
||||
}
|
||||
if (selected.lakes() != null) {
|
||||
materialPages.addAll(selected.lakes().choices().stream().map(PortLakePlan.Choice::page).toList());
|
||||
var encoded = PortLakePlan.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, selected.lakes()).getOrThrow();
|
||||
if (!selected.lakes().equals(PortLakePlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow()))
|
||||
throw new IllegalStateException("Random lake codec mismatch");
|
||||
}
|
||||
if (selected.terrain() != null) {
|
||||
var plan = selected.terrain();
|
||||
materialPages.addAll(plan.choices().stream().map(PortTerrainFeaturePlan.Choice::page)
|
||||
.filter(page -> !page.equals(PortTerrainFeaturePlan.CRYSTAL_FORMATIONS)).toList());
|
||||
skylands |= plan.skylands();
|
||||
crystals |= plan.choices().stream().anyMatch(choice -> choice.page().equals(PortTerrainFeaturePlan.CRYSTAL_FORMATIONS));
|
||||
fissures |= plan.starFissures();
|
||||
var encoded = PortTerrainFeaturePlan.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, plan).getOrThrow();
|
||||
if (!plan.equals(PortTerrainFeaturePlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow()))
|
||||
throw new IllegalStateException("Random terrain feature codec mismatch");
|
||||
}
|
||||
int count = materialPages.size();
|
||||
int expected = new java.util.Random(seed ^ 0x4D4154455249414CL).nextInt(3);
|
||||
if (count != expected || new java.util.HashSet<>(materialPages).size() != count)
|
||||
throw new IllegalStateException("Random material feature count/uniqueness failed");
|
||||
var terrainPage = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + terrain.name().toLowerCase(Locale.ROOT));
|
||||
for (Identifier feature : materialPages)
|
||||
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(java.util.List.of(terrainPage, feature)))
|
||||
throw new IllegalStateException("Random feature is incompatible with its terrain");
|
||||
seen.addAll(materialPages);
|
||||
empty |= count == 0;
|
||||
single |= count == 1;
|
||||
doubleFeature |= count == 2;
|
||||
}
|
||||
if (seen.size() != 9 || !empty || !single || !doubleFeature || !skylands || !crystals || !fissures)
|
||||
throw new IllegalStateException("Random feature seed coverage failed");
|
||||
}
|
||||
|
||||
private static int createImpliedMaterial(CommandContext<CommandSourceStack> context) {
|
||||
try {
|
||||
verifyMaterialCompletion();
|
||||
var server = context.getSource().getServer();
|
||||
long seed = LongArgumentType.getLong(context, "seed");
|
||||
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
||||
var material = net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(
|
||||
IdentifierArgument.getId(context, "material"));
|
||||
var pages = java.util.List.of(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
|
||||
"terrain/" + terrain.name().toLowerCase(Locale.ROOT)), material);
|
||||
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server, seed);
|
||||
int result = createAge(context, terrain, biomes,
|
||||
net.dnorberg.rivencraft.writing.PortAuthoredWriting.rockPlan(pages, seed), null, 0,
|
||||
net.dnorberg.rivencraft.writing.PortAuthoredWriting.lakePlan(pages, seed),
|
||||
net.dnorberg.rivencraft.writing.PortAuthoredWriting.terrainFeaturePlan(pages, seed));
|
||||
return result == 0 ? 0 : probeFeaturePlan(context);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void verifyMaterialCompletion() {
|
||||
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||
var solid = net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(Identifier.withDefaultNamespace("glowstone"));
|
||||
var fluid = net.dnorberg.rivencraft.writing.PortRegistryPages.blockPageId(Identifier.withDefaultNamespace("water"));
|
||||
var seen = new java.util.HashSet<Identifier>();
|
||||
for (var terrain : PortAgeTerrain.values()) for (long seed = 0; seed < 64; seed++) {
|
||||
var terrainPage = symbol.apply("terrain/" + terrain.name().toLowerCase(Locale.ROOT));
|
||||
var pages = java.util.List.of(terrainPage, solid, solid);
|
||||
var completed = net.dnorberg.rivencraft.writing.PortMaterialCompletion.complete(pages, seed);
|
||||
if (completed.size() != 5 || !completed.get(1).equals(solid) || !completed.get(3).equals(solid)
|
||||
|| !net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(completed)
|
||||
|| !completed.equals(net.dnorberg.rivencraft.writing.PortMaterialCompletion.complete(pages, seed))
|
||||
|| !completed.equals(net.dnorberg.rivencraft.writing.PortMaterialCompletion.complete(completed, seed)))
|
||||
throw new IllegalStateException("Implied material completion lost order, compatibility or determinism");
|
||||
seen.add(completed.get(2));
|
||||
var explicit = java.util.List.of(terrainPage, solid, PortTerrainFeaturePlan.TENDRILS);
|
||||
if (!explicit.equals(net.dnorberg.rivencraft.writing.PortMaterialCompletion.complete(explicit, seed)))
|
||||
throw new IllegalStateException("Explicit material target was replaced");
|
||||
boolean wetTerrain = terrain == PortAgeTerrain.STANDARD || terrain == PortAgeTerrain.AMPLIFIED || terrain == PortAgeTerrain.ISLAND;
|
||||
var wet = java.util.List.of(terrainPage, fluid);
|
||||
if (net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(wet) != wetTerrain)
|
||||
throw new IllegalStateException("Implied fluid terrain validation failed");
|
||||
if (wetTerrain) {
|
||||
var lakes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.lakePlan(wet, seed);
|
||||
if (lakes == null || lakes.choices().size() != 1
|
||||
|| !lakes.choices().getFirst().fluid().equals(Identifier.withDefaultNamespace("water")))
|
||||
throw new IllegalStateException("Implied lake discarded its authored fluid");
|
||||
}
|
||||
}
|
||||
if (seen.size() < 6 || net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(
|
||||
java.util.List.of(fluid, PortTerrainFeaturePlan.TENDRILS)))
|
||||
throw new IllegalStateException("Implied feature coverage/explicit incompatibility check failed");
|
||||
}
|
||||
|
||||
private static int probeFeaturePlan(CommandContext<CommandSourceStack> context) {
|
||||
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||
if (level == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age not loaded"));
|
||||
return 0;
|
||||
}
|
||||
if (!(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)) {
|
||||
context.getSource().sendSuccess(() -> Component.literal("Feature plan " + ageId(context) + ": none"), false);
|
||||
return 1;
|
||||
}
|
||||
context.getSource().sendSuccess(() -> Component.literal("Feature plan " + ageId(context)
|
||||
+ " rocks=" + features.rocks() + " lakes=" + features.lakes() + " dungeons=" + features.dungeons()
|
||||
+ " terrain=" + features.terrainFeatures()), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int createCompletedBiomes(CommandContext<CommandSourceStack> context) {
|
||||
try {
|
||||
var server = context.getSource().getServer();
|
||||
long seed = LongArgumentType.getLong(context, "seed");
|
||||
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
||||
var distribution = PortBiomePlan.Distribution.valueOf(
|
||||
StringArgumentType.getString(context, "distribution").toUpperCase(Locale.ROOT));
|
||||
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||
var pages = java.util.List.of(symbol.apply("terrain/" + terrain.name().toLowerCase(Locale.ROOT)),
|
||||
symbol.apply("biome/" + distribution.name().toLowerCase(Locale.ROOT)));
|
||||
var plan = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server, seed);
|
||||
// Controller-only and partial pools complete; already-sufficient pools never gain extras.
|
||||
var plains = net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace("plains"));
|
||||
var forest = net.dnorberg.rivencraft.writing.PortRegistryPages.biomePageId(Identifier.withDefaultNamespace("forest"));
|
||||
for (var mode : PortBiomePlan.Distribution.values()) {
|
||||
var controller = symbol.apply("biome/" + mode.name().toLowerCase(Locale.ROOT));
|
||||
for (long s = 0; s < 32; s++) {
|
||||
var selected = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(controller), server, s);
|
||||
if (!selected.equals(net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(controller), server, s))
|
||||
|| selected.distribution() != mode)
|
||||
throw new IllegalStateException("Biome completion is not deterministic");
|
||||
}
|
||||
if (mode == PortBiomePlan.Distribution.NATIVE || mode == PortBiomePlan.Distribution.SINGLE) continue;
|
||||
var partial = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(controller, plains), server, seed);
|
||||
var full = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(controller, plains, forest), server, seed);
|
||||
var repeated = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(java.util.List.of(controller, plains, plains), server, seed);
|
||||
if (partial.biomes().size() != 2 || !partial.biomes().getFirst().equals(Identifier.withDefaultNamespace("plains"))
|
||||
|| !full.biomes().equals(java.util.List.of(Identifier.withDefaultNamespace("plains"), Identifier.withDefaultNamespace("forest")))
|
||||
|| repeated.biomes().size() != 3 || new java.util.HashSet<>(repeated.biomes()).size() != 2)
|
||||
throw new IllegalStateException("Authored pool completion changed weights or added unnecessary biomes");
|
||||
}
|
||||
var encoded = PortBiomePlan.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, plan).getOrThrow();
|
||||
if (!plan.equals(PortBiomePlan.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow()))
|
||||
throw new IllegalStateException("Completed biome plan codec mismatch");
|
||||
int created = createAge(context, terrain, plan);
|
||||
if (created != 0) context.getSource().sendSuccess(() -> Component.literal("Completed biome plan: " + plan), false);
|
||||
return created == 0 ? 0 : probeBiomePool(context);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int probeBiomePool(CommandContext<CommandSourceStack> context) {
|
||||
var level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, ageId(context)));
|
||||
if (level == null) {
|
||||
context.getSource().sendFailure(Component.literal("Age not loaded"));
|
||||
return 0;
|
||||
}
|
||||
var biomes = level.getChunkSource().getGenerator().getBiomeSource().possibleBiomes().stream()
|
||||
.flatMap(holder -> holder.unwrapKey().stream()).map(key -> key.identifier()).sorted().toList();
|
||||
context.getSource().sendSuccess(() -> Component.literal("Biome pool " + ageId(context) + ": " + biomes), false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static int createCompletedSky(CommandContext<CommandSourceStack> context, String sky) {
|
||||
try {
|
||||
verifyRandomSkyColors();
|
||||
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||
var pages = new java.util.ArrayList<Identifier>(java.util.stream.Stream.of("terrain/flat", "lighting/normal", "weather/off").map(symbol).toList());
|
||||
if (sky != null) {
|
||||
PortSkyStyle.valueOf(sky.toUpperCase(Locale.ROOT));
|
||||
pages.add(symbol.apply("sky/" + sky.toLowerCase(Locale.ROOT)));
|
||||
}
|
||||
var server = context.getSource().getServer();
|
||||
long seed = LongArgumentType.getLong(context, "seed"), origin = PortDayCycle.nativeTicks(server.overworld());
|
||||
var cycle = PortDayCycle.resolve(pages, seed, origin);
|
||||
int zeroSeed = -1;
|
||||
boolean sunOnly = false, moonOnly = false, mixed = false;
|
||||
for (int s = 0; s < 256; s++) {
|
||||
var selected = PortDayCycle.resolve(java.util.List.of(), s, origin);
|
||||
long suns = selected.bodies().stream().filter(body -> body.kind() == PortDayCycle.SUN).count();
|
||||
long moons = selected.bodies().size() - suns;
|
||||
if (suns > 3 || moons > 3 || !selected.authored()
|
||||
|| !selected.equals(PortDayCycle.resolve(pages, s, origin)))
|
||||
throw new IllegalStateException("Missing-celestial completion bounds/determinism failed");
|
||||
if (suns == 0 && moons == 0) {
|
||||
zeroSeed = zeroSeed < 0 ? s : zeroSeed;
|
||||
if (selected.bodyMode() != PortDayCycle.NONE || selected.customLight())
|
||||
throw new IllegalStateException("Empty sky incorrectly acquires a hidden native pair");
|
||||
}
|
||||
sunOnly |= suns > 0 && moons == 0;
|
||||
moonOnly |= moons > 0 && suns == 0;
|
||||
mixed |= suns > 0 && moons > 0;
|
||||
}
|
||||
for (String controller : java.util.List.of("celestial/sun", "celestial/moon", "celestial/normal_time")) {
|
||||
var explicit = java.util.List.of(symbol.apply(controller));
|
||||
if (!PortDayCycle.resolve(explicit, seed, origin).equals(PortDayCycle.compile(explicit, seed, origin)))
|
||||
throw new IllegalStateException("Explicit controller gained additional bodies");
|
||||
}
|
||||
var older = PortDayCycle.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE,
|
||||
com.google.gson.JsonParser.parseString("{\"authored\":false,\"period\":1,\"stopped_time\":0}")).getOrThrow();
|
||||
if (zeroSeed < 0 || !sunOnly || !moonOnly || !mixed || !older.equals(PortDayCycle.NATIVE))
|
||||
throw new IllegalStateException("Completion coverage/old-profile default failed");
|
||||
var environment = PortEnvironmentSelection.resolve(pages, seed, PortAgeTerrain.FLAT);
|
||||
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
|
||||
RuntimeAgeBackend.create(server, ageId(context), seed, biomes, PortAgeTerrain.FLAT,
|
||||
null, null, 0, null, null, environment.weather(), environment.lighting(), environment.sky(),
|
||||
PortAgeColors.compile(pages, seed), cycle);
|
||||
final int sample = zeroSeed;
|
||||
context.getSource().sendSuccess(() -> Component.literal("Completed sky; sample zero-body seed=" + sample), false);
|
||||
return probeDayCycle(context);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int createRandomCelestialsAge(CommandContext<CommandSourceStack> context) {
|
||||
try {
|
||||
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||
var pages = java.util.stream.Stream.of("celestial/sun", "celestial/moon", "celestial/sun",
|
||||
"weather/off", "lighting/normal", "terrain/flat").map(symbol).toList();
|
||||
var server = context.getSource().getServer();
|
||||
long seed = LongArgumentType.getLong(context, "seed"), origin = PortDayCycle.nativeTicks(server.overworld());
|
||||
var cycle = PortDayCycle.compile(pages, seed, origin);
|
||||
var decorated = new java.util.ArrayList<Identifier>(java.util.stream.Stream.of("weather/off", "sky/overworld",
|
||||
"terrain/flat", "number/10", "color/red", "lighting/ambient_color").map(symbol).toList());
|
||||
decorated.addAll(pages);
|
||||
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(pages)
|
||||
|| cycle.bodies().size() != 3 || cycle.bodies().get(0).equals(cycle.bodies().get(2))
|
||||
|| !cycle.equals(PortDayCycle.compile(pages, seed, origin))
|
||||
|| !cycle.bodies().equals(PortDayCycle.compile(decorated, seed, origin).bodies()))
|
||||
throw new IllegalStateException("Random body determinism/independence failed");
|
||||
var oneSun = PortDayCycle.compile(java.util.List.of(symbol.apply("celestial/sun")), seed, origin);
|
||||
var oneMoon = PortDayCycle.compile(java.util.List.of(symbol.apply("celestial/moon")), seed, origin);
|
||||
if (oneSun.bodies().size() != 1 || oneMoon.bodies().size() != 1
|
||||
|| !oneSun.bodies().getFirst().equals(cycle.bodies().getFirst())
|
||||
|| oneMoon.bodies().getFirst().kind() != PortDayCycle.MOON)
|
||||
throw new IllegalStateException("Standalone bare body compilation failed");
|
||||
int stopped = 0, moving = 0, forward = 0, reverse = 0;
|
||||
for (int s = 0; s < 128; s++) {
|
||||
var random = PortDayCycle.compile(pages, s, origin);
|
||||
for (var body : random.bodies()) {
|
||||
if (body.period() == 0) stopped++; else moving++;
|
||||
if (body.clockwise()) forward++; else reverse++;
|
||||
if (body.period() != 0 && (body.period() < 0.2 || body.period() > 2)
|
||||
|| body.size() < 0.75 || body.size() >= 1.75 || body.bearing() < 0
|
||||
|| body.bearing() >= 360 || body.phaseTick() < 0 || body.phaseTick() >= 24000
|
||||
|| body.color() < 0 || body.color() > 0xffffff)
|
||||
throw new IllegalStateException("Random body bounds failed");
|
||||
}
|
||||
}
|
||||
if (stopped == 0 || moving == 0 || forward == 0 || reverse == 0)
|
||||
throw new IllegalStateException("Random body pool coverage failed");
|
||||
var explicit = PortDayCycle.compile(java.util.List.of(symbol.apply("size/small"), symbol.apply("celestial/sun")), seed, origin);
|
||||
var normal = PortDayCycle.compile(java.util.List.of(PortDayCycle.NORMAL_TIME), seed, origin);
|
||||
if (!explicit.bodies().isEmpty() || explicit.sunSize() != 0.5F || explicit.period() != 1 || explicit.bearing() != -1
|
||||
|| !normal.bodies().isEmpty() || normal.period() != 1 || normal.sunSize() != 1)
|
||||
throw new IllegalStateException("Explicit/Normal Time default changed");
|
||||
var environment = PortEnvironmentSelection.resolve(pages, seed, PortAgeTerrain.FLAT);
|
||||
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
|
||||
RuntimeAgeBackend.create(server, ageId(context), seed, biomes, PortAgeTerrain.FLAT,
|
||||
null, null, 0, null, null, environment.weather(), environment.lighting(), environment.sky(),
|
||||
PortAgeColors.compile(pages, seed), cycle);
|
||||
return probeDayCycle(context);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int createEclipseAge(CommandContext<CommandSourceStack> context) {
|
||||
return createEclipseAge(context, null);
|
||||
}
|
||||
|
||||
private static int createEclipseAge(CommandContext<CommandSourceStack> context, String sky) {
|
||||
try {
|
||||
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||
String mode = StringArgumentType.getString(context, "mode");
|
||||
if (!java.util.Set.of("aligned", "partial", "clear", "below", "moving").contains(mode))
|
||||
throw new IllegalArgumentException("Use aligned, partial, clear, below or moving");
|
||||
long seed = LongArgumentType.getLong(context, "seed");
|
||||
var server = context.getSource().getServer();
|
||||
long origin = PortDayCycle.nativeTicks(server.overworld());
|
||||
var pages = new java.util.ArrayList<Identifier>();
|
||||
pages.add(symbol.apply("period/zero"));
|
||||
if (mode.equals("partial")) pages.add(symbol.apply("number/13"));
|
||||
pages.add(symbol.apply(mode.equals("below") ? "phase/nadir" : "phase/zenith"));
|
||||
if (mode.equals("partial")) { pages.add(symbol.apply("number/24")); pages.add(symbol.apply("size/large")); }
|
||||
pages.addAll(java.util.stream.Stream.of("number/24", "color/red", "celestial/sun",
|
||||
mode.equals("moving") ? "period/half" : "period/zero",
|
||||
mode.equals("below") || mode.equals("clear") ? "phase/nadir" : "phase/zenith").map(symbol).toList());
|
||||
if (mode.equals("partial")) pages.add(symbol.apply("number/24"));
|
||||
pages.addAll(java.util.stream.Stream.of("size/large", "celestial/moon", "weather/off", "lighting/normal", "terrain/flat").map(symbol).toList());
|
||||
if (sky != null) {
|
||||
PortSkyStyle.valueOf(sky.toUpperCase(Locale.ROOT));
|
||||
pages.add(symbol.apply("sky/" + sky.toLowerCase(Locale.ROOT)));
|
||||
}
|
||||
var cycle = PortDayCycle.compile(pages, seed, origin);
|
||||
var bodies = cycle.bodies();
|
||||
float eclipse = PortCelestialBody.eclipseAt(bodies, origin);
|
||||
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(pages)
|
||||
|| (mode.equals("aligned") || mode.equals("moving")) && (Math.abs(eclipse - 1) > 0.0001F
|
||||
|| Math.abs(cycle.bodyLight(origin) - 0.15F) > 0.0001F)
|
||||
|| mode.equals("partial") && !(eclipse > 0 && eclipse < 1)
|
||||
|| (mode.equals("clear") || mode.equals("below")) && eclipse != 0)
|
||||
throw new IllegalStateException("Eclipse fixture failed: " + mode);
|
||||
var sun = bodies.get(0);
|
||||
var moon = bodies.get(1);
|
||||
if (sun.eclipse(java.util.List.of(sun, sun), origin) != 0
|
||||
|| sun.eclipseColor(1) != 0xfccccc || moon.eclipseColor(1) != 0
|
||||
|| mode.equals("moving") && (PortCelestialBody.eclipseAt(bodies, origin + 100) >= eclipse
|
||||
|| PortCelestialBody.eclipseAt(bodies, origin + 800) != 0))
|
||||
throw new IllegalStateException("Eclipse color/movement invariant failed");
|
||||
var environment = PortEnvironmentSelection.resolve(pages, seed, PortAgeTerrain.FLAT);
|
||||
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
|
||||
RuntimeAgeBackend.create(server, ageId(context), seed, biomes, PortAgeTerrain.FLAT,
|
||||
null, null, 0, null, null, environment.weather(), environment.lighting(), environment.sky(),
|
||||
PortAgeColors.compile(pages, seed), cycle);
|
||||
context.getSource().sendSuccess(() -> Component.literal("Eclipse fixture " + mode + " overlap=" + eclipse
|
||||
+ " bodyLight=" + cycle.bodyLight(origin) + " skyShade=" + (1F - 0.65F * eclipse)), false);
|
||||
return probeDayCycle(context);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int createMultiBodyAge(CommandContext<CommandSourceStack> context) {
|
||||
try {
|
||||
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||
var pages = java.util.stream.Stream.of("period/zero", "phase/zenith", "size/large", "number/24", "color/red", "celestial/sun",
|
||||
"period/zero", "phase/nadir", "size/small", "number/24", "color/blue", "celestial/moon",
|
||||
"period/half", "phase/rising", "angle/east", "direction/counterclockwise", "number/12", "color/green", "celestial/sun",
|
||||
"weather/off", "lighting/normal", "terrain/flat").map(symbol).toList();
|
||||
var server = context.getSource().getServer();
|
||||
long seed = LongArgumentType.getLong(context, "seed"), origin = PortDayCycle.nativeTicks(server.overworld());
|
||||
var cycle = PortDayCycle.compile(pages, seed, origin);
|
||||
var bodies = cycle.bodies();
|
||||
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(pages) || bodies.size() != 3
|
||||
|| bodies.get(0).kind() != PortDayCycle.SUN || bodies.get(0).color() != 0xf00000 || bodies.get(0).size() != 2F
|
||||
|| bodies.get(1).kind() != PortDayCycle.MOON || bodies.get(1).color() != 0x0000f0 || bodies.get(1).size() != 0.5F
|
||||
|| bodies.get(2).color() != 0x007800 || bodies.get(2).size() != 1F || bodies.get(2).bearing() != 90
|
||||
|| Math.abs(bodies.get(2).angle(origin + 100) + 93F) > 0.001F
|
||||
|| cycle.bodyLight(origin) != 1F)
|
||||
throw new IllegalStateException("Independent body fixture failed");
|
||||
var limits = new java.util.ArrayList<Identifier>();
|
||||
for (int i = 0; i < 10; i++) limits.add(PortDayCycle.NORMAL_TIME);
|
||||
var atLimit = PortDayCycle.compile(limits, seed, origin);
|
||||
if (!PortDayCycle.validWriting(limits) || atLimit.bodies().size() != 20)
|
||||
throw new IllegalStateException("Body limit rejected valid writing");
|
||||
limits.add(symbol.apply("celestial/sun"));
|
||||
if (PortDayCycle.validWriting(limits)) throw new IllegalStateException("Body limit not enforced");
|
||||
var reset = PortDayCycle.compile(java.util.stream.Stream.of("number/24", "color/red", "size/large", "celestial/sun",
|
||||
"size/normal", "celestial/moon", "celestial/normal_time").map(symbol).toList(), seed, origin).bodies();
|
||||
if (reset.size() != 4 || reset.get(1).color() != -1 || reset.get(1).size() != 1F
|
||||
|| reset.get(2).color() != -1 || reset.get(3).size() != 1F
|
||||
|| Math.abs(Math.abs(reset.get(2).angle(origin) - reset.get(3).angle(origin)) - 180F) > 0.001F)
|
||||
throw new IllegalStateException("Body modifier reset/paired expansion failed");
|
||||
var moonlight = PortDayCycle.compile(java.util.stream.Stream.of("period/zero", "phase/zenith", "celestial/moon",
|
||||
"period/zero", "phase/zenith", "celestial/moon").map(symbol).toList(), seed, origin);
|
||||
var sunlight = PortDayCycle.compile(java.util.stream.Stream.of("period/zero", "phase/zenith", "celestial/sun",
|
||||
"period/zero", "phase/zenith", "celestial/sun").map(symbol).toList(), seed, origin);
|
||||
if (Math.abs(moonlight.bodyLight(origin) - 0.36F) > 0.0001F || sunlight.bodyLight(origin) != 1F)
|
||||
throw new IllegalStateException("Multi-body light aggregation failed");
|
||||
var encoded = PortDayCycle.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, cycle).getOrThrow();
|
||||
if (!cycle.equals(PortDayCycle.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow()))
|
||||
throw new IllegalStateException("Body profile codec round trip failed");
|
||||
var environment = PortEnvironmentSelection.resolve(pages, seed, PortAgeTerrain.FLAT);
|
||||
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
|
||||
RuntimeAgeBackend.create(server, ageId(context), seed, biomes, PortAgeTerrain.FLAT,
|
||||
null, null, 0, null, null, environment.weather(), environment.lighting(), environment.sky(),
|
||||
PortAgeColors.compile(pages, seed), cycle);
|
||||
return probeDayCycle(context);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int createBodyAge(CommandContext<CommandSourceStack> context) {
|
||||
try {
|
||||
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||
String body = StringArgumentType.getString(context, "body");
|
||||
Identifier controller = symbol.apply("celestial/" + body);
|
||||
Identifier phase = symbol.apply("phase/" + StringArgumentType.getString(context, "phase"));
|
||||
if ((!body.equals("sun") && !body.equals("moon")) || !PortDayCycle.isPhase(phase))
|
||||
throw new IllegalArgumentException("Use sun/moon and rising/zenith/setting/nadir");
|
||||
var pages = java.util.stream.Stream.of("period/zero", phase.getPath(), "angle/east",
|
||||
"size/large", "number/24", "color/red", controller.getPath(), "weather/off",
|
||||
"lighting/normal", "terrain/flat").map(symbol).toList();
|
||||
var server = context.getSource().getServer();
|
||||
long seed = LongArgumentType.getLong(context, "seed"), origin = PortDayCycle.nativeTicks(server.overworld());
|
||||
var cycle = PortDayCycle.compile(pages, seed, origin);
|
||||
var colors = PortAgeColors.compile(pages, seed);
|
||||
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(pages)
|
||||
|| !cycle.singleBody() || colors.celestial() != 0xf00000 || colors.sky() != -1
|
||||
|| cycle.sunSize() != 2 || cycle.bearing() != 90)
|
||||
throw new IllegalStateException("Single-body fixture failed");
|
||||
for (String kind : java.util.List.of("sun", "moon")) {
|
||||
var target = symbol.apply("celestial/" + kind);
|
||||
int mode = kind.equals("sun") ? PortDayCycle.SUN : PortDayCycle.MOON;
|
||||
for (var entry : java.util.Map.of("rising", 0, "zenith", 6000, "setting", 12000, "nadir", 18000).entrySet()) {
|
||||
var selected = PortDayCycle.compile(java.util.List.of(symbol.apply("period/zero"),
|
||||
symbol.apply("phase/" + entry.getKey()), target), seed, origin);
|
||||
float expectedLight = entry.getKey().equals("zenith") ? (mode == PortDayCycle.MOON ? 0.18F : 1F) : 0F;
|
||||
if (selected.bodyMode() != mode || selected.ticks(origin) != entry.getValue()
|
||||
|| Math.abs(selected.bodyLight(origin) - expectedLight) > 0.0001F)
|
||||
throw new IllegalStateException("Single-body phase/light invariant failed");
|
||||
}
|
||||
var moving = PortDayCycle.compile(java.util.stream.Stream.of("period/half", "phase/zenith",
|
||||
"direction/counterclockwise", "number/12", "size/large", target.getPath()).map(symbol).toList(), seed, origin);
|
||||
if (moving.sunSize() != 1.625F || moving.ticks(origin + 100) != 5800
|
||||
|| Math.abs(moving.bodyAngle(origin + 100) + 3F) > 0.0001F
|
||||
|| !PortDayCycle.validWriting(java.util.List.of(target, PortDayCycle.NORMAL_TIME))
|
||||
|| !PortDayCycle.validWriting(java.util.List.of(target, target))
|
||||
|| PortDayCycle.validWriting(java.util.List.of(target, symbol.apply("size/large"))))
|
||||
throw new IllegalStateException("Single-body modifier/order invariant failed");
|
||||
}
|
||||
var older = PortDayCycle.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE,
|
||||
com.google.gson.JsonParser.parseString("{\"authored\":true,\"period\":1,\"stopped_time\":0,\"sun_size\":2}")).getOrThrow();
|
||||
if (older.bodyMode() != PortDayCycle.PAIRED || older.singleBody())
|
||||
throw new IllegalStateException("Old paired body default failed");
|
||||
var environment = PortEnvironmentSelection.resolve(pages, seed, PortAgeTerrain.FLAT);
|
||||
if (environment.sky() != PortSkyStyle.OVERWORLD)
|
||||
throw new IllegalStateException("Single body selected an incompatible sky");
|
||||
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
|
||||
RuntimeAgeBackend.create(server, ageId(context), seed, biomes, PortAgeTerrain.FLAT,
|
||||
null, null, 0, null, null, environment.weather(), environment.lighting(), environment.sky(), colors, cycle);
|
||||
return probeColors(context) == 1 ? probeDayCycle(context) : 0;
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int createCelestialColorAge(CommandContext<CommandSourceStack> context) {
|
||||
try {
|
||||
java.util.function.Function<String, Identifier> symbol = path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path);
|
||||
Identifier phase = symbol.apply("phase/" + StringArgumentType.getString(context, "phase"));
|
||||
Identifier color = symbol.apply("color/" + StringArgumentType.getString(context, "color"));
|
||||
if (!PortDayCycle.isPhase(phase) || !PortAgeColors.isColor(color))
|
||||
throw new IllegalArgumentException("Use rising/zenith/setting/nadir and red/green/blue/white/black");
|
||||
var pages = java.util.List.of(symbol.apply("number/10"), symbol.apply("color/blue"),
|
||||
symbol.apply("sky/overworld"), symbol.apply("number/20"), symbol.apply("color/white"),
|
||||
PortAgeColors.AMBIENT, symbol.apply("period/zero"), phase,
|
||||
symbol.apply("number/" + IntegerArgumentType.getInteger(context, "number")), color,
|
||||
PortDayCycle.NORMAL_TIME, symbol.apply("weather/off"), symbol.apply("lighting/normal"),
|
||||
symbol.apply("terrain/flat"));
|
||||
long seed = LongArgumentType.getLong(context, "seed");
|
||||
var colors = PortAgeColors.compile(pages, seed);
|
||||
int amount = IntegerArgumentType.getInteger(context, "number") * 10;
|
||||
int expected = switch (color.getPath()) {
|
||||
case "color/red" -> amount << 16;
|
||||
case "color/green" -> amount << 8;
|
||||
case "color/blue" -> amount;
|
||||
case "color/white" -> amount * 0x010101;
|
||||
default -> 0;
|
||||
};
|
||||
if (!net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(pages)
|
||||
|| !colors.equals(new PortAgeColors(0x000064, 0xc8c8c8, expected)))
|
||||
throw new IllegalStateException("Celestial color fixture failed");
|
||||
// Color groups persist across intervening Length/Phase/Size modifiers, then
|
||||
// are consumed by Normal Time rather than leaking into a following target.
|
||||
var mixed = PortAgeColors.compile(java.util.stream.Stream.of("number/24", "color/red",
|
||||
"number/24", "color/red", "number/24", "color/black", "size/large",
|
||||
"celestial/normal_time", "sky/overworld", "lighting/ambient_color")
|
||||
.map(symbol).toList(), seed);
|
||||
var trailing = PortAgeColors.compile(java.util.stream.Stream.of("number/10", "color/red",
|
||||
"celestial/normal_time", "number/5", "color/green").map(symbol).toList(), seed);
|
||||
var unnumbered = java.util.List.of(symbol.apply("color/blue"), PortDayCycle.NORMAL_TIME);
|
||||
var older = PortAgeColors.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE,
|
||||
com.google.gson.JsonParser.parseString("{\"sky\":100,\"ambient\":-1}")).getOrThrow();
|
||||
if (!mixed.equals(new PortAgeColors(-1, -1, 0xf00000))
|
||||
|| !trailing.equals(new PortAgeColors(0x003200, -1, 0x640000))
|
||||
|| !older.equals(new PortAgeColors(100, -1))
|
||||
|| !PortAgeColors.compile(unnumbered, seed).equals(PortAgeColors.compile(unnumbered, seed))
|
||||
|| PortAgeColors.compile(unnumbered, seed).sky() != -1)
|
||||
throw new IllegalStateException("Celestial color grouping/default invariant failed");
|
||||
var server = context.getSource().getServer();
|
||||
var cycle = PortDayCycle.compile(pages, seed, PortDayCycle.nativeTicks(server.overworld()));
|
||||
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
|
||||
var environment = PortEnvironmentSelection.resolve(pages, seed, PortAgeTerrain.FLAT);
|
||||
RuntimeAgeBackend.create(server, ageId(context), seed, biomes, PortAgeTerrain.FLAT,
|
||||
null, null, 0, null, null, environment.weather(), environment.lighting(), environment.sky(), colors, cycle);
|
||||
return probeColors(context) == 1 ? probeDayCycle(context) : 0;
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int createColorAge(CommandContext<CommandSourceStack> context) {
|
||||
try {
|
||||
String sky = StringArgumentType.getString(context, "sky").toLowerCase(Locale.ROOT);
|
||||
@@ -641,7 +1378,8 @@ public final class PortAgeCommands {
|
||||
var sky = level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.SKY_COLOR, pos);
|
||||
var fog = level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.FOG_COLOR, pos);
|
||||
var ambient = level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.AMBIENT_LIGHT_COLOR, pos);
|
||||
if (colors.sky() >= 0 && (!sky.equals(PortAgeColors.vector(colors.sky())) || !fog.equals(sky))
|
||||
var expectedFog = colors.sky() < 0 ? fog : new org.joml.Vector3f(PortAgeColors.vector(colors.sky())).mul(PortSkyStyle.fogShade(level));
|
||||
if (colors.sky() >= 0 && (!sky.equals(PortAgeColors.vector(colors.sky())) || !fog.equals(expectedFog))
|
||||
|| colors.ambient() >= 0 && !ambient.equals(PortAgeColors.vector(colors.ambient()))) {
|
||||
context.getSource().sendFailure(Component.literal("Color attribute mismatch"));
|
||||
return 0;
|
||||
@@ -651,6 +1389,29 @@ public final class PortAgeCommands {
|
||||
return 1;
|
||||
}
|
||||
|
||||
private static void verifyRandomSkyColors() {
|
||||
long[] seeds = {0, 1, 2, 14, 86, 87};
|
||||
int[] expected = {0x8abac0, 0x8cb39c, 0x6e3841, 0xcda38c, 0xb7a6cd, 0x406859};
|
||||
for (int i = 0; i < seeds.length; i++) {
|
||||
var colors = PortAgeColors.resolve(java.util.List.of(), seeds[i]);
|
||||
var encoded = PortAgeColors.CODEC.encodeStart(com.mojang.serialization.JsonOps.INSTANCE, colors).getOrThrow();
|
||||
if (colors.sky() != expected[i] || colors.ambient() != -1 || colors.celestial() != -1
|
||||
|| !colors.equals(PortAgeColors.resolve(java.util.List.of(), seeds[i]))
|
||||
|| !colors.equals(PortAgeColors.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE, encoded).getOrThrow()))
|
||||
throw new IllegalStateException("Random sky root-vector/persistence mismatch");
|
||||
}
|
||||
for (var paths : java.util.List.of(java.util.List.of("sky/end"), java.util.List.of("celestial/sun"),
|
||||
java.util.List.of("terrain/flat"), java.util.List.of("number/12", "color/red", "sky/overworld"),
|
||||
java.util.List.of("number/0", "color/black"))) {
|
||||
var pages = paths.stream().map(path -> Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, path)).toList();
|
||||
if (!PortAgeColors.resolve(pages, 87).equals(PortAgeColors.compile(pages, 87)))
|
||||
throw new IllegalStateException("Authored sky colors were randomized");
|
||||
}
|
||||
var older = PortAgeColors.CODEC.parse(com.mojang.serialization.JsonOps.INSTANCE,
|
||||
com.google.gson.JsonParser.parseString("{\"sky\":-1,\"ambient\":-1}")).getOrThrow();
|
||||
if (!older.equals(PortAgeColors.NATIVE)) throw new IllegalStateException("Old sky-color defaults changed");
|
||||
}
|
||||
|
||||
private static void verifyCatalogRoundTrip(net.minecraft.server.MinecraftServer server) {
|
||||
var payload = new AgeCatalogPayload(1, server.levelKeys().stream().map(key -> key.identifier()).toList(),
|
||||
RuntimeAgeBackend.savedColors(server), RuntimeAgeBackend.savedDayCycles(server));
|
||||
@@ -925,7 +1686,41 @@ public final class PortAgeCommands {
|
||||
java.util.Collections.nCopies(8, page));
|
||||
}
|
||||
|
||||
private static int probeDungeons(CommandContext<CommandSourceStack> context) {
|
||||
private static int createDungeonTerrain(CommandContext<CommandSourceStack> context) {
|
||||
try {
|
||||
var terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
|
||||
var terrainPage = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID,
|
||||
"terrain/" + terrain.name().toLowerCase(Locale.ROOT));
|
||||
var dungeon = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/dungeons");
|
||||
var cave = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/cave");
|
||||
var pages = new java.util.ArrayList<Identifier>(java.util.Collections.nCopies(8, dungeon));
|
||||
pages.addFirst(terrainPage);
|
||||
var maximum = new java.util.ArrayList<Identifier>(java.util.Collections.nCopies(32, dungeon));
|
||||
maximum.addFirst(cave);
|
||||
if (net.dnorberg.rivencraft.writing.PortAuthoredWriting.dungeonCount(maximum) != 32)
|
||||
throw new IllegalStateException("Cave Dungeons stacking failed");
|
||||
maximum.add(dungeon);
|
||||
if (net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(maximum)
|
||||
|| net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(java.util.List.of(cave,
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/surface_lakes"))))
|
||||
throw new IllegalStateException("Dungeon cap/lake restriction changed");
|
||||
for (String unsupported : java.util.List.of("flat", "void"))
|
||||
if (net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(java.util.List.of(dungeon,
|
||||
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + unsupported))))
|
||||
throw new IllegalStateException("Dungeons unexpectedly accepted unsupported terrain");
|
||||
var server = context.getSource().getServer();
|
||||
int count = net.dnorberg.rivencraft.writing.PortAuthoredWriting.dungeonCount(pages);
|
||||
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server,
|
||||
LongArgumentType.getLong(context, "seed"));
|
||||
int created = createAge(context, terrain, biomes, null, null, count);
|
||||
return created == 0 ? 0 : probeFeaturePlan(context);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static int probeDungeons(CommandContext<CommandSourceStack> context, int chunks) {
|
||||
Identifier id = ageId(context);
|
||||
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
|
||||
if (level == null || !(level.getChunkSource().getGenerator() instanceof PortFeatureChunkGenerator features)) {
|
||||
@@ -934,7 +1729,7 @@ public final class PortAgeCommands {
|
||||
}
|
||||
int count = features.dungeons();
|
||||
int spawners = 0;
|
||||
for (int chunkX = 0; chunkX < 4; chunkX++) for (int chunkZ = 0; chunkZ < 4; chunkZ++) {
|
||||
for (int chunkX = 0; chunkX < chunks; chunkX++) for (int chunkZ = 0; chunkZ < chunks; chunkZ++) {
|
||||
var chunk = level.getChunk(chunkX, chunkZ);
|
||||
for (var pos : chunk.getBlockEntitiesPos()) {
|
||||
if (chunk.getBlockState(pos).is(Blocks.SPAWNER)) spawners++;
|
||||
@@ -942,7 +1737,7 @@ public final class PortAgeCommands {
|
||||
}
|
||||
int placed = spawners;
|
||||
context.getSource().sendSuccess(() -> Component.literal(
|
||||
"Dungeons probe " + id + " pages=" + count + " spawners=" + placed), false);
|
||||
"Dungeons probe " + id + " pages=" + count + " chunks=" + chunks + "x" + chunks + " spawners=" + placed), false);
|
||||
return placed;
|
||||
}
|
||||
|
||||
@@ -1092,6 +1887,24 @@ public final class PortAgeCommands {
|
||||
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
|
||||
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures,
|
||||
PortEnvironmentSelection environment) {
|
||||
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures,
|
||||
environment, PortDayCycle.NATIVE);
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||
PortAgeTerrain terrain, PortBiomePlan biomePlan,
|
||||
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
|
||||
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures,
|
||||
PortEnvironmentSelection environment, PortDayCycle cycle) {
|
||||
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures,
|
||||
environment, cycle, PortAgeColors.NATIVE);
|
||||
}
|
||||
|
||||
private static int createAge(CommandContext<CommandSourceStack> context,
|
||||
PortAgeTerrain terrain, PortBiomePlan biomePlan,
|
||||
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
|
||||
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures,
|
||||
PortEnvironmentSelection environment, PortDayCycle cycle, PortAgeColors colors) {
|
||||
String name = StringArgumentType.getString(context, "name");
|
||||
Identifier id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
||||
long seed = LongArgumentType.getLong(context, "seed");
|
||||
@@ -1099,7 +1912,7 @@ public final class PortAgeCommands {
|
||||
try {
|
||||
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan,
|
||||
terrain, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures,
|
||||
environment.weather(), environment.lighting(), environment.sky());
|
||||
environment.weather(), environment.lighting(), environment.sky(), colors, cycle);
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
context.getSource().sendFailure(Component.literal(exception.getMessage()));
|
||||
return 0;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.core.HolderLookup;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.data.worldgen.material.EndMaterialRules;
|
||||
import net.minecraft.data.worldgen.material.NetherMaterialRules;
|
||||
import net.minecraft.world.level.biome.Biomes;
|
||||
import net.minecraft.world.level.levelgen.NoiseGeneratorSettings;
|
||||
import net.minecraft.world.level.levelgen.material.MaterialRules;
|
||||
|
||||
/** Root AgeDimensionService.withBiomeMaterialRules adapted to 26.3's rule registry. */
|
||||
final class PortBiomeMaterialRules {
|
||||
private PortBiomeMaterialRules() {}
|
||||
|
||||
static Holder<NoiseGeneratorSettings> apply(HolderLookup.Provider registries, NoiseGeneratorSettings base) {
|
||||
var biomes = registries.lookupOrThrow(Registries.BIOME);
|
||||
var rules = registries.lookupOrThrow(Registries.MATERIAL_RULE);
|
||||
var materials = MaterialRules.sequence(
|
||||
MaterialRules.ifTrue(MaterialRules.isBiome(biomes, Biomes.THE_END, Biomes.END_HIGHLANDS,
|
||||
Biomes.END_MIDLANDS, Biomes.SMALL_END_ISLANDS, Biomes.END_BARRENS),
|
||||
MaterialRules.getRule(rules, EndMaterialRules.END)),
|
||||
MaterialRules.ifTrue(MaterialRules.isBiome(biomes, Biomes.NETHER_WASTES, Biomes.SOUL_SAND_VALLEY,
|
||||
Biomes.CRIMSON_FOREST, Biomes.WARPED_FOREST, Biomes.BASALT_DELTAS),
|
||||
MaterialRules.getRule(rules, NetherMaterialRules.NETHER)),
|
||||
base.materialRule().value());
|
||||
return Holder.direct(new NoiseGeneratorSettings(base.noiseSettings(), base.defaultBlock(), base.defaultFluid(),
|
||||
base.noiseRouter(), Holder.direct(materials), base.spawnTarget(), base.seaLevel(), base.disableMobGeneration(),
|
||||
base.aquifers(), base.useLegacyRandomSource(), base.debugFunctions()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import com.mojang.serialization.Codec;
|
||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
||||
import net.minecraft.network.RegistryFriendlyByteBuf;
|
||||
import net.minecraft.network.codec.ByteBufCodecs;
|
||||
import net.minecraft.network.codec.StreamCodec;
|
||||
|
||||
/** One frozen authored orbit, using the same angular motion as 1.21.1 Body. */
|
||||
public record PortCelestialBody(int kind, double period, int stoppedTime, long originTick,
|
||||
int phaseTick, boolean clockwise, int bearing, float size, int color) {
|
||||
public static final Codec<PortCelestialBody> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
Codec.intRange(1, 2).fieldOf("kind").forGetter(PortCelestialBody::kind),
|
||||
Codec.doubleRange(0, 2).fieldOf("period").forGetter(PortCelestialBody::period),
|
||||
Codec.intRange(0, 23999).fieldOf("stopped_time").forGetter(PortCelestialBody::stoppedTime),
|
||||
Codec.LONG.fieldOf("origin_tick").forGetter(PortCelestialBody::originTick),
|
||||
Codec.intRange(0, 23999).fieldOf("phase_tick").forGetter(PortCelestialBody::phaseTick),
|
||||
Codec.BOOL.fieldOf("clockwise").forGetter(PortCelestialBody::clockwise),
|
||||
Codec.intRange(-1, 359).fieldOf("bearing").forGetter(PortCelestialBody::bearing),
|
||||
Codec.floatRange(0.25F, 3F).fieldOf("size").forGetter(PortCelestialBody::size),
|
||||
Codec.intRange(-1, 0xffffff).fieldOf("color").forGetter(PortCelestialBody::color)
|
||||
).apply(instance, PortCelestialBody::new));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, PortCelestialBody> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.VAR_INT, PortCelestialBody::kind, ByteBufCodecs.DOUBLE, PortCelestialBody::period,
|
||||
ByteBufCodecs.VAR_INT, PortCelestialBody::stoppedTime, ByteBufCodecs.VAR_LONG, PortCelestialBody::originTick,
|
||||
ByteBufCodecs.VAR_INT, PortCelestialBody::phaseTick, ByteBufCodecs.BOOL, PortCelestialBody::clockwise,
|
||||
ByteBufCodecs.INT, PortCelestialBody::bearing, ByteBufCodecs.FLOAT, PortCelestialBody::size,
|
||||
ByteBufCodecs.INT, PortCelestialBody::color, PortCelestialBody::new);
|
||||
|
||||
public PortCelestialBody {
|
||||
if (kind != PortDayCycle.SUN && kind != PortDayCycle.MOON || !Double.isFinite(period)
|
||||
|| period < 0 || period > 2 || stoppedTime < 0 || stoppedTime >= 24000
|
||||
|| phaseTick < 0 || phaseTick >= 24000 || bearing < -1 || bearing > 359
|
||||
|| !Float.isFinite(size) || size < 0.25F || size > 3 || color < -1 || color > 0xffffff)
|
||||
throw new IllegalArgumentException("Invalid celestial body");
|
||||
}
|
||||
|
||||
public static PortCelestialBody of(PortDayCycle cycle, int kind, int color, boolean pairedMoon) {
|
||||
int offset = pairedMoon ? 12000 : 0;
|
||||
return new PortCelestialBody(kind, cycle.period(), (cycle.stoppedTime() + offset) % 24000,
|
||||
cycle.originTick(), (cycle.phaseTick() + offset) % 24000, cycle.clockwise(),
|
||||
cycle.bearing(), pairedMoon ? 1F : cycle.sunSize(), color);
|
||||
}
|
||||
|
||||
public float angle(double nativeTicks) {
|
||||
double time = period == 0 ? stoppedTime
|
||||
: (clockwise ? 1 : -1) * (nativeTicks - originTick) / period + phaseTick;
|
||||
return (float) (((time - 6000D) % 24000D) * (360D / 24000D));
|
||||
}
|
||||
|
||||
public float light(long nativeTicks) {
|
||||
return Math.max(0F, (float) Math.cos(Math.toRadians(angle(nativeTicks))))
|
||||
* (kind == PortDayCycle.MOON ? 0.18F : 1F);
|
||||
}
|
||||
|
||||
public net.minecraft.world.phys.Vec3 direction(double ticks) {
|
||||
double orbit = Math.toRadians(angle(ticks)), axis = Math.toRadians(bearing < 0 ? -90 : bearing);
|
||||
double lateral = Math.sin(orbit);
|
||||
return new net.minecraft.world.phys.Vec3(Math.sin(axis) * lateral, Math.cos(orbit), Math.cos(axis) * lateral);
|
||||
}
|
||||
|
||||
/** Root AgeSkyProfile.eclipseFor: contact uses the central quarter-size disc, not its halo. */
|
||||
public float eclipse(java.util.List<PortCelestialBody> bodies, double ticks) {
|
||||
float strongest = 0;
|
||||
var direction = direction(ticks);
|
||||
double radius = Math.atan((kind == PortDayCycle.SUN ? 30D : 20D) * size * 0.25D / 100D);
|
||||
for (var other : bodies) {
|
||||
if (other.kind == kind) continue;
|
||||
double separation = Math.acos(Math.max(-1D, Math.min(1D, direction.dot(other.direction(ticks)))));
|
||||
double contact = radius + Math.atan((other.kind == PortDayCycle.SUN ? 30D : 20D) * other.size * 0.25D / 100D);
|
||||
strongest = Math.max(strongest, (float) Math.max(0D, 1D - separation / contact));
|
||||
}
|
||||
return strongest;
|
||||
}
|
||||
|
||||
public static float eclipseAt(java.util.List<PortCelestialBody> bodies, double ticks) {
|
||||
float strongest = 0;
|
||||
for (var body : bodies) if (body.kind == PortDayCycle.SUN && body.direction(ticks).y > 0D)
|
||||
strongest = Math.max(strongest, body.eclipse(bodies, ticks));
|
||||
return strongest;
|
||||
}
|
||||
|
||||
/** Shared client/server-checkable RGB rule; alpha is handled by the renderer. */
|
||||
public int eclipseColor(float eclipse) {
|
||||
int rgb = color < 0 ? 0xffffff : color;
|
||||
float corona = kind == PortDayCycle.SUN ? (float) Math.pow(eclipse, 8D) * 0.8F : 0F;
|
||||
int red = channel(rgb >> 16 & 255, eclipse, corona);
|
||||
int green = channel(rgb >> 8 & 255, eclipse, corona);
|
||||
int blue = channel(rgb & 255, eclipse, corona);
|
||||
return red << 16 | green << 8 | blue;
|
||||
}
|
||||
|
||||
private int channel(int value, float eclipse, float corona) {
|
||||
return Math.round(kind == PortDayCycle.MOON ? value * (1F - eclipse) : value + (255 - value) * corona);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,9 @@ import net.minecraft.world.level.Level;
|
||||
|
||||
/** Authored native celestial controls. Never mutates the shared clock. */
|
||||
public record PortDayCycle(boolean authored, double period, int stoppedTime,
|
||||
long originTick, int phaseTick, boolean clockwise, int bearing, float sunSize) {
|
||||
long originTick, int phaseTick, boolean clockwise, int bearing, float sunSize, int bodyMode,
|
||||
List<PortCelestialBody> bodies) {
|
||||
public static final int PAIRED = 0, SUN = 1, MOON = 2, NONE = 3;
|
||||
public static final PortDayCycle NATIVE = new PortDayCycle(false, 1, 0);
|
||||
public static final Identifier NORMAL_TIME = Identifier.fromNamespaceAndPath("rivencraft", "celestial/normal_time");
|
||||
public static final Codec<PortDayCycle> CODEC = RecordCodecBuilder.create(instance -> instance.group(
|
||||
@@ -30,20 +32,40 @@ public record PortDayCycle(boolean authored, double period, int stoppedTime,
|
||||
Codec.intRange(0, 23999).optionalFieldOf("phase_tick", 0).forGetter(PortDayCycle::phaseTick),
|
||||
Codec.BOOL.optionalFieldOf("clockwise", true).forGetter(PortDayCycle::clockwise),
|
||||
Codec.intRange(-1, 359).optionalFieldOf("bearing", -1).forGetter(PortDayCycle::bearing),
|
||||
Codec.floatRange(0.25F, 3F).optionalFieldOf("sun_size", 1F).forGetter(PortDayCycle::sunSize)
|
||||
Codec.floatRange(0.25F, 3F).optionalFieldOf("sun_size", 1F).forGetter(PortDayCycle::sunSize),
|
||||
Codec.intRange(0, 3).optionalFieldOf("body_mode", PAIRED).forGetter(PortDayCycle::bodyMode),
|
||||
PortCelestialBody.CODEC.listOf().optionalFieldOf("bodies", List.of()).forGetter(PortDayCycle::bodies)
|
||||
).apply(instance, PortDayCycle::new));
|
||||
public static final StreamCodec<RegistryFriendlyByteBuf, PortDayCycle> STREAM_CODEC = StreamCodec.composite(
|
||||
ByteBufCodecs.BOOL, PortDayCycle::authored, ByteBufCodecs.DOUBLE, PortDayCycle::period,
|
||||
ByteBufCodecs.VAR_INT, PortDayCycle::stoppedTime, ByteBufCodecs.VAR_LONG, PortDayCycle::originTick,
|
||||
ByteBufCodecs.VAR_INT, PortDayCycle::phaseTick, ByteBufCodecs.BOOL, PortDayCycle::clockwise,
|
||||
ByteBufCodecs.INT, PortDayCycle::bearing, ByteBufCodecs.FLOAT, PortDayCycle::sunSize, PortDayCycle::new);
|
||||
ByteBufCodecs.INT, PortDayCycle::bearing, ByteBufCodecs.FLOAT, PortDayCycle::sunSize,
|
||||
ByteBufCodecs.VAR_INT, PortDayCycle::bodyMode,
|
||||
PortCelestialBody.STREAM_CODEC.apply(ByteBufCodecs.list()), PortDayCycle::bodies, PortDayCycle::new);
|
||||
private static volatile Map<Identifier, PortDayCycle> clientCycles = Map.of();
|
||||
|
||||
public PortDayCycle {
|
||||
bodies = List.copyOf(bodies);
|
||||
if (!Double.isFinite(period) || period < 0 || period > 2 || stoppedTime < 0 || stoppedTime >= 24000
|
||||
|| phaseTick < 0 || phaseTick >= 24000 || bearing < -1 || bearing >= 360
|
||||
|| !Float.isFinite(sunSize) || sunSize < 0.25F || sunSize > 3F)
|
||||
|| !Float.isFinite(sunSize) || sunSize < 0.25F || sunSize > 3F
|
||||
|| bodyMode < PAIRED || bodyMode > NONE
|
||||
|| bodyMode == NONE && (!authored || !bodies.isEmpty()))
|
||||
throw new IllegalArgumentException("Invalid day cycle");
|
||||
if (bodies.stream().filter(body -> body.kind() == SUN).count() > 10
|
||||
|| bodies.stream().filter(body -> body.kind() == MOON).count() > 10)
|
||||
throw new IllegalArgumentException("An Age may have at most ten suns and ten moons");
|
||||
}
|
||||
|
||||
public PortDayCycle(boolean authored, double period, int stoppedTime,
|
||||
long originTick, int phaseTick, boolean clockwise, int bearing, float sunSize, int bodyMode) {
|
||||
this(authored, period, stoppedTime, originTick, phaseTick, clockwise, bearing, sunSize, bodyMode, List.of());
|
||||
}
|
||||
|
||||
public PortDayCycle(boolean authored, double period, int stoppedTime,
|
||||
long originTick, int phaseTick, boolean clockwise, int bearing, float sunSize) {
|
||||
this(authored, period, stoppedTime, originTick, phaseTick, clockwise, bearing, sunSize, PAIRED);
|
||||
}
|
||||
|
||||
public PortDayCycle(boolean authored, double period, int stoppedTime) {
|
||||
@@ -87,7 +109,14 @@ public record PortDayCycle(boolean authored, double period, int stoppedTime,
|
||||
}
|
||||
|
||||
public static boolean supports(Identifier page) {
|
||||
return page.equals(NORMAL_TIME) || isPeriod(page) || isPhase(page) || isDirection(page) || isAngle(page) || isSize(page);
|
||||
return isController(page) || isPeriod(page) || isPhase(page) || isDirection(page) || isAngle(page) || isSize(page);
|
||||
}
|
||||
|
||||
public static boolean isController(Identifier page) {
|
||||
return page.getNamespace().equals("rivencraft") && switch (page.getPath()) {
|
||||
case "celestial/normal_time", "celestial/sun", "celestial/moon" -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean isSize(Identifier page) {
|
||||
@@ -98,25 +127,74 @@ public record PortDayCycle(boolean authored, double period, int stoppedTime,
|
||||
}
|
||||
|
||||
public static boolean validWriting(List<Identifier> pages) {
|
||||
int target = pages.indexOf(NORMAL_TIME), periods = 0, phases = 0, directions = 0, angles = 0, sizes = 0;
|
||||
if (target != pages.lastIndexOf(NORMAL_TIME)) return false;
|
||||
for (int i = 0; i < pages.size(); i++) {
|
||||
if (isPeriod(pages.get(i)) && (++periods > 1 || i > target)) return false;
|
||||
if (isPhase(pages.get(i)) && (++phases > 1 || i > target)) return false;
|
||||
if (isDirection(pages.get(i)) && (++directions > 1 || i > target)) return false;
|
||||
if (isAngle(pages.get(i)) && (++angles > 1 || i > target)) return false;
|
||||
if (isSize(pages.get(i)) && (++sizes > 1 || i > target)) return false;
|
||||
int periods = 0, phases = 0, directions = 0, angles = 0, sizes = 0, suns = 0, moons = 0;
|
||||
for (Identifier page : pages) {
|
||||
if (isPeriod(page) && ++periods > 1 || isPhase(page) && ++phases > 1
|
||||
|| isDirection(page) && ++directions > 1 || isAngle(page) && ++angles > 1
|
||||
|| isSize(page) && ++sizes > 1) return false;
|
||||
if (isController(page)) {
|
||||
if (!page.getPath().equals("celestial/moon")) suns++;
|
||||
if (!page.getPath().equals("celestial/sun")) moons++;
|
||||
if (suns > 10 || moons > 10) return false;
|
||||
periods = phases = directions = angles = sizes = 0;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return periods + phases + directions + angles + sizes == 0;
|
||||
}
|
||||
|
||||
public static PortDayCycle compile(List<Identifier> pages, long seed) {
|
||||
return compile(pages, seed, 0);
|
||||
}
|
||||
|
||||
/** Creation-only completion. Exact diagnostic fixtures can still use compile. */
|
||||
public static PortDayCycle resolve(List<Identifier> pages, long seed, long nativeTicks) {
|
||||
if (!validWriting(pages) || !PortAgeColors.validNumbers(pages))
|
||||
throw new IllegalArgumentException("Invalid celestial modifier order");
|
||||
if (pages.stream().anyMatch(PortDayCycle::isController)) return compile(pages, seed, nativeTicks);
|
||||
// Match root AgeSkyProfile.random's count draw positions: sky style,
|
||||
// mixed sky color, then independent 0..3 sun and moon counts.
|
||||
var random = new Random(seed ^ 0x534B5950524F464CL);
|
||||
random.nextInt(5);
|
||||
randomColor(random);
|
||||
int suns = random.nextInt(4), moons = random.nextInt(4);
|
||||
var bodies = new java.util.ArrayList<PortCelestialBody>();
|
||||
for (int i = 0; i < suns + moons; i++)
|
||||
bodies.addAll(randomUnmodified(seed, i, nativeTicks, i < suns ? SUN : MOON).bodies);
|
||||
if (bodies.isEmpty()) return new PortDayCycle(true, 1, 0, 0, 0, true, -1, 1F, NONE);
|
||||
var first = bodies.getFirst();
|
||||
return new PortDayCycle(true, first.period(), first.stoppedTime(), first.originTick(), first.phaseTick(),
|
||||
first.clockwise(), first.bearing(), first.size(), first.kind(), bodies);
|
||||
}
|
||||
|
||||
public static PortDayCycle compile(List<Identifier> pages, long seed, long nativeTicks) {
|
||||
if (!validWriting(pages)) throw new IllegalArgumentException("Place at most one Length, Phase, Direction, Angle, and Size before Normal Time");
|
||||
if (!pages.contains(NORMAL_TIME)) return NATIVE;
|
||||
if (!validWriting(pages)) throw new IllegalArgumentException("Place at most one of each celestial modifier before its controller; limit ten suns and ten moons");
|
||||
if (pages.stream().filter(PortDayCycle::isController).count() <= 1)
|
||||
return compileOne(pages, seed, nativeTicks, 0);
|
||||
var bodies = new java.util.ArrayList<PortCelestialBody>();
|
||||
PortDayCycle first = null;
|
||||
int start = 0, ordinal = 0;
|
||||
for (int i = 0; i < pages.size(); i++) if (isController(pages.get(i))) {
|
||||
var cycle = compileOne(pages.subList(start, i + 1), seed, nativeTicks, ordinal++);
|
||||
if (first == null) first = cycle;
|
||||
// Prefix retains original page indices for seeded unnumbered colors.
|
||||
int color = PortAgeColors.compile(pages.subList(0, i + 1), seed).celestial();
|
||||
if (!cycle.bodies.isEmpty()) bodies.addAll(cycle.bodies);
|
||||
else {
|
||||
if (cycle.bodyMode != MOON) bodies.add(PortCelestialBody.of(cycle, SUN, color, false));
|
||||
if (cycle.bodyMode != SUN) bodies.add(PortCelestialBody.of(cycle, MOON, color, cycle.bodyMode == PAIRED));
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
return new PortDayCycle(first.authored, first.period, first.stoppedTime, first.originTick,
|
||||
first.phaseTick, first.clockwise, first.bearing, first.sunSize, first.bodyMode, bodies);
|
||||
}
|
||||
|
||||
private static PortDayCycle compileOne(List<Identifier> pages, long seed, long nativeTicks, int ordinal) {
|
||||
var controller = pages.stream().filter(PortDayCycle::isController).findFirst().orElse(null);
|
||||
if (controller == null) return NATIVE;
|
||||
int mode = controller.getPath().equals("celestial/sun") ? SUN
|
||||
: controller.getPath().equals("celestial/moon") ? MOON : PAIRED;
|
||||
if (mode != PAIRED && !hasBodyModifiers(pages)) return randomUnmodified(seed, ordinal, nativeTicks, mode);
|
||||
double period = 1;
|
||||
Integer phase = null;
|
||||
boolean clockwise = true;
|
||||
@@ -160,7 +238,59 @@ public record PortDayCycle(boolean authored, double period, int stoppedTime,
|
||||
int stopped = period == 0 ? phase != null ? phase
|
||||
: new Random(seed ^ 0x4441594359434C45L).nextInt(24000) : 0;
|
||||
return new PortDayCycle(true, period, stopped, phase != null ? nativeTicks : 0,
|
||||
phase == null ? 0 : phase, clockwise, bearing, size);
|
||||
phase == null ? 0 : phase, clockwise, bearing, size, mode);
|
||||
}
|
||||
|
||||
private static boolean hasBodyModifiers(List<Identifier> pages) {
|
||||
boolean color = false, controls = false;
|
||||
for (var page : pages) {
|
||||
if (isController(page)) return controls || color;
|
||||
if (PortAgeColors.isColor(page)) color = true;
|
||||
else if (PortSkyStyle.forPage(page) != null || page.equals(PortAgeColors.AMBIENT)) color = false;
|
||||
if (isPeriod(page) || isPhase(page) || isDirection(page) || isAngle(page) || isSize(page)) controls = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static PortDayCycle randomUnmodified(long seed, int ordinal, long nativeTicks, int mode) {
|
||||
// Independent per-controller stream: unrelated terrain/sky/weather pages
|
||||
// cannot reshuffle the bodies. Use root random-Age bounds, not native defaults.
|
||||
var random = new Random(seed ^ 0x43454C4553544941L ^ (ordinal + 1L) * 0x9E3779B97F4A7C15L);
|
||||
double period = random.nextInt(7) == 0 ? 0D : 0.2D + random.nextDouble() * 1.8D;
|
||||
int phase = random.nextInt(24000), bearing = random.nextInt(360);
|
||||
float size = 0.75F + random.nextFloat();
|
||||
boolean clockwise = random.nextBoolean();
|
||||
int color = randomColor(random);
|
||||
var cycle = new PortDayCycle(true, period, period == 0 ? phase : 0, nativeTicks,
|
||||
phase, clockwise, bearing, size, mode);
|
||||
return new PortDayCycle(true, period, cycle.stoppedTime, nativeTicks, phase, clockwise, bearing, size,
|
||||
mode, List.of(PortCelestialBody.of(cycle, mode, color, false)));
|
||||
}
|
||||
|
||||
private static int randomColor(Random random) {
|
||||
return PortAgeColors.randomColor(random, false);
|
||||
}
|
||||
|
||||
public boolean singleBody() { return authored && (bodyMode == SUN || bodyMode == MOON); }
|
||||
public boolean customLight() { return singleBody() || !bodies.isEmpty(); }
|
||||
|
||||
/** Linear authored orbit, as in 1.21.1 Body; zero degrees is Zenith. */
|
||||
public float bodyAngle(long nativeTicks) {
|
||||
return (float) (((time(nativeTicks) - 6000D) % 24000D) * (360D / 24000D));
|
||||
}
|
||||
|
||||
public float bodyLight(long nativeTicks) {
|
||||
if (bodyMode == NONE) return 0;
|
||||
if (!bodies.isEmpty()) {
|
||||
float sun = 0, moon = 0;
|
||||
for (var body : bodies) {
|
||||
if (body.kind() == SUN) sun += body.light(nativeTicks) * (1F - 0.85F * body.eclipse(bodies, nativeTicks));
|
||||
else moon += body.light(nativeTicks);
|
||||
}
|
||||
return Math.min(1F, sun > 0 ? sun : moon);
|
||||
}
|
||||
float altitude = Math.max(0F, (float) Math.cos(Math.toRadians(bodyAngle(nativeTicks))));
|
||||
return altitude * (bodyMode == MOON ? 0.18F : 1F);
|
||||
}
|
||||
|
||||
public long ticks(long nativeTicks) {
|
||||
|
||||
+4
-4
@@ -113,7 +113,7 @@ public final class PortFeatureChunkGenerator extends ChunkGenerator {
|
||||
}
|
||||
@Override public ChunkGeneratorStructureState createState(HolderLookup<StructureSet> sets,
|
||||
RandomState random, long seed) {
|
||||
if (hasStructurePages()) {
|
||||
if (controlsStructures()) {
|
||||
var lookup = PortStructurePages.filtered(sets, terrainFeatures.structures(), getBiomeSource());
|
||||
var planning = PortStructurePages.planningBiomes(getBiomeSource(), sets, terrainFeatures.structures());
|
||||
if (base instanceof PortFlatChunkGenerator || base instanceof net.minecraft.world.level.levelgen.FlatLevelSource)
|
||||
@@ -124,13 +124,13 @@ public final class PortFeatureChunkGenerator extends ChunkGenerator {
|
||||
return base.createState(sets, random, seed);
|
||||
}
|
||||
|
||||
private boolean hasStructurePages() {
|
||||
return terrainFeatures != null && !terrainFeatures.structures().isEmpty();
|
||||
private boolean controlsStructures() {
|
||||
return terrainFeatures != null && (terrainFeatures.structureControl() || !terrainFeatures.structures().isEmpty());
|
||||
}
|
||||
@Override public void createStructures(RegistryAccess registries, ChunkGeneratorStructureState state,
|
||||
StructureManager structures, ChunkAccess chunk,
|
||||
StructureTemplateManager templates, ResourceKey<Level> dimension) {
|
||||
if (hasStructurePages()) super.createStructures(registries, state, structures, chunk, templates, dimension);
|
||||
if (controlsStructures()) super.createStructures(registries, state, structures, chunk, templates, dimension);
|
||||
else base.createStructures(registries, state, structures, chunk, templates, dimension);
|
||||
}
|
||||
@Override public void createReferences(WorldGenLevel level, StructureManager structures, ChunkAccess chunk) {
|
||||
|
||||
@@ -54,7 +54,8 @@ public final class PortRandomAgeSelection {
|
||||
return new Selection(terrain, new PortBiomePlan(distribution, shuffled.subList(0, count)));
|
||||
}
|
||||
|
||||
private static List<Identifier> eligibleBiomes(MinecraftServer server, PortAgeTerrain terrain) {
|
||||
public static List<Identifier> eligibleBiomes(MinecraftServer server, PortAgeTerrain terrain) {
|
||||
if (terrain == PortAgeTerrain.VOID) return List.of(Identifier.withDefaultNamespace("the_void"));
|
||||
if (terrain == PortAgeTerrain.CAVE) {
|
||||
List<Identifier> caves = server.registryAccess().lookupOrThrow(Registries.BIOME).listElements()
|
||||
.filter(PortRegistryPages::isCaveBiome)
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import net.dnorberg.rivencraft.writing.PortAuthoredWriting;
|
||||
import net.dnorberg.rivencraft.writing.PortRegistryPages;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Frozen page-free material features and rare formations from 1.21.1. */
|
||||
public final class PortRandomFeatureSelection {
|
||||
private PortRandomFeatureSelection() {}
|
||||
public record Selection(PortRockPlan rocks, PortLakePlan lakes, PortTerrainFeaturePlan terrain, int dungeons) {}
|
||||
|
||||
public static Selection select(net.minecraft.server.MinecraftServer server, long seed,
|
||||
PortAgeTerrain terrain, PortBiomePlan biomes) {
|
||||
var base = select(seed, terrain);
|
||||
var structures = PortRandomStructureSelection.select(server, seed, terrain, biomes);
|
||||
var plan = base.terrain();
|
||||
plan = plan == null ? new PortTerrainFeaturePlan(List.of(), false, 0, false, structures.structures(), List.of(), true)
|
||||
: new PortTerrainFeaturePlan(plan.choices(), plan.skylands(), plan.denseOres(), plan.starFissures(),
|
||||
structures.structures(), plan.carvers(), true);
|
||||
return new Selection(base.rocks(), base.lakes(), plan, structures.dungeons());
|
||||
}
|
||||
|
||||
public static Selection select(long seed, PortAgeTerrain terrain) {
|
||||
var random = new Random(seed ^ 0x4D4154455249414CL);
|
||||
int count = random.nextInt(3);
|
||||
var terrainPage = id("terrain/" + terrain.name().toLowerCase(java.util.Locale.ROOT));
|
||||
var candidates = new ArrayList<PortRankedChoice.Option<Identifier>>();
|
||||
for (String name : List.of("rocks", "floating_islands", "tendrils", "spheres", "spikes", "obelisks",
|
||||
"surface_lakes", "deep_lakes", "lava_lakes")) {
|
||||
var page = id("feature/" + name);
|
||||
if (PortAuthoredWriting.supports(List.of(terrainPage, page)))
|
||||
candidates.add(new PortRankedChoice.Option<>(page, rank(name)));
|
||||
}
|
||||
var pages = new ArrayList<Identifier>();
|
||||
pages.add(terrainPage);
|
||||
List<Identifier> solids = PortRegistryPages.solidMaterialIds(), fluids = PortRegistryPages.fluidMaterialIds();
|
||||
for (int selected = 0; selected < count && !candidates.isEmpty();) {
|
||||
Identifier feature = PortRankedChoice.choose(random, candidates);
|
||||
candidates.removeIf(option -> option.value().equals(feature));
|
||||
var materials = PortLakePlan.isLake(feature) ? fluids : solids;
|
||||
if (materials.isEmpty()) continue;
|
||||
pages.add(PortRegistryPages.blockPageId(materials.get(random.nextInt(materials.size()))));
|
||||
pages.add(feature);
|
||||
selected++;
|
||||
}
|
||||
if (terrain != PortAgeTerrain.FLAT && terrain != PortAgeTerrain.VOID
|
||||
&& Math.floorMod(mix(seed ^ 0x534B594C414E4453L), 8) == 0)
|
||||
pages.add(PortTerrainFeaturePlan.SKYLANDS);
|
||||
if (terrain != PortAgeTerrain.CAVE && terrain != PortAgeTerrain.VOID && random.nextInt(10) == 0)
|
||||
pages.add(PortTerrainFeaturePlan.CRYSTAL_FORMATIONS);
|
||||
if (randomStarFissure(seed)) pages.add(PortTerrainFeaturePlan.STAR_FISSURES);
|
||||
return new Selection(PortAuthoredWriting.rockPlan(pages, seed), PortAuthoredWriting.lakePlan(pages, seed),
|
||||
PortAuthoredWriting.terrainFeaturePlan(pages, seed), 0);
|
||||
}
|
||||
|
||||
private static int rank(String name) {
|
||||
return switch (name) {
|
||||
case "surface_lakes", "deep_lakes" -> 1;
|
||||
case "lava_lakes" -> 2;
|
||||
case "spheres", "spikes", "obelisks" -> 3;
|
||||
default -> 4;
|
||||
};
|
||||
}
|
||||
|
||||
private static boolean randomStarFissure(long seed) {
|
||||
var random = new Random(mix(seed ^ 0x535441524752414DL));
|
||||
for (int step = 0; step < 27; step++) {
|
||||
int branch = random.nextInt(5);
|
||||
if (branch == 0) return false;
|
||||
if (random.nextInt(9) == 0) return true;
|
||||
if (branch == 1) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Identifier id(String path) { return Identifier.fromNamespaceAndPath("rivencraft", path); }
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import net.minecraft.core.registries.Registries;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.resources.ResourceKey;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.world.level.dimension.LevelStem;
|
||||
|
||||
/** Creation-only structure completion, adapted from 1.21.1 AgeStructureSelection. */
|
||||
public final class PortRandomStructureSelection {
|
||||
private static final Identifier DUNGEONS = page("dungeons");
|
||||
private PortRandomStructureSelection() {}
|
||||
public record Selection(int dungeons, List<Identifier> structures) {
|
||||
public Selection { structures = List.copyOf(structures); }
|
||||
}
|
||||
|
||||
public static Selection select(MinecraftServer server, long seed, PortAgeTerrain terrain, PortBiomePlan plan) {
|
||||
Set<Identifier> biomes = actualBiomes(server, terrain, plan);
|
||||
var eligible = new ArrayList<PortRankedChoice.Option<Identifier>>();
|
||||
for (String name : List.of("dungeons", "mineshafts", "nether_fortresses", "strongholds", "villages",
|
||||
"trial_chambers", "bastions", "buried_treasure", "shipwrecks", "ocean_ruins", "pillager_outposts",
|
||||
"ruined_portals", "nether_fossils", "ancient_cities", "trail_ruins", "abandoned_camps")) {
|
||||
Identifier feature = page(name);
|
||||
boolean allowed = feature.equals(DUNGEONS)
|
||||
? net.dnorberg.rivencraft.writing.PortAuthoredWriting.supports(List.of(
|
||||
Identifier.fromNamespaceAndPath("rivencraft", "terrain/" + terrain.name().toLowerCase(java.util.Locale.ROOT)), feature))
|
||||
&& server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
|
||||
.get(ResourceKey.create(Registries.PLACED_FEATURE,
|
||||
Identifier.fromNamespaceAndPath("rivencraft", "dungeons"))).isPresent()
|
||||
: PortStructurePages.randomEligible(feature, terrain, biomes, server.registryAccess());
|
||||
if (allowed) eligible.add(new PortRankedChoice.Option<>(feature, rank(name)));
|
||||
}
|
||||
var random = new Random(seed ^ 0x5354525543545552L);
|
||||
int count = random.nextInt(3), dungeons = 0;
|
||||
var structures = new ArrayList<Identifier>();
|
||||
for (int selected = 0; selected < count && !eligible.isEmpty(); selected++) {
|
||||
var feature = PortRankedChoice.choose(random, eligible);
|
||||
if (feature.equals(DUNGEONS)) dungeons++;
|
||||
else structures.add(feature);
|
||||
eligible.removeIf(option -> option.value().equals(feature));
|
||||
}
|
||||
return new Selection(dungeons, structures);
|
||||
}
|
||||
|
||||
private static Set<Identifier> actualBiomes(MinecraftServer server, PortAgeTerrain terrain, PortBiomePlan plan) {
|
||||
if (plan != null && plan.distribution() != PortBiomePlan.Distribution.NATIVE)
|
||||
return Set.copyOf(plan.biomes());
|
||||
if (terrain == PortAgeTerrain.FLAT) return Set.of(Identifier.withDefaultNamespace("plains"));
|
||||
if (terrain == PortAgeTerrain.VOID) return Set.of(Identifier.withDefaultNamespace("the_void"));
|
||||
if (terrain == PortAgeTerrain.CAVE) return Set.of(Identifier.withDefaultNamespace("lush_caves"));
|
||||
return server.registryAccess().lookupOrThrow(Registries.LEVEL_STEM).getOrThrow(LevelStem.OVERWORLD)
|
||||
.value().generator().getBiomeSource().possibleBiomes().stream()
|
||||
.flatMap(holder -> holder.unwrapKey().stream()).map(key -> key.identifier())
|
||||
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||
}
|
||||
|
||||
private static int rank(String name) {
|
||||
return switch (name) {
|
||||
case "villages", "strongholds", "mineshafts" -> 1;
|
||||
case "dungeons", "nether_fortresses" -> 2;
|
||||
default -> 3;
|
||||
};
|
||||
}
|
||||
private static Identifier page(String name) { return Identifier.fromNamespaceAndPath("rivencraft", "feature/" + name); }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package net.dnorberg.rivencraft.age;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/** The 1.21.1 LegacyRankChoice rule, retained for seeded grammar completion. */
|
||||
final class PortRankedChoice {
|
||||
private PortRankedChoice() {}
|
||||
record Option<T>(T value, int rank) {}
|
||||
|
||||
static <T> T choose(Random random, List<Option<T>> options) {
|
||||
if (options.isEmpty()) throw new IllegalArgumentException("No ranked choices");
|
||||
var counts = new HashMap<Integer, Integer>();
|
||||
int highest = 0;
|
||||
for (var option : options) {
|
||||
counts.merge(option.rank(), 1, Integer::sum);
|
||||
highest = Math.max(highest, option.rank());
|
||||
}
|
||||
var weights = new HashMap<Integer, Integer>();
|
||||
int weight = 1, previousTotal = 0;
|
||||
for (int rank = highest; rank >= 0; rank--) {
|
||||
int count = counts.getOrDefault(rank, 0);
|
||||
if (weight != 1 && count > 0) weight = Math.max(weight, previousTotal / count + 1);
|
||||
weights.put(rank, weight);
|
||||
previousTotal = count * weight;
|
||||
weight++;
|
||||
}
|
||||
int ticket = random.nextInt(options.stream().mapToInt(option -> weights.get(option.rank())).sum());
|
||||
for (var option : options) {
|
||||
ticket -= weights.get(option.rank());
|
||||
if (ticket < 0) return option.value();
|
||||
}
|
||||
throw new IllegalStateException("Ranked choice ticket was not consumed");
|
||||
}
|
||||
}
|
||||
@@ -31,11 +31,20 @@ public enum PortSkyStyle {
|
||||
? OVERWORLD : random(seed));
|
||||
}
|
||||
|
||||
/** Nether-style Ages use their fog clear color as the visible backdrop. */
|
||||
public static float fogShade(net.minecraft.world.level.Level level) {
|
||||
boolean nether = level.dimensionTypeRegistration().unwrapKey().map(key ->
|
||||
key.identifier().getNamespace().equals("rivencraft")
|
||||
&& key.identifier().getPath().endsWith("_sky_nether")).orElse(false);
|
||||
var cycle = PortDayCycle.forLevel(level);
|
||||
return nether && !cycle.bodies().isEmpty()
|
||||
? 1F - 0.65F * PortCelestialBody.eclipseAt(cycle.bodies(), PortDayCycle.nativeTicks(level)) : 1F;
|
||||
}
|
||||
|
||||
private static boolean isCelestial(Identifier page) {
|
||||
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
|
||||
return switch (page.getPath()) {
|
||||
// Only Overworld currently draws bodies in this port. Recognize the
|
||||
// Sun/Moon intentions too, without marking those pages implemented.
|
||||
// Preserve the established compatible-default rule for written controllers.
|
||||
case "celestial/normal_time", "celestial/sun", "celestial/moon" -> true;
|
||||
default -> false;
|
||||
};
|
||||
|
||||
@@ -88,6 +88,20 @@ public final class PortStructurePages {
|
||||
.orElseThrow(() -> new IllegalArgumentException("Missing structure set for " + page)));
|
||||
}
|
||||
|
||||
/** Random choices need at least one normally eligible biome; authored pages may override it. */
|
||||
public static boolean randomEligible(Identifier page, PortAgeTerrain terrain, Set<Identifier> biomes,
|
||||
HolderLookup.Provider registries) {
|
||||
if (!supportsTerrain(page, terrain)) return false;
|
||||
var set = registries.lookupOrThrow(Registries.STRUCTURE_SET)
|
||||
.get(ResourceKey.create(Registries.STRUCTURE_SET, SETS.get(page)));
|
||||
if (set.isEmpty()) return false;
|
||||
var compatible = set.get().value().structures().stream()
|
||||
.flatMap(entry -> entry.structure().value().biomes().stream())
|
||||
.flatMap(holder -> holder.unwrapKey().stream()).map(key -> key.identifier())
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
return compatible.isEmpty() || biomes.stream().anyMatch(compatible::contains);
|
||||
}
|
||||
|
||||
private static Map<Identifier, Integer> enabled(List<Identifier> pages) {
|
||||
Map<Identifier, Integer> result = new HashMap<>();
|
||||
for (Identifier page : pages) result.merge(SETS.get(page), 1, Integer::sum);
|
||||
|
||||
@@ -9,7 +9,8 @@ import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Frozen terrain filters and ordered solid/surface features for an Age. */
|
||||
public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres,
|
||||
boolean starFissures, List<Identifier> structures, List<Identifier> carvers) {
|
||||
boolean starFissures, List<Identifier> structures, List<Identifier> carvers,
|
||||
boolean structureControl) {
|
||||
public static final Identifier FLOATING_ISLANDS = Identifier.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "feature/floating_islands");
|
||||
public static final Identifier TENDRILS = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "feature/tendrils");
|
||||
@@ -30,7 +31,8 @@ public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int
|
||||
.forGetter(PortTerrainFeaturePlan::denseOres),
|
||||
Codec.BOOL.optionalFieldOf("star_fissures", false).forGetter(PortTerrainFeaturePlan::starFissures),
|
||||
Identifier.CODEC.listOf().optionalFieldOf("structures", List.of()).forGetter(PortTerrainFeaturePlan::structures),
|
||||
Identifier.CODEC.listOf().optionalFieldOf("carvers", List.of()).forGetter(PortTerrainFeaturePlan::carvers)
|
||||
Identifier.CODEC.listOf().optionalFieldOf("carvers", List.of()).forGetter(PortTerrainFeaturePlan::carvers),
|
||||
Codec.BOOL.optionalFieldOf("structure_control", false).forGetter(PortTerrainFeaturePlan::structureControl)
|
||||
).apply(instance, PortTerrainFeaturePlan::new));
|
||||
|
||||
public record Choice(Identifier page, Identifier material) {
|
||||
@@ -55,7 +57,7 @@ public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int
|
||||
PortCarverPages.validateCounts(carvers);
|
||||
if (denseOres < 0 || denseOres > MAX_PER_FEATURE)
|
||||
throw new IllegalArgumentException("Dense Ores permits at most 32 occurrences");
|
||||
if (choices.isEmpty() && !skylands && denseOres == 0 && !starFissures && structures.isEmpty() && carvers.isEmpty())
|
||||
if (choices.isEmpty() && !skylands && denseOres == 0 && !starFissures && structures.isEmpty() && carvers.isEmpty() && !structureControl)
|
||||
throw new IllegalArgumentException("Terrain feature plan must not be empty");
|
||||
var counts = new HashMap<Identifier, Integer>();
|
||||
for (Choice choice : choices) {
|
||||
@@ -64,6 +66,11 @@ public record PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int
|
||||
}
|
||||
}
|
||||
|
||||
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres,
|
||||
boolean starFissures, List<Identifier> structures, List<Identifier> carvers) {
|
||||
this(choices, skylands, denseOres, starFissures, structures, carvers, false);
|
||||
}
|
||||
|
||||
public PortTerrainFeaturePlan(List<Choice> choices, boolean skylands, int denseOres,
|
||||
boolean starFissures, List<Identifier> structures) {
|
||||
this(choices, skylands, denseOres, starFissures, structures, List.of());
|
||||
|
||||
@@ -465,7 +465,7 @@ public final class RuntimeAgeBackend {
|
||||
case CAVE -> dryCaveSettings(server);
|
||||
case FLAT, VOID -> throw new IllegalStateException("Flat terrain should use FlatLevelSource");
|
||||
};
|
||||
generator = new NoiseBasedChunkGenerator(source, settings);
|
||||
generator = new NoiseBasedChunkGenerator(source, PortBiomeMaterialRules.apply(server.registryAccess(), settings.value()));
|
||||
}
|
||||
if (rocks != null || vegetation != null || dungeons > 0 || lakes != null || terrainFeatures != null)
|
||||
generator = new PortFeatureChunkGenerator(generator, rocks, vegetation, dungeons, lakes, terrainFeatures, seed);
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package net.dnorberg.rivencraft.client;
|
||||
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.blaze3d.vertex.BufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.ByteBufferBuilder;
|
||||
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
|
||||
import com.mojang.renderpearl.api.buffers.GpuBuffer;
|
||||
import com.mojang.renderpearl.api.commands.RenderPass;
|
||||
import com.mojang.renderpearl.api.pipeline.BlendFunction;
|
||||
import com.mojang.renderpearl.api.pipeline.ColorTargetState;
|
||||
import com.mojang.renderpearl.api.pipeline.PrimitiveTopology;
|
||||
import com.mojang.renderpearl.api.pipeline.RenderPipeline;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.Map;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.minecraft.client.renderer.RenderPipelines;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlas;
|
||||
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
|
||||
import net.minecraft.resources.Identifier;
|
||||
import net.minecraft.world.level.MoonPhase;
|
||||
import net.neoforged.api.distmarker.Dist;
|
||||
import net.neoforged.bus.api.SubscribeEvent;
|
||||
import net.neoforged.fml.common.EventBusSubscriber;
|
||||
import net.neoforged.neoforge.client.event.RegisterRenderPipelinesEvent;
|
||||
import org.joml.Matrix4f;
|
||||
import org.joml.Vector4f;
|
||||
|
||||
/** Legacy's opaque central disc, separate from the native transparent glow. */
|
||||
@EventBusSubscriber(modid = Rivencraft.MOD_ID, value = Dist.CLIENT)
|
||||
public final class PortCelestialDiscs implements AutoCloseable {
|
||||
private static final Identifier SUN = Identifier.withDefaultNamespace("sun");
|
||||
private static final RenderPipeline CORE = RenderPipelines.CELESTIAL.toBuilder()
|
||||
.withLocation(Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "pipeline/celestial_core"))
|
||||
.withColorTargetState(new ColorTargetState(BlendFunction.TRANSLUCENT)).build();
|
||||
private final Map<TextureAtlasSprite, GpuBuffer> cores = new IdentityHashMap<>();
|
||||
private TextureAtlasSprite atlasGeneration;
|
||||
|
||||
@SubscribeEvent public static void register(RegisterRenderPipelinesEvent event) { event.registerPipeline(CORE); }
|
||||
|
||||
public void draw(RenderPass pass, TextureAtlas atlas, boolean moon, MoonPhase phase,
|
||||
Matrix4f transform, int color, float alpha) {
|
||||
if (alpha <= 0) return;
|
||||
var generation = atlas.getSprite(SUN);
|
||||
if (generation != atlasGeneration) {
|
||||
close();
|
||||
atlasGeneration = generation;
|
||||
}
|
||||
var sprite = moon ? atlas.getSprite(Identifier.withDefaultNamespace("moon/" + phase.getSerializedName())) : generation;
|
||||
var buffer = cores.computeIfAbsent(sprite, ignored -> build(sprite, moon));
|
||||
var indices = RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);
|
||||
var indexBuffer = indices.getBuffer(6);
|
||||
var rgb = net.dnorberg.rivencraft.age.PortAgeColors.vector(color);
|
||||
var uniforms = RenderSystem.getDynamicUniforms().writeTransform(transform, new Vector4f(rgb, alpha));
|
||||
pass.setPipeline(RenderSystem.getCompiledPipeline(CORE));
|
||||
RenderSystem.bindDefaultUniforms(pass);
|
||||
pass.setUniform("DynamicTransforms", uniforms);
|
||||
pass.setUniform("Sampler0", atlas.getTextureView(), atlas.getSampler());
|
||||
pass.setVertexBuffer(0, buffer.slice());
|
||||
pass.setIndexBuffer(indexBuffer, indices.type());
|
||||
pass.drawIndexed(6, 1, 0, 0, 0);
|
||||
}
|
||||
|
||||
private static GpuBuffer build(TextureAtlasSprite sprite, boolean moon) {
|
||||
var format = DefaultVertexFormat.POSITION_TEX;
|
||||
try (var bytes = ByteBufferBuilder.exactlySized(4 * format.getVertexSize())) {
|
||||
var builder = new BufferBuilder(bytes, PrimitiveTopology.QUADS, format);
|
||||
// Same 8/32 central area as root's ordinary-alpha second pass.
|
||||
float u0 = sprite.getU(0.375F), u1 = sprite.getU(0.625F);
|
||||
float v0 = sprite.getV(0.375F), v1 = sprite.getV(0.625F);
|
||||
builder.addVertex(-0.25F, 0, -0.25F).setUv(moon ? u1 : u0, moon ? v1 : v0);
|
||||
builder.addVertex(0.25F, 0, -0.25F).setUv(moon ? u0 : u1, moon ? v1 : v0);
|
||||
builder.addVertex(0.25F, 0, 0.25F).setUv(moon ? u0 : u1, moon ? v0 : v1);
|
||||
builder.addVertex(-0.25F, 0, 0.25F).setUv(moon ? u1 : u0, moon ? v0 : v1);
|
||||
try (var mesh = builder.buildOrThrow()) {
|
||||
return RenderSystem.getDevice().createBuffer(() -> "Rivencraft celestial core", 40, mesh.vertexBuffer());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void close() {
|
||||
cores.values().forEach(GpuBuffer::close);
|
||||
cores.clear();
|
||||
atlasGeneration = null;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,11 @@ package net.dnorberg.rivencraft.client;
|
||||
|
||||
/** Carries extracted sky presentation values without live-world reads during drawing. */
|
||||
public interface StaticSkyState {
|
||||
int rivencraft$bodyOverlay();
|
||||
void rivencraft$bodyOverlay(int mode);
|
||||
record Body(int kind, float angle, int bearing, float size, int color, float eclipse) {}
|
||||
java.util.List<Body> rivencraft$bodies();
|
||||
void rivencraft$bodies(java.util.List<Body> bodies);
|
||||
long rivencraft$staticFrame();
|
||||
void rivencraft$staticFrame(long frame);
|
||||
int rivencraft$skyTint();
|
||||
@@ -10,4 +15,8 @@ public interface StaticSkyState {
|
||||
void rivencraft$orbitBearing(int bearing);
|
||||
float rivencraft$sunSize();
|
||||
void rivencraft$sunSize(float size);
|
||||
int rivencraft$celestialTint();
|
||||
void rivencraft$celestialTint(int color);
|
||||
int rivencraft$bodyMode();
|
||||
void rivencraft$bodyMode(int mode);
|
||||
}
|
||||
+3
-1
@@ -22,7 +22,9 @@ public class AgeColorAttributesMixin {
|
||||
});
|
||||
builder.addTimeBasedLayer(EnvironmentAttributes.FOG_COLOR, (base, tick) -> {
|
||||
int rgb = PortAgeColors.forLevel(level).sky();
|
||||
return rgb < 0 ? base : PortAgeColors.vector(rgb);
|
||||
var color = rgb < 0 ? base : PortAgeColors.vector(rgb);
|
||||
float shade = net.dnorberg.rivencraft.age.PortSkyStyle.fogShade(level);
|
||||
return shade == 1F ? color : new org.joml.Vector3f(color).mul(shade);
|
||||
});
|
||||
builder.addTimeBasedLayer(EnvironmentAttributes.AMBIENT_LIGHT_COLOR, (base, tick) -> {
|
||||
int rgb = PortAgeColors.forLevel(level).ambient();
|
||||
|
||||
@@ -2,6 +2,7 @@ package net.dnorberg.rivencraft.mixin;
|
||||
|
||||
import net.dnorberg.rivencraft.age.PortDayCycle;
|
||||
import net.minecraft.world.attribute.EnvironmentAttributeSystem;
|
||||
import net.minecraft.world.attribute.EnvironmentAttributes;
|
||||
import net.minecraft.world.attribute.WeatherAttributes;
|
||||
import net.minecraft.world.level.Level;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
@@ -11,6 +12,30 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
|
||||
|
||||
@Mixin(EnvironmentAttributeSystem.Builder.class)
|
||||
public class AgeDayTimelineMixin {
|
||||
@Inject(method = "addDefaultLayers", at = @At("RETURN"))
|
||||
private void rivencraft$singleBody(Level level, CallbackInfoReturnable<EnvironmentAttributeSystem.Builder> cir) {
|
||||
var id = level.dimension().identifier();
|
||||
if (!id.getNamespace().equals("rivencraft") || !id.getPath().startsWith("age/")) return;
|
||||
var builder = cir.getReturnValue();
|
||||
builder.addTimeBasedLayer(EnvironmentAttributes.SUN_ANGLE, (base, tick) -> {
|
||||
var cycle = PortDayCycle.forLevel(level);
|
||||
return cycle.singleBody() ? cycle.bodyAngle(PortDayCycle.nativeTicks(level)) : base;
|
||||
});
|
||||
builder.addTimeBasedLayer(EnvironmentAttributes.MOON_ANGLE, (base, tick) -> {
|
||||
var cycle = PortDayCycle.forLevel(level);
|
||||
return cycle.singleBody() ? cycle.bodyAngle(PortDayCycle.nativeTicks(level)) : base;
|
||||
});
|
||||
builder.addTimeBasedLayer(EnvironmentAttributes.SKY_LIGHT_LEVEL, (base, tick) -> {
|
||||
var cycle = PortDayCycle.forLevel(level);
|
||||
if (!cycle.customLight()) return base;
|
||||
// Match root AgeSkyProfile.lightAt and LevelSkyBrightnessMixin's
|
||||
// single skylight channel. Bright/Dark and no-skylight geometry remain separate.
|
||||
float weather = (1F - level.getRainLevel(1F) * 5F / 16F)
|
||||
* (1F - level.getThunderLevel(1F) * 5F / 16F);
|
||||
return 15F - (int) ((1F - cycle.bodyLight(PortDayCycle.nativeTicks(level)) * weather) * 11F);
|
||||
});
|
||||
}
|
||||
|
||||
@Inject(method = "addDynamicLayers", at = @At("HEAD"), cancellable = true)
|
||||
private void rivencraft$dayCycle(Level level, CallbackInfoReturnable<EnvironmentAttributeSystem.Builder> cir) {
|
||||
var id = level.dimension().identifier();
|
||||
|
||||
+193
-3
@@ -26,6 +26,24 @@ public class StaticSkyRendererMixin {
|
||||
@Unique private int rivencraft$endTint = -1;
|
||||
@Unique private int rivencraft$orbitBearing = -1;
|
||||
@Unique private float rivencraft$sunSize = 1F;
|
||||
@Unique private int rivencraft$celestialTint = -1;
|
||||
@Unique private int rivencraft$bodyMode;
|
||||
@Unique private java.util.List<StaticSkyState.Body> rivencraft$bodies = java.util.List.of();
|
||||
@Unique private net.dnorberg.rivencraft.client.PortCelestialDiscs rivencraft$discs;
|
||||
@Shadow @Final private net.minecraft.client.renderer.texture.TextureAtlas celestialsAtlas;
|
||||
@Shadow private org.joml.Matrix4f applyCelestialBodyTransform(com.mojang.blaze3d.vertex.PoseStack pose,
|
||||
float height, float size) { throw new AssertionError(); }
|
||||
@Shadow private void renderSun(com.mojang.renderpearl.api.commands.RenderPass pass, float alpha,
|
||||
com.mojang.blaze3d.vertex.PoseStack pose) { throw new AssertionError(); }
|
||||
@Shadow private void renderMoon(com.mojang.renderpearl.api.commands.RenderPass pass,
|
||||
net.minecraft.world.level.MoonPhase phase, float alpha,
|
||||
com.mojang.blaze3d.vertex.PoseStack pose) { throw new AssertionError(); }
|
||||
@Shadow private void renderStars(com.mojang.renderpearl.api.commands.RenderPass pass, float brightness,
|
||||
com.mojang.blaze3d.vertex.PoseStack pose) { throw new AssertionError(); }
|
||||
@Shadow private void renderSunMoonAndStars(com.mojang.renderpearl.api.commands.RenderPass pass,
|
||||
com.mojang.blaze3d.vertex.PoseStack pose, float sunAngle,
|
||||
float moonAngle, float starAngle, net.minecraft.world.level.MoonPhase phase,
|
||||
float alpha, float stars) { throw new AssertionError(); }
|
||||
|
||||
@Inject(method = "extractRenderState", at = @At("TAIL"))
|
||||
private void rivencraft$extract(ClientLevel level, float partialTicks, Camera camera,
|
||||
@@ -36,11 +54,66 @@ public class StaticSkyRendererMixin {
|
||||
((StaticSkyState) state).rivencraft$staticFrame(enabled ? level.getGameTime() / 2 : Long.MIN_VALUE);
|
||||
((StaticSkyState) state).rivencraft$skyTint(net.dnorberg.rivencraft.age.PortAgeColors.forLevel(level).sky());
|
||||
var cycle = net.dnorberg.rivencraft.age.PortDayCycle.forLevel(level);
|
||||
// 0=native Overworld/no authored overlay, 1=fog-only backdrop,
|
||||
// 2=native End backdrop, 3=custom Static backdrop.
|
||||
int overlay = !cycle.authored() || cycle.bodyMode() == net.dnorberg.rivencraft.age.PortDayCycle.NONE
|
||||
|| state.skybox == DimensionType.Skybox.OVERWORLD ? 0
|
||||
: enabled ? 3 : state.skybox == DimensionType.Skybox.NONE ? 1 : 2;
|
||||
((StaticSkyState) state).rivencraft$bodyOverlay(overlay);
|
||||
((StaticSkyState) state).rivencraft$orbitBearing(cycle.authored() ? cycle.bearing() : -1);
|
||||
((StaticSkyState) state).rivencraft$sunSize(cycle.authored() ? cycle.sunSize() : 1F);
|
||||
((StaticSkyState) state).rivencraft$celestialTint(
|
||||
net.dnorberg.rivencraft.age.PortAgeColors.forLevel(level).celestial());
|
||||
((StaticSkyState) state).rivencraft$bodyMode(cycle.authored() ? cycle.bodyMode() : 0);
|
||||
// Root's authored Moon always samples the full-moon sprite. Preserve
|
||||
// native calendar phases only for the native Normal Time pair.
|
||||
if (cycle.singleBody() && cycle.bodyMode() == net.dnorberg.rivencraft.age.PortDayCycle.MOON)
|
||||
state.moonPhase = net.minecraft.world.level.MoonPhase.FULL_MOON;
|
||||
var authoredBodies = cycle.bodies();
|
||||
if (authoredBodies.isEmpty() && overlay != 0) {
|
||||
var bodies = new java.util.ArrayList<net.dnorberg.rivencraft.age.PortCelestialBody>();
|
||||
int tint = net.dnorberg.rivencraft.age.PortAgeColors.forLevel(level).celestial();
|
||||
if (cycle.bodyMode() != net.dnorberg.rivencraft.age.PortDayCycle.MOON)
|
||||
bodies.add(net.dnorberg.rivencraft.age.PortCelestialBody.of(cycle, 1, tint, false));
|
||||
if (cycle.bodyMode() != net.dnorberg.rivencraft.age.PortDayCycle.SUN)
|
||||
bodies.add(net.dnorberg.rivencraft.age.PortCelestialBody.of(cycle, 2, tint, cycle.bodyMode() == 0));
|
||||
authoredBodies = java.util.List.copyOf(bodies);
|
||||
}
|
||||
if (authoredBodies.isEmpty()) ((StaticSkyState) state).rivencraft$bodies(java.util.List.of());
|
||||
else {
|
||||
var clock = level.clockManager().getInstance(level.registryAccess().lookupOrThrow(
|
||||
net.minecraft.core.registries.Registries.WORLD_CLOCK).getOrThrow(net.minecraft.world.clock.WorldClocks.OVERWORLD));
|
||||
double ticks = clock.totalTicks() + clock.partialTick();
|
||||
var sources = authoredBodies;
|
||||
var bodies = sources.stream().map(body -> {
|
||||
float eclipse = body.eclipse(sources, ticks);
|
||||
return new StaticSkyState.Body(body.kind(), body.angle(ticks), body.bearing(), body.size(),
|
||||
body.eclipseColor(eclipse), eclipse);
|
||||
}).toList();
|
||||
((StaticSkyState) state).rivencraft$bodies(bodies);
|
||||
float eclipse = 0;
|
||||
for (var body : bodies) if (body.kind() == 1 && Math.cos(Math.toRadians(body.angle())) > 0)
|
||||
eclipse = Math.max(eclipse, body.eclipse());
|
||||
if (state.skybox == DimensionType.Skybox.OVERWORLD) {
|
||||
state.skyColor = new org.joml.Vector3f(state.skyColor).mul(1F - 0.65F * eclipse);
|
||||
} else if (eclipse > 0 && overlay >= 2) {
|
||||
int tint = ((StaticSkyState) state).rivencraft$skyTint();
|
||||
if (tint < 0) tint = enabled ? 0x929292 : 0xffffff;
|
||||
((StaticSkyState) state).rivencraft$skyTint(rivencraft$shade(tint, 1F - 0.65F * eclipse));
|
||||
}
|
||||
}
|
||||
// Only the extracted presentation uses END to schedule the sky pass. The actual
|
||||
// dimension has NONE, so it never acquires End flashes or End gameplay behavior.
|
||||
if (enabled) state.skybox = DimensionType.Skybox.END;
|
||||
if (enabled || overlay == 1) {
|
||||
state.skybox = DimensionType.Skybox.END;
|
||||
state.endFlashIntensity = 0;
|
||||
}
|
||||
if (overlay != 0) state.rainBrightness = 1F - level.getRainLevel(partialTicks);
|
||||
}
|
||||
|
||||
@Unique private static int rivencraft$shade(int rgb, float shade) {
|
||||
return Math.round((rgb >> 16 & 255) * shade) << 16
|
||||
| Math.round((rgb >> 8 & 255) * shade) << 8 | Math.round((rgb & 255) * shade);
|
||||
}
|
||||
|
||||
@Inject(method = "render", at = @At("HEAD"), cancellable = true)
|
||||
@@ -49,9 +122,82 @@ public class StaticSkyRendererMixin {
|
||||
rivencraft$endTint = ((StaticSkyState) state).rivencraft$skyTint();
|
||||
rivencraft$orbitBearing = ((StaticSkyState) state).rivencraft$orbitBearing();
|
||||
rivencraft$sunSize = ((StaticSkyState) state).rivencraft$sunSize();
|
||||
rivencraft$celestialTint = ((StaticSkyState) state).rivencraft$celestialTint();
|
||||
rivencraft$bodyMode = ((StaticSkyState) state).rivencraft$bodyMode();
|
||||
rivencraft$bodies = ((StaticSkyState) state).rivencraft$bodies();
|
||||
if (((StaticSkyState) state).rivencraft$bodyOverlay() == 1) {
|
||||
rivencraft$overlay(fog, state);
|
||||
ci.cancel();
|
||||
return;
|
||||
}
|
||||
if (frame == Long.MIN_VALUE) return;
|
||||
if (rivencraft$staticSky == null) rivencraft$staticSky = new PortStaticSky();
|
||||
rivencraft$staticSky.render(renderTarget, fog, frame, rivencraft$endTint);
|
||||
if (((StaticSkyState) state).rivencraft$bodyOverlay() == 3) rivencraft$overlay(fog, state);
|
||||
ci.cancel();
|
||||
}
|
||||
|
||||
@Inject(method = "render", at = @At("RETURN"))
|
||||
private void rivencraft$afterBackground(GpuBufferSlice fog, SkyRenderState state, CallbackInfo ci) {
|
||||
// RETURN includes the native End branch's early return, after its pass closes.
|
||||
if (((StaticSkyState) state).rivencraft$bodyOverlay() == 2) rivencraft$overlay(fog, state);
|
||||
}
|
||||
|
||||
@Unique private void rivencraft$overlay(GpuBufferSlice fog, SkyRenderState state) {
|
||||
if (rivencraft$bodies.isEmpty()) return;
|
||||
com.mojang.blaze3d.systems.RenderSystem.setShaderFog(fog);
|
||||
try (var pass = com.mojang.blaze3d.systems.RenderSystem.getDevice().createCommandEncoder().createRenderPass(
|
||||
() -> "Rivencraft celestial overlay", renderTarget.getColorTextureView(), java.util.Optional.empty(),
|
||||
renderTarget.getDepthTextureView(), java.util.OptionalDouble.empty())) {
|
||||
renderSunMoonAndStars(pass, new com.mojang.blaze3d.vertex.PoseStack(), 0, 0, 0,
|
||||
net.minecraft.world.level.MoonPhase.FULL_MOON, state.rainBrightness, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Inject(method = "renderSunMoonAndStars", at = @At("HEAD"), cancellable = true)
|
||||
private void rivencraft$drawBodies(com.mojang.renderpearl.api.commands.RenderPass pass,
|
||||
com.mojang.blaze3d.vertex.PoseStack pose, float sunAngle,
|
||||
float moonAngle, float starAngle, net.minecraft.world.level.MoonPhase phase,
|
||||
float alpha, float stars, CallbackInfo ci) {
|
||||
if (rivencraft$bodies.isEmpty()) return;
|
||||
int savedMode = rivencraft$bodyMode, savedTint = rivencraft$celestialTint;
|
||||
float savedSize = rivencraft$sunSize;
|
||||
try {
|
||||
// Stars belong behind opaque discs, not on top of a dark eclipsing moon.
|
||||
if (stars > 0) {
|
||||
pose.pushPose();
|
||||
try {
|
||||
pose.rotateDegrees(com.mojang.math.Axis.YP, -90);
|
||||
pose.rotate(com.mojang.math.Axis.XP, starAngle);
|
||||
renderStars(pass, stars, pose);
|
||||
} finally { pose.popPose(); }
|
||||
}
|
||||
// Same ordering as 1.21.1: all suns, then all moons; each opaque
|
||||
// central disc is drawn after its transparent glow.
|
||||
for (int kind = 1; kind <= 2; kind++) for (var body : rivencraft$bodies) {
|
||||
if (body.kind() != kind) continue;
|
||||
rivencraft$bodyMode = kind;
|
||||
rivencraft$celestialTint = body.color();
|
||||
rivencraft$sunSize = body.size();
|
||||
pose.pushPose();
|
||||
try {
|
||||
pose.rotateDegrees(com.mojang.math.Axis.YP, body.bearing() < 0 ? -90 : body.bearing());
|
||||
pose.rotateDegrees(com.mojang.math.Axis.XP, body.angle());
|
||||
float glowAlpha = alpha * (kind == 1 ? 0.84F : 0.88F + 0.12F * body.eclipse());
|
||||
var bodyPhase = net.minecraft.world.level.MoonPhase.FULL_MOON;
|
||||
if (kind == 1) renderSun(pass, glowAlpha, pose); else renderMoon(pass, bodyPhase, glowAlpha, pose);
|
||||
if (rivencraft$discs == null) rivencraft$discs = new net.dnorberg.rivencraft.client.PortCelestialDiscs();
|
||||
// Both passes must use the same sprite. Native dark phase pixels
|
||||
// become black patches under ordinary alpha, unlike the old glow-only pass.
|
||||
var transform = applyCelestialBodyTransform(pose, 100F, (kind == 1 ? 30F : 20F) * body.size());
|
||||
rivencraft$discs.draw(pass, celestialsAtlas, kind == 2, bodyPhase, transform, body.color(), alpha);
|
||||
} finally { pose.popPose(); }
|
||||
}
|
||||
} finally {
|
||||
rivencraft$bodyMode = savedMode;
|
||||
rivencraft$celestialTint = savedTint;
|
||||
rivencraft$sunSize = savedSize;
|
||||
}
|
||||
ci.cancel();
|
||||
}
|
||||
|
||||
@@ -68,10 +214,48 @@ public class StaticSkyRendererMixin {
|
||||
return nativeSize * rivencraft$sunSize;
|
||||
}
|
||||
|
||||
@Inject(method = "renderSunriseAndSunset", at = @At("HEAD"))
|
||||
@ModifyArg(method = "renderMoon", at = @At(value = "INVOKE", target =
|
||||
"Lnet/minecraft/client/renderer/SkyRenderer;applyCelestialBodyTransform(Lcom/mojang/blaze3d/vertex/PoseStack;FF)Lorg/joml/Matrix4f;"), index = 2)
|
||||
private float rivencraft$scaleMoon(float nativeSize) {
|
||||
return rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.MOON
|
||||
? nativeSize * rivencraft$sunSize : nativeSize;
|
||||
}
|
||||
|
||||
@Inject(method = "renderSun", at = @At("HEAD"), cancellable = true)
|
||||
private void rivencraft$hideSun(com.mojang.renderpearl.api.commands.RenderPass pass, float alpha,
|
||||
com.mojang.blaze3d.vertex.PoseStack pose, CallbackInfo ci) {
|
||||
if (rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.MOON
|
||||
|| rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.NONE) ci.cancel();
|
||||
}
|
||||
|
||||
@Inject(method = "renderMoon", at = @At("HEAD"), cancellable = true)
|
||||
private void rivencraft$hideMoon(com.mojang.renderpearl.api.commands.RenderPass pass,
|
||||
net.minecraft.world.level.MoonPhase phase, float alpha,
|
||||
com.mojang.blaze3d.vertex.PoseStack pose, CallbackInfo ci) {
|
||||
if (rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.SUN
|
||||
|| rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.NONE) ci.cancel();
|
||||
}
|
||||
|
||||
@ModifyArg(method = {"renderSun", "renderMoon"}, at = @At(value = "INVOKE", target =
|
||||
"Lnet/minecraft/client/renderer/DynamicGpuData;writeTransform(Lorg/joml/Matrix4f;Lorg/joml/Vector4f;)Lcom/mojang/renderpearl/api/buffers/GpuBufferSlice;"), index = 1)
|
||||
private org.joml.Vector4f rivencraft$tintCelestial(org.joml.Vector4f nativeColor) {
|
||||
// Normal Time consumes one shared color group for its sun and moon in 1.21.1.
|
||||
// Keep native alpha (including weather fading) and the sprite's transparent halo.
|
||||
if (rivencraft$celestialTint < 0) return nativeColor;
|
||||
var tint = net.dnorberg.rivencraft.age.PortAgeColors.vector(rivencraft$celestialTint);
|
||||
return new org.joml.Vector4f(nativeColor.x * tint.x(), nativeColor.y * tint.y(),
|
||||
nativeColor.z * tint.z(), nativeColor.w);
|
||||
}
|
||||
|
||||
@Inject(method = "renderSunriseAndSunset", at = @At("HEAD"), cancellable = true)
|
||||
private void rivencraft$rotateHorizon(com.mojang.renderpearl.api.commands.RenderPass pass,
|
||||
com.mojang.blaze3d.vertex.PoseStack pose, float angle,
|
||||
org.joml.Vector4fc color, CallbackInfo ci) {
|
||||
if (rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.MOON
|
||||
|| rivencraft$bodyMode == net.dnorberg.rivencraft.age.PortDayCycle.NONE) {
|
||||
ci.cancel();
|
||||
return;
|
||||
}
|
||||
if (rivencraft$orbitBearing < 0) return;
|
||||
pose.pushPose();
|
||||
pose.rotateDegrees(com.mojang.math.Axis.YP, rivencraft$orbitBearing + 90);
|
||||
@@ -81,7 +265,9 @@ public class StaticSkyRendererMixin {
|
||||
private void rivencraft$restoreHorizon(com.mojang.renderpearl.api.commands.RenderPass pass,
|
||||
com.mojang.blaze3d.vertex.PoseStack pose, float angle,
|
||||
org.joml.Vector4fc color, CallbackInfo ci) {
|
||||
if (rivencraft$orbitBearing >= 0) pose.popPose();
|
||||
if (rivencraft$bodyMode != net.dnorberg.rivencraft.age.PortDayCycle.MOON
|
||||
&& rivencraft$bodyMode != net.dnorberg.rivencraft.age.PortDayCycle.NONE
|
||||
&& rivencraft$orbitBearing >= 0) pose.popPose();
|
||||
}
|
||||
|
||||
@Redirect(method = "renderEndSky", at = @At(value = "INVOKE", target =
|
||||
@@ -94,6 +280,10 @@ public class StaticSkyRendererMixin {
|
||||
|
||||
@Inject(method = "close", at = @At("HEAD"))
|
||||
private void rivencraft$close(CallbackInfo ci) {
|
||||
if (rivencraft$discs != null) {
|
||||
rivencraft$discs.close();
|
||||
rivencraft$discs = null;
|
||||
}
|
||||
if (rivencraft$staticSky != null) {
|
||||
rivencraft$staticSky.close();
|
||||
rivencraft$staticSky = null;
|
||||
|
||||
@@ -14,6 +14,14 @@ public class StaticSkyStateMixin implements StaticSkyState {
|
||||
@Unique private int rivencraft$tint = -1;
|
||||
@Unique private int rivencraft$bearing = -1;
|
||||
@Unique private float rivencraft$sunSize = 1F;
|
||||
@Unique private int rivencraft$celestialTint = -1;
|
||||
@Unique private int rivencraft$bodyMode;
|
||||
@Unique private int rivencraft$bodyOverlay;
|
||||
public int rivencraft$bodyOverlay() { return rivencraft$bodyOverlay; }
|
||||
public void rivencraft$bodyOverlay(int mode) { rivencraft$bodyOverlay = mode; }
|
||||
@Unique private java.util.List<StaticSkyState.Body> rivencraft$bodies = java.util.List.of();
|
||||
public java.util.List<StaticSkyState.Body> rivencraft$bodies() { return rivencraft$bodies; }
|
||||
public void rivencraft$bodies(java.util.List<StaticSkyState.Body> bodies) { rivencraft$bodies = java.util.List.copyOf(bodies); }
|
||||
public long rivencraft$staticFrame() { return rivencraft$frame; }
|
||||
public void rivencraft$staticFrame(long frame) { rivencraft$frame = frame; }
|
||||
public int rivencraft$skyTint() { return rivencraft$tint; }
|
||||
@@ -22,6 +30,10 @@ public class StaticSkyStateMixin implements StaticSkyState {
|
||||
public void rivencraft$orbitBearing(int bearing) { rivencraft$bearing = bearing; }
|
||||
public float rivencraft$sunSize() { return rivencraft$sunSize; }
|
||||
public void rivencraft$sunSize(float size) { rivencraft$sunSize = size; }
|
||||
public int rivencraft$celestialTint() { return rivencraft$celestialTint; }
|
||||
public void rivencraft$celestialTint(int color) { rivencraft$celestialTint = color; }
|
||||
public int rivencraft$bodyMode() { return rivencraft$bodyMode; }
|
||||
public void rivencraft$bodyMode(int mode) { rivencraft$bodyMode = mode; }
|
||||
|
||||
@Inject(method = "reset", at = @At("TAIL"))
|
||||
private void rivencraft$reset(CallbackInfo ci) {
|
||||
@@ -29,5 +41,9 @@ public class StaticSkyStateMixin implements StaticSkyState {
|
||||
rivencraft$tint = -1;
|
||||
rivencraft$bearing = -1;
|
||||
rivencraft$sunSize = 1F;
|
||||
rivencraft$celestialTint = -1;
|
||||
rivencraft$bodyMode = 0;
|
||||
rivencraft$bodyOverlay = 0;
|
||||
rivencraft$bodies = java.util.List.of();
|
||||
}
|
||||
}
|
||||
@@ -108,9 +108,10 @@ public final class PortBookNetworking {
|
||||
|
||||
public static Identifier createAge(ServerPlayer player, String title, List<Identifier> pages) {
|
||||
if (title == null || title.isBlank() || title.length() > MAX_BOOK_TITLE_LENGTH) return null;
|
||||
long seed = player.getRandom().nextLong();
|
||||
net.dnorberg.rivencraft.age.PortBiomePlan biomePlan;
|
||||
try {
|
||||
biomePlan = PortAuthoredWriting.biomePlan(pages, player.registryAccess());
|
||||
biomePlan = PortAuthoredWriting.biomePlan(pages, player.level().getServer(), seed);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
player.sendOverlayMessage(Component.literal("Age writing unavailable: " + exception.getMessage()));
|
||||
return null;
|
||||
@@ -127,7 +128,6 @@ public final class PortBookNetworking {
|
||||
id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
|
||||
}
|
||||
try {
|
||||
long seed = player.getRandom().nextLong();
|
||||
PortAgeTerrain terrain = PortAuthoredWriting.terrain(pages);
|
||||
if (pages.isEmpty()) {
|
||||
PortRandomAgeSelection.Selection random = PortRandomAgeSelection.select(server, seed);
|
||||
@@ -135,15 +135,17 @@ public final class PortBookNetworking {
|
||||
biomePlan = random.biomes();
|
||||
}
|
||||
var environment = net.dnorberg.rivencraft.age.PortEnvironmentSelection.resolve(pages, seed, terrain);
|
||||
var randomFeatures = pages.isEmpty()
|
||||
? net.dnorberg.rivencraft.age.PortRandomFeatureSelection.select(server, seed, terrain, biomePlan) : null;
|
||||
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
|
||||
PortAuthoredWriting.rockPlan(pages, seed),
|
||||
randomFeatures == null ? PortAuthoredWriting.rockPlan(pages, seed) : randomFeatures.rocks(),
|
||||
PortAuthoredWriting.vegetationPlan(pages),
|
||||
PortAuthoredWriting.dungeonCount(pages),
|
||||
PortAuthoredWriting.lakePlan(pages, seed),
|
||||
PortAuthoredWriting.terrainFeaturePlan(pages, seed),
|
||||
randomFeatures == null ? PortAuthoredWriting.dungeonCount(pages) : randomFeatures.dungeons(),
|
||||
randomFeatures == null ? PortAuthoredWriting.lakePlan(pages, seed) : randomFeatures.lakes(),
|
||||
randomFeatures == null ? PortAuthoredWriting.terrainFeaturePlan(pages, seed) : randomFeatures.terrain(),
|
||||
environment.weather(), environment.lighting(), environment.sky(),
|
||||
net.dnorberg.rivencraft.age.PortAgeColors.compile(pages, seed),
|
||||
net.dnorberg.rivencraft.age.PortDayCycle.compile(pages, seed,
|
||||
net.dnorberg.rivencraft.age.PortAgeColors.resolve(pages, seed),
|
||||
net.dnorberg.rivencraft.age.PortDayCycle.resolve(pages, seed,
|
||||
net.dnorberg.rivencraft.age.PortDayCycle.nativeTicks(server.overworld())));
|
||||
} catch (IllegalArgumentException | IllegalStateException exception) {
|
||||
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
|
||||
|
||||
@@ -72,6 +72,12 @@ public final class PortAuthoredWriting {
|
||||
|
||||
/** A page never disappears into an Age unless this port can apply its effect. */
|
||||
public static boolean supports(List<Identifier> pages) {
|
||||
try {
|
||||
return supportsExplicit(PortMaterialCompletion.complete(pages, 0));
|
||||
} catch (IllegalArgumentException exception) { return false; }
|
||||
}
|
||||
|
||||
private static boolean supportsExplicit(List<Identifier> pages) {
|
||||
if (!net.dnorberg.rivencraft.age.PortAgeColors.validNumbers(pages)) return false;
|
||||
if (!net.dnorberg.rivencraft.age.PortDayCycle.validWriting(pages)) return false;
|
||||
boolean terrain = false;
|
||||
@@ -97,7 +103,7 @@ public final class PortAuthoredWriting {
|
||||
|| pendingMaterial.equals("rivencraft:fluid") && PortLakePlan.isLake(page))) return false;
|
||||
if (net.dnorberg.rivencraft.age.PortAgeColors.isModifier(page)
|
||||
|| net.dnorberg.rivencraft.age.PortDayCycle.supports(page)) {
|
||||
// Shared color groups can precede a sky/ambient target or tint the sky at the end.
|
||||
// Shared colors target Sky, Ambient Light Color or Normal Time; trailing colors tint the sky.
|
||||
} else if (isTerrain(page) && !terrain) {
|
||||
terrain = true;
|
||||
} else if (PortSkyStyle.forPage(page) != null && sky++ == 0) {
|
||||
@@ -107,7 +113,7 @@ public final class PortAuthoredWriting {
|
||||
} else if (PortWeatherMode.forPage(page) != null && weather++ == 0) {
|
||||
// A single independent weather policy for this Age.
|
||||
} else if (controller(page) != null && controllers++ == 0) {
|
||||
// An explicit controller needs an authored pool.
|
||||
// Missing biome slots are seed-completed when the Age is created.
|
||||
} else if (isBlockPage(page) && pendingMaterial == null
|
||||
&& PortSymbolPages.find(page) != null
|
||||
&& (PortSymbolPages.find(page).category().equals("rivencraft:block")
|
||||
@@ -152,7 +158,8 @@ public final class PortAuthoredWriting {
|
||||
}
|
||||
if (pendingMaterial != null || (rocks > 0 || pages.stream().anyMatch(PortVegetationPlan::requiresSurface))
|
||||
&& (pages.contains(CAVE_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
||||
if ((dungeons > 0 || lakes > 0) && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN)
|
||||
if (dungeons > 0 && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
||||
if (lakes > 0 && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN)
|
||||
|| pages.contains(CAVE_TERRAIN))) return false;
|
||||
if (denseOres > 0 && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
||||
if (!carvers.isEmpty() && (pages.contains(FLAT_TERRAIN) || pages.contains(VOID_TERRAIN))) return false;
|
||||
@@ -169,8 +176,8 @@ public final class PortAuthoredWriting {
|
||||
.filter(value -> value != null).findFirst().orElseThrow();
|
||||
boolean valid = switch (distribution) {
|
||||
case NATIVE -> biomes == 0;
|
||||
case SINGLE -> biomes == 1;
|
||||
default -> biomes >= 2;
|
||||
case SINGLE -> biomes <= 1;
|
||||
default -> true;
|
||||
};
|
||||
return valid;
|
||||
}
|
||||
@@ -214,13 +221,14 @@ public final class PortAuthoredWriting {
|
||||
}
|
||||
|
||||
public static PortRockPlan rockPlan(List<Identifier> pages, long seed) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||
pages = completedMaterials(pages, seed);
|
||||
List<Identifier> materials = solidMaterials(pages, seed, ROCKS);
|
||||
return materials.isEmpty() ? null : new PortRockPlan(materials);
|
||||
}
|
||||
|
||||
public static PortTerrainFeaturePlan terrainFeaturePlan(List<Identifier> pages, long seed) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||
pages = completedMaterials(pages, seed);
|
||||
final List<Identifier> completed = pages;
|
||||
Map<Identifier, List<Identifier>> materials = new HashMap<>();
|
||||
Map<Identifier, Integer> occurrences = new HashMap<>();
|
||||
List<PortTerrainFeaturePlan.Choice> choices = new ArrayList<>();
|
||||
@@ -230,7 +238,7 @@ public final class PortAuthoredWriting {
|
||||
choices.add(new PortTerrainFeaturePlan.Choice(page, PortTerrainFeaturePlan.CRYSTAL_MATERIAL));
|
||||
continue;
|
||||
}
|
||||
List<Identifier> selected = materials.computeIfAbsent(page, feature -> solidMaterials(pages, seed, feature));
|
||||
List<Identifier> selected = materials.computeIfAbsent(page, feature -> solidMaterials(completed, seed, feature));
|
||||
int occurrence = occurrences.merge(page, 1, Integer::sum) - 1;
|
||||
choices.add(new PortTerrainFeaturePlan.Choice(page, selected.get(occurrence)));
|
||||
}
|
||||
@@ -262,7 +270,7 @@ public final class PortAuthoredWriting {
|
||||
}
|
||||
|
||||
public static PortLakePlan lakePlan(List<Identifier> pages, long seed) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||
pages = completedMaterials(pages, seed);
|
||||
if (pages.stream().noneMatch(PortLakePlan::isLake)) return null;
|
||||
List<Identifier> fluidPool = PortRegistryPages.fluidMaterialIds();
|
||||
if (fluidPool.isEmpty()) throw new IllegalStateException("No source-fluid materials are registered");
|
||||
@@ -288,6 +296,12 @@ public final class PortAuthoredWriting {
|
||||
return new PortLakePlan(choices);
|
||||
}
|
||||
|
||||
private static List<Identifier> completedMaterials(List<Identifier> pages, long seed) {
|
||||
var completed = PortMaterialCompletion.complete(pages, seed);
|
||||
if (!supportsExplicit(completed)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||
return completed;
|
||||
}
|
||||
|
||||
public static PortVegetationPlan vegetationPlan(List<Identifier> pages) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException("Unsupported authored Age pages");
|
||||
List<Identifier> vegetation = pages.stream().filter(PortVegetationPlan::isVegetation).toList();
|
||||
@@ -307,6 +321,17 @@ public final class PortAuthoredWriting {
|
||||
|
||||
/** Resolves all authored biomes from the active registry before creating a level. */
|
||||
public static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries) {
|
||||
return biomePlan(pages, registries, 0, null);
|
||||
}
|
||||
|
||||
/** Creation-only completion; the saved plan never rerolls on load or reseed. */
|
||||
public static PortBiomePlan biomePlan(List<Identifier> pages, net.minecraft.server.MinecraftServer server, long seed) {
|
||||
return biomePlan(pages, server.registryAccess(), seed,
|
||||
net.dnorberg.rivencraft.age.PortRandomAgeSelection.eligibleBiomes(server, terrain(pages)));
|
||||
}
|
||||
|
||||
private static PortBiomePlan biomePlan(List<Identifier> pages, HolderLookup.Provider registries,
|
||||
long seed, List<Identifier> available) {
|
||||
if (!supports(pages)) throw new IllegalArgumentException(
|
||||
"Unsupported terrain or biome-page combination in this port slice");
|
||||
List<Identifier> biomes = new ArrayList<>();
|
||||
@@ -341,6 +366,25 @@ public final class PortAuthoredWriting {
|
||||
throw new IllegalArgumentException("Dense Ores needs at least one biome with registered ore placements");
|
||||
if (distribution == PortBiomePlan.Distribution.NATIVE)
|
||||
return new PortBiomePlan(distribution, List.of());
|
||||
if (distribution != null && available != null) {
|
||||
int minimum = distribution == PortBiomePlan.Distribution.SINGLE ? 1 : 2;
|
||||
int target = minimum;
|
||||
if (biomes.isEmpty() && distribution != PortBiomePlan.Distribution.SINGLE) {
|
||||
var random = new Random(seed ^ 0x42494F4D45434F55L);
|
||||
while (target < available.size() && random.nextInt(3) != 0) target++;
|
||||
}
|
||||
var distinct = new java.util.HashSet<>(biomes);
|
||||
// Void may retain authored weather/season biomes, but must not gain
|
||||
// an unrequested The Void biome just to fill a multi-biome controller.
|
||||
var filler = new ArrayList<>(pages.contains(VOID_TERRAIN) && !biomes.isEmpty() ? List.<Identifier>of() : available);
|
||||
Collections.shuffle(filler, new Random(seed ^ 0x42494F4D45504F4FL));
|
||||
for (Identifier biome : filler) {
|
||||
if (distinct.size() >= target) break;
|
||||
if (distinct.add(biome)) biomes.add(biome);
|
||||
}
|
||||
if (distinct.size() < minimum) throw new IllegalArgumentException(
|
||||
distribution + " requires " + minimum + " distinct biomes; supply more biome pages for this terrain");
|
||||
}
|
||||
if (biomes.isEmpty()) return null;
|
||||
if (distribution == null) distribution = biomes.size() == 1
|
||||
? PortBiomePlan.Distribution.SINGLE : biomes.size() == 2
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import net.dnorberg.rivencraft.age.PortCarverPages;
|
||||
import net.dnorberg.rivencraft.age.PortLakePlan;
|
||||
import net.dnorberg.rivencraft.age.PortStructurePages;
|
||||
import net.dnorberg.rivencraft.age.PortTerrainFeaturePlan;
|
||||
import net.dnorberg.rivencraft.age.PortVegetationPlan;
|
||||
import net.minecraft.resources.Identifier;
|
||||
|
||||
/** Creation-time implied features, adapted from 1.21.1 AgeFeatureSelection. */
|
||||
public final class PortMaterialCompletion {
|
||||
private PortMaterialCompletion() {}
|
||||
|
||||
public static List<Identifier> complete(List<Identifier> pages, long seed) {
|
||||
var result = new ArrayList<Identifier>();
|
||||
for (int index = 0; index < pages.size(); index++) {
|
||||
Identifier page = pages.get(index);
|
||||
result.add(page);
|
||||
var entry = PortSymbolPages.find(page);
|
||||
if (entry == null || !(entry.category().equals("rivencraft:block")
|
||||
|| entry.category().equals("rivencraft:fluid"))) continue;
|
||||
if (index + 1 < pages.size() && isFeature(pages.get(index + 1))) continue;
|
||||
Identifier material = PortRegistryPages.blockId(page);
|
||||
var candidates = candidates(pages, entry.category().equals("rivencraft:fluid"));
|
||||
if (candidates.isEmpty()) throw new IllegalArgumentException(
|
||||
"No compatible feature for material " + material + " on this terrain");
|
||||
long mixed = mix(seed ^ (long) material.hashCode() * 0x9E3779B97F4A7C15L ^ index);
|
||||
result.add(candidates.get(Math.floorMod(mixed, candidates.size())));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
private static List<Identifier> candidates(List<Identifier> pages, boolean fluid) {
|
||||
boolean cave = pages.contains(id("terrain/cave"));
|
||||
boolean empty = pages.contains(id("terrain/void"));
|
||||
boolean flat = pages.contains(id("terrain/flat"));
|
||||
if (fluid) return cave || empty || flat ? List.of()
|
||||
: List.of(id("feature/surface_lakes"), id("feature/deep_lakes"), id("feature/lava_lakes"));
|
||||
var choices = new ArrayList<Identifier>();
|
||||
if (!cave && !empty && !flat) choices.add(id("feature/rocks"));
|
||||
if (!cave) choices.add(PortTerrainFeaturePlan.FLOATING_ISLANDS);
|
||||
choices.add(PortTerrainFeaturePlan.TENDRILS);
|
||||
choices.add(PortTerrainFeaturePlan.SPHERES);
|
||||
if (!cave && !empty) {
|
||||
choices.add(PortTerrainFeaturePlan.SPIKES);
|
||||
choices.add(PortTerrainFeaturePlan.OBELISKS);
|
||||
}
|
||||
return choices;
|
||||
}
|
||||
|
||||
private static boolean isFeature(Identifier page) {
|
||||
return PortTerrainFeaturePlan.isSupported(page) || PortLakePlan.isLake(page)
|
||||
|| PortVegetationPlan.isVegetation(page) || PortStructurePages.supports(page)
|
||||
|| PortCarverPages.supports(page) || page.equals(id("feature/rocks"))
|
||||
|| page.equals(id("feature/dungeons")) || page.equals(PortTerrainFeaturePlan.SKYLANDS)
|
||||
|| page.equals(PortTerrainFeaturePlan.DENSE_ORES) || page.equals(PortTerrainFeaturePlan.STAR_FISSURES);
|
||||
}
|
||||
|
||||
private static Identifier id(String path) { return Identifier.fromNamespaceAndPath("rivencraft", path); }
|
||||
|
||||
private static long mix(long value) {
|
||||
value = (value ^ value >>> 30) * 0xBF58476D1CE4E5B9L;
|
||||
value = (value ^ value >>> 27) * 0x94D049BB133111EBL;
|
||||
return value ^ value >>> 31;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user