Add authored Age skies and celestial pages (#44)
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-10-21 23:56:58 -04:00
1 parent 9044835ec5
commit 3ac4c499ab
54 files changed
+880 -36

No files matched your search

+11
View File
@@ -124,6 +124,17 @@ the top or underside of one of its blocks. Insert a bound Linking or Descriptive
Book to activate it; the book remains visible on the receptacle. A Linking Book aimed at its Book to activate it; the book remains visible on the receptacle. A Linking Book aimed at its
own dimension still opens a visual portal, but travel requires Intra Linking. own dimension still opens a visual portal, but travel requires Intra Linking.
Sky writing includes eight color pages and a Static Sky page. Multiple sky-color
pages blend their colors, tinting the Age sky, book preview, and the fallback
surface of portals linked there. Multiple celestial-color pages blend for the
next written sun or moon. An Age may have no suns or moons at all;
Static Sky animates the sky with television-like noise. Sun and Moon pages add
independent bodies; preceding color, size, angle, period, phase, and direction
pages configure the next body. The third and each subsequent sun add increasing
instability. Newly generated page-free Ages also choose a deterministic sky,
color, and celestial arrangement from their Age seed; older saved Ages retain
their previous sky.
Craft a Book Binder from three iron ingots and five planks. Put leather or an Craft a Book Binder from three iron ingots and five planks. Put leather or an
empty Notebook in the left cover slot. Enter a title, then place a Link Panel empty Notebook in the left cover slot. Enter a title, then place a Link Panel
Page first in the horizontal page strip. Click a page in the strip with a Page first in the horizontal page strip. Click a page in the strip with a
+1 -1
View File
@@ -36,7 +36,7 @@ mod_name=Rivencraft
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default. # The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=LGPL-3.0-or-later mod_license=LGPL-3.0-or-later
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md. # The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
mod_version=0.6.8.4 mod_version=0.6.8.7
immersive_portals_version=6.0.7 immersive_portals_version=6.0.7
terrablender_version=4.1.0.8 terrablender_version=4.1.0.8
serene_seasons_version_id=pHEgQQUE serene_seasons_version_id=pHEgQQUE
@@ -1,6 +1,7 @@
package net.dnorberg.rivencraft.block.entity; package net.dnorberg.rivencraft.block.entity;
import net.dnorberg.rivencraft.book.LinkTarget; import net.dnorberg.rivencraft.book.LinkTarget;
import net.dnorberg.rivencraft.book.AgeEnvironmentCompiler;
import net.dnorberg.rivencraft.registry.ModBlockEntities; import net.dnorberg.rivencraft.registry.ModBlockEntities;
import net.dnorberg.rivencraft.portal.CrystalPortalNetwork; import net.dnorberg.rivencraft.portal.CrystalPortalNetwork;
import net.minecraft.nbt.NbtOps; import net.minecraft.nbt.NbtOps;
@@ -20,8 +21,10 @@ import net.neoforged.fml.ModList;
public final class BookReceptacleBlockEntity extends BlockEntity { public final class BookReceptacleBlockEntity extends BlockEntity {
private static final String BOOK_TAG = "Book"; private static final String BOOK_TAG = "Book";
private static final String DESTINATION_TAG = "Destination"; private static final String DESTINATION_TAG = "Destination";
private static final String SKY_COLOR_TAG = "SkyColor";
private ItemStack book = ItemStack.EMPTY; private ItemStack book = ItemStack.EMPTY;
private LinkTarget destination; private LinkTarget destination;
private int skyColor = 0xFFFFFF;
private boolean restoreView = true; private boolean restoreView = true;
public BookReceptacleBlockEntity(BlockPos pos, BlockState state) { public BookReceptacleBlockEntity(BlockPos pos, BlockState state) {
@@ -30,6 +33,7 @@ public final class BookReceptacleBlockEntity extends BlockEntity {
public ItemStack getBook() { return book; } public ItemStack getBook() { return book; }
public LinkTarget destination() { return destination; } public LinkTarget destination() { return destination; }
public int skyColor() { return skyColor; }
public void setBook(ItemStack stack) { public void setBook(ItemStack stack) {
book = stack.copyWithCount(1); book = stack.copyWithCount(1);
@@ -38,6 +42,12 @@ public final class BookReceptacleBlockEntity extends BlockEntity {
public void setDestination(LinkTarget target) { public void setDestination(LinkTarget target) {
destination = target; destination = target;
skyColor = 0xFFFFFF;
if (target != null && level instanceof ServerLevel server) {
ServerLevel targetLevel = server.getServer().getLevel(target.location().dimension());
if (targetLevel != null) skyColor = AgeEnvironmentCompiler.previewSkyColor(
targetLevel, target.location().pos()) & 0xFFFFFF;
}
sync(); sync();
} }
@@ -45,6 +55,7 @@ public final class BookReceptacleBlockEntity extends BlockEntity {
ItemStack result = book; ItemStack result = book;
book = ItemStack.EMPTY; book = ItemStack.EMPTY;
destination = null; destination = null;
skyColor = 0xFFFFFF;
sync(); sync();
return result; return result;
} }
@@ -63,6 +74,7 @@ public final class BookReceptacleBlockEntity extends BlockEntity {
: ItemStack.EMPTY; : ItemStack.EMPTY;
destination = tag.contains(DESTINATION_TAG) destination = tag.contains(DESTINATION_TAG)
? LinkTarget.CODEC.parse(NbtOps.INSTANCE, tag.get(DESTINATION_TAG)).result().orElse(null) : null; ? LinkTarget.CODEC.parse(NbtOps.INSTANCE, tag.get(DESTINATION_TAG)).result().orElse(null) : null;
skyColor = tag.contains(SKY_COLOR_TAG) ? tag.getInt(SKY_COLOR_TAG) & 0xFFFFFF : 0xFFFFFF;
restoreView = true; restoreView = true;
} }
@@ -72,6 +84,7 @@ public final class BookReceptacleBlockEntity extends BlockEntity {
if (!book.isEmpty()) tag.put(BOOK_TAG, book.save(registries)); if (!book.isEmpty()) tag.put(BOOK_TAG, book.save(registries));
if (destination != null) LinkTarget.CODEC.encodeStart(NbtOps.INSTANCE, destination) if (destination != null) LinkTarget.CODEC.encodeStart(NbtOps.INSTANCE, destination)
.result().ifPresent(encoded -> tag.put(DESTINATION_TAG, encoded)); .result().ifPresent(encoded -> tag.put(DESTINATION_TAG, encoded));
if (destination != null) tag.putInt(SKY_COLOR_TAG, skyColor);
} }
@Override public Packet<ClientGamePacketListener> getUpdatePacket() { @Override public Packet<ClientGamePacketListener> getUpdatePacket() {
@@ -28,7 +28,8 @@ public record AgeDefinition(
public static final ResourceLocation RANDOM_VANILLA_PROFILE = ResourceLocation.fromNamespaceAndPath( public static final ResourceLocation RANDOM_VANILLA_PROFILE = ResourceLocation.fromNamespaceAndPath(
Rivencraft.MOD_ID, "random_vanilla" Rivencraft.MOD_ID, "random_vanilla"
); );
public static final int CURRENT_GENERATION_VERSION = 16; public static final int LEGACY_GENERATOR_CUTOFF = 16;
public static final int CURRENT_GENERATION_VERSION = AgeSkyProfile.RANDOM_SKY_GENERATION_VERSION;
public AgeDefinition { public AgeDefinition {
Objects.requireNonNull(id, "id"); Objects.requireNonNull(id, "id");
@@ -352,7 +352,7 @@ public final class AgeDimensionService {
server.registryAccess().lookupOrThrow(Registries.STRUCTURE_SET), server.registryAccess().lookupOrThrow(Registries.STRUCTURE_SET),
server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE) server.registryAccess().lookupOrThrow(Registries.PLACED_FEATURE)
); );
return age.generationVersion() < AgeDefinition.CURRENT_GENERATION_VERSION return age.generationVersion() < AgeDefinition.LEGACY_GENERATOR_CUTOFF
? new FlatLevelSource(settings) : new AgeFlatChunkGenerator(biomeSource, settings, features, age.seed(), libraryAgeSymbols); ? new FlatLevelSource(settings) : new AgeFlatChunkGenerator(biomeSource, settings, features, age.seed(), libraryAgeSymbols);
} }
if (profile.terrain() == AgeRuntimeProfile.Terrain.VOID) { if (profile.terrain() == AgeRuntimeProfile.Terrain.VOID) {
@@ -362,7 +362,7 @@ public final class AgeDimensionService {
List.of() List.of()
); );
settings.updateLayers(); settings.updateLayers();
return age.generationVersion() < AgeDefinition.CURRENT_GENERATION_VERSION return age.generationVersion() < AgeDefinition.LEGACY_GENERATOR_CUTOFF
? new FlatLevelSource(settings) : new AgeFlatChunkGenerator(biomeSource, settings, features, age.seed(), libraryAgeSymbols); ? new FlatLevelSource(settings) : new AgeFlatChunkGenerator(biomeSource, settings, features, age.seed(), libraryAgeSymbols);
} }
@@ -372,7 +372,7 @@ public final class AgeDimensionService {
NoiseGeneratorSettings baseSettings = noiseSettings.getOrThrow(settingsKey).value(); NoiseGeneratorSettings baseSettings = noiseSettings.getOrThrow(settingsKey).value();
Holder<NoiseGeneratorSettings> settings = Holder.direct(withBiomeMaterialRules(baseSettings)); Holder<NoiseGeneratorSettings> settings = Holder.direct(withBiomeMaterialRules(baseSettings));
boolean caveTerrain = AgeBiomeSelection.isCave(profile); boolean caveTerrain = AgeBiomeSelection.isCave(profile);
return age.generationVersion() < AgeDefinition.CURRENT_GENERATION_VERSION return age.generationVersion() < AgeDefinition.LEGACY_GENERATOR_CUTOFF
? new NoiseBasedChunkGenerator(biomeSource, settings) ? new NoiseBasedChunkGenerator(biomeSource, settings)
: new AgeFeatureChunkGenerator(biomeSource, settings, features, age.seed(), : new AgeFeatureChunkGenerator(biomeSource, settings, features, age.seed(),
caveTerrain, caveTerrain ? TerraBlenderBiomes.nether(server.registryAccess()) : List.of(), caveTerrain, caveTerrain ? TerraBlenderBiomes.nether(server.registryAccess()) : List.of(),
@@ -455,7 +455,7 @@ public final class AgeDimensionService {
AgeDefinition age, AgeDefinition age,
AgeRuntimeProfile profile AgeRuntimeProfile profile
) { ) {
if (age.generationVersion() < AgeDefinition.CURRENT_GENERATION_VERSION) { if (age.generationVersion() < AgeDefinition.LEGACY_GENERATOR_CUTOFF) {
return legacyBiomeSource(server, age, profile); return legacyBiomeSource(server, age, profile);
} }
AgeBiomeSelection selection = age.plan() != null && age.plan().biomeSelection() != null AgeBiomeSelection selection = age.plan() != null && age.plan().biomeSelection() != null
@@ -558,7 +558,9 @@ public final class AgeDimensionService {
DimensionType base = server.registryAccess().lookupOrThrow(Registries.DIMENSION_TYPE) DimensionType base = server.registryAccess().lookupOrThrow(Registries.DIMENSION_TYPE)
.getOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, profile.dimensionType())).value(); .getOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, profile.dimensionType())).value();
AgeTraits.Lighting lighting = age.traits().lighting(); AgeTraits.Lighting lighting = age.traits().lighting();
boolean hasSkyLight = base.hasSkyLight() && lighting != AgeTraits.Lighting.DARK; AgeSkyProfile sky = AgeSkyProfile.forAge(age);
boolean hasSkyLight = (base.hasSkyLight() || !sky.bodies().isEmpty())
&& lighting != AgeTraits.Lighting.DARK && !age.caveTerrain();
float ambientLight = lighting == AgeTraits.Lighting.BRIGHT ? 0.25F : 0.0F; float ambientLight = lighting == AgeTraits.Lighting.BRIGHT ? 0.25F : 0.0F;
return new DimensionType( return new DimensionType(
base.fixedTime(), base.fixedTime(),
@@ -573,7 +575,9 @@ public final class AgeDimensionService {
base.height(), base.height(),
base.logicalHeight(), base.logicalHeight(),
base.infiniburn(), base.infiniburn(),
AgeEnvironmentCompiler.skyEffects(age.plan(), base.effectsLocation()), age.plan() == null && sky.custom()
? AgeEnvironmentCompiler.CUSTOM_SKY_EFFECTS
: AgeEnvironmentCompiler.skyEffects(age.plan(), base.effectsLocation()),
ambientLight, ambientLight,
base.monsterSettings() base.monsterSettings()
); );
@@ -2,10 +2,16 @@ package net.dnorberg.rivencraft.book;
import net.dnorberg.rivencraft.writing.SymbolSemantics; import net.dnorberg.rivencraft.writing.SymbolSemantics;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.dimension.BuiltinDimensionTypes; import net.minecraft.world.level.dimension.BuiltinDimensionTypes;
/** Resolves written environment pages without changing the seeded page-free Age path. */ /** Resolves written environment pages without changing the seeded page-free Age path. */
public final class AgeEnvironmentCompiler { public final class AgeEnvironmentCompiler {
public static final ResourceLocation CUSTOM_SKY_EFFECTS = ResourceLocation.fromNamespaceAndPath(
"rivencraft", "custom_sky");
private AgeEnvironmentCompiler() {} private AgeEnvironmentCompiler() {}
public static AgeTraits apply(AgeGenerationPlan plan, AgeTraits seeded) { public static AgeTraits apply(AgeGenerationPlan plan, AgeTraits seeded) {
@@ -19,6 +25,7 @@ public final class AgeEnvironmentCompiler {
} else { } else {
applied = applyLegacy(plan, seeded); applied = applyLegacy(plan, seeded);
} }
AgeSkyProfile.compile(plan, applied);
return forTerrain(applied, plan.runtimeProfile()); return forTerrain(applied, plan.runtimeProfile());
} }
@@ -67,8 +74,7 @@ public final class AgeEnvironmentCompiler {
phaseDegrees = writtenPhase ? circularMidpoint(phaseDegrees, next) : next; phaseDegrees = writtenPhase ? circularMidpoint(phaseDegrees, next) : next;
writtenPhase = true; writtenPhase = true;
} }
case ANGLE_MODIFIER -> throw new IllegalArgumentException( case ANGLE_MODIFIER -> { } // Consumed by the next independently rendered celestial.
"Sun angle pages require a custom sky renderer and are not supported yet");
default -> { } default -> { }
} }
} }
@@ -81,6 +87,8 @@ public final class AgeEnvironmentCompiler {
/** Vanilla effects are the sky styles the current renderer can honor exactly. */ /** Vanilla effects are the sky styles the current renderer can honor exactly. */
public static ResourceLocation skyEffects(AgeGenerationPlan plan, ResourceLocation fallback) { public static ResourceLocation skyEffects(AgeGenerationPlan plan, ResourceLocation fallback) {
if (plan == null) return fallback; if (plan == null) return fallback;
if (skyColor(plan) != -1 || staticSky(plan) || AgeSkyProfile.hasCustomCelestials(plan))
return CUSTOM_SKY_EFFECTS;
return plan.activeSymbols().stream() return plan.activeSymbols().stream()
.filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.SKY) .filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.SKY)
.map(symbol -> ((SymbolSemantics.ChoiceValue) symbol.semantics().value()).id()) .map(symbol -> ((SymbolSemantics.ChoiceValue) symbol.semantics().value()).id())
@@ -92,6 +100,52 @@ public final class AgeEnvironmentCompiler {
}).findFirst().orElse(fallback); }).findFirst().orElse(fallback);
} }
public static boolean staticSky(AgeGenerationPlan plan) {
return plan != null && plan.activeSymbols().stream().anyMatch(symbol ->
symbol.semantics().role() == SymbolSemantics.Role.SKY
&& ((SymbolSemantics.ChoiceValue) symbol.semantics().value()).id().equals("static"));
}
/** An authored color is fixed; -1 means the biome or selected vanilla sky supplies it. */
public static int skyColor(AgeGenerationPlan plan) {
if (plan == null) return -1;
return AgeSkyProfile.mixColors(plan.activeSymbols().stream()
.filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.SKY_COLOR)
.map(symbol -> ((SymbolSemantics.ChoiceValue) symbol.semantics().value()).id())
.map(AgeSkyProfile::color).toList());
}
public static int selectedSkyColor(AgeGenerationPlan plan, ResourceLocation fallback, int biomeSkyColor) {
int authored = skyColor(plan);
if (authored != -1) return authored;
if (staticSky(plan)) return 0xFF929292;
ResourceLocation effects = skyEffects(plan, fallback);
return previewSkyColor(effects, biomeSkyColor);
}
/** Shared server answer for book previews and the fallback crystal portal surface. */
public static int previewSkyColor(ServerLevel level, BlockPos origin) {
int biomeColor = level.getBiome(origin).value().getSkyColor();
var age = AgeRegistrySavedData.get(level).definitions().stream()
.filter(candidate -> AgeDimensionService.dimensionKey(candidate).equals(level.dimension()))
.findFirst();
if (age.isEmpty()) return previewSkyColor(level.dimensionType().effectsLocation(), biomeColor);
var definition = age.orElseThrow();
AgeSkyProfile sky = AgeSkyProfile.forAge(definition);
if (definition.plan() == null && definition.generationVersion() >= AgeSkyProfile.RANDOM_SKY_GENERATION_VERSION) {
if (sky.color() != -1) return sky.color();
if (sky.style() == AgeSkyProfile.Style.STATIC) return 0xFF929292;
}
var profile = definition.plan() == null
? AgeRuntimeProfiles.resolve(definition.generationProfile())
: definition.plan().runtimeProfile();
ResourceLocation baseEffects = level.getServer().registryAccess()
.lookupOrThrow(Registries.DIMENSION_TYPE)
.getOrThrow(ResourceKey.create(Registries.DIMENSION_TYPE, profile.dimensionType()))
.value().effectsLocation();
return selectedSkyColor(definition.plan(), baseEffects, biomeColor);
}
public static boolean hasWrittenPhase(AgeGenerationPlan plan) { public static boolean hasWrittenPhase(AgeGenerationPlan plan) {
if (plan == null) return false; if (plan == null) return false;
if (plan.version() >= 4) return orderedCelestial(plan, null).writtenPhase(); if (plan.version() >= 4) return orderedCelestial(plan, null).writtenPhase();
@@ -188,7 +188,7 @@ public record AgeGenerationPlan(int version, long ageSeed, OrderedWriting writin
String raw = item.getString("Value"); String raw = item.getString("Value");
SymbolSemantics.Value value = switch (role) { SymbolSemantics.Value value = switch (role) {
case TERRAIN, BIOME, BLOCK, FEATURE -> new SymbolSemantics.ResourceValue(ResourceLocation.parse(raw)); case TERRAIN, BIOME, BLOCK, FEATURE -> new SymbolSemantics.ResourceValue(ResourceLocation.parse(raw));
case PERIOD_MODIFIER, ANGLE_MODIFIER, PHASE_MODIFIER -> case PERIOD_MODIFIER, ANGLE_MODIFIER, PHASE_MODIFIER, SIZE_MODIFIER ->
new SymbolSemantics.NumberValue(Double.parseDouble(raw)); new SymbolSemantics.NumberValue(Double.parseDouble(raw));
default -> new SymbolSemantics.ChoiceValue(raw); default -> new SymbolSemantics.ChoiceValue(raw);
}; };
@@ -51,6 +51,10 @@ public record AgeInstability(int version, int score, List<Contribution> breakdow
} }
static AgeInstability compute(AgeGenerationPlan plan, AgeTraits traits, RegistryAccess registries) { static AgeInstability compute(AgeGenerationPlan plan, AgeTraits traits, RegistryAccess registries) {
return compute(plan, traits, registries, Long.MIN_VALUE);
}
static AgeInstability compute(AgeGenerationPlan plan, AgeTraits traits, RegistryAccess registries, long ageSeed) {
Objects.requireNonNull(traits, "traits"); Objects.requireNonNull(traits, "traits");
List<Contribution> contributions = new ArrayList<>(); List<Contribution> contributions = new ArrayList<>();
if (plan == null) { if (plan == null) {
@@ -82,6 +86,17 @@ public record AgeInstability(int version, int score, List<Contribution> breakdow
contributions.add(new Contribution("bright_lighting", -1, BRIGHT_LIGHTING_COST, contributions.add(new Contribution("bright_lighting", -1, BRIGHT_LIGHTING_COST,
"Seeded bright lighting")); "Seeded bright lighting"));
} }
AgeSkyProfile sky = plan == null && ageSeed != Long.MIN_VALUE ? AgeSkyProfile.random(ageSeed, traits)
: plan == null ? new AgeSkyProfile(AgeSkyProfile.Style.INHERITED, -1, List.of())
: AgeSkyProfile.compile(plan, traits);
int seenSuns = 0;
for (AgeSkyProfile.Body body : sky.bodies()) {
if (body.kind() != AgeSkyProfile.Kind.SUN) continue;
if (++seenSuns <= 2) continue;
int points = 1000 * (seenSuns - 2);
contributions.add(new Contribution("extra_sun", -1, points,
"Sun " + seenSuns + " adds growing instability"));
}
return new AgeInstability(CURRENT_VERSION, return new AgeInstability(CURRENT_VERSION,
contributions.stream().mapToInt(Contribution::points).sum(), contributions); contributions.stream().mapToInt(Contribution::points).sum(), contributions);
} }
@@ -94,7 +109,8 @@ public record AgeInstability(int version, int score, List<Contribution> breakdow
if (cave) { if (cave) {
var role = symbol.semantics().role(); var role = symbol.semantics().role();
if (switch (role) { if (switch (role) {
case WEATHER, SEASON, CELESTIAL, PERIOD_MODIFIER, PHASE_MODIFIER, ANGLE_MODIFIER -> true; case WEATHER, SEASON, CELESTIAL, PERIOD_MODIFIER, PHASE_MODIFIER,
ANGLE_MODIFIER, DIRECTION_MODIFIER -> true;
default -> false; default -> false;
}) { }) {
contributions.add(new Contribution("cave_ignored_" + role.id(), symbol.index(), contributions.add(new Contribution("cave_ignored_" + role.id(), symbol.index(),
@@ -179,7 +179,7 @@ public final class AgeRegistrySavedData extends SavedData {
AgeTraits effectiveTraits = plan == null AgeTraits effectiveTraits = plan == null
? AgeEnvironmentCompiler.forTerrain(traits, AgeRuntimeProfiles.resolve(seedProfile)) ? AgeEnvironmentCompiler.forTerrain(traits, AgeRuntimeProfiles.resolve(seedProfile))
: AgeEnvironmentCompiler.apply(plan, traits); : AgeEnvironmentCompiler.apply(plan, traits);
AgeInstability instability = AgeInstability.compute(plan, effectiveTraits, registries); AgeInstability instability = AgeInstability.compute(plan, effectiveTraits, registries, seed);
AgeInstabilityPlan instabilityPlan = AgeInstabilityPlan.select(instability, seed); AgeInstabilityPlan instabilityPlan = AgeInstabilityPlan.select(instability, seed);
AgeDefinition created = new AgeDefinition( AgeDefinition created = new AgeDefinition(
reference.id(), reference.id(),
@@ -0,0 +1,181 @@
package net.dnorberg.rivencraft.book;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import net.dnorberg.rivencraft.writing.SymbolSemantics;
/** The ordered, deterministic sky specification for an Age. No client data is trusted here. */
public record AgeSkyProfile(Style style, int color, List<Body> bodies) {
public static final int RANDOM_SKY_GENERATION_VERSION = 17;
private static final int[] COLORS = {0xB74A50, 0xD9834B, 0xDBC574, 0x5AAB80,
0x6FA8DF, 0x9870B9, 0xD5DBE1, 0x252531};
public AgeSkyProfile {
bodies = List.copyOf(bodies);
long suns = bodies.stream().filter(body -> body.kind() == Kind.SUN).count();
long moons = bodies.size() - suns;
if (suns > 10 || moons > 10) throw new IllegalArgumentException("An Age may have at most ten suns and ten moons");
}
public static AgeSkyProfile forAge(AgeDefinition age) {
if (age.plan() != null) return compile(age.plan(), age.traits());
if (age.generationVersion() >= RANDOM_SKY_GENERATION_VERSION)
return random(age.seed(), age.traits());
return new AgeSkyProfile(Style.INHERITED, -1, List.of());
}
public static AgeSkyProfile random(long seed, AgeTraits traits) {
Random random = new Random(seed ^ 0x534B5950524F464CL);
Style style = Style.values()[random.nextInt(Style.values().length)];
int color = randomColor(random);
int suns = random.nextInt(4);
int moons = random.nextInt(4);
List<Body> bodies = new ArrayList<>();
for (int i = 0; i < suns; i++)
bodies.add(new Body(Kind.SUN, randomColor(random),
0.75F + random.nextFloat(), random.nextFloat() * 360.0F,
traits.celestialPeriod(), random.nextFloat(), random.nextBoolean()));
for (int i = 0; i < moons; i++)
bodies.add(new Body(Kind.MOON, randomColor(random),
0.75F + random.nextFloat(), random.nextFloat() * 360.0F,
traits.celestialPeriod(), random.nextFloat(), random.nextBoolean()));
return new AgeSkyProfile(style, color, bodies);
}
public static AgeSkyProfile compile(AgeGenerationPlan plan, AgeTraits traits) {
if (plan == null) return new AgeSkyProfile(Style.INHERITED, -1, List.of());
Style style = Style.INHERITED;
int color = AgeEnvironmentCompiler.skyColor(plan);
boolean customBodies = hasCustomCelestials(plan);
List<Body> bodies = new ArrayList<>();
Set<Integer> active = new HashSet<>(plan.activeSymbolIndices());
AgeTraits seeded = AgeTraits.select(plan.ageSeed());
double period = 1.0D;
float angle = seeded.celestialAngle();
float phase = seeded.celestialPhase();
float size = 1.0F;
List<Integer> bodyColors = new ArrayList<>();
boolean clockwise = true;
for (AgeGenerationPlan.ResolvedSymbol symbol : plan.symbols()) {
if (!active.contains(symbol.index())) continue;
SymbolSemantics semantics = symbol.semantics();
switch (semantics.role()) {
case SKY -> style = switch (((SymbolSemantics.ChoiceValue) semantics.value()).id()) {
case "overworld" -> Style.OVERWORLD;
case "nether" -> Style.NETHER;
case "end" -> Style.END;
case "static" -> Style.STATIC;
default -> throw new IllegalArgumentException("Unknown sky style");
};
case PERIOD_MODIFIER -> period = ((SymbolSemantics.NumberValue) semantics.value()).amount();
case ANGLE_MODIFIER -> angle = (float) ((SymbolSemantics.NumberValue) semantics.value()).amount();
case PHASE_MODIFIER -> phase = (float) (((SymbolSemantics.NumberValue) semantics.value()).amount() / 360.0D);
case SIZE_MODIFIER -> size = (float) ((SymbolSemantics.NumberValue) semantics.value()).amount();
case CELESTIAL_COLOR -> bodyColors.add(color(
((SymbolSemantics.ChoiceValue) semantics.value()).id()));
case DIRECTION_MODIFIER -> clockwise = ((SymbolSemantics.ChoiceValue) semantics.value())
.id().equals("clockwise");
case CELESTIAL -> {
String choice = ((SymbolSemantics.ChoiceValue) semantics.value()).id();
int bodyColor = bodyColors.isEmpty() ? 0xFFFFFF : mixColors(bodyColors);
if (choice.equals("normal_time")) {
if (customBodies) {
bodies.add(new Body(Kind.SUN, bodyColor, size, angle, period, phase, clockwise));
bodies.add(new Body(Kind.MOON, bodyColor, 1.0F, angle, period,
(phase + 0.5F) % 1.0F, clockwise));
}
} else {
bodies.add(new Body(choice.equals("sun") ? Kind.SUN : Kind.MOON,
bodyColor, size, angle, period, phase, clockwise));
}
period = 1.0D;
angle = seeded.celestialAngle();
phase = seeded.celestialPhase();
size = 1.0F;
bodyColors.clear();
clockwise = true;
}
default -> { }
}
}
// Missing branches are seed-completed, including the celestial branch.
// An explicit sky or sky-color page does not suppress that completion.
if (plan.activeSymbols().stream().noneMatch(symbol ->
symbol.semantics().role() == SymbolSemantics.Role.CELESTIAL)) {
bodies.addAll(random(plan.ageSeed(), seeded).bodies());
}
return new AgeSkyProfile(style, color, bodies);
}
public static int mixColors(List<Integer> colors) {
if (colors.isEmpty()) return -1;
long red = 0, green = 0, blue = 0;
for (int color : colors) {
red += color >> 16 & 255;
green += color >> 8 & 255;
blue += color & 255;
}
int count = colors.size();
return (int) ((red + count / 2) / count) << 16
| (int) ((green + count / 2) / count) << 8
| (int) ((blue + count / 2) / count);
}
private static int randomColor(Random random) {
int count = 1 + random.nextInt(3);
List<Integer> colors = new ArrayList<>(count);
for (int i = 0; i < count; i++) colors.add(COLORS[random.nextInt(COLORS.length)]);
return mixColors(colors);
}
public static boolean hasCustomCelestials(AgeGenerationPlan plan) {
return plan != null && plan.activeSymbols().stream().anyMatch(symbol ->
symbol.semantics().role() == SymbolSemantics.Role.CELESTIAL
&& !((SymbolSemantics.ChoiceValue) symbol.semantics().value()).id().equals("normal_time")
|| symbol.semantics().role() == SymbolSemantics.Role.CELESTIAL_COLOR
|| symbol.semantics().role() == SymbolSemantics.Role.SIZE_MODIFIER
|| symbol.semantics().role() == SymbolSemantics.Role.DIRECTION_MODIFIER);
}
public boolean custom() {
return style == Style.STATIC || color != -1 || !bodies.isEmpty();
}
/** Vanilla has one skylight channel; combine visible bodies into that channel. */
public float lightAt(double dayTime) {
float sunlight = 0.0F;
float moonlight = 0.0F;
for (Body body : bodies) {
double orbit = body.period() == 0.0D ? body.phase()
: (body.clockwise() ? 1.0D : -1.0D) * dayTime
/ (24000.0D * body.period()) + body.phase();
float altitude = (float) Math.cos(orbit * Math.PI * 2.0D);
if (altitude <= 0.0F) continue;
if (body.kind() == Kind.SUN) sunlight += altitude;
else moonlight += altitude * 0.18F;
}
return Math.min(1.0F, sunlight > 0.0F ? sunlight : moonlight);
}
public static int color(String name) {
return switch (name) {
case "red" -> COLORS[0];
case "orange" -> COLORS[1];
case "yellow" -> COLORS[2];
case "green" -> COLORS[3];
case "blue" -> COLORS[4];
case "purple" -> COLORS[5];
case "white" -> COLORS[6];
case "black" -> COLORS[7];
default -> throw new IllegalArgumentException("Unknown sky color " + name);
};
}
public enum Style { INHERITED, OVERWORLD, NETHER, END, STATIC }
public enum Kind { SUN, MOON }
public record Body(Kind kind, int color, float size, float angle,
double period, float phase, boolean clockwise) { }
}
@@ -52,9 +52,11 @@ public final class EnvironmentWritingGameTests {
public static void environmentPagesEnterCreativeAndJeiFeed(GameTestHelper helper) { public static void environmentPagesEnterCreativeAndJeiFeed(GameTestHelper helper) {
Set<String> expected = Set.of( Set<String> expected = Set.of(
"lighting/normal", "lighting/dark", "lighting/bright", "lighting/normal", "lighting/dark", "lighting/bright",
"sky/overworld", "sky/nether", "sky/end", "sky/overworld", "sky/nether", "sky/end", "sky/static", "sky_color/purple",
"weather/normal", "weather/fast", "weather/slow", "weather/off", "weather/normal", "weather/fast", "weather/slow", "weather/off",
"weather/always", "weather/storm", "celestial/normal_time", "weather/always", "weather/storm", "celestial/normal_time", "celestial/sun",
"celestial/moon", "celestial_color/red", "size/large", "angle/east",
"direction/clockwise", "direction/counterclockwise",
"season/no_seasons", "season/seasons", "season/eternal_winter", "season/no_seasons", "season/seasons", "season/eternal_winter",
"season/eternal_summer", "season/eternal_fall", "season/eternal_spring", "season/eternal_summer", "season/eternal_fall", "season/eternal_spring",
"period/zero", "period/half", "period/full", "period/double", "period/zero", "period/half", "period/full", "period/double",
@@ -135,6 +137,104 @@ public final class EnvironmentWritingGameTests {
helper.succeed(); helper.succeed();
} }
@GameTest(template = "empty")
public static void authoredStaticSkyColorSurvivesWritingAndFeedsPreview(GameTestHelper helper) {
OrderedWriting writing = new OrderedWriting(List.of(
WritingEntry.linkPanel(), page("terrain/standard"), page("sky/static"),
page("sky_color/purple"), page("celestial/normal_time")));
AgeTraits seeded = AgeTraits.select(44L);
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 44L, AgeGenerationProfiles.STANDARD,
seeded, helper.getLevel().registryAccess());
helper.assertTrue(AgeEnvironmentCompiler.staticSky(plan),
"The authored Static Sky page must survive Age compilation");
helper.assertValueEqual(AgeEnvironmentCompiler.skyEffects(plan, BuiltinDimensionTypes.OVERWORLD_EFFECTS),
AgeEnvironmentCompiler.CUSTOM_SKY_EFFECTS,
"The static sky must select Rivencraft's custom renderer");
helper.assertValueEqual(AgeEnvironmentCompiler.selectedSkyColor(plan,
BuiltinDimensionTypes.OVERWORLD_EFFECTS, 0xFF77AAFF),
0x9870B9, "The authored color must tint the sky and preview consistently");
AgeGenerationPlan restored = AgeGenerationPlan.load(plan.save());
helper.assertTrue(AgeEnvironmentCompiler.staticSky(restored),
"A saved Age must retain its static sky choice");
helper.assertValueEqual(AgeEnvironmentCompiler.skyColor(restored), 0x9870B9,
"A saved Age must retain its sky color choice");
AgeGenerationPlan coloredEnd = AgeGenerationPlan.compile(new OrderedWriting(List.of(
WritingEntry.linkPanel(), page("terrain/standard"), page("sky/end"),
page("sky_color/red"), page("celestial/normal_time"))),
45L, AgeGenerationProfiles.STANDARD, AgeTraits.select(45L),
helper.getLevel().registryAccess());
helper.assertValueEqual(AgeEnvironmentCompiler.skyEffects(coloredEnd,
BuiltinDimensionTypes.OVERWORLD_EFFECTS),
AgeEnvironmentCompiler.CUSTOM_SKY_EFFECTS,
"A color page must make non-Overworld skies renderable by the custom sky effect");
helper.assertValueEqual(AgeEnvironmentCompiler.selectedSkyColor(coloredEnd,
BuiltinDimensionTypes.OVERWORLD_EFFECTS, 0xFF77AAFF),
0xB74A50, "A color page must override the selected sky type's default tint");
helper.assertTrue(PageItem.bundledCreativeSymbols().contains("sky/static")
&& PageItem.bundledCreativeSymbols().contains("sky_color/purple"),
"New sky pages must be available through the Creative and JEI page catalog");
helper.succeed();
}
@GameTest(template = "empty")
public static void skyAndCelestialColorsBlendWithoutRequiringBodies(GameTestHelper helper) {
OrderedWriting writing = new OrderedWriting(List.of(
WritingEntry.linkPanel(), page("terrain/standard"), page("sky/overworld"),
page("sky_color/red"), page("sky_color/blue"),
page("celestial_color/yellow"), page("celestial_color/blue"), page("celestial/sun")));
helper.assertTrue(WritingGrammar.parse(writing).canCreate(),
"Two sky colors and two colors for one sun must be valid writing");
AgeTraits traits = AgeTraits.select(46L);
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 46L,
AgeGenerationProfiles.STANDARD, traits, helper.getLevel().registryAccess());
int skyColor = AgeSkyProfile.mixColors(List.of(
AgeSkyProfile.color("red"), AgeSkyProfile.color("blue")));
int sunColor = AgeSkyProfile.mixColors(List.of(
AgeSkyProfile.color("yellow"), AgeSkyProfile.color("blue")));
helper.assertValueEqual(AgeEnvironmentCompiler.skyColor(plan), skyColor,
"Sky color pages must blend without discarding either page");
AgeSkyProfile profile = AgeSkyProfile.compile(plan, traits);
helper.assertValueEqual(profile.bodies().size(), 1, "The written Age has exactly one sun");
helper.assertValueEqual(profile.bodies().getFirst().color(), sunColor,
"Celestial colors must blend for the following body");
helper.assertValueEqual(AgeSkyProfile.compile(AgeGenerationPlan.load(plan.save()), traits), profile,
"Blended colors must survive the saved writing plan");
AgeSkyProfile bodyless = new AgeSkyProfile(AgeSkyProfile.Style.OVERWORLD, skyColor, List.of());
helper.assertTrue(bodyless.custom() && bodyless.bodies().isEmpty(),
"An Age can have an authored sky without any celestial bodies");
helper.succeed();
}
@GameTest(template = "empty")
public static void randomAndAuthoredCelestialsKeepIndependentOrbits(GameTestHelper helper) {
AgeTraits traits = AgeTraits.select(12345L);
AgeSkyProfile first = AgeSkyProfile.random(12345L, traits);
helper.assertValueEqual(first, AgeSkyProfile.random(12345L, traits),
"A random Age must recreate the same sky from its saved seed");
helper.assertTrue(first.bodies().stream().filter(body -> body.kind() == AgeSkyProfile.Kind.SUN).count() <= 10
&& first.bodies().stream().filter(body -> body.kind() == AgeSkyProfile.Kind.MOON).count() <= 10,
"Random Ages must obey the same celestial limits as authored Ages");
OrderedWriting writing = new OrderedWriting(List.of(
WritingEntry.linkPanel(), page("terrain/standard"), page("sky/end"),
page("angle/east"), page("direction/counterclockwise"), page("celestial_color/red"),
page("size/large"), page("period/half"), page("celestial/sun"),
page("direction/clockwise"), page("celestial/moon")));
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, 12345L, AgeGenerationProfiles.STANDARD,
traits, helper.getLevel().registryAccess());
AgeSkyProfile sky = AgeSkyProfile.compile(plan, AgeEnvironmentCompiler.apply(plan, traits));
helper.assertValueEqual(sky.bodies().size(), 2, "Both written celestial bodies must be retained");
helper.assertValueEqual(sky.bodies().getFirst().angle(), 90.0F,
"The first body's compass angle must be independent");
helper.assertFalse(sky.bodies().getFirst().clockwise(),
"The first body should move counterclockwise");
helper.assertTrue(sky.bodies().getLast().clockwise(),
"The second body should move clockwise");
helper.assertValueEqual(AgeSkyProfile.compile(AgeGenerationPlan.load(plan.save()), traits), sky,
"Body angle and direction must survive Age-plan serialization");
helper.succeed();
}
@GameTest(template = "empty") @GameTest(template = "empty")
public static void writtenEnvironmentPersistsAndPageFreeAgesStaySeeded(GameTestHelper helper) { public static void writtenEnvironmentPersistsAndPageFreeAgesStaySeeded(GameTestHelper helper) {
OrderedWriting writing = new OrderedWriting(List.of( OrderedWriting writing = new OrderedWriting(List.of(
@@ -2,23 +2,58 @@ package net.dnorberg.rivencraft.client;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import net.dnorberg.rivencraft.book.AgeSkyProfile;
import net.dnorberg.rivencraft.network.AgeCelestialAnglePayload; import net.dnorberg.rivencraft.network.AgeCelestialAnglePayload;
import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
import net.neoforged.neoforge.network.handling.IPayloadContext; import net.neoforged.neoforge.network.handling.IPayloadContext;
import net.neoforged.neoforge.network.PacketDistributor;
import net.dnorberg.rivencraft.network.RequestAgeSkyPayload;
/** Client cache used by both the active world and remote preview renderers. */ /** Client cache used by both the active world and remote preview renderers. */
public final class ClientAgeCelestialAngles { public final class ClientAgeCelestialAngles {
private static final Map<ResourceKey<Level>, Float> ANGLES = new ConcurrentHashMap<>(); private static final Map<ResourceKey<Level>, Float> ANGLES = new ConcurrentHashMap<>();
private static final Map<ResourceKey<Level>, AgeSkyProfile> SKIES = new ConcurrentHashMap<>();
private static final Map<ResourceKey<Level>, Long> LAST_REQUEST = new ConcurrentHashMap<>();
private ClientAgeCelestialAngles() { private ClientAgeCelestialAngles() {
} }
public static void handle(AgeCelestialAnglePayload payload, IPayloadContext context) { public static void handle(AgeCelestialAnglePayload payload, IPayloadContext context) {
ANGLES.put(payload.dimension(), payload.angle()); ANGLES.put(payload.dimension(), payload.angle());
SKIES.put(payload.dimension(), payload.sky());
LAST_REQUEST.remove(payload.dimension());
}
public static void requestIfMissing(ResourceKey<Level> dimension) {
if (!dimension.location().getNamespace().equals("rivencraft") || SKIES.containsKey(dimension)) return;
long now = System.nanoTime();
Long previous = LAST_REQUEST.putIfAbsent(dimension, now);
if (previous != null && now - previous < 2_000_000_000L) return;
LAST_REQUEST.put(dimension, now);
PacketDistributor.sendToServer(new RequestAgeSkyPayload(dimension));
}
public static void clear() {
ANGLES.clear();
SKIES.clear();
LAST_REQUEST.clear();
} }
public static float get(ResourceKey<Level> dimension) { public static float get(ResourceKey<Level> dimension) {
return ANGLES.getOrDefault(dimension, 0.0F); return ANGLES.getOrDefault(dimension, 0.0F);
} }
public static int skyColor(ResourceKey<Level> dimension) {
return sky(dimension).color();
}
public static boolean staticSky(ResourceKey<Level> dimension) {
return sky(dimension).style() == AgeSkyProfile.Style.STATIC;
}
public static AgeSkyProfile sky(ResourceKey<Level> dimension) {
return SKIES.getOrDefault(dimension,
new AgeSkyProfile(AgeSkyProfile.Style.INHERITED, -1, java.util.List.of()));
}
} }
@@ -19,6 +19,12 @@ import net.neoforged.neoforge.common.NeoForge;
import net.neoforged.neoforge.client.event.EntityRenderersEvent; import net.neoforged.neoforge.client.event.EntityRenderersEvent;
import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent; import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent;
import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent; import net.neoforged.neoforge.client.event.RegisterClientReloadListenersEvent;
import net.neoforged.neoforge.client.event.RegisterDimensionSpecialEffectsEvent;
import net.neoforged.neoforge.client.event.RegisterColorHandlersEvent;
import net.neoforged.neoforge.client.event.ClientPlayerNetworkEvent;
import net.dnorberg.rivencraft.block.entity.CrystalPortalBlockEntity;
import net.dnorberg.rivencraft.block.entity.BookReceptacleBlockEntity;
import net.dnorberg.rivencraft.book.AgeEnvironmentCompiler;
import net.neoforged.neoforge.client.extensions.common.IClientItemExtensions; import net.neoforged.neoforge.client.extensions.common.IClientItemExtensions;
import net.neoforged.neoforge.client.extensions.common.RegisterClientExtensionsEvent; import net.neoforged.neoforge.client.extensions.common.RegisterClientExtensionsEvent;
import net.minecraft.client.renderer.BlockEntityWithoutLevelRenderer; import net.minecraft.client.renderer.BlockEntityWithoutLevelRenderer;
@@ -43,6 +49,23 @@ public final class ClientEvents {
event.registerBlockEntityRenderer(ModBlockEntities.STAR_FISSURE.get(), StarFissureRenderer::new); event.registerBlockEntityRenderer(ModBlockEntities.STAR_FISSURE.get(), StarFissureRenderer::new);
} }
@SubscribeEvent
public static void registerSkyEffects(RegisterDimensionSpecialEffectsEvent event) {
event.register(AgeEnvironmentCompiler.CUSTOM_SKY_EFFECTS, new RivencraftSkyEffects());
}
@SubscribeEvent
public static void registerPortalColor(RegisterColorHandlersEvent.Block event) {
event.register((state, level, pos, tintIndex) -> {
if (level == null || pos == null || tintIndex != 0
|| !(level.getBlockEntity(pos) instanceof CrystalPortalBlockEntity portal)
|| portal.receptacle() == null
|| !(level.getBlockEntity(portal.receptacle()) instanceof BookReceptacleBlockEntity holder)
|| holder.destination() == null) return 0xFFFFFF;
return holder.skyColor();
}, ModBlocks.CRYSTAL_PORTAL.get());
}
@SubscribeEvent @SubscribeEvent
public static void registerScreens(RegisterMenuScreensEvent event) { public static void registerScreens(RegisterMenuScreensEvent event) {
event.register(ModMenus.NOTEBOOK.get(), NotebookScreen::new); event.register(ModMenus.NOTEBOOK.get(), NotebookScreen::new);
@@ -99,6 +122,11 @@ public final class ClientEvents {
NeoForge.EVENT_BUS.addListener(ClientBookExploreOverlay::onClientTickPost); NeoForge.EVENT_BUS.addListener(ClientBookExploreOverlay::onClientTickPost);
NeoForge.EVENT_BUS.addListener(ClientBookExploreOverlay::onRenderGui); NeoForge.EVENT_BUS.addListener(ClientBookExploreOverlay::onRenderGui);
NeoForge.EVENT_BUS.addListener(ClientBookExploreOverlay::onMouseButton); NeoForge.EVENT_BUS.addListener(ClientBookExploreOverlay::onMouseButton);
NeoForge.EVENT_BUS.addListener(ClientEvents::onLogout);
}
private static void onLogout(ClientPlayerNetworkEvent.LoggingOut event) {
ClientAgeCelestialAngles.clear();
} }
} }
@@ -0,0 +1,145 @@
package net.dnorberg.rivencraft.client;
import com.mojang.blaze3d.systems.RenderSystem;
import com.mojang.blaze3d.vertex.BufferBuilder;
import com.mojang.blaze3d.vertex.BufferUploader;
import com.mojang.blaze3d.vertex.DefaultVertexFormat;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.Tesselator;
import com.mojang.blaze3d.vertex.VertexFormat;
import java.util.Random;
import net.dnorberg.rivencraft.book.AgeSkyProfile;
import net.minecraft.client.Camera;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.DimensionSpecialEffects;
import net.minecraft.client.renderer.FogRenderer;
import net.minecraft.client.renderer.GameRenderer;
import net.minecraft.world.phys.Vec3;
import org.joml.Matrix4f;
import com.mojang.math.Axis;
import net.minecraft.resources.ResourceLocation;
/** An authored sky can use vanilla celestial rendering or Legacy-style moving TV static. */
public final class RivencraftSkyEffects extends DimensionSpecialEffects {
private static final int GRID = 16;
private static final float RADIUS = 100.0F;
private static final ResourceLocation SUN = ResourceLocation.withDefaultNamespace("textures/environment/sun.png");
private static final ResourceLocation MOON = ResourceLocation.withDefaultNamespace("textures/environment/moon_phases.png");
public RivencraftSkyEffects() {
super(192.0F, true, SkyType.NORMAL, false, false);
}
@Override
public Vec3 getBrightnessDependentFogColor(Vec3 color, float brightness) {
return color.multiply(brightness * 0.94F + 0.06F,
brightness * 0.94F + 0.06F, brightness * 0.91F + 0.09F);
}
@Override
public boolean isFoggyAt(int x, int y) {
return false;
}
@Override
public boolean renderSky(ClientLevel level, int ticks, float partialTick, Matrix4f modelViewMatrix,
Camera camera, Matrix4f projectionMatrix, boolean isFoggy, Runnable setupFog) {
AgeSkyProfile sky = ClientAgeCelestialAngles.sky(level.dimension());
if (sky.style() != AgeSkyProfile.Style.STATIC && sky.color() == -1 && sky.bodies().isEmpty()
&& sky.style() != AgeSkyProfile.Style.NETHER && sky.style() != AgeSkyProfile.Style.END)
return false;
setupFog.run();
if (isFoggy) return true;
PoseStack pose = new PoseStack();
pose.mulPose(modelViewMatrix);
Matrix4f matrix = pose.last().pose();
Random random = new Random((level.getGameTime() + (long) ticks) / 2L);
int tint = sky.color();
if (tint == -1) tint = switch (sky.style()) {
case NETHER -> 0x6B2725;
case END -> 0x211835;
case STATIC -> 0x929292;
default -> level.getBiome(camera.getBlockPosition()).value().getSkyColor();
};
RenderSystem.depthMask(false);
RenderSystem.disableCull();
RenderSystem.setShader(GameRenderer::getPositionColorShader);
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
FogRenderer.setupNoFog();
BufferBuilder vertices = Tesselator.getInstance().begin(
VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION_COLOR);
for (int axis = 0; axis < 3; axis++) {
for (int sign : new int[]{-1, 1}) {
for (int x = 0; x < GRID; x++) {
for (int y = 0; y < GRID; y++) {
int noise = sky.style() == AgeSkyProfile.Style.STATIC
? 65 + random.nextInt(160) : 255;
int red = ((tint >> 16) & 255) * noise / 255;
int green = ((tint >> 8) & 255) * noise / 255;
int blue = (tint & 255) * noise / 255;
int color = 0xFF000000 | red << 16 | green << 8 | blue;
float left = -RADIUS + x * (2 * RADIUS / GRID);
float right = -RADIUS + (x + 1) * (2 * RADIUS / GRID);
float bottom = -RADIUS + y * (2 * RADIUS / GRID);
float top = -RADIUS + (y + 1) * (2 * RADIUS / GRID);
vertex(vertices, matrix, axis, sign, left, bottom, color);
vertex(vertices, matrix, axis, sign, right, bottom, color);
vertex(vertices, matrix, axis, sign, right, top, color);
vertex(vertices, matrix, axis, sign, left, top, color);
}
}
}
}
BufferUploader.drawWithShader(vertices.buildOrThrow());
if (!sky.bodies().isEmpty()) {
RenderSystem.enableBlend();
RenderSystem.defaultBlendFunc();
double time = level.getGameTime() + partialTick;
for (AgeSkyProfile.Body body : sky.bodies()) drawBody(pose, body, time);
RenderSystem.disableBlend();
}
setupFog.run();
RenderSystem.enableCull();
RenderSystem.depthMask(true);
return true;
}
private static void drawBody(PoseStack pose, AgeSkyProfile.Body body, double time) {
pose.pushPose();
pose.mulPose(Axis.YP.rotationDegrees(body.angle()));
double orbit = body.period() == 0 ? body.phase()
: (body.clockwise() ? 1.0D : -1.0D) * time
/ (24000.0D * body.period()) + body.phase();
pose.mulPose(Axis.XP.rotationDegrees((float) (orbit * 360.0D)));
float size = (body.kind() == AgeSkyProfile.Kind.SUN ? 30.0F : 20.0F) * body.size();
int color = body.color();
RenderSystem.setShader(GameRenderer::getPositionTexShader);
RenderSystem.setShaderTexture(0, body.kind() == AgeSkyProfile.Kind.SUN ? SUN : MOON);
RenderSystem.setShaderColor(((color >> 16) & 255) / 255.0F,
((color >> 8) & 255) / 255.0F, (color & 255) / 255.0F, 1.0F);
float uMax = body.kind() == AgeSkyProfile.Kind.SUN ? 1.0F : 0.25F;
float vMax = body.kind() == AgeSkyProfile.Kind.SUN ? 1.0F : 0.5F;
BufferBuilder vertices = Tesselator.getInstance().begin(VertexFormat.Mode.QUADS,
DefaultVertexFormat.POSITION_TEX);
Matrix4f matrix = pose.last().pose();
vertices.addVertex(matrix, -size, RADIUS - 1.0F, -size).setUv(0.0F, 0.0F);
vertices.addVertex(matrix, size, RADIUS - 1.0F, -size).setUv(uMax, 0.0F);
vertices.addVertex(matrix, size, RADIUS - 1.0F, size).setUv(uMax, vMax);
vertices.addVertex(matrix, -size, RADIUS - 1.0F, size).setUv(0.0F, vMax);
BufferUploader.drawWithShader(vertices.buildOrThrow());
RenderSystem.setShaderColor(1.0F, 1.0F, 1.0F, 1.0F);
pose.popPose();
}
private static void vertex(BufferBuilder vertices, Matrix4f matrix, int axis, int sign,
float x, float y, int color) {
float fixed = sign * RADIUS;
switch (axis) {
case 0 -> vertices.addVertex(matrix, fixed, x, y).setColor(color);
case 1 -> vertices.addVertex(matrix, x, fixed, y).setColor(color);
default -> vertices.addVertex(matrix, x, y, fixed).setColor(color);
}
}
}
@@ -3,11 +3,11 @@ package net.dnorberg.rivencraft.event;
import net.dnorberg.rivencraft.book.AgeDefinition; import net.dnorberg.rivencraft.book.AgeDefinition;
import net.dnorberg.rivencraft.book.AgeDimensionService; import net.dnorberg.rivencraft.book.AgeDimensionService;
import net.dnorberg.rivencraft.book.AgeRegistrySavedData; import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
import net.dnorberg.rivencraft.book.AgeSkyProfile;
import net.dnorberg.rivencraft.network.AgeCelestialAnglePayload; import net.dnorberg.rivencraft.network.AgeCelestialAnglePayload;
import net.minecraft.server.level.ServerPlayer; import net.minecraft.server.level.ServerPlayer;
import net.neoforged.neoforge.event.entity.player.PlayerEvent; import net.neoforged.neoforge.event.entity.player.PlayerEvent;
import net.neoforged.neoforge.network.PacketDistributor; import net.neoforged.neoforge.network.PacketDistributor;
import net.neoforged.neoforge.network.registration.NetworkRegistry;
/** Sends saved celestial angles when a client joins or enters an Age. */ /** Sends saved celestial angles when a client joins or enters an Age. */
public final class AgeCelestialAngleSync { public final class AgeCelestialAngleSync {
@@ -34,10 +34,8 @@ public final class AgeCelestialAngleSync {
} }
public static void send(ServerPlayer player, AgeDefinition age) { public static void send(ServerPlayer player, AgeDefinition age) {
if (!NetworkRegistry.hasChannel(player.connection, AgeCelestialAnglePayload.TYPE.id())) {
return;
}
PacketDistributor.sendToPlayer(player, new AgeCelestialAnglePayload( PacketDistributor.sendToPlayer(player, new AgeCelestialAnglePayload(
AgeDimensionService.dimensionKey(age), age.traits().celestialAngle())); AgeDimensionService.dimensionKey(age), age.traits().celestialAngle(),
AgeSkyProfile.forAge(age)));
} }
} }
@@ -0,0 +1,40 @@
package net.dnorberg.rivencraft.mixin;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.book.AgeDimensionService;
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
import net.dnorberg.rivencraft.book.AgeSkyProfile;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** Combines multiple celestial light sources into Minecraft's one skylight channel. */
@Mixin(Level.class)
public abstract class LevelSkyBrightnessMixin {
@Shadow private int skyDarken;
@Inject(method = "updateSkyBrightness", at = @At("TAIL"))
private void rivencraft$stackCelestialLight(CallbackInfo callback) {
if (!((Object) this instanceof ServerLevel level) || !level.dimensionType().hasSkyLight()) return;
// ServerLevel updates sky brightness in its constructor, before the
// MinecraftServer has registered even the Overworld level. Only an
// established Rivencraft Age can have a saved celestial profile.
if (!level.dimension().location().getNamespace().equals(Rivencraft.MOD_ID)
|| !level.dimension().location().getPath().startsWith("age/")
|| level.getServer().overworld() == null) return;
AgeRegistrySavedData.get(level).definitions().stream()
.filter(age -> AgeDimensionService.dimensionKey(age).equals(level.dimension()))
.findFirst().ifPresent(age -> {
AgeSkyProfile sky = AgeSkyProfile.forAge(age);
if (sky.bodies().isEmpty()) return;
float light = sky.lightAt(level.getDayTime());
float weather = (1.0F - level.getRainLevel(1.0F) * 5.0F / 16.0F)
* (1.0F - level.getThunderLevel(1.0F) * 5.0F / 16.0F);
skyDarken = (int) ((1.0F - light * weather) * 11.0F);
});
}
}
@@ -0,0 +1,40 @@
package net.dnorberg.rivencraft.mixin.client;
import net.dnorberg.rivencraft.client.ClientAgeCelestialAngles;
import net.dnorberg.rivencraft.book.AgeSkyProfile;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/** Keeps authored color pages visible in the sky without replacing vanilla sun and stars. */
@Mixin(ClientLevel.class)
public abstract class ClientLevelSkyColorMixin {
@Inject(method = "getSkyColor", at = @At("RETURN"), cancellable = true)
private void rivencraft$colorAuthoredSky(Vec3 position, float partialTick,
CallbackInfoReturnable<Vec3> result) {
ClientLevel level = (ClientLevel) (Object) this;
int color = ClientAgeCelestialAngles.skyColor(level.dimension());
if (color == -1 || ClientAgeCelestialAngles.staticSky(level.dimension())) return;
Vec3 natural = result.getReturnValue();
// Retain the vanilla day/night and precipitation darkening while replacing its hue.
AgeSkyProfile sky = ClientAgeCelestialAngles.sky(level.dimension());
double brightness = sky.bodies().isEmpty()
? Math.max(natural.x, Math.max(natural.y, natural.z))
: Math.max(0.08D, sky.lightAt(level.getDayTime() + partialTick));
result.setReturnValue(Vec3.fromRGB24(color).scale(brightness));
}
@Inject(method = "getSkyDarken", at = @At("RETURN"), cancellable = true)
private void rivencraft$stackClientCelestialLight(float partialTick,
CallbackInfoReturnable<Float> result) {
ClientLevel level = (ClientLevel) (Object) this;
AgeSkyProfile sky = ClientAgeCelestialAngles.sky(level.dimension());
if (sky.bodies().isEmpty() || !level.dimensionType().hasSkyLight()) return;
float weather = (1.0F - level.getRainLevel(partialTick) * 5.0F / 16.0F)
* (1.0F - level.getThunderLevel(partialTick) * 5.0F / 16.0F);
result.setReturnValue(0.2F + 0.8F * sky.lightAt(level.getDayTime() + partialTick) * weather);
}
}
@@ -2,20 +2,37 @@ package net.dnorberg.rivencraft.mixin.client;
import com.mojang.math.Axis; import com.mojang.math.Axis;
import net.dnorberg.rivencraft.client.ClientAgeCelestialAngles; import net.dnorberg.rivencraft.client.ClientAgeCelestialAngles;
import net.dnorberg.rivencraft.client.RivencraftSkyEffects;
import net.minecraft.client.Camera;
import net.minecraft.client.multiplayer.ClientLevel; import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.renderer.LevelRenderer; import net.minecraft.client.renderer.LevelRenderer;
import org.joml.Matrix4f;
import org.joml.Quaternionf; import org.joml.Quaternionf;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
/** Rotates the vanilla sun, moon, and star path by each Age's saved angle. */ /** Rotates the vanilla sun, moon, and star path by each Age's saved angle. */
@Mixin(LevelRenderer.class) @Mixin(LevelRenderer.class)
public abstract class LevelRendererMixin { public abstract class LevelRendererMixin {
private static final RivencraftSkyEffects RIVENCRAFT_SKY = new RivencraftSkyEffects();
@Shadow @Shadow
private ClientLevel level; private ClientLevel level;
@Inject(method = "renderSky", at = @At("HEAD"), cancellable = true)
private void rivencraft$renderAgeSky(Matrix4f frustumMatrix, Matrix4f projectionMatrix,
float partialTick, Camera camera, boolean isFoggy,
Runnable skyFogSetup, CallbackInfo callback) {
if (!level.dimension().location().getNamespace().equals("rivencraft")) return;
ClientAgeCelestialAngles.requestIfMissing(level.dimension());
if (RIVENCRAFT_SKY.renderSky(level, (int) level.getGameTime(), partialTick,
frustumMatrix, camera, projectionMatrix, isFoggy, skyFogSetup)) callback.cancel();
}
@Redirect( @Redirect(
method = "renderSky", method = "renderSky",
at = @At( at = @At(
@@ -1,6 +1,7 @@
package net.dnorberg.rivencraft.network; package net.dnorberg.rivencraft.network;
import net.dnorberg.rivencraft.Rivencraft; import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.book.AgeSkyProfile;
import net.minecraft.core.registries.Registries; import net.minecraft.core.registries.Registries;
import net.minecraft.network.RegistryFriendlyByteBuf; import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.codec.StreamCodec;
@@ -9,8 +10,8 @@ import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.Level; import net.minecraft.world.level.Level;
/** Synchronizes an Age's saved celestial-plane rotation to clients. */ /** Synchronizes an Age's saved celestial-plane rotation and authored sky to clients. */
public record AgeCelestialAnglePayload(ResourceKey<Level> dimension, float angle) public record AgeCelestialAnglePayload(ResourceKey<Level> dimension, float angle, AgeSkyProfile sky)
implements CustomPacketPayload { implements CustomPacketPayload {
public static final Type<AgeCelestialAnglePayload> TYPE = new Type<>( public static final Type<AgeCelestialAnglePayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "age_celestial_angle") ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "age_celestial_angle")
@@ -20,9 +21,34 @@ public record AgeCelestialAnglePayload(ResourceKey<Level> dimension, float angle
(buffer, payload) -> { (buffer, payload) -> {
buffer.writeResourceKey(payload.dimension()); buffer.writeResourceKey(payload.dimension());
buffer.writeFloat(payload.angle()); buffer.writeFloat(payload.angle());
buffer.writeEnum(payload.sky().style());
buffer.writeInt(payload.sky().color());
buffer.writeVarInt(payload.sky().bodies().size());
for (AgeSkyProfile.Body body : payload.sky().bodies()) {
buffer.writeEnum(body.kind());
buffer.writeInt(body.color());
buffer.writeFloat(body.size());
buffer.writeFloat(body.angle());
buffer.writeDouble(body.period());
buffer.writeFloat(body.phase());
buffer.writeBoolean(body.clockwise());
}
}, },
buffer -> new AgeCelestialAnglePayload( buffer -> {
buffer.readResourceKey(Registries.DIMENSION), buffer.readFloat()) ResourceKey<Level> dimension = buffer.readResourceKey(Registries.DIMENSION);
float angle = buffer.readFloat();
AgeSkyProfile.Style style = buffer.readEnum(AgeSkyProfile.Style.class);
int color = buffer.readInt();
int count = buffer.readVarInt();
if (count < 0 || count > 20) throw new IllegalArgumentException("Invalid celestial body count");
java.util.List<AgeSkyProfile.Body> bodies = new java.util.ArrayList<>(count);
for (int i = 0; i < count; i++) bodies.add(new AgeSkyProfile.Body(
buffer.readEnum(AgeSkyProfile.Kind.class), buffer.readInt(),
buffer.readFloat(), buffer.readFloat(), buffer.readDouble(), buffer.readFloat(),
buffer.readBoolean()));
return new AgeCelestialAnglePayload(dimension, angle,
new AgeSkyProfile(style, color, bodies));
}
); );
@Override @Override
@@ -61,7 +61,7 @@ public final class ModNetworking {
} }
public static void register(RegisterPayloadHandlersEvent event) { public static void register(RegisterPayloadHandlersEvent event) {
PayloadRegistrar registrar = event.registrar("17"); PayloadRegistrar registrar = event.registrar("18");
registrar.playToServer(RequestDescriptiveBookScreenPayload.TYPE, RequestDescriptiveBookScreenPayload.STREAM_CODEC, registrar.playToServer(RequestDescriptiveBookScreenPayload.TYPE, RequestDescriptiveBookScreenPayload.STREAM_CODEC,
ModNetworking::requestDescriptiveBookScreen); ModNetworking::requestDescriptiveBookScreen);
registrar.playToClient(OpenDescriptiveBookScreenPayload.TYPE, OpenDescriptiveBookScreenPayload.STREAM_CODEC, registrar.playToClient(OpenDescriptiveBookScreenPayload.TYPE, OpenDescriptiveBookScreenPayload.STREAM_CODEC,
@@ -81,6 +81,8 @@ public final class ModNetworking {
ClientLinkPreviewHandler::handle); ClientLinkPreviewHandler::handle);
registrar.playToClient(AgeCelestialAnglePayload.TYPE, AgeCelestialAnglePayload.STREAM_CODEC, registrar.playToClient(AgeCelestialAnglePayload.TYPE, AgeCelestialAnglePayload.STREAM_CODEC,
ClientAgeCelestialAngles::handle); ClientAgeCelestialAngles::handle);
registrar.playToServer(RequestAgeSkyPayload.TYPE, RequestAgeSkyPayload.STREAM_CODEC,
ModNetworking::requestAgeSky);
registrar.playToClient(SereneAgeWhitelistPayload.TYPE, SereneAgeWhitelistPayload.STREAM_CODEC, registrar.playToClient(SereneAgeWhitelistPayload.TYPE, SereneAgeWhitelistPayload.STREAM_CODEC,
ClientSereneAgeWhitelist::handle); ClientSereneAgeWhitelist::handle);
registrar.playToClient(WritingDeskCatalogPayload.TYPE, WritingDeskCatalogPayload.STREAM_CODEC, registrar.playToClient(WritingDeskCatalogPayload.TYPE, WritingDeskCatalogPayload.STREAM_CODEC,
@@ -93,6 +95,16 @@ public final class ModNetworking {
ModNetworking::setLinkModifierSeed); ModNetworking::setLinkModifierSeed);
} }
private static void requestAgeSky(RequestAgeSkyPayload payload, IPayloadContext context) {
if (!(context.player() instanceof ServerPlayer player)) return;
// Portal renderers may request another Age. Only return registered Age data,
// never an arbitrary client-supplied dimension or generated terrain.
AgeRegistrySavedData.get(player.serverLevel()).definitions().stream()
.filter(age -> AgeDimensionService.dimensionKey(age).equals(payload.dimension()))
.findFirst()
.ifPresent(age -> AgeCelestialAngleSync.send(player, age));
}
private static void setBookBinderTitle(SetBookBinderTitlePayload payload, IPayloadContext context) { private static void setBookBinderTitle(SetBookBinderTitlePayload payload, IPayloadContext context) {
if (context.player() instanceof ServerPlayer player if (context.player() instanceof ServerPlayer player
&& player.containerMenu instanceof BookBinderMenu menu && player.containerMenu instanceof BookBinderMenu menu
@@ -348,8 +360,7 @@ public final class ModNetworking {
/** Sky effects, not dimension IDs, decide the backdrop for custom Ages. */ /** Sky effects, not dimension IDs, decide the backdrop for custom Ages. */
private static int previewSkyColor(ServerLevel level, BlockPos origin) { private static int previewSkyColor(ServerLevel level, BlockPos origin) {
return AgeEnvironmentCompiler.previewSkyColor(level.dimensionType().effectsLocation(), return AgeEnvironmentCompiler.previewSkyColor(level, origin);
level.getBiome(origin).value().getSkyColor());
} }
private static InteractionHand hand(boolean offhand) { private static InteractionHand hand(boolean offhand) {
@@ -0,0 +1,24 @@
package net.dnorberg.rivencraft.network;
import net.dnorberg.rivencraft.Rivencraft;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.Level;
/** Requests the saved sky profile when a renderer reaches an Age before dimension-change sync. */
public record RequestAgeSkyPayload(ResourceKey<Level> dimension) implements CustomPacketPayload {
public static final Type<RequestAgeSkyPayload> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "request_age_sky"));
public static final StreamCodec<RegistryFriendlyByteBuf, RequestAgeSkyPayload> STREAM_CODEC = StreamCodec.of(
(buffer, payload) -> buffer.writeResourceKey(payload.dimension()),
buffer -> new RequestAgeSkyPayload(buffer.readResourceKey(Registries.DIMENSION)));
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
}
@@ -77,12 +77,18 @@ public record SymbolSemantics(Role role, Value value) {
"native", "single", "tiled", "grid", "tiny", "small", "medium", "large", "huge"), "native", "single", "tiled", "grid", "tiny", "small", "medium", "large", "huge"),
BIOME("biome", Kind.RESOURCE), BIOME("biome", Kind.RESOURCE),
LIGHTING("lighting", Kind.CHOICE, "normal", "dark", "bright"), LIGHTING("lighting", Kind.CHOICE, "normal", "dark", "bright"),
SKY("sky", Kind.CHOICE, "overworld", "nether", "end"), SKY("sky", Kind.CHOICE, "overworld", "nether", "end", "static"),
SKY_COLOR("sky_color", Kind.CHOICE,
"red", "orange", "yellow", "green", "blue", "purple", "white", "black"),
WEATHER("weather", Kind.CHOICE, WEATHER("weather", Kind.CHOICE,
"normal", "fast", "slow", "off", "always", "cloudy", "rain", "snow", "storm"), "normal", "fast", "slow", "off", "always", "cloudy", "rain", "snow", "storm"),
SEASON("season", Kind.CHOICE, SEASON("season", Kind.CHOICE,
"no_seasons", "seasons", "eternal_winter", "eternal_summer", "eternal_fall", "eternal_spring"), "no_seasons", "seasons", "eternal_winter", "eternal_summer", "eternal_fall", "eternal_spring"),
CELESTIAL("celestial", Kind.CHOICE, "normal_time"), CELESTIAL("celestial", Kind.CHOICE, "normal_time", "sun", "moon"),
CELESTIAL_COLOR("celestial_color", Kind.CHOICE,
"red", "orange", "yellow", "green", "blue", "purple", "white", "black"),
DIRECTION_MODIFIER("direction_modifier", Kind.CHOICE, "clockwise", "counterclockwise"),
SIZE_MODIFIER("size_modifier", Kind.NUMBER),
PERIOD_MODIFIER("period_modifier", Kind.NUMBER), PERIOD_MODIFIER("period_modifier", Kind.NUMBER),
ANGLE_MODIFIER("angle_modifier", Kind.NUMBER), ANGLE_MODIFIER("angle_modifier", Kind.NUMBER),
PHASE_MODIFIER("phase_modifier", Kind.NUMBER), PHASE_MODIFIER("phase_modifier", Kind.NUMBER),
@@ -109,6 +115,7 @@ public record SymbolSemantics(Role role, Value value) {
if (!(value instanceof NumberValue number)) return false; if (!(value instanceof NumberValue number)) return false;
return switch (this) { return switch (this) {
case PERIOD_MODIFIER -> number.amount() >= 0.0 && number.amount() <= 2.0; case PERIOD_MODIFIER -> number.amount() >= 0.0 && number.amount() <= 2.0;
case SIZE_MODIFIER -> number.amount() >= 0.25 && number.amount() <= 3.0;
case ANGLE_MODIFIER, PHASE_MODIFIER -> number.amount() >= 0.0 && number.amount() < 360.0; case ANGLE_MODIFIER, PHASE_MODIFIER -> number.amount() >= 0.0 && number.amount() < 360.0;
default -> false; default -> false;
}; };
@@ -119,6 +126,7 @@ public record SymbolSemantics(Role role, Value value) {
case RESOURCE -> "a canonical namespaced ID"; case RESOURCE -> "a canonical namespaced ID";
case CHOICE -> "one of " + choices.stream().sorted().toList(); case CHOICE -> "one of " + choices.stream().sorted().toList();
case NUMBER -> this == PERIOD_MODIFIER ? "a finite number in [0, 2]" case NUMBER -> this == PERIOD_MODIFIER ? "a finite number in [0, 2]"
: this == SIZE_MODIFIER ? "a finite number in [0.25, 3]"
: "a finite number in [0, 360) degrees"; : "a finite number in [0, 360) degrees";
}; };
} }
@@ -96,7 +96,7 @@ public final class WritingGrammar {
} }
pendingBlock = null; pendingBlock = null;
} }
if (role == Role.ANGLE_MODIFIER || role == Role.WEATHER if (role == Role.WEATHER
&& parsed.semantics().value() instanceof ChoiceValue choice && parsed.semantics().value() instanceof ChoiceValue choice
&& UNSUPPORTED_WEATHER.contains(choice.id())) { && UNSUPPORTED_WEATHER.contains(choice.id())) {
diagnostics.add(new Diagnostic(Code.UNSUPPORTED_ENVIRONMENT, index, diagnostics.add(new Diagnostic(Code.UNSUPPORTED_ENVIRONMENT, index,
@@ -142,7 +142,10 @@ public final class WritingGrammar {
} }
for (Role required : List.of(Role.TERRAIN, Role.BIOME_CONTROLLER, Role.WEATHER, for (Role required : List.of(Role.TERRAIN, Role.BIOME_CONTROLLER, Role.WEATHER,
Role.LIGHTING, Role.CELESTIAL)) { Role.LIGHTING, Role.CELESTIAL)) {
if (!singular.containsKey(required)) diagnostics.add(new Diagnostic(Code.INCOMPLETE, -1, boolean present = required == Role.CELESTIAL
? symbols.stream().anyMatch(symbol -> symbol.semantics().role() == Role.CELESTIAL)
: singular.containsKey(required);
if (!present) diagnostics.add(new Diagnostic(Code.INCOMPLETE, -1,
"The Age compiler will fill the missing " + required.id() + " branch from the Age seed")); "The Age compiler will fill the missing " + required.id() + " branch from the Age seed"));
} }
if (controller != null && controller.semantics().value() instanceof ChoiceValue choice) { if (controller != null && controller.semantics().value() instanceof ChoiceValue choice) {
@@ -160,13 +163,15 @@ public final class WritingGrammar {
private static boolean singular(Role role) { private static boolean singular(Role role) {
return switch (role) { return switch (role) {
case TERRAIN, BIOME_CONTROLLER, WEATHER, SEASON, LIGHTING, SKY, CELESTIAL -> true; case TERRAIN, BIOME_CONTROLLER, WEATHER, SEASON, LIGHTING, SKY -> true;
default -> false; default -> false;
}; };
} }
private static boolean modifier(Role role) { private static boolean modifier(Role role) {
return role == Role.PERIOD_MODIFIER || role == Role.ANGLE_MODIFIER || role == Role.PHASE_MODIFIER; return role == Role.PERIOD_MODIFIER || role == Role.ANGLE_MODIFIER
|| role == Role.PHASE_MODIFIER || role == Role.SIZE_MODIFIER
|| role == Role.CELESTIAL_COLOR || role == Role.DIRECTION_MODIFIER;
} }
private static int stage(Role role) { private static int stage(Role role) {
@@ -177,8 +182,9 @@ public final class WritingGrammar {
case WEATHER -> 3; case WEATHER -> 3;
case SEASON -> 4; case SEASON -> 4;
case LIGHTING -> 5; case LIGHTING -> 5;
case SKY -> 6; case SKY, SKY_COLOR -> 6;
case PERIOD_MODIFIER, ANGLE_MODIFIER, PHASE_MODIFIER -> 7; case PERIOD_MODIFIER, ANGLE_MODIFIER, PHASE_MODIFIER, SIZE_MODIFIER,
CELESTIAL_COLOR, DIRECTION_MODIFIER -> 7;
case CELESTIAL -> 8; case CELESTIAL -> 8;
case BLOCK, FEATURE, ENVIRONMENT, CORRUPTED -> 9; case BLOCK, FEATURE, ENVIRONMENT, CORRUPTED -> 9;
}; };
@@ -223,6 +223,24 @@
"symbol.rivencraft.sky.nether.description": "Use the Nether sky and red preview background.", "symbol.rivencraft.sky.nether.description": "Use the Nether sky and red preview background.",
"symbol.rivencraft.sky.end": "End Sky", "symbol.rivencraft.sky.end": "End Sky",
"symbol.rivencraft.sky.end.description": "Use the End sky and dark preview background.", "symbol.rivencraft.sky.end.description": "Use the End sky and dark preview background.",
"symbol.rivencraft.sky.static": "Static Sky",
"symbol.rivencraft.sky.static.description": "Fill the sky with shifting television static.",
"symbol.rivencraft.sky_color.red": "Red Sky Color",
"symbol.rivencraft.sky_color.red.description": "Tint the sky and linked portal red.",
"symbol.rivencraft.sky_color.orange": "Orange Sky Color",
"symbol.rivencraft.sky_color.orange.description": "Tint the sky and linked portal orange.",
"symbol.rivencraft.sky_color.yellow": "Yellow Sky Color",
"symbol.rivencraft.sky_color.yellow.description": "Tint the sky and linked portal yellow.",
"symbol.rivencraft.sky_color.green": "Green Sky Color",
"symbol.rivencraft.sky_color.green.description": "Tint the sky and linked portal green.",
"symbol.rivencraft.sky_color.blue": "Blue Sky Color",
"symbol.rivencraft.sky_color.blue.description": "Tint the sky and linked portal blue.",
"symbol.rivencraft.sky_color.purple": "Purple Sky Color",
"symbol.rivencraft.sky_color.purple.description": "Tint the sky and linked portal purple.",
"symbol.rivencraft.sky_color.white": "White Sky Color",
"symbol.rivencraft.sky_color.white.description": "Tint the sky and linked portal white.",
"symbol.rivencraft.sky_color.black": "Black Sky Color",
"symbol.rivencraft.sky_color.black.description": "Tint the sky and linked portal near-black.",
"symbol.rivencraft.weather.normal": "Normal Weather", "symbol.rivencraft.weather.normal": "Normal Weather",
"symbol.rivencraft.weather.normal.description": "Use the normal weather cycle.", "symbol.rivencraft.weather.normal.description": "Use the normal weather cycle.",
"symbol.rivencraft.weather.fast": "Fast Weather", "symbol.rivencraft.weather.fast": "Fast Weather",
@@ -249,6 +267,44 @@
"symbol.rivencraft.season.eternal_spring.description": "Hold this Age in midspring.", "symbol.rivencraft.season.eternal_spring.description": "Hold this Age in midspring.",
"symbol.rivencraft.celestial.normal_time": "Normal Time", "symbol.rivencraft.celestial.normal_time": "Normal Time",
"symbol.rivencraft.celestial.normal_time.description": "Use a normal-length celestial cycle.", "symbol.rivencraft.celestial.normal_time.description": "Use a normal-length celestial cycle.",
"symbol.rivencraft.celestial.sun": "Sun",
"symbol.rivencraft.celestial.sun.description": "Add one independently moving sun to the sky.",
"symbol.rivencraft.celestial.moon": "Moon",
"symbol.rivencraft.celestial.moon.description": "Add one independently moving moon to the sky.",
"symbol.rivencraft.celestial_color.red": "Red Celestial",
"symbol.rivencraft.celestial_color.red.description": "Color the next sun or moon red.",
"symbol.rivencraft.celestial_color.orange": "Orange Celestial",
"symbol.rivencraft.celestial_color.orange.description": "Color the next sun or moon orange.",
"symbol.rivencraft.celestial_color.yellow": "Yellow Celestial",
"symbol.rivencraft.celestial_color.yellow.description": "Color the next sun or moon yellow.",
"symbol.rivencraft.celestial_color.green": "Green Celestial",
"symbol.rivencraft.celestial_color.green.description": "Color the next sun or moon green.",
"symbol.rivencraft.celestial_color.blue": "Blue Celestial",
"symbol.rivencraft.celestial_color.blue.description": "Color the next sun or moon blue.",
"symbol.rivencraft.celestial_color.purple": "Purple Celestial",
"symbol.rivencraft.celestial_color.purple.description": "Color the next sun or moon purple.",
"symbol.rivencraft.celestial_color.white": "White Celestial",
"symbol.rivencraft.celestial_color.white.description": "Color the next sun or moon white.",
"symbol.rivencraft.celestial_color.black": "Black Celestial",
"symbol.rivencraft.celestial_color.black.description": "Blend black into the next sun or moon's color.",
"symbol.rivencraft.size.small": "Small Celestial",
"symbol.rivencraft.size.small.description": "Halve the apparent size of the next sun or moon.",
"symbol.rivencraft.size.normal": "Normal Celestial Size",
"symbol.rivencraft.size.normal.description": "Set the next sun or moon to normal apparent size.",
"symbol.rivencraft.size.large": "Large Celestial",
"symbol.rivencraft.size.large.description": "Double the apparent size of the next sun or moon.",
"symbol.rivencraft.angle.north": "North Celestial Angle",
"symbol.rivencraft.angle.north.description": "Orient the next sun or moon toward the north.",
"symbol.rivencraft.angle.east": "East Celestial Angle",
"symbol.rivencraft.angle.east.description": "Orient the next sun or moon toward the east.",
"symbol.rivencraft.angle.south": "South Celestial Angle",
"symbol.rivencraft.angle.south.description": "Orient the next sun or moon toward the south.",
"symbol.rivencraft.angle.west": "West Celestial Angle",
"symbol.rivencraft.angle.west.description": "Orient the next sun or moon toward the west.",
"symbol.rivencraft.direction.clockwise": "Clockwise Motion",
"symbol.rivencraft.direction.clockwise.description": "Move the next sun or moon clockwise in its orbital plane.",
"symbol.rivencraft.direction.counterclockwise": "Counterclockwise Motion",
"symbol.rivencraft.direction.counterclockwise.description": "Move the next sun or moon counterclockwise in its orbital plane.",
"symbol.rivencraft.period.zero": "Zero Length", "symbol.rivencraft.period.zero": "Zero Length",
"symbol.rivencraft.period.zero.description": "Stop the celestial cycle at its written phase.", "symbol.rivencraft.period.zero.description": "Stop the celestial cycle at its written phase.",
"symbol.rivencraft.period.half": "Half Length", "symbol.rivencraft.period.half": "Half Length",
@@ -1 +1 @@
{"render_type":"minecraft:translucent","ambientocclusion":false,"textures":{"particle":"rivencraft:block/crystal_portal","portal":"rivencraft:block/crystal_portal"},"elements":[{"from":[0,0,6],"to":[16,16,10],"shade":false,"faces":{"north":{"texture":"#portal"},"south":{"texture":"#portal"}}}]} {"render_type":"minecraft:translucent","ambientocclusion":false,"textures":{"particle":"rivencraft:block/crystal_portal","portal":"rivencraft:block/crystal_portal"},"elements":[{"from":[0,0,6],"to":[16,16,10],"shade":false,"faces":{"north":{"texture":"#portal","tintindex":0},"south":{"texture":"#portal","tintindex":0}}}]}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.angle.east","description":"symbol.rivencraft.angle.east.description","category":"rivencraft:celestial","semantics":{"role":"angle_modifier","value":90.0},"poem":"Transform East Illuminate Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.angle.north","description":"symbol.rivencraft.angle.north.description","category":"rivencraft:celestial","semantics":{"role":"angle_modifier","value":0.0},"poem":"Transform North Illuminate Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.angle.south","description":"symbol.rivencraft.angle.south.description","category":"rivencraft:celestial","semantics":{"role":"angle_modifier","value":180.0},"poem":"Transform South Illuminate Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.angle.west","description":"symbol.rivencraft.angle.west.description","category":"rivencraft:celestial","semantics":{"role":"angle_modifier","value":270.0},"poem":"Transform West Illuminate Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.celestial.moon","description":"symbol.rivencraft.celestial.moon.description","category":"rivencraft:celestial","semantics":{"role":"celestial","value":"moon"},"poem":"Transform Time Shadow Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.celestial.sun","description":"symbol.rivencraft.celestial.sun.description","category":"rivencraft:celestial","semantics":{"role":"celestial","value":"sun"},"poem":"Transform Time Illuminate Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.black","description":"symbol.rivencraft.celestial_color.black.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"black"},"poem":"Transform Black Illuminate Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.blue","description":"symbol.rivencraft.celestial_color.blue.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"blue"},"poem":"Transform Blue Illuminate Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.green","description":"symbol.rivencraft.celestial_color.green.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"green"},"poem":"Transform Green Illuminate Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.orange","description":"symbol.rivencraft.celestial_color.orange.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"orange"},"poem":"Transform Orange Illuminate Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.purple","description":"symbol.rivencraft.celestial_color.purple.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"purple"},"poem":"Transform Purple Illuminate Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.red","description":"symbol.rivencraft.celestial_color.red.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"red"},"poem":"Transform Red Illuminate Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.white","description":"symbol.rivencraft.celestial_color.white.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"white"},"poem":"Transform White Illuminate Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.celestial_color.yellow","description":"symbol.rivencraft.celestial_color.yellow.description","category":"rivencraft:celestial","semantics":{"role":"celestial_color","value":"yellow"},"poem":"Transform Yellow Illuminate Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.direction.clockwise","description":"symbol.rivencraft.direction.clockwise.description","category":"rivencraft:celestial","semantics":{"role":"direction_modifier","value":"clockwise"},"poem":"Transform Time Clockwise Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.direction.counterclockwise","description":"symbol.rivencraft.direction.counterclockwise.description","category":"rivencraft:celestial","semantics":{"role":"direction_modifier","value":"counterclockwise"},"poem":"Transform Time Reverse Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.size.large","description":"symbol.rivencraft.size.large.description","category":"rivencraft:celestial","semantics":{"role":"size_modifier","value":2.0},"poem":"Transform Large Illuminate Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.size.normal","description":"symbol.rivencraft.size.normal.description","category":"rivencraft:celestial","semantics":{"role":"size_modifier","value":1.0},"poem":"Transform Balance Illuminate Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.size.small","description":"symbol.rivencraft.size.small.description","category":"rivencraft:celestial","semantics":{"role":"size_modifier","value":0.5},"poem":"Transform Small Illuminate Motion"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.sky.static","description":"symbol.rivencraft.sky.static.description","category":"rivencraft:sky","semantics":{"role":"sky","value":"static"},"poem":"Ethereal Void Sky Flux"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.sky_color.black","description":"symbol.rivencraft.sky_color.black.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"black"},"poem":"Ethereal Black Sky Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.sky_color.blue","description":"symbol.rivencraft.sky_color.blue.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"blue"},"poem":"Ethereal Blue Sky Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.sky_color.green","description":"symbol.rivencraft.sky_color.green.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"green"},"poem":"Ethereal Green Sky Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.sky_color.orange","description":"symbol.rivencraft.sky_color.orange.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"orange"},"poem":"Ethereal Orange Sky Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.sky_color.purple","description":"symbol.rivencraft.sky_color.purple.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"purple"},"poem":"Ethereal Purple Sky Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.sky_color.red","description":"symbol.rivencraft.sky_color.red.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"red"},"poem":"Ethereal Red Sky Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.sky_color.white","description":"symbol.rivencraft.sky_color.white.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"white"},"poem":"Ethereal White Sky Balance"}
@@ -0,0 +1 @@
{"schema_version":2,"name":"symbol.rivencraft.sky_color.yellow","description":"symbol.rivencraft.sky_color.yellow.description","category":"rivencraft:sky","semantics":{"role":"sky_color","value":"yellow"},"poem":"Ethereal Yellow Sky Balance"}
@@ -5,12 +5,14 @@
"compatibilityLevel": "JAVA_21", "compatibilityLevel": "JAVA_21",
"mixins": [ "mixins": [
"AgeDimensionSeedMixin", "AgeDimensionSeedMixin",
"LevelSkyBrightnessMixin",
"AgeDungeonPlacementMixin", "AgeDungeonPlacementMixin",
"AgeStructureBiomeMixin", "AgeStructureBiomeMixin",
"MobDropChanceAccessor", "MobDropChanceAccessor",
"StructurePlacementAccessor" "StructurePlacementAccessor"
], ],
"client": [ "client": [
"client.ClientLevelSkyColorMixin",
"client.ClientLevelSoundMixin", "client.ClientLevelSoundMixin",
"client.ImmersivePortalsGuiRenderingMixin", "client.ImmersivePortalsGuiRenderingMixin",
"client.LevelRendererMixin" "client.LevelRendererMixin"