Aetheria/tools/AetherDb/VerseGrammarCommand.cs
Meta Vens 859ecad83a verse-grammar apply: the grammar update builds a new record instead of editing the cached one; replace test compares fields
The command mutated the cache's own VerseGrammar before the batch upsert, so the upsert was redundant and two Soul
mutants (update branch dropped; Same ignoring description or render path) survived: the replace test judged the result
with the same Same() the command uses. The test now reads the fields. Unrun until the full job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 14:39:44 +02:00

106 lines
6.6 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.IO;
using System.Linq;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
// verse-grammar [apply] (aetheria-release, cut verse-grammar; question verse-social-verbs, default speak-only): authors
// the starter verse grammar into GameData/Aetheria.cc. The ship-action half is one verb per TaskType, the shapes of what
// the game can film today; the conversation half is `speak`, matching Ghostlight's kernel-built speak. Dry run unless
// passed "apply", same contract as the other *-migrate commands.
//
// The catalog is opened here rather than through AetheriaStores.Open: that refuses a populated catalog that lacks a
// [CultGlobal], and this command is what authors the VerseGrammar global into a catalog that predates it.
public static class VerseGrammarCommand
{
public const int StarterRevision = 1;
public const string StarterDescription = "Verbs a Ghostlight-simulated Aetheria world may use: ship actions the game films in-engine, and speech rendered as Aetheria conversation.";
private static VerseRole Role(string name, VerseReferentKind binds) => new VerseRole { Name = name, Binds = binds };
// One ShipAction verb per TaskType (None excepted), then the conversation verbs.
public static VerseVerb[] StarterVerbs() => new[]
{
new VerseVerb { Name = "mine", RenderPath = VerseRenderPath.ShipAction, Description = "The actor works an ore body at a place and takes the ore into its hold.",
Roles = new[] { Role("site", VerseReferentKind.Place), Role("ore", VerseReferentKind.Cargo) } },
new VerseVerb { Name = "haul", RenderPath = VerseRenderPath.ShipAction, Description = "The actor carries cargo to a destination place.",
Roles = new[] { Role("cargo", VerseReferentKind.Cargo), Role("destination", VerseReferentKind.Place) } },
new VerseVerb { Name = "tow", RenderPath = VerseRenderPath.ShipAction, Description = "The actor tows a station or hull belonging to the target faction.",
Roles = new[] { Role("target", VerseReferentKind.Faction) } },
new VerseVerb { Name = "defend", RenderPath = VerseRenderPath.ShipAction, Description = "The actor guards a protected faction's ships and holdings at a place.",
Roles = new[] { Role("protected", VerseReferentKind.Faction), Role("at", VerseReferentKind.Place) } },
new VerseVerb { Name = "attack", RenderPath = VerseRenderPath.ShipAction, Description = "The actor attacks the target faction's ships and holdings.",
Roles = new[] { Role("target", VerseReferentKind.Faction) } },
new VerseVerb { Name = "explore", RenderPath = VerseRenderPath.ShipAction, Description = "The actor surveys a region and charts what is there.",
Roles = new[] { Role("region", VerseReferentKind.Place) } },
new VerseVerb { Name = "speak", RenderPath = VerseRenderPath.Conversation, Description = "The actor says something to whoever is in earshot. Shown as Aetheria dialogue: face to face when docked, by radio with the speaker's portrait over their ship when undocked.",
Roles = new VerseRole[0] },
};
public static bool Same(VerseVerb a, VerseVerb b) =>
a.Name == b.Name && a.RenderPath == b.RenderPath && a.Description == b.Description &&
a.Roles.Select(r => (r.Name, r.Binds)).SequenceEqual(b.Roles.Select(r => (r.Name, r.Binds)));
private static string Shape(VerseVerb verb) =>
$"{verb.Name}({string.Join(", ", verb.Roles.Select(r => $"{r.Name}: {r.Binds}"))}) [{verb.RenderPath}]";
public static int Run(bool apply, string root = null)
{
var catalogPath = Path.Combine(root ?? AetherDb.FindRoot(), "GameData", "Aetheria.cc");
using var cache = new CultCache();
cache.AddBackingStore(new SingleFileMessagePackBackingStore(catalogPath, !apply), AetheriaStores.CatalogTypes);
var starter = StarterVerbs();
foreach (var verb in starter) VerseGrammarValidation.Validate(verb);
var grammar = cache.GetGlobal<VerseGrammar>();
var grammarCurrent = grammar != null && grammar.Revision == StarterRevision && grammar.Description == StarterDescription;
Console.WriteLine(grammar == null ? $"Grammar: new, revision {StarterRevision}."
: grammarCurrent ? $"Grammar: revision {grammar.Revision}, already authored."
: $"Grammar: revision {grammar.Revision} -> {StarterRevision}.");
Console.WriteLine($" {StarterDescription}");
var existing = cache.GetAll<VerseVerb>().ToDictionary(verb => verb.Name, StringComparer.Ordinal);
var changes = starter.Where(verb => !existing.TryGetValue(verb.Name, out var current) || !Same(current, verb)).ToArray();
Console.WriteLine($"\nStarter verbs ({starter.Length}):");
foreach (var verb in starter)
{
var state = !existing.TryGetValue(verb.Name, out var current) ? "new" : Same(current, verb) ? "unchanged" : "differs, will be replaced";
Console.WriteLine($" {Shape(verb)} {state}");
Console.WriteLine($" {verb.Description}");
}
var extra = existing.Keys.Where(name => starter.All(verb => verb.Name != name)).OrderBy(name => name, StringComparer.Ordinal).ToArray();
if (extra.Length > 0) Console.WriteLine($"\nAlready in the catalog and left alone: {string.Join(", ", extra)}");
if (grammarCurrent && changes.Length == 0)
{
Console.WriteLine("\nNothing to do.");
return 0;
}
if (!apply)
{
Console.WriteLine($"\nDry run. Pass \"apply\" to land the grammar global and {changes.Length} verb(s).");
return 0;
}
cache.Commit(batch =>
{
if (!grammarCurrent)
{
var record = new VerseGrammar { Revision = StarterRevision, Description = StarterDescription };
if (grammar == null) batch.Upsert(typeof(VerseGrammar), record);
else batch.Upsert(typeof(VerseGrammar), record, cache.RefOf(grammar).Key);
}
foreach (var verb in changes)
{
if (existing.TryGetValue(verb.Name, out var current)) batch.Upsert(typeof(VerseVerb), verb, cache.RefOf(current).Key);
else batch.Upsert(typeof(VerseVerb), verb);
}
});
Console.WriteLine($"\nLanded the grammar global and {changes.Length} verb(s) in Aetheria.cc");
return 0;
}
}