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
+367
-37
No files matched your search
@@ -1,4 +1,4 @@
|
|||||||
# Live block-instability profiling (0.5.8.11–0.5.8.15)
|
# Live block-instability profiling (0.5.8.11–0.5.8.17)
|
||||||
|
|
||||||
Mystcraft Legacy measures the abundance of watched blocks in generated chunks,
|
Mystcraft Legacy measures the abundance of watched blocks in generated chunks,
|
||||||
compares that abundance with a baseline, and adds only positive excess to the
|
compares that abundance with a baseline, and adds only positive excess to the
|
||||||
@@ -26,8 +26,24 @@ Legacy weights are used for its watched blocks: coal/lapis 5, iron 60, emerald
|
|||||||
50. Deepslate variants inherit their ore's weight. Modern/high-value extensions
|
50. Deepslate variants inherit their ore's weight. Modern/high-value extensions
|
||||||
include diamond, emerald, gold, iron, raw gold, raw iron and netherite storage
|
include diamond, emerald, gold, iron, raw gold, raw iron and netherite storage
|
||||||
blocks, plus ancient debris. Netherite blocks cost 8,000 per excess block per
|
blocks, plus ancient debris. Netherite blocks cost 8,000 per excess block per
|
||||||
sampled chunk; ancient debris costs 4,000. Modded blocks are not automatically
|
sampled chunk; ancient debris costs 4,000.
|
||||||
classified as valuable; this avoids charging unknown decorative materials.
|
|
||||||
|
Other mods remain optional. When an installed mod registers common-tagged ores
|
||||||
|
or storage blocks, recognizable `*_ore` blocks, or source-fluid blocks,
|
||||||
|
Rivencraft automatically includes those blocks in live profiling. The source
|
||||||
|
fluid weight is 1.25; common mineral names such as tin,
|
||||||
|
lead, zinc, silver, uranium, platinum, and iridium have explicit tiers, while
|
||||||
|
other tagged ores use 60 and other tagged storage blocks use 120. The optional
|
||||||
|
`instability_modded_materials.block_weight_overrides` list accepts a full-block
|
||||||
|
`modid:block_id=points` entry to cover a block without common tags or replace
|
||||||
|
its inferred weight; zero excludes it. Block entities and non-source fluid
|
||||||
|
plants are excluded. Rivencraft never requires any particular content mod.
|
||||||
|
|
||||||
|
Profiles written before 0.5.8.17 did not retain all modded material
|
||||||
|
counts, so their block sample data resets and is collected again after upgrade.
|
||||||
|
The saved Age definition and authored score are not changed. Later config
|
||||||
|
changes can rescore materials already captured in the profile; a newly added
|
||||||
|
override for a block that was not previously recognized needs fresh samples.
|
||||||
|
|
||||||
The authored score and provider plan remain saved as the Age's immutable base.
|
The authored score and provider plan remain saved as the Age's immutable base.
|
||||||
Only Ages created with instability schema v2 participate; existing v1 Ages
|
Only Ages created with instability schema v2 participate; existing v1 Ages
|
||||||
@@ -72,5 +88,7 @@ Do not retry that stalled scenario automatically. In a disposable world:
|
|||||||
receive little or no ore contribution. Check that profiling does not make
|
receive little or no ore contribution. Check that profiling does not make
|
||||||
chunk loading noticeably stall, and report any exception or server hang.
|
chunk loading noticeably stall, and report any exception or server hang.
|
||||||
|
|
||||||
Do not use a valued save for this check. The game jar builds, but live chunk
|
Do not use a valued save for this check. The player reported an in-game pass
|
||||||
profiling still needs this player-run validation before issue #73 is closed.
|
for the 0.5.8.18 candidate on 2026-10-12. The three detailed scenarios above
|
||||||
|
remain useful for future regression runs; the report does not establish that
|
||||||
|
each was exercised individually.
|
||||||
@@ -27,6 +27,8 @@ handed to the player in [the block-profile checklist](LIVE_BLOCK_INSTABILITY.md)
|
|||||||
| Decay loaded-chunk target and spawn protection | `AgeDecayGameTests.targetMustRemainInChosenLoadedChunk`, `savedSpawnAndPlatformAreProtected` |
|
| Decay loaded-chunk target and spawn protection | `AgeDecayGameTests.targetMustRemainInChosenLoadedChunk`, `savedSpawnAndPlatformAreProtected` |
|
||||||
| Explosion policy gates and protected blocks | `Milestone6RegressionGameTests.disabledGriefAndProtectedBlocksStayUntouched` |
|
| Explosion policy gates and protected blocks | `Milestone6RegressionGameTests.disabledGriefAndProtectedBlocksStayUntouched` |
|
||||||
| No Overworld status or decay provider | `AgeStatusEffectGameTests.overworldCannotRunAnAgeStatusProvider`; `AgeDecayGameTests.overworldCannotRunDecayProvider` |
|
| No Overworld status or decay provider | `AgeStatusEffectGameTests.overworldCannotRunAnAgeStatusProvider`; `AgeDecayGameTests.overworldCannotRunDecayProvider` |
|
||||||
|
| Optional-mod material weights, block overrides, and old profile migration | `Milestone6RegressionGameTests.commonModdedMaterialWeightsStayBounded`, `blockProfileSamplesPersistWithoutDoubleCounting` |
|
||||||
|
| Ordered celestial modifier consumption, repeated modifiers, and dangling +100 per kind | `WritingGrammarGameTests.orderedGrammarAcceptsPartialWritingAndDiagnosesInvalidSequences`; `EnvironmentWritingGameTests.celestialModifiersFollowWrittenOrder` |
|
||||||
|
|
||||||
These tests exercise saved-data serialization and deterministic policies. They
|
These tests exercise saved-data serialization and deterministic policies. They
|
||||||
do **not** simulate a complete process restart, an actual multiplayer server,
|
do **not** simulate a complete process restart, an actual multiplayer server,
|
||||||
@@ -45,6 +47,9 @@ checks live only in the `instability` archive to avoid duplicating default tests
|
|||||||
That check is narrower than arbitrary missing biome, block, or worldgen
|
That check is narrower than arbitrary missing biome, block, or worldgen
|
||||||
registry entries: loading a world with removed data packs is not guaranteed.
|
registry entries: loading a world with removed data packs is not guaranteed.
|
||||||
Broad save compatibility and migration remain tracked in #60.
|
Broad save compatibility and migration remain tracked in #60.
|
||||||
|
- Version-3 plans keep their previous stage-sorted celestial interpretation and
|
||||||
|
frozen writing cost. Only new version-4 plans consume period/phase modifiers
|
||||||
|
in authored order and charge a trailing modifier once per pending kind.
|
||||||
- In-process roundtrips cannot prove that Age dimensions reload correctly
|
- In-process roundtrips cannot prove that Age dimensions reload correctly
|
||||||
after a full stop/start. Check that in a real client/server world. Do not add
|
after a full stop/start. Check that in a real client/server world. Do not add
|
||||||
another GameTest for it if it hits the documented storage stall; hand the
|
another GameTest for it if it hits the documented storage stall; hand the
|
||||||
@@ -81,6 +86,13 @@ entry from a valued world; copy the test save first.
|
|||||||
a disposable copy; confirm its Age season behavior and that Rivencraft
|
a disposable copy; confirm its Age season behavior and that Rivencraft
|
||||||
itself still loads. JEI should display the relevant pages and recipes when
|
itself still loads. JEI should display the relevant pages and recipes when
|
||||||
installed, while the mod works without it.
|
installed, while the mod works without it.
|
||||||
|
Separately, install a mod with common-tagged ores/storage blocks or source
|
||||||
|
fluids. Compare a material-rich Age with an ordinary Age after the profile
|
||||||
|
thresholds are reached; the installed mod's recognized blocks should affect
|
||||||
|
the result without a Rivencraft allowlist. Then remove that optional mod in
|
||||||
|
a disposable copy and confirm Rivencraft still starts. Test a single
|
||||||
|
`block_weight_overrides` entry and a zero-weight exclusion. This remains a
|
||||||
|
player-run scenario, not an automated pass.
|
||||||
5. Check the book preview separately with its fallback animation, Ponder/Create,
|
5. Check the book preview separately with its fallback animation, Ponder/Create,
|
||||||
and Immersive Portals when available. Opening a book should not change
|
and Immersive Portals when available. Opening a book should not change
|
||||||
player location, instability diagnostics, or ambient audio in the actual
|
player location, instability diagnostics, or ambient audio in the actual
|
||||||
@@ -93,3 +105,54 @@ entry from a valued world; copy the test save first.
|
|||||||
|
|
||||||
The matrix is guidance for a player-run pass; none of these six scenarios is
|
The matrix is guidance for a player-run pass; none of these six scenarios is
|
||||||
claimed as completed by the automated result above.
|
claimed as completed by the automated result above.
|
||||||
|
The player reported an in-game pass for the 0.5.8.18 candidate on 2026-10-12.
|
||||||
|
That is a release-slice acceptance report, not a claim that every expanded
|
||||||
|
matrix row below was individually exercised.
|
||||||
|
|
||||||
|
### Regression run record
|
||||||
|
|
||||||
|
For each row, record pass/fail, world seed, Age name, Rivencraft version,
|
||||||
|
optional-mod versions, and a short observation. "Not run" is intentional
|
||||||
|
until a player completes the in-game check; a successful build is not a pass.
|
||||||
|
|
||||||
|
| Area | Specific manual observation | Status |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| New vs reused book | Dimension ID, score, and providers do not reroll | Not run |
|
||||||
|
| Full process restart | Re-enter two Ages; saved traits, score, plan, and cooldowns persist | Not run |
|
||||||
|
| Multiplayer isolation | Two Age players receive effects; Overworld player does not | Not run |
|
||||||
|
| Loaded-chunk scope | Departing players permit unloading; hazards do not reach old chunks | Not run |
|
||||||
|
| Controls | Global, per-Age, mutation, and provider toggles suppress effects without reroll | Not run |
|
||||||
|
| Protected blocks | Explosion/decay/erosion spare arrival platforms, portals, and block entities | Not run |
|
||||||
|
| Hazard visuals | Lightning, explosions, decay, status, burning, meteor, and erosion each behave when selected | Not run |
|
||||||
|
| Third-party biomes | TerraBlender biome pages and cave classification survive Age creation/restart | Not run |
|
||||||
|
| Optional integrations absent | Mod launches without JEI, Create/Ponder, Immersive Portals, Serene Seasons, or biome mods | Not run |
|
||||||
|
| Optional integrations present | JEI pages/recipes, preview backends, seasonal behavior, and ambient sound work | Not run |
|
||||||
|
| Optional-mod materials | Tagged ore/storage/source-fluid samples change live profile automatically | Not run |
|
||||||
|
| Material overrides | Positive and zero `block_weight_overrides` values affect only named blocks | Not run |
|
||||||
|
| Removed optional mod | Disposable copy launches without the mod; unknown sampled IDs stay serialized | Not run |
|
||||||
|
| Removed symbol/handler | Frozen score and provider plan survive when nonessential data is removed | Not run |
|
||||||
|
| Ordered modifiers | Five book-order cases below produce expected time and dangling costs | Not run |
|
||||||
|
|
||||||
|
### Focused ordered-writing and optional-material checks for 0.5.8.18
|
||||||
|
|
||||||
|
Use `/rivencraft age instability` in each newly bound Age; old Ages retain
|
||||||
|
their original plan and score. Record each row as pass/fail with the Age name,
|
||||||
|
world seed, installed optional mods, and the command output. The ordered
|
||||||
|
modifier assertions are archived, not a claim that the stalled GameTest suite ran.
|
||||||
|
|
||||||
|
| Book pages after Link Panel, in exact order | Expected result | Result |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Half Length, Double Length, Normal Time | Length combines to 1.25×, no dangling cost | Not run in game |
|
||||||
|
| Normal Time, Zero Length | Sun keeps normal motion; one dangling +100 | Not run in game |
|
||||||
|
| Rising, Normal Time, Setting | Rising start phase; Setting dangles +100 | Not run in game |
|
||||||
|
| Half Length, Double Length, no celestial page | One combined pending length dangles +100; seeded clock remains | Not run in game |
|
||||||
|
| Normal Time, Zero Length, Normal Time | Last sun consumes Zero Length; duplicate celestial cost remains separate | Not run in game |
|
||||||
|
|
||||||
|
With a common-tagged ore/storage-block or source-fluid mod installed, bind
|
||||||
|
comparable material-rich and ordinary Ages and wait for profiling thresholds.
|
||||||
|
Verify the modded material contributes automatically, then restart and confirm
|
||||||
|
the score/profile remains consistent. On a disposable copy, remove the mod and
|
||||||
|
confirm Rivencraft launches without it; restore the mod before judging the
|
||||||
|
world's registry-dependent terrain. Also check a zero-weight override and a
|
||||||
|
positive override. These are optional-mod compatibility checks, not a new
|
||||||
|
required dependency.
|
||||||
+1
-1
@@ -36,7 +36,7 @@ mod_name=Rivencraft
|
|||||||
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
||||||
mod_license=LGPL-3.0-or-later
|
mod_license=LGPL-3.0-or-later
|
||||||
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
||||||
mod_version=0.5.8.15
|
mod_version=0.5.8.18
|
||||||
immersive_portals_version=6.0.7
|
immersive_portals_version=6.0.7
|
||||||
terrablender_version=4.1.0.8
|
terrablender_version=4.1.0.8
|
||||||
serene_seasons_version_id=B6nTjh4b
|
serene_seasons_version_id=B6nTjh4b
|
||||||
|
|||||||
@@ -3,8 +3,10 @@ package net.dnorberg.rivencraft.book;
|
|||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import net.dnorberg.rivencraft.Rivencraft;
|
import net.dnorberg.rivencraft.Rivencraft;
|
||||||
import net.dnorberg.rivencraft.config.RivencraftConfig;
|
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.ChunkPos;
|
||||||
import net.minecraft.world.level.chunk.LevelChunk;
|
import net.minecraft.world.level.chunk.LevelChunk;
|
||||||
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
import net.minecraft.world.level.chunk.status.ChunkStatus;
|
||||||
|
import net.neoforged.neoforge.common.Tags;
|
||||||
import net.neoforged.neoforge.event.level.ChunkEvent;
|
import net.neoforged.neoforge.event.level.ChunkEvent;
|
||||||
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
|
import net.neoforged.neoforge.event.server.ServerStoppingEvent;
|
||||||
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
||||||
@@ -24,7 +27,19 @@ import net.neoforged.neoforge.event.tick.ServerTickEvent;
|
|||||||
public final class AgeBlockInstability {
|
public final class AgeBlockInstability {
|
||||||
// Legacy's watched blocks and factor-1 values; modern ore variants share their parent weight.
|
// Legacy's watched blocks and factor-1 values; modern ore variants share their parent weight.
|
||||||
// Storage blocks are deliberate extensions, including the requested diamond-block case.
|
// Storage blocks are deliberate extensions, including the requested diamond-block case.
|
||||||
private static 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 MAX_PENDING = 2048;
|
||||||
private static final int SAMPLES_PER_TICK = 4;
|
private static final int SAMPLES_PER_TICK = 4;
|
||||||
private static final int GENERATION_INTERVAL_TICKS = 20;
|
private static final int GENERATION_INTERVAL_TICKS = 20;
|
||||||
@@ -38,15 +53,73 @@ public final class AgeBlockInstability {
|
|||||||
|
|
||||||
private 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) {
|
||||||
private static Map<ResourceLocation, Double> watchedWeights() {
|
if (VANILLA_WEIGHTS.containsKey(id)) return true;
|
||||||
Map<ResourceLocation, Double> current = weights;
|
if (id.getNamespace().equals("minecraft") || id.getNamespace().equals("rivencraft")) return false;
|
||||||
if (current != null) return current;
|
if (!BuiltInRegistries.BLOCK.containsKey(id)) return false;
|
||||||
synchronized (AgeBlockInstability.class) {
|
var block = BuiltInRegistries.BLOCK.get(id);
|
||||||
if (weights == null) weights = weights();
|
return eligibleMaterial(block) && (configuredOverride(id) != null || inferredModdedWeight(id, block) > 0);
|
||||||
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) {
|
public static void onChunkLoad(ChunkEvent.Load event) {
|
||||||
@@ -174,10 +247,14 @@ public final class AgeBlockInstability {
|
|||||||
if (ageChunks < AgeBlockProfileSavedData.MIN_AGE_SAMPLES
|
if (ageChunks < AgeBlockProfileSavedData.MIN_AGE_SAMPLES
|
||||||
|| baselineChunks < AgeBlockProfileSavedData.MIN_BASELINE_SAMPLES) return 0;
|
|| baselineChunks < AgeBlockProfileSavedData.MIN_BASELINE_SAMPLES) return 0;
|
||||||
double result = 0;
|
double result = 0;
|
||||||
for (var entry : watchedWeights().entrySet()) {
|
Set<ResourceLocation> watched = new HashSet<>(age.keySet());
|
||||||
double excess = age.getOrDefault(entry.getKey(), 0L) / (double) ageChunks
|
watched.addAll(baseline.keySet());
|
||||||
- 1.05 * baseline.getOrDefault(entry.getKey(), 0L) / (double) baselineChunks;
|
for (ResourceLocation id : watched) {
|
||||||
if (excess > 0) result += excess * entry.getValue();
|
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));
|
return (int) Math.min(100_000, Math.round(result));
|
||||||
}
|
}
|
||||||
@@ -237,13 +314,6 @@ public final class AgeBlockInstability {
|
|||||||
add(values, 4000, "ancient_debris");
|
add(values, 4000, "ancient_debris");
|
||||||
add(values, 60, "iron_block", "raw_iron_block");
|
add(values, 60, "iron_block", "raw_iron_block");
|
||||||
add(values, 8000, "netherite_block");
|
add(values, 8000, "netherite_block");
|
||||||
// Legacy automatically watched eligible non-vanilla fluid blocks with
|
|
||||||
// accessibility=1 and flat=0.25. Our abundance-only sampler uses their
|
|
||||||
// combined default factor until a spatial accessibility map is available.
|
|
||||||
BuiltInRegistries.BLOCK.keySet().stream()
|
|
||||||
.filter(id -> !id.getNamespace().equals("minecraft"))
|
|
||||||
.filter(id -> BlockSymbolPages.isFluidMaterial(BuiltInRegistries.BLOCK.get(id)))
|
|
||||||
.forEach(id -> values.putIfAbsent(id, 1.25));
|
|
||||||
return Map.copyOf(values);
|
return Map.copyOf(values);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import net.minecraft.world.level.saveddata.SavedData;
|
|||||||
|
|
||||||
/** Persistent, bounded samples of generated block abundance for one dimension. */
|
/** Persistent, bounded samples of generated block abundance for one dimension. */
|
||||||
public final class AgeBlockProfileSavedData extends SavedData {
|
public final class AgeBlockProfileSavedData extends SavedData {
|
||||||
|
private static final int PROFILE_VERSION = 2;
|
||||||
static final int MIN_AGE_SAMPLES = 401;
|
static final int MIN_AGE_SAMPLES = 401;
|
||||||
static final int MIN_BASELINE_SAMPLES = 500;
|
static final int MIN_BASELINE_SAMPLES = 500;
|
||||||
static final int MAX_SAMPLES = 1024;
|
static final int MAX_SAMPLES = 1024;
|
||||||
@@ -43,7 +44,7 @@ public final class AgeBlockProfileSavedData extends SavedData {
|
|||||||
if (section.hasOnlyAir()) continue;
|
if (section.hasOnlyAir()) continue;
|
||||||
section.getStates().count((state, amount) -> {
|
section.getStates().count((state, amount) -> {
|
||||||
ResourceLocation id = BuiltInRegistries.BLOCK.getKey(state.getBlock());
|
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);
|
add(chunk.getPos(), counts);
|
||||||
@@ -59,6 +60,7 @@ public final class AgeBlockProfileSavedData extends SavedData {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
|
public CompoundTag save(CompoundTag tag, HolderLookup.Provider registries) {
|
||||||
|
tag.putInt("Version", PROFILE_VERSION);
|
||||||
ListTag entries = new ListTag();
|
ListTag entries = new ListTag();
|
||||||
for (var sample : samples.entrySet()) {
|
for (var sample : samples.entrySet()) {
|
||||||
CompoundTag entry = new CompoundTag();
|
CompoundTag entry = new CompoundTag();
|
||||||
@@ -74,6 +76,9 @@ public final class AgeBlockProfileSavedData extends SavedData {
|
|||||||
|
|
||||||
static AgeBlockProfileSavedData load(CompoundTag tag, HolderLookup.Provider registries) {
|
static AgeBlockProfileSavedData load(CompoundTag tag, HolderLookup.Provider registries) {
|
||||||
AgeBlockProfileSavedData data = new AgeBlockProfileSavedData();
|
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);
|
ListTag entries = tag.getList("Samples", CompoundTag.TAG_COMPOUND);
|
||||||
for (int index = 0; index < Math.min(entries.size(), MAX_SAMPLES); index++) {
|
for (int index = 0; index < Math.min(entries.size(), MAX_SAMPLES); index++) {
|
||||||
CompoundTag entry = entries.getCompound(index);
|
CompoundTag entry = entries.getCompound(index);
|
||||||
@@ -81,7 +86,7 @@ public final class AgeBlockProfileSavedData extends SavedData {
|
|||||||
Map<ResourceLocation, Integer> counts = new HashMap<>();
|
Map<ResourceLocation, Integer> counts = new HashMap<>();
|
||||||
for (String key : blocks.getAllKeys()) {
|
for (String key : blocks.getAllKeys()) {
|
||||||
ResourceLocation id = ResourceLocation.tryParse(key);
|
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));
|
counts.put(id, blocks.getInt(key));
|
||||||
}
|
}
|
||||||
data.add(new ChunkPos(entry.getLong("Chunk")), counts);
|
data.add(new ChunkPos(entry.getLong("Chunk")), counts);
|
||||||
|
|||||||
@@ -10,7 +10,17 @@ public final class AgeEnvironmentCompiler {
|
|||||||
|
|
||||||
public static AgeTraits apply(AgeGenerationPlan plan, AgeTraits seeded) {
|
public static AgeTraits apply(AgeGenerationPlan plan, AgeTraits seeded) {
|
||||||
if (plan == null) return 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.Lighting lighting = seeded.lighting();
|
||||||
AgeTraits.Weather weather = seeded.weather();
|
AgeTraits.Weather weather = seeded.weather();
|
||||||
double period = seeded.celestialPeriod();
|
double period = seeded.celestialPeriod();
|
||||||
@@ -73,10 +83,45 @@ public final class AgeEnvironmentCompiler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean hasWrittenPhase(AgeGenerationPlan plan) {
|
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);
|
.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) {
|
public static int previewSkyColor(ResourceLocation effects, int biomeSkyColor) {
|
||||||
if (effects.equals(BuiltinDimensionTypes.NETHER_EFFECTS)) return 0xFF6B2725;
|
if (effects.equals(BuiltinDimensionTypes.NETHER_EFFECTS)) return 0xFF6B2725;
|
||||||
if (effects.equals(BuiltinDimensionTypes.END_EFFECTS)) return 0xFF211835;
|
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,
|
public record AgeGenerationPlan(int version, long ageSeed, OrderedWriting writing, List<ResolvedSymbol> symbols,
|
||||||
AgeRuntimeProfile runtimeProfile, AgeBiomeSelection biomeSelection,
|
AgeRuntimeProfile runtimeProfile, AgeBiomeSelection biomeSelection,
|
||||||
List<Integer> activeSymbolIndices, int writingInstability) {
|
List<Integer> activeSymbolIndices, int writingInstability) {
|
||||||
public static final int CURRENT_VERSION = 3;
|
public static final int CURRENT_VERSION = 4;
|
||||||
|
|
||||||
public AgeGenerationPlan {
|
public AgeGenerationPlan {
|
||||||
if (version < 1 || version > CURRENT_VERSION) {
|
if (version < 1 || version > CURRENT_VERSION) {
|
||||||
|
|||||||
@@ -56,6 +56,12 @@ 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"));
|
contributions.add(new Contribution("random_baseline", -1, 0, "Page-free or Link-Panel-only Age"));
|
||||||
} else {
|
} else {
|
||||||
contributions.add(new Contribution("authored_baseline", -1, 0, "Authored Age"));
|
contributions.add(new Contribution("authored_baseline", -1, 0, "Authored Age"));
|
||||||
|
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());
|
WritingGrammar.Result parsed = WritingGrammar.parse(plan.writing());
|
||||||
if (!parsed.canCreate() || parsed.writingInstability() != plan.writingInstability()) {
|
if (!parsed.canCreate() || parsed.writingInstability() != plan.writingInstability()) {
|
||||||
throw new IllegalArgumentException("Writing changed between Age compilation and scoring");
|
throw new IllegalArgumentException("Writing changed between Age compilation and scoring");
|
||||||
@@ -64,6 +70,7 @@ public record AgeInstability(int version, int score, List<Contribution> breakdow
|
|||||||
contributions.add(new Contribution(diagnostic.code().name().toLowerCase(java.util.Locale.ROOT),
|
contributions.add(new Contribution(diagnostic.code().name().toLowerCase(java.util.Locale.ROOT),
|
||||||
diagnostic.index(), diagnostic.instability(), diagnostic.message()));
|
diagnostic.index(), diagnostic.instability(), diagnostic.message()));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
addAuditedSymbolCosts(plan, registries, contributions);
|
addAuditedSymbolCosts(plan, registries, contributions);
|
||||||
}
|
}
|
||||||
// A missing lighting controller is seed-completed. Legacy runs the generated
|
// A missing lighting controller is seed-completed. Legacy runs the generated
|
||||||
|
|||||||
@@ -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) {
|
private static WritingEntry page(String path) {
|
||||||
return WritingEntry.symbol(new SymbolReference(ResourceLocation.fromNamespaceAndPath("rivencraft", path)));
|
return WritingEntry.symbol(new SymbolReference(ResourceLocation.fromNamespaceAndPath("rivencraft", path)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,33 @@ public final class Milestone6RegressionGameTests {
|
|||||||
helper.succeed();
|
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")
|
@GameTest(template = "empty")
|
||||||
public static void blockProfileSamplesPersistWithoutDoubleCounting(GameTestHelper helper) {
|
public static void blockProfileSamplesPersistWithoutDoubleCounting(GameTestHelper helper) {
|
||||||
var data = new AgeBlockProfileSavedData();
|
var data = new AgeBlockProfileSavedData();
|
||||||
@@ -121,6 +148,11 @@ public final class Milestone6RegressionGameTests {
|
|||||||
helper.getLevel().registryAccess()), helper.getLevel().registryAccess());
|
helper.getLevel().registryAccess()), helper.getLevel().registryAccess());
|
||||||
helper.assertValueEqual(restored.count(), 1, "The profiled chunk set must survive restart");
|
helper.assertValueEqual(restored.count(), 1, "The profiled chunk set must survive restart");
|
||||||
helper.assertValueEqual(restored.totals(), data.totals(), "Block counts 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();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -155,6 +155,8 @@ public final class RivencraftCommands {
|
|||||||
source.sendSuccess(() -> Component.literal("Block profile: Age " + ageSamples
|
source.sendSuccess(() -> Component.literal("Block profile: Age " + ageSamples
|
||||||
+ "/401, Overworld baseline " + baselineSamples + "/500"
|
+ "/401, Overworld baseline " + baselineSamples + "/500"
|
||||||
+ (ageLevel == null ? " (Age unloaded)" : "")), false);
|
+ (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());
|
boolean enabled = AgeInstabilityControls.get(source.getLevel()).enabled(age.id());
|
||||||
source.sendSuccess(() -> Component.literal("Runtime: " + (enabled ? "Age enabled" : "Age disabled")
|
source.sendSuccess(() -> Component.literal("Runtime: " + (enabled ? "Age enabled" : "Age disabled")
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package net.dnorberg.rivencraft.config;
|
package net.dnorberg.rivencraft.config;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
import net.neoforged.neoforge.common.ModConfigSpec;
|
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 INSTABILITY_MAX_CHUNKS;
|
||||||
private static final ModConfigSpec.IntValue ACCELERATED_MAX_CHUNKS;
|
private static final ModConfigSpec.IntValue ACCELERATED_MAX_CHUNKS;
|
||||||
private static final ModConfigSpec.IntValue ACCELERATED_MAX_ATTEMPTS;
|
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_ENABLED;
|
||||||
private static final ModConfigSpec.BooleanValue STATUS_EFFECTS_PLAYERS;
|
private static final ModConfigSpec.BooleanValue STATUS_EFFECTS_PLAYERS;
|
||||||
private static final ModConfigSpec.BooleanValue STATUS_EFFECTS_NONPLAYERS;
|
private static final ModConfigSpec.BooleanValue STATUS_EFFECTS_NONPLAYERS;
|
||||||
@@ -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.")
|
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);
|
.defineInRange("max_random_tick_attempts_per_tick", 16384, 0, 65536);
|
||||||
builder.pop();
|
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");
|
builder.push("instability_status_effects");
|
||||||
STATUS_EFFECTS_ENABLED = builder.comment("Allow bounded instability status effects in loaded Ages.")
|
STATUS_EFFECTS_ENABLED = builder.comment("Allow bounded instability status effects in loaded Ages.")
|
||||||
.define("enabled", true);
|
.define("enabled", true);
|
||||||
@@ -236,6 +245,22 @@ public final class RivencraftConfig {
|
|||||||
public static int instabilityMaxChunks() { return INSTABILITY_MAX_CHUNKS.get(); }
|
public static int instabilityMaxChunks() { return INSTABILITY_MAX_CHUNKS.get(); }
|
||||||
public static int acceleratedMaxChunks() { return ACCELERATED_MAX_CHUNKS.get(); }
|
public static int acceleratedMaxChunks() { return ACCELERATED_MAX_CHUNKS.get(); }
|
||||||
public static int acceleratedMaxAttempts() { return ACCELERATED_MAX_ATTEMPTS.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 statusEffectsEnabled() { return STATUS_EFFECTS_ENABLED.get(); }
|
||||||
public static boolean statusEffectsPlayers() { return STATUS_EFFECTS_PLAYERS.get(); }
|
public static boolean statusEffectsPlayers() { return STATUS_EFFECTS_PLAYERS.get(); }
|
||||||
public static boolean statusEffectsNonplayers() { return STATUS_EFFECTS_NONPLAYERS.get(); }
|
public static boolean statusEffectsNonplayers() { return STATUS_EFFECTS_NONPLAYERS.get(); }
|
||||||
|
|||||||
@@ -87,6 +87,12 @@ public final class WritingGrammarGameTests {
|
|||||||
WritingGrammar.Result repeatedModifiers = parse(resolver, WritingEntry.linkPanel(),
|
WritingGrammar.Result repeatedModifiers = parse(resolver, WritingEntry.linkPanel(),
|
||||||
WritingEntry.symbol(periodHalf), WritingEntry.symbol(periodFull), WritingEntry.symbol(celestial));
|
WritingEntry.symbol(periodHalf), WritingEntry.symbol(periodFull), WritingEntry.symbol(celestial));
|
||||||
helper.assertTrue(repeatedModifiers.canCreate(), "Repeated period modifiers must remain legal");
|
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),
|
assertPenalty(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(terrain),
|
||||||
WritingEntry.symbol(terrain)), WritingGrammar.Code.DUPLICATE);
|
WritingEntry.symbol(terrain)), WritingGrammar.Code.DUPLICATE);
|
||||||
WritingGrammar.Result replacedTerrain = parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(terrain),
|
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");
|
"The last supported controller must supply the effective terrain");
|
||||||
assertPenalty(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(weather),
|
assertPenalty(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(weather),
|
||||||
WritingEntry.symbol(terrain)), WritingGrammar.Code.OUT_OF_ORDER);
|
WritingEntry.symbol(terrain)), WritingGrammar.Code.OUT_OF_ORDER);
|
||||||
assertPenalty(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(celestial),
|
WritingGrammar.Result afterCelestial = parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(celestial),
|
||||||
WritingEntry.symbol(periodHalf)), WritingGrammar.Code.MISPLACED_MODIFIER);
|
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),
|
assertPenalty(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(nativeBiomes),
|
||||||
WritingEntry.symbol(plains)), WritingGrammar.Code.OUT_OF_ORDER);
|
WritingEntry.symbol(plains)), WritingGrammar.Code.OUT_OF_ORDER);
|
||||||
assertPenalty(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(plains),
|
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);
|
if (writing.entries().isEmpty()) return new Result(symbols, diagnostics);
|
||||||
|
|
||||||
EnumMap<Role, ParsedSymbol> singular = new EnumMap<>(Role.class);
|
EnumMap<Role, ParsedSymbol> singular = new EnumMap<>(Role.class);
|
||||||
|
EnumMap<Role, ParsedSymbol> pendingModifiers = new EnumMap<>(Role.class);
|
||||||
int panels = 0;
|
int panels = 0;
|
||||||
int lastStage = -1;
|
int lastStage = -1;
|
||||||
int biomeCount = 0;
|
int biomeCount = 0;
|
||||||
@@ -72,6 +73,13 @@ public final class WritingGrammar {
|
|||||||
Role role = semantics.orElseThrow().role();
|
Role role = semantics.orElseThrow().role();
|
||||||
ParsedSymbol parsed = new ParsedSymbol(index, reference, semantics.orElseThrow());
|
ParsedSymbol parsed = new ParsedSymbol(index, reference, semantics.orElseThrow());
|
||||||
symbols.add(parsed);
|
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) {
|
if (pendingBlock != null && role != Role.FEATURE) {
|
||||||
// A material page without a following feature receives a seeded compatible
|
// A material page without a following feature receives a seeded compatible
|
||||||
// feature when the Age plan is compiled.
|
// feature when the Age plan is compiled.
|
||||||
@@ -107,8 +115,8 @@ public final class WritingGrammar {
|
|||||||
}
|
}
|
||||||
if (role == Role.BIOME) biomeCount++;
|
if (role == Role.BIOME) biomeCount++;
|
||||||
int stage = stage(role);
|
int stage = stage(role);
|
||||||
if (stage < lastStage) {
|
if (stage < lastStage && !modifier(role)) {
|
||||||
diagnostics.add(new Diagnostic(modifier(role) ? Code.MISPLACED_MODIFIER : Code.OUT_OF_ORDER,
|
diagnostics.add(new Diagnostic(Code.OUT_OF_ORDER,
|
||||||
index, "Move this " + role.id() + " page before later grammar categories"));
|
index, "Move this " + role.id() + " page before later grammar categories"));
|
||||||
} else {
|
} else {
|
||||||
lastStage = stage;
|
lastStage = stage;
|
||||||
@@ -116,6 +124,10 @@ public final class WritingGrammar {
|
|||||||
}
|
}
|
||||||
if (panels == 0) diagnostics.add(new Diagnostic(Code.MISSING_PANEL, -1,
|
if (panels == 0) diagnostics.add(new Diagnostic(Code.MISSING_PANEL, -1,
|
||||||
"Add a Link Panel as the first page"));
|
"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);
|
if (symbols.isEmpty()) return new Result(symbols, diagnostics);
|
||||||
|
|
||||||
ParsedSymbol controller = singular.get(Role.BIOME_CONTROLLER);
|
ParsedSymbol controller = singular.get(Role.BIOME_CONTROLLER);
|
||||||
@@ -174,14 +186,14 @@ public final class WritingGrammar {
|
|||||||
|
|
||||||
public enum Code {
|
public enum Code {
|
||||||
TOO_MANY, MISSING_PANEL, PANEL_ORDER, DUPLICATE_PANEL, UNRESOLVED, UNSUPPORTED,
|
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,
|
UNKNOWN_BIOME, UNSUPPORTED_BIOME, UNSUPPORTED_ENVIRONMENT, UNSUPPORTED_FEATURE,
|
||||||
UNSUPPORTED_COMBINATION;
|
UNSUPPORTED_COMBINATION;
|
||||||
|
|
||||||
public Severity severity() {
|
public Severity severity() {
|
||||||
return switch (this) {
|
return switch (this) {
|
||||||
case INCOMPLETE -> Severity.SEEDED_COMPLETION;
|
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;
|
default -> Severity.FATAL;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -205,7 +217,7 @@ public final class WritingGrammar {
|
|||||||
public int instability() {
|
public int instability() {
|
||||||
return switch (code) {
|
return switch (code) {
|
||||||
case DUPLICATE, CONFLICT -> 500;
|
case DUPLICATE, CONFLICT -> 500;
|
||||||
case OUT_OF_ORDER, MISPLACED_MODIFIER -> 100;
|
case OUT_OF_ORDER, MISPLACED_MODIFIER, DANGLING_MODIFIER -> 100;
|
||||||
default -> 0;
|
default -> 0;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user