Files
BT411/tools/eggmodel.py
T
arcattackandClaude Fable 5 5e993de99d Operator-set callsigns: the console's name-bitmap generator recreated
The 1995 console let the operator type player callsigns; the pods render
them from 1bpp BITMAPS streamed in the egg ([largebitmap]/[smallbitmap]
lists + hex raster pages, bitmapindex per pilot; large 128x32 on the
score/ranking display via DPLRenderer name textures, small 64x16 on the
kill-feed and radar/target labels). The textual name= pilot key is dead
in the shipped binary -- its pilot parse reads bitmapindex only (decomp
part_014). Historical bug surfaced by this work: every egg in the repo
listed only ONE name bitmap (Aeolus) while pilot 2 pointed at index 2,
so player 2+'s callsign has never rendered (null-checked, silently
blank).

eggmodel.py: EggDoc.set_callsigns rewrites the lists + one raster page
per callsign per size + per-pilot name=/bitmapindex= (duplicate names
get disambiguated page tags); get_callsign_rows decodes pages back
(exact inverse of the engine's nibble-stream parse, GRAPH2D.cpp:340,
MSB-first bit order :558); rasterize_callsign/make_callsigns render
text via Qt (auto-shrink to fit, NoAntialias, 1bpp threshold) -- the
only PySide6-dependent corner of the module. validate() now catches
dangling bitmapindexes and missing raster pages.

btoperator.py: Callsign column in the roster (free text, PLAYERn
default); every save rasterizes the column into the egg exactly like
the original console did.

MP_BHMC.EGG template repaired: authentic Aeolus raster preserved
bit-exact, Boreas page generated and listed.

Verified: eggmodel self-test 20/20 (bit-exact raster round-trip, dup
disambiguation, dangling-index catch); ASCII-art decode of the
app-generated VIPER/MONGOOSE rasters reads correctly; live 2-node
relay session on the generated egg -- full ladder, launch, zero
name-bitmap load warnings; operator GUI e2e PASS after the column
shift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 12:29:57 -05:00

618 lines
26 KiB
Python

#!/usr/bin/env python
"""eggmodel.py -- headless mission-egg data model for the BT411 operator console.
Parses/edits/generates mission eggs with full preservation of the sections the
editor does not manage (bitmap rasters, role pages), and derives every legal
value set LIVE from content/BTL4.RES so the editor's dropdowns are authoritative:
- maps = resource names present as BOTH MakeMessageStream (14) AND
ExistanceBoxStream (26) -- the game aborts unless both exist
(MISSION.cpp:344-373)
- colors/badges/patches = the VehicleTable resource (type 25) [color]/[badge]/
[patch] sections (btl4vid.cpp SetupMaterialSubstitutionList)
- vehicles = ModelList (type 1) names, with the known-good mech models
surfaced first (egg 'vehicle=' resolves via
FindResourceDescription(..., ModelListResourceType))
Egg schema rules encoded here (readers: engine/MUNGA/MISSION.cpp ctor,
game/reconstructed/btl4mssn.cpp SetPlayerData):
[mission] REQUIRED: map, time, weather, scenario (missing => abort)
[pilots] pilot=<address> lines; each address names a [<address>] page
pilot page REQUIRED: hostType, vehicle, dropzone, color, patch, badge,
experience (unless vehicle=camera), role (-> a [role] page w/ model=)
badge doubles as the team name. experience in {novice,standard,veteran,expert}.
This module is GUI-free and importable (the console app and tests both use it).
Self-test: python eggmodel.py [--selftest]
"""
import collections
import os
import struct
HERE = os.path.dirname(os.path.abspath(__file__))
DEFAULT_RES = os.path.join(HERE, "..", "content", "BTL4.RES")
DEFAULT_TEMPLATE = os.path.join(HERE, "..", "content", "MP_BHMC.EGG")
# Static (observed / code-enumerated) value sets.
EXPERIENCES = ["novice", "standard", "veteran", "expert"] # btl4mssn.cpp:296-321
TIMES = ["night", "day"] # env-table tokens
WEATHERS = ["clear", "fog", "soup"] # env-table tokens
SCENARIOS = ["freeforall"] # observed
HOST_TYPES = {"0": "GameMachine", "1": "CameraShip", "2": "MissionReview"}
DROPZONES = ["one"] # only confirmed-valid name (embedded in map streams)
# Mech game models: known-good first (verified live), then candidates that ship
# skeleton/dead assets but are unverified as player vehicles.
KNOWN_GOOD_VEHICLES = ["bhk1", "madcat"]
CANDIDATE_VEHICLES = ["ava1", "avatar", "blkhawk", "lok1", "lok2", "loki",
"mad1", "mad2", "own1", "owens", "snd1", "sunder",
"thor", "thr1", "vul1", "vulture", "camera"]
RELAY_TAG_FMT = "10.99.0.{n}:1502" # relay-mode [pilots] identity tags
# ---------------------------------------------------------------------------
# BTL4.RES reader (the resscan.py parser, importable)
# ---------------------------------------------------------------------------
Res = collections.namedtuple("Res", "rid rtype name prio flags off length")
def read_res(path=DEFAULT_RES):
"""Parse BTL4.RES -> {rid: Res}."""
data = open(path, "rb").read()
_lab_only, max_id = struct.unpack_from("<ii", data, 4)
offsets = struct.unpack_from("<%dI" % max_id, data, 12)
resources = {}
for o in offsets:
if o == 0:
continue
rid, rtype = struct.unpack_from("<ii", data, o)
name = data[o + 8:o + 40].split(b"\0")[0].decode("ascii", "replace")
prio, flags, roff, rlen = struct.unpack_from("<iIII", data, o + 40)
resources[rid] = Res(rid, rtype, name, prio, flags, roff, rlen)
return data, resources
def _parse_table_text(text):
"""Parse the vehicletable resource: NUL-separated lines (the NotationFile
wire format), ';' comment lines, '//' trailing comments."""
sections = {}
current = None
for raw in text.replace("\0", "\n").replace("\r", "\n").split("\n"):
line = raw.split("//")[0].strip()
if not line or line.startswith(";"):
continue
if line.startswith("[") and line.endswith("]"):
current = line[1:-1].strip().lower()
sections[current] = collections.OrderedDict()
elif "=" in line and current is not None:
key, _, value = line.partition("=")
sections[current][key.strip()] = value.strip()
return sections
class ValueSets:
"""Every legal dropdown value, read live from BTL4.RES."""
def __init__(self, res_path=DEFAULT_RES):
data, resources = read_res(res_path)
by_type = collections.defaultdict(list)
for r in resources.values():
by_type[r.rtype].append(r)
# maps: need BOTH type 14 and type 26
maps14 = {r.name for r in by_type[14]}
maps26 = {r.name for r in by_type[26]}
self.maps = sorted(maps14 & maps26)
# vehicletable (type 25)
self.colors, self.badges, self.patches = [], [], []
for r in by_type[25]:
text = data[r.off:r.off + r.length].decode("latin-1", "replace")
table = _parse_table_text(text)
self.colors = list(table.get("color", {}).keys())
self.badges = list(table.get("badge", {}).keys())
self.patches = list(table.get("patch", {}).keys())
# vehicles: known-good first, then candidate mechs present in type 1
type1 = {r.name for r in by_type[1]}
self.vehicles = [v for v in KNOWN_GOOD_VEHICLES if v in type1]
self.vehicles += [v for v in CANDIDATE_VEHICLES
if v in type1 and v not in self.vehicles]
self.all_models = sorted(type1)
self.experiences = list(EXPERIENCES)
self.times = list(TIMES)
self.weathers = list(WEATHERS)
self.scenarios = list(SCENARIOS)
self.dropzones = list(DROPZONES)
# ---------------------------------------------------------------------------
# Egg document: order-preserving section parse/emit
# ---------------------------------------------------------------------------
class EggDoc:
"""An egg as an ordered list of (section_name, [raw 'key=value' lines]).
Lines before the first [section] live under section name None. Everything
the editor does not manage (bitmap rasters, role pages, ordinals) survives
byte-identically.
"""
def __init__(self, sections=None):
self.sections = sections if sections is not None else []
# ---- parse / emit ----
@classmethod
def load(cls, path):
return cls.parse(open(path, "r", encoding="latin-1").read())
@classmethod
def parse(cls, text):
sections = []
name, lines = None, []
for raw in text.replace("\r\n", "\n").split("\n"):
line = raw.rstrip()
if line.startswith("[") and line.endswith("]"):
sections.append((name, lines))
name, lines = line[1:-1], []
else:
if line:
lines.append(line)
sections.append((name, lines))
return cls(sections)
def emit(self):
out = []
for name, lines in self.sections:
if name is not None:
out.append("[%s]" % name)
out.extend(lines)
return "\n".join(out) + "\n"
def save(self, path):
with open(path, "w", encoding="latin-1", newline="\n") as f:
f.write(self.emit())
# ---- section access ----
def index_of(self, name):
for i, (n, _) in enumerate(self.sections):
if n is not None and n.lower() == name.lower():
return i
return -1
def get_lines(self, name):
i = self.index_of(name)
return self.sections[i][1] if i >= 0 else None
def get_kv(self, section, key):
lines = self.get_lines(section)
if lines is None:
return None
for line in lines:
k, _, v = line.partition("=")
if k.strip().lower() == key.lower():
return v.strip()
return None
def set_kv(self, section, key, value):
i = self.index_of(section)
if i < 0:
self.sections.append((section, ["%s=%s" % (key, value)]))
return
name, lines = self.sections[i]
for j, line in enumerate(lines):
k, _, _v = line.partition("=")
if k.strip().lower() == key.lower():
lines[j] = "%s=%s" % (key, value)
return
lines.append("%s=%s" % (key, value))
def remove_section(self, name):
i = self.index_of(name)
if i >= 0:
del self.sections[i]
def replace_section(self, name, lines, after=None):
"""Replace (or insert after section `after`) a whole section."""
i = self.index_of(name)
if i >= 0:
self.sections[i] = (name, list(lines))
return
pos = len(self.sections)
if after is not None:
j = self.index_of(after)
if j >= 0:
pos = j + 1
self.sections.insert(pos, (name, list(lines)))
# ---- the managed model ----
MISSION_KEYS = ["adventure", "map", "scenario", "time", "weather",
"temperature", "length"]
PILOT_KEYS = ["hostType", "advancedDamage", "loadzones", "name",
"bitmapindex", "experience", "badge", "patch",
"role", "dropzone", "vehicle", "vehicleValue", "color"]
def mission(self):
return {k: self.get_kv("mission", k) for k in self.MISSION_KEYS}
def pilots(self):
"""[{'address':..., <pilot keys>...}] in [pilots] order."""
lines = self.get_lines("pilots") or []
out = []
for line in lines:
k, _, v = line.partition("=")
if k.strip().lower() != "pilot":
continue
address = v.strip()
entry = {"address": address}
for key in self.PILOT_KEYS:
entry[key] = self.get_kv(address, key)
out.append(entry)
return out
def set_mission(self, **kv):
for k, v in kv.items():
if v is not None and v != "":
self.set_kv("mission", k, v)
def set_pilots(self, pilots):
"""Rewrite [pilots] + every per-pilot page from a pilots() -shaped list.
Old pilot pages are removed; page order matches roster order; everything
else in the egg is untouched."""
for old in self.pilots():
self.remove_section(old["address"])
self.replace_section(
"pilots", ["pilot=%s" % p["address"] for p in pilots])
anchor = "pilots"
for p in pilots:
lines = []
for key in self.PILOT_KEYS:
value = p.get(key)
if value is not None and value != "":
lines.append("%s=%s" % (key, value))
self.replace_section(p["address"], lines, after=anchor)
anchor = p["address"]
# ---- callsigns (the console's name-bitmap generator) ----
#
# The 1995 pods render player callsigns from 1bpp BITMAPS streamed in the
# egg, not from text: [largebitmap]/[smallbitmap] list the page names in
# bitmapindex order (1-based, Mission::LoadNameBitmaps MISSION.cpp:147);
# each page carries hex raster rows + x/y/width-in-words keys
# (BitMap::BitMap GRAPH2D.cpp:340). Bit order is MSB-first: 0x8000 =
# leftmost pixel of a 16-bit word (GRAPH2D.cpp:558), so a row is one
# straight big-endian bitstring and hex nibbles map to pixels in order.
# The textual name= key is console-side bookkeeping only -- the shipped
# binary's pilot parse never reads it (decomp part_014 @004d2b60 area).
LARGE_W, LARGE_H = 128, 32 # width=8 words
SMALL_W, SMALL_H = 64, 16 # width=4 words
@staticmethod
def _bitmap_page_lines(rows, width_px, height_px):
assert len(rows) == height_px, "raster height mismatch"
digits = width_px // 4
lines = []
for row in rows:
assert 0 <= row < (1 << width_px), "raster row wider than bitmap"
lines.append("bitmap=%0*X" % (digits, row))
lines.append("x=%d" % width_px)
lines.append("y=%d" % height_px)
lines.append("width=%d" % (width_px // 16))
return lines
@staticmethod
def _page_tag(name, taken):
tag = "".join(c if c.isalnum() else "_" for c in name.strip()) or "X"
base, n = tag, 2
while tag.lower() in taken:
tag = "%s_%d" % (base, n)
n += 1
taken.add(tag.lower())
return tag
def set_callsigns(self, callsigns):
"""Rewrite the callsign system from [(name, large_rows, small_rows)]
in pilot order: per-pilot name=/bitmapindex=, the [largebitmap]/
[smallbitmap] lists, and one raster page per callsign per size.
large_rows = LARGE_H ints of LARGE_W bits (MSB = leftmost pixel);
small_rows likewise SMALL_H x SMALL_W."""
pilots = self.pilots()
assert len(callsigns) == len(pilots), \
"need one callsign per pilot (%d != %d)" % (len(callsigns),
len(pilots))
# strip every old name-bitmap page (template leftovers included)
for name, _lines in list(self.sections):
if name and (name.lower().startswith("bitmap::large::")
or name.lower().startswith("bitmap::small::")):
self.remove_section(name)
taken = set()
tags = [self._page_tag(cs[0], taken) for cs in callsigns]
for p, (cs_name, _l, _s), i in zip(pilots, callsigns,
range(len(pilots))):
self.set_kv(p["address"], "name", cs_name)
self.set_kv(p["address"], "bitmapindex", str(i + 1))
anchor = pilots[-1]["address"] if self.index_of("largebitmap") < 0 \
else None
self.replace_section(
"largebitmap",
["bitmap=BitMap::Large::%s" % t for t in tags], after=anchor)
prev = "largebitmap"
for tag, (_n, large_rows, _s) in zip(tags, callsigns):
page = "BitMap::Large::%s" % tag
self.replace_section(
page, self._bitmap_page_lines(large_rows, self.LARGE_W,
self.LARGE_H), after=prev)
prev = page
self.replace_section(
"smallbitmap",
["bitmap=BitMap::Small::%s" % t for t in tags], after=prev)
prev = "smallbitmap"
for tag, (_n, _l, small_rows) in zip(tags, callsigns):
page = "BitMap::Small::%s" % tag
self.replace_section(
page, self._bitmap_page_lines(small_rows, self.SMALL_W,
self.SMALL_H), after=prev)
prev = page
def get_callsign_rows(self, page):
"""Decode a raster page back to row ints (None if absent) -- the
exact inverse of the engine's nibble stream parse."""
lines = self.get_lines(page)
if lines is None:
return None
width_px = int(self.get_kv(page, "x") or 0)
stream = "".join(line.partition("=")[2].strip() for line in lines
if line.partition("=")[0].strip().lower() == "bitmap")
rows, digits = [], width_px // 4
for i in range(0, len(stream), digits):
chunk = stream[i:i + digits]
if len(chunk) == digits:
rows.append(int(chunk, 16))
return rows
# ---- validation (the crash-prevention layer) ----
def validate(self, values: "ValueSets"):
"""Return a list of problem strings; empty == launchable."""
problems = []
m = self.mission()
for key in ("map", "time", "weather", "scenario"):
if not m.get(key):
problems.append("[mission] missing required key '%s'" % key)
if m.get("map") and m["map"] not in values.maps:
problems.append("map '%s' not in BTL4.RES (legal: %s)"
% (m["map"], ", ".join(values.maps)))
pilots = self.pilots()
if not pilots:
problems.append("no [pilots] entries")
seen = set()
for p in pilots:
a = p["address"]
if a in seen:
problems.append("duplicate pilot address '%s'" % a)
seen.add(a)
if self.index_of(a) < 0:
problems.append("pilot '%s' has no [%s] page" % (a, a))
continue
is_camera = (p.get("vehicle") == "camera")
required = ["hostType", "vehicle", "dropzone", "color",
"patch", "badge", "role"]
if not is_camera:
required.append("experience")
for key in required:
if not p.get(key):
problems.append("pilot '%s' missing required '%s'" % (a, key))
if p.get("color") and p["color"] not in values.colors:
problems.append("pilot '%s' color '%s' illegal (legal: %s)"
% (a, p["color"], ", ".join(values.colors)))
if p.get("badge") and p["badge"] not in values.badges:
problems.append("pilot '%s' badge '%s' illegal" % (a, p["badge"]))
if p.get("patch") and p["patch"] not in values.patches:
problems.append("pilot '%s' patch '%s' illegal" % (a, p["patch"]))
if p.get("experience") and p["experience"] not in values.experiences:
problems.append("pilot '%s' experience '%s' illegal (legal: %s)"
% (a, p["experience"], ", ".join(values.experiences)))
role = p.get("role")
if role and self.index_of(role) < 0:
problems.append("pilot '%s' role page [%s] missing" % (a, role))
elif role and not self.get_kv(role, "model"):
problems.append("role page [%s] missing 'model='" % role)
# callsign name bitmaps: every pilot's bitmapindex must resolve in
# BOTH lists (a miss = blank name in the score/kill-feed displays)
large = [l.partition("=")[2].strip()
for l in (self.get_lines("largebitmap") or [])
if l.partition("=")[0].strip().lower() == "bitmap"]
small = [l.partition("=")[2].strip()
for l in (self.get_lines("smallbitmap") or [])
if l.partition("=")[0].strip().lower() == "bitmap"]
for p in pilots:
bi = p.get("bitmapindex")
if not bi or not bi.isdigit():
continue # optional in the engine (warns)
i = int(bi)
for count, pages, which in ((len(large), large, "large"),
(len(small), small, "small")):
if i < 1 or i > count:
problems.append(
"pilot '%s' bitmapindex %d has no %sbitmap entry "
"(%d listed)" % (p["address"], i, which, count))
elif self.index_of(pages[i - 1]) < 0:
problems.append("%sbitmap page [%s] missing"
% (which, pages[i - 1]))
return problems
def new_from_template(template_path=DEFAULT_TEMPLATE):
"""A fresh editable egg seeded from the shipped template (keeps the bitmap
rasters + role pages the editor never touches)."""
return EggDoc.load(template_path)
def relay_tags(count):
return [RELAY_TAG_FMT.format(n=i + 1) for i in range(count)]
# ---------------------------------------------------------------------------
# Callsign rasterizer (the console's job in 1995: type a callsign, it becomes
# a bitmap in the egg). Needs PySide6 for font rendering -- the ONLY function
# here that does; everything else stays stdlib.
# ---------------------------------------------------------------------------
_qt_app = None
def rasterize_callsign(text, width_px, height_px):
"""Render `text` to a 1bpp raster: list of height_px ints, width_px bits
each, MSB = leftmost pixel (the GRAPH2D.cpp word/bit order). The font is
shrunk until the text fits."""
global _qt_app
from PySide6.QtCore import Qt, QRect
from PySide6.QtGui import (QGuiApplication, QImage, QPainter, QFont,
QFontMetrics)
if QGuiApplication.instance() is None:
_qt_app = QGuiApplication(["eggmodel"])
img = QImage(width_px, height_px, QImage.Format.Format_Grayscale8)
img.fill(0)
painter = QPainter(img)
font = QFont("Arial")
font.setBold(True)
font.setStyleStrategy(QFont.StyleStrategy.NoAntialias)
size = height_px - 4
while size > 5:
font.setPixelSize(size)
fm = QFontMetrics(font)
if (fm.horizontalAdvance(text) <= width_px - 4
and fm.height() <= height_px):
break
size -= 1
painter.setFont(font)
painter.setPen(Qt.GlobalColor.white)
painter.drawText(QRect(0, 0, width_px, height_px),
Qt.AlignmentFlag.AlignCenter, text)
painter.end()
rows = []
for y in range(height_px):
row = 0
for x in range(width_px):
row = (row << 1) | (1 if img.pixelColor(x, y).red() >= 128 else 0)
rows.append(row)
return rows
def make_callsigns(names):
"""[(name, large_rows, small_rows)] for EggDoc.set_callsigns."""
return [(n,
rasterize_callsign(n, EggDoc.LARGE_W, EggDoc.LARGE_H),
rasterize_callsign(n, EggDoc.SMALL_W, EggDoc.SMALL_H))
for n in names]
# ---------------------------------------------------------------------------
# Self-test
# ---------------------------------------------------------------------------
def _selftest():
ok = True
def check(cond, what):
nonlocal ok
print(" [%s] %s" % ("ok" if cond else "FAIL", what))
ok = ok and cond
values = ValueSets()
check(values.maps == ["arena1", "arena2", "artrucks", "cavern", "dbase",
"grass", "polar3", "polar4", "rav"],
"maps from RES: %s" % values.maps)
check(values.colors == ["Black", "Brown", "Crimson", "Green", "Grey",
"Tan", "White"], "colors: %s" % values.colors)
check(len(values.badges) == 7 and "VGL" in values.badges,
"badges: %s" % values.badges)
check(len(values.patches) == 8 and "Yellow" in values.patches,
"patches: %s" % values.patches)
check(values.vehicles[:2] == ["bhk1", "madcat"],
"vehicles (known-good first): %s" % values.vehicles[:6])
# Round-trip the shipped template: parse -> emit -> parse, semantics equal.
doc = EggDoc.load(DEFAULT_TEMPLATE)
doc2 = EggDoc.parse(doc.emit())
check(doc.mission() == doc2.mission(), "mission round-trips")
check(doc.pilots() == doc2.pilots(), "pilots round-trip")
check(len(doc.sections) == len(doc2.sections),
"section count preserved (%d)" % len(doc.sections))
check(doc.validate(values) == [],
"shipped template validates clean: %s" % doc.validate(values))
# Build a fresh 3-player relay egg and validate.
egg = new_from_template()
egg.set_mission(map="arena1", time="day", weather="fog")
tags = relay_tags(3)
base = doc.pilots()[0]
pilots = []
for i, tag in enumerate(tags):
p = dict(base)
p["address"] = tag
p["vehicle"] = ["bhk1", "madcat", "bhk1"][i]
p["color"] = ["White", "Crimson", "Green"][i]
pilots.append(p)
egg.set_pilots(pilots)
check(egg.validate(values) == [],
"generated 3-player relay egg validates: %s" % egg.validate(values))
check(len(egg.pilots()) == 3, "3 pilots emitted")
check(egg.get_kv(tags[2], "color") == "Green", "per-pilot page rewritten")
# bitmap sections survive
check(egg.index_of("largebitmap") >= 0 and egg.index_of("ordinals") >= 0,
"bitmap/ordinal sections preserved")
# bad values are caught
egg.set_kv(tags[0], "color", "Red")
check(any("Red" in p for p in egg.validate(values)),
"illegal color=Red caught by validation")
egg.set_kv(tags[0], "color", "White")
# Callsigns: synthetic rasters round-trip through set_callsigns exactly.
def synth(w, h, seed):
return [((0x9E3779B97F4A7C15 * (seed + y + 1)) & ((1 << w) - 1))
for y in range(h)]
names = ["Maverick", "Ice Man", "Maverick"] # dup + space on purpose
cs = [(n, synth(EggDoc.LARGE_W, EggDoc.LARGE_H, i * 100),
synth(EggDoc.SMALL_W, EggDoc.SMALL_H, i * 100 + 7))
for i, n in enumerate(names)]
egg.set_callsigns(cs)
egg2 = EggDoc.parse(egg.emit())
check(egg2.validate(values) == [],
"egg with callsigns validates: %s" % egg2.validate(values))
large_list = [l.partition("=")[2] for l in egg2.get_lines("largebitmap")]
check(len(large_list) == 3 and large_list[0] != large_list[2],
"3 large bitmaps listed, duplicate name disambiguated: %s"
% large_list)
ok_rt = True
for i, (n, lg, sm) in enumerate(cs):
p = egg2.pilots()[i]
ok_rt &= (p["name"] == n and p["bitmapindex"] == str(i + 1))
ok_rt &= (egg2.get_callsign_rows(large_list[i]) == lg)
check(ok_rt, "name=/bitmapindex= set + rasters decode back bit-exact")
check(egg2.index_of("BitMap::Large::Aeolus") < 0,
"stale template name-bitmap pages removed")
# a broken index is caught
egg2.set_kv(egg2.pilots()[2]["address"], "bitmapindex", "9")
check(any("bitmapindex 9" in p for p in egg2.validate(values)),
"dangling bitmapindex caught by validation")
# The Qt rasterizer produces something non-empty that fits (skipped if
# PySide6 is unavailable -- e.g. a bare CI box).
try:
rows = rasterize_callsign("BOREAS", EggDoc.SMALL_W, EggDoc.SMALL_H)
check(len(rows) == EggDoc.SMALL_H and any(rows)
and all(r < (1 << EggDoc.SMALL_W) for r in rows),
"rasterize_callsign('BOREAS') non-empty, in-bounds")
except ImportError:
print(" [--] rasterizer test skipped (no PySide6)")
print("\n%s" % ("ALL PASS" if ok else "FAILURES PRESENT"))
return 0 if ok else 1
if __name__ == "__main__":
raise SystemExit(_selftest())