Extend Legacy instability parity and accelerated ages
Build / build (push) Canceled after 0s

This commit is contained in:
dnorberg committed 2026-10-11 17:14:46 -04:00
1 parent 783a33456d
commit 4f464dbe60
22 files changed
+384 -60

No files matched your search

+11 -8
View File
@@ -1,4 +1,4 @@
# Live block-instability profiling (0.5.8.11) # Live block-instability profiling (0.5.8.11–0.5.8.15)
Mystcraft Legacy measures the abundance of watched blocks in generated chunks, Mystcraft Legacy measures the abundance of watched blocks in generated chunks,
compares that abundance with a baseline, and adds only positive excess to the compares that abundance with a baseline, and adds only positive excess to the
@@ -9,8 +9,11 @@ blocks include coal, lapis, iron, emerald, redstone, gold and diamond ores,
quartz ore, glowstone, and crystal blocks. Diamond **blocks** are not on that quartz ore, glowstone, and crystal blocks. Diamond **blocks** are not on that
default list, but are included here at the player's request. default list, but are included here at the player's request.
Rivencraft samples up to 1,024 distinct, newly generated chunks per dimension, Rivencraft samples up to 1,024 distinct loaded chunks per dimension,
at most four per server tick. It never generates a chunk merely to profile it. at most four per server tick. While a player is in an Age, it also gradually
generates unsampled chunks around that Age's spawn and the Overworld spawn,
at most one chunk per second across both dimensions, until their minimum
sample counts are reached.
The Overworld is sampled as the baseline; an Age needs 401 samples and the The Overworld is sampled as the baseline; an Age needs 401 samples and the
baseline needs 500 before the live block component contributes points. Counts and sampled chunk baseline needs 500 before the live block component contributes points. Counts and sampled chunk
positions persist per dimension, preventing reloads from double-counting. positions persist per dimension, preventing reloads from double-counting.
@@ -42,10 +45,10 @@ map and two factors per block, actively builds a dummy-world baseline, and can
recompute after subsequent chunk profiling. Rivencraft uses a simpler recompute after subsequent chunk profiling. Rivencraft uses a simpler
per-chunk abundance metric, a real-world Overworld baseline, a sample/work cap, per-chunk abundance metric, a real-world Overworld baseline, a sample/work cap,
and a saved authored base so modern servers remain bounded and restart-safe. and a saved authored base so modern servers remain bounded and restart-safe.
The greater-than-400-chunk activation delay is retained. Because only newly The greater-than-400-chunk activation delay is retained. Already-explored
generated chunks are sampled, old already-explored worlds may need substantial chunks count when reloaded, but the bounded spawn-area pass can generate
new exploration before their Overworld baseline is ready. A mod that mutates additional terrain. A mod that mutates blocks after the chunk-load event may
blocks after the chunk-load event may not be reflected in the first sample. not be reflected in the first sample.
## Manual check handed to the player ## Manual check handed to the player
@@ -70,4 +73,4 @@ Do not retry that stalled scenario automatically. In a disposable world:
chunk loading noticeably stall, and report any exception or server hang. chunk loading noticeably stall, and report any exception or server hang.
Do not use a valued save for this check. The game jar builds, but live chunk Do not use a valued save for this check. The game jar builds, but live chunk
profiling still needs this player-run validation before release. profiling still needs this player-run validation before issue #73 is closed.
+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.5.8.11 mod_version=0.5.8.15
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=B6nTjh4b serene_seasons_version_id=B6nTjh4b
@@ -0,0 +1,104 @@
package net.dnorberg.rivencraft.book;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import net.dnorberg.rivencraft.config.RivencraftConfig;
import net.dnorberg.rivencraft.writing.SymbolSemantics;
import net.minecraft.core.BlockPos;
import net.minecraft.core.SectionPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.util.RandomSource;
/** Extra random block ticks from authored Accelerated pages, bounded to loaded player chunks. */
final class AgeAcceleratedTicks {
private AgeAcceleratedTicks() {}
static void tick(ServerLevel level) {
int maximumAttempts = RivencraftConfig.acceleratedMaxAttempts();
if (maximumAttempts == 0) return;
AgeDefinition age = AgeRegistrySavedData.get(level)
.findByDimension(level.dimension().location()).orElse(null);
int pages = age == null || age.plan() == null ? 0 : acceleratedPages(age.plan());
if (pages == 0) return;
List<ChunkPos> chunks = loadedPlayerChunks(level);
if (chunks.isEmpty()) return;
int maximumChunks = Math.min(chunks.size(), RivencraftConfig.acceleratedMaxChunks());
int first = Math.floorMod(level.getServer().getTickCount(), chunks.size());
List<TickSection> eligible = new ArrayList<>();
for (int offset = 0; offset < maximumChunks; offset++) {
ChunkPos pos = chunks.get((first + offset) % chunks.size());
LevelChunk chunk = level.getChunkSource().getChunkNow(pos.x, pos.z);
if (chunk == null) continue;
LevelChunkSection[] sections = chunk.getSections();
for (int sectionIndex = 0; sectionIndex < sections.length; sectionIndex++) {
LevelChunkSection section = sections[sectionIndex];
if (!section.isRandomlyTickingBlocks()) continue;
int baseY = SectionPos.sectionToBlockCoord(chunk.getSectionYFromSectionIndex(sectionIndex));
eligible.add(new TickSection(pos, baseY, section));
}
}
if (eligible.isEmpty()) return;
// Legacy attempts three positions per random-ticking section per page. Divide
// the bounded total between sections before ticking any one of them, so a
// lower section cannot exhaust the entire budget ahead of surface crops.
int total = attemptBudget(pages, eligible.size(), maximumAttempts);
int perSection = total / eligible.size();
int remainder = total % eligible.size();
int firstSection = Math.floorMod(level.getServer().getTickCount(), eligible.size());
RandomSource random = level.getRandom();
for (int offset = 0; offset < eligible.size(); offset++) {
TickSection candidate = eligible.get((firstSection + offset) % eligible.size());
int samples = perSection + (offset < remainder ? 1 : 0);
for (int sample = 0; sample < samples; sample++) {
int x = random.nextInt(16);
int y = random.nextInt(16);
int z = random.nextInt(16);
BlockState state = candidate.section().getBlockState(x, y, z);
if (state.isRandomlyTicking()) {
state.randomTick(level, new BlockPos(candidate.chunk().getMinBlockX() + x,
candidate.baseY() + y, candidate.chunk().getMinBlockZ() + z), random);
}
}
}
}
static int attemptBudget(int pages, int sections, int maximumAttempts) {
if (pages < 0 || sections < 0 || maximumAttempts < 0)
throw new IllegalArgumentException("Negative accelerated tick budget input");
return (int) Math.min(maximumAttempts, Math.min(Integer.MAX_VALUE, 3L * pages * sections));
}
static int acceleratedPages(AgeGenerationPlan plan) {
return (int) plan.symbols().stream().filter(symbol ->
symbol.semantics().role() == SymbolSemantics.Role.ENVIRONMENT
&& symbol.semantics().value() instanceof SymbolSemantics.ChoiceValue choice
&& choice.id().equals("accelerated")).count();
}
private static List<ChunkPos> loadedPlayerChunks(ServerLevel level) {
Set<ChunkPos> chunks = new LinkedHashSet<>();
for (var player : level.players()) {
ChunkPos center = player.chunkPosition();
for (int x = -1; x <= 1; x++) {
for (int z = -1; z <= 1; z++) {
int chunkX = center.x + x;
int chunkZ = center.z + z;
if (level.getChunkSource().hasChunk(chunkX, chunkZ)) {
chunks.add(new ChunkPos(chunkX, chunkZ));
}
}
}
}
return new ArrayList<>(chunks);
}
private record TickSection(ChunkPos chunk, int baseY, LevelChunkSection section) {}
}
@@ -1,31 +1,56 @@
package net.dnorberg.rivencraft.book; package net.dnorberg.rivencraft.book;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.CompletableFuture;
import net.dnorberg.rivencraft.Rivencraft;
import net.dnorberg.rivencraft.config.RivencraftConfig;
import net.dnorberg.rivencraft.writing.BlockSymbolPages;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation; import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import net.neoforged.neoforge.event.level.ChunkEvent; import net.neoforged.neoforge.event.level.ChunkEvent;
import net.neoforged.neoforge.event.server.ServerStoppingEvent; import net.neoforged.neoforge.event.server.ServerStoppingEvent;
import net.neoforged.neoforge.event.tick.ServerTickEvent; import net.neoforged.neoforge.event.tick.ServerTickEvent;
/** Legacy-style excess-block profiling; no chunks are generated or force-loaded for sampling. */ /** Legacy-style excess-block profiling with gradual spawn-area generation. */
public final class AgeBlockInstability { public final class AgeBlockInstability {
// Legacy's watched blocks and factor-1 values; modern ore variants share their parent weight. // Legacy's watched blocks and factor-1 values; modern ore variants share their parent weight.
// Storage blocks are deliberate extensions, including the requested diamond-block case. // Storage blocks are deliberate extensions, including the requested diamond-block case.
private static final Map<ResourceLocation, Integer> WEIGHTS = weights(); private static volatile Map<ResourceLocation, Double> weights;
private static final int MAX_PENDING = 2048; private static final int MAX_PENDING = 2048;
private static final int SAMPLES_PER_TICK = 4; private static final int SAMPLES_PER_TICK = 4;
private static final int GENERATION_INTERVAL_TICKS = 20;
private static final List<ChunkPos> SPAWN_OFFSETS = spawnOffsets();
private static final Map<MinecraftServer, ArrayDeque<Pending>> PENDING = new java.util.WeakHashMap<>(); private static final Map<MinecraftServer, ArrayDeque<Pending>> PENDING = new java.util.WeakHashMap<>();
private static final Map<MinecraftServer, Map<ResourceLocation, Integer>> NEXT_SPAWN_SAMPLE =
new java.util.WeakHashMap<>();
private static final Map<MinecraftServer, Integer> NEXT_TARGET = new java.util.WeakHashMap<>();
private static final Map<MinecraftServer, Boolean> GENERATION_IN_FLIGHT =
new java.util.WeakHashMap<>();
private AgeBlockInstability() {} private AgeBlockInstability() {}
static int weight(ResourceLocation id) { return WEIGHTS.getOrDefault(id, 0); } static double weight(ResourceLocation id) { return watchedWeights().getOrDefault(id, 0.0); }
private static Map<ResourceLocation, Double> watchedWeights() {
Map<ResourceLocation, Double> current = weights;
if (current != null) return current;
synchronized (AgeBlockInstability.class) {
if (weights == null) weights = weights();
return weights;
}
}
public static void onChunkLoad(ChunkEvent.Load event) { public static void onChunkLoad(ChunkEvent.Load event) {
if (!event.isNewChunk() || !(event.getLevel() instanceof ServerLevel level) if (!(event.getLevel() instanceof ServerLevel level)
|| !(event.getChunk() instanceof LevelChunk chunk)) return; || !(event.getChunk() instanceof LevelChunk chunk)) return;
boolean overworld = level.dimension() == net.minecraft.world.level.Level.OVERWORLD; boolean overworld = level.dimension() == net.minecraft.world.level.Level.OVERWORLD;
if (!overworld && !AgeInstabilityRuntime.isAgeDimension(level.dimension().location())) return; if (!overworld && !AgeInstabilityRuntime.isAgeDimension(level.dimension().location())) return;
@@ -44,15 +69,99 @@ public final class AgeBlockInstability {
var queue = PENDING.get(event.getServer()); var queue = PENDING.get(event.getServer());
pending = queue == null ? null : queue.pollFirst(); pending = queue == null ? null : queue.pollFirst();
} }
if (pending == null) return; if (pending == null) break;
var pos = pending.chunk().getPos(); var pos = pending.chunk().getPos();
if (pending.level().getChunkSource().hasChunk(pos.x, pos.z)) if (pending.level().getChunkSource().hasChunk(pos.x, pos.z))
AgeBlockProfileSavedData.get(pending.level()).sample(pending.chunk()); AgeBlockProfileSavedData.get(pending.level()).sample(pending.chunk());
} }
if (event.getServer().getTickCount() % GENERATION_INTERVAL_TICKS == 0) {
generateNextSpawnSample(event.getServer());
}
} }
public static void onServerStopping(ServerStoppingEvent event) { public static void onServerStopping(ServerStoppingEvent event) {
synchronized (PENDING) { PENDING.remove(event.getServer()); } synchronized (PENDING) { PENDING.remove(event.getServer()); }
NEXT_SPAWN_SAMPLE.remove(event.getServer());
NEXT_TARGET.remove(event.getServer());
GENERATION_IN_FLIGHT.remove(event.getServer());
}
/** Generate at most one chunk per second across the active Age and its Overworld baseline. */
private static void generateNextSpawnSample(MinecraftServer server) {
if (GENERATION_IN_FLIGHT.containsKey(server)) return;
List<SampleTarget> targets = new ArrayList<>();
for (ServerLevel level : server.getAllLevels()) {
if (!AgeInstabilityRuntime.isAgeDimension(level.dimension().location())
|| level.players().isEmpty()) continue;
AgeDefinition age = AgeRegistrySavedData.get(level)
.findByDimension(level.dimension().location()).orElse(null);
if (age == null || age.instability().version() < 2) continue;
targets.add(new SampleTarget(level,
age.spawn() == null ? level.getSharedSpawnPos() : age.spawn(),
AgeBlockProfileSavedData.MIN_AGE_SAMPLES));
}
if (targets.isEmpty()) return;
ServerLevel overworld = server.overworld();
targets.add(0, new SampleTarget(overworld, overworld.getSharedSpawnPos(),
AgeBlockProfileSavedData.MIN_BASELINE_SAMPLES));
int first = Math.floorMod(NEXT_TARGET.getOrDefault(server, 0), targets.size());
for (int offset = 0; offset < targets.size(); offset++) {
int index = (first + offset) % targets.size();
SampleTarget target = targets.get(index);
if (generateNextSpawnSample(target.level(), target.origin(), target.minimum(), server)) {
NEXT_TARGET.put(server, index + 1);
return;
}
}
}
private static boolean generateNextSpawnSample(ServerLevel level, net.minecraft.core.BlockPos origin,
int minimum, MinecraftServer server) {
AgeBlockProfileSavedData profile = AgeBlockProfileSavedData.get(level);
if (profile.count() >= minimum) return false;
Map<ResourceLocation, Integer> cursors = NEXT_SPAWN_SAMPLE.computeIfAbsent(server,
ignored -> new HashMap<>());
ResourceLocation dimension = level.dimension().location();
int index = cursors.getOrDefault(dimension, 0);
ChunkPos center = new ChunkPos(origin);
while (index < SPAWN_OFFSETS.size()) {
ChunkPos offset = SPAWN_OFFSETS.get(index++);
ChunkPos target = new ChunkPos(center.x + offset.x, center.z + offset.z);
if (profile.contains(target)) continue;
cursors.put(dimension, index);
GENERATION_IN_FLIGHT.put(server, true);
// getChunkFuture blocks when called on the server thread. Its off-thread
// path schedules generation on Minecraft's chunk executor instead.
CompletableFuture.supplyAsync(() -> level.getChunkSource()
.getChunkFuture(target.x, target.z, ChunkStatus.FULL, true))
.thenCompose(future -> future)
.whenComplete((result, error) -> server.execute(() -> {
GENERATION_IN_FLIGHT.remove(server);
if (error != null) {
Rivencraft.LOGGER.warn("Could not profile chunk {} in {}", target, dimension, error);
} else if (result.orElse(null) instanceof LevelChunk chunk) {
profile.sample(chunk);
}
}));
return true;
}
return false;
}
private static List<ChunkPos> spawnOffsets() {
List<ChunkPos> offsets = new ArrayList<>(AgeBlockProfileSavedData.MAX_SAMPLES);
offsets.add(new ChunkPos(0, 0));
for (int radius = 1; offsets.size() < AgeBlockProfileSavedData.MAX_SAMPLES; radius++) {
for (int x = -radius; x <= radius; x++) {
offsets.add(new ChunkPos(x, -radius));
offsets.add(new ChunkPos(x, radius));
}
for (int z = -radius + 1; z < radius; z++) {
offsets.add(new ChunkPos(-radius, z));
offsets.add(new ChunkPos(radius, z));
}
}
return List.copyOf(offsets.subList(0, AgeBlockProfileSavedData.MAX_SAMPLES));
} }
/** Zero until the Age exceeds 400 samples and the baseline reaches 500. */ /** Zero until the Age exceeds 400 samples and the baseline reaches 500. */
@@ -65,7 +174,7 @@ public final class AgeBlockInstability {
if (ageChunks < AgeBlockProfileSavedData.MIN_AGE_SAMPLES if (ageChunks < AgeBlockProfileSavedData.MIN_AGE_SAMPLES
|| baselineChunks < AgeBlockProfileSavedData.MIN_BASELINE_SAMPLES) return 0; || baselineChunks < AgeBlockProfileSavedData.MIN_BASELINE_SAMPLES) return 0;
double result = 0; double result = 0;
for (var entry : WEIGHTS.entrySet()) { for (var entry : watchedWeights().entrySet()) {
double excess = age.getOrDefault(entry.getKey(), 0L) / (double) ageChunks double excess = age.getOrDefault(entry.getKey(), 0L) / (double) ageChunks
- 1.05 * baseline.getOrDefault(entry.getKey(), 0L) / (double) baselineChunks; - 1.05 * baseline.getOrDefault(entry.getKey(), 0L) / (double) baselineChunks;
if (excess > 0) result += excess * entry.getValue(); if (excess > 0) result += excess * entry.getValue();
@@ -86,27 +195,33 @@ public final class AgeBlockInstability {
} }
public static int effectiveScore(ServerLevel level, AgeDefinition age) { public static int effectiveScore(ServerLevel level, AgeDefinition age) {
return (int) Math.min(Integer.MAX_VALUE, (long) age.instability().score() + score(level, age)); return difficultyScore((long) age.instability().score() + score(level, age),
RivencraftConfig.instabilityDifficulty());
}
public static int difficultyScore(long rawScore, int difficulty) {
double multiplier = switch (difficulty) {
case 0 -> 0.25;
case 1 -> 0.5;
case 2 -> 1.0;
case 3 -> 1.75;
default -> throw new IllegalArgumentException("Unknown instability difficulty " + difficulty);
};
return (int) Math.min(Integer.MAX_VALUE, Math.max(0, rawScore * multiplier));
} }
public static AgeInstabilityPlan effectivePlan(ServerLevel level, AgeDefinition age) { public static AgeInstabilityPlan effectivePlan(ServerLevel level, AgeDefinition age) {
int blockScore = score(level, age); int blockScore = score(level, age);
if (blockScore == 0) return age.instabilityPlan(); if (blockScore == 0 && RivencraftConfig.instabilityDifficulty() == 2) return age.instabilityPlan();
int combined = effectiveScore(level, age); int combined = effectiveScore(level, age);
AgeInstability effective = new AgeInstability(AgeInstability.CURRENT_VERSION, combined, AgeInstability effective = new AgeInstability(AgeInstability.CURRENT_VERSION, combined,
java.util.List.of(new AgeInstability.Contribution("effective_block_profile", -1, combined, java.util.List.of(new AgeInstability.Contribution("effective_block_profile", -1, combined,
"Saved writing score plus sampled excess blocks"))); "Saved writing score plus sampled excess blocks")));
var selected = AgeInstabilityPlan.select(effective, age.seed()); return AgeInstabilityPlan.select(effective, age.seed());
Map<ResourceLocation, Integer> levels = new java.util.LinkedHashMap<>();
age.instabilityPlan().providers().forEach(provider -> levels.put(provider.id(), provider.level()));
selected.providers().forEach(provider -> levels.merge(provider.id(), provider.level(), Math::max));
return new AgeInstabilityPlan(AgeInstabilityPlan.CURRENT_VERSION,
levels.entrySet().stream().map(entry ->
new AgeInstabilityPlan.ProviderLevel(entry.getKey(), entry.getValue())).toList());
} }
private static Map<ResourceLocation, Integer> weights() { private static Map<ResourceLocation, Double> weights() {
Map<ResourceLocation, Integer> values = new HashMap<>(); Map<ResourceLocation, Double> values = new HashMap<>();
add(values, 5, "coal_ore", "deepslate_coal_ore", "lapis_ore", "deepslate_lapis_ore"); add(values, 5, "coal_ore", "deepslate_coal_ore", "lapis_ore", "deepslate_lapis_ore");
add(values, 60, "iron_ore", "deepslate_iron_ore"); add(values, 60, "iron_ore", "deepslate_iron_ore");
add(values, 200, "emerald_ore", "deepslate_emerald_ore"); add(values, 200, "emerald_ore", "deepslate_emerald_ore");
@@ -115,19 +230,28 @@ public final class AgeBlockInstability {
add(values, 4000, "diamond_ore", "deepslate_diamond_ore", "diamond_block"); add(values, 4000, "diamond_ore", "deepslate_diamond_ore", "diamond_block");
add(values, 20, "nether_quartz_ore"); add(values, 20, "nether_quartz_ore");
add(values, 50, "glowstone"); add(values, 50, "glowstone");
values.put(ResourceLocation.parse("rivencraft:crystal_block"), 20); values.put(ResourceLocation.parse("rivencraft:crystal_block"), 20.0);
// Valuable blocks introduced after Legacy, and storage forms authorable in Rivencraft. // Valuable blocks introduced after Legacy, and storage forms authorable in Rivencraft.
add(values, 200, "emerald_block"); add(values, 200, "emerald_block");
add(values, 750, "gold_block", "raw_gold_block"); add(values, 750, "gold_block", "raw_gold_block");
add(values, 4000, "ancient_debris"); add(values, 4000, "ancient_debris");
add(values, 60, "iron_block", "raw_iron_block"); add(values, 60, "iron_block", "raw_iron_block");
add(values, 8000, "netherite_block"); add(values, 8000, "netherite_block");
// Legacy automatically watched eligible non-vanilla fluid blocks with
// accessibility=1 and flat=0.25. Our abundance-only sampler uses their
// combined default factor until a spatial accessibility map is available.
BuiltInRegistries.BLOCK.keySet().stream()
.filter(id -> !id.getNamespace().equals("minecraft"))
.filter(id -> BlockSymbolPages.isFluidMaterial(BuiltInRegistries.BLOCK.get(id)))
.forEach(id -> values.putIfAbsent(id, 1.25));
return Map.copyOf(values); return Map.copyOf(values);
} }
private static void add(Map<ResourceLocation, Integer> values, int weight, String... paths) { private static void add(Map<ResourceLocation, Double> values, int weight, String... paths) {
for (String path : paths) values.put(ResourceLocation.withDefaultNamespace(path), weight); for (String path : paths) values.put(ResourceLocation.withDefaultNamespace(path), (double) weight);
} }
private record Pending(ServerLevel level, LevelChunk chunk) {} private record Pending(ServerLevel level, LevelChunk chunk) {}
private record SampleTarget(ServerLevel level, net.minecraft.core.BlockPos origin, int minimum) {}
} }
@@ -32,6 +32,8 @@ public final class AgeBlockProfileSavedData extends SavedData {
int count() { return samples.size(); } int count() { return samples.size(); }
boolean contains(ChunkPos pos) { return samples.containsKey(pos.toLong()); }
Map<ResourceLocation, Long> totals() { return Map.copyOf(totals); } Map<ResourceLocation, Long> totals() { return Map.copyOf(totals); }
boolean sample(LevelChunk chunk) { boolean sample(LevelChunk chunk) {
@@ -82,6 +82,24 @@ public record AgeInstability(int version, int score, List<Contribution> breakdow
List<Contribution> contributions) { List<Contribution> contributions) {
Map<ResourceLocation, Integer> structureCounts = new HashMap<>(); Map<ResourceLocation, Integer> structureCounts = new HashMap<>();
for (AgeGenerationPlan.ResolvedSymbol symbol : plan.symbols()) { for (AgeGenerationPlan.ResolvedSymbol symbol : plan.symbols()) {
if (symbol.semantics().role()
== net.dnorberg.rivencraft.writing.SymbolSemantics.Role.ENVIRONMENT
&& symbol.semantics().value()
instanceof net.dnorberg.rivencraft.writing.SymbolSemantics.ChoiceValue choice
&& choice.id().equals("accelerated")) {
contributions.add(new Contribution("accelerated", symbol.index(), 1000,
"Authored accelerated random ticks"));
continue;
}
if (symbol.semantics().role()
== net.dnorberg.rivencraft.writing.SymbolSemantics.Role.CORRUPTED) {
String size = ((net.dnorberg.rivencraft.writing.SymbolSemantics.ChoiceValue)
symbol.semantics().value()).id();
int cost = corruptedCost(size);
contributions.add(new Contribution("corrupted_" + size, symbol.index(), cost,
"Legacy corrupted " + size + " feature page"));
continue;
}
if (symbol.semantics().role() if (symbol.semantics().role()
== net.dnorberg.rivencraft.writing.SymbolSemantics.Role.LIGHTING == net.dnorberg.rivencraft.writing.SymbolSemantics.Role.LIGHTING
&& symbol.semantics().value() && symbol.semantics().value()
@@ -118,6 +136,15 @@ public record AgeInstability(int version, int score, List<Contribution> breakdow
? EXCESS_LEGACY_STRUCTURE_COST : 0; ? EXCESS_LEGACY_STRUCTURE_COST : 0;
} }
static int corruptedCost(String size) {
return switch (size) {
case "large" -> 0;
case "medium" -> 1000;
case "small" -> 2000;
default -> throw new IllegalArgumentException("Unknown corrupted page size " + size);
};
}
/** Current totals exposed to diagnostics. Player-driven mutable bonuses are deliberately disabled. */ /** Current totals exposed to diagnostics. Player-driven mutable bonuses are deliberately disabled. */
public Totals totals() { public Totals totals() {
return new Totals(score, 0, 0, score); return new Totals(score, 0, 0, score);
@@ -38,8 +38,11 @@ public final class AgeInstabilityRuntime {
} }
public static void onLevelTickPost(LevelTickEvent.Post event) { public static void onLevelTickPost(LevelTickEvent.Post event) {
if (!(event.getLevel() instanceof ServerLevel level) || !RivencraftConfig.instabilityEnabled() if (!(event.getLevel() instanceof ServerLevel level)
|| !isAgeDimension(level.dimension().location()) || level.players().isEmpty()) return; || !isAgeDimension(level.dimension().location()) || level.players().isEmpty()) return;
// Authored environmental pages are active Age rules, not selected instability cards.
AgeAcceleratedTicks.tick(level);
if (!RivencraftConfig.instabilityEnabled()) return;
MinecraftServer server = level.getServer(); MinecraftServer server = level.getServer();
long tick = server.getTickCount(); long tick = server.getTickCount();
int cooldown = RivencraftConfig.instabilityCooldown(); int cooldown = RivencraftConfig.instabilityCooldown();
@@ -54,7 +54,6 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
private final List<SymbolDisplayMetadata> writingSymbols; private final List<SymbolDisplayMetadata> writingSymbols;
private final boolean pageFree; private final boolean pageFree;
private final String author; private final String author;
private final Integer instabilityScore;
private final InteractionHand hand; private final InteractionHand hand;
private final BlockPos bookStandPos; private final BlockPos bookStandPos;
private DestinationPreviewRenderer preview; private DestinationPreviewRenderer preview;
@@ -75,7 +74,6 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
this.writingSymbols = List.copyOf(writingSymbols); this.writingSymbols = List.copyOf(writingSymbols);
pageFree = writing.entries().stream().noneMatch(entry -> entry.kind() == WritingEntry.Kind.SYMBOL); pageFree = writing.entries().stream().noneMatch(entry -> entry.kind() == WritingEntry.Kind.SYMBOL);
author = book.get(ModDataComponents.BOOK_AUTHOR.get()); author = book.get(ModDataComponents.BOOK_AUTHOR.get());
instabilityScore = book.get(ModDataComponents.AGE_INSTABILITY_SCORE.get());
this.hand = hand; this.hand = hand;
bookStandPos = null; bookStandPos = null;
} }
@@ -90,7 +88,6 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
this.writingSymbols = List.copyOf(writingSymbols); this.writingSymbols = List.copyOf(writingSymbols);
pageFree = writing.entries().stream().noneMatch(entry -> entry.kind() == WritingEntry.Kind.SYMBOL); pageFree = writing.entries().stream().noneMatch(entry -> entry.kind() == WritingEntry.Kind.SYMBOL);
author = book.get(ModDataComponents.BOOK_AUTHOR.get()); author = book.get(ModDataComponents.BOOK_AUTHOR.get());
instabilityScore = book.get(ModDataComponents.AGE_INSTABILITY_SCORE.get());
hand = InteractionHand.MAIN_HAND; hand = InteractionHand.MAIN_HAND;
this.bookStandPos = bookStandPos.immutable(); this.bookStandPos = bookStandPos.immutable();
} }
@@ -254,11 +251,6 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
author == null || author.isBlank() author == null || author.isBlank()
? Component.translatable("screen.rivencraft.age.unknown_author") ? Component.translatable("screen.rivencraft.age.unknown_author")
: Component.literal(author), x + 10, nameEnd + 2, 100) + 4; : Component.literal(author), x + 10, nameEnd + 2, 100) + 4;
if (instabilityScore != null) {
detailsY = drawWrappedWithoutShadow(graphics,
Component.translatable("item.rivencraft.descriptive_book.instability", instabilityScore),
x, detailsY, 110, instabilityScore > 0 ? UNRESOLVED_TEXT_COLOR : BOOK_TEXT_COLOR) + 6;
}
if (!pageFree) return; if (!pageFree) return;
if (generationProfile != null) { if (generationProfile != null) {
Component profileName = generationProfile.getNamespace().equals("rivencraft") Component profileName = generationProfile.getNamespace().equals("rivencraft")
@@ -266,9 +266,17 @@ public final class WritingDeskScreen extends AbstractContainerScreen<WritingDesk
if (selectedPage >= source.size() || source.get(selectedPage).isEmpty()) return; if (selectedPage >= source.size() || source.get(selectedPage).isEmpty()) return;
ItemStack selected = source.get(selectedPage); ItemStack selected = source.get(selectedPage);
drawPage(graphics, selected, leftPos + 265, topPos + 26); drawPage(graphics, selected, leftPos + 265, topPos + 26);
String title = font.plainSubstrByWidth(selected.getHoverName().getString(), 95);
graphics.fill(leftPos + 261, topPos + 81, leftPos + 360, topPos + 95, 0xFF202020); graphics.fill(leftPos + 261, topPos + 81, leftPos + 360, topPos + 95, 0xFF202020);
graphics.drawString(font, title, leftPos + 263, topPos + 84, 0xFFE0E0E0, false); var symbol = PageItem.content(selected).symbol();
if (symbol.isPresent() && PageItem.isCorrupted(symbol.orElseThrow())) {
graphics.enableScissor(leftPos + 263, topPos + 82, leftPos + 358, topPos + 94);
graphics.drawString(font, selected.getHoverName(), leftPos + 263, topPos + 84,
0xFFE0E0E0, false);
graphics.disableScissor();
} else {
String title = font.plainSubstrByWidth(selected.getHoverName().getString(), 95);
graphics.drawString(font, title, leftPos + 263, topPos + 84, 0xFFE0E0E0, false);
}
} }
private void drawPage(GuiGraphics graphics, ItemStack page, int x, int y) { private void drawPage(GuiGraphics graphics, ItemStack page, int x, int y) {
@@ -138,13 +138,15 @@ public final class RivencraftCommands {
var selectedPlan = ageLevel == null ? age.instabilityPlan() var selectedPlan = ageLevel == null ? age.instabilityPlan()
: net.dnorberg.rivencraft.book.AgeBlockInstability.effectivePlan(ageLevel, age); : net.dnorberg.rivencraft.book.AgeBlockInstability.effectivePlan(ageLevel, age);
var totals = age.instability().totals(); var totals = age.instability().totals();
int effectiveScore = net.dnorberg.rivencraft.book.AgeBlockInstability.difficultyScore(
(long) totals.effectiveScore() + blockScore, RivencraftConfig.instabilityDifficulty());
source.sendSuccess(() -> Component.literal(age.name() + " — " + age.dimensionId()), false); source.sendSuccess(() -> Component.literal(age.name() + " — " + age.dimensionId()), false);
source.sendSuccess(() -> Component.literal("Instability: base " + totals.baseScore() source.sendSuccess(() -> Component.literal("Instability: base " + totals.baseScore()
+ ", stability bonus " + totals.stabilityBonus() + ", stability bonus " + totals.stabilityBonus()
+ ", penalty " + totals.instabilityPenalty() + ", penalty " + totals.instabilityPenalty()
+ ", profiled blocks +" + blockScore + ", profiled blocks +" + blockScore
+ ", effective " + Math.min(Integer.MAX_VALUE, + ", effective " + effectiveScore
(long) totals.effectiveScore() + blockScore)), false); + " (difficulty " + RivencraftConfig.instabilityDifficulty() + ")"), false);
if (age.instability().version() >= 2) { if (age.instability().version() >= 2) {
int ageSamples = ageLevel == null ? 0 int ageSamples = ageLevel == null ? 0
: net.dnorberg.rivencraft.book.AgeBlockInstability.sampledChunks(ageLevel); : net.dnorberg.rivencraft.book.AgeBlockInstability.sampledChunks(ageLevel);
@@ -178,7 +180,7 @@ public final class RivencraftCommands {
+ RivencraftConfig.instabilityMaxLevel()), false); + RivencraftConfig.instabilityMaxLevel()), false);
source.sendSuccess(() -> Component.literal("Mutable player bonuses: disabled (no active Legacy registration)"), source.sendSuccess(() -> Component.literal("Mutable player bonuses: disabled (no active Legacy registration)"),
false); false);
return (int) Math.min(Integer.MAX_VALUE, (long) totals.effectiveScore() + blockScore); return effectiveScore;
} }
private static java.util.concurrent.CompletableFuture<com.mojang.brigadier.suggestion.Suggestions> suggestAges( private static java.util.concurrent.CompletableFuture<com.mojang.brigadier.suggestion.Suggestions> suggestAges(
@@ -22,6 +22,7 @@ public final class RivencraftConfig {
private static final ModConfigSpec.IntValue LINK_PANEL_INK_COST; private static final ModConfigSpec.IntValue LINK_PANEL_INK_COST;
private static final ModConfigSpec.BooleanValue AUTO_WHITELIST_SERENE_AGES; private static final ModConfigSpec.BooleanValue AUTO_WHITELIST_SERENE_AGES;
private static final ModConfigSpec.BooleanValue INSTABILITY_ENABLED; private static final ModConfigSpec.BooleanValue INSTABILITY_ENABLED;
private static final ModConfigSpec.IntValue INSTABILITY_DIFFICULTY;
private static final ModConfigSpec.BooleanValue INSTABILITY_WORLD_MUTATION; private static final ModConfigSpec.BooleanValue INSTABILITY_WORLD_MUTATION;
private static final ModConfigSpec.IntValue INSTABILITY_COOLDOWN; private static final ModConfigSpec.IntValue INSTABILITY_COOLDOWN;
private static final ModConfigSpec.IntValue INSTABILITY_MAX_AGES; private static final ModConfigSpec.IntValue INSTABILITY_MAX_AGES;
@@ -29,6 +30,8 @@ public final class RivencraftConfig {
private static final ModConfigSpec.IntValue INSTABILITY_MAX_ACTIONS; private static final ModConfigSpec.IntValue INSTABILITY_MAX_ACTIONS;
private static final ModConfigSpec.IntValue INSTABILITY_MAX_LEVEL; private static final ModConfigSpec.IntValue INSTABILITY_MAX_LEVEL;
private static final ModConfigSpec.IntValue INSTABILITY_MAX_CHUNKS; private static final ModConfigSpec.IntValue INSTABILITY_MAX_CHUNKS;
private static final ModConfigSpec.IntValue ACCELERATED_MAX_CHUNKS;
private static final ModConfigSpec.IntValue ACCELERATED_MAX_ATTEMPTS;
private static final ModConfigSpec.BooleanValue STATUS_EFFECTS_ENABLED; private static final ModConfigSpec.BooleanValue STATUS_EFFECTS_ENABLED;
private static final ModConfigSpec.BooleanValue STATUS_EFFECTS_PLAYERS; private static final ModConfigSpec.BooleanValue STATUS_EFFECTS_PLAYERS;
private static final ModConfigSpec.BooleanValue STATUS_EFFECTS_NONPLAYERS; private static final ModConfigSpec.BooleanValue STATUS_EFFECTS_NONPLAYERS;
@@ -85,6 +88,8 @@ public final class RivencraftConfig {
builder.push("instability"); builder.push("instability");
INSTABILITY_ENABLED = builder.comment("Run selected instability providers in loaded Ages. Disabling does not alter saved plans.") INSTABILITY_ENABLED = builder.comment("Run selected instability providers in loaded Ages. Disabling does not alter saved plans.")
.define("enabled", true); .define("enabled", true);
INSTABILITY_DIFFICULTY = builder.comment("Legacy instability difficulty: 0=quarter, 1=half, 2=normal, 3=1.75x. Changes affect active effects, not saved writing.")
.defineInRange("difficulty", 2, 0, 3);
INSTABILITY_WORLD_MUTATION = builder.comment("Allow selected instability providers to change blocks or create explosions, as in Legacy.") INSTABILITY_WORLD_MUTATION = builder.comment("Allow selected instability providers to change blocks or create explosions, as in Legacy.")
.define("world_mutation", true); .define("world_mutation", true);
INSTABILITY_COOLDOWN = builder.comment("Minimum ticks between instability runs for each loaded Age.") INSTABILITY_COOLDOWN = builder.comment("Minimum ticks between instability runs for each loaded Age.")
@@ -100,6 +105,12 @@ public final class RivencraftConfig {
INSTABILITY_MAX_CHUNKS = builder.comment("Maximum already-loaded candidate chunks inspected per Age run.") INSTABILITY_MAX_CHUNKS = builder.comment("Maximum already-loaded candidate chunks inspected per Age run.")
.defineInRange("max_loaded_chunks", 16, 1, 64); .defineInRange("max_loaded_chunks", 16, 1, 64);
builder.pop(); builder.pop();
builder.push("accelerated_age");
ACCELERATED_MAX_CHUNKS = builder.comment("Maximum already-loaded chunks given extra random ticks per Age tick.")
.defineInRange("max_loaded_chunks", 8, 1, 64);
ACCELERATED_MAX_ATTEMPTS = builder.comment("Maximum extra random-tick positions sampled per Age tick, shared fairly across loaded sections; zero disables the Accelerated page effect. The previous 1024-attempt default capped heavily accelerated Ages too early.")
.defineInRange("max_random_tick_attempts_per_tick", 16384, 0, 65536);
builder.pop();
builder.push("instability_status_effects"); builder.push("instability_status_effects");
STATUS_EFFECTS_ENABLED = builder.comment("Allow bounded instability status effects in loaded Ages.") STATUS_EFFECTS_ENABLED = builder.comment("Allow bounded instability status effects in loaded Ages.")
.define("enabled", true); .define("enabled", true);
@@ -210,6 +221,7 @@ public final class RivencraftConfig {
} }
public static boolean instabilityEnabled() { return INSTABILITY_ENABLED.get(); } public static boolean instabilityEnabled() { return INSTABILITY_ENABLED.get(); }
public static int instabilityDifficulty() { return INSTABILITY_DIFFICULTY.get(); }
public static boolean instabilityProviderEnabled(ResourceLocation id) { public static boolean instabilityProviderEnabled(ResourceLocation id) {
ModConfigSpec.BooleanValue value = id.getNamespace().equals("rivencraft") ModConfigSpec.BooleanValue value = id.getNamespace().equals("rivencraft")
? INSTABILITY_PROVIDERS.get(id.getPath()) : null; ? INSTABILITY_PROVIDERS.get(id.getPath()) : null;
@@ -222,6 +234,8 @@ public final class RivencraftConfig {
public static int instabilityMaxActions() { return INSTABILITY_MAX_ACTIONS.get(); } public static int instabilityMaxActions() { return INSTABILITY_MAX_ACTIONS.get(); }
public static int instabilityMaxLevel() { return INSTABILITY_MAX_LEVEL.get(); } public static int instabilityMaxLevel() { return INSTABILITY_MAX_LEVEL.get(); }
public static int instabilityMaxChunks() { return INSTABILITY_MAX_CHUNKS.get(); } public static int instabilityMaxChunks() { return INSTABILITY_MAX_CHUNKS.get(); }
public static int acceleratedMaxChunks() { return ACCELERATED_MAX_CHUNKS.get(); }
public static int acceleratedMaxAttempts() { return ACCELERATED_MAX_ATTEMPTS.get(); }
public static boolean statusEffectsEnabled() { return STATUS_EFFECTS_ENABLED.get(); } public static boolean statusEffectsEnabled() { return STATUS_EFFECTS_ENABLED.get(); }
public static boolean statusEffectsPlayers() { return STATUS_EFFECTS_PLAYERS.get(); } public static boolean statusEffectsPlayers() { return STATUS_EFFECTS_PLAYERS.get(); }
public static boolean statusEffectsNonplayers() { return STATUS_EFFECTS_NONPLAYERS.get(); } public static boolean statusEffectsNonplayers() { return STATUS_EFFECTS_NONPLAYERS.get(); }
@@ -112,11 +112,6 @@ public final class DescriptiveBookItem extends Item {
? Component.translatable("item.rivencraft.descriptive_book.unwritten") ? Component.translatable("item.rivencraft.descriptive_book.unwritten")
: Component.translatable("item.rivencraft.descriptive_book.age", age.name()); : Component.translatable("item.rivencraft.descriptive_book.age", age.name());
tooltipComponents.add(status.withStyle(ChatFormatting.GRAY)); tooltipComponents.add(status.withStyle(ChatFormatting.GRAY));
Integer instability = stack.get(ModDataComponents.AGE_INSTABILITY_SCORE.get());
if (age != null && instability != null) {
tooltipComponents.add(Component.translatable("item.rivencraft.descriptive_book.instability", instability)
.withStyle(instability > 0 ? ChatFormatting.DARK_RED : ChatFormatting.DARK_GREEN));
}
stack.getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE) stack.getOrDefault(ModDataComponents.LINK_PROPERTIES.get(), LinkProperties.NONE)
.forDescriptiveBook().appendTooltip(tooltipComponents); .forDescriptiveBook().appendTooltip(tooltipComponents);
} }
@@ -290,13 +290,17 @@ public final class PageItem extends Item {
: SymbolDisplayMetadata.unresolved(reference); : SymbolDisplayMetadata.unresolved(reference);
if (metadata.resolved()) { if (metadata.resolved()) {
tooltipComponents.add(Component.translatable(metadata.descriptionTranslationKey()) tooltipComponents.add(Component.translatable(metadata.descriptionTranslationKey())
.withStyle(ChatFormatting.GRAY)); .withStyle(isCorrupted(reference)
? style -> style.applyFormats(ChatFormatting.GRAY, ChatFormatting.OBFUSCATED)
: style -> style.applyFormat(ChatFormatting.GRAY)));
} else { } else {
tooltipComponents.add(Component.translatable( tooltipComponents.add(Component.translatable(
"symbol.rivencraft.unresolved.description", reference.unresolvedName()) "symbol.rivencraft.unresolved.description", reference.unresolvedName())
.withStyle(ChatFormatting.RED)); .withStyle(ChatFormatting.RED));
} }
tooltipComponents.add(Component.literal(reference.id().toString()).withStyle(ChatFormatting.DARK_GRAY)); if (!isCorrupted(reference)) {
tooltipComponents.add(Component.literal(reference.id().toString()).withStyle(ChatFormatting.DARK_GRAY));
}
} }
private static ItemStack create(PageContent content) { private static ItemStack create(PageContent content) {
@@ -313,6 +317,9 @@ public final class PageItem extends Item {
SymbolDisplayMetadata metadata = stack.getOrDefault( SymbolDisplayMetadata metadata = stack.getOrDefault(
ModDataComponents.PAGE_SYMBOL_DISPLAY.get(), SymbolCatalog.display(reference)); ModDataComponents.PAGE_SYMBOL_DISPLAY.get(), SymbolCatalog.display(reference));
if (!metadata.resolved()) return Component.translatable("item.rivencraft.page.unresolved"); if (!metadata.resolved()) return Component.translatable("item.rivencraft.page.unresolved");
if (isCorrupted(reference)) {
return Component.translatable(metadata.nameTranslationKey()).withStyle(ChatFormatting.OBFUSCATED);
}
return reference.id().getNamespace().equals(Rivencraft.MOD_ID) return reference.id().getNamespace().equals(Rivencraft.MOD_ID)
&& reference.id().getPath().startsWith("biome/") && reference.id().getPath().startsWith("biome/")
? Component.translatableWithFallback(metadata.nameTranslationKey(), ? Component.translatableWithFallback(metadata.nameTranslationKey(),
@@ -324,4 +331,9 @@ public final class PageItem extends Item {
: Component.translatable(metadata.nameTranslationKey()); : Component.translatable(metadata.nameTranslationKey());
} }
public static boolean isCorrupted(SymbolReference reference) {
return reference.id().getNamespace().equals(Rivencraft.MOD_ID)
&& reference.id().getPath().startsWith("corrupted/");
}
} }
@@ -111,11 +111,6 @@ public final class ModNetworking {
access.stack().set(ModDataComponents.AGE_GENERATION_PROFILE.get(), definition.generationProfile()); access.stack().set(ModDataComponents.AGE_GENERATION_PROFILE.get(), definition.generationProfile());
access.sync().run(); access.sync().run();
} }
if (definition != null && !java.util.Objects.equals(
access.stack().get(ModDataComponents.AGE_INSTABILITY_SCORE.get()), definition.instability().score())) {
access.stack().set(ModDataComponents.AGE_INSTABILITY_SCORE.get(), definition.instability().score());
access.sync().run();
}
// Requests run on the same server thread as Age creation. A request queued // Requests run on the same server thread as Age creation. A request queued
// during generation can only open the finished book, never a stale client copy. // during generation can only open the finished book, never a stale client copy.
player.inventoryMenu.broadcastChanges(); player.inventoryMenu.broadcastChanges();
@@ -177,7 +172,6 @@ public final class ModNetworking {
access.stack().set(ModDataComponents.BOOK_AUTHOR.get(), player.getGameProfile().getName()); access.stack().set(ModDataComponents.BOOK_AUTHOR.get(), player.getGameProfile().getName());
AgeRegistrySavedData.get(player.serverLevel()).find(reference.id()).ifPresent(definition -> { AgeRegistrySavedData.get(player.serverLevel()).find(reference.id()).ifPresent(definition -> {
access.stack().set(ModDataComponents.AGE_GENERATION_PROFILE.get(), definition.generationProfile()); access.stack().set(ModDataComponents.AGE_GENERATION_PROFILE.get(), definition.generationProfile());
access.stack().set(ModDataComponents.AGE_INSTABILITY_SCORE.get(), definition.instability().score());
}); });
access.stack().set(DataComponents.CUSTOM_NAME, Component.literal(title)); access.stack().set(DataComponents.CUSTOM_NAME, Component.literal(title));
access.sync().run(); access.sync().run();
@@ -50,7 +50,7 @@ public final class ModCreativeTabs {
.build()); .build());
private static final List<String> CATEGORY_ORDER = List.of( private static final List<String> CATEGORY_ORDER = List.of(
"terrain", "biome", "cave_biome", "biome_generation", "weather", "season", "lighting", "sky", "celestial", "block", "fluid", "feature"); "terrain", "biome", "cave_biome", "biome_generation", "weather", "season", "lighting", "sky", "celestial", "block", "fluid", "feature", "environment", "corrupted");
public static final Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> SYMBOL_PAGES = public static final Map<ResourceLocation, DeferredHolder<CreativeModeTab, CreativeModeTab>> SYMBOL_PAGES =
registerSymbolPageTabs(); registerSymbolPageTabs();
@@ -87,7 +87,9 @@ public record SymbolSemantics(Role role, Value value) {
ANGLE_MODIFIER("angle_modifier", Kind.NUMBER), ANGLE_MODIFIER("angle_modifier", Kind.NUMBER),
PHASE_MODIFIER("phase_modifier", Kind.NUMBER), PHASE_MODIFIER("phase_modifier", Kind.NUMBER),
BLOCK("block", Kind.RESOURCE), BLOCK("block", Kind.RESOURCE),
FEATURE("feature", Kind.RESOURCE); FEATURE("feature", Kind.RESOURCE),
ENVIRONMENT("environment", Kind.CHOICE, "accelerated"),
CORRUPTED("corrupted", Kind.CHOICE, "large", "medium", "small");
private final String id; private final String id;
private final Kind kind; private final Kind kind;
@@ -168,7 +168,7 @@ public final class WritingGrammar {
case SKY -> 6; case SKY -> 6;
case PERIOD_MODIFIER, ANGLE_MODIFIER, PHASE_MODIFIER -> 7; case PERIOD_MODIFIER, ANGLE_MODIFIER, PHASE_MODIFIER -> 7;
case CELESTIAL -> 8; case CELESTIAL -> 8;
case BLOCK, FEATURE -> 9; case BLOCK, FEATURE, ENVIRONMENT, CORRUPTED -> 9;
}; };
} }
@@ -351,5 +351,15 @@
"itemGroup.rivencraft.pages.celestial": "Rivencraft Celestial Pages", "itemGroup.rivencraft.pages.celestial": "Rivencraft Celestial Pages",
"itemGroup.rivencraft.pages.block": "Rivencraft Block Pages", "itemGroup.rivencraft.pages.block": "Rivencraft Block Pages",
"itemGroup.rivencraft.pages.fluid": "Rivencraft Fluid Pages", "itemGroup.rivencraft.pages.fluid": "Rivencraft Fluid Pages",
"itemGroup.rivencraft.pages.feature": "Rivencraft Feature Pages" "itemGroup.rivencraft.pages.feature": "Rivencraft Feature Pages",
"itemGroup.rivencraft.pages.corrupted": "Rivencraft Corrupted Pages",
"itemGroup.rivencraft.pages.environment": "Rivencraft Environment Pages",
"symbol.rivencraft.environment.accelerated": "Accelerated",
"symbol.rivencraft.environment.accelerated.description": "Causes blocks in this Age to receive extra random ticks.",
"symbol.rivencraft.corrupted.large": "Corrupted Page",
"symbol.rivencraft.corrupted.large.description": "The writing shifts beyond comprehension.",
"symbol.rivencraft.corrupted.medium": "Corrupted Page",
"symbol.rivencraft.corrupted.medium.description": "The writing shifts beyond comprehension.",
"symbol.rivencraft.corrupted.small": "Corrupted Page",
"symbol.rivencraft.corrupted.small.description": "The writing shifts beyond comprehension."
} }
@@ -0,0 +1,8 @@
{
"schema_version": 2,
"name": "symbol.rivencraft.corrupted.large",
"description": "symbol.rivencraft.corrupted.large.description",
"category": "rivencraft:corrupted",
"semantics": { "role": "corrupted", "value": "large" },
"poem": "Contradict Chaos Exist Terrain"
}
@@ -0,0 +1,8 @@
{
"schema_version": 2,
"name": "symbol.rivencraft.corrupted.medium",
"description": "symbol.rivencraft.corrupted.medium.description",
"category": "rivencraft:corrupted",
"semantics": { "role": "corrupted", "value": "medium" },
"poem": "Contradict Chaos Exist Balance"
}
@@ -0,0 +1,8 @@
{
"schema_version": 2,
"name": "symbol.rivencraft.corrupted.small",
"description": "symbol.rivencraft.corrupted.small.description",
"category": "rivencraft:corrupted",
"semantics": { "role": "corrupted", "value": "small" },
"poem": "Contradict Chaos Exist Form"
}
@@ -0,0 +1,8 @@
{
"schema_version": 2,
"name": "symbol.rivencraft.environment.accelerated",
"description": "symbol.rivencraft.environment.accelerated.description",
"category": "rivencraft:environment",
"semantics": { "role": "environment", "value": "accelerated" },
"poem": "Environment Dynamic Change Spur"
}