Public Access
Audit Legacy instability and allow unstable supported writing
Build / build (push) Canceled after 0s
Build / build (push) Canceled after 0s
Persist repaired Age writing and provisional instability penalties, retain fatal diagnostics for unsupported content, and hide TerraBlender's deferred placeholder biome from creative and JEI.
This commit is contained in:
1 parent
a03e005a03
commit
86cda69d11
20 files changed
+274
-62
No files matched your search
@@ -138,7 +138,7 @@ public final class BookBinderBlockEntity extends BlockEntity implements Containe
|
||||
List<WritingGrammar.Diagnostic> diagnostics = new ArrayList<>(parsed.diagnostics());
|
||||
var terrain = parsed.symbols().stream()
|
||||
.filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.TERRAIN)
|
||||
.findFirst();
|
||||
.reduce((first, last) -> last);
|
||||
if (terrain.isPresent()) {
|
||||
var profileId = ((SymbolSemantics.ResourceValue) terrain.orElseThrow().semantics().value()).id();
|
||||
if (!AgeRuntimeProfiles.profiles().containsKey(profileId)) {
|
||||
|
||||
@@ -15,14 +15,14 @@ public final class AgeEnvironmentCompiler {
|
||||
AgeTraits.Weather weather = seeded.weather();
|
||||
double period = seeded.celestialPeriod();
|
||||
float phase = seeded.celestialPhase();
|
||||
boolean normalTime = plan.symbols().stream()
|
||||
boolean normalTime = plan.activeSymbols().stream()
|
||||
.anyMatch(symbol -> symbol.semantics().role() == SymbolSemantics.Role.CELESTIAL);
|
||||
if (normalTime) period = 1.0D;
|
||||
boolean writtenPeriod = false;
|
||||
boolean writtenPhase = false;
|
||||
float phaseDegrees = 0.0F;
|
||||
|
||||
for (AgeGenerationPlan.ResolvedSymbol symbol : plan.symbols()) {
|
||||
for (AgeGenerationPlan.ResolvedSymbol symbol : plan.activeSymbols()) {
|
||||
SymbolSemantics semantics = symbol.semantics();
|
||||
switch (semantics.role()) {
|
||||
case LIGHTING -> lighting = AgeTraits.Lighting.valueOf(
|
||||
@@ -61,7 +61,7 @@ public final class AgeEnvironmentCompiler {
|
||||
/** Vanilla effects are the sky styles the current renderer can honor exactly. */
|
||||
public static ResourceLocation skyEffects(AgeGenerationPlan plan, ResourceLocation fallback) {
|
||||
if (plan == null) return fallback;
|
||||
return plan.symbols().stream()
|
||||
return plan.activeSymbols().stream()
|
||||
.filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.SKY)
|
||||
.map(symbol -> ((SymbolSemantics.ChoiceValue) symbol.semantics().value()).id())
|
||||
.map(choice -> switch (choice) {
|
||||
@@ -73,7 +73,7 @@ public final class AgeEnvironmentCompiler {
|
||||
}
|
||||
|
||||
public static boolean hasWrittenPhase(AgeGenerationPlan plan) {
|
||||
return plan != null && plan.symbols().stream()
|
||||
return plan != null && plan.activeSymbols().stream()
|
||||
.anyMatch(symbol -> symbol.semantics().role() == SymbolSemantics.Role.PHASE_MODIFIER);
|
||||
}
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ public final class AgeFeatureSelection {
|
||||
Map<ResourceLocation, List<ResourceLocation>> materialOrders = new HashMap<>();
|
||||
Map<ResourceLocation, Integer> occurrences = new HashMap<>();
|
||||
List<Choice> resolved = new ArrayList<>();
|
||||
for (Choice choice : choices(plan.symbols().stream().map(symbol -> symbol.semantics()).toList(),
|
||||
for (Choice choice : choices(plan.activeSymbols().stream().map(symbol -> symbol.semantics()).toList(),
|
||||
plan.runtimeProfile(), plan.ageSeed())) {
|
||||
if (choice.material().isPresent()
|
||||
|| !isRock(choice.id()) && !isLake(choice.id())
|
||||
|
||||
@@ -16,8 +16,9 @@ import net.minecraft.resources.ResourceLocation;
|
||||
|
||||
/** A frozen interpretation of authored pages. Later slices consume its resolved branches. */
|
||||
public record AgeGenerationPlan(int version, long ageSeed, OrderedWriting writing, List<ResolvedSymbol> symbols,
|
||||
AgeRuntimeProfile runtimeProfile, AgeBiomeSelection biomeSelection) {
|
||||
public static final int CURRENT_VERSION = 2;
|
||||
AgeRuntimeProfile runtimeProfile, AgeBiomeSelection biomeSelection,
|
||||
List<Integer> activeSymbolIndices, int writingInstability) {
|
||||
public static final int CURRENT_VERSION = 3;
|
||||
|
||||
public AgeGenerationPlan {
|
||||
if (version < 1 || version > CURRENT_VERSION) {
|
||||
@@ -25,12 +26,14 @@ public record AgeGenerationPlan(int version, long ageSeed, OrderedWriting writin
|
||||
}
|
||||
Objects.requireNonNull(writing, "writing");
|
||||
symbols = List.copyOf(symbols);
|
||||
activeSymbolIndices = List.copyOf(activeSymbolIndices);
|
||||
if (writingInstability < 0) throw new IllegalArgumentException("Negative writing instability");
|
||||
Objects.requireNonNull(runtimeProfile, "runtimeProfile");
|
||||
if (runtimeProfile.schemaVersion() != AgeRuntimeProfile.CURRENT_SCHEMA_VERSION) {
|
||||
throw new IllegalArgumentException("Unsupported runtime profile snapshot version");
|
||||
}
|
||||
if (version == CURRENT_VERSION && biomeSelection == null) {
|
||||
throw new IllegalArgumentException("Version 2 Age plan requires a biome selection snapshot");
|
||||
if (version >= 2 && biomeSelection == null) {
|
||||
throw new IllegalArgumentException("Version 2+ Age plan requires a biome selection snapshot");
|
||||
}
|
||||
for (ResolvedSymbol symbol : symbols) {
|
||||
if (symbol.index() < 0 || symbol.index() >= writing.entries().size()
|
||||
@@ -38,12 +41,31 @@ public record AgeGenerationPlan(int version, long ageSeed, OrderedWriting writin
|
||||
throw new IllegalArgumentException("Resolved symbol does not match original page order");
|
||||
}
|
||||
}
|
||||
if (version >= 3 && !symbols.isEmpty() && activeSymbolIndices.isEmpty()) {
|
||||
throw new IllegalArgumentException("Version 3 Age plan has no active symbols");
|
||||
}
|
||||
if (activeSymbolIndices.stream().distinct().count() != activeSymbolIndices.size()) {
|
||||
throw new IllegalArgumentException("Age plan repeats an active symbol index");
|
||||
}
|
||||
for (int index : activeSymbolIndices) {
|
||||
if (symbols.stream().noneMatch(symbol -> symbol.index() == index)) {
|
||||
throw new IllegalArgumentException("Active symbol is not in the saved writing: " + index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Existing version 1 plans retain their original biome behavior on reload. */
|
||||
public AgeGenerationPlan(int version, long ageSeed, OrderedWriting writing, List<ResolvedSymbol> symbols,
|
||||
AgeRuntimeProfile runtimeProfile) {
|
||||
this(version, ageSeed, writing, symbols, runtimeProfile, null);
|
||||
this(version, ageSeed, writing, symbols, runtimeProfile, null,
|
||||
symbols.stream().map(ResolvedSymbol::index).toList(), 0);
|
||||
}
|
||||
|
||||
/** Existing call sites and version-2 plans retain every resolved symbol. */
|
||||
public AgeGenerationPlan(int version, long ageSeed, OrderedWriting writing, List<ResolvedSymbol> symbols,
|
||||
AgeRuntimeProfile runtimeProfile, AgeBiomeSelection biomeSelection) {
|
||||
this(version, ageSeed, writing, symbols, runtimeProfile, biomeSelection,
|
||||
symbols.stream().map(ResolvedSymbol::index).toList(), 0);
|
||||
}
|
||||
|
||||
/** No plan is made for old page-free books or Link-Panel-only random Ages. */
|
||||
@@ -52,7 +74,9 @@ public record AgeGenerationPlan(int version, long ageSeed, OrderedWriting writin
|
||||
WritingGrammar.Result parsed = WritingGrammar.parse(writing);
|
||||
if (!parsed.canCreate()) throw new IllegalArgumentException(parsed.firstError().orElseThrow().message());
|
||||
if (parsed.randomFallback()) return null;
|
||||
ResourceLocation terrain = parsed.symbols().stream()
|
||||
List<WritingGrammar.ParsedSymbol> active = parsed.activeSymbols();
|
||||
WritingGrammar.Result effective = new WritingGrammar.Result(active, parsed.diagnostics());
|
||||
ResourceLocation terrain = active.stream()
|
||||
.filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.TERRAIN)
|
||||
.map(symbol -> ((SymbolSemantics.ResourceValue) symbol.semantics().value()).id())
|
||||
.findFirst().orElse(null);
|
||||
@@ -64,23 +88,29 @@ public record AgeGenerationPlan(int version, long ageSeed, OrderedWriting writin
|
||||
ResourceLocation selected = terrain == null ? seedProfile : terrain;
|
||||
AgeRuntimeProfile profile = AgeRuntimeProfiles.profiles().get(selected);
|
||||
if (profile == null) throw new IllegalArgumentException("Unsupported Age terrain profile " + selected);
|
||||
AgeBiomeSelection biomes = AgeBiomeSelection.compile(parsed, traits, profile, ageSeed, registries);
|
||||
AgeFeatureSelection.compile(parsed, profile, biomes, registries, ageSeed);
|
||||
AgeBiomeSelection biomes = AgeBiomeSelection.compile(effective, traits, profile, ageSeed, registries);
|
||||
AgeFeatureSelection.compile(effective, profile, biomes, registries, ageSeed);
|
||||
AgeGenerationPlan plan = new AgeGenerationPlan(CURRENT_VERSION, ageSeed, writing,
|
||||
parsed.symbols().stream().map(symbol -> new ResolvedSymbol(
|
||||
symbol.index(), symbol.reference(), symbol.semantics())).toList(),
|
||||
profile, biomes);
|
||||
profile, biomes, active.stream().map(WritingGrammar.ParsedSymbol::index).toList(),
|
||||
parsed.writingInstability());
|
||||
AgeEnvironmentCompiler.apply(plan, traits);
|
||||
AgeSeasonMode.validate(plan);
|
||||
return plan;
|
||||
}
|
||||
|
||||
public ResourceLocation authoredTerrain() {
|
||||
return symbols.stream().filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.TERRAIN)
|
||||
return activeSymbols().stream().filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.TERRAIN)
|
||||
.map(symbol -> ((SymbolSemantics.ResourceValue) symbol.semantics().value()).id())
|
||||
.findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public List<ResolvedSymbol> activeSymbols() {
|
||||
return activeSymbolIndices.stream().map(index -> symbols.stream()
|
||||
.filter(symbol -> symbol.index() == index).findFirst().orElseThrow()).toList();
|
||||
}
|
||||
|
||||
public CompoundTag save() {
|
||||
CompoundTag tag = new CompoundTag();
|
||||
tag.putInt("Version", version);
|
||||
@@ -106,6 +136,10 @@ public record AgeGenerationPlan(int version, long ageSeed, OrderedWriting writin
|
||||
resolved.add(item);
|
||||
}
|
||||
tag.put("Resolved", resolved);
|
||||
if (version >= 3) {
|
||||
tag.putIntArray("ActiveSymbolIndices", activeSymbolIndices);
|
||||
tag.putInt("WritingInstability", writingInstability);
|
||||
}
|
||||
CompoundTag profile = new CompoundTag();
|
||||
profile.putInt("Version", runtimeProfile.schemaVersion());
|
||||
profile.putString("Terrain", runtimeProfile.terrain().name());
|
||||
@@ -162,12 +196,16 @@ public record AgeGenerationPlan(int version, long ageSeed, OrderedWriting writin
|
||||
selection = new AgeBiomeSelection(AgeTraits.BiomeDistribution.valueOf(
|
||||
biomeTag.getString("Distribution")), ids);
|
||||
}
|
||||
List<Integer> activeIndices = version >= 3
|
||||
? java.util.Arrays.stream(tag.getIntArray("ActiveSymbolIndices")).boxed().toList()
|
||||
: resolved.stream().map(ResolvedSymbol::index).toList();
|
||||
return new AgeGenerationPlan(version, tag.getLong("AgeSeed"), new OrderedWriting(pages), resolved,
|
||||
new AgeRuntimeProfile(profile.getInt("Version"),
|
||||
AgeRuntimeProfile.Terrain.valueOf(profile.getString("Terrain")),
|
||||
ResourceLocation.parse(profile.getString("Noise")),
|
||||
ResourceLocation.parse(profile.getString("Biomes")),
|
||||
ResourceLocation.parse(profile.getString("Dimension"))), selection);
|
||||
ResourceLocation.parse(profile.getString("Dimension"))), selection,
|
||||
activeIndices, version >= 3 ? tag.getInt("WritingInstability") : 0);
|
||||
}
|
||||
|
||||
public record ResolvedSymbol(int index, SymbolReference reference, SymbolSemantics semantics) {
|
||||
|
||||
@@ -69,22 +69,24 @@ public final class AgeRuntimeProfiles {
|
||||
public static ResourceLocation selectForWriting(long seed, OrderedWriting writing, RegistryAccess registries) {
|
||||
ResourceLocation selected = select(seed);
|
||||
WritingGrammar.Result parsed = WritingGrammar.parse(writing);
|
||||
if (parsed.symbols().stream().anyMatch(symbol ->
|
||||
if (parsed.activeSymbols().stream().anyMatch(symbol ->
|
||||
symbol.semantics().role() == SymbolSemantics.Role.TERRAIN)) return selected;
|
||||
List<ResourceLocation> features = parsed.symbols().stream()
|
||||
List<ResourceLocation> features = parsed.activeSymbols().stream()
|
||||
.filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.FEATURE)
|
||||
.map(symbol -> ((SymbolSemantics.ResourceValue) symbol.semantics().value()).id()).toList();
|
||||
boolean hasMaterial = parsed.symbols().stream().anyMatch(symbol ->
|
||||
boolean hasMaterial = parsed.activeSymbols().stream().anyMatch(symbol ->
|
||||
symbol.semantics().role() == SymbolSemantics.Role.BLOCK);
|
||||
if ((features.isEmpty() && !hasMaterial) || PROFILES.isEmpty()) return selected;
|
||||
AgeRuntimeProfile initial = PROFILES.get(selected);
|
||||
if (initial != null && features.stream().allMatch(id ->
|
||||
AgeFeatureSelection.error(id, initial, registries) == null)
|
||||
&& AgeFeatureSelection.supportsUnpairedMaterials(parsed, initial)) return selected;
|
||||
&& AgeFeatureSelection.supportsUnpairedMaterials(
|
||||
new WritingGrammar.Result(parsed.activeSymbols(), parsed.diagnostics()), initial)) return selected;
|
||||
List<ResourceLocation> compatible = PROFILES.entrySet().stream()
|
||||
.filter(entry -> features.stream().allMatch(id ->
|
||||
AgeFeatureSelection.error(id, entry.getValue(), registries) == null)
|
||||
&& AgeFeatureSelection.supportsUnpairedMaterials(parsed, entry.getValue()))
|
||||
&& AgeFeatureSelection.supportsUnpairedMaterials(
|
||||
new WritingGrammar.Result(parsed.activeSymbols(), parsed.diagnostics()), entry.getValue()))
|
||||
.map(Map.Entry::getKey).sorted().toList();
|
||||
return compatible.isEmpty() ? selected
|
||||
: compatible.get(Math.floorMod(Long.hashCode(seed), compatible.size()));
|
||||
|
||||
@@ -35,7 +35,7 @@ public enum AgeSeasonMode {
|
||||
}
|
||||
|
||||
public static boolean hasAuthoredPage(AgeGenerationPlan plan) {
|
||||
return plan != null && plan.symbols().stream()
|
||||
return plan != null && plan.activeSymbols().stream()
|
||||
.anyMatch(symbol -> symbol.semantics().role() == SymbolSemantics.Role.SEASON);
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ public enum AgeSeasonMode {
|
||||
|
||||
public static AgeSeasonMode from(AgeGenerationPlan plan) {
|
||||
if (plan == null) return SEASONS;
|
||||
return plan.symbols().stream()
|
||||
return plan.activeSymbols().stream()
|
||||
.filter(symbol -> symbol.semantics().role() == SymbolSemantics.Role.SEASON)
|
||||
.map(symbol -> valueOf(((SymbolSemantics.ChoiceValue) symbol.semantics().value())
|
||||
.id().toUpperCase(Locale.ROOT)))
|
||||
|
||||
@@ -91,8 +91,10 @@ public final class EnvironmentWritingGameTests {
|
||||
AgeSeasonMode.NO_SEASONS, "The Age's seasonal policy must survive reload");
|
||||
OrderedWriting conflict = new OrderedWriting(List.of(WritingEntry.linkPanel(),
|
||||
page("season/no_seasons"), page("season/eternal_winter")));
|
||||
helper.assertFalse(WritingGrammar.parse(conflict).canCreate(),
|
||||
"Only one seasonal page may be used in an Age");
|
||||
helper.assertTrue(WritingGrammar.parse(conflict).canCreate(),
|
||||
"A repeated seasonal page must be recoverable as unstable writing");
|
||||
helper.assertValueEqual(WritingGrammar.parse(conflict).writingInstability(), 500,
|
||||
"The seasonal conflict must contribute instability");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ public final class BookBinderScreen extends AbstractContainerScreen<BookBinderMe
|
||||
Component grammarTooltip = menu.grammarError().map(code -> Component.translatable(
|
||||
"screen.rivencraft.book_binder.grammar." + code.name().toLowerCase(Locale.ROOT),
|
||||
errorStripIndex + 1)).orElse(null);
|
||||
if (menu.getSlot(BookBinderMenu.RESULT).getItem().isEmpty()
|
||||
if ((menu.grammarWarning() || menu.getSlot(BookBinderMenu.RESULT).getItem().isEmpty())
|
||||
&& mouseX >= leftPos + 152 && mouseX < leftPos + 168
|
||||
&& mouseY >= topPos + 27 && mouseY < topPos + 43
|
||||
&& grammarTooltip != null) {
|
||||
@@ -169,7 +169,8 @@ public final class BookBinderScreen extends AbstractContainerScreen<BookBinderMe
|
||||
case SYMBOL, UNRESOLVED_SYMBOL -> graphics.blit(NotebookScreen.symbolTexture(page),
|
||||
x + 1, y + 5, 24, 24, 0, 0, 128, 128, 128, 128);
|
||||
}
|
||||
if (index == menu.grammarErrorStripIndex() && menu.grammarError().isPresent()) {
|
||||
if (index == menu.grammarErrorStripIndex() && menu.grammarError().isPresent()
|
||||
&& !menu.grammarWarning()) {
|
||||
graphics.fill(x, y, x + PAGE_WIDTH, y + PAGE_HEIGHT, 0x55FF2020);
|
||||
graphics.renderOutline(x, y, PAGE_WIDTH, PAGE_HEIGHT, 0xFFFF3030);
|
||||
}
|
||||
|
||||
@@ -108,21 +108,22 @@ public final class BookBinderGameTests {
|
||||
binder.setItem(BookBinderBlockEntity.LINK_PANEL, PageItem.createLinkPanelPage());
|
||||
binder.setItem(BookBinderBlockEntity.FIRST_PAGE, terrainPage());
|
||||
binder.setItem(BookBinderBlockEntity.FIRST_PAGE + 5, terrainPage());
|
||||
helper.assertFalse(binder.canAssemble(), "Duplicate terrain must not assemble a new Age");
|
||||
int errorEntry = binder.writingGrammar().firstError().orElseThrow().index();
|
||||
helper.assertTrue(binder.canAssemble(), "Duplicate terrain must create an unstable Age");
|
||||
int errorEntry = binder.writingGrammar().firstWarning().orElseThrow().index();
|
||||
helper.assertValueEqual(errorEntry, 2, "Parser must identify the second authored symbol");
|
||||
helper.assertValueEqual(binder.writingStripIndex(errorEntry), 6,
|
||||
"A grammar error must highlight the correct strip page despite empty positions");
|
||||
helper.assertFalse(binder.consumeAssembly(), "Rejected grammar must not consume binder inputs");
|
||||
"An instability warning must highlight the correct strip page despite empty positions");
|
||||
helper.assertValueEqual(binder.writingGrammar().writingInstability(), 500,
|
||||
"An extra controller must carry a persisted writing penalty");
|
||||
helper.assertTrue(binder.getItem(BookBinderBlockEntity.COVER).is(Items.LEATHER),
|
||||
"Rejected grammar must leave the cover recoverable");
|
||||
"Warning preview must not consume the cover");
|
||||
helper.assertTrue(binder.getItem(BookBinderBlockEntity.LINK_PANEL).is(ModItems.PAGE.get()),
|
||||
"Rejected grammar must leave the Link Panel recoverable");
|
||||
"Warning preview must not consume the Link Panel");
|
||||
helper.assertTrue(binder.getItem(BookBinderBlockEntity.FIRST_PAGE + 5).is(ModItems.PAGE.get()),
|
||||
"Rejected grammar must leave the duplicate page removable");
|
||||
"Unstable writing must remain editable before assembly");
|
||||
ItemStack removed = binder.takePage(5);
|
||||
helper.assertTrue(removed.is(ModItems.PAGE.get()), "Player must be able to correct the duplicate page");
|
||||
helper.assertTrue(binder.canAssemble(), "Corrected partial writing must become assemblable");
|
||||
helper.assertTrue(removed.is(ModItems.PAGE.get()), "Player must be able to remove the duplicate page");
|
||||
helper.assertTrue(binder.canAssemble(), "Corrected partial writing must remain assemblable");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
|
||||
@@ -101,6 +101,11 @@ public final class CoreGameTests {
|
||||
helper.assertFalse(BiomeSymbolPages.symbolId(ResourceLocation.parse("example:orchard"))
|
||||
.equals(BiomeSymbolPages.symbolId(ResourceLocation.parse("another:orchard"))),
|
||||
"Equal biome paths from different mods must not share a page ID");
|
||||
helper.assertFalse(BiomeSymbolPages.isPublicPageBiome(
|
||||
ResourceLocation.parse("terrablender:deferred_placeholder")),
|
||||
"TerraBlender's internal staging biome must not appear as a creative or JEI page");
|
||||
helper.assertTrue(BiomeSymbolPages.isPublicPageBiome(ResourceLocation.parse("biomesoplenty:orchard")),
|
||||
"Real modded biome pages must remain visible");
|
||||
helper.succeed();
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import net.dnorberg.rivencraft.book.AgeGenerationPlan;
|
||||
import net.dnorberg.rivencraft.book.AgeGenerationProfiles;
|
||||
import net.dnorberg.rivencraft.book.AgeTraits;
|
||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||
import net.dnorberg.rivencraft.writing.SymbolCatalog;
|
||||
import net.dnorberg.rivencraft.writing.SymbolDefinition;
|
||||
@@ -84,20 +87,42 @@ public final class WritingGrammarGameTests {
|
||||
WritingGrammar.Result repeatedModifiers = parse(resolver, WritingEntry.linkPanel(),
|
||||
WritingEntry.symbol(periodHalf), WritingEntry.symbol(periodFull), WritingEntry.symbol(celestial));
|
||||
helper.assertTrue(repeatedModifiers.canCreate(), "Repeated period modifiers must remain legal");
|
||||
assertCode(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(terrain),
|
||||
assertPenalty(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(terrain),
|
||||
WritingEntry.symbol(terrain)), WritingGrammar.Code.DUPLICATE);
|
||||
assertCode(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(terrain),
|
||||
WritingEntry.symbol(alternateTerrain)), WritingGrammar.Code.CONFLICT);
|
||||
assertCode(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(weather),
|
||||
WritingGrammar.Result replacedTerrain = parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(terrain),
|
||||
WritingEntry.symbol(alternateTerrain));
|
||||
assertPenalty(helper, replacedTerrain, WritingGrammar.Code.CONFLICT);
|
||||
helper.assertValueEqual(replacedTerrain.activeSymbols().getFirst().reference(), alternateTerrain,
|
||||
"The last supported controller must supply the effective terrain");
|
||||
assertPenalty(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(weather),
|
||||
WritingEntry.symbol(terrain)), WritingGrammar.Code.OUT_OF_ORDER);
|
||||
assertCode(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(celestial),
|
||||
assertPenalty(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(celestial),
|
||||
WritingEntry.symbol(periodHalf)), WritingGrammar.Code.MISPLACED_MODIFIER);
|
||||
assertCode(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);
|
||||
assertCode(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(plains),
|
||||
assertPenalty(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(plains),
|
||||
WritingEntry.symbol(nativeBiomes)), WritingGrammar.Code.CONFLICT);
|
||||
assertCode(helper, parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(plains),
|
||||
WritingEntry.symbol(forest), WritingEntry.symbol(single)), WritingGrammar.Code.CONFLICT);
|
||||
WritingGrammar.Result extraBiomes = parse(resolver, WritingEntry.linkPanel(), WritingEntry.symbol(plains),
|
||||
WritingEntry.symbol(forest), WritingEntry.symbol(single));
|
||||
assertPenalty(helper, extraBiomes, WritingGrammar.Code.CONFLICT);
|
||||
helper.assertValueEqual(extraBiomes.activeSymbols().stream().filter(symbol ->
|
||||
symbol.semantics().role() == SymbolSemantics.Role.BIOME).count(), 1L,
|
||||
"Single-biome distribution must consume only one authored biome");
|
||||
OrderedWriting repaired = writing(WritingEntry.linkPanel(), WritingEntry.symbol(weather),
|
||||
WritingEntry.symbol(terrain), WritingEntry.symbol(ref("rivencraft:terrain/flat")));
|
||||
AgeGenerationPlan plan = AgeGenerationPlan.compile(repaired, 42L, AgeGenerationProfiles.STANDARD,
|
||||
AgeTraits.select(42L), helper.getLevel().registryAccess());
|
||||
helper.assertValueEqual(plan.authoredTerrain(), ResourceLocation.parse("rivencraft:flat"),
|
||||
"The last terrain controller must win without deleting earlier pages");
|
||||
helper.assertValueEqual(plan.writingInstability(), 600,
|
||||
"An extra controller and repaired ordering must be counted once each");
|
||||
helper.assertValueEqual(plan.symbols().size(), 3,
|
||||
"The saved plan must retain all authored pages, including the extra controller");
|
||||
AgeGenerationPlan loaded = AgeGenerationPlan.load(plan.save());
|
||||
helper.assertValueEqual(loaded.writingInstability(), 600,
|
||||
"Writing instability must survive a saved-plan round trip");
|
||||
helper.assertValueEqual(loaded.activeSymbolIndices(), plan.activeSymbolIndices(),
|
||||
"The effective writing order must not reroll on load");
|
||||
WritingGrammar.Result repeatedFeatures = parse(resolver, WritingEntry.linkPanel(),
|
||||
WritingEntry.symbol(caves), WritingEntry.symbol(caves));
|
||||
helper.assertTrue(repeatedFeatures.canCreate(), "Repeated feature pages must remain authorable");
|
||||
@@ -142,4 +167,11 @@ public final class WritingGrammarGameTests {
|
||||
helper.assertTrue(result.diagnostics().stream().anyMatch(diagnostic -> diagnostic.code() == code),
|
||||
"Writing must report " + code + " rather than silently dropping a page");
|
||||
}
|
||||
|
||||
private static void assertPenalty(GameTestHelper helper, WritingGrammar.Result result, WritingGrammar.Code code) {
|
||||
helper.assertTrue(result.canCreate(), "Known writing defect " + code + " must not block Age creation");
|
||||
helper.assertTrue(result.writingInstability() > 0, "Known writing defect must contribute instability");
|
||||
helper.assertTrue(result.diagnostics().stream().anyMatch(diagnostic -> diagnostic.code() == code),
|
||||
"Writing must report " + code + " rather than silently dropping a page");
|
||||
}
|
||||
}
|
||||
@@ -102,6 +102,7 @@ public final class PageItem extends Item {
|
||||
Set<String> bundled = CREATIVE_SYMBOLS.keySet();
|
||||
registries.lookupOrThrow(Registries.BIOME).listElements()
|
||||
.map(holder -> holder.key().location())
|
||||
.filter(BiomeSymbolPages::isPublicPageBiome)
|
||||
.filter(id -> !bundled.contains(BiomeSymbolPages.symbolId(id).getPath()))
|
||||
.sorted()
|
||||
.forEach(id -> pages.add(createCreativeBiomePage(id)));
|
||||
|
||||
@@ -56,15 +56,15 @@ public final class BookBinderMenu extends AbstractContainerMenu {
|
||||
});
|
||||
addDataSlot(new DataSlot() {
|
||||
@Override public int get() {
|
||||
return clientSide || binder == null ? grammarErrorCode : binder.writingGrammar().firstError()
|
||||
.map(error -> error.code().ordinal()).orElse(-1);
|
||||
return clientSide || binder == null ? grammarErrorCode : binderDiagnostic()
|
||||
.map(diagnostic -> diagnostic.code().ordinal()).orElse(-1);
|
||||
}
|
||||
@Override public void set(int value) { grammarErrorCode = value; }
|
||||
});
|
||||
addDataSlot(new DataSlot() {
|
||||
@Override public int get() {
|
||||
return clientSide || binder == null ? grammarErrorStripIndex : binder.writingGrammar().firstError()
|
||||
.map(error -> binder.writingStripIndex(error.index())).orElse(-1);
|
||||
return clientSide || binder == null ? grammarErrorStripIndex : binderDiagnostic()
|
||||
.map(diagnostic -> binder.writingStripIndex(diagnostic.index())).orElse(-1);
|
||||
}
|
||||
@Override public void set(int value) { grammarErrorStripIndex = value; }
|
||||
});
|
||||
@@ -87,11 +87,21 @@ public final class BookBinderMenu extends AbstractContainerMenu {
|
||||
}
|
||||
|
||||
public int selectedPage() { return selectedPage; }
|
||||
private Optional<WritingGrammar.Diagnostic> binderDiagnostic() {
|
||||
WritingGrammar.Result grammar = binder.writingGrammar();
|
||||
return grammar.firstError().or(grammar::firstWarning);
|
||||
}
|
||||
public Optional<WritingGrammar.Code> grammarError() {
|
||||
WritingGrammar.Code[] codes = WritingGrammar.Code.values();
|
||||
return grammarErrorCode >= 0 && grammarErrorCode < codes.length
|
||||
? 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);
|
||||
}
|
||||
public int grammarErrorStripIndex() { return grammarErrorStripIndex; }
|
||||
public String title() { return binder == null ? "" : binder.title(); }
|
||||
public boolean setTitle(Player player, String title) {
|
||||
|
||||
@@ -9,9 +9,16 @@ import net.minecraft.resources.ResourceLocation;
|
||||
/** Registry-derived pages for biomes without hand-authored symbol JSON. */
|
||||
public final class BiomeSymbolPages {
|
||||
private static final ResourceLocation CATEGORY = ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome");
|
||||
private static final ResourceLocation TERRABLENDER_DEFERRED_PLACEHOLDER =
|
||||
ResourceLocation.fromNamespaceAndPath("terrablender", "deferred_placeholder");
|
||||
|
||||
private BiomeSymbolPages() { }
|
||||
|
||||
/** TerraBlender's internal staging biome is registered, but is not a player-authorable biome page. */
|
||||
public static boolean isPublicPageBiome(ResourceLocation biomeId) {
|
||||
return !TERRABLENDER_DEFERRED_PLACEHOLDER.equals(biomeId);
|
||||
}
|
||||
|
||||
public static ResourceLocation symbolId(ResourceLocation biomeId) {
|
||||
// Preserve old vanilla page IDs, while keeping modded namespaces distinct.
|
||||
return ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "biome/"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.dnorberg.rivencraft.writing;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -82,7 +83,7 @@ public final class WritingGrammar {
|
||||
ResourceLocation feature = ((SymbolSemantics.ResourceValue) parsed.semantics().value()).id();
|
||||
if (!MATERIAL_FEATURES.contains(feature.getPath())
|
||||
|| !feature.getNamespace().equals("rivencraft")) {
|
||||
diagnostics.add(new Diagnostic(Code.MISPLACED_MODIFIER, pendingBlock.index(),
|
||||
diagnostics.add(new Diagnostic(Code.UNSUPPORTED_FEATURE, pendingBlock.index(),
|
||||
"This feature cannot consume a material page"));
|
||||
}
|
||||
pendingBlock = null;
|
||||
@@ -94,7 +95,7 @@ public final class WritingGrammar {
|
||||
"This environment effect needs renderer or precipitation support"));
|
||||
}
|
||||
if (singular(role)) {
|
||||
ParsedSymbol previous = singular.putIfAbsent(role, parsed);
|
||||
ParsedSymbol previous = singular.put(role, parsed);
|
||||
if (previous != null) {
|
||||
Code code = previous.semantics().value().equals(parsed.semantics().value())
|
||||
? Code.DUPLICATE : Code.CONFLICT;
|
||||
@@ -183,7 +184,21 @@ public final class WritingGrammar {
|
||||
Objects.requireNonNull(message, "message");
|
||||
}
|
||||
|
||||
public boolean error() { return code != Code.INCOMPLETE; }
|
||||
public boolean error() {
|
||||
return switch (code) {
|
||||
case INCOMPLETE, DUPLICATE, CONFLICT, OUT_OF_ORDER, MISPLACED_MODIFIER -> false;
|
||||
default -> true;
|
||||
};
|
||||
}
|
||||
|
||||
/** The known writing defects have an explicit, provisional M6 score. */
|
||||
public int instability() {
|
||||
return switch (code) {
|
||||
case DUPLICATE, CONFLICT -> 500;
|
||||
case OUT_OF_ORDER, MISPLACED_MODIFIER -> 100;
|
||||
default -> 0;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public record ParsedSymbol(int index, SymbolReference reference, SymbolSemantics semantics) {
|
||||
@@ -202,5 +217,39 @@ public final class WritingGrammar {
|
||||
public boolean randomFallback() { return symbols.isEmpty() && diagnostics.isEmpty(); }
|
||||
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();
|
||||
}
|
||||
public int writingInstability() {
|
||||
return diagnostics.stream().mapToInt(Diagnostic::instability).sum();
|
||||
}
|
||||
|
||||
/** Preserve the book's original entries, but compile a repaired Legacy-style order. */
|
||||
public List<ParsedSymbol> activeSymbols() {
|
||||
EnumMap<Role, ParsedSymbol> lastChoice = new EnumMap<>(Role.class);
|
||||
for (ParsedSymbol symbol : symbols) {
|
||||
if (singular(symbol.semantics().role())) lastChoice.put(symbol.semantics().role(), symbol);
|
||||
}
|
||||
List<ParsedSymbol> active = new ArrayList<>();
|
||||
for (ParsedSymbol symbol : symbols) {
|
||||
if (!singular(symbol.semantics().role())
|
||||
|| lastChoice.get(symbol.semantics().role()) == symbol) active.add(symbol);
|
||||
}
|
||||
active.sort(Comparator.comparingInt(symbol -> stage(symbol.semantics().role())));
|
||||
ParsedSymbol controller = lastChoice.get(Role.BIOME_CONTROLLER);
|
||||
if (controller != null && controller.semantics().value() instanceof ChoiceValue choice) {
|
||||
if (choice.id().equals("native")) {
|
||||
active.removeIf(symbol -> symbol.semantics().role() == Role.BIOME);
|
||||
} else if (choice.id().equals("single")) {
|
||||
boolean retained = false;
|
||||
for (int index = 0; index < active.size(); index++) {
|
||||
if (active.get(index).semantics().role() != Role.BIOME) continue;
|
||||
if (!retained) retained = true;
|
||||
else active.remove(index--);
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.copyOf(active);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,10 +47,10 @@
|
||||
"screen.rivencraft.book_binder.grammar.unsupported": "Page %s: this symbol needs semantic schema v2; update its data pack or remove it",
|
||||
"screen.rivencraft.book_binder.grammar.unsupported_profile": "Page %s: its Age profile is unavailable; restore the data pack or remove this page",
|
||||
"screen.rivencraft.book_binder.grammar.unsupported_environment": "Page %s: this weather or sky effect is not supported yet",
|
||||
"screen.rivencraft.book_binder.grammar.duplicate": "Page %s: remove the duplicate symbol",
|
||||
"screen.rivencraft.book_binder.grammar.conflict": "Page %s: replace the conflicting symbol",
|
||||
"screen.rivencraft.book_binder.grammar.out_of_order": "Page %s: move this symbol before later categories",
|
||||
"screen.rivencraft.book_binder.grammar.misplaced_modifier": "Page %s: move this modifier before its celestial page",
|
||||
"screen.rivencraft.book_binder.grammar.duplicate": "Page %s: duplicate page adds instability; the last choice is used",
|
||||
"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.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