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

@@ -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;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
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.server.MinecraftServer;
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.status.ChunkStatus;
import net.neoforged.neoforge.event.level.ChunkEvent;
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
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 {
// 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.
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 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, 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() {}
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) {
if (!event.isNewChunk() || !(event.getLevel() instanceof ServerLevel level)
if (!(event.getLevel() instanceof ServerLevel level)
|| !(event.getChunk() instanceof LevelChunk chunk)) return;
boolean overworld = level.dimension() == net.minecraft.world.level.Level.OVERWORLD;
if (!overworld && !AgeInstabilityRuntime.isAgeDimension(level.dimension().location())) return;
@@ -44,15 +69,99 @@ public final class AgeBlockInstability {
var queue = PENDING.get(event.getServer());
pending = queue == null ? null : queue.pollFirst();
}
if (pending == null) return;
if (pending == null) break;
var pos = pending.chunk().getPos();
if (pending.level().getChunkSource().hasChunk(pos.x, pos.z))
AgeBlockProfileSavedData.get(pending.level()).sample(pending.chunk());
}
if (event.getServer().getTickCount() % GENERATION_INTERVAL_TICKS == 0) {
generateNextSpawnSample(event.getServer());
}
}
public static void onServerStopping(ServerStoppingEvent event) {
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. */
@@ -65,7 +174,7 @@ public final class AgeBlockInstability {
if (ageChunks < AgeBlockProfileSavedData.MIN_AGE_SAMPLES
|| baselineChunks < AgeBlockProfileSavedData.MIN_BASELINE_SAMPLES) return 0;
double result = 0;
for (var entry : WEIGHTS.entrySet()) {
for (var entry : watchedWeights().entrySet()) {
double excess = age.getOrDefault(entry.getKey(), 0L) / (double) ageChunks
- 1.05 * baseline.getOrDefault(entry.getKey(), 0L) / (double) baselineChunks;
if (excess > 0) result += excess * entry.getValue();
@@ -86,27 +195,33 @@ public final class AgeBlockInstability {
}
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) {
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);
AgeInstability effective = new AgeInstability(AgeInstability.CURRENT_VERSION, combined,
java.util.List.of(new AgeInstability.Contribution("effective_block_profile", -1, combined,
"Saved writing score plus sampled excess blocks")));
var selected = 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());
return AgeInstabilityPlan.select(effective, age.seed());
}
private static Map<ResourceLocation, Integer> weights() {
Map<ResourceLocation, Integer> values = new HashMap<>();
private static Map<ResourceLocation, Double> weights() {
Map<ResourceLocation, Double> values = new HashMap<>();
add(values, 5, "coal_ore", "deepslate_coal_ore", "lapis_ore", "deepslate_lapis_ore");
add(values, 60, "iron_ore", "deepslate_iron_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, 20, "nether_quartz_ore");
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.
add(values, 200, "emerald_block");
add(values, 750, "gold_block", "raw_gold_block");
add(values, 4000, "ancient_debris");
add(values, 60, "iron_block", "raw_iron_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);
}
private static void add(Map<ResourceLocation, Integer> values, int weight, String... paths) {
for (String path : paths) values.put(ResourceLocation.withDefaultNamespace(path), weight);
private static void add(Map<ResourceLocation, Double> values, int weight, String... paths) {
for (String path : paths) values.put(ResourceLocation.withDefaultNamespace(path), (double) weight);
}
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(); }
boolean contains(ChunkPos pos) { return samples.containsKey(pos.toLong()); }
Map<ResourceLocation, Long> totals() { return Map.copyOf(totals); }
boolean sample(LevelChunk chunk) {
@@ -82,6 +82,24 @@ public record AgeInstability(int version, int score, List<Contribution> breakdow
List<Contribution> contributions) {
Map<ResourceLocation, Integer> structureCounts = new HashMap<>();
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()
== net.dnorberg.rivencraft.writing.SymbolSemantics.Role.LIGHTING
&& symbol.semantics().value()
@@ -118,6 +136,15 @@ public record AgeInstability(int version, int score, List<Contribution> breakdow
? 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. */
public Totals totals() {
return new Totals(score, 0, 0, score);
@@ -38,8 +38,11 @@ public final class AgeInstabilityRuntime {
}
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;
// Authored environmental pages are active Age rules, not selected instability cards.
AgeAcceleratedTicks.tick(level);
if (!RivencraftConfig.instabilityEnabled()) return;
MinecraftServer server = level.getServer();
long tick = server.getTickCount();
int cooldown = RivencraftConfig.instabilityCooldown();
@@ -54,7 +54,6 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
private final List<SymbolDisplayMetadata> writingSymbols;
private final boolean pageFree;
private final String author;
private final Integer instabilityScore;
private final InteractionHand hand;
private final BlockPos bookStandPos;
private DestinationPreviewRenderer preview;
@@ -75,7 +74,6 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
this.writingSymbols = List.copyOf(writingSymbols);
pageFree = writing.entries().stream().noneMatch(entry -> entry.kind() == WritingEntry.Kind.SYMBOL);
author = book.get(ModDataComponents.BOOK_AUTHOR.get());
instabilityScore = book.get(ModDataComponents.AGE_INSTABILITY_SCORE.get());
this.hand = hand;
bookStandPos = null;
}
@@ -90,7 +88,6 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
this.writingSymbols = List.copyOf(writingSymbols);
pageFree = writing.entries().stream().noneMatch(entry -> entry.kind() == WritingEntry.Kind.SYMBOL);
author = book.get(ModDataComponents.BOOK_AUTHOR.get());
instabilityScore = book.get(ModDataComponents.AGE_INSTABILITY_SCORE.get());
hand = InteractionHand.MAIN_HAND;
this.bookStandPos = bookStandPos.immutable();
}
@@ -254,11 +251,6 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
author == null || author.isBlank()
? Component.translatable("screen.rivencraft.age.unknown_author")
: 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 (generationProfile != null) {
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;
ItemStack selected = source.get(selectedPage);
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.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) {
@@ -138,13 +138,15 @@ public final class RivencraftCommands {
var selectedPlan = ageLevel == null ? age.instabilityPlan()
: net.dnorberg.rivencraft.book.AgeBlockInstability.effectivePlan(ageLevel, age);
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("Instability: base " + totals.baseScore()
+ ", stability bonus " + totals.stabilityBonus()
+ ", penalty " + totals.instabilityPenalty()
+ ", profiled blocks +" + blockScore
+ ", effective " + Math.min(Integer.MAX_VALUE,
(long) totals.effectiveScore() + blockScore)), false);
+ ", effective " + effectiveScore
+ " (difficulty " + RivencraftConfig.instabilityDifficulty() + ")"), false);
if (age.instability().version() >= 2) {
int ageSamples = ageLevel == null ? 0
: net.dnorberg.rivencraft.book.AgeBlockInstability.sampledChunks(ageLevel);
@@ -178,7 +180,7 @@ public final class RivencraftCommands {
+ RivencraftConfig.instabilityMaxLevel()), false);
source.sendSuccess(() -> Component.literal("Mutable player bonuses: disabled (no active Legacy registration)"),
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(
@@ -22,6 +22,7 @@ public final class RivencraftConfig {
private static final ModConfigSpec.IntValue LINK_PANEL_INK_COST;
private static final ModConfigSpec.BooleanValue AUTO_WHITELIST_SERENE_AGES;
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.IntValue INSTABILITY_COOLDOWN;
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_LEVEL;
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_PLAYERS;
private static final ModConfigSpec.BooleanValue STATUS_EFFECTS_NONPLAYERS;
@@ -85,6 +88,8 @@ public final class RivencraftConfig {
builder.push("instability");
INSTABILITY_ENABLED = builder.comment("Run selected instability providers in loaded Ages. Disabling does not alter saved plans.")
.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.")
.define("world_mutation", true);
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.")
.defineInRange("max_loaded_chunks", 16, 1, 64);
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");
STATUS_EFFECTS_ENABLED = builder.comment("Allow bounded instability status effects in loaded Ages.")
.define("enabled", true);
@@ -210,6 +221,7 @@ public final class RivencraftConfig {
}
public static boolean instabilityEnabled() { return INSTABILITY_ENABLED.get(); }
public static int instabilityDifficulty() { return INSTABILITY_DIFFICULTY.get(); }
public static boolean instabilityProviderEnabled(ResourceLocation id) {
ModConfigSpec.BooleanValue value = id.getNamespace().equals("rivencraft")
? 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 instabilityMaxLevel() { return INSTABILITY_MAX_LEVEL.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 statusEffectsPlayers() { return STATUS_EFFECTS_PLAYERS.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.age", age.name());
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)
.forDescriptiveBook().appendTooltip(tooltipComponents);
}
@@ -290,13 +290,17 @@ public final class PageItem extends Item {
: SymbolDisplayMetadata.unresolved(reference);
if (metadata.resolved()) {
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 {
tooltipComponents.add(Component.translatable(
"symbol.rivencraft.unresolved.description", reference.unresolvedName())
.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) {
@@ -313,6 +317,9 @@ public final class PageItem extends Item {
SymbolDisplayMetadata metadata = stack.getOrDefault(
ModDataComponents.PAGE_SYMBOL_DISPLAY.get(), SymbolCatalog.display(reference));
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)
&& reference.id().getPath().startsWith("biome/")
? Component.translatableWithFallback(metadata.nameTranslationKey(),
@@ -324,4 +331,9 @@ public final class PageItem extends Item {
: 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.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
// during generation can only open the finished book, never a stale client copy.
player.inventoryMenu.broadcastChanges();
@@ -177,7 +172,6 @@ public final class ModNetworking {
access.stack().set(ModDataComponents.BOOK_AUTHOR.get(), player.getGameProfile().getName());
AgeRegistrySavedData.get(player.serverLevel()).find(reference.id()).ifPresent(definition -> {
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.sync().run();
@@ -50,7 +50,7 @@ public final class ModCreativeTabs {
.build());
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 =
registerSymbolPageTabs();
@@ -87,7 +87,9 @@ public record SymbolSemantics(Role role, Value value) {
ANGLE_MODIFIER("angle_modifier", Kind.NUMBER),
PHASE_MODIFIER("phase_modifier", Kind.NUMBER),
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 Kind kind;
@@ -168,7 +168,7 @@ public final class WritingGrammar {
case SKY -> 6;
case PERIOD_MODIFIER, ANGLE_MODIFIER, PHASE_MODIFIER -> 7;
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.block": "Rivencraft Block 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"
}