Public Access
This commit is contained in:
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"
|
||||
}
|
||||
Reference in new issue
Block a user