From b8d23ee0c05cbe794429fb170b137f3b515656af Mon Sep 17 00:00:00 2001 From: Dustin Norberg Date: Wed, 23 Sep 2026 14:24:40 -0400 Subject: [PATCH] Define writing and symbol data model (#17) --- README.md | 10 ++- docs/PORTING.md | 6 ++ docs/WRITING_DATA.md | 46 +++++++++++ gradle.properties | 2 +- .../net/dnorberg/rivencraft/Rivencraft.java | 2 + .../gametest/WritingDataGameTests.java | 69 ++++++++++++++++ .../rivencraft/writing/OrderedWriting.java | 47 +++++++++++ .../rivencraft/writing/SymbolCatalog.java | 67 +++++++++++++++ .../rivencraft/writing/SymbolDefinition.java | 47 +++++++++++ .../writing/SymbolDisplayMetadata.java | 44 ++++++++++ .../rivencraft/writing/SymbolReference.java | 24 ++++++ .../rivencraft/writing/WritingEntry.java | 82 +++++++++++++++++++ .../assets/rivencraft/lang/en_us.json | 14 ++++ .../rivencraft/symbols/biome/native.json | 6 ++ .../symbols/celestial/normal_time.json | 6 ++ .../rivencraft/symbols/feature/caves.json | 6 ++ .../rivencraft/symbols/lighting/normal.json | 6 ++ .../rivencraft/symbols/terrain/standard.json | 6 ++ .../rivencraft/symbols/weather/normal.json | 6 ++ 19 files changed, 491 insertions(+), 5 deletions(-) create mode 100644 docs/WRITING_DATA.md create mode 100644 src/main/java/net/dnorberg/rivencraft/gametest/WritingDataGameTests.java create mode 100644 src/main/java/net/dnorberg/rivencraft/writing/OrderedWriting.java create mode 100644 src/main/java/net/dnorberg/rivencraft/writing/SymbolCatalog.java create mode 100644 src/main/java/net/dnorberg/rivencraft/writing/SymbolDefinition.java create mode 100644 src/main/java/net/dnorberg/rivencraft/writing/SymbolDisplayMetadata.java create mode 100644 src/main/java/net/dnorberg/rivencraft/writing/SymbolReference.java create mode 100644 src/main/java/net/dnorberg/rivencraft/writing/WritingEntry.java create mode 100644 src/main/resources/data/rivencraft/rivencraft/symbols/biome/native.json create mode 100644 src/main/resources/data/rivencraft/rivencraft/symbols/celestial/normal_time.json create mode 100644 src/main/resources/data/rivencraft/rivencraft/symbols/feature/caves.json create mode 100644 src/main/resources/data/rivencraft/rivencraft/symbols/lighting/normal.json create mode 100644 src/main/resources/data/rivencraft/rivencraft/symbols/terrain/standard.json create mode 100644 src/main/resources/data/rivencraft/rivencraft/symbols/weather/normal.json diff --git a/README.md b/README.md index 0dac780..f0e2bde 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,12 @@ Rivencraft is a clean, incremental port of the ideas and gameplay of [Mystcraft Legacy](https://github.com/Mystcraft/Mystcraft-Legacy) to Minecraft 1.21.1 on NeoForge. -The bootstrap, books/linking, and Ages milestones are complete. The project -currently provides functional Linking Books and randomly seeded, persistent -vanilla-like Ages created from page-free Descriptive Books. Symbols, -instability, and the full writing mechanics have not been ported yet. +The bootstrap, books/linking, and Ages milestones are complete. Milestone 4 is +building the writing system on a data-pack symbol catalog and persistent +ordered-writing model. The project currently provides functional Linking Books +and randomly seeded, persistent vanilla-like Ages created from page-free +Descriptive Books. Instability and full symbol-to-world grammar interpretation +have not been ported yet. ## Linking Book diff --git a/docs/PORTING.md b/docs/PORTING.md index ecd8557..4a36824 100644 --- a/docs/PORTING.md +++ b/docs/PORTING.md @@ -34,6 +34,7 @@ acceptance issue and all required implementation issues are complete. renderer compatibility. **Complete.** See [Milestone 3](https://gitea.tail180fb1.ts.net/dnorberg/rivencraft/milestone/8). 4. **Writing system** — pages, symbols, notebooks, desks, and book assembly. + **In progress.** 5. **Age grammar** — translate symbol combinations into modern biome, noise, dimension-type, and world-generation configuration. Optional integrations may add composable biome regions and seasonal behavior. @@ -61,6 +62,11 @@ The writing system and Age grammar will build on the existing page-free Age path. Page-free books remain a useful deterministic fallback and test fixture after written symbol combinations become available. +Milestone 4 begins with a versioned, data-pack-defined symbol catalog and an +immutable ordered-writing model. See [WRITING_DATA.md](WRITING_DATA.md). These +contracts preserve unknown symbol identifiers and store author intent; they do +not interpret symbols into Age generation, which remains Milestone 5 work. + ## Provenance rules - New code uses the `net.dnorberg.rivencraft` package. diff --git a/docs/WRITING_DATA.md b/docs/WRITING_DATA.md new file mode 100644 index 0000000..2297d9c --- /dev/null +++ b/docs/WRITING_DATA.md @@ -0,0 +1,46 @@ +# Writing and symbol data + +Milestone 4 stores what a player wrote without deciding what that writing does +to an Age. Milestone 5 owns grammar validation, symbol interaction, instability, +and translation into biome, noise, dimension-type, and other world-generation +configuration. + +## Symbol catalog + +The authoritative server catalog loads JSON resources from +`data//rivencraft/symbols/.json`. The resource ID is the +symbol ID. For example, `data/rivencraft/rivencraft/symbols/terrain/standard.json` +defines `rivencraft:terrain/standard`. + +Schema version 1 contains display metadata only: + +```json +{ + "schema_version": 1, + "name": "symbol.rivencraft.terrain.standard", + "description": "symbol.rivencraft.terrain.standard.description", + "category": "rivencraft:terrain" +} +``` + +`name` and `description` are translation keys, not arbitrary formatted text or +executable behavior. `category` is a namespaced organizational identifier. +Invalid definitions are logged and excluded by the server. Menus and future +page synchronization must obtain resolved display metadata from the server; +clients never make a symbol valid by naming it. + +Rivencraft ships a small catalog covering terrain, biomes, lighting, weather, +celestial behavior, and features. It exists to exercise the complete writing +workflow before Milestone 5 implements the full legacy grammar. + +## Persistent writing + +`SymbolReference` stores only a namespaced symbol ID. `WritingEntry` is either +a symbol reference or a baseline link-panel marker. `OrderedWriting` retains up +to 256 entries in exact player-authored order. All three contracts have both +persistent codecs and bounded network codecs. + +Removing a data pack never removes its IDs from saved writing. A missing ID is +resolved as client-safe **Unresolved Symbol** display metadata while retaining +the original namespaced ID for diagnostics and later restoration. This is +deliberately different from treating an unknown symbol as valid grammar. diff --git a/gradle.properties b/gradle.properties index d5504ed..40104cb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -35,7 +35,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.3.0.0 +mod_version=0.3.1.0 immersive_portals_version=6.0.7 # The group ID for the mod. It is only important when publishing as an artifact to a Maven repository. # This should match the base package used for the mod sources. diff --git a/src/main/java/net/dnorberg/rivencraft/Rivencraft.java b/src/main/java/net/dnorberg/rivencraft/Rivencraft.java index 2363172..49e3d77 100644 --- a/src/main/java/net/dnorberg/rivencraft/Rivencraft.java +++ b/src/main/java/net/dnorberg/rivencraft/Rivencraft.java @@ -18,6 +18,7 @@ import net.dnorberg.rivencraft.config.RivencraftClientConfig; import net.dnorberg.rivencraft.book.AgeDimensionService; import net.dnorberg.rivencraft.book.AgeWeatherService; import net.dnorberg.rivencraft.book.AgeRuntimeProfiles; +import net.dnorberg.rivencraft.writing.SymbolCatalog; import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.ModContainer; import net.neoforged.fml.common.Mod; @@ -50,6 +51,7 @@ public final class Rivencraft { NeoForge.EVENT_BUS.addListener(AgeLifecycleEvents::onServerStopping); NeoForge.EVENT_BUS.addListener(RivencraftCommands::register); NeoForge.EVENT_BUS.addListener(AgeRuntimeProfiles::register); + NeoForge.EVENT_BUS.addListener(SymbolCatalog::register); LOGGER.info("Initializing Rivencraft"); } } diff --git a/src/main/java/net/dnorberg/rivencraft/gametest/WritingDataGameTests.java b/src/main/java/net/dnorberg/rivencraft/gametest/WritingDataGameTests.java new file mode 100644 index 0000000..80b269f --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/gametest/WritingDataGameTests.java @@ -0,0 +1,69 @@ +package net.dnorberg.rivencraft.gametest; + +import com.mojang.serialization.JsonOps; +import io.netty.buffer.Unpooled; +import java.util.List; +import net.dnorberg.rivencraft.Rivencraft; +import net.dnorberg.rivencraft.writing.OrderedWriting; +import net.dnorberg.rivencraft.writing.SymbolCatalog; +import net.dnorberg.rivencraft.writing.SymbolReference; +import net.dnorberg.rivencraft.writing.WritingEntry; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestHelper; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.neoforge.gametest.GameTestHolder; +import net.neoforged.neoforge.gametest.PrefixGameTestTemplate; + +/** Acceptance coverage for Milestone 4's persistent writing contracts. */ +@GameTestHolder(Rivencraft.MOD_ID) +@PrefixGameTestTemplate(false) +public final class WritingDataGameTests { + private WritingDataGameTests() { + } + + @GameTest(template = "empty") + public static void orderedWritingPreservesKnownUnknownAndLinkPanelEntries(GameTestHelper helper) { + SymbolReference known = reference("rivencraft:terrain/standard"); + SymbolReference unknown = reference("example:removed_symbol"); + OrderedWriting original = new OrderedWriting(List.of( + WritingEntry.symbol(known), + WritingEntry.symbol(unknown), + WritingEntry.linkPanel() + )); + + var json = OrderedWriting.CODEC.encodeStart(JsonOps.INSTANCE, original).result().orElseThrow(); + OrderedWriting persistentCopy = OrderedWriting.CODEC.parse(JsonOps.INSTANCE, json).result().orElseThrow(); + helper.assertValueEqual(persistentCopy, original, + "Persistent writing codec must retain entry order and unknown symbol IDs"); + + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); + OrderedWriting.STREAM_CODEC.encode(buffer, original); + OrderedWriting networkCopy = OrderedWriting.STREAM_CODEC.decode(buffer); + helper.assertValueEqual(networkCopy, original, + "Network writing codec must retain entry order and unknown symbol IDs"); + helper.succeed(); + } + + @GameTest(template = "empty") + public static void serverCatalogResolvesBuiltinsAndKeepsUnknownReferencesReadable(GameTestHelper helper) { + SymbolReference known = reference("rivencraft:terrain/standard"); + SymbolReference unknown = reference("example:removed_symbol"); + helper.assertTrue(SymbolCatalog.symbols().size() >= 6, + "The built-in catalog must exercise every Milestone 4 writing workflow"); + helper.assertTrue(SymbolCatalog.isKnown(known), + "The built-in Standard Terrain symbol must be server-authoritative"); + helper.assertFalse(SymbolCatalog.isKnown(unknown), + "An absent data-pack symbol must remain unresolved"); + var unresolved = SymbolCatalog.display(unknown); + helper.assertFalse(unresolved.resolved(), + "Unknown symbol display metadata must be marked unresolved"); + helper.assertValueEqual(unresolved.id(), unknown.id(), + "Unresolved display metadata must preserve the original namespaced ID"); + helper.succeed(); + } + + private static SymbolReference reference(String id) { + return new SymbolReference(ResourceLocation.parse(id)); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/writing/OrderedWriting.java b/src/main/java/net/dnorberg/rivencraft/writing/OrderedWriting.java new file mode 100644 index 0000000..bec36ff --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/writing/OrderedWriting.java @@ -0,0 +1,47 @@ +package net.dnorberg.rivencraft.writing; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import io.netty.buffer.ByteBuf; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; + +/** Immutable ordered writing shared by pages, notebooks, desks, and books. */ +public record OrderedWriting(List entries) { + public static final int MAX_ENTRIES = 256; + public static final OrderedWriting EMPTY = new OrderedWriting(List.of()); + public static final Codec CODEC = WritingEntry.CODEC.listOf() + .validate(entries -> entries.size() <= MAX_ENTRIES + ? DataResult.success(entries) + : DataResult.error(() -> "Writing exceeds " + MAX_ENTRIES + " entries")) + .xmap(OrderedWriting::new, OrderedWriting::entries); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + OrderedWriting::encode, OrderedWriting::decode); + + public OrderedWriting { + entries = List.copyOf(Objects.requireNonNull(entries, "entries")); + if (entries.size() > MAX_ENTRIES) { + throw new IllegalArgumentException("Writing exceeds " + MAX_ENTRIES + " entries"); + } + } + + private static void encode(ByteBuf buffer, OrderedWriting writing) { + ByteBufCodecs.VAR_INT.encode(buffer, writing.entries.size()); + writing.entries.forEach(entry -> WritingEntry.STREAM_CODEC.encode(buffer, entry)); + } + + private static OrderedWriting decode(ByteBuf buffer) { + int size = ByteBufCodecs.VAR_INT.decode(buffer); + if (size < 0 || size > MAX_ENTRIES) { + throw new IllegalArgumentException("Invalid writing entry count " + size); + } + List entries = new ArrayList<>(size); + for (int index = 0; index < size; index++) { + entries.add(WritingEntry.STREAM_CODEC.decode(buffer)); + } + return new OrderedWriting(entries); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/writing/SymbolCatalog.java b/src/main/java/net/dnorberg/rivencraft/writing/SymbolCatalog.java new file mode 100644 index 0000000..780b92d --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/writing/SymbolCatalog.java @@ -0,0 +1,67 @@ +package net.dnorberg.rivencraft.writing; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.mojang.serialization.JsonOps; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import net.dnorberg.rivencraft.Rivencraft; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.resources.ResourceManager; +import net.minecraft.server.packs.resources.SimpleJsonResourceReloadListener; +import net.minecraft.util.profiling.ProfilerFiller; +import net.neoforged.neoforge.event.AddReloadListenerEvent; + +/** Authoritative server catalog loaded from data/<namespace>/rivencraft/symbols. */ +public final class SymbolCatalog { + private static final Gson GSON = new GsonBuilder().create(); + private static final String DIRECTORY = "rivencraft/symbols"; + private static volatile Map symbols = Map.of(); + + private SymbolCatalog() { + } + + public static void register(AddReloadListenerEvent event) { + event.addListener(new Loader()); + } + + public static Optional find(SymbolReference reference) { + return Optional.ofNullable(symbols.get(reference.id())); + } + + public static boolean isKnown(SymbolReference reference) { + return symbols.containsKey(reference.id()); + } + + public static SymbolDisplayMetadata display(SymbolReference reference) { + SymbolDefinition definition = symbols.get(reference.id()); + return definition == null + ? SymbolDisplayMetadata.unresolved(reference) + : definition.display(reference.id()); + } + + public static Map symbols() { + return symbols; + } + + private static final class Loader extends SimpleJsonResourceReloadListener { + private Loader() { + super(GSON, DIRECTORY); + } + + @Override + protected void apply(Map resources, + ResourceManager resourceManager, + ProfilerFiller profiler) { + Map loaded = new HashMap<>(); + resources.forEach((id, json) -> SymbolDefinition.CODEC.parse(JsonOps.INSTANCE, json) + .resultOrPartial(error -> Rivencraft.LOGGER.error( + "Ignoring invalid writing symbol {}: {}", id, error)) + .ifPresent(definition -> loaded.put(id, definition))); + symbols = Map.copyOf(loaded); + Rivencraft.LOGGER.info("Loaded {} writing symbols", loaded.size()); + } + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/writing/SymbolDefinition.java b/src/main/java/net/dnorberg/rivencraft/writing/SymbolDefinition.java new file mode 100644 index 0000000..8f25d87 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/writing/SymbolDefinition.java @@ -0,0 +1,47 @@ +package net.dnorberg.rivencraft.writing; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import java.util.Objects; +import java.util.regex.Pattern; +import net.minecraft.resources.ResourceLocation; + +/** Server-loaded, non-executable display metadata for a writing symbol. */ +public record SymbolDefinition( + int schemaVersion, + String nameTranslationKey, + String descriptionTranslationKey, + ResourceLocation category +) { + public static final int CURRENT_SCHEMA_VERSION = 1; + private static final Pattern TRANSLATION_KEY = Pattern.compile("[a-z0-9_.-]+"); + private static final Codec TRANSLATION_KEY_CODEC = Codec.STRING.comapFlatMap( + value -> TRANSLATION_KEY.matcher(value).matches() + ? DataResult.success(value) + : DataResult.error(() -> "Invalid translation key: " + value), + value -> value + ); + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + Codec.INT.fieldOf("schema_version").forGetter(SymbolDefinition::schemaVersion), + TRANSLATION_KEY_CODEC.fieldOf("name").forGetter(SymbolDefinition::nameTranslationKey), + TRANSLATION_KEY_CODEC.fieldOf("description").forGetter(SymbolDefinition::descriptionTranslationKey), + ResourceLocation.CODEC.fieldOf("category").forGetter(SymbolDefinition::category) + ).apply(instance, SymbolDefinition::new)).flatXmap(SymbolDefinition::validate, SymbolDefinition::validate); + + public SymbolDefinition { + Objects.requireNonNull(nameTranslationKey, "nameTranslationKey"); + Objects.requireNonNull(descriptionTranslationKey, "descriptionTranslationKey"); + Objects.requireNonNull(category, "category"); + } + + public SymbolDisplayMetadata display(ResourceLocation id) { + return new SymbolDisplayMetadata(id, nameTranslationKey, descriptionTranslationKey, category, true); + } + + private static DataResult validate(SymbolDefinition definition) { + return definition.schemaVersion == CURRENT_SCHEMA_VERSION + ? DataResult.success(definition) + : DataResult.error(() -> "Unsupported symbol schema_version " + definition.schemaVersion); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/writing/SymbolDisplayMetadata.java b/src/main/java/net/dnorberg/rivencraft/writing/SymbolDisplayMetadata.java new file mode 100644 index 0000000..f462b20 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/writing/SymbolDisplayMetadata.java @@ -0,0 +1,44 @@ +package net.dnorberg.rivencraft.writing; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import io.netty.buffer.ByteBuf; +import net.dnorberg.rivencraft.Rivencraft; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; + +/** Client-safe symbol presentation selected by the authoritative server catalog. */ +public record SymbolDisplayMetadata( + ResourceLocation id, + String nameTranslationKey, + String descriptionTranslationKey, + ResourceLocation category, + boolean resolved +) { + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + ResourceLocation.CODEC.fieldOf("id").forGetter(SymbolDisplayMetadata::id), + Codec.STRING.fieldOf("name").forGetter(SymbolDisplayMetadata::nameTranslationKey), + Codec.STRING.fieldOf("description").forGetter(SymbolDisplayMetadata::descriptionTranslationKey), + ResourceLocation.CODEC.fieldOf("category").forGetter(SymbolDisplayMetadata::category), + Codec.BOOL.fieldOf("resolved").forGetter(SymbolDisplayMetadata::resolved) + ).apply(instance, SymbolDisplayMetadata::new)); + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ResourceLocation.STREAM_CODEC, SymbolDisplayMetadata::id, + ByteBufCodecs.STRING_UTF8, SymbolDisplayMetadata::nameTranslationKey, + ByteBufCodecs.STRING_UTF8, SymbolDisplayMetadata::descriptionTranslationKey, + ResourceLocation.STREAM_CODEC, SymbolDisplayMetadata::category, + ByteBufCodecs.BOOL, SymbolDisplayMetadata::resolved, + SymbolDisplayMetadata::new + ); + + public static SymbolDisplayMetadata unresolved(SymbolReference reference) { + return new SymbolDisplayMetadata( + reference.id(), + "symbol.rivencraft.unresolved", + "symbol.rivencraft.unresolved.description", + ResourceLocation.fromNamespaceAndPath(Rivencraft.MOD_ID, "unresolved"), + false + ); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/writing/SymbolReference.java b/src/main/java/net/dnorberg/rivencraft/writing/SymbolReference.java new file mode 100644 index 0000000..7b12162 --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/writing/SymbolReference.java @@ -0,0 +1,24 @@ +package net.dnorberg.rivencraft.writing; + +import com.mojang.serialization.Codec; +import io.netty.buffer.ByteBuf; +import java.util.Objects; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; + +/** Stable namespaced identity for one writing symbol. */ +public record SymbolReference(ResourceLocation id) { + public static final Codec CODEC = ResourceLocation.CODEC.xmap( + SymbolReference::new, SymbolReference::id); + public static final StreamCodec STREAM_CODEC = ResourceLocation.STREAM_CODEC.map( + SymbolReference::new, SymbolReference::id); + + public SymbolReference { + Objects.requireNonNull(id, "id"); + } + + /** Remains useful when the defining data pack has been removed. */ + public String unresolvedName() { + return id.toString(); + } +} diff --git a/src/main/java/net/dnorberg/rivencraft/writing/WritingEntry.java b/src/main/java/net/dnorberg/rivencraft/writing/WritingEntry.java new file mode 100644 index 0000000..ffab98a --- /dev/null +++ b/src/main/java/net/dnorberg/rivencraft/writing/WritingEntry.java @@ -0,0 +1,82 @@ +package net.dnorberg.rivencraft.writing; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.DataResult; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import io.netty.buffer.ByteBuf; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; + +/** One ordered unit of book writing. Interpretation belongs to Milestone 5. */ +public record WritingEntry(Kind kind, Optional symbol) { + private static final Codec KIND_CODEC = Codec.STRING.comapFlatMap( + value -> { + try { + return DataResult.success(Kind.valueOf(value.toUpperCase(Locale.ROOT))); + } catch (IllegalArgumentException exception) { + return DataResult.error(() -> "Unknown writing entry kind: " + value); + } + }, + value -> value.name().toLowerCase(Locale.ROOT) + ); + private static final Codec RECORD_CODEC = RecordCodecBuilder.create(instance -> instance.group( + KIND_CODEC.fieldOf("kind").forGetter(WritingEntry::kind), + SymbolReference.CODEC.optionalFieldOf("symbol").forGetter(WritingEntry::symbol) + ).apply(instance, WritingEntry::new)); + public static final Codec CODEC = RECORD_CODEC.flatXmap( + WritingEntry::validate, WritingEntry::validate); + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + WritingEntry::encode, WritingEntry::decode); + + public WritingEntry { + Objects.requireNonNull(kind, "kind"); + symbol = Objects.requireNonNull(symbol, "symbol"); + if (kind == Kind.SYMBOL && symbol.isEmpty()) { + throw new IllegalArgumentException("Symbol writing entries require a symbol reference"); + } + if (kind == Kind.LINK_PANEL && symbol.isPresent()) { + throw new IllegalArgumentException("Link-panel writing entries cannot contain a symbol reference"); + } + } + + public static WritingEntry symbol(SymbolReference symbol) { + return new WritingEntry(Kind.SYMBOL, Optional.of(symbol)); + } + + public static WritingEntry linkPanel() { + return new WritingEntry(Kind.LINK_PANEL, Optional.empty()); + } + + private static DataResult validate(WritingEntry entry) { + return entry.kind == Kind.SYMBOL && entry.symbol.isEmpty() + || entry.kind == Kind.LINK_PANEL && entry.symbol.isPresent() + ? DataResult.error(() -> "Writing entry contents do not match kind " + entry.kind) + : DataResult.success(entry); + } + + private static void encode(ByteBuf buffer, WritingEntry entry) { + ByteBufCodecs.VAR_INT.encode(buffer, entry.kind.ordinal()); + if (entry.kind == Kind.SYMBOL) { + SymbolReference.STREAM_CODEC.encode(buffer, entry.symbol.orElseThrow()); + } + } + + private static WritingEntry decode(ByteBuf buffer) { + int ordinal = ByteBufCodecs.VAR_INT.decode(buffer); + if (ordinal < 0 || ordinal >= Kind.values().length) { + throw new IllegalArgumentException("Unknown writing entry kind ordinal " + ordinal); + } + Kind kind = Kind.values()[ordinal]; + return kind == Kind.SYMBOL + ? symbol(SymbolReference.STREAM_CODEC.decode(buffer)) + : linkPanel(); + } + + public enum Kind { + SYMBOL, + LINK_PANEL + } +} diff --git a/src/main/resources/assets/rivencraft/lang/en_us.json b/src/main/resources/assets/rivencraft/lang/en_us.json index fa79589..ba2e668 100644 --- a/src/main/resources/assets/rivencraft/lang/en_us.json +++ b/src/main/resources/assets/rivencraft/lang/en_us.json @@ -57,6 +57,20 @@ "trait.rivencraft.weather.snow": "Snow", "trait.rivencraft.weather.storm": "Storm", "trait.rivencraft.day_length.stopped": "Stopped", + "symbol.rivencraft.unresolved": "Unresolved Symbol", + "symbol.rivencraft.unresolved.description": "Missing symbol: %s", + "symbol.rivencraft.terrain.standard": "Standard Terrain", + "symbol.rivencraft.terrain.standard.description": "A familiar continental terrain foundation.", + "symbol.rivencraft.biome.native": "Native Biomes", + "symbol.rivencraft.biome.native.description": "Use the terrain profile's native biome climate.", + "symbol.rivencraft.lighting.normal": "Normal Lighting", + "symbol.rivencraft.lighting.normal.description": "Use normal skylight and ambient brightness.", + "symbol.rivencraft.weather.normal": "Normal Weather", + "symbol.rivencraft.weather.normal.description": "Use the normal weather cycle.", + "symbol.rivencraft.celestial.normal_time": "Normal Time", + "symbol.rivencraft.celestial.normal_time.description": "Use a normal-length celestial cycle.", + "symbol.rivencraft.feature.caves": "Caves", + "symbol.rivencraft.feature.caves.description": "Describe underground cave features.", "generator.rivencraft.standard": "Standard", "generator.rivencraft.amplified": "Amplified", "generator.rivencraft.island": "Island", diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/biome/native.json b/src/main/resources/data/rivencraft/rivencraft/symbols/biome/native.json new file mode 100644 index 0000000..6c24b36 --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/biome/native.json @@ -0,0 +1,6 @@ +{ + "schema_version": 1, + "name": "symbol.rivencraft.biome.native", + "description": "symbol.rivencraft.biome.native.description", + "category": "rivencraft:biome" +} diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/celestial/normal_time.json b/src/main/resources/data/rivencraft/rivencraft/symbols/celestial/normal_time.json new file mode 100644 index 0000000..afbaa29 --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/celestial/normal_time.json @@ -0,0 +1,6 @@ +{ + "schema_version": 1, + "name": "symbol.rivencraft.celestial.normal_time", + "description": "symbol.rivencraft.celestial.normal_time.description", + "category": "rivencraft:celestial" +} diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/feature/caves.json b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/caves.json new file mode 100644 index 0000000..92f5750 --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/feature/caves.json @@ -0,0 +1,6 @@ +{ + "schema_version": 1, + "name": "symbol.rivencraft.feature.caves", + "description": "symbol.rivencraft.feature.caves.description", + "category": "rivencraft:feature" +} diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/lighting/normal.json b/src/main/resources/data/rivencraft/rivencraft/symbols/lighting/normal.json new file mode 100644 index 0000000..0dd7d83 --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/lighting/normal.json @@ -0,0 +1,6 @@ +{ + "schema_version": 1, + "name": "symbol.rivencraft.lighting.normal", + "description": "symbol.rivencraft.lighting.normal.description", + "category": "rivencraft:lighting" +} diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/terrain/standard.json b/src/main/resources/data/rivencraft/rivencraft/symbols/terrain/standard.json new file mode 100644 index 0000000..6af3144 --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/terrain/standard.json @@ -0,0 +1,6 @@ +{ + "schema_version": 1, + "name": "symbol.rivencraft.terrain.standard", + "description": "symbol.rivencraft.terrain.standard.description", + "category": "rivencraft:terrain" +} diff --git a/src/main/resources/data/rivencraft/rivencraft/symbols/weather/normal.json b/src/main/resources/data/rivencraft/rivencraft/symbols/weather/normal.json new file mode 100644 index 0000000..7e7a503 --- /dev/null +++ b/src/main/resources/data/rivencraft/rivencraft/symbols/weather/normal.json @@ -0,0 +1,6 @@ +{ + "schema_version": 1, + "name": "symbol.rivencraft.weather.normal", + "description": "symbol.rivencraft.weather.normal.description", + "category": "rivencraft:weather" +}