Compare commits

..
3 Commits
Author SHA1 Message Date
dnorberg b8d23ee0c0 Define writing and symbol data model (#17)
Build / build (push) Canceled after 0s
2026-09-23 14:24:40 -04:00
dnorberg f276d412b9 Describe your changes
Build / build (push) Canceled after 0s
2026-09-23 06:30:15 -04:00
dnorberg 954dd7097b Complete Milestone 3 documentation (#12)
Build / build (push) Canceled after 0s
2026-09-23 05:51:59 -04:00
21 changed files with 567 additions and 7 deletions

No files matched your search

+10 -4
View File
@@ -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 and books/linking 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
@@ -60,6 +62,10 @@ according to the configured Return Link policy.
Server owners can set `descriptive_books.return_link_mode` in Rivencraft's
server config to `ON_CREATION` (default), `ALWAYS`, or `DISABLED`.
Age creation, persistence, lifecycle commands, configuration, backups, and
administrator recovery are documented in [docs/AGES.md](docs/AGES.md).
Data-pack runtime profiles are documented in
[docs/AGE_PROFILES.md](docs/AGE_PROFILES.md).
## Requirements
Binary file not shown.

After

Width:  |  Height:  |  Size: 553 KiB

+71 -1
View File
@@ -1,9 +1,35 @@
# Age administration
# Age creation and administration
## Creation
A page-free Descriptive Book creates an Age when the player assigns a name and
presses **Create**. Rivencraft immediately assigns a UUID, derives a unique
Minecraft-safe dimension identifier from the requested name, rolls a seed and
traits, and persists that definition before asking Dynamic Dimensions to create
the runtime world. Names that normalize to the same identifier receive stable
suffixes such as `_2` and `_3`.
The selected terrain profile, biome distribution, lighting, weather, celestial
period, celestial angle, and phase are deterministic and become part of the
saved definition. A five-by-five cobblestone platform with clear headroom is
prepared at a safe arrival point. The completed book and every copy carrying
the same UUID resolve to that definition.
Creating an Age can take several ticks. The book cannot be reopened while its
creation request is active. If creation is interrupted after the definition is
saved, using the book again resumes from the same immutable definition instead
of rolling a replacement.
## Persistence
Rivencraft owns each Age definition for the lifetime of a world. A definition
contains the Age UUID, canonical name, seed, generation version, runtime
dimension identifier, spawn, and deterministic traits. Definitions are stored
in the Overworld saved data and are never rerolled during ordinary loading.
Runtime terrain and entity data remain in the normal world dimension storage
managed by Minecraft and Dynamic Dimensions. Back up the complete world folder
while the server is stopped; copying only a dimension folder does not preserve
the authoritative Age registry.
## Lifecycle
@@ -37,3 +63,47 @@ The commands require permission level 2.
Opening a Descriptive Book for an unloaded Age loads it again from its saved
definition.
## Configuration
Rivencraft writes gameplay settings to the world/server configuration managed
by NeoForge. `descriptive_books.return_link_mode` accepts:
- `ON_CREATION` (default): create one Return Link the first time a player
enters each Age.
- `ALWAYS`: create a Return Link after every successful Descriptive Book trip.
- `DISABLED`: never create Return Links automatically.
Each client has a separate `rivencraft-client.toml`. Its `book_preview.mode`
setting accepts `AUTO`, `IMMERSIVE_PORTALS`, `PONDER`, or `LEGACY`. This changes
only presentation and cannot change server-side Age generation or travel
validation. See [PREVIEW_COMPATIBILITY.md](PREVIEW_COMPATIBILITY.md) for the
selection matrix and integration limits.
Runtime generation profiles are server data-pack resources. Their schema,
override rules, reload behavior, and deterministic fallback are documented in
[AGE_PROFILES.md](AGE_PROFILES.md).
## Administrator recovery
Use this sequence when a book cannot load an existing Age:
1. Stop the server and make a copy of the complete world folder.
2. Start the server and run `/rivencraft age list`. Confirm that the expected
canonical name and dimension identifier remain registered.
3. If the Age is listed as unloaded, open its Descriptive Book to reload it.
This keeps the UUID, seed, profile, traits, and existing world folder.
4. If a data-pack profile is missing or invalid, restore or repair that pack,
run `/reload`, then unload and reopen the Age. A removed custom profile uses
the deterministic `rivencraft:standard` fallback and logs the substitution.
5. If the definition is missing, restore the complete world backup. Moving a
dimension folder into another world cannot reconstruct its UUID or books.
6. If the definition exists but its dimension data is damaged, restore the
matching dimension data and authoritative world saved data from the same
backup generation. Do not combine them from different backups.
There is intentionally no delete command in Milestone 3. Do not manually
delete an Age definition or rename its dimension folder: books may exist in
inventories, containers, Book Stands, or unloaded chunks, and would be
orphaned. `/rivencraft age unload` is the supported reversible maintenance
operation.
+7 -1
View File
@@ -31,9 +31,10 @@ acceptance issue and all required implementation issues are complete.
**Complete.**
3. **Ages** — persistent generated dimensions, deterministic traits,
lifecycle management, data-pack integration, and optional destination
renderer compatibility. **Active.** See
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.
+46
View File
@@ -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/<namespace>/rivencraft/symbols/<path>.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.
+1 -1
View File
@@ -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.2.30.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.
@@ -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");
}
}
@@ -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));
}
}
@@ -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<WritingEntry> entries) {
public static final int MAX_ENTRIES = 256;
public static final OrderedWriting EMPTY = new OrderedWriting(List.of());
public static final Codec<OrderedWriting> 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<ByteBuf, OrderedWriting> 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<WritingEntry> entries = new ArrayList<>(size);
for (int index = 0; index < size; index++) {
entries.add(WritingEntry.STREAM_CODEC.decode(buffer));
}
return new OrderedWriting(entries);
}
}
@@ -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/&lt;namespace&gt;/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<ResourceLocation, SymbolDefinition> symbols = Map.of();
private SymbolCatalog() {
}
public static void register(AddReloadListenerEvent event) {
event.addListener(new Loader());
}
public static Optional<SymbolDefinition> 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<ResourceLocation, SymbolDefinition> symbols() {
return symbols;
}
private static final class Loader extends SimpleJsonResourceReloadListener {
private Loader() {
super(GSON, DIRECTORY);
}
@Override
protected void apply(Map<ResourceLocation, JsonElement> resources,
ResourceManager resourceManager,
ProfilerFiller profiler) {
Map<ResourceLocation, SymbolDefinition> 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());
}
}
}
@@ -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<String> 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<SymbolDefinition> CODEC = RecordCodecBuilder.<SymbolDefinition>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<SymbolDefinition> validate(SymbolDefinition definition) {
return definition.schemaVersion == CURRENT_SCHEMA_VERSION
? DataResult.success(definition)
: DataResult.error(() -> "Unsupported symbol schema_version " + definition.schemaVersion);
}
}
@@ -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<SymbolDisplayMetadata> 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<ByteBuf, SymbolDisplayMetadata> 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
);
}
}
@@ -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<SymbolReference> CODEC = ResourceLocation.CODEC.xmap(
SymbolReference::new, SymbolReference::id);
public static final StreamCodec<ByteBuf, SymbolReference> 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();
}
}
@@ -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<SymbolReference> symbol) {
private static final Codec<Kind> 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<WritingEntry> 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<WritingEntry> CODEC = RECORD_CODEC.flatXmap(
WritingEntry::validate, WritingEntry::validate);
public static final StreamCodec<ByteBuf, WritingEntry> 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<WritingEntry> 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
}
}
@@ -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",
@@ -0,0 +1,6 @@
{
"schema_version": 1,
"name": "symbol.rivencraft.biome.native",
"description": "symbol.rivencraft.biome.native.description",
"category": "rivencraft:biome"
}
@@ -0,0 +1,6 @@
{
"schema_version": 1,
"name": "symbol.rivencraft.celestial.normal_time",
"description": "symbol.rivencraft.celestial.normal_time.description",
"category": "rivencraft:celestial"
}
@@ -0,0 +1,6 @@
{
"schema_version": 1,
"name": "symbol.rivencraft.feature.caves",
"description": "symbol.rivencraft.feature.caves.description",
"category": "rivencraft:feature"
}
@@ -0,0 +1,6 @@
{
"schema_version": 1,
"name": "symbol.rivencraft.lighting.normal",
"description": "symbol.rivencraft.lighting.normal.description",
"category": "rivencraft:lighting"
}
@@ -0,0 +1,6 @@
{
"schema_version": 1,
"name": "symbol.rivencraft.terrain.standard",
"description": "symbol.rivencraft.terrain.standard.description",
"category": "rivencraft:terrain"
}
@@ -0,0 +1,6 @@
{
"schema_version": 1,
"name": "symbol.rivencraft.weather.normal",
"description": "symbol.rivencraft.weather.normal.description",
"category": "rivencraft:weather"
}