Aetheria/Assets/Scripts/ServerShared/VerseGrammar.cs
Meta Vens 623072c711 Verse grammar: VerseGrammar and VerseVerb catalog documents, validation, the verse-grammar AetherDb command, tests
Adds the aetheria.verse_grammar global and aetheria.verse_verb documents (one render path per verb, typed
participant roles), VerseGrammarValidation run by AetheriaStores.Open over every verb, the AetherDb
'verse-grammar [apply]' command (starter set: one ship-action verb per TaskType plus speak; question
verse-social-verbs default speak-only), and VerseGrammarTests with a deterministic fixture writer.

The shipped catalog does not carry the grammar yet: it lands in the next commit, so
The_shipped_catalog_holds_revision_one_and_the_seven_starter_verbs fails at this commit. Not yet built when committed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 06:23:01 +02:00

82 lines
3.5 KiB
C#

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using GameCult.Caching;
using MessagePack;
// The verse grammar (aetheria-release, cut verse-grammar; ghostlight-verse rulings verse-grammar-home-ruled and
// verse-verbs-two-render-paths): the verbs a Ghostlight-simulated Aetheria world may use, each rendering by exactly one
// path the game already has, with typed participant roles. Aetheria owns which verbs exist and how each renders;
// Ghostlight owns what each does to world state. Nothing in the game runtime reads these documents.
[CultDocument("aetheria.verse_grammar", "1"), CultGlobal, MessagePackObject]
public class VerseGrammar
{
[Key(1)] public int Revision;
[Key(2)] public string Description;
}
// How a verb is shown: filmed in-engine as a ship action, or presented as an Aetheria conversation.
public enum VerseRenderPath
{
ShipAction = 1,
Conversation = 2,
}
// What kind of thing a role binds.
public enum VerseReferentKind
{
Person = 1,
Faction = 2,
Population = 3,
Place = 4,
Cargo = 5,
}
[MessagePackObject]
public class VerseRole
{
[Key(1)] public string Name;
[Key(2)] public VerseReferentKind Binds;
}
// The actor is the invoking subject and is never a role.
[CultDocument("aetheria.verse_verb", "1"), MessagePackObject]
public class VerseVerb
{
[CultName, Key(1)] public string Name;
[Key(2)] public VerseRenderPath RenderPath;
[Key(3)] public VerseRole[] Roles = Array.Empty<VerseRole>();
[Key(4)] public string Description;
}
public static class VerseGrammarValidation
{
private static readonly Regex VerbName = new Regex("^[a-z][a-z0-9_]{0,47}\z", RegexOptions.CultureInvariant);
private static readonly Regex RoleName = new Regex("^[a-z][a-z0-9_]{0,31}\z", RegexOptions.CultureInvariant);
// Canonical names, unique role names, defined enum values. A failure names the field and the rule, and the verb once
// its name is known canonical; it never prints a value the author wrote that is itself the fault.
public static void Validate(VerseVerb verb)
{
if (verb.Name == null || !VerbName.IsMatch(verb.Name))
throw new InvalidOperationException("VerseVerb.Name: not a canonical verb name ([a-z][a-z0-9_]{0,47}).");
if (!Enum.IsDefined(typeof(VerseRenderPath), verb.RenderPath))
throw new InvalidOperationException($"VerseVerb \"{verb.Name}\" RenderPath: not a defined render path.");
if (verb.Roles == null)
throw new InvalidOperationException($"VerseVerb \"{verb.Name}\" Roles: missing.");
var seen = new HashSet<string>(StringComparer.Ordinal);
foreach (var role in verb.Roles)
{
if (role == null || role.Name == null || !RoleName.IsMatch(role.Name))
throw new InvalidOperationException($"VerseVerb \"{verb.Name}\" Roles.Name: not a canonical role name ([a-z][a-z0-9_]{{0,31}}).");
if (!seen.Add(role.Name))
throw new InvalidOperationException($"VerseVerb \"{verb.Name}\" Roles.Name: duplicate role name.");
if (!Enum.IsDefined(typeof(VerseReferentKind), role.Binds))
throw new InvalidOperationException($"VerseVerb \"{verb.Name}\" Roles.Binds: not a defined referent kind.");
}
}
}