AetherDb: ship-authoring stats sets a package hull's mass, durability, armor, drag and top speed, judged before the file opens
Ruling ship-authoring-stats-command. --top-speed writes a fixed top speed (Min = Max) on the hull's one VelocityLimit and refuses a hull with none. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3efc8cddd7
commit
2fd8cbe8e9
2 changed files with 95 additions and 1 deletions
|
|
@ -325,6 +325,64 @@ public sealed class ShipProductTests : IDisposable
|
|||
RefusedAndUntouched("product", path, "ACM", "--remove");
|
||||
}
|
||||
|
||||
// The hull as data, to compare everything stats did not name.
|
||||
private static string HullJson(string path) => Newtonsoft.Json.JsonConvert.SerializeObject(ShipAuthoringStore.Load(path).Hull);
|
||||
|
||||
[Fact]
|
||||
public void StatsWritesOnlyTheFieldsItNamesAndARefusalLeavesTheFileUntouched()
|
||||
{
|
||||
Environment.CurrentDirectory = _root.Path;
|
||||
var path = Package("mod.skiff", "Skiffhull", 0);
|
||||
var bare = Path.Combine(ShipFixture.WritePackage(Mods, "mod.bare", "Barehull", tweak: ship => ship.Hull.Behaviors.Clear()), "ship.cc");
|
||||
void RefusedAndUntouched(string file, params string[] flags)
|
||||
{
|
||||
var before = File.ReadAllBytes(file);
|
||||
Assert.Equal(1, Run(new[] { "stats", file }.Concat(flags).ToArray()).Code);
|
||||
Assert.Equal(before, File.ReadAllBytes(file));
|
||||
}
|
||||
|
||||
RefusedAndUntouched(path);
|
||||
RefusedAndUntouched(path, "--speed", "5");
|
||||
RefusedAndUntouched(path, "--mass");
|
||||
RefusedAndUntouched(path, "--mass", "NaN");
|
||||
RefusedAndUntouched(path, "--mass", "Infinity");
|
||||
RefusedAndUntouched(path, "--armor", "-1");
|
||||
RefusedAndUntouched(path, "--drag", "slow");
|
||||
RefusedAndUntouched(path, "--mass", "5", "--mass", "6");
|
||||
// A valid flag beside a bad one writes nothing.
|
||||
RefusedAndUntouched(path, "--mass", "5", "--durability", "-2");
|
||||
RefusedAndUntouched(bare, "--top-speed", "50");
|
||||
RefusedAndUntouched(bare, "--mass", "5", "--top-speed", "50");
|
||||
|
||||
// Each flag sets its own field and nothing else.
|
||||
var expected = ShipAuthoringStore.Load(path).Hull;
|
||||
foreach (var (flag, value, set) in new (string, float, Action<HullData>)[]
|
||||
{
|
||||
("--mass", 60000f, hull => hull.Mass = 60000f),
|
||||
("--durability", 8000f, hull => hull.Durability = 8000f),
|
||||
("--armor", 40f, hull => hull.Armor = 40f),
|
||||
("--drag", .35f, hull => hull.Drag = .35f),
|
||||
("--top-speed", 37.5f, hull => { var speed = hull.Behaviors.OfType<VelocityLimitData>().Single().TopSpeed; speed.Min = speed.Max = 37.5f; }),
|
||||
})
|
||||
{
|
||||
var (code, output, error) = Run("stats", path, flag, value.ToString("R", System.Globalization.CultureInfo.InvariantCulture));
|
||||
Assert.True(code == 0, error);
|
||||
Assert.Contains(flag.Substring(2), output);
|
||||
set(expected);
|
||||
Assert.Equal(Newtonsoft.Json.JsonConvert.SerializeObject(expected), HullJson(path));
|
||||
}
|
||||
var written = ShipAuthoringStore.Load(path).Hull;
|
||||
Assert.Equal((60000f, 8000f, 40f, .35f), (written.Mass, written.Durability, written.Armor, written.Drag));
|
||||
var top = written.Behaviors.OfType<VelocityLimitData>().Single().TopSpeed;
|
||||
Assert.Equal((37.5f, 37.5f), (top.Min, top.Max));
|
||||
|
||||
// Several at once, and a hull with no top speed takes the others.
|
||||
Assert.Equal(0, Run("stats", path, "--mass", "1", "--drag", "0").Code);
|
||||
Assert.Equal((1f, 0f, 8000f), (ShipAuthoringStore.Load(path).Hull.Mass, ShipAuthoringStore.Load(path).Hull.Drag, ShipAuthoringStore.Load(path).Hull.Durability));
|
||||
Assert.Equal(0, Run("stats", bare, "--armor", "12").Code);
|
||||
Assert.Equal(12f, ShipAuthoringStore.Load(bare).Hull.Armor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateJudgesProductsOnADraftAndInspectListsThem()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ using System.Linq;
|
|||
// Uses the existing database executable; mod authoring does not earn another binary target.
|
||||
public static class ShipAuthoringCommands
|
||||
{
|
||||
private const string Usage = "ship-authoring create <ship.cc> <stable-id> <name> [--like <shipped hull name>] | inspect <ship.cc> | centre <ship.cc> | validate <ship.cc> | product <ship.cc> <maker> (--name <name> [--description <text>] [--not-offered] | --remove) | price <ship.cc> <price> | compose <shipped.cc> <derived.cc> <mods-dir>";
|
||||
private const string Usage = "ship-authoring create <ship.cc> <stable-id> <name> [--like <shipped hull name>] | inspect <ship.cc> | centre <ship.cc> | validate <ship.cc> | product <ship.cc> <maker> (--name <name> [--description <text>] [--not-offered] | --remove) | price <ship.cc> <price> | stats <ship.cc> [--mass m] [--durability d] [--armor a] [--drag x] [--top-speed v] | compose <shipped.cc> <derived.cc> <mods-dir>";
|
||||
|
||||
private static readonly string[] StatFlags = { "--mass", "--durability", "--armor", "--drag", "--top-speed" };
|
||||
|
||||
public static int Run(string[] args)
|
||||
{
|
||||
|
|
@ -91,6 +93,40 @@ public static class ShipAuthoringCommands
|
|||
Console.WriteLine($"{ship.Id}: price {price}.");
|
||||
return 0;
|
||||
}
|
||||
if (args[0] == "stats" && args.Length >= 2)
|
||||
{
|
||||
// Every flag is judged before the file is touched: a refusal leaves it byte-identical.
|
||||
var stats = new Dictionary<string, float>(StringComparer.Ordinal);
|
||||
for (var i = 2; i < args.Length; i += 2)
|
||||
{
|
||||
if (Array.IndexOf(StatFlags, args[i]) < 0) throw new InvalidOperationException($"stats: unknown flag {args[i]}.");
|
||||
if (stats.ContainsKey(args[i])) throw new InvalidOperationException($"stats: {args[i]} is given twice.");
|
||||
if (i + 1 == args.Length) throw new InvalidOperationException($"stats: {args[i]} needs a value.");
|
||||
if (!float.TryParse(args[i + 1], NumberStyles.Float, CultureInfo.InvariantCulture, out var value) || !float.IsFinite(value) || value < 0)
|
||||
throw new InvalidOperationException($"stats: {args[i]} must be a finite, non-negative number.");
|
||||
stats[args[i]] = value;
|
||||
}
|
||||
if (stats.Count == 0) throw new InvalidOperationException("stats: name at least one stat.");
|
||||
var (hull, ship, _) = ShipAuthoringStore.Load(args[1]);
|
||||
if (stats.TryGetValue("--top-speed", out var topSpeed))
|
||||
{
|
||||
var limits = (hull.Behaviors ?? new List<BehaviorData>()).OfType<VelocityLimitData>().ToArray();
|
||||
if (limits.Length != 1) throw new InvalidOperationException($"{ship.Id}: --top-speed needs exactly one VelocityLimit on the hull; it has {limits.Length}.");
|
||||
// A fixed top speed, as every shipped hull authors one (Min equal to Max); its terms are kept.
|
||||
limits[0].TopSpeed.Min = limits[0].TopSpeed.Max = topSpeed;
|
||||
}
|
||||
if (stats.TryGetValue("--mass", out var mass)) hull.Mass = mass;
|
||||
if (stats.TryGetValue("--durability", out var durability)) hull.Durability = durability;
|
||||
if (stats.TryGetValue("--armor", out var armor)) hull.Armor = armor;
|
||||
if (stats.TryGetValue("--drag", out var drag)) hull.Drag = drag;
|
||||
using (var cache = ShipAuthoringStore.Open(args[1], writable: true))
|
||||
{
|
||||
ShipAuthoringStore.Write(cache, hull, ship);
|
||||
cache.FlushAsync().Wait();
|
||||
}
|
||||
Console.WriteLine($"{ship.Id}: " + string.Join(", ", stats.Select(stat => string.Create(CultureInfo.InvariantCulture, $"{stat.Key.Substring(2)} {stat.Value:R}"))) + ".");
|
||||
return 0;
|
||||
}
|
||||
if (args[0] == "product" && args.Length >= 4)
|
||||
{
|
||||
// A trailing --not-offered takes the product off the market (ruling off-market-is-unoffered-product); --remove takes none.
|
||||
|
|
|
|||
Loading…
Reference in a new issue