ship_cc carries product envelopes through every push; PRODUCT_SCHEMA pinned; authoring doc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
943c492bb8
commit
cc77c4b5f5
4 changed files with 72 additions and 5 deletions
|
|
@ -162,7 +162,17 @@ keys. Each mod's stable `ShipAuthoring.Id` yields separate deterministic keys
|
|||
for its copied `HullData` (`mod-hull:<id>`) and its runtime authoring record
|
||||
(`mod-ship:<id>`). A mod's `ship.cc` stores its two records under those same
|
||||
keys, and compose copies them verbatim, so the derived catalog holds one hull
|
||||
per mod ship and `HullData.Visual` names the `mod-ship` record. The generated
|
||||
per mod ship and `HullData.Visual` names the `mod-ship` record. A package may
|
||||
also hold the hull's products, `FactionProductData` records stored at
|
||||
`mod-product:<id>:<maker key>` (the third key beside `mod-hull` and
|
||||
`mod-ship`; a key the mods own, never named by the shipped catalog). The key
|
||||
makes a second product by one maker impossible. A ship is its maker's: the hull
|
||||
carries the maker's name for it and one product by that maker, and a further
|
||||
product is a rebrand by another faction (the Pirates' stolen-and-adapted
|
||||
hulls). Compose copies the products verbatim and excludes a package, with a
|
||||
named reason, whose product names a maker the shipped catalog holds no faction
|
||||
for, or whose product name equals a shipped product's or another package's.
|
||||
Blender never writes or edits a product; AetherDb does. The generated
|
||||
catalog is disposable; the shipped catalog and each mod's source `.cc` remain
|
||||
the owners. A player session opens one catalog snapshot and cannot hot-swap its
|
||||
designs beneath existing entities.
|
||||
|
|
@ -223,7 +233,15 @@ catalog. Continue refuses a run naming a design the catalog no longer holds
|
|||
`aetheria.id`, is the object's `aetheria.id`. It then runs
|
||||
`AetherDb ship-authoring validate`, which judges the GLB too: node ids,
|
||||
and a mesh under every thruster and radiator anchor. The model supplies
|
||||
geometry, not a second copy of hull semantics.
|
||||
geometry, not a second copy of hull semantics. A ship hull needs a top
|
||||
speed (a `VelocityLimitData` behavior; a station or turret does not), and
|
||||
validation refuses one without.
|
||||
After Package, `ship-authoring price <ship.cc> <price>` sets the hull's
|
||||
price and `ship-authoring product <ship.cc> <maker> --name <name>
|
||||
[--description <text>]` writes the package's product by that maker
|
||||
(`--remove` deletes it). A package with a product needs a price above 0,
|
||||
because generation skips price 0; a package with none stays valid and is
|
||||
console-only. Compose follows.
|
||||
3. A command-line validator rejects malformed cells, overlapping or dangling
|
||||
hardpoints, missing model nodes, and unresolved asset references with
|
||||
precise errors. It changes no live catalog or scene on failure. The
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ public sealed class ShipSchemaPinTests
|
|||
{
|
||||
Assert.Equal(typeof(ShipAuthoring).GetCustomAttribute<CultDocumentAttribute>().SchemaName, Text("SCHEMA"));
|
||||
Assert.Equal(typeof(HullData).GetCustomAttribute<CultDocumentAttribute>().SchemaName, Text("HULL_SCHEMA"));
|
||||
Assert.Equal(typeof(FactionProductData).GetCustomAttribute<CultDocumentAttribute>().SchemaName, Text("PRODUCT_SCHEMA"));
|
||||
Assert.Equal(1, Integer("RETIRED_HULL_SLOT"));
|
||||
// Key 1 held the embedded hull until S1. A later member must not take it, or an old file would decode as it.
|
||||
Assert.DoesNotContain(typeof(ShipAuthoring).GetMembers(BindingFlags.Public | BindingFlags.Instance),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
"""Aetheria's Blender-side view of one ship file: a typed HullData record and the ShipAuthoring visual it names.
|
||||
|
||||
The schemas and authority live in Aetheria's C# HullData and ShipAuthoring types. This module edits the hull's grid
|
||||
and hardpoints and the visual's lines, and preserves the C# schema catalog and every slot it does not edit. Brokkr
|
||||
and hardpoints and the visual's lines, and preserves the C# schema catalog and every slot it does not edit. The file
|
||||
also holds the hull's products (FactionProductData records), which C# owns: every push here leaves them untouched. Brokkr
|
||||
supplies the Blender host and CultLib Python dependency path.
|
||||
"""
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ from typing import Any, NamedTuple
|
|||
|
||||
SCHEMA = "aetheria.ship_authoring"
|
||||
HULL_SCHEMA = "aetheria.hulldata"
|
||||
PRODUCT_SCHEMA = "aetheria.factionproductdata"
|
||||
|
||||
# MessagePack slots of the C# types, which own them (ShipAuthoring, ItemData.Shape, HullData.Hardpoints,
|
||||
# HardpointData). RETIRED_HULL_SLOT is the ShipAuthoring key that held the embedded hull before S1; no member owns it now. ShipSchemaPinTests in tests/Aetheria.Shared.Tests fails when any number or name below
|
||||
|
|
@ -97,8 +99,10 @@ def read(path: str, cultlib_packages: str) -> ShipFile:
|
|||
raise ValueError(
|
||||
f"The ship file has a legacy embedded hull at retired key {RETIRED_HULL_SLOT} of the {SCHEMA} record; "
|
||||
f"migrate it into its own {HULL_SCHEMA} record that names the visual, and drop key {RETIRED_HULL_SLOT}")
|
||||
if len(envelopes) != 2 or len(ships) != 1 or len(hulls) != 1:
|
||||
raise ValueError(f"The ship file must hold exactly one {SCHEMA} record and one {HULL_SCHEMA} record")
|
||||
products = [envelope for envelope in envelopes if envelope.type == PRODUCT_SCHEMA]
|
||||
if len(ships) != 1 or len(hulls) != 1 or len(ships) + len(hulls) + len(products) != len(envelopes):
|
||||
raise ValueError(f"The ship file must hold exactly one {SCHEMA} record and one {HULL_SCHEMA} record, "
|
||||
f"and may hold {PRODUCT_SCHEMA} records beside them, nothing else")
|
||||
bodies = []
|
||||
for envelope in (ships[0], hulls[0]):
|
||||
body = msgpack.unpackb(envelope.payload, raw=False)
|
||||
|
|
|
|||
|
|
@ -97,6 +97,17 @@ class ShipFileCase(unittest.TestCase):
|
|||
def ship(self):
|
||||
return ship_cc.read(self.path, PACKAGES).ship.body
|
||||
|
||||
def add_products(self):
|
||||
"""Two products as C# stores them, one per maker; Python reads them as envelopes and never encodes one."""
|
||||
entry = catalog("product-schema-1", ship_cc.PRODUCT_SCHEMA)
|
||||
store = cultcache_py.SingleFileMessagePackBackingStore(self.path)
|
||||
store.push(envelope("mod-product:mod.skiff:faction:acme", ship_cc.PRODUCT_SCHEMA, entry, [None, "Acme Skiff", "Reliable."]))
|
||||
store.push(envelope("mod-product:mod.skiff:faction:rival", ship_cc.PRODUCT_SCHEMA, entry, [None, "Rival Skiff", None, "tail"]))
|
||||
|
||||
def products(self):
|
||||
return {(e.type, e.key): e for e in cultcache_py.SingleFileMessagePackBackingStore(self.path).pull_all()
|
||||
if e.type == ship_cc.PRODUCT_SCHEMA}
|
||||
|
||||
def hull(self):
|
||||
return ship_cc.read(self.path, PACKAGES).hull.body
|
||||
|
||||
|
|
@ -115,6 +126,12 @@ class ReadTests(ShipFileCase):
|
|||
self.assertEqual("Skiff", file.hull.body[1])
|
||||
self.assertEqual(ship_cc.HULL_SCHEMA, file.hull.envelope.type)
|
||||
|
||||
def test_reads_a_file_whose_hull_also_carries_products(self):
|
||||
self.add_products()
|
||||
file = ship_cc.read(self.path, PACKAGES)
|
||||
self.assertEqual("mod.skiff", file.ship.body[0])
|
||||
self.assertEqual("Skiff", file.hull.body[1])
|
||||
|
||||
def test_refuses_a_file_with_another_record_beside_the_pair(self):
|
||||
cultcache_py.SingleFileMessagePackBackingStore(self.path).push(cultcache_py.CultCacheEnvelope.create(
|
||||
key="other", type="aetheria.something", payload=msgpack.packb([1], use_bin_type=True)))
|
||||
|
|
@ -190,6 +207,33 @@ class ReadTests(ShipFileCase):
|
|||
ship_cc.read(self.path, PACKAGES)
|
||||
|
||||
|
||||
class ProductCarryThroughTests(ShipFileCase):
|
||||
"""Products lie beside the pair, owned by C#: every push rewrites the file and leaves them byte-identical."""
|
||||
|
||||
def test_each_edit_leaves_both_product_envelopes_byte_identical(self):
|
||||
self.add_products()
|
||||
before = self.products()
|
||||
self.assertEqual(2, len(before))
|
||||
hull = hull_body()
|
||||
revision = ship_cc.layout_revision(hull[ship_cc.HULL_SHAPE_SLOT][0], hull[ship_cc.HULL_HARDPOINTS_SLOT])
|
||||
edits = (
|
||||
lambda: ship_cc.replace_lines(self.path, PACKAGES, LINES),
|
||||
lambda: ship_cc.replace_visual(self.path, PACKAGES, "mod.skiff", "ship.glb", [["map", "map-icon", "map", None, 0]]),
|
||||
lambda: ship_cc.replace_layout(self.path, PACKAGES, "mod.skiff", revision, [2, 2, [True, True, True, False]],
|
||||
[row[:len(ship_cc.HARDPOINT_MEMBERS)] for row in hull[ship_cc.HULL_HARDPOINTS_SLOT]]),
|
||||
)
|
||||
for index, edit in enumerate(edits):
|
||||
with self.subTest(edit=index):
|
||||
edit()
|
||||
after = self.products()
|
||||
self.assertEqual(before.keys(), after.keys())
|
||||
for key in before:
|
||||
self.assertEqual(before[key].payload, after[key].payload)
|
||||
self.assertEqual(before[key].key, after[key].key)
|
||||
# The edits really ran: the shape the layout edit wrote is in the file beside the carried products.
|
||||
self.assertEqual([2, 2, [True, True, True, False]], ship_cc.read(self.path, PACKAGES).hull.body[ship_cc.HULL_SHAPE_SLOT][0])
|
||||
|
||||
|
||||
class RigCarryThroughTests(ShipFileCase):
|
||||
"""The rig (ShipAuthoring slot 5 Joints, ShipAnchor slot 5 Joint) lies past every slot this module edits, so a save keeps it."""
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue