Public Access
Complete instability regression slice and ordered modifiers (#73)
Build / build (push) Canceled after 0s
Build / build (push) Canceled after 0s
This commit is contained in:
1 parent
4f464dbe60
commit
44205f80e0
14 files changed
+373
-43
No files matched your search
@@ -3,8 +3,10 @@ package net.dnorberg.rivencraft.book;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.config.RivencraftConfig;
|
||||
@@ -16,6 +18,7 @@ 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.common.Tags;
|
||||
import net.neoforged.neoforge.event.level.ChunkEvent;
|
||||
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
|
||||
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||
@@ -24,7 +27,19 @@ import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||
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 volatile Map<ResourceLocation, Double> weights;
|
||||
private static final Map<ResourceLocation, Double> VANILLA_WEIGHTS = weights();
|
||||
private static final Map<String, Double> COMMON_MINERAL_WEIGHTS = Map.ofEntries(
|
||||
Map.entry("coal", 5.0), Map.entry("sulfur", 5.0), Map.entry("sulphur", 5.0),
|
||||
Map.entry("salt", 5.0), Map.entry("tin", 60.0), Map.entry("lead", 60.0),
|
||||
Map.entry("aluminum", 60.0), Map.entry("aluminium", 60.0),
|
||||
Map.entry("zinc", 60.0), Map.entry("copper", 60.0),
|
||||
Map.entry("iron", 60.0), Map.entry("nickel", 200.0),
|
||||
Map.entry("osmium", 200.0), Map.entry("silver", 200.0),
|
||||
Map.entry("emerald", 200.0), Map.entry("ruby", 200.0),
|
||||
Map.entry("sapphire", 200.0), Map.entry("redstone", 250.0),
|
||||
Map.entry("gold", 750.0), Map.entry("uranium", 750.0),
|
||||
Map.entry("platinum", 750.0), Map.entry("diamond", 4000.0),
|
||||
Map.entry("iridium", 4000.0), Map.entry("netherite", 8000.0));
|
||||
private static final int MAX_PENDING = 2048;
|
||||
private static final int SAMPLES_PER_TICK = 4;
|
||||
private static final int GENERATION_INTERVAL_TICKS = 20;
|
||||
@@ -38,15 +53,73 @@ public final class AgeBlockInstability {
|
||||
|
||||
private AgeBlockInstability() {}
|
||||
|
||||
static double weight(ResourceLocation id) { return watchedWeights().getOrDefault(id, 0.0); }
|
||||
/** Track recognized materials from installed mods without requiring any mod-specific API. */
|
||||
static boolean tracked(ResourceLocation id) {
|
||||
if (VANILLA_WEIGHTS.containsKey(id)) return true;
|
||||
if (id.getNamespace().equals("minecraft") || id.getNamespace().equals("rivencraft")) return false;
|
||||
if (!BuiltInRegistries.BLOCK.containsKey(id)) return false;
|
||||
var block = BuiltInRegistries.BLOCK.get(id);
|
||||
return eligibleMaterial(block) && (configuredOverride(id) != null || inferredModdedWeight(id, block) > 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;
|
||||
static double weight(ResourceLocation id) {
|
||||
Double vanilla = VANILLA_WEIGHTS.get(id);
|
||||
if (vanilla != null) return vanilla;
|
||||
if (id.getNamespace().equals("minecraft") || id.getNamespace().equals("rivencraft")
|
||||
|| !BuiltInRegistries.BLOCK.containsKey(id)) return 0;
|
||||
var block = BuiltInRegistries.BLOCK.get(id);
|
||||
if (!eligibleMaterial(block)) return 0;
|
||||
return moddedWeight(id, inferredModdedWeight(id, block),
|
||||
RivencraftConfig.moddedMaterialOverrides());
|
||||
}
|
||||
|
||||
private static boolean eligibleMaterial(net.minecraft.world.level.block.Block block) {
|
||||
return BlockSymbolPages.isRockMaterial(block) || BlockSymbolPages.isFluidMaterial(block);
|
||||
}
|
||||
|
||||
private static Double configuredOverride(ResourceLocation id) {
|
||||
return configuredOverride(id, RivencraftConfig.moddedMaterialOverrides());
|
||||
}
|
||||
|
||||
private static Double configuredOverride(ResourceLocation id, List<? extends String> overrides) {
|
||||
for (String entry : overrides) {
|
||||
int separator = entry.lastIndexOf('=');
|
||||
if (separator > 0 && entry.substring(0, separator).equals(id.toString()))
|
||||
return (double) Integer.parseInt(entry.substring(separator + 1));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static double moddedWeight(ResourceLocation id, double inferred, List<? extends String> overrides) {
|
||||
Double override = configuredOverride(id, overrides);
|
||||
return override == null ? inferred : override;
|
||||
}
|
||||
|
||||
private static double inferredModdedWeight(ResourceLocation id, net.minecraft.world.level.block.Block block) {
|
||||
if (BlockSymbolPages.isFluidMaterial(block)) return 1.25;
|
||||
var state = block.defaultBlockState();
|
||||
String path = id.getPath();
|
||||
boolean ore = state.is(Tags.Blocks.ORES) || path.endsWith("_ore") || path.contains("_ore_");
|
||||
boolean storage = state.is(Tags.Blocks.STORAGE_BLOCKS)
|
||||
|| (path.endsWith("_block") || path.startsWith("block_of_"))
|
||||
&& namedMineralWeight(path) != null;
|
||||
return inferredModdedWeight(id, ore, storage, false);
|
||||
}
|
||||
|
||||
static double inferredModdedWeight(ResourceLocation id, boolean ore, boolean storage, boolean fluid) {
|
||||
if (fluid) return 1.25;
|
||||
Double named = namedMineralWeight(id.getPath());
|
||||
if (ore) return named == null ? 60.0 : named;
|
||||
if (storage) return named == null ? 120.0 : named;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static Double namedMineralWeight(String path) {
|
||||
for (String part : path.split("[/_]")) {
|
||||
Double value = COMMON_MINERAL_WEIGHTS.get(part);
|
||||
if (value != null) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void onChunkLoad(ChunkEvent.Load event) {
|
||||
@@ -174,10 +247,14 @@ public final class AgeBlockInstability {
|
||||
if (ageChunks < AgeBlockProfileSavedData.MIN_AGE_SAMPLES
|
||||
|| baselineChunks < AgeBlockProfileSavedData.MIN_BASELINE_SAMPLES) return 0;
|
||||
double result = 0;
|
||||
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();
|
||||
Set<ResourceLocation> watched = new HashSet<>(age.keySet());
|
||||
watched.addAll(baseline.keySet());
|
||||
for (ResourceLocation id : watched) {
|
||||
double weight = weight(id);
|
||||
if (weight <= 0) continue;
|
||||
double excess = age.getOrDefault(id, 0L) / (double) ageChunks
|
||||
- 1.05 * baseline.getOrDefault(id, 0L) / (double) baselineChunks;
|
||||
if (excess > 0) result += excess * weight;
|
||||
}
|
||||
return (int) Math.min(100_000, Math.round(result));
|
||||
}
|
||||
@@ -237,13 +314,6 @@ public final class AgeBlockInstability {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import net.minecraft.world.level.saveddata.SavedData;
|
||||
|
||||
/** Persistent, bounded samples of generated block abundance for one dimension. */
|
||||
public final class AgeBlockProfileSavedData extends SavedData {
|
||||
private static final int PROFILE_VERSION = 2;
|
||||
static final int MIN_AGE_SAMPLES = 401;
|
||||
static final int MIN_BASELINE_SAMPLES = 500;
|
||||
static final int MAX_SAMPLES = 1024;
|
||||
@@ -43,7 +44,7 @@ public final class AgeBlockProfileSavedData extends SavedData {
|
||||
if (section.hasOnlyAir()) continue;
|
||||
section.getStates().count((state, amount) -> {
|
||||
ResourceLocation id = BuiltInRegistries.BLOCK.getKey(state.getBlock());
|
||||
if (AgeBlockInstability.weight(id) > 0) counts.merge(id, amount, Integer::sum);
|
||||
if (AgeBlockInstability.tracked(id)) counts.merge(id, amount, Integer::sum);
|
||||
});
|
||||
}
|
||||
add(chunk.getPos(), counts);
|
||||
@@ -59,6 +60,7 @@ public final class AgeBlockProfileSavedData extends SavedData {
|
||||
|
||||
@Override
|
||||
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
tag.putInt("Version", PROFILE_VERSION);
|
||||
ListTag entries = new ListTag();
|
||||
for (var sample : samples.entrySet()) {
|
||||
CompoundTag entry = new CompoundTag();
|
||||
@@ -74,6 +76,9 @@ public final class AgeBlockProfileSavedData extends SavedData {
|
||||
|
||||
static AgeBlockProfileSavedData load(CompoundTag tag, HolderLookup.Provider registries) {
|
||||
AgeBlockProfileSavedData data = new AgeBlockProfileSavedData();
|
||||
// Earlier profiles omitted some modded candidates. Resample rather than
|
||||
// mixing incompatible chunk averages after installing content mods.
|
||||
if (tag.getInt("Version") != PROFILE_VERSION) return data;
|
||||
ListTag entries = tag.getList("Samples", CompoundTag.TAG_COMPOUND);
|
||||
for (int index = 0; index < Math.min(entries.size(), MAX_SAMPLES); index++) {
|
||||
CompoundTag entry = entries.getCompound(index);
|
||||
@@ -81,7 +86,7 @@ public final class AgeBlockProfileSavedData extends SavedData {
|
||||
Map<ResourceLocation, Integer> counts = new HashMap<>();
|
||||
for (String key : blocks.getAllKeys()) {
|
||||
ResourceLocation id = ResourceLocation.tryParse(key);
|
||||
if (id != null && AgeBlockInstability.weight(id) > 0 && blocks.getInt(key) > 0)
|
||||
if (id != null && blocks.getInt(key) > 0)
|
||||
counts.put(id, blocks.getInt(key));
|
||||
}
|
||||
data.add(new ChunkPos(entry.getLong("Chunk")), counts);
|
||||
|
||||
@@ -10,7 +10,17 @@ public final class AgeEnvironmentCompiler {
|
||||
|
||||
public static AgeTraits apply(AgeGenerationPlan plan, AgeTraits seeded) {
|
||||
if (plan == null) return seeded;
|
||||
if (plan.version() >= 4) {
|
||||
AgeTraits base = applyLegacy(plan, seeded);
|
||||
CelestialWriting ordered = orderedCelestial(plan, seeded);
|
||||
return new AgeTraits(base.biomeDistribution(), base.lighting(), base.weather(),
|
||||
ordered.period(), base.celestialAngle(), ordered.phase());
|
||||
}
|
||||
return applyLegacy(plan, seeded);
|
||||
}
|
||||
|
||||
/** Version-3 plans keep their frozen, stage-sorted environment interpretation. */
|
||||
private static AgeTraits applyLegacy(AgeGenerationPlan plan, AgeTraits seeded) {
|
||||
AgeTraits.Lighting lighting = seeded.lighting();
|
||||
AgeTraits.Weather weather = seeded.weather();
|
||||
double period = seeded.celestialPeriod();
|
||||
@@ -73,10 +83,45 @@ public final class AgeEnvironmentCompiler {
|
||||
}
|
||||
|
||||
public static boolean hasWrittenPhase(AgeGenerationPlan plan) {
|
||||
return plan != null && plan.activeSymbols().stream()
|
||||
if (plan == null) return false;
|
||||
if (plan.version() >= 4) return orderedCelestial(plan, null).writtenPhase();
|
||||
return plan.activeSymbols().stream()
|
||||
.anyMatch(symbol -> symbol.semantics().role() == SymbolSemantics.Role.PHASE_MODIFIER);
|
||||
}
|
||||
|
||||
private static CelestialWriting orderedCelestial(AgeGenerationPlan plan, AgeTraits seeded) {
|
||||
double period = seeded == null ? 1.0D : seeded.celestialPeriod();
|
||||
float phase = seeded == null ? 0.0F : seeded.celestialPhase();
|
||||
Double pendingPeriod = null;
|
||||
Float pendingPhase = null;
|
||||
boolean writtenPhase = false;
|
||||
for (AgeGenerationPlan.ResolvedSymbol symbol : plan.symbols()) {
|
||||
SymbolSemantics semantics = symbol.semantics();
|
||||
switch (semantics.role()) {
|
||||
case PERIOD_MODIFIER -> {
|
||||
double next = ((SymbolSemantics.NumberValue) semantics.value()).amount();
|
||||
pendingPeriod = pendingPeriod == null ? next : (pendingPeriod + next) / 2.0D;
|
||||
}
|
||||
case PHASE_MODIFIER -> {
|
||||
float next = (float) ((SymbolSemantics.NumberValue) semantics.value()).amount();
|
||||
pendingPhase = pendingPhase == null ? next : circularMidpoint(pendingPhase, next);
|
||||
}
|
||||
case CELESTIAL -> {
|
||||
period = pendingPeriod == null ? 1.0D : pendingPeriod;
|
||||
phase = pendingPhase == null ? (seeded == null ? 0.0F : seeded.celestialPhase())
|
||||
: pendingPhase / 360.0F;
|
||||
writtenPhase = pendingPhase != null;
|
||||
pendingPeriod = null;
|
||||
pendingPhase = null;
|
||||
}
|
||||
default -> { }
|
||||
}
|
||||
}
|
||||
return new CelestialWriting(period, phase, writtenPhase);
|
||||
}
|
||||
|
||||
private record CelestialWriting(double period, float phase, boolean writtenPhase) { }
|
||||
|
||||
public static int previewSkyColor(ResourceLocation effects, int biomeSkyColor) {
|
||||
if (effects.equals(BuiltinDimensionTypes.NETHER_EFFECTS)) return 0xFF6B2725;
|
||||
if (effects.equals(BuiltinDimensionTypes.END_EFFECTS)) return 0xFF211835;
|
||||
|
||||
@@ -18,7 +18,7 @@ import net.minecraft.resources.ResourceLocation;
|
||||
public record AgeGenerationPlan(int version, long ageSeed, OrderedWriting writing, List<ResolvedSymbol> symbols,
|
||||
AgeRuntimeProfile runtimeProfile, AgeBiomeSelection biomeSelection,
|
||||
List<Integer> activeSymbolIndices, int writingInstability) {
|
||||
public static final int CURRENT_VERSION = 3;
|
||||
public static final int CURRENT_VERSION = 4;
|
||||
|
||||
public AgeGenerationPlan {
|
||||
if (version < 1 || version > CURRENT_VERSION) {
|
||||
|
||||
@@ -56,13 +56,20 @@ public record AgeInstability(int version, int score, List<Contribution> breakdow
|
||||
contributions.add(new Contribution("random_baseline", -1, 0, "Page-free or Link-Panel-only Age"));
|
||||
} else {
|
||||
contributions.add(new Contribution("authored_baseline", -1, 0, "Authored Age"));
|
||||
WritingGrammar.Result parsed = WritingGrammar.parse(plan.writing());
|
||||
if (!parsed.canCreate() || parsed.writingInstability() != plan.writingInstability()) {
|
||||
throw new IllegalArgumentException("Writing changed between Age compilation and scoring");
|
||||
}
|
||||
for (WritingGrammar.Diagnostic diagnostic : parsed.diagnostics()) {
|
||||
contributions.add(new Contribution(diagnostic.code().name().toLowerCase(java.util.Locale.ROOT),
|
||||
diagnostic.index(), diagnostic.instability(), diagnostic.message()));
|
||||
if (plan.version() < 4) {
|
||||
// An old plan's writing cost was frozen under the previous grammar.
|
||||
// Never validate it against the new ordered-modifier interpretation.
|
||||
contributions.add(new Contribution("legacy_writing", -1, plan.writingInstability(),
|
||||
"Writing cost frozen by the original Age plan"));
|
||||
} else {
|
||||
WritingGrammar.Result parsed = WritingGrammar.parse(plan.writing());
|
||||
if (!parsed.canCreate() || parsed.writingInstability() != plan.writingInstability()) {
|
||||
throw new IllegalArgumentException("Writing changed between Age compilation and scoring");
|
||||
}
|
||||
for (WritingGrammar.Diagnostic diagnostic : parsed.diagnostics()) {
|
||||
contributions.add(new Contribution(diagnostic.code().name().toLowerCase(java.util.Locale.ROOT),
|
||||
diagnostic.index(), diagnostic.instability(), diagnostic.message()));
|
||||
}
|
||||
}
|
||||
addAuditedSymbolCosts(plan, registries, contributions);
|
||||
}
|
||||
|
||||
@@ -193,6 +193,48 @@ public final class EnvironmentWritingGameTests {
|
||||
});
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void celestialModifiersFollowWrittenOrder(GameTestHelper helper) {
|
||||
AgeTraits seeded = AgeTraits.select(42L);
|
||||
OrderedWriting consumed = new OrderedWriting(List.of(WritingEntry.linkPanel(),
|
||||
page("period/half"), page("period/double"), page("phase/rising"),
|
||||
page("celestial/normal_time")));
|
||||
AgeGenerationPlan consumedPlan = AgeGenerationPlan.compile(consumed, 42L,
|
||||
AgeGenerationProfiles.STANDARD, seeded, helper.getLevel().registryAccess());
|
||||
AgeTraits consumedTraits = AgeEnvironmentCompiler.apply(consumedPlan, seeded);
|
||||
helper.assertValueEqual(consumedTraits.celestialPeriod(), 1.25D,
|
||||
"Repeated period pages must combine before Normal Time consumes them");
|
||||
helper.assertValueEqual(consumedTraits.celestialPhase(), 0.25F,
|
||||
"A preceding phase page must be consumed by Normal Time");
|
||||
helper.assertValueEqual(consumedPlan.writingInstability(), 0,
|
||||
"Consumed modifiers must not be charged as dangling");
|
||||
|
||||
OrderedWriting dangling = new OrderedWriting(List.of(WritingEntry.linkPanel(),
|
||||
page("celestial/normal_time"), page("period/zero"), page("phase/rising")));
|
||||
AgeGenerationPlan danglingPlan = AgeGenerationPlan.compile(dangling, 42L,
|
||||
AgeGenerationProfiles.STANDARD, seeded, helper.getLevel().registryAccess());
|
||||
AgeTraits danglingTraits = AgeEnvironmentCompiler.apply(danglingPlan, seeded);
|
||||
helper.assertValueEqual(danglingTraits.celestialPeriod(), 1.0D,
|
||||
"A period after Normal Time must not change the already-written sun");
|
||||
helper.assertValueEqual(danglingTraits.celestialPhase(), seeded.celestialPhase(),
|
||||
"A phase after Normal Time must not change the already-written sun");
|
||||
helper.assertFalse(AgeEnvironmentCompiler.hasWrittenPhase(danglingPlan),
|
||||
"An unconsumed phase page must not set the Age's start time");
|
||||
helper.assertValueEqual(danglingPlan.writingInstability(), 200,
|
||||
"Each unconsumed modifier kind must contribute Legacy's +100");
|
||||
helper.assertValueEqual(AgeEnvironmentCompiler.apply(AgeGenerationPlan.load(danglingPlan.save()), seeded),
|
||||
danglingTraits, "Ordered modifier behavior must survive reload");
|
||||
AgeGenerationPlan previousVersion = new AgeGenerationPlan(3, danglingPlan.ageSeed(),
|
||||
danglingPlan.writing(), danglingPlan.symbols(), danglingPlan.runtimeProfile(),
|
||||
danglingPlan.biomeSelection(), danglingPlan.activeSymbolIndices(), danglingPlan.writingInstability());
|
||||
helper.assertTrue(AgeEnvironmentCompiler.apply(previousVersion, seeded).hasStoppedCelestialCycle(),
|
||||
"An existing version-3 Age must retain its earlier stage-sorted time behavior");
|
||||
helper.assertTrue(AgeInstability.compute(previousVersion, seeded, helper.getLevel().registryAccess())
|
||||
.breakdown().stream().anyMatch(entry -> entry.reason().equals("legacy_writing")),
|
||||
"Version-3 writing cost must not be revalidated under version-4 grammar");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
private static WritingEntry page(String path) {
|
||||
return WritingEntry.symbol(new SymbolReference(ResourceLocation.fromNamespaceAndPath("rivencraft", path)));
|
||||
}
|
||||
|
||||
@@ -109,6 +109,33 @@ public final class Milestone6RegressionGameTests {
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void commonModdedMaterialWeightsStayBounded(GameTestHelper helper) {
|
||||
var tin = net.minecraft.resources.ResourceLocation.parse("examplemod:deepslate_tin_ore");
|
||||
var uranium = net.minecraft.resources.ResourceLocation.parse("examplemod:raw_uranium_block");
|
||||
var oil = net.minecraft.resources.ResourceLocation.parse("examplemod:oil");
|
||||
var unknown = net.minecraft.resources.ResourceLocation.parse("examplemod:unknown_ore");
|
||||
helper.assertValueEqual(AgeBlockInstability.inferredModdedWeight(tin, true, false, false), 60.0,
|
||||
"Common tin ore should use the low mineral tier");
|
||||
helper.assertValueEqual(AgeBlockInstability.inferredModdedWeight(uranium, false, true, false), 750.0,
|
||||
"A common radioactive mineral block should use its material tier");
|
||||
helper.assertValueEqual(AgeBlockInstability.inferredModdedWeight(oil, false, false, true), 1.25,
|
||||
"Modded source fluids should use the Legacy fluid factor");
|
||||
helper.assertValueEqual(AgeBlockInstability.inferredModdedWeight(unknown, true, false, false), 60.0,
|
||||
"Unknown but tagged ores should use a conservative fallback");
|
||||
helper.assertValueEqual(AgeBlockInstability.inferredModdedWeight(unknown, false, false, false), 0.0,
|
||||
"Uncategorized modded blocks must not be watched automatically");
|
||||
helper.assertValueEqual(AgeBlockInstability.moddedWeight(tin, 60.0, List.of()), 60.0,
|
||||
"Recognized ore from an installed mod should be scored automatically");
|
||||
helper.assertValueEqual(AgeBlockInstability.moddedWeight(tin, 60.0,
|
||||
List.of("examplemod:deepslate_tin_ore=750")), 750.0,
|
||||
"A block override should replace the discovered weight");
|
||||
helper.assertValueEqual(AgeBlockInstability.moddedWeight(tin, 60.0,
|
||||
List.of("examplemod:deepslate_tin_ore=0")), 0.0,
|
||||
"A zero-weight override should exclude a discovered block");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void blockProfileSamplesPersistWithoutDoubleCounting(GameTestHelper helper) {
|
||||
var data = new AgeBlockProfileSavedData();
|
||||
@@ -121,6 +148,11 @@ public final class Milestone6RegressionGameTests {
|
||||
helper.getLevel().registryAccess()), helper.getLevel().registryAccess());
|
||||
helper.assertValueEqual(restored.count(), 1, "The profiled chunk set must survive restart");
|
||||
helper.assertValueEqual(restored.totals(), data.totals(), "Block counts must survive restart");
|
||||
var oldProfile = data.save(new net.minecraft.nbt.CompoundTag(), helper.getLevel().registryAccess());
|
||||
oldProfile.remove("Version");
|
||||
helper.assertValueEqual(AgeBlockProfileSavedData.load(oldProfile,
|
||||
helper.getLevel().registryAccess()).count(), 0,
|
||||
"Profiles lacking modded candidate counts must resample after migration");
|
||||
helper.succeed();
|
||||
}
|
||||
}
|
||||
@@ -155,6 +155,8 @@ public final class RivencraftCommands {
|
||||
source.sendSuccess(() -> Component.literal("Block profile: Age " + ageSamples
|
||||
+ "/401, Overworld baseline " + baselineSamples + "/500"
|
||||
+ (ageLevel == null ? " (Age unloaded)" : "")), false);
|
||||
source.sendSuccess(() -> Component.literal("Modded materials: automatic when installed, "
|
||||
+ RivencraftConfig.moddedMaterialOverrides().size() + " exact block override(s)"), false);
|
||||
}
|
||||
boolean enabled = AgeInstabilityControls.get(source.getLevel()).enabled(age.id());
|
||||
source.sendSuccess(() -> Component.literal("Runtime: " + (enabled ? "Age enabled" : "Age disabled")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.dnorberg.rivencraft.config;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.neoforged.neoforge.common.ModConfigSpec;
|
||||
@@ -32,6 +33,7 @@ public final class RivencraftConfig {
|
||||
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.ConfigValue<List<? extends String>> MODDED_MATERIAL_OVERRIDES;
|
||||
private static final ModConfigSpec.BooleanValue STATUS_EFFECTS_ENABLED;
|
||||
private static final ModConfigSpec.BooleanValue STATUS_EFFECTS_PLAYERS;
|
||||
private static final ModConfigSpec.BooleanValue STATUS_EFFECTS_NONPLAYERS;
|
||||
@@ -111,6 +113,13 @@ public final class RivencraftConfig {
|
||||
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_modded_materials");
|
||||
MODDED_MATERIAL_OVERRIDES = builder.comment(
|
||||
"Installed mods' recognized ores, mineral blocks and source fluids are scored automatically without requiring those mods.",
|
||||
"Optional exact block weights use modid:block_id=points; zero excludes a block. Example: [\"examplemod:rare_ore=4000\"].")
|
||||
.defineList("block_weight_overrides", List.<String>of(), () -> "examplemod:rare_ore=4000",
|
||||
RivencraftConfig::validModdedMaterialOverride);
|
||||
builder.pop();
|
||||
builder.push("instability_status_effects");
|
||||
STATUS_EFFECTS_ENABLED = builder.comment("Allow bounded instability status effects in loaded Ages.")
|
||||
.define("enabled", true);
|
||||
@@ -236,6 +245,22 @@ public final class RivencraftConfig {
|
||||
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 List<? extends String> moddedMaterialOverrides() { return MODDED_MATERIAL_OVERRIDES.get(); }
|
||||
|
||||
private static boolean validModdedMaterialOverride(Object value) {
|
||||
if (!(value instanceof String entry)) return false;
|
||||
int separator = entry.lastIndexOf('=');
|
||||
if (separator < 1) return false;
|
||||
ResourceLocation id = ResourceLocation.tryParse(entry.substring(0, separator));
|
||||
if (id == null || !entry.substring(0, separator).contains(":")
|
||||
|| id.getNamespace().equals("minecraft") || id.getNamespace().equals("rivencraft")) return false;
|
||||
try {
|
||||
int weight = Integer.parseInt(entry.substring(separator + 1));
|
||||
return weight >= 0 && weight <= 100_000;
|
||||
} catch (NumberFormatException ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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(); }
|
||||
|
||||
@@ -87,6 +87,12 @@ public final class WritingGrammarGameTests {
|
||||
WritingGrammar.Result repeatedModifiers = parse(resolver, WritingEntry.linkPanel(),
|
||||
WritingEntry.symbol(periodHalf), WritingEntry.symbol(periodFull), WritingEntry.symbol(celestial));
|
||||
helper.assertTrue(repeatedModifiers.canCreate(), "Repeated period modifiers must remain legal");
|
||||
helper.assertValueEqual(repeatedModifiers.writingInstability(), 0,
|
||||
"Repeated same-kind modifiers combine before the celestial page consumes them");
|
||||
WritingGrammar.Result unconsumed = parse(resolver, WritingEntry.linkPanel(),
|
||||
WritingEntry.symbol(periodHalf), WritingEntry.symbol(periodFull));
|
||||
helper.assertValueEqual(unconsumed.writingInstability(), 100,
|
||||
"Only the final combined modifier dangles when no celestial page follows");
|
||||
assertPenalty(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(terrain),
|
||||
WritingEntry.symbol(terrain)), WritingGrammar.Code.DUPLICATE);
|
||||
WritingGrammar.Result replacedTerrain = parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(terrain),
|
||||
@@ -96,8 +102,11 @@ public final class WritingGrammarGameTests {
|
||||
"The last supported controller must supply the effective terrain");
|
||||
assertPenalty(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(weather),
|
||||
WritingEntry.symbol(terrain)), WritingGrammar.Code.OUT_OF_ORDER);
|
||||
assertPenalty(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(celestial),
|
||||
WritingEntry.symbol(periodHalf)), WritingGrammar.Code.MISPLACED_MODIFIER);
|
||||
WritingGrammar.Result afterCelestial = parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(celestial),
|
||||
WritingEntry.symbol(periodHalf));
|
||||
assertPenalty(helper, afterCelestial, WritingGrammar.Code.DANGLING_MODIFIER);
|
||||
helper.assertValueEqual(afterCelestial.writingInstability(), 100,
|
||||
"A modifier after the last celestial page must be charged only once");
|
||||
assertPenalty(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(nativeBiomes),
|
||||
WritingEntry.symbol(plains)), WritingGrammar.Code.OUT_OF_ORDER);
|
||||
assertPenalty(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(plains),
|
||||
|
||||
@@ -40,6 +40,7 @@ public final class WritingGrammar {
|
||||
if (writing.entries().isEmpty()) return new Result(symbols, diagnostics);
|
||||
|
||||
EnumMap<Role, ParsedSymbol> singular = new EnumMap<>(Role.class);
|
||||
EnumMap<Role, ParsedSymbol> pendingModifiers = new EnumMap<>(Role.class);
|
||||
int panels = 0;
|
||||
int lastStage = -1;
|
||||
int biomeCount = 0;
|
||||
@@ -72,6 +73,13 @@ public final class WritingGrammar {
|
||||
Role role = semantics.orElseThrow().role();
|
||||
ParsedSymbol parsed = new ParsedSymbol(index, reference, semantics.orElseThrow());
|
||||
symbols.add(parsed);
|
||||
if (modifier(role)) {
|
||||
// Legacy modifier pages pop and combine an earlier modifier of the same
|
||||
// kind. Only the final pending value can be left dangling.
|
||||
pendingModifiers.put(role, parsed);
|
||||
} else if (role == Role.CELESTIAL) {
|
||||
pendingModifiers.clear();
|
||||
}
|
||||
if (pendingBlock != null && role != Role.FEATURE) {
|
||||
// A material page without a following feature receives a seeded compatible
|
||||
// feature when the Age plan is compiled.
|
||||
@@ -107,8 +115,8 @@ public final class WritingGrammar {
|
||||
}
|
||||
if (role == Role.BIOME) biomeCount++;
|
||||
int stage = stage(role);
|
||||
if (stage < lastStage) {
|
||||
diagnostics.add(new Diagnostic(modifier(role) ? Code.MISPLACED_MODIFIER : Code.OUT_OF_ORDER,
|
||||
if (stage < lastStage && !modifier(role)) {
|
||||
diagnostics.add(new Diagnostic(Code.OUT_OF_ORDER,
|
||||
index, "Move this " + role.id() + " page before later grammar categories"));
|
||||
} else {
|
||||
lastStage = stage;
|
||||
@@ -116,6 +124,10 @@ public final class WritingGrammar {
|
||||
}
|
||||
if (panels == 0) diagnostics.add(new Diagnostic(Code.MISSING_PANEL, -1,
|
||||
"Add a Link Panel as the first page"));
|
||||
for (ParsedSymbol dangling : pendingModifiers.values()) {
|
||||
diagnostics.add(new Diagnostic(Code.DANGLING_MODIFIER, dangling.index(),
|
||||
"This " + dangling.semantics().role().id() + " page has no later celestial page to consume it"));
|
||||
}
|
||||
if (symbols.isEmpty()) return new Result(symbols, diagnostics);
|
||||
|
||||
ParsedSymbol controller = singular.get(Role.BIOME_CONTROLLER);
|
||||
@@ -174,14 +186,14 @@ public final class WritingGrammar {
|
||||
|
||||
public enum Code {
|
||||
TOO_MANY, MISSING_PANEL, PANEL_ORDER, DUPLICATE_PANEL, UNRESOLVED, UNSUPPORTED,
|
||||
DUPLICATE, CONFLICT, OUT_OF_ORDER, MISPLACED_MODIFIER, INCOMPLETE, UNSUPPORTED_PROFILE,
|
||||
DUPLICATE, CONFLICT, OUT_OF_ORDER, MISPLACED_MODIFIER, DANGLING_MODIFIER, INCOMPLETE, UNSUPPORTED_PROFILE,
|
||||
UNKNOWN_BIOME, UNSUPPORTED_BIOME, UNSUPPORTED_ENVIRONMENT, UNSUPPORTED_FEATURE,
|
||||
UNSUPPORTED_COMBINATION;
|
||||
|
||||
public Severity severity() {
|
||||
return switch (this) {
|
||||
case INCOMPLETE -> Severity.SEEDED_COMPLETION;
|
||||
case DUPLICATE, CONFLICT, OUT_OF_ORDER, MISPLACED_MODIFIER -> Severity.INSTABILITY;
|
||||
case DUPLICATE, CONFLICT, OUT_OF_ORDER, MISPLACED_MODIFIER, DANGLING_MODIFIER -> Severity.INSTABILITY;
|
||||
default -> Severity.FATAL;
|
||||
};
|
||||
}
|
||||
@@ -205,7 +217,7 @@ public final class WritingGrammar {
|
||||
public int instability() {
|
||||
return switch (code) {
|
||||
case DUPLICATE, CONFLICT -> 500;
|
||||
case OUT_OF_ORDER, MISPLACED_MODIFIER -> 100;
|
||||
case OUT_OF_ORDER, MISPLACED_MODIFIER, DANGLING_MODIFIER -> 100;
|
||||
default -> 0;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user