Package's art exemption keys on Build's maker record, not hull.scad; case-variant Manufacturer and attack maker keys pinned in smoke_hull_source

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
soul 2026-10-19 01:44:15 +02:00
commit f121b46463
2 changed files with 57 additions and 15 deletions

View file

@ -1625,10 +1625,11 @@ def _build_hull_source(context, collection):
def _art_takes_over(collection):
"""Whether the body is the operator's: a hull.scad and a Ship Root exist, the collection holds render meshes that
Build did not make as they stand, and no zone mesh of Build's is left."""
"""Whether the body is the operator's: Build's maker record and a Ship Root exist, the collection holds render meshes
that Build did not make as they stand, and no zone mesh of Build's is left. The record alone says the hull was
Build-made; hull.scad is not consulted."""
root = _ship_root(collection)
if _hull_source_file(collection) is None or root is None:
if HULL_MAKER not in collection or root is None:
return False
untouched, _ = _hull_built(collection)
zones = [obj for obj in untouched if obj.type == "MESH" and "aetheria.role" not in obj]
@ -1636,12 +1637,14 @@ def _art_takes_over(collection):
def _retire_hull_source(collection):
"""Deletes the package's hull.scad when the body is the operator's (_art_takes_over). The anchors and collider she
kept are hers from then on. A hull never built, or built and packaged as it stands, keeps its source. Returns whether
the file was deleted."""
"""Deletes Build's records, and the package's hull.scad if it is still there, when the body is the operator's
(_art_takes_over). The anchors and collider she kept are hers from then on. A hull never built, or built and
packaged as it stands, keeps its source. Returns whether art took over."""
if not _art_takes_over(collection):
return False
_hull_source_file(collection).unlink()
source = _hull_source_file(collection)
if source is not None:
source.unlink()
for record in (HULL_BUILT, HULL_MAKER):
if record in collection:
del collection[record]
@ -1738,7 +1741,7 @@ class AETHERIA_OT_package_ship(bpy.types.Operator):
return {"CANCELLED"}
scene.aetheria_package_report = f"Packaged {len(anchors)} anchors; the validator accepted the ship"
if _retire_hull_source(collection):
scene.aetheria_package_report += "; deleted hull.scad: this package ships your art"
scene.aetheria_package_report += "; retired Build's hull records: this package ships your art"
self.report({"INFO"}, scene.aetheria_package_report)
return {"FINISHED"}

View file

@ -23,8 +23,9 @@ the hull.scad into the package and runs the panel's operators:
or a generated zone or mount that was edited in place.
Package accepts the built ship (the C# validator), writes ship.glb with a node per anchor, and leaves hull.scad; a
Build after Package still works.
The author replaces the zone meshes with her art and keeps Build's anchors and collider: Package deletes hull.scad in
that one act and says so, and Build then refuses.
The author replaces the zone meshes with her art and keeps Build's anchors and collider: Package retires Build's
records, and deletes hull.scad if it is still there (the second hull has had it removed by hand), in that one act and
says so, and Build then refuses. Build's maker record alone decides that a hull is Build-made and under the guard.
A hull that was never built keeps its hull.scad through the same deletion rule, and Package refuses it (no maker was
judged).
Package refuses an agent-built hull before it writes anything (ship.glb is not made) when the package holds no product,
@ -35,6 +36,7 @@ the hull.scad into the package and runs the panel's operators:
Package moves no row and turns no mount: every layout row is as authored after it, and a hand-turned mount keeps its yaw.
"""
import dataclasses
import json
import math
import os
@ -47,6 +49,7 @@ from pathlib import Path
import addon_utils
import bmesh
import bpy
import msgpack
REPO = Path(__file__).resolve().parents[3]
PACKAGES = os.environ.get("CULTLIB_PACKAGES") or sys.exit("Set CULTLIB_PACKAGES to CultLib's packages directory")
@ -75,17 +78,23 @@ HULLS = (
# per fixture: the variable the hull's language is in, a value that redefines it (every rule loosened), the language file a
# copy of which beside hull.scad would shadow the include, and a colour in it
# copy of which beside hull.scad would shadow the include, and a colour in it. %MAKER% is the maker key the faction's own
# language file names, read by the production reader (language_maker), so a language edited alone cannot leave it stale
ATTACKS = {
"zh-ammo-tender": ("ZH_LANG", '["zhestokost", ZH_ZONES, "primary", ZH_GLOW, ZH_RADIATOR, [[0, 9], [0, 9]], ["asymmetric", 0.0], "16bad4f9-7fb5-41c5-8da0-dc76709eb7d3"]',
"zh-ammo-tender": ("ZH_LANG", '["zhestokost", ZH_ZONES, "primary", ZH_GLOW, ZH_RADIATOR, [[0, 9], [0, 9]], ["asymmetric", 0.0], "%MAKER%"]',
"zhestokost.scad", "[0.42, 0.43, 0.45]"),
"pi-fighter": ("PI_LANG", '["pirate", PI_ZONES, "", PI_GLOW, PI_RADIATOR, [], ["bilateral", 0.0], "1d8a168def994a76a1ce7cb18f59c038"]',
"pi-fighter": ("PI_LANG", '["pirate", PI_ZONES, "", PI_GLOW, PI_RADIATOR, [], ["bilateral", 0.0], "%MAKER%"]',
"pirate.scad", "[0.48, 0.27, 0.22]"),
}
EXECUTABLE = None # the OpenSCAD the add-on preference names, set by main
hull_source = None # aetheria_ships.hull_source, set by main
def language_maker(faction):
"""The maker key the faction's own language file names, as Build reads it."""
return hull_source.read_languages(hull_source.language_library(EXECUTABLE)(faction))[0].maker
def require(condition, message):
if not condition:
raise SystemExit(message)
@ -184,6 +193,7 @@ def one_hull(scene, number, fixture, ship_id, extent, length, rows, body, palett
rows_before = layout_rows(scene)
source = package / "hull.scad"
lang_var, lang_value, shadowed, shadow_colour = ATTACKS[fixture]
lang_value = lang_value.replace("%MAKER%", language_maker(shadowed[:-len(".scad")]))
runner = hull_source.openscad_runner(EXECUTABLE, source)
require(source.is_file(), "The fixture hull.scad was not copied")
require("Build From Hull Source" == bpy.ops.aetheria.build_hull_source.get_rna_type().name, "Build is not registered")
@ -301,6 +311,32 @@ def one_hull(scene, number, fixture, ship_id, extent, length, rows, body, palett
root.location.x = off_centre
require(aetherdb(ship_cc, "product", ship_cc, other, "--remove") == 0, "AetherDb could not remove the other maker's product")
# Record keys compare exactly: a Manufacturer that differs from the language's maker only in case names no Faction.
# AetherDb cannot write one, but a hand-edited ship.cc or a cultcache-py push can, and the guard alone refuses it.
def manufacturer_as(change):
ship_module = sys.modules["aetheria_ships.ship_cc"]
store = ship_module.read(ship_cc, PACKAGES).store
seen = []
for envelope in store.pull_all():
if envelope.type == ship_module.PRODUCT_SCHEMA:
values = msgpack.unpackb(envelope.payload, raw=False)
seen.append(values[ship_module.PRODUCT_MANUFACTURER_SLOT])
values[ship_module.PRODUCT_MANUFACTURER_SLOT] = change(seen[-1])
store.push(dataclasses.replace(envelope, payload=msgpack.packb(values, use_bin_type=True)))
return seen
written = manufacturer_as(str.upper)
require(written and all(key.upper() != key for key in written), "The package holds no product with a cased key to rewrite")
select(box)
result, message = run(bpy.ops.aetheria.package_ship)
report = scene.aetheria_package_report
require(result == {"CANCELLED"} and "not by the maker of the hull's faction language" in report,
f"Package took a product whose Manufacturer differs from the maker in case: {report!r} {message!r}")
require(not (package / "ship.glb").exists(), "Package wrote ship.glb for a case-variant Manufacturer")
for echoed in (ship_id, str(GAME), maker, collection["aetheria.hull_maker"], collection["aetheria.hull_maker"].upper()):
require(echoed not in report, f"The refusal echoes an input ({echoed})")
manufacturer_as(lambda _: written.pop(0))
# Package: the validator accepts the built ship and the GLB holds a node per anchor.
mount = bpy.data.objects[roles["weapon-mount"][0]]
straight = mount.rotation_euler.z
@ -413,10 +449,13 @@ def one_hull(scene, number, fixture, ship_id, extent, length, rows, body, palett
art.parent = root
require(any(o.get("aetheria.role") for o in generated(collection).values()), "The scene kept no anchor of Build's")
require(aetherdb(ship_cc, "product", ship_cc, maker, "--remove") == 0, "AetherDb could not remove the product") # her art needs none
if number % 2 == 0: # the second hull: she (or git) removed hull.scad herself first; Build's record outlives it
source.unlink()
require(collection.get("aetheria.hull_maker"), "Build's maker record is gone before Package")
select(box)
result, message = run(bpy.ops.aetheria.package_ship)
report = scene.aetheria_package_report
require(result == {"FINISHED"} and "deleted hull.scad" in report, f"Package did not retire the hull source: {report!r} {message!r}")
require(result == {"FINISHED"} and "retired Build's hull records" in report, f"Package did not retire the hull source: {report!r} {message!r}")
require(not source.exists(), "hull.scad is still there after the author's art was packaged")
require("aetheria.hull_built" not in collection and "aetheria.hull_maker" not in collection,
"The record of Build's objects or its maker outlived hull.scad")
@ -424,7 +463,7 @@ def one_hull(scene, number, fixture, ship_id, extent, length, rows, body, palett
select(box)
result, message = run(bpy.ops.aetheria.build_hull_source)
require(result == {"CANCELLED"} and bpy.data.objects.get("author hull") is art, f"Build ran after takeover: {message!r}")
print(f"PASS {number}.9 her art with Build's anchors kept retires hull.scad, with a report; Build then refuses")
print(f"PASS {number}.9 her art with Build's anchors kept retires Build's records and any hull.scad, with a report; Build then refuses")
return collection