Port Age lighting and animated sky styles to 26.3 beta
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-11-02 15:49:58 -05:00
1 parent 48fbae9be9
commit a7a73d3c17
23 files changed
+967 -24

No files matched your search

@@ -34,7 +34,11 @@ final class AgeLevelIndex extends SavedData {
Codec.unboundedMap(Identifier.CODEC, PortLakePlan.CODEC)
.optionalFieldOf("lakes", Map.of()).forGetter(index -> index.lakes),
Codec.unboundedMap(Identifier.CODEC, PortTerrainFeaturePlan.CODEC)
.optionalFieldOf("terrain_features", Map.of()).forGetter(index -> index.terrainFeatures)
.optionalFieldOf("terrain_features", Map.of()).forGetter(index -> index.terrainFeatures),
Codec.unboundedMap(Identifier.CODEC, PortLighting.CODEC)
.optionalFieldOf("lighting", Map.of()).forGetter(index -> index.lighting),
Codec.unboundedMap(Identifier.CODEC, PortSkyStyle.CODEC)
.optionalFieldOf("sky", Map.of()).forGetter(index -> index.sky)
).apply(instance, AgeLevelIndex::new));
static final SavedDataType<AgeLevelIndex> TYPE = new SavedDataType<>(
@@ -53,6 +57,8 @@ final class AgeLevelIndex extends SavedData {
private final Map<Identifier, Integer> dungeons;
private final Map<Identifier, PortLakePlan> lakes;
private final Map<Identifier, PortTerrainFeaturePlan> terrainFeatures;
private final Map<Identifier, PortLighting> lighting;
private final Map<Identifier, PortSkyStyle> sky;
private AgeLevelIndex() {
this.ages = new LinkedHashMap<>();
@@ -65,6 +71,8 @@ final class AgeLevelIndex extends SavedData {
this.dungeons = new LinkedHashMap<>();
this.lakes = new LinkedHashMap<>();
this.terrainFeatures = new LinkedHashMap<>();
this.lighting = new LinkedHashMap<>();
this.sky = new LinkedHashMap<>();
}
private AgeLevelIndex(Map<Identifier, Long> ages, List<Identifier> retired,
@@ -75,7 +83,9 @@ final class AgeLevelIndex extends SavedData {
Map<Identifier, PortVegetationPlan> vegetation,
Map<Identifier, Integer> dungeons,
Map<Identifier, PortLakePlan> lakes,
Map<Identifier, PortTerrainFeaturePlan> terrainFeatures) {
Map<Identifier, PortTerrainFeaturePlan> terrainFeatures,
Map<Identifier, PortLighting> lighting,
Map<Identifier, PortSkyStyle> sky) {
this.ages = new LinkedHashMap<>(ages);
this.retired = new LinkedHashSet<>(retired);
this.biomeOverrides = new LinkedHashMap<>(biomeOverrides);
@@ -86,6 +96,8 @@ final class AgeLevelIndex extends SavedData {
this.dungeons = new LinkedHashMap<>(dungeons);
this.lakes = new LinkedHashMap<>(lakes);
this.terrainFeatures = new LinkedHashMap<>(terrainFeatures);
this.lighting = new LinkedHashMap<>(lighting);
this.sky = new LinkedHashMap<>(sky);
}
Map<Identifier, Long> entries() {
@@ -116,10 +128,12 @@ final class AgeLevelIndex extends SavedData {
int dungeons(Identifier id) { return dungeons.getOrDefault(id, 0); }
PortLakePlan lakes(Identifier id) { return lakes.get(id); }
PortTerrainFeaturePlan terrainFeatures(Identifier id) { return terrainFeatures.get(id); }
PortLighting lighting(Identifier id) { return lighting.getOrDefault(id, PortLighting.NORMAL); }
PortSkyStyle sky(Identifier id) { return sky.getOrDefault(id, PortSkyStyle.OVERWORLD); }
void add(Identifier id, long seed, PortBiomePlan biomePlan, PortAgeTerrain ageTerrain,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan, int dungeonCount,
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan) {
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan, PortLighting light, PortSkyStyle skyStyle) {
if (contains(id)) {
throw new IllegalArgumentException("Age already exists: " + id);
}
@@ -131,6 +145,8 @@ final class AgeLevelIndex extends SavedData {
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
if (lakePlan != null) lakes.put(id, lakePlan);
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
if (light != PortLighting.NORMAL) lighting.put(id, light);
if (skyStyle != PortSkyStyle.OVERWORLD) sky.put(id, skyStyle);
setDirty();
}
@@ -144,6 +160,8 @@ final class AgeLevelIndex extends SavedData {
dungeons.remove(id);
lakes.remove(id);
terrainFeatures.remove(id);
lighting.remove(id);
sky.remove(id);
retired.add(id);
setDirty();
}
@@ -151,7 +169,8 @@ final class AgeLevelIndex extends SavedData {
void restoreAfterFailedRetirement(Identifier id, long seed, PortBiomePlan biomePlan,
PortAgeTerrain ageTerrain, PortRockPlan rockPlan,
PortVegetationPlan vegetationPlan, int dungeonCount,
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan) {
PortLakePlan lakePlan, PortTerrainFeaturePlan terrainFeaturePlan, PortLighting light,
PortSkyStyle skyStyle) {
if (!retired.remove(id)) throw new IllegalStateException("Age is not retired: " + id);
ages.put(id, seed);
if (biomePlan != null) biomePlans.put(id, biomePlan);
@@ -161,6 +180,8 @@ final class AgeLevelIndex extends SavedData {
if (dungeonCount > 0) dungeons.put(id, dungeonCount);
if (lakePlan != null) lakes.put(id, lakePlan);
if (terrainFeaturePlan != null) terrainFeatures.put(id, terrainFeaturePlan);
if (light != PortLighting.NORMAL) lighting.put(id, light);
if (skyStyle != PortSkyStyle.OVERWORLD) sky.put(id, skyStyle);
setDirty();
}
@@ -68,6 +68,32 @@ public final class PortAgeCommands {
return 1;
})))
.then(Commands.literal("age")
.then(Commands.literal("create_sky")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("sky", StringArgumentType.word())
.then(Commands.argument("lighting", StringArgumentType.word())
.then(Commands.argument("terrain", StringArgumentType.word())
.executes(PortAgeCommands::createSkyAge)))))))
.then(Commands.literal("sky_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeSky)))
.then(Commands.literal("environment_probe")
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("terrain", StringArgumentType.word())
.executes(PortAgeCommands::probeEnvironment))))
.then(Commands.literal("create_lighting")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("seed", LongArgumentType.longArg())
.then(Commands.argument("mode", StringArgumentType.word())
.suggests((context, builder) -> net.minecraft.commands.SharedSuggestionProvider.suggest(
java.util.Arrays.stream(PortLighting.values())
.map(mode -> mode.name().toLowerCase(Locale.ROOT)), builder))
.then(Commands.argument("terrain", StringArgumentType.word())
.executes(PortAgeCommands::createLightingAge))))))
.then(Commands.literal("lighting_probe")
.then(Commands.argument("name", StringArgumentType.word())
.executes(PortAgeCommands::probeLighting)))
.then(Commands.literal("surface_probe")
.then(Commands.argument("name", StringArgumentType.word())
.then(Commands.argument("x", IntegerArgumentType.integer())
@@ -137,10 +163,13 @@ public final class PortAgeCommands {
long seed = LongArgumentType.getLong(context, "seed");
PortRandomAgeSelection.Selection selected = PortRandomAgeSelection.select(
context.getSource().getServer(), seed);
int created = createAge(context, selected.terrain(), selected.biomes());
var environment = PortEnvironmentSelection.resolve(java.util.List.of(), seed, selected.terrain());
int created = createAge(context, selected.terrain(), selected.biomes(),
null, null, 0, null, null, environment);
if (created != 0) context.getSource().sendSuccess(() -> Component.literal(
"Random Age plan: terrain=" + selected.terrain()
+ " biomes=" + selected.biomes()), false);
+ " biomes=" + selected.biomes()
+ " environment=" + environment), false);
return created;
}))))
.then(Commands.literal("create_rocks")
@@ -318,6 +347,115 @@ public final class PortAgeCommands {
.executes(PortAgeCommands::sampleBiome)))))));
}
private static int createSkyAge(CommandContext<CommandSourceStack> context) {
try {
PortSkyStyle sky = PortSkyStyle.valueOf(StringArgumentType.getString(context, "sky").toUpperCase(Locale.ROOT));
PortLighting lighting = PortLighting.valueOf(StringArgumentType.getString(context, "lighting").toUpperCase(Locale.ROOT));
PortAgeTerrain terrain = PortAgeTerrain.valueOf(StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
var pages = java.util.List.of(
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "sky/" + sky.name().toLowerCase(Locale.ROOT)),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "lighting/" + lighting.name().toLowerCase(Locale.ROOT)),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + terrain.name().toLowerCase(Locale.ROOT)),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "weather/off"));
var server = context.getSource().getServer();
var biomes = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
RuntimeAgeBackend.create(server, ageId(context), LongArgumentType.getLong(context, "seed"),
biomes, terrain, null, null, 0, null, null, PortWeatherMode.OFF, lighting, sky);
return probeSky(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int probeSky(CommandContext<CommandSourceStack> context) {
Identifier id = ageId(context);
var server = context.getSource().getServer();
var level = server.getLevel(ResourceKey.create(Registries.DIMENSION, id));
if (level == null) {
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
return 0;
}
var type = level.dimensionType();
context.getSource().sendSuccess(() -> Component.literal("Sky probe " + id
+ " type=" + level.dimensionTypeRegistration().unwrapKey().orElseThrow().identifier()
+ " skybox=" + type.skybox() + " skylight=" + type.hasSkyLight() + " ambient=" + type.ambientLight()
+ " fog=" + level.environmentAttributes().getValue(net.minecraft.world.attribute.EnvironmentAttributes.FOG_COLOR,
new net.minecraft.core.BlockPos(0, 80, 0))
+ " ceiling=" + type.hasCeiling() + " scale=" + type.coordinateScale()
+ " minY=" + type.minY() + " height=" + type.height() + " dragon=" + type.hasEnderDragonFight()), false);
return 1;
}
private static int probeEnvironment(CommandContext<CommandSourceStack> context) {
try {
long seed = LongArgumentType.getLong(context, "seed");
PortAgeTerrain terrain = PortAgeTerrain.valueOf(
StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
var selected = PortEnvironmentSelection.resolve(java.util.List.of(), seed, terrain);
var explicitPages = java.util.List.of(
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "weather/storm"),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "lighting/bright"));
var explicit = PortEnvironmentSelection.resolve(explicitPages, seed, terrain);
var weatherOnly = PortEnvironmentSelection.resolve(java.util.List.of(explicitPages.get(0)), seed, terrain);
var lightingOnly = PortEnvironmentSelection.resolve(java.util.List.of(explicitPages.get(1)), seed, terrain);
if (!selected.equals(PortEnvironmentSelection.resolve(java.util.List.of(), seed, terrain))
|| explicit.lighting() != PortLighting.BRIGHT
|| explicit.weather() != (terrain == PortAgeTerrain.CAVE ? PortWeatherMode.OFF : PortWeatherMode.STORM)
|| weatherOnly.lighting() != selected.lighting() || lightingOnly.weather() != selected.weather())
throw new IllegalStateException("Environment selection invariant failed");
context.getSource().sendSuccess(() -> Component.literal("Environment probe seed=" + seed
+ " terrain=" + terrain + " selected=" + selected + " explicit=" + explicit
+ " deterministic=true"), false);
return 1;
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int createLightingAge(CommandContext<CommandSourceStack> context) {
try {
PortLighting mode = PortLighting.valueOf(
StringArgumentType.getString(context, "mode").toUpperCase(Locale.ROOT));
PortAgeTerrain terrain = PortAgeTerrain.valueOf(
StringArgumentType.getString(context, "terrain").toUpperCase(Locale.ROOT));
var pages = java.util.List.of(
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "lighting/" + mode.name().toLowerCase(Locale.ROOT)),
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "terrain/" + terrain.name().toLowerCase(Locale.ROOT)));
var server = context.getSource().getServer();
var biomePlan = net.dnorberg.rivencraft.writing.PortAuthoredWriting.biomePlan(pages, server.registryAccess());
RuntimeAgeBackend.create(server, ageId(context), LongArgumentType.getLong(context, "seed"),
biomePlan, terrain, null, null, 0, null, null, PortWeatherMode.NORMAL, mode);
return probeLighting(context);
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
}
}
private static int probeLighting(CommandContext<CommandSourceStack> context) {
Identifier id = ageId(context);
var server = context.getSource().getServer();
ServerLevel level = server.getLevel(ResourceKey.create(Registries.DIMENSION, id));
if (level == null) {
context.getSource().sendFailure(Component.literal("Age not loaded: " + id));
return 0;
}
context.getSource().sendSuccess(() -> Component.literal("Lighting probe " + id
+ " type=" + level.dimensionTypeRegistration().unwrapKey().orElseThrow().identifier()
+ " skylight=" + level.dimensionType().hasSkyLight()
+ " ambient=" + level.dimensionType().ambientLight()
+ " undergroundBrightness=" + level.getMaxLocalRawBrightness(
new net.minecraft.core.BlockPos(0, level.getMinY() + 2, 0))
+ " vanillaDarkEnough=" + net.minecraft.world.entity.monster.Monster.isDarkEnoughToSpawn(
level, new net.minecraft.core.BlockPos(0, level.getMinY() + 2, 0),
net.minecraft.util.RandomSource.create(0))
+ " overworldSkylight=" + server.overworld().dimensionType().hasSkyLight()
+ " overworldAmbient=" + server.overworld().dimensionType().ambientLight()), false);
return 1;
}
private static int probeSurface(CommandContext<CommandSourceStack> context) {
Identifier id = ageId(context);
ServerLevel level = context.getSource().getServer().getLevel(ResourceKey.create(Registries.DIMENSION, id));
@@ -602,13 +740,23 @@ public final class PortAgeCommands {
PortAgeTerrain terrain, PortBiomePlan biomePlan,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures) {
return createAge(context, terrain, biomePlan, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures,
new PortEnvironmentSelection(PortWeatherMode.NORMAL, PortLighting.NORMAL));
}
private static int createAge(CommandContext<CommandSourceStack> context,
PortAgeTerrain terrain, PortBiomePlan biomePlan,
PortRockPlan rockPlan, PortVegetationPlan vegetationPlan,
int dungeons, PortLakePlan lakes, PortTerrainFeaturePlan terrainFeatures,
PortEnvironmentSelection environment) {
String name = StringArgumentType.getString(context, "name");
Identifier id = Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, "age/" + name);
long seed = LongArgumentType.getLong(context, "seed");
ServerLevel level;
try {
level = RuntimeAgeBackend.create(context.getSource().getServer(), id, seed, biomePlan,
terrain, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures);
terrain, rockPlan, vegetationPlan, dungeons, lakes, terrainFeatures,
environment.weather(), environment.lighting());
} catch (IllegalArgumentException | IllegalStateException exception) {
context.getSource().sendFailure(Component.literal(exception.getMessage()));
return 0;
@@ -0,0 +1,28 @@
package net.dnorberg.rivencraft.age;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import net.minecraft.resources.Identifier;
/** Seed-completes missing environment controllers once, before an Age is saved. */
public record PortEnvironmentSelection(PortWeatherMode weather, PortLighting lighting) {
public static PortEnvironmentSelection resolve(List<Identifier> pages, long seed, PortAgeTerrain terrain) {
PortLighting lighting = pages.stream().map(PortLighting::forPage).filter(java.util.Objects::nonNull)
.findFirst().orElseGet(() -> rankedDefault(new Random(seed ^ 0x4C49474854494E47L), PortLighting.values()));
PortWeatherMode weather = terrain == PortAgeTerrain.CAVE ? PortWeatherMode.OFF
: pages.stream().map(PortWeatherMode::forPage).filter(java.util.Objects::nonNull)
.findFirst().orElseGet(() -> rankedDefault(new Random(seed ^ 0x574541544845525FL), PortWeatherMode.values()));
return new PortEnvironmentSelection(weather, lighting);
}
private static <T> T rankedDefault(Random random, T[] choices) {
// LegacyRankChoice: the first (Normal) option is rank 1, the rest rank 2.
// Its rank-1 weight is the complete rare group plus one. Derive this
// from the supported pool, rather than selecting unported weather.
int[] weights = new int[choices.length];
Arrays.fill(weights, 1);
weights[0] = choices.length;
return PortRandomAgeSelection.weighted(random, List.of(choices), weights);
}
}
@@ -0,0 +1,52 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import java.util.Locale;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.Holder;
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.DimensionType;
import net.minecraft.world.level.Level;
/** Authored lighting choices using statically registered, client-synchronized types. */
public enum PortLighting {
NORMAL, DARK, BRIGHT;
public static final Codec<PortLighting> CODEC = Codec.STRING.xmap(
name -> valueOf(name.toUpperCase(Locale.ROOT)), mode -> mode.name().toLowerCase(Locale.ROOT));
public static PortLighting forPage(Identifier page) {
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
return switch (page.getPath()) {
case "lighting/normal" -> NORMAL;
case "lighting/dark" -> DARK;
case "lighting/bright" -> BRIGHT;
default -> null;
};
}
public static boolean isBright(Level level) {
return level.dimensionTypeRegistration().unwrapKey().map(key -> {
Identifier id = key.identifier();
return id.getNamespace().equals(Rivencraft.MOD_ID)
&& (id.getPath().equals("age_bright") || id.getPath().equals("age_cave_bright")
|| id.getPath().startsWith("age_bright_sky_")
|| id.getPath().startsWith("age_cave_bright_sky_"));
}).orElse(false);
}
Holder<DimensionType> dimensionType(MinecraftServer server, PortAgeTerrain terrain,
Holder<DimensionType> overworldType, PortSkyStyle sky) {
boolean cave = terrain == PortAgeTerrain.CAVE;
if (this == NORMAL && !cave && sky == PortSkyStyle.OVERWORLD) return overworldType;
String variant = this == BRIGHT ? (cave ? "age_cave_bright" : "age_bright")
: this == DARK || cave ? "age_dark" : "age_normal";
if (sky != PortSkyStyle.OVERWORLD) variant += "_sky_" + sky.name().toLowerCase(Locale.ROOT);
return server.registryAccess().lookupOrThrow(Registries.DIMENSION_TYPE).getOrThrow(
ResourceKey.create(Registries.DIMENSION_TYPE,
Identifier.fromNamespaceAndPath(Rivencraft.MOD_ID, variant)));
}
}
@@ -68,7 +68,7 @@ public final class PortRandomAgeSelection {
return surface.isEmpty() ? List.of(Identifier.withDefaultNamespace("plains")) : surface;
}
private static <T> T weighted(Random random, List<T> choices, int[] weights) {
static <T> T weighted(Random random, List<T> choices, int[] weights) {
int total = 0;
for (int weight : weights) total += weight;
int ticket = random.nextInt(total);
@@ -0,0 +1,31 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import java.util.List;
import java.util.Locale;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.resources.Identifier;
/** Persisted sky styles, including the client-rendered Legacy static background. */
public enum PortSkyStyle {
OVERWORLD, NETHER, END, STATIC;
public static final Codec<PortSkyStyle> CODEC = Codec.STRING.xmap(
name -> valueOf(name.toUpperCase(Locale.ROOT)), sky -> sky.name().toLowerCase(Locale.ROOT));
public static PortSkyStyle forPage(Identifier page) {
if (!page.getNamespace().equals(Rivencraft.MOD_ID)) return null;
return switch (page.getPath()) {
case "sky/overworld" -> OVERWORLD;
case "sky/nether" -> NETHER;
case "sky/end" -> END;
case "sky/static" -> STATIC;
default -> null;
};
}
public static PortSkyStyle fromPages(List<Identifier> pages) {
return pages.stream().map(PortSkyStyle::forPage).filter(java.util.Objects::nonNull)
.findFirst().orElse(OVERWORLD);
}
}
@@ -1,7 +1,6 @@
package net.dnorberg.rivencraft.age;
import com.mojang.serialization.Codec;
import java.util.List;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.resources.Identifier;
@@ -26,8 +25,4 @@ public enum PortWeatherMode {
};
}
public static PortWeatherMode fromPages(List<Identifier> pages) {
return pages.stream().map(PortWeatherMode::forPage).filter(java.util.Objects::nonNull)
.findFirst().orElse(NORMAL);
}
}
@@ -113,13 +113,32 @@ public final class RuntimeAgeBackend {
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather) {
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures, weather, PortLighting.NORMAL);
}
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
PortLighting lighting) {
return create(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures, weather, lighting, PortSkyStyle.OVERWORLD);
}
public static ServerLevel create(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures, PortWeatherMode weather,
PortLighting lighting, PortSkyStyle sky) {
checkServerThread(server);
ageName(id);
AgeLevelIndex index = index(server);
if (index.contains(id)) throw new IllegalArgumentException("Age already exists: " + id);
ServerLevel level = load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
ServerLevel level = load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures, lighting, sky);
((PortAgeLevel) level).configureWeather(weather);
index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures);
index.add(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes, terrainFeatures, lighting, sky);
// A newly bound book may be used immediately. Persist its Age identity
// before publishing the catalog so a later crash cannot leave that
// book pointing at a world omitted from the on-disk index.
@@ -194,6 +213,8 @@ public final class RuntimeAgeBackend {
PortTerrainFeaturePlan terrainFeatures = index.terrainFeatures(id);
boolean loaded = server.getLevel(dimensionKey(id)) != null;
ArchivePaths paths = archivePaths(server, name);
PortLighting lighting = index.lighting(id);
PortSkyStyle sky = index.sky(id);
if (loaded) unloadLoaded(server, id);
// Persist the tombstone before moving files: a crash mid-archive must
// not let an old book silently regenerate this identity.
@@ -211,7 +232,7 @@ public final class RuntimeAgeBackend {
} catch (IOException | RuntimeException exception) {
try {
index.restoreAfterFailedRetirement(id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures);
terrainFeatures, lighting, sky);
server.overworld().getDataStorage().saveAndJoin();
} catch (RuntimeException rollbackFailure) {
exception.addSuppressed(rollbackFailure);
@@ -337,6 +358,14 @@ public final class RuntimeAgeBackend {
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures) {
return load(server, id, seed, biomePlan, terrain, rocks, vegetation, dungeons, lakes,
terrainFeatures, index(server).lighting(id), index(server).sky(id));
}
private static ServerLevel load(MinecraftServer server, Identifier id, long seed,
PortBiomePlan biomePlan, PortAgeTerrain terrain, PortRockPlan rocks,
PortVegetationPlan vegetation, int dungeons, PortLakePlan lakes,
PortTerrainFeaturePlan terrainFeatures, PortLighting lighting, PortSkyStyle sky) {
ResourceKey<Level> key = ResourceKey.create(Registries.DIMENSION, id);
if (server.getLevel(key) != null) throw new IllegalArgumentException("Level already loaded: " + id);
@@ -409,7 +438,8 @@ public final class RuntimeAgeBackend {
}
if (rocks != null || vegetation != null || dungeons > 0 || lakes != null || terrainFeatures != null)
generator = new PortFeatureChunkGenerator(generator, rocks, vegetation, dungeons, lakes, terrainFeatures, seed);
LevelStem stem = new LevelStem(overworldStem.type(), generator, OptionalLong.of(seed));
LevelStem stem = new LevelStem(lighting.dimensionType(server, terrain, overworldStem.type(), sky),
generator, OptionalLong.of(seed));
long biomeZoomSeed = BiomeManager.obfuscateSeed(server.getWorldGenSettings().options().seed());
ServerLevel level = new PortAgeLevel(server, key, stem, biomeZoomSeed, terrain);
@@ -0,0 +1,87 @@
package net.dnorberg.rivencraft.client;
import com.mojang.blaze3d.pipeline.RenderTarget;
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.blaze3d.vertex.MeshData;
import com.mojang.renderpearl.api.buffers.GpuBuffer;
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
import com.mojang.renderpearl.api.pipeline.PrimitiveTopology;
import java.util.Optional;
import java.util.OptionalDouble;
import net.minecraft.client.renderer.RenderPipelines;
import net.minecraft.client.renderer.texture.DynamicTexture;
import net.minecraft.util.RandomSource;
import org.joml.Matrix4f;
/** Legacy's six 48x48 noise faces, uploaded once per animation frame rather than per cell. */
public final class PortStaticSky implements AutoCloseable {
private static final int GRID = 48;
private final DynamicTexture texture = new DynamicTexture("Rivencraft static sky", GRID * 6, GRID, false);
private final GpuBuffer cube = buildCube();
private long uploadedFrame = Long.MIN_VALUE;
public void render(RenderTarget target, GpuBufferSlice fog, long frame) {
if (uploadedFrame != frame) {
RandomSource random = RandomSource.create(frame);
var image = texture.getPixels();
for (int face = 0; face < 6; face++) {
for (int y = 0; y < GRID; y++) {
for (int x = 0; x < GRID; x++) {
int gray = 146 * (65 + random.nextInt(160)) / 255;
image.setPixel(face * GRID + x, y, 0xff000000 | gray << 16 | gray << 8 | gray);
}
}
}
texture.upload();
uploadedFrame = frame;
}
RenderSystem.setShaderFog(fog);
var indices = RenderSystem.getSequentialBuffer(PrimitiveTopology.QUADS);
var indexBuffer = indices.getBuffer(36);
var transform = RenderSystem.getDynamicUniforms().writeTransform(RenderSystem.getModelViewMatrixCopy());
try (var pass = RenderSystem.getDevice().createCommandEncoder().createRenderPass(
() -> "Rivencraft static sky", target.getColorTextureView(), Optional.empty(),
target.getDepthTextureView(), OptionalDouble.empty())) {
pass.setPipeline(RenderSystem.getCompiledPipeline(RenderPipelines.END_SKY));
RenderSystem.bindDefaultUniforms(pass);
pass.setUniform("DynamicTransforms", transform);
pass.setUniform("Sampler0", texture.getTextureView(), texture.getSampler());
pass.setVertexBuffer(0, cube.slice());
pass.setIndexBuffer(indexBuffer, indices.type());
pass.drawIndexed(36, 1, 0, 0, 0);
}
}
private static GpuBuffer buildCube() {
var format = DefaultVertexFormat.POSITION_TEX_COLOR;
try (var bytes = ByteBufferBuilder.exactlySized(24 * format.getVertexSize())) {
var builder = new BufferBuilder(bytes, PrimitiveTopology.QUADS, format);
for (int face = 0; face < 6; face++) {
var pose = new Matrix4f();
switch (face) {
case 1 -> pose.rotationX((float) Math.PI / 2);
case 2 -> pose.rotationX((float) -Math.PI / 2);
case 3 -> pose.rotationX((float) Math.PI);
case 4 -> pose.rotationZ((float) Math.PI / 2);
case 5 -> pose.rotationZ((float) -Math.PI / 2);
}
float u0 = face / 6f, u1 = (face + 1) / 6f;
builder.addVertex(pose, -100, -100, -100).setUv(u0, 0).setColor(-1);
builder.addVertex(pose, -100, -100, 100).setUv(u0, 1).setColor(-1);
builder.addVertex(pose, 100, -100, 100).setUv(u1, 1).setColor(-1);
builder.addVertex(pose, 100, -100, -100).setUv(u1, 0).setColor(-1);
}
try (MeshData mesh = builder.buildOrThrow()) {
return RenderSystem.getDevice().createBuffer(() -> "Rivencraft static sky cube", 40, mesh.vertexBuffer());
}
}
}
@Override public void close() {
cube.close();
texture.close();
}
}
@@ -0,0 +1,7 @@
package net.dnorberg.rivencraft.client;
/** Carries the extracted animation frame without consulting the live world during drawing. */
public interface StaticSkyState {
long rivencraft$staticFrame();
void rivencraft$staticFrame(long frame);
}
@@ -0,0 +1,23 @@
package net.dnorberg.rivencraft.mixin;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import net.dnorberg.rivencraft.age.PortLighting;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.LevelHeightAccessor;
import net.minecraft.world.level.lighting.LevelLightEngine;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
/** Ambient light contributes to effective brightness, not fake block/sky propagation. */
@Mixin(LevelLightEngine.class)
public abstract class AgeAmbientLightMixin {
@Shadow @Final protected LevelHeightAccessor levelHeightAccessor;
@ModifyReturnValue(method = "getRawBrightness", at = @At("RETURN"))
private int rivencraft$ambientBrightness(int brightness) {
return levelHeightAccessor instanceof Level level && PortLighting.isBright(level)
? Math.max(brightness, 15) : brightness;
}
}
@@ -0,0 +1,53 @@
package net.dnorberg.rivencraft.mixin;
import com.mojang.blaze3d.pipeline.RenderTarget;
import com.mojang.renderpearl.api.buffers.GpuBufferSlice;
import net.dnorberg.rivencraft.client.PortStaticSky;
import net.dnorberg.rivencraft.client.StaticSkyState;
import net.minecraft.client.Camera;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.SkyRenderer;
import net.minecraft.client.renderer.state.level.SkyRenderState;
import net.minecraft.world.level.dimension.DimensionType;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(SkyRenderer.class)
public class StaticSkyRendererMixin {
@Shadow @Final private RenderTarget renderTarget;
@Unique private PortStaticSky rivencraft$staticSky;
@Inject(method = "extractRenderState", at = @At("TAIL"))
private void rivencraft$extract(ClientLevel level, float partialTicks, Camera camera,
SkyRenderState state, CallbackInfo ci) {
boolean enabled = level.dimensionTypeRegistration().unwrapKey().map(key ->
key.identifier().getNamespace().equals("rivencraft")
&& key.identifier().getPath().endsWith("_sky_static")).orElse(false);
((StaticSkyState) state).rivencraft$staticFrame(enabled ? level.getGameTime() / 2 : Long.MIN_VALUE);
// 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;
}
@Inject(method = "render", at = @At("HEAD"), cancellable = true)
private void rivencraft$render(GpuBufferSlice fog, SkyRenderState state, CallbackInfo ci) {
long frame = ((StaticSkyState) state).rivencraft$staticFrame();
if (frame == Long.MIN_VALUE) return;
if (rivencraft$staticSky == null) rivencraft$staticSky = new PortStaticSky();
rivencraft$staticSky.render(renderTarget, fog, frame);
ci.cancel();
}
@Inject(method = "close", at = @At("HEAD"))
private void rivencraft$close(CallbackInfo ci) {
if (rivencraft$staticSky != null) {
rivencraft$staticSky.close();
rivencraft$staticSky = null;
}
}
}
@@ -0,0 +1,19 @@
package net.dnorberg.rivencraft.mixin;
import net.dnorberg.rivencraft.client.StaticSkyState;
import net.minecraft.client.renderer.state.level.SkyRenderState;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(SkyRenderState.class)
public class StaticSkyStateMixin implements StaticSkyState {
@Unique private long rivencraft$frame = Long.MIN_VALUE;
public long rivencraft$staticFrame() { return rivencraft$frame; }
public void rivencraft$staticFrame(long frame) { rivencraft$frame = frame; }
@Inject(method = "reset", at = @At("TAIL"))
private void rivencraft$reset(CallbackInfo ci) { rivencraft$frame = Long.MIN_VALUE; }
}
@@ -134,13 +134,15 @@ public final class PortBookNetworking {
terrain = random.terrain();
biomePlan = random.biomes();
}
var environment = net.dnorberg.rivencraft.age.PortEnvironmentSelection.resolve(pages, seed, terrain);
RuntimeAgeBackend.create(server, id, seed, biomePlan, terrain,
PortAuthoredWriting.rockPlan(pages, seed),
PortAuthoredWriting.vegetationPlan(pages),
PortAuthoredWriting.dungeonCount(pages),
PortAuthoredWriting.lakePlan(pages, seed),
PortAuthoredWriting.terrainFeaturePlan(pages, seed),
net.dnorberg.rivencraft.age.PortWeatherMode.fromPages(pages));
environment.weather(), environment.lighting(),
net.dnorberg.rivencraft.age.PortSkyStyle.fromPages(pages));
} catch (IllegalArgumentException | IllegalStateException exception) {
player.sendOverlayMessage(Component.literal("Age creation failed: " + exception.getMessage()));
return null;
@@ -15,6 +15,8 @@ import net.dnorberg.rivencraft.age.PortTerrainFeaturePlan;
import net.dnorberg.rivencraft.age.PortStructurePages;
import net.dnorberg.rivencraft.age.PortCarverPages;
import net.dnorberg.rivencraft.age.PortWeatherMode;
import net.dnorberg.rivencraft.age.PortLighting;
import net.dnorberg.rivencraft.age.PortSkyStyle;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.registries.Registries;
@@ -53,7 +55,8 @@ public final class PortAuthoredWriting {
if (page == null || !page.getNamespace().equals(Rivencraft.MOD_ID)) return false;
var entry = PortSymbolPages.find(page);
if (entry == null) return false;
if (isTerrain(page) || controller(page) != null || PortWeatherMode.forPage(page) != null) return true;
if (isTerrain(page) || controller(page) != null || PortWeatherMode.forPage(page) != null
|| PortLighting.forPage(page) != null || PortSkyStyle.forPage(page) != null) return true;
if (entry.category().equals("rivencraft:biome") && PortRegistryPages.biomeId(page) != null) return true;
if (isBlockPage(page) && (entry.category().equals("rivencraft:block")
|| entry.category().equals("rivencraft:fluid"))) return true;
@@ -79,6 +82,8 @@ public final class PortAuthoredWriting {
int denseOres = 0;
int starFissures = 0;
int weather = 0;
int lighting = 0;
int sky = 0;
Map<Identifier, Integer> terrainFeatures = new HashMap<>();
Map<Identifier, Integer> structures = new HashMap<>();
Map<Identifier, Integer> carvers = new HashMap<>();
@@ -88,6 +93,10 @@ public final class PortAuthoredWriting {
|| pendingMaterial.equals("rivencraft:fluid") && PortLakePlan.isLake(page))) return false;
if (isTerrain(page) && !terrain) {
terrain = true;
} else if (PortSkyStyle.forPage(page) != null && sky++ == 0) {
// Sky presentation, independent of terrain and illumination.
} else if (PortLighting.forPage(page) != null && lighting++ == 0) {
// A single lighting controller; omitted choices resolve from the Age seed.
} else if (PortWeatherMode.forPage(page) != null && weather++ == 0) {
// A single independent weather policy for this Age.
} else if (controller(page) != null && controllers++ == 0) {
@@ -3,6 +3,7 @@
"minVersion": "0.8",
"package": "net.dnorberg.rivencraft.mixin",
"compatibilityLevel": "JAVA_25",
"mixins": ["StructurePlacementAccessor", "AgeStructureBiomeMixin", "AgeCarversMixin"],
"mixins": ["StructurePlacementAccessor", "AgeStructureBiomeMixin", "AgeCarversMixin", "AgeAmbientLightMixin"],
"client": ["StaticSkyStateMixin", "StaticSkyRendererMixin"],
"injectors": {"defaultRequire": 1}
}