Public Access
1 parent
86cda69d11
commit
935678a3dd
14 files changed
+467
-51
No files matched your search
@@ -0,0 +1,9 @@
|
||||
# Frozen Age instability score (Milestone 6, issue #64)
|
||||
|
||||
Every newly created Age now gets a version-1 `Instability` compound inside its server-owned `rivencraft_ages` saved definition. It contains the total `Score` and an ordered `Breakdown` of reason, original page index (zero-based, or -1 for an Age-wide cause), points, and explanation. `AgeRegistrySavedData.instability(UUID)` is the read-only server query. No client tooltip or live data-pack lookup is needed to read a saved score.
|
||||
|
||||
The baseline is explicitly zero for page-free and Link-Panel-only random Ages, and for authored Ages. Incomplete writing receives seed-based completion and a zero-point explanatory entry. Supported writing defects use the provisional Milestone 6 grammar costs: duplicate/conflicting single-choice pages +500, out-of-order or misplaced pages +100. Legacy's audited bright-lighting symbol adds +500 whether authored or seed-selected. Other balance costs are not guessed for symbols that Rivencraft has not implemented. The score is not yet an active hazard or effect selection.
|
||||
|
||||
The score is calculated after the Age plan and effective traits are chosen, then saved with the Age identity before its dimension is generated. The same UUID returns the existing definition, including the same score, on repeated use. Spawn updates preserve it. Reload only deserializes the frozen breakdown; it never reruns the current grammar or catalog. An older Age definition without `Instability` is migrated to a persisted version-1 neutral score of zero, rather than retroactively scoring its writing. A future scoring-rule change must introduce an explicit new schema/migration policy, not silently reinterpret existing Ages.
|
||||
|
||||
Unknown or unsupported pages remain invalid for *new* Ages. Their absence from a later registry does not change an already saved score. Focused GameTests cover stable/defective writing, random baseline and bright lighting, repeated identity, save round-trip, and missing-symbol/old-save behavior.
|
||||
@@ -47,9 +47,11 @@ Assets in the supplied local tree: `assets/mystcraft/textures/blocks/decay_{blac
|
||||
|
||||
## Rivencraft boundary and proposed safety contract
|
||||
|
||||
[`WritingGrammar`](../src/main/java/net/dnorberg/rivencraft/writing/WritingGrammar.java) treats `INCOMPLETE` and supported writing defects as warnings. `AgeGenerationPlan.compile()` freezes the original writing, repaired active-page indexes, and a provisional writing-instability score before world generation. #64 still needs a full versioned score breakdown and saved provider/deck choice so reloads do not reroll effects. The plan must not mutate an already-created Age when a data pack changes. Server-side settings may disable execution, but should not rewrite the saved authored score.
|
||||
[`WritingGrammar`](../src/main/java/net/dnorberg/rivencraft/writing/WritingGrammar.java) classifies `INCOMPLETE` as seeded completion and supported writing defects as accepted instability. `AgeGenerationPlan.compile()` freezes the original writing, repaired active-page indexes, and a provisional writing-instability score before world generation. #64 now persists a versioned score breakdown; saved provider/deck choice remains future work. The plan must not mutate an already-created Age when a data pack changes. Server-side settings may disable execution, but should not rewrite the saved authored score.
|
||||
|
||||
For #65, **eligible-to-become-unstable** means a known, supported page whose semantics and resulting world are still deterministic: repeated/conflicting single-choice pages, an otherwise valid misplaced modifier, and excess supported symbols within the Binder capacity. The initial 0.5.1.0 implementation retains every original page, uses the last single-choice page, sorts active categories, and saves 500 per extra/conflicting choice or 100 per misplaced page. These are provisional Rivencraft values, not a full port of every `IAgeSymbol.instabilityModifier`. Missing branches remain completion warnings; legal repeated feature/material pages are not automatically errors. **Remain fatal**: missing/duplicate/misordered Link Panel, over-capacity book, unresolved or schema-v1 display-only symbol, unsupported weather/angle/runtime profile/biome/feature, unavailable required optional mod, and any combination the compiler cannot represent safely. This intentionally differs from Legacy's silent unmatched-symbol skip. #65 still needs the final symbol-specific penalty table and tests.
|
||||
For #65, **eligible-to-become-unstable** means a known, supported page whose semantics and resulting world are still deterministic: repeated/conflicting single-choice pages and otherwise valid out-of-order or misplaced pages. The implementation retains every original page, uses the last single-choice page, sorts active categories, and saves 500 per extra/conflicting choice or 100 per misplaced page. These are provisional Rivencraft values, not a full port of every `IAgeSymbol.instabilityModifier`. Missing branches are explicitly classified as zero-cost seeded completion; legal repeated feature/material pages are not automatically errors. **Remain fatal**: missing/duplicate/misordered Link Panel, over-capacity book, unresolved or schema-v1 display-only symbol, unsupported weather/angle/runtime profile/biome/feature, unavailable required optional mod, and any combination the compiler cannot represent safely. Binder and unbound-book previews distinguish blocked writing, seeded completion, and accepted instability before binding; the server revalidates on output. This intentionally differs from Legacy's silent unmatched-symbol skip, universal controller cost assumptions, and modifier-specific dangling costs. A full symbol-specific Legacy penalty port remains a separate balance task, not a claim of parity in this slice.
|
||||
|
||||
Automated #65 checks cover classification, preserved input slots on rejection, and an accepted unstable preview. The live output/one-time Age creation check is assigned to in-game testing because the GameTest storage phase stalled when that transaction ran; the test world is not retained.
|
||||
|
||||
Proposed server defaults for #66–#72 (to confirm during implementation): instability scoring on for newly created Ages; passive entity effects on; world-mutating hazards **off by default** until an operator opts in; explicit per-Age toggle and separate per-family switches. Enforce a central work budget rather than letting every loaded chunk and provider roll independently: default at most one hazard attempt per Age per 20 ticks, one block mutation per successful attempt, and no forced chunk loads. Budgets should be configurable with hard upper bounds, including active Ages, target radius, tick interval, block changes/explosions/meteor impacts, entity effect attempts, and decay spread. Never target another dimension, unloaded chunk, book preview/snapshot, protected spawn/link platform, or portal. Stop safely when the Age unloads or settings change. These are **Rivencraft proposals**, not Legacy defaults or code that exists today; implementation issues must test/adjust them, especially multiple concurrent Ages. Expose score breakdown, selected effects, enablement, and budget suppression in server diagnostics (#72).
|
||||
|
||||
|
||||
+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.
|
||||
mod_license=LGPL-3.0-or-later
|
||||
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
||||
mod_version=0.5.1.1
|
||||
mod_version=0.5.3.0
|
||||
immersive_portals_version=6.0.7
|
||||
terrablender_version=4.1.0.8
|
||||
serene_seasons_version_id=B6nTjh4b
|
||||
|
||||
@@ -5,7 +5,6 @@ import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.dnorberg.rivencraft.book.AgeReference;
|
||||
import net.dnorberg.rivencraft.book.AgeRegistrySavedData;
|
||||
import net.dnorberg.rivencraft.book.AgeGenerationPlan;
|
||||
import net.dnorberg.rivencraft.book.AgeGenerationProfiles;
|
||||
import net.dnorberg.rivencraft.book.AgeRuntimeProfiles;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
@@ -117,19 +116,7 @@ public final class BookBinderBlockEntity extends BlockEntity implements Containe
|
||||
if (!page.isEmpty() && (!validPage(page) || !PageItem.isUsableWriting(page) || page.getCount() != 1
|
||||
|| PageItem.content(page).kind() == PageContent.Kind.BLANK)) return false;
|
||||
}
|
||||
OrderedWriting writing = currentWriting();
|
||||
if (!writingGrammar().canCreate()) return false;
|
||||
try {
|
||||
if (level instanceof ServerLevel serverLevel) {
|
||||
AgeRegistrySavedData.validateNewWriting(serverLevel, title, writing);
|
||||
} else {
|
||||
AgeGenerationPlan.compile(writing, 0L, AgeGenerationProfiles.STANDARD,
|
||||
net.dnorberg.rivencraft.book.AgeTraits.select(0L), level.registryAccess());
|
||||
}
|
||||
return true;
|
||||
} catch (IllegalArgumentException exception) {
|
||||
return false;
|
||||
}
|
||||
return writingGrammar().canCreate();
|
||||
}
|
||||
|
||||
public WritingGrammar.Result writingGrammar() {
|
||||
@@ -181,18 +168,21 @@ public final class BookBinderBlockEntity extends BlockEntity implements Containe
|
||||
// check. This also pinpoints biome-pool conflicts that cannot be inferred from
|
||||
// an individual feature page alone (for example Trees with only Nether biomes).
|
||||
if (level instanceof ServerLevel serverLevel && diagnostics.stream().noneMatch(
|
||||
diagnostic -> diagnostic.code() == WritingGrammar.Code.UNSUPPORTED_FEATURE)) {
|
||||
WritingGrammar.Diagnostic::error)) {
|
||||
try {
|
||||
AgeRegistrySavedData.validateNewWriting(serverLevel, title, currentWriting());
|
||||
} catch (IllegalArgumentException exception) {
|
||||
if (exception.getMessage() != null && (exception.getMessage().startsWith("Feature ")
|
||||
|| exception.getMessage().startsWith("Caves "))) {
|
||||
parsed.symbols().stream().filter(symbol -> symbol.semantics().role()
|
||||
== SymbolSemantics.Role.FEATURE).findFirst().ifPresent(symbol ->
|
||||
diagnostics.add(new WritingGrammar.Diagnostic(
|
||||
WritingGrammar.Code.UNSUPPORTED_FEATURE, symbol.index(),
|
||||
exception.getMessage())));
|
||||
}
|
||||
boolean feature = exception.getMessage() != null
|
||||
&& (exception.getMessage().startsWith("Feature ")
|
||||
|| exception.getMessage().startsWith("Caves "));
|
||||
int index = feature ? parsed.symbols().stream()
|
||||
.filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.FEATURE)
|
||||
.mapToInt(WritingGrammar.ParsedSymbol::index).findFirst().orElse(-1) : -1;
|
||||
diagnostics.add(new WritingGrammar.Diagnostic(
|
||||
feature ? WritingGrammar.Code.UNSUPPORTED_FEATURE
|
||||
: WritingGrammar.Code.UNSUPPORTED_COMBINATION,
|
||||
index, exception.getMessage() == null ? "The Age compiler rejected this writing"
|
||||
: exception.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,8 @@ public record AgeDefinition(
|
||||
AgeTraits traits,
|
||||
AgeGenerationPlan plan,
|
||||
@Nullable AgeSeasonMode seasonMode,
|
||||
List<AgeFeatureSelection.Choice> randomFeatures
|
||||
List<AgeFeatureSelection.Choice> randomFeatures,
|
||||
AgeInstability instability
|
||||
) {
|
||||
public static final ResourceLocation RANDOM_VANILLA_PROFILE = ResourceLocation.fromNamespaceAndPath(
|
||||
Rivencraft.MOD_ID, "random_vanilla"
|
||||
@@ -34,6 +35,7 @@ public record AgeDefinition(
|
||||
Objects.requireNonNull(generationProfile, "generationProfile");
|
||||
Objects.requireNonNull(dimensionId, "dimensionId");
|
||||
Objects.requireNonNull(traits, "traits");
|
||||
Objects.requireNonNull(instability, "instability");
|
||||
randomFeatures = List.copyOf(randomFeatures);
|
||||
if (plan != null && !randomFeatures.isEmpty()) {
|
||||
throw new IllegalArgumentException("Authored Ages cannot also have random features");
|
||||
@@ -47,14 +49,14 @@ public record AgeDefinition(
|
||||
int generationVersion, long createdGameTime, ResourceLocation dimensionId,
|
||||
BlockPos spawn, AgeTraits traits) {
|
||||
this(id, name, seed, generationProfile, generationVersion, createdGameTime,
|
||||
dimensionId, spawn, traits, null, null, List.of());
|
||||
dimensionId, spawn, traits, null, null, List.of(), AgeInstability.legacyNeutral());
|
||||
}
|
||||
|
||||
public AgeDefinition(UUID id, String name, long seed, ResourceLocation generationProfile,
|
||||
int generationVersion, long createdGameTime, ResourceLocation dimensionId,
|
||||
BlockPos spawn, AgeTraits traits, AgeGenerationPlan plan) {
|
||||
this(id, name, seed, generationProfile, generationVersion, createdGameTime,
|
||||
dimensionId, spawn, traits, plan, null, List.of());
|
||||
dimensionId, spawn, traits, plan, null, List.of(), AgeInstability.legacyNeutral());
|
||||
}
|
||||
|
||||
public AgeDefinition(UUID id, String name, long seed, ResourceLocation generationProfile,
|
||||
@@ -62,7 +64,15 @@ public record AgeDefinition(
|
||||
BlockPos spawn, AgeTraits traits, AgeGenerationPlan plan,
|
||||
@Nullable AgeSeasonMode seasonMode) {
|
||||
this(id, name, seed, generationProfile, generationVersion, createdGameTime,
|
||||
dimensionId, spawn, traits, plan, seasonMode, List.of());
|
||||
dimensionId, spawn, traits, plan, seasonMode, List.of(), AgeInstability.legacyNeutral());
|
||||
}
|
||||
|
||||
public AgeDefinition(UUID id, String name, long seed, ResourceLocation generationProfile,
|
||||
int generationVersion, long createdGameTime, ResourceLocation dimensionId,
|
||||
BlockPos spawn, AgeTraits traits, AgeGenerationPlan plan,
|
||||
@Nullable AgeSeasonMode seasonMode, List<AgeFeatureSelection.Choice> randomFeatures) {
|
||||
this(id, name, seed, generationProfile, generationVersion, createdGameTime,
|
||||
dimensionId, spawn, traits, plan, seasonMode, randomFeatures, AgeInstability.legacyNeutral());
|
||||
}
|
||||
|
||||
public static ResourceLocation dimensionId(UUID ageId) {
|
||||
@@ -93,6 +103,6 @@ public record AgeDefinition(
|
||||
|
||||
public AgeDefinition withSpawn(BlockPos spawn) {
|
||||
return new AgeDefinition(id, name, seed, generationProfile, generationVersion,
|
||||
createdGameTime, dimensionId, spawn.immutable(), traits, plan, seasonMode, randomFeatures);
|
||||
createdGameTime, dimensionId, spawn.immutable(), traits, plan, seasonMode, randomFeatures, instability);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package net.dnorberg.rivencraft.book;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import net.dnorberg.rivencraft.writing.WritingGrammar;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.nbt.ListTag;
|
||||
|
||||
/** Frozen, server-owned score. Only new Age creation consults the current symbol catalog. */
|
||||
public record AgeInstability(int version, int score, List<Contribution> breakdown) {
|
||||
public static final int CURRENT_VERSION = 1;
|
||||
|
||||
public AgeInstability {
|
||||
if (version != CURRENT_VERSION) throw new IllegalArgumentException("Unsupported instability version " + version);
|
||||
breakdown = List.copyOf(breakdown);
|
||||
if (score < 0 || score != breakdown.stream().mapToInt(Contribution::points).sum()) {
|
||||
throw new IllegalArgumentException("Instability score does not match breakdown");
|
||||
}
|
||||
}
|
||||
|
||||
/** Pre-scoring saves are explicitly neutral, never rescored from current data packs. */
|
||||
public static AgeInstability legacyNeutral() {
|
||||
return new AgeInstability(CURRENT_VERSION, 0,
|
||||
List.of(new Contribution("legacy_neutral", -1, 0, "Age predates frozen instability scoring")));
|
||||
}
|
||||
|
||||
static AgeInstability compute(AgeGenerationPlan plan, AgeTraits traits) {
|
||||
Objects.requireNonNull(traits, "traits");
|
||||
List<Contribution> contributions = new ArrayList<>();
|
||||
if (plan == null) {
|
||||
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()));
|
||||
}
|
||||
}
|
||||
// Legacy's bright-lighting symbol adds +500. Other balance costs are deferred
|
||||
// until their corresponding authored symbols and audited values exist.
|
||||
if (traits.lighting() == AgeTraits.Lighting.BRIGHT) {
|
||||
int pageIndex = plan == null ? -1 : plan.activeSymbols().stream()
|
||||
.filter(symbol -> symbol.semantics().role()
|
||||
== net.dnorberg.rivencraft.writing.SymbolSemantics.Role.LIGHTING)
|
||||
.mapToInt(AgeGenerationPlan.ResolvedSymbol::index).findFirst().orElse(-1);
|
||||
contributions.add(new Contribution("bright_lighting", pageIndex, 500,
|
||||
pageIndex < 0 ? "Seeded bright lighting" : "Authored bright lighting"));
|
||||
}
|
||||
return new AgeInstability(CURRENT_VERSION,
|
||||
contributions.stream().mapToInt(Contribution::points).sum(), contributions);
|
||||
}
|
||||
|
||||
public CompoundTag save() {
|
||||
CompoundTag tag = new CompoundTag();
|
||||
tag.putInt("Version", version);
|
||||
tag.putInt("Score", score);
|
||||
ListTag entries = new ListTag();
|
||||
for (Contribution contribution : breakdown) {
|
||||
CompoundTag entry = new CompoundTag();
|
||||
entry.putString("Reason", contribution.reason());
|
||||
entry.putInt("PageIndex", contribution.pageIndex());
|
||||
entry.putInt("Points", contribution.points());
|
||||
entry.putString("Detail", contribution.detail());
|
||||
entries.add(entry);
|
||||
}
|
||||
tag.put("Breakdown", entries);
|
||||
return tag;
|
||||
}
|
||||
|
||||
public static AgeInstability load(CompoundTag tag) {
|
||||
ListTag entries = tag.getList("Breakdown", CompoundTag.TAG_COMPOUND);
|
||||
List<Contribution> contributions = new ArrayList<>();
|
||||
for (int index = 0; index < entries.size(); index++) {
|
||||
CompoundTag entry = entries.getCompound(index);
|
||||
contributions.add(new Contribution(entry.getString("Reason"), entry.getInt("PageIndex"),
|
||||
entry.getInt("Points"), entry.getString("Detail")));
|
||||
}
|
||||
return new AgeInstability(tag.getInt("Version"), tag.getInt("Score"), contributions);
|
||||
}
|
||||
|
||||
public record Contribution(String reason, int pageIndex, int points, String detail) {
|
||||
public Contribution {
|
||||
Objects.requireNonNull(reason, "reason");
|
||||
Objects.requireNonNull(detail, "detail");
|
||||
if (reason.isBlank() || pageIndex < -1 || points < 0) {
|
||||
throw new IllegalArgumentException("Invalid instability contribution");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||
import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||
import net.dnorberg.rivencraft.writing.WritingGrammar;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
@@ -55,6 +56,103 @@ public final class AgePersistenceGameTests {
|
||||
registry.save(new CompoundTag(), helper.getLevel().registryAccess()),
|
||||
helper.getLevel().registryAccess()).find(id).orElseThrow();
|
||||
helper.assertValueEqual(restored, created, "The complete plan must survive a saved-data round trip");
|
||||
helper.assertValueEqual(restored.instability(), created.instability(),
|
||||
"The complete instability breakdown must survive reload");
|
||||
helper.assertValueEqual(registry.instability(id).orElseThrow(), created.instability(),
|
||||
"The server diagnostic query must return the saved score");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void instabilitySeparatesStableAndDefectiveWriting(GameTestHelper helper) {
|
||||
OrderedWriting stable = new OrderedWriting(List.of(
|
||||
WritingEntry.linkPanel(),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:terrain/standard"))),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:lighting/normal")))
|
||||
));
|
||||
OrderedWriting defective = new OrderedWriting(List.of(
|
||||
WritingEntry.linkPanel(),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:lighting/normal"))),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:terrain/standard"))),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:terrain/standard")))
|
||||
));
|
||||
AgeDefinition stableAge = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(),
|
||||
new AgeReference(UUID.randomUUID(), "Stable Writing"), stable);
|
||||
AgeDefinition defectiveAge = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(),
|
||||
new AgeReference(UUID.randomUUID(), "Defective Writing"), defective);
|
||||
helper.assertValueEqual(stableAge.instability().score(), 0,
|
||||
"Missing seeded branches do not add instability");
|
||||
helper.assertValueEqual(defectiveAge.instability().score(), 600,
|
||||
"Out-of-order and repeated terrain pages each add their saved penalty");
|
||||
helper.assertTrue(defectiveAge.instability().breakdown().stream()
|
||||
.anyMatch(part -> part.reason().equals("duplicate") && part.pageIndex() == 3),
|
||||
"The score must explain the offending page");
|
||||
AgeDefinition reused = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(),
|
||||
new AgeReference(defectiveAge.id(), "Changed Name"), stable);
|
||||
helper.assertValueEqual(reused, defectiveAge, "Repeated book use must not rescore an Age");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void randomBaselineAndBrightBalanceAreFrozen(GameTestHelper helper) {
|
||||
AgeDefinition random = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(),
|
||||
new AgeReference(UUID.randomUUID(), "Random Score"), OrderedWriting.EMPTY);
|
||||
helper.assertTrue(random.instability().breakdown().stream()
|
||||
.anyMatch(part -> part.reason().equals("random_baseline") && part.points() == 0),
|
||||
"Random Ages require an explicit baseline entry");
|
||||
helper.assertValueEqual(random.instability().score(),
|
||||
random.traits().lighting() == AgeTraits.Lighting.BRIGHT ? 500 : 0,
|
||||
"Seeded bright lighting must use the audited balance cost");
|
||||
AgeInstability bright = AgeInstability.compute(null, new AgeTraits(
|
||||
AgeTraits.LEGACY_DEFAULTS.biomeDistribution(), AgeTraits.Lighting.BRIGHT,
|
||||
AgeTraits.LEGACY_DEFAULTS.weather(), AgeTraits.LEGACY_DEFAULTS.celestialPeriod(),
|
||||
AgeTraits.LEGACY_DEFAULTS.celestialAngle(), AgeTraits.LEGACY_DEFAULTS.celestialPhase()));
|
||||
helper.assertValueEqual(bright.score(), 500, "Bright lighting has the audited Legacy cost");
|
||||
helper.assertValueEqual(AgeInstability.load(bright.save()), bright,
|
||||
"A score must survive its own versioned round trip");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void missingRegistryEntryDoesNotReinterpretAnOldAge(GameTestHelper helper) {
|
||||
UUID id = UUID.randomUUID();
|
||||
CompoundTag root = new CompoundTag();
|
||||
ListTag entries = new ListTag();
|
||||
entries.add(entry(id, "Old Age", 17L, AgeGenerationProfiles.STANDARD, 15, 1L,
|
||||
AgeDefinition.dimensionId(id), new BlockPos(0, 64, 0), AgeTraits.LEGACY_DEFAULTS));
|
||||
root.put("Ages", entries);
|
||||
AgeRegistrySavedData old = AgeRegistrySavedData.load(root, helper.getLevel().registryAccess());
|
||||
helper.assertValueEqual(old.instability(id).orElseThrow(), AgeInstability.legacyNeutral(),
|
||||
"Old Ages without a score must migrate to a neutral saved value");
|
||||
helper.assertValueEqual(AgeRegistrySavedData.load(
|
||||
old.save(new CompoundTag(), helper.getLevel().registryAccess()),
|
||||
helper.getLevel().registryAccess()).instability(id).orElseThrow(), AgeInstability.legacyNeutral(),
|
||||
"Neutral migration must persist after restart");
|
||||
OrderedWriting missing = new OrderedWriting(List.of(WritingEntry.linkPanel(),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:missing_symbol")))));
|
||||
helper.assertTrue(!WritingGrammar.parse(missing, symbol -> java.util.Optional.empty()).canCreate(),
|
||||
"Missing registry symbols remain fatal for new Ages");
|
||||
|
||||
OrderedWriting authored = new OrderedWriting(List.of(WritingEntry.linkPanel(),
|
||||
WritingEntry.symbol(new SymbolReference(ResourceLocation.parse("rivencraft:terrain/standard")))));
|
||||
AgeDefinition created = AgeRegistrySavedData.resolveOrCreate(helper.getLevel(),
|
||||
new AgeReference(UUID.randomUUID(), "Catalog Snapshot"), authored);
|
||||
CompoundTag saved = AgeRegistrySavedData.get(helper.getLevel())
|
||||
.save(new CompoundTag(), helper.getLevel().registryAccess());
|
||||
ListTag savedAges = saved.getList("Ages", CompoundTag.TAG_COMPOUND);
|
||||
for (int index = 0; index < savedAges.size(); index++) {
|
||||
CompoundTag savedAge = savedAges.getCompound(index);
|
||||
if (!savedAge.getUUID("Id").equals(created.id())) continue;
|
||||
CompoundTag plan = savedAge.getCompound("GenerationPlan");
|
||||
plan.getList("Resolved", CompoundTag.TAG_COMPOUND).getCompound(0)
|
||||
.putString("Symbol", "rivencraft:removed_from_catalog");
|
||||
plan.getList("Writing", CompoundTag.TAG_COMPOUND).getCompound(1)
|
||||
.putString("Symbol", "rivencraft:removed_from_catalog");
|
||||
}
|
||||
AgeDefinition restored = AgeRegistrySavedData.load(saved, helper.getLevel().registryAccess())
|
||||
.find(created.id()).orElseThrow();
|
||||
helper.assertValueEqual(restored.instability(), created.instability(),
|
||||
"A removed symbol must not reroll or reinterpret the frozen instability score");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@@ -170,6 +268,8 @@ public final class AgePersistenceGameTests {
|
||||
"Definitions predating traits must receive stable legacy defaults");
|
||||
helper.assertTrue(definition.plan() == null,
|
||||
"Existing Ages must not be reinterpreted as authored plans during migration");
|
||||
helper.assertValueEqual(definition.instability(), AgeInstability.legacyNeutral(),
|
||||
"Old Ages must not be rescored from modern page rules");
|
||||
|
||||
CompoundTag serialized = migrated.save(new CompoundTag(), helper.getLevel().registryAccess());
|
||||
AgeDefinition reloaded = AgeRegistrySavedData.load(serialized, helper.getLevel().registryAccess())
|
||||
|
||||
@@ -35,6 +35,7 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
private static final String PLAN_TAG = "GenerationPlan";
|
||||
private static final String RANDOM_FEATURES_TAG = "RandomFeatures";
|
||||
private static final String SEASON_MODE_TAG = "SeasonMode";
|
||||
private static final String INSTABILITY_TAG = "Instability";
|
||||
private static final String BIOME_DISTRIBUTION_TAG = "BiomeDistribution";
|
||||
private static final String LIGHTING_TAG = "Lighting";
|
||||
private static final String WEATHER_TAG = "Weather";
|
||||
@@ -89,6 +90,11 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
return Optional.ofNullable(ages.get(ageId));
|
||||
}
|
||||
|
||||
/** Read-only server diagnostic; never compiles writing or mutates a saved Age. */
|
||||
public Optional<AgeInstability> instability(UUID ageId) {
|
||||
return find(ageId).map(AgeDefinition::instability);
|
||||
}
|
||||
|
||||
public Optional<AgeDefinition> findByDimension(ResourceLocation dimensionId) {
|
||||
return ages.values().stream()
|
||||
.filter(age -> age.dimensionId().equals(dimensionId))
|
||||
@@ -123,6 +129,8 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
ResourceLocation seedProfile = AgeRuntimeProfiles.selectForWriting(seed, writing, registries);
|
||||
AgeTraits traits = AgeTraits.select(seed);
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(writing, seed, seedProfile, traits, registries);
|
||||
AgeTraits effectiveTraits = AgeEnvironmentCompiler.apply(plan, traits);
|
||||
AgeInstability instability = AgeInstability.compute(plan, effectiveTraits);
|
||||
AgeDefinition created = new AgeDefinition(
|
||||
reference.id(),
|
||||
name,
|
||||
@@ -132,11 +140,12 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
createdGameTime,
|
||||
dimensionId,
|
||||
null,
|
||||
AgeEnvironmentCompiler.apply(plan, traits),
|
||||
effectiveTraits,
|
||||
plan,
|
||||
AgeSeasonMode.select(plan, seed, ModList.get().isLoaded("sereneseasons")),
|
||||
plan == null ? AgeFeatureSelection.randomChoices(seed,
|
||||
AgeRuntimeProfiles.resolve(seedProfile), registries) : List.of()
|
||||
AgeRuntimeProfiles.resolve(seedProfile), registries) : List.of(),
|
||||
instability
|
||||
);
|
||||
ages.put(created.id(), created);
|
||||
setDirty();
|
||||
@@ -183,7 +192,10 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
? AgeGenerationPlan.load(entry.getCompound(PLAN_TAG)) : null,
|
||||
entry.contains(SEASON_MODE_TAG, CompoundTag.TAG_STRING)
|
||||
? AgeSeasonMode.parseOrNull(entry.getString(SEASON_MODE_TAG)) : null,
|
||||
readRandomFeatures(entry)
|
||||
readRandomFeatures(entry),
|
||||
entry.contains(INSTABILITY_TAG, CompoundTag.TAG_COMPOUND)
|
||||
? AgeInstability.load(entry.getCompound(INSTABILITY_TAG))
|
||||
: AgeInstability.legacyNeutral()
|
||||
));
|
||||
}
|
||||
return data;
|
||||
@@ -228,6 +240,7 @@ public final class AgeRegistrySavedData extends SavedData {
|
||||
entry.put(RANDOM_FEATURES_TAG, featureTags);
|
||||
}
|
||||
if (age.seasonMode() != null) entry.putString(SEASON_MODE_TAG, age.seasonMode().name());
|
||||
entry.put(INSTABILITY_TAG, age.instability().save());
|
||||
serializedAges.add(entry);
|
||||
});
|
||||
tag.put(AGES_TAG, serializedAges);
|
||||
|
||||
@@ -6,6 +6,7 @@ import net.dnorberg.rivencraft.block.entity.BookBinderBlockEntity;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.menu.BookBinderMenu;
|
||||
import net.dnorberg.rivencraft.network.SetBookBinderTitlePayload;
|
||||
import net.dnorberg.rivencraft.writing.WritingGrammar;
|
||||
import net.minecraft.client.gui.GuiGraphics;
|
||||
import net.minecraft.client.gui.components.EditBox;
|
||||
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
|
||||
@@ -106,14 +107,33 @@ public final class BookBinderScreen extends AbstractContainerScreen<BookBinderMe
|
||||
}
|
||||
graphics.disableScissor();
|
||||
drawScrollEnds(graphics);
|
||||
var severity = menu.grammarSeverity().orElse(null);
|
||||
Component status = severity == WritingGrammar.Severity.FATAL
|
||||
? Component.translatable("screen.rivencraft.book_binder.status.fatal")
|
||||
: severity == WritingGrammar.Severity.INSTABILITY
|
||||
? Component.translatable("screen.rivencraft.book_binder.status.unstable", menu.grammarInstability())
|
||||
: severity == WritingGrammar.Severity.SEEDED_COMPLETION
|
||||
? Component.translatable("screen.rivencraft.book_binder.status.seeded") : null;
|
||||
if (status != null) graphics.drawString(font, status, leftPos + 77, topPos + 30,
|
||||
severity == WritingGrammar.Severity.FATAL ? 0xFFAA3030
|
||||
: severity == WritingGrammar.Severity.INSTABILITY ? 0xFF924F18 : 0xFF50624B, false);
|
||||
Component grammarTooltip = menu.grammarError().map(code -> Component.translatable(
|
||||
"screen.rivencraft.book_binder.grammar." + code.name().toLowerCase(Locale.ROOT),
|
||||
errorStripIndex + 1)).orElse(null);
|
||||
if ((menu.grammarWarning() || menu.getSlot(BookBinderMenu.RESULT).getItem().isEmpty())
|
||||
&& mouseX >= leftPos + 152 && mouseX < leftPos + 168
|
||||
if (grammarTooltip != null && ((mouseX >= leftPos + 77 && mouseX < leftPos + 151)
|
||||
|| (mouseX >= leftPos + 152 && mouseX < leftPos + 168))
|
||||
&& mouseY >= topPos + 27 && mouseY < topPos + 43
|
||||
&& grammarTooltip != null) {
|
||||
graphics.renderTooltip(font, grammarTooltip, mouseX, mouseY);
|
||||
&& (status != null || menu.getSlot(BookBinderMenu.RESULT).getItem().isEmpty())) {
|
||||
java.util.List<Component> details = new java.util.ArrayList<>();
|
||||
if (status != null) details.add(status);
|
||||
details.add(grammarTooltip);
|
||||
if (menu.grammarInstability() > 0 && severity != WritingGrammar.Severity.INSTABILITY)
|
||||
details.add(Component.translatable("screen.rivencraft.book_binder.status.unstable",
|
||||
menu.grammarInstability()));
|
||||
if (menu.grammarIncompleteCount() > 0 && severity != WritingGrammar.Severity.SEEDED_COMPLETION)
|
||||
details.add(Component.translatable("screen.rivencraft.book_binder.status.seeded_count",
|
||||
menu.grammarIncompleteCount()));
|
||||
graphics.renderComponentTooltip(font, details, mouseX, mouseY);
|
||||
return;
|
||||
}
|
||||
if (mouseY >= topPos + STRIP_Y && mouseY < topPos + STRIP_Y + STRIP_HEIGHT) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.dnorberg.rivencraft.writing.SymbolDisplayMetadata;
|
||||
import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||
import net.dnorberg.rivencraft.writing.WritingGrammar;
|
||||
import net.dnorberg.rivencraft.client.preview.DestinationPreviewRenderer;
|
||||
import net.dnorberg.rivencraft.client.preview.DestinationPreviewRenderers;
|
||||
import net.minecraft.core.BlockPos;
|
||||
@@ -226,6 +227,22 @@ public final class DescriptiveBookScreen extends Screen implements ExplorableBoo
|
||||
x, top + 40, 110);
|
||||
drawWrappedWithoutShadow(graphics, Component.translatable("screen.rivencraft.age.name_prompt"),
|
||||
x, top + 52, 110);
|
||||
WritingGrammar.Result grammar = WritingGrammar.parse(writing);
|
||||
boolean unresolved = writingSymbols.stream().anyMatch(display -> !display.resolved());
|
||||
if (!grammar.canCreate() || unresolved) {
|
||||
drawWrappedWithoutShadow(graphics,
|
||||
Component.translatable("screen.rivencraft.age.writing.fatal"),
|
||||
x, top + 132, 110, UNRESOLVED_TEXT_COLOR);
|
||||
} else if (grammar.writingInstability() > 0) {
|
||||
drawWrappedWithoutShadow(graphics,
|
||||
Component.translatable("screen.rivencraft.age.writing.unstable",
|
||||
grammar.writingInstability()), x, top + 132, 110);
|
||||
}
|
||||
if (grammar.firstIncomplete().isPresent()) {
|
||||
drawWrappedWithoutShadow(graphics,
|
||||
Component.translatable("screen.rivencraft.age.writing.seeded"),
|
||||
x, top + 145, 110);
|
||||
}
|
||||
return;
|
||||
}
|
||||
int nameEnd = drawWrappedWithoutShadow(graphics, Component.literal(age.name()),
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
package net.dnorberg.rivencraft.gametest;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import net.dnorberg.rivencraft.Rivencraft;
|
||||
import net.dnorberg.rivencraft.block.entity.BookBinderBlockEntity;
|
||||
import net.dnorberg.rivencraft.book.AgeReference;
|
||||
import net.dnorberg.rivencraft.item.PageItem;
|
||||
import net.dnorberg.rivencraft.registry.ModBlocks;
|
||||
import net.dnorberg.rivencraft.registry.ModDataComponents;
|
||||
import net.dnorberg.rivencraft.registry.ModItems;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||
import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||
import net.dnorberg.rivencraft.writing.WritingGrammar;
|
||||
import net.minecraft.core.BlockPos;
|
||||
import net.minecraft.gametest.framework.GameTest;
|
||||
import net.minecraft.gametest.framework.GameTestHelper;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.item.Items;
|
||||
import net.neoforged.neoforge.gametest.GameTestHolder;
|
||||
import net.neoforged.neoforge.gametest.PrefixGameTestTemplate;
|
||||
|
||||
/** Focused classification and transaction checks for Milestone 6 issue #65. */
|
||||
@GameTestHolder(Rivencraft.MOD_ID)
|
||||
@PrefixGameTestTemplate(false)
|
||||
public final class Milestone6GrammarGameTests {
|
||||
private Milestone6GrammarGameTests() {}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void writingOutcomesAreDistinct(GameTestHelper helper) {
|
||||
var terrain = symbol("rivencraft:terrain/standard");
|
||||
var weather = symbol("rivencraft:weather/normal");
|
||||
var missing = symbol("example:missing_definition");
|
||||
var fatal = WritingGrammar.parse(new OrderedWriting(List.of(terrain)));
|
||||
helper.assertValueEqual(fatal.firstError().orElseThrow().severity(), WritingGrammar.Severity.FATAL,
|
||||
"A missing Link Panel is fatal");
|
||||
helper.assertTrue(!WritingGrammar.parse(new OrderedWriting(List.of(WritingEntry.linkPanel(), missing)))
|
||||
.canCreate(), "An unresolved symbol must remain fatal");
|
||||
|
||||
var incomplete = WritingGrammar.parse(new OrderedWriting(List.of(WritingEntry.linkPanel(), terrain)));
|
||||
helper.assertTrue(incomplete.canCreate() && incomplete.firstIncomplete().isPresent(),
|
||||
"Missing branches should be seeded rather than rejected");
|
||||
helper.assertValueEqual(incomplete.firstIncomplete().orElseThrow().severity(),
|
||||
WritingGrammar.Severity.SEEDED_COMPLETION, "Seeded completion needs its own classification");
|
||||
helper.assertValueEqual(incomplete.writingInstability(), 0,
|
||||
"Missing branches alone must not make an Age unstable");
|
||||
|
||||
OrderedWriting unstableWriting = new OrderedWriting(List.of(WritingEntry.linkPanel(), weather,
|
||||
terrain, terrain));
|
||||
var unstable = WritingGrammar.parse(unstableWriting);
|
||||
helper.assertTrue(unstable.canCreate() && unstable.firstInstability().isPresent(),
|
||||
"Supported order and duplicate defects should remain authorable");
|
||||
helper.assertValueEqual(unstable.writingInstability(), 600,
|
||||
"A misplaced terrain and duplicate choice should score once each");
|
||||
helper.assertValueEqual(unstable.symbols().size(), 3,
|
||||
"The diagnostic must preserve all authored page IDs and order");
|
||||
helper.assertValueEqual(unstable.symbols().get(2).reference().id(),
|
||||
ResourceLocation.parse("rivencraft:terrain/standard"),
|
||||
"The duplicate page must remain in the frozen writing");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
@GameTest(template = "empty")
|
||||
public static void rejectedBinderKeepsInputsAndUnstablePreview(GameTestHelper helper) {
|
||||
BlockPos pos = helper.absolutePos(new BlockPos(1, 1, 1));
|
||||
helper.getLevel().setBlock(pos, ModBlocks.BOOK_BINDER.get().defaultBlockState(), 3);
|
||||
var binder = (BookBinderBlockEntity) helper.getLevel().getBlockEntity(pos);
|
||||
helper.assertTrue(binder != null, "Binder must exist");
|
||||
binder.setTitle("Grammar Transaction " + UUID.randomUUID());
|
||||
binder.setItem(BookBinderBlockEntity.COVER, new ItemStack(Items.LEATHER));
|
||||
binder.setItem(BookBinderBlockEntity.FIRST_PAGE, page("rivencraft:terrain/standard"));
|
||||
helper.assertTrue(!binder.canAssemble() && !binder.consumeAssembly(),
|
||||
"Missing panel must reject before consuming inputs");
|
||||
helper.assertTrue(binder.getItem(BookBinderBlockEntity.COVER).is(Items.LEATHER)
|
||||
&& binder.getItem(BookBinderBlockEntity.FIRST_PAGE).is(ModItems.PAGE.get()),
|
||||
"A structural failure must preserve cover and page");
|
||||
|
||||
binder.setItem(BookBinderBlockEntity.LINK_PANEL, PageItem.createLinkPanelPage());
|
||||
binder.setItem(BookBinderBlockEntity.FIRST_PAGE + 1,
|
||||
PageItem.createRecoveredPage(new SymbolReference(ResourceLocation.parse("example:missing"))));
|
||||
helper.assertTrue(!binder.canAssemble() && !binder.consumeAssembly(),
|
||||
"Missing registry entry must reject without consuming ink or pages");
|
||||
helper.assertTrue(!binder.getItem(BookBinderBlockEntity.LINK_PANEL).isEmpty()
|
||||
&& !binder.getItem(BookBinderBlockEntity.FIRST_PAGE + 1).isEmpty(),
|
||||
"Rejected writing must keep the Link Panel and unresolved Page");
|
||||
|
||||
binder.setItem(BookBinderBlockEntity.FIRST_PAGE + 1, page("rivencraft:terrain/standard"));
|
||||
helper.assertTrue(binder.canAssemble(), "Known duplicate terrain should be accepted as unstable");
|
||||
helper.assertValueEqual(binder.writingGrammar().writingInstability(), 500,
|
||||
"Binder must expose the instability cost before assembly");
|
||||
ItemStack preview = binder.previewBook();
|
||||
helper.assertTrue(preview.get(ModDataComponents.AGE_REFERENCE.get()) instanceof AgeReference,
|
||||
"Accepted writing must preview a stable Age identity without consuming inputs");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
private static WritingEntry symbol(String id) {
|
||||
return WritingEntry.symbol(new SymbolReference(ResourceLocation.parse(id)));
|
||||
}
|
||||
|
||||
private static ItemStack page(String id) {
|
||||
return PageItem.createSymbolPage(new SymbolReference(ResourceLocation.parse(id)));
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,9 @@ public final class BookBinderMenu extends AbstractContainerMenu {
|
||||
private int selectedPage = -1;
|
||||
private int grammarErrorCode = -1;
|
||||
private int grammarErrorStripIndex = -1;
|
||||
private int grammarInstability;
|
||||
private int grammarIncompleteCount;
|
||||
private WritingGrammar.Result serverGrammar;
|
||||
|
||||
public BookBinderMenu(int id, Inventory inventory, RegistryFriendlyByteBuf data) {
|
||||
this(id, inventory, inventory.player.level().getBlockEntity(data.readBlockPos())
|
||||
@@ -68,6 +71,22 @@ public final class BookBinderMenu extends AbstractContainerMenu {
|
||||
}
|
||||
@Override public void set(int value) { grammarErrorStripIndex = value; }
|
||||
});
|
||||
addDataSlot(new DataSlot() {
|
||||
@Override public int get() {
|
||||
return clientSide || binder == null ? grammarInstability
|
||||
: serverGrammar().writingInstability();
|
||||
}
|
||||
@Override public void set(int value) { grammarInstability = value; }
|
||||
});
|
||||
addDataSlot(new DataSlot() {
|
||||
@Override public int get() {
|
||||
return clientSide || binder == null ? grammarIncompleteCount
|
||||
: (int) serverGrammar().diagnostics().stream()
|
||||
.filter(diagnostic -> diagnostic.severity()
|
||||
== WritingGrammar.Severity.SEEDED_COMPLETION).count();
|
||||
}
|
||||
@Override public void set(int value) { grammarIncompleteCount = value; }
|
||||
});
|
||||
addSlot(new BinderSlot(BookBinderBlockEntity.COVER, 8, 27, 64));
|
||||
addSlot(new BinderSlot(BookBinderBlockEntity.LINK_PANEL, -1000, -1000, 1));
|
||||
addSlot(new BinderSlot(BookBinderBlockEntity.NOTEBOOK, -1000, -1000, 1));
|
||||
@@ -87,9 +106,13 @@ public final class BookBinderMenu extends AbstractContainerMenu {
|
||||
}
|
||||
|
||||
public int selectedPage() { return selectedPage; }
|
||||
private WritingGrammar.Result serverGrammar() {
|
||||
if (serverGrammar == null) serverGrammar = binder.writingGrammar();
|
||||
return serverGrammar;
|
||||
}
|
||||
private Optional<WritingGrammar.Diagnostic> binderDiagnostic() {
|
||||
WritingGrammar.Result grammar = binder.writingGrammar();
|
||||
return grammar.firstError().or(grammar::firstWarning);
|
||||
WritingGrammar.Result grammar = serverGrammar();
|
||||
return grammar.firstError().or(grammar::firstInstability).or(grammar::firstIncomplete);
|
||||
}
|
||||
public Optional<WritingGrammar.Code> grammarError() {
|
||||
WritingGrammar.Code[] codes = WritingGrammar.Code.values();
|
||||
@@ -97,11 +120,13 @@ public final class BookBinderMenu extends AbstractContainerMenu {
|
||||
? Optional.of(codes[grammarErrorCode]) : Optional.empty();
|
||||
}
|
||||
public boolean grammarWarning() {
|
||||
return grammarError().map(code -> switch (code) {
|
||||
case DUPLICATE, CONFLICT, OUT_OF_ORDER, MISPLACED_MODIFIER -> true;
|
||||
default -> false;
|
||||
}).orElse(false);
|
||||
return grammarSeverity().orElse(null) == WritingGrammar.Severity.INSTABILITY;
|
||||
}
|
||||
public Optional<WritingGrammar.Severity> grammarSeverity() {
|
||||
return grammarError().map(WritingGrammar.Code::severity);
|
||||
}
|
||||
public int grammarInstability() { return grammarInstability; }
|
||||
public int grammarIncompleteCount() { return grammarIncompleteCount; }
|
||||
public int grammarErrorStripIndex() { return grammarErrorStripIndex; }
|
||||
public String title() { return binder == null ? "" : binder.title(); }
|
||||
public boolean setTitle(Player player, String title) {
|
||||
@@ -228,6 +253,7 @@ public final class BookBinderMenu extends AbstractContainerMenu {
|
||||
}
|
||||
|
||||
@Override public void broadcastChanges() {
|
||||
serverGrammar = null;
|
||||
refreshResult();
|
||||
super.broadcastChanges();
|
||||
}
|
||||
|
||||
@@ -175,22 +175,32 @@ 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,
|
||||
UNKNOWN_BIOME, UNSUPPORTED_BIOME, UNSUPPORTED_ENVIRONMENT, UNSUPPORTED_FEATURE
|
||||
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;
|
||||
default -> Severity.FATAL;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public enum Severity { FATAL, SEEDED_COMPLETION, INSTABILITY }
|
||||
|
||||
public record Diagnostic(Code code, int index, String message) {
|
||||
public Diagnostic {
|
||||
Objects.requireNonNull(code, "code");
|
||||
Objects.requireNonNull(message, "message");
|
||||
}
|
||||
|
||||
public boolean error() {
|
||||
return switch (code) {
|
||||
case INCOMPLETE, DUPLICATE, CONFLICT, OUT_OF_ORDER, MISPLACED_MODIFIER -> false;
|
||||
default -> true;
|
||||
};
|
||||
public Severity severity() {
|
||||
return code.severity();
|
||||
}
|
||||
|
||||
public boolean error() { return severity() == Severity.FATAL; }
|
||||
|
||||
/** The known writing defects have an explicit, provisional M6 score. */
|
||||
public int instability() {
|
||||
return switch (code) {
|
||||
@@ -218,7 +228,13 @@ public final class WritingGrammar {
|
||||
public boolean canCreate() { return diagnostics.stream().noneMatch(Diagnostic::error); }
|
||||
public Optional<Diagnostic> firstError() { return diagnostics.stream().filter(Diagnostic::error).findFirst(); }
|
||||
public Optional<Diagnostic> firstWarning() {
|
||||
return diagnostics.stream().filter(diagnostic -> diagnostic.instability() > 0).findFirst();
|
||||
return firstInstability();
|
||||
}
|
||||
public Optional<Diagnostic> firstInstability() {
|
||||
return diagnostics.stream().filter(diagnostic -> diagnostic.severity() == Severity.INSTABILITY).findFirst();
|
||||
}
|
||||
public Optional<Diagnostic> firstIncomplete() {
|
||||
return diagnostics.stream().filter(diagnostic -> diagnostic.severity() == Severity.SEEDED_COMPLETION).findFirst();
|
||||
}
|
||||
public int writingInstability() {
|
||||
return diagnostics.stream().mapToInt(Diagnostic::instability).sum();
|
||||
|
||||
@@ -51,6 +51,18 @@
|
||||
"screen.rivencraft.book_binder.grammar.conflict": "Page %s: conflicting page adds instability; the supported choice is used",
|
||||
"screen.rivencraft.book_binder.grammar.out_of_order": "Page %s: incorrect order adds instability; the Age repairs the order",
|
||||
"screen.rivencraft.book_binder.grammar.misplaced_modifier": "Page %s: misplaced modifier adds instability; the Age repairs its order",
|
||||
"screen.rivencraft.book_binder.grammar.incomplete": "Missing branches will be completed from the Age seed",
|
||||
"screen.rivencraft.book_binder.grammar.unknown_biome": "Page %s: biome is unavailable; restore its data pack",
|
||||
"screen.rivencraft.book_binder.grammar.unsupported_biome": "Page %s: biome is incompatible with this terrain",
|
||||
"screen.rivencraft.book_binder.grammar.unsupported_feature": "Page %s: feature is incompatible with this Age",
|
||||
"screen.rivencraft.book_binder.grammar.unsupported_combination": "These pages cannot generate together; no inputs will be consumed",
|
||||
"screen.rivencraft.book_binder.status.fatal": "Blocked",
|
||||
"screen.rivencraft.book_binder.status.unstable": "Unstable +%s",
|
||||
"screen.rivencraft.book_binder.status.seeded": "Seeded",
|
||||
"screen.rivencraft.book_binder.status.seeded_count": "%s missing branches will be seeded",
|
||||
"screen.rivencraft.age.writing.fatal": "Writing blocked",
|
||||
"screen.rivencraft.age.writing.unstable": "Unstable +%s",
|
||||
"screen.rivencraft.age.writing.seeded": "Missing parts seeded",
|
||||
"screen.rivencraft.writing_desk.ink": "Ink: %s/%s",
|
||||
"screen.rivencraft.writing_desk.search": "Search",
|
||||
"screen.rivencraft.writing_desk.sort": "Sort pages by name (A–Z)",
|
||||
|
||||
Reference in new issue
Block a user