Public Access
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c900634c9 | ||
|
|
8cce41fd71 |
No files matched your search
+4
-4
@@ -1,15 +1,15 @@
|
|||||||
# GameTest suites
|
# GameTest suites
|
||||||
|
|
||||||
The default dedicated-server run is a small smoke suite: 16 Rivencraft tests
|
The default dedicated-server run is a small smoke suite: 17 Rivencraft tests
|
||||||
cover core linking, deterministic Age definition, writing persistence, legacy
|
cover core linking, deterministic Age definition, writing persistence, legacy
|
||||||
data migration, workstation transactions, and forged/replayed menu actions.
|
data migration, workstation transactions, and forged/replayed menu actions.
|
||||||
NeoForge currently adds two of its own tests, so the runner reports 18.
|
NeoForge currently adds two of its own tests, so the runner reports 19.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
./gradlew runGameTestServer
|
./gradlew runGameTestServer
|
||||||
```
|
```
|
||||||
|
|
||||||
The remaining 46 Rivencraft tests are archived, not deleted. They cover
|
The remaining 47 Rivencraft tests are archived, not deleted. They cover
|
||||||
completed milestone details such as every Age terrain profile, Notebook and
|
completed milestone details such as every Age terrain profile, Notebook and
|
||||||
Desk surface interactions, refill edge cases, book assembly controls, and
|
Desk surface interactions, refill edge cases, book assembly controls, and
|
||||||
workstation break/drop behavior. Run only the relevant archive alongside the
|
workstation break/drop behavior. Run only the relevant archive alongside the
|
||||||
@@ -21,7 +21,7 @@ core tests when investigating an issue:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Available suite names are `linking`, `writing`, `desk`, `binder`, `mixer`, and
|
Available suite names are `linking`, `writing`, `desk`, `binder`, `mixer`, and
|
||||||
`recipe`. Run all 62 Rivencraft tests (64 including NeoForge's two) before a
|
`recipe`. Run all 64 Rivencraft tests (66 including NeoForge's two) before a
|
||||||
release or after broad cross-system changes:
|
release or after broad cross-system changes:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Mystcraft Legacy Age grammar audit (milestone 5, issue #28)
|
||||||
|
|
||||||
|
Reference: [Mystcraft-Legacy commit `9bc8ddc`](https://github.com/Mystcraft/Mystcraft-Legacy/tree/9bc8ddc061845df0cd5ea47f4fdef773cff786ae), whose [build properties](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/build.properties) identify version 0.13.7.06. The user-provided local Minecraft 1.12.2 extraction (`mystcraft-1.12.2-0.13.7.06`) has the same version in `mcmod.info`; its `assets/mystcraft/textures/symbolcomponents.png` is byte-identical to the source checkout. That extraction contains compiled classes and assets, but no shipped `.cfg` or `.java` files. Source-linked behavioral claims below use the corresponding GitHub Java source. This is a behavior specification, not a code port. Any later adapted code needs its own copyright/provenance record under [PORTING.md](PORTING.md).
|
||||||
|
|
||||||
|
## What Legacy actually does
|
||||||
|
|
||||||
|
Legacy's [root rule](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/GrammarRules.java#L62) produces, in order: terrain, biome controller, weather, lighting, optional spawning, suns, moons, starfields, doodads, visuals, small/medium/large features, and effects. Each symbol declares the rule it can satisfy and any preceding modifier tokens it needs in [SymbolRules](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/SymbolRules.java). The written page order therefore matters: modifiers appear before their consumer, while missing grammar branches can be generated. The [grammar tree](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/grammar/GrammarTree.java#L158-L222) connects authored terminals and inserts missing branches; [AgeData](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/world/agedata/AgeData.java#L230-L258) does this on first link with an Age-seeded RNG. Weighted rule *ranks* choose generated symbols; a `null` rank is not randomly generated, though such a rule can connect authored pages ([GrammarGenerator](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/grammar/GrammarGenerator.java#L28-L112)).
|
||||||
|
|
||||||
|
Legacy does **not** reject a book just because it omits terrain, biome, weather, or lighting. It fills the gaps. It also has separate instability accounting for dangling/excess symbols; this audit records those as diagnostics, not as milestone 5 generation effects ([AgeController](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/world/AgeController.java#L196-L247)).
|
||||||
|
|
||||||
|
## Symbol-to-Rivencraft mapping
|
||||||
|
|
||||||
|
| Legacy category and symbols | Existing Rivencraft path | Milestone 5 disposition |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `TerrainNormal`, `TerrainAmplified`, `TerrainFlat`, `TerrainVoid`, `TerrainNether`, `TerrainEnd`; terrain and sea block modifiers | `AgeProfile` selects noise/flat/void and registered noise settings; random profiles already persist per Age | Compile supported terrain shapes into profile/resources. Nether/End and material modifiers require explicit compatible data-pack resources; do not claim exact 1.12 terrain parity. |
|
||||||
|
| `BioConNative`, `Single`, `Tiled`, `Grid`, `Tiny`, `Small`, `Medium`, `Large`, `Huge`, plus biome pages | `AgeTraits.BiomeDistribution` has all nine modes; `AgeDimensionService` has sources for these modes | Map modes, consume authored biome IDs in order, then deterministically fill required minimums. Native uses the profile's modern biome preset, not the old `BiomeProvider`. |
|
||||||
|
| `LightingNormal`, `Dark`, `Bright` | `AgeTraits.Lighting`, dimension type construction | Compile the choice to a stable dimension type/light behavior. Exact Legacy 1.12 brightness-table rendering is not guaranteed by NeoForge's dimension-type fields. |
|
||||||
|
| `WeatherNorm`, `Fast`, `Slow`, `Off`, `On`, `Cloudy`, `Rain`, `Snow`, `Storm` | `AgeTraits.Weather` has matching names except Legacy `On` maps to `ALWAYS` | Compile weather semantics separately from biome selection. `Snow` must remain deferred until optional season integration; do not silently substitute another weather mode. |
|
||||||
|
| Suns, moons, starfields; angle, phase, period, color/gradient/sunset modifiers | One `celestialPeriod`, `celestialAngle`, `celestialPhase` on `AgeTraits` | Compile only the representable single-cycle subset first. Multiple celestial bodies, independent periods, custom stars, gradients and sunsets need a richer definition and renderer, not flattening into one value. |
|
||||||
|
| Sky/fog/cloud/grass/foliage/water colors, natural variants, horizon, rainbow | Existing sky/dimension visuals and preview | Require an explicit modern client/biome-color design; metadata-only symbol pages must not pretend to change rendering. |
|
||||||
|
| Caves, ores, huge trees, lakes, floating islands, tendrils, skylands, spheres, spikes, crystals; vanilla villages, strongholds, mineshafts, dungeons, fortresses, obelisks, fissures | Current cave profile and vanilla data-pack generation | Compile only features for which registered 1.21.1 placed features/structures exist. Defer bespoke structures and polish to milestone 7. |
|
||||||
|
| `EnvAccel`, explosions, lightning, meteors, scorching; PvP and dummy suppression pages | No equivalent Age-definition effect | Defer environmental hazards/instability effects to milestone 6, PvP rule to a separately scoped server policy; retain authored IDs without fake behavior. |
|
||||||
|
|
||||||
|
The Legacy symbol list and page names come from [ModSymbols](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/ModSymbols.java#L129-L240) and [SymbolRules](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/SymbolRules.java#L19-L137). Rivencraft's [catalog](../src/main/java/net/dnorberg/rivencraft/writing/SymbolCatalog.java) currently holds display metadata only; its built-in set is just standard terrain, native biomes, normal weather, normal lighting, normal time, and caves. [OrderedWriting](../src/main/java/net/dnorberg/rivencraft/writing/OrderedWriting.java) preserves page order, but no compiler yet connects it to [AgeTraits](../src/main/java/net/dnorberg/rivencraft/book/AgeTraits.java) or [Age profiles](AGE_PROFILES.md).
|
||||||
|
|
||||||
|
## Symbol-page artwork: `symbolcomponents.png`
|
||||||
|
|
||||||
|
Legacy does not allocate one image tile to each complete Age symbol. The [default word image source](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/api/java/com/xcompwiz/mystcraft/api/word/DrawableWord.java#L24-L75) is `mystcraft:textures/symbolcomponents.png`, a 512×512 RGBA sheet. It has an 8×8 grid of 64×64 component cells. For zero-based component index `n`, source pixels begin at `x = (n % 8) * 64`, `y = (n / 8) * 64`. [GuiUtils.drawComponent](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/client/gui/GuiUtils.java#L131-L152) uses that mapping for GUI drawing; [PageBuilder.PageSprite.stitchWord](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/client/render/PageBuilder.java#L287-L331) uses it when composing an item sprite. Cell `0` is the missing-word/question-mark fallback. Each component may be tinted and its alpha layered with the other components of a word.
|
||||||
|
|
||||||
|
The complete lookup chain for a unique page is:
|
||||||
|
|
||||||
|
1. [WordData.init](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/api/java/com/xcompwiz/mystcraft/api/word/WordData.java#L96-L166) registers a list of component indices for each named word (for example, `Nature` uses `5, 6, 8, 10, 11, 12, 15, 16, 17, 22`). [DrawableWord](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/api/java/com/xcompwiz/mystcraft/api/word/DrawableWord.java#L26-L75) also permits per-component colors and an alternate image source. Unregistered words get a deterministic component list from their lowercase name hash ([DrawableWordManager](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/words/DrawableWordManager.java#L34-L56)).
|
||||||
|
2. [ModSymbols.registerSymbol](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/ModSymbols.java#L105-L116) assigns four poem words to each Age symbol. For example, `LightingNormal` is `Ethereal, Dynamic, Cycle, Balance` ([registration](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/ModSymbols.java#L184-L186)). The poem, not the symbol ID, selects the component layers.
|
||||||
|
3. [PageBuilder.PageSprite](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/client/render/PageBuilder.java#L232-L279) draws those words above, right, below, and left of the page center, respectively. Their target rectangles on its 160×160 working image are `(48,0)`, `(96,48)`, `(48,96)`, `(0,48)`, each 64×64 ([getPageTarget](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/client/render/PageBuilder.java#L360-L378)). It scales the composed image to a 128×128 item sprite. [GuiUtils.drawSymbol](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/client/gui/GuiUtils.java#L74-L96) renders the same four-word arrangement directly in book/workstation GUIs.
|
||||||
|
|
||||||
|
For Rivencraft, keep **symbol semantics** (`SymbolReference`/catalog ID) separate from **page artwork** (four ordered word names, their component indices/colors, and the shared sheet). Existing built-in page images are static renditions of this process ([asset provenance](ASSET_PROVENANCE.md)); future symbol pages can compose it at build time or in a client-only renderer. Unknown words and missing symbols need distinct fallbacks, and data-pack-only symbols must remain usable without silently borrowing another symbol's image. Do not copy the Legacy renderer wholesale: its texture-atlas and GUI APIs target 1.12.2.
|
||||||
|
|
||||||
|
## Verified numeric defaults and bounds
|
||||||
|
|
||||||
|
These values are from code, **not** a supposed shipped `.cfg`: the inspected Legacy repository contains no default configuration file with numeric min/max fields for age grammar. [MystConfig](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/config/MystConfig.java) supplies typed config helpers, while symbol enablement uses a Boolean `<symbol>.enabled` option ([SymbolManager](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/symbol/SymbolManager.java#L91-L107)). Do not label Rivencraft's present numeric range as a Legacy config range.
|
||||||
|
|
||||||
|
| Parameter | Legacy source value/rule | Implementation consequence |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Period pages | `Zero=0`, `Half=0.5`, `Full=1`, `Double=2` ([ModSymbols](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/ModSymbols.java#L194-L201)); repeated period modifiers average pairwise ([SymbolLength](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/symbol/modifiers/SymbolLength.java#L25-L37)) | Authored basic values have min 0 and max 2; no arbitrary continuous written range is established. A sun without a period uses `0.8 + 0.4 * nextDouble()`—`[0.8, 1.2)`—before conversion to ticks ([SymbolSunNormal](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/symbol/symbols/SymbolSunNormal.java#L47-L73)). Rivencraft's random nonzero `[0.2, 2.0)` is its own existing policy. |
|
||||||
|
| Angle pages | North/East/South/West = `0/90/180/270` degrees ([ModSymbols](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/ModSymbols.java#L187-L190)); repeated values use shortest-arc averaging ([ModifierUtils](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/api/java/com/xcompwiz/mystcraft/api/symbol/ModifierUtils.java#L54-L73)) | Canonical angle is `[0, 360)`; consumers may seed a random angle if omitted. |
|
||||||
|
| Phase pages | Nadir/Rising/Zenith/Setting = `0/90/180/270` degrees ([ModSymbols](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/ModSymbols.java#L198-L201)); the sun divides by 360 ([SymbolSunNormal](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/symbol/symbols/SymbolSunNormal.java#L65-L73)) | Written phase is `[0, 360)` degrees, represented by Rivencraft as `[0, 1)` turns. |
|
||||||
|
| Biome count | Grammar requires one page for `Single`, `Tiled`, `Grid`; `Tiny`–`Huge` have two explicit biome slots plus a biome-list branch ([SymbolRules](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/data/SymbolRules.java#L91-L99)). `Single` fills to 1 ([source](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/symbol/symbols/SymbolBiomeControllerSingle.java#L22-L37)), `Tiled`/`Grid` to 2 ([source](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/symbol/symbols/SymbolBiomeControllerTiled.java#L25-L38)), and `Tiny`–`Huge` to 3 ([source](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/symbol/symbols/SymbolBiomeControllerLarge.java#L61-L84)) | These are **minimums**, not maximums. `Native` uses vanilla's provider and has no authored biome-list requirement ([source](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/symbol/symbols/SymbolBiomeControllerNative.java#L34-L44)). Preserve additional authored biomes where the chosen controller can accept them. |
|
||||||
|
| Weather normal | Rain duration/base `12000/12000`, cooldown/base `168000/12000`; thunder duration/base `12000/3600`, cooldown/base `168000/12000` ticks ([SymbolWeatherNormal](https://github.com/Mystcraft/Mystcraft-Legacy/blob/9bc8ddc061845df0cd5ea47f4fdef773cff786ae/src/main/java/com/xcompwiz/mystcraft/symbol/symbols/SymbolWeatherNormal.java#L25-L36)) | These are source constants, not configurable min/max bounds. Other weather controllers need individual review before parity claims. |
|
||||||
|
|
||||||
|
## Compilation and fallback contract for the next issues
|
||||||
|
|
||||||
|
1. A Link-Panel-only book, a legacy page-free book, and any book with no effective supported Age symbols retain the current seed-derived random Age path. Preserve the saved writing untouched.
|
||||||
|
2. A supported partial sequence is **not** invalid: complete missing required branches deterministically from the Age seed, then compile the result. Authored symbols take precedence over generated fillers. This follows Legacy's grammar tree, rather than requiring a full hand-written list.
|
||||||
|
3. Unknown/unresolved IDs, unsupported-but-known effects, misplaced modifiers, duplicates, and conflicts must be reported distinctly. Do not silently apply a different effect or discard an authored page. A first compiler may use the existing random Age path for combinations it cannot represent, with an explicit diagnostic; it must never claim such a book was successfully compiled.
|
||||||
|
4. Grammar diagnostics are not instability. Milestone 5 decides recognition, ordering, completion, and whether a supported combination can compile. Milestone 6 may use dangling/excess/conflict diagnostics for instability effects. Milestone 7 owns bespoke structures and polish.
|
||||||
|
5. Freeze the completed, effective generation definition with the Age identity before creating chunks. Keep page order and original IDs for display/migration, and keep seeded completion deterministic across reloads. New symbol behavior belongs in versioned server data, not the schema-1 display catalog.
|
||||||
|
|
||||||
|
The mapping is intentionally narrower than all Legacy symbols. The exact rule and compiler schema are follow-up issues #29–#31; terrain, biome, environment, and feature implementations are #32–#35. No generation behavior or release version changes in this audit.
|
||||||
+2
-1
@@ -39,7 +39,8 @@ acceptance issue and all required implementation issues are complete.
|
|||||||
and [writing-system data and migration](WRITING_DATA.md).
|
and [writing-system data and migration](WRITING_DATA.md).
|
||||||
5. **Age grammar** — translate symbol combinations into modern biome, noise,
|
5. **Age grammar** — translate symbol combinations into modern biome, noise,
|
||||||
dimension-type, and world-generation configuration. Optional integrations
|
dimension-type, and world-generation configuration. Optional integrations
|
||||||
may add composable biome regions and seasonal behavior.
|
may add composable biome regions and seasonal behavior. See the
|
||||||
|
[Legacy grammar audit](LEGACY_AGE_GRAMMAR_AUDIT.md).
|
||||||
6. **Instability** — deterministic validation and server-safe effects.
|
6. **Instability** — deterministic validation and server-safe effects.
|
||||||
7. **Structures and polish** — libraries, crystals, portals, recipes, models,
|
7. **Structures and polish** — libraries, crystals, portals, recipes, models,
|
||||||
sounds, advancements, compatibility, and migration tests.
|
sounds, advancements, compatibility, and migration tests.
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# Server-authoritative symbol semantics (schema v2)
|
||||||
|
|
||||||
|
Milestone 5 issue #29 adds typed meaning to the existing data-pack symbol catalog. Put definitions at `data/<namespace>/rivencraft/symbols/<path>.json`; the file ID becomes the stable symbol ID. For example, `data/example/rivencraft/symbols/terrain/highlands.json` defines `example:terrain/highlands`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schema_version": 2,
|
||||||
|
"name": "symbol.example.terrain.highlands",
|
||||||
|
"description": "symbol.example.terrain.highlands.description",
|
||||||
|
"category": "example:terrain",
|
||||||
|
"semantics": {
|
||||||
|
"role": "terrain",
|
||||||
|
"value": "example:highlands"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`name` and `description` are translation keys. `category` is for display and sorting; it does not determine grammar. `semantics.role` and its typed `value` are server-owned. The terrain value above names an Age runtime profile that a later compiler must resolve and validate. A syntactically valid resource ID alone does not guarantee the profile or feature exists.
|
||||||
|
|
||||||
|
| Role | Value type and allowed values |
|
||||||
|
| --- | --- |
|
||||||
|
| `terrain` | Canonical namespaced Age profile ID. |
|
||||||
|
| `biome_controller` | `native`, `single`, `tiled`, `grid`, `tiny`, `small`, `medium`, `large`, or `huge`. |
|
||||||
|
| `biome` | Canonical namespaced biome ID. |
|
||||||
|
| `lighting` | `normal`, `dark`, or `bright`. |
|
||||||
|
| `weather` | `normal`, `fast`, `slow`, `off`, `always`, `cloudy`, `rain`, `snow`, or `storm`. |
|
||||||
|
| `celestial` | `normal_time` (the first representable single-cycle behavior). |
|
||||||
|
| `period_modifier` | JSON number in `[0, 2]`; zero is stopped time. |
|
||||||
|
| `angle_modifier` | JSON number in `[0, 360)` degrees. |
|
||||||
|
| `phase_modifier` | JSON number in `[0, 360)` degrees; the compiler will convert to turns. |
|
||||||
|
| `feature` | Canonical namespaced feature-effect ID; future generation mapping must recognize it. |
|
||||||
|
|
||||||
|
These are schema bounds, not claims that Legacy shipped numeric config ranges. See the [Legacy grammar audit](LEGACY_AGE_GRAMMAR_AUDIT.md) for the source values and biome-count rules. Roles outside this table—including multiple suns, gradients, special structures, and hazards—are intentionally unsupported by schema v2. Their meaning requires a later versioned extension, not an unvalidated free-form field.
|
||||||
|
|
||||||
|
The server rejects a v2 definition with an unknown top-level or semantics field, missing/incorrectly typed field, unknown role or choice, malformed/non-canonical resource ID, or out-of-range/non-finite number. An invalid definition is logged with its file ID and excluded from the active catalog. Saved pages and books still retain that ID, allowing the player to recover them if the data pack is corrected. A reload atomically replaces the catalog rather than mutating saved writing.
|
||||||
|
|
||||||
|
Schema v1 definitions remain readable as **display-only** symbols; existing packs need not be rewritten merely to show or copy pages. A v1 definition cannot add a `semantics` field. To make one eligible for future Age compilation, publish a v2 definition with the **same file path and ID**, adding a valid role and value. Do not rename the page ID or reorder a book's `OrderedWriting`. The client receives only `SymbolDisplayMetadata` (ID, translation keys, category, resolved flag); semantic definitions are not serialized to it. This slice defines and validates meaning but does **not yet apply** written symbols to Age generation; issues #30 and #31 implement parsing and compilation.
|
||||||
|
|
||||||
|
The six built-in definitions demonstrate terrain, biome-controller, lighting, weather, celestial, and feature roles. A third-party pack may add its own definitions under its namespace. Use an explicit semantic role even when the display category has the same name; the two fields serve different purposes.
|
||||||
+10
-4
@@ -1,9 +1,11 @@
|
|||||||
# Writing and symbol data
|
# Writing and symbol data
|
||||||
|
|
||||||
Milestone 4 stores what a player wrote without deciding what that writing does
|
Milestone 4 stores what a player wrote without deciding what that writing does
|
||||||
to an Age. Milestone 5 owns grammar validation, symbol interaction, instability,
|
to an Age. Milestone 5 owns grammar validation and symbol interaction,
|
||||||
and translation into biome, noise, dimension-type, and other world-generation
|
and translation into biome, noise, dimension-type, and other world-generation
|
||||||
configuration.
|
configuration. Milestone 6 owns instability effects. See the
|
||||||
|
[Legacy grammar audit](LEGACY_AGE_GRAMMAR_AUDIT.md) for the source-backed
|
||||||
|
Milestone 5 contract.
|
||||||
|
|
||||||
## Symbol catalog
|
## Symbol catalog
|
||||||
|
|
||||||
@@ -25,13 +27,17 @@ Schema version 1 contains display metadata only:
|
|||||||
|
|
||||||
`name` and `description` are translation keys, not arbitrary formatted text or
|
`name` and `description` are translation keys, not arbitrary formatted text or
|
||||||
executable behavior. `category` is a namespaced organizational identifier.
|
executable behavior. `category` is a namespaced organizational identifier.
|
||||||
|
Milestone 5 adds [server-authoritative schema v2 semantics](SYMBOL_SEMANTICS.md)
|
||||||
|
with a typed role and bounded value. Existing v1 definitions remain readable
|
||||||
|
but display-only; updating an ID to v2 never rewrites saved pages or books.
|
||||||
Invalid definitions are logged and excluded by the server. Menus and future
|
Invalid definitions are logged and excluded by the server. Menus and future
|
||||||
page synchronization must obtain resolved display metadata from the server;
|
page synchronization must obtain resolved display metadata from the server;
|
||||||
clients never make a symbol valid by naming it.
|
clients never make a symbol valid by naming it.
|
||||||
|
|
||||||
Rivencraft ships a small catalog covering terrain, biomes, lighting, weather,
|
Rivencraft ships a small catalog covering terrain, biomes, lighting, weather,
|
||||||
celestial behavior, and features. It exists to exercise the complete writing
|
celestial behavior, and features. Its v2 semantics prepare for the full
|
||||||
workflow before Milestone 5 implements the full legacy grammar.
|
Legacy-inspired grammar; writing still uses random Age fallback until the
|
||||||
|
parser and compiler are implemented.
|
||||||
|
|
||||||
## Persistent writing
|
## Persistent writing
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -36,7 +36,7 @@ mod_name=Rivencraft
|
|||||||
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
|
||||||
mod_license=LGPL-3.0-or-later
|
mod_license=LGPL-3.0-or-later
|
||||||
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
# The mod version. Rivencraft uses the x.y.z.b policy documented in README.md.
|
||||||
mod_version=0.4.0.2
|
mod_version=0.4.1.0
|
||||||
immersive_portals_version=6.0.7
|
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.
|
# 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.
|
# This should match the base package used for the mod sources.
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ public final class CoreGameTests {
|
|||||||
WritingDataGameTests.pageItemPreservesContentAndRejectsForgedUnknownSymbols(helper);
|
WritingDataGameTests.pageItemPreservesContentAndRejectsForgedUnknownSymbols(helper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void semanticSymbolSchema(GameTestHelper helper) {
|
||||||
|
WritingDataGameTests.semanticSchemaIsServerOnlyBoundedAndV1Compatible(helper);
|
||||||
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
@GameTest(template = "empty")
|
||||||
public static void notebookPersistence(GameTestHelper helper) {
|
public static void notebookPersistence(GameTestHelper helper) {
|
||||||
WritingDataGameTests.notebookContentsPreserveOrderGapsNamesAndUnknownPages(helper);
|
WritingDataGameTests.notebookContentsPreserveOrderGapsNamesAndUnknownPages(helper);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package net.dnorberg.rivencraft.gametest;
|
package net.dnorberg.rivencraft.gametest;
|
||||||
|
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
import com.mojang.serialization.JsonOps;
|
import com.mojang.serialization.JsonOps;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -8,6 +9,8 @@ import net.dnorberg.rivencraft.book.AgeReference;
|
|||||||
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
import net.dnorberg.rivencraft.writing.OrderedWriting;
|
||||||
import net.dnorberg.rivencraft.writing.PageContent;
|
import net.dnorberg.rivencraft.writing.PageContent;
|
||||||
import net.dnorberg.rivencraft.writing.SymbolCatalog;
|
import net.dnorberg.rivencraft.writing.SymbolCatalog;
|
||||||
|
import net.dnorberg.rivencraft.writing.SymbolDefinition;
|
||||||
|
import net.dnorberg.rivencraft.writing.SymbolSemantics;
|
||||||
import net.dnorberg.rivencraft.writing.SymbolReference;
|
import net.dnorberg.rivencraft.writing.SymbolReference;
|
||||||
import net.dnorberg.rivencraft.writing.WritingEntry;
|
import net.dnorberg.rivencraft.writing.WritingEntry;
|
||||||
import net.dnorberg.rivencraft.writing.NotebookContents;
|
import net.dnorberg.rivencraft.writing.NotebookContents;
|
||||||
@@ -100,6 +103,62 @@ public final class WritingDataGameTests {
|
|||||||
helper.succeed();
|
helper.succeed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GameTest(template = "empty")
|
||||||
|
public static void semanticSchemaIsServerOnlyBoundedAndV1Compatible(GameTestHelper helper) {
|
||||||
|
var builtIn = SymbolCatalog.find(reference("rivencraft:terrain/standard")).orElseThrow();
|
||||||
|
helper.assertValueEqual(builtIn.schemaVersion(), 2, "Built-in symbol must use semantic schema v2");
|
||||||
|
helper.assertValueEqual(builtIn.semantics().orElseThrow().role(), SymbolSemantics.Role.TERRAIN,
|
||||||
|
"Built-in terrain symbol must declare its grammar role");
|
||||||
|
|
||||||
|
String display = "\"name\":\"symbol.example.test\","
|
||||||
|
+ "\"description\":\"symbol.example.test.description\","
|
||||||
|
+ "\"category\":\"example:terrain\"";
|
||||||
|
SymbolDefinition legacy = SymbolDefinition.parse(JsonParser.parseString(
|
||||||
|
"{\"schema_version\":1," + display + "}"));
|
||||||
|
helper.assertTrue(legacy.semantics().isEmpty(), "Existing v1 definitions must remain display-only");
|
||||||
|
SymbolDefinition oldCategory = SymbolDefinition.parse(JsonParser.parseString(
|
||||||
|
"{\"schema_version\":1,\"name\":\"symbol.example.test\","
|
||||||
|
+ "\"description\":\"symbol.example.test.description\","
|
||||||
|
+ "\"category\":\"terrain\"}"));
|
||||||
|
helper.assertValueEqual(oldCategory.category().toString(), "minecraft:terrain",
|
||||||
|
"A v1 category without a namespace must retain the old ResourceLocation behavior");
|
||||||
|
helper.assertValueEqual(legacy.display(reference("example:test").id()).id(),
|
||||||
|
reference("example:test").id(), "Display metadata must preserve the symbol ID");
|
||||||
|
|
||||||
|
SymbolDefinition period = SymbolDefinition.parse(JsonParser.parseString(
|
||||||
|
"{\"schema_version\":2," + display
|
||||||
|
+ ",\"semantics\":{\"role\":\"period_modifier\",\"value\":0}}"));
|
||||||
|
helper.assertValueEqual(period.semantics().orElseThrow().role(), SymbolSemantics.Role.PERIOD_MODIFIER,
|
||||||
|
"Zero-length modifier must be accepted");
|
||||||
|
rejectDefinition(helper, "{\"schema_version\":2," + display
|
||||||
|
+ ",\"semantics\":{\"role\":\"unknown\",\"value\":\"normal\"}}", "role");
|
||||||
|
rejectDefinition(helper, "{\"schema_version\":2," + display
|
||||||
|
+ ",\"semantics\":{\"role\":\"weather\",\"value\":\"blizzard\"}}", "value");
|
||||||
|
rejectDefinition(helper, "{\"schema_version\":2," + display
|
||||||
|
+ ",\"semantics\":{\"role\":\"period_modifier\",\"value\":2.01}}", "value");
|
||||||
|
rejectDefinition(helper, "{\"schema_version\":2," + display
|
||||||
|
+ ",\"semantics\":{\"role\":\"terrain\",\"value\":\"minecraft:overworld\",\"extra\":1}}", "extra");
|
||||||
|
rejectDefinition(helper, "{\"schema_version\":1," + display
|
||||||
|
+ ",\"semantics\":{\"role\":\"weather\",\"value\":\"normal\"}}", "v1");
|
||||||
|
|
||||||
|
var clientJson = builtIn.display(reference("rivencraft:terrain/standard").id());
|
||||||
|
var encoded = net.dnorberg.rivencraft.writing.SymbolDisplayMetadata.CODEC
|
||||||
|
.encodeStart(JsonOps.INSTANCE, clientJson).result().orElseThrow();
|
||||||
|
helper.assertFalse(encoded.getAsJsonObject().has("semantics"),
|
||||||
|
"Client display payload must never contain generation semantics");
|
||||||
|
helper.succeed();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void rejectDefinition(GameTestHelper helper, String json, String diagnostic) {
|
||||||
|
boolean rejected = false;
|
||||||
|
try {
|
||||||
|
SymbolDefinition.parse(JsonParser.parseString(json));
|
||||||
|
} catch (IllegalArgumentException exception) {
|
||||||
|
rejected = exception.getMessage().contains(diagnostic);
|
||||||
|
}
|
||||||
|
helper.assertTrue(rejected, "Invalid symbol must be rejected with diagnostic containing '" + diagnostic + "'");
|
||||||
|
}
|
||||||
|
|
||||||
@GameTest(template = "empty")
|
@GameTest(template = "empty")
|
||||||
public static void pageStatesPersistAndNetworkRoundTrip(GameTestHelper helper) {
|
public static void pageStatesPersistAndNetworkRoundTrip(GameTestHelper helper) {
|
||||||
SymbolReference known = reference("rivencraft:terrain/standard");
|
SymbolReference known = reference("rivencraft:terrain/standard");
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package net.dnorberg.rivencraft.writing;
|
|||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.GsonBuilder;
|
import com.google.gson.GsonBuilder;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.mojang.serialization.JsonOps;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -56,10 +55,13 @@ public final class SymbolCatalog {
|
|||||||
ResourceManager resourceManager,
|
ResourceManager resourceManager,
|
||||||
ProfilerFiller profiler) {
|
ProfilerFiller profiler) {
|
||||||
Map<ResourceLocation, SymbolDefinition> loaded = new HashMap<>();
|
Map<ResourceLocation, SymbolDefinition> loaded = new HashMap<>();
|
||||||
resources.forEach((id, json) -> SymbolDefinition.CODEC.parse(JsonOps.INSTANCE, json)
|
resources.forEach((id, json) -> {
|
||||||
.resultOrPartial(error -> Rivencraft.LOGGER.error(
|
try {
|
||||||
"Ignoring invalid writing symbol {}: {}", id, error))
|
loaded.put(id, SymbolDefinition.parse(json));
|
||||||
.ifPresent(definition -> loaded.put(id, definition)));
|
} catch (IllegalArgumentException exception) {
|
||||||
|
Rivencraft.LOGGER.error("Ignoring invalid writing symbol {}: {}", id, exception.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
symbols = Map.copyOf(loaded);
|
symbols = Map.copyOf(loaded);
|
||||||
Rivencraft.LOGGER.info("Loaded {} writing symbols", loaded.size());
|
Rivencraft.LOGGER.info("Loaded {} writing symbols", loaded.size());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,120 @@
|
|||||||
package net.dnorberg.rivencraft.writing;
|
package net.dnorberg.rivencraft.writing;
|
||||||
|
|
||||||
import com.mojang.serialization.Codec;
|
import com.google.gson.JsonElement;
|
||||||
import com.mojang.serialization.DataResult;
|
import com.google.gson.JsonObject;
|
||||||
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
import com.google.gson.JsonPrimitive;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import net.minecraft.resources.ResourceLocation;
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
/** Server-loaded, non-executable display metadata for a writing symbol. */
|
/** Server-only symbol definition. Only display metadata crosses to clients. */
|
||||||
public record SymbolDefinition(
|
public record SymbolDefinition(
|
||||||
int schemaVersion,
|
int schemaVersion,
|
||||||
String nameTranslationKey,
|
String nameTranslationKey,
|
||||||
String descriptionTranslationKey,
|
String descriptionTranslationKey,
|
||||||
ResourceLocation category
|
ResourceLocation category,
|
||||||
|
Optional<SymbolSemantics> semantics
|
||||||
) {
|
) {
|
||||||
public static final int CURRENT_SCHEMA_VERSION = 1;
|
public static final int CURRENT_SCHEMA_VERSION = 2;
|
||||||
private static final Pattern TRANSLATION_KEY = Pattern.compile("[a-z0-9_.-]+");
|
private static final Pattern TRANSLATION_KEY = Pattern.compile("[a-z0-9_.-]+");
|
||||||
private static final Codec<String> TRANSLATION_KEY_CODEC = Codec.STRING.comapFlatMap(
|
private static final Set<String> V2_FIELDS = Set.of(
|
||||||
value -> TRANSLATION_KEY.matcher(value).matches()
|
"schema_version", "name", "description", "category", "semantics");
|
||||||
? 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 {
|
public SymbolDefinition {
|
||||||
Objects.requireNonNull(nameTranslationKey, "nameTranslationKey");
|
Objects.requireNonNull(nameTranslationKey, "nameTranslationKey");
|
||||||
Objects.requireNonNull(descriptionTranslationKey, "descriptionTranslationKey");
|
Objects.requireNonNull(descriptionTranslationKey, "descriptionTranslationKey");
|
||||||
Objects.requireNonNull(category, "category");
|
Objects.requireNonNull(category, "category");
|
||||||
|
semantics = Objects.requireNonNull(semantics, "semantics");
|
||||||
|
if (schemaVersion != 1 && schemaVersion != CURRENT_SCHEMA_VERSION) {
|
||||||
|
throw new IllegalArgumentException("Unsupported schema_version " + schemaVersion);
|
||||||
|
}
|
||||||
|
if (schemaVersion == 1 && semantics.isPresent()) {
|
||||||
|
throw new IllegalArgumentException("Schema v1 symbols cannot have semantics");
|
||||||
|
}
|
||||||
|
if (schemaVersion == CURRENT_SCHEMA_VERSION && semantics.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Schema v2 symbols require semantics");
|
||||||
|
}
|
||||||
|
validateTranslationKey(nameTranslationKey, "name");
|
||||||
|
validateTranslationKey(descriptionTranslationKey, "description");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** v1 remains display-only; v2 has an exact field set and typed semantics. */
|
||||||
|
public static SymbolDefinition parse(JsonElement json) {
|
||||||
|
if (json == null || !json.isJsonObject()) {
|
||||||
|
throw new IllegalArgumentException("Symbol definition must be a JSON object");
|
||||||
|
}
|
||||||
|
JsonObject object = json.getAsJsonObject();
|
||||||
|
int version = requiredInteger(object, "schema_version");
|
||||||
|
if (version != 1 && version != CURRENT_SCHEMA_VERSION) {
|
||||||
|
throw new IllegalArgumentException("Unsupported schema_version " + version);
|
||||||
|
}
|
||||||
|
if (version == CURRENT_SCHEMA_VERSION) {
|
||||||
|
for (String field : object.keySet()) {
|
||||||
|
if (!V2_FIELDS.contains(field)) {
|
||||||
|
throw new IllegalArgumentException("Unknown symbol field '" + field + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (object.has("semantics")) {
|
||||||
|
throw new IllegalArgumentException("Schema v1 cannot declare semantics; use schema_version 2");
|
||||||
|
}
|
||||||
|
String name = requiredString(object, "name");
|
||||||
|
String description = requiredString(object, "description");
|
||||||
|
ResourceLocation category = requiredResourceLocation(object, "category",
|
||||||
|
version == CURRENT_SCHEMA_VERSION);
|
||||||
|
Optional<SymbolSemantics> semantics = version == CURRENT_SCHEMA_VERSION
|
||||||
|
? Optional.of(SymbolSemantics.parse(object.get("semantics")))
|
||||||
|
: Optional.empty();
|
||||||
|
return new SymbolDefinition(version, name, description, category, semantics);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SymbolDisplayMetadata display(ResourceLocation id) {
|
public SymbolDisplayMetadata display(ResourceLocation id) {
|
||||||
return new SymbolDisplayMetadata(id, nameTranslationKey, descriptionTranslationKey, category, true);
|
return new SymbolDisplayMetadata(id, nameTranslationKey, descriptionTranslationKey, category, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static DataResult<SymbolDefinition> validate(SymbolDefinition definition) {
|
static String requiredString(JsonObject object, String field) {
|
||||||
return definition.schemaVersion == CURRENT_SCHEMA_VERSION
|
JsonElement element = object.get(field);
|
||||||
? DataResult.success(definition)
|
if (element == null || !element.isJsonPrimitive()
|
||||||
: DataResult.error(() -> "Unsupported symbol schema_version " + definition.schemaVersion);
|
|| !element.getAsJsonPrimitive().isString()) {
|
||||||
|
throw new IllegalArgumentException("'" + field + "' must be a string");
|
||||||
|
}
|
||||||
|
return element.getAsString();
|
||||||
|
}
|
||||||
|
|
||||||
|
static ResourceLocation requiredResourceLocation(JsonObject object, String field) {
|
||||||
|
return requiredResourceLocation(object, field, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ResourceLocation requiredResourceLocation(JsonObject object, String field, boolean canonical) {
|
||||||
|
String value = requiredString(object, field);
|
||||||
|
ResourceLocation id = ResourceLocation.tryParse(value);
|
||||||
|
if (id == null || (canonical && !id.toString().equals(value))) {
|
||||||
|
throw new IllegalArgumentException("'" + field + "' must be "
|
||||||
|
+ (canonical ? "a canonical namespaced ID" : "a resource location") + ": " + value);
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int requiredInteger(JsonObject object, String field) {
|
||||||
|
JsonElement element = object.get(field);
|
||||||
|
if (element == null || !element.isJsonPrimitive()) {
|
||||||
|
throw new IllegalArgumentException("'" + field + "' must be an integer");
|
||||||
|
}
|
||||||
|
JsonPrimitive primitive = element.getAsJsonPrimitive();
|
||||||
|
if (!primitive.isNumber() || !primitive.getAsString().matches("-?\\d+")) {
|
||||||
|
throw new IllegalArgumentException("'" + field + "' must be an integer");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Integer.parseInt(primitive.getAsString());
|
||||||
|
} catch (NumberFormatException exception) {
|
||||||
|
throw new IllegalArgumentException("'" + field + "' is outside the integer range", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void validateTranslationKey(String value, String field) {
|
||||||
|
if (!TRANSLATION_KEY.matcher(value).matches()) {
|
||||||
|
throw new IllegalArgumentException("Invalid '" + field + "' translation key: " + value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package net.dnorberg.rivencraft.writing;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonPrimitive;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import net.minecraft.resources.ResourceLocation;
|
||||||
|
|
||||||
|
/** Bounded grammar role and value, retained by the server catalog only. */
|
||||||
|
public record SymbolSemantics(Role role, Value value) {
|
||||||
|
public SymbolSemantics {
|
||||||
|
Objects.requireNonNull(role, "role");
|
||||||
|
Objects.requireNonNull(value, "value");
|
||||||
|
if (!role.accepts(value)) {
|
||||||
|
throw new IllegalArgumentException("Invalid semantics.value for role '" + role.id
|
||||||
|
+ "': " + value + "; expected " + role.expected());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SymbolSemantics parse(JsonElement json) {
|
||||||
|
if (json == null || !json.isJsonObject()) {
|
||||||
|
throw new IllegalArgumentException("'semantics' must be an object");
|
||||||
|
}
|
||||||
|
JsonObject object = json.getAsJsonObject();
|
||||||
|
for (String field : object.keySet()) {
|
||||||
|
if (!field.equals("role") && !field.equals("value")) {
|
||||||
|
throw new IllegalArgumentException("Unknown semantics field '" + field + "'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String roleId = SymbolDefinition.requiredString(object, "role");
|
||||||
|
Role role = Arrays.stream(Role.values()).filter(candidate -> candidate.id.equals(roleId))
|
||||||
|
.findFirst().orElseThrow(() -> new IllegalArgumentException("Unknown semantic role '" + roleId + "'"));
|
||||||
|
JsonElement raw = object.get("value");
|
||||||
|
if (raw == null || !raw.isJsonPrimitive()) {
|
||||||
|
throw new IllegalArgumentException("'semantics.value' is required");
|
||||||
|
}
|
||||||
|
JsonPrimitive primitive = raw.getAsJsonPrimitive();
|
||||||
|
Value value;
|
||||||
|
if (role.kind == Kind.RESOURCE) {
|
||||||
|
value = new ResourceValue(SymbolDefinition.requiredResourceLocation(object, "value"));
|
||||||
|
} else if (role.kind == Kind.NUMBER) {
|
||||||
|
if (!primitive.isNumber()) {
|
||||||
|
throw new IllegalArgumentException("'semantics.value' for " + role.id + " must be a number");
|
||||||
|
}
|
||||||
|
value = new NumberValue(primitive.getAsDouble());
|
||||||
|
} else {
|
||||||
|
value = new ChoiceValue(SymbolDefinition.requiredString(object, "value"));
|
||||||
|
}
|
||||||
|
return new SymbolSemantics(role, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed interface Value permits ResourceValue, ChoiceValue, NumberValue {}
|
||||||
|
|
||||||
|
public record ResourceValue(ResourceLocation id) implements Value {
|
||||||
|
public ResourceValue { Objects.requireNonNull(id, "id"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ChoiceValue(String id) implements Value {
|
||||||
|
public ChoiceValue { Objects.requireNonNull(id, "id"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record NumberValue(double amount) implements Value {
|
||||||
|
public NumberValue {
|
||||||
|
if (!Double.isFinite(amount)) {
|
||||||
|
throw new IllegalArgumentException("Semantic numeric value must be finite");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Kind { RESOURCE, CHOICE, NUMBER }
|
||||||
|
|
||||||
|
public enum Role {
|
||||||
|
TERRAIN("terrain", Kind.RESOURCE),
|
||||||
|
BIOME_CONTROLLER("biome_controller", Kind.CHOICE,
|
||||||
|
"native", "single", "tiled", "grid", "tiny", "small", "medium", "large", "huge"),
|
||||||
|
BIOME("biome", Kind.RESOURCE),
|
||||||
|
LIGHTING("lighting", Kind.CHOICE, "normal", "dark", "bright"),
|
||||||
|
WEATHER("weather", Kind.CHOICE,
|
||||||
|
"normal", "fast", "slow", "off", "always", "cloudy", "rain", "snow", "storm"),
|
||||||
|
CELESTIAL("celestial", Kind.CHOICE, "normal_time"),
|
||||||
|
PERIOD_MODIFIER("period_modifier", Kind.NUMBER),
|
||||||
|
ANGLE_MODIFIER("angle_modifier", Kind.NUMBER),
|
||||||
|
PHASE_MODIFIER("phase_modifier", Kind.NUMBER),
|
||||||
|
FEATURE("feature", Kind.RESOURCE);
|
||||||
|
|
||||||
|
private final String id;
|
||||||
|
private final Kind kind;
|
||||||
|
private final Set<String> choices;
|
||||||
|
|
||||||
|
Role(String id, Kind kind, String... choices) {
|
||||||
|
this.id = id;
|
||||||
|
this.kind = kind;
|
||||||
|
this.choices = Set.of(choices);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String id() { return id; }
|
||||||
|
|
||||||
|
private boolean accepts(Value value) {
|
||||||
|
if (kind == Kind.RESOURCE) return value instanceof ResourceValue;
|
||||||
|
if (kind == Kind.CHOICE) return value instanceof ChoiceValue choice && choices.contains(choice.id());
|
||||||
|
if (!(value instanceof NumberValue number)) return false;
|
||||||
|
return switch (this) {
|
||||||
|
case PERIOD_MODIFIER -> number.amount() >= 0.0 && number.amount() <= 2.0;
|
||||||
|
case ANGLE_MODIFIER, PHASE_MODIFIER -> number.amount() >= 0.0 && number.amount() < 360.0;
|
||||||
|
default -> false;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String expected() {
|
||||||
|
return switch (kind) {
|
||||||
|
case RESOURCE -> "a canonical namespaced ID";
|
||||||
|
case CHOICE -> "one of " + choices.stream().sorted().toList();
|
||||||
|
case NUMBER -> this == PERIOD_MODIFIER ? "a finite number in [0, 2]"
|
||||||
|
: "a finite number in [0, 360) degrees";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schema_version": 1,
|
"schema_version": 2,
|
||||||
"name": "symbol.rivencraft.biome.native",
|
"name": "symbol.rivencraft.biome.native",
|
||||||
"description": "symbol.rivencraft.biome.native.description",
|
"description": "symbol.rivencraft.biome.native.description",
|
||||||
"category": "rivencraft:biome"
|
"category": "rivencraft:biome",
|
||||||
|
"semantics": { "role": "biome_controller", "value": "native" }
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schema_version": 1,
|
"schema_version": 2,
|
||||||
"name": "symbol.rivencraft.celestial.normal_time",
|
"name": "symbol.rivencraft.celestial.normal_time",
|
||||||
"description": "symbol.rivencraft.celestial.normal_time.description",
|
"description": "symbol.rivencraft.celestial.normal_time.description",
|
||||||
"category": "rivencraft:celestial"
|
"category": "rivencraft:celestial",
|
||||||
|
"semantics": { "role": "celestial", "value": "normal_time" }
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schema_version": 1,
|
"schema_version": 2,
|
||||||
"name": "symbol.rivencraft.feature.caves",
|
"name": "symbol.rivencraft.feature.caves",
|
||||||
"description": "symbol.rivencraft.feature.caves.description",
|
"description": "symbol.rivencraft.feature.caves.description",
|
||||||
"category": "rivencraft:feature"
|
"category": "rivencraft:feature",
|
||||||
|
"semantics": { "role": "feature", "value": "rivencraft:caves" }
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schema_version": 1,
|
"schema_version": 2,
|
||||||
"name": "symbol.rivencraft.lighting.normal",
|
"name": "symbol.rivencraft.lighting.normal",
|
||||||
"description": "symbol.rivencraft.lighting.normal.description",
|
"description": "symbol.rivencraft.lighting.normal.description",
|
||||||
"category": "rivencraft:lighting"
|
"category": "rivencraft:lighting",
|
||||||
|
"semantics": { "role": "lighting", "value": "normal" }
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schema_version": 1,
|
"schema_version": 2,
|
||||||
"name": "symbol.rivencraft.terrain.standard",
|
"name": "symbol.rivencraft.terrain.standard",
|
||||||
"description": "symbol.rivencraft.terrain.standard.description",
|
"description": "symbol.rivencraft.terrain.standard.description",
|
||||||
"category": "rivencraft:terrain"
|
"category": "rivencraft:terrain",
|
||||||
|
"semantics": { "role": "terrain", "value": "rivencraft:standard" }
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schema_version": 1,
|
"schema_version": 2,
|
||||||
"name": "symbol.rivencraft.weather.normal",
|
"name": "symbol.rivencraft.weather.normal",
|
||||||
"description": "symbol.rivencraft.weather.normal.description",
|
"description": "symbol.rivencraft.weather.normal.description",
|
||||||
"category": "rivencraft:weather"
|
"category": "rivencraft:weather",
|
||||||
|
"semantics": { "role": "weather", "value": "normal" }
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user