KEYBOARD LAMP MIRROR (L4KEYLIGHT). Ported from RP412, itself a port of vRIO's KeyboardLampMirror. Keys bound to a lamp address in bindings.txt glow with the panel palette through Windows Dynamic Lighting -- yellow for the map's side columns (0x10-0x1F), red for the rest -- flashing in step with the on-screen buttons (its LampLevel copy matches the FIXED BTLampBrightnessOf). Per-key boards light each bound key; zone-lit boards mirror the strongest lamp board-wide. All WinRT runs on a private worker thread: watcher, claim, and a 100 ms paint loop that repaints only on change. RP412's packing hazard does NOT transfer: it forces default struct packing there because its engine is /Zp1, which would break the WinRT ABI. BT411's BT_OPTS carries no /Zp, so only the dialect flags are needed -- /std:c++17 /permissive- per-file, since the project otherwise builds C++14 /permissive. The scalars-only interface is kept anyway so the isolation survives if packing is ever added. Wired in PadRIO: map built from bindings.keyBindings (ActionButton binds only, first-binding-wins per key), fed from PadRIO::SetLamp, stopped in the dtor (which hands the LEDs back to Windows). BT_KEYLIGHT=0 opts out; a machine without Dynamic Lighting logs once and stays dormant. Verified live: claimed this machine's 24-zone keyboard and mirrors 25 bound keys; BT_KEYLIGHT=0 and the pod profile produce zero keylight lines and run clean. SHIPPED CONTROLS REFERENCE. docs/CONTROLS.md carries the keyboard table plus the full 72-BUTTON POD MAP, grouped by the display each bank surrounds, with the coolant-valve detent warning (1-5-50-CLOSED, one press past max shuts the loop) and the jam/eject procedure. mkdist flattens it to ASCII as CONTROLS.txt at the zip root, the README's own idiom. players/README.txt gained pointers to it, to environ.ini, to -fit, and to the RGB mirror. KB: context/glass-cockpit.md and docs/GLASS_COCKPIT.md updated for the whole branch -- layout modes, L4RIOBANK and the under-glass rule, the letterbox fit and its ordering trap, player-tunable displays, the measured legend grid, the closed dead-button backlog, and this mirror. Verified: five-mode regression (surround/exploded/dock/pod/dev) boots and simulates with zero faults; mkdist writes a 5149-byte pure-ASCII CONTROLS.txt. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
173 lines
7.8 KiB
Python
173 lines
7.8 KiB
Python
#!/usr/bin/env python3
|
||
"""mkdist.py -- build the tester-distribution zip.
|
||
|
||
Layout (matches what the player bats expect):
|
||
BT411-<date>-<hash>/
|
||
play_solo.bat join.bat join_lan.bat (from players/)
|
||
build/Release/btl4.exe (pod build -- fallback)
|
||
build-glass/Release/btl4.exe (preferred: clickable cockpit)
|
||
content/... (git-TRACKED files only --
|
||
the working tree carries dev
|
||
junk that must not ship)
|
||
|
||
The bats prefer build-glass when present and set BT_PLATFORM=glass with it,
|
||
so testers boot the glass experience (PadRIO clicks, trigger config,
|
||
bindings.txt keymap) with the pod build as fallback.
|
||
|
||
Usage: python tools/mkdist.py [outdir] (default outdir = dist/)
|
||
"""
|
||
import os
|
||
import re
|
||
import subprocess
|
||
import sys
|
||
import time
|
||
import zipfile
|
||
|
||
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
|
||
def main():
|
||
os.chdir(REPO)
|
||
outdir = sys.argv[1] if len(sys.argv) > 1 else os.path.join(REPO, "dist")
|
||
os.makedirs(outdir, exist_ok=True)
|
||
|
||
# Name by the repo version convention (BT411_4.11.NNN, tools/btversion.cmake:
|
||
# build = commit count). Falls back to date+hash if the header is absent.
|
||
name = None
|
||
for h in ("build/btversion.h", "build-glass/btversion.h"):
|
||
if os.path.exists(h):
|
||
m = re.search(r'BT_VERSION_STRING\s+"([^"]+)"', open(h).read())
|
||
if m:
|
||
name = "BT411_" + m.group(1)
|
||
break
|
||
if name is None:
|
||
githash = subprocess.check_output(
|
||
["git", "rev-parse", "--short", "HEAD"]).decode().strip()
|
||
name = "BT411-%s-%s" % (time.strftime("%Y-%m-%d"), githash)
|
||
|
||
tracked = subprocess.check_output(
|
||
["git", "ls-files", "-z", "content/"]).decode().split("\0")
|
||
tracked = [t for t in tracked if t]
|
||
|
||
# ONE unified build (2026-07-21): every layer lives in the single exe.
|
||
# THE LICENSE GATE: BT_STEAM is compile-time (Steamworks SDK terms bar a
|
||
# public release with Steam inside). Read the configure's answer from
|
||
# the CMake cache; a steam-OFF zip must carry neither play_steam.bat nor
|
||
# steam_api.dll (a stale DLL can linger from an earlier ON build).
|
||
steam_on = False
|
||
cache = ""
|
||
try:
|
||
cache = open("build/CMakeCache.txt", encoding="utf-8", errors="ignore").read()
|
||
steam_on = "BT_STEAM:BOOL=ON" in cache
|
||
except OSError:
|
||
pass
|
||
print(" BT_STEAM=%s (from build/CMakeCache.txt)" % ("ON" if steam_on else "OFF"))
|
||
if not steam_on:
|
||
name += "-nosteam" # distinct name: never clobbers the dev-flavor zip
|
||
|
||
# TESTER-BUILD EXPIRY (BT_EXPIRE, default ON): a zip cut from an
|
||
# expire-OFF build never dies in the field -- mark it loudly so it is a
|
||
# deliberate act, not an accident.
|
||
expire_on = "BT_EXPIRE:BOOL=ON" in cache if cache else True
|
||
print(" BT_EXPIRE=%s (from build/CMakeCache.txt)" % ("ON" if expire_on else "OFF"))
|
||
if not expire_on:
|
||
name += "-noexpire"
|
||
print(" WARNING: this zip NEVER EXPIRES -- intended for distribution?")
|
||
zpath = os.path.join(outdir, name + ".zip")
|
||
|
||
exes = ["build/Release/btl4.exe"]
|
||
bats = ["players/play_solo.bat", "players/join.bat", "players/join_lan.bat",
|
||
"players/joyconfig.bat"]
|
||
if steam_on:
|
||
bats.append("players/play_steam.bat")
|
||
for p in exes + bats:
|
||
if not os.path.exists(p):
|
||
sys.exit("MISSING: %s -- build/refresh it first" % p)
|
||
|
||
# Runtime DLLs living next to each exe (OpenAL32.dll etc.) -- the exe
|
||
# fails to load without them (caught by the clean-extract boot test).
|
||
for exe in list(exes):
|
||
d = os.path.dirname(exe)
|
||
for f in sorted(os.listdir(d)):
|
||
if not f.lower().endswith(".dll"):
|
||
continue
|
||
if f.lower() == "steam_api.dll" and not steam_on:
|
||
print(" (steam OFF: leaving stale %s out of the zip)" % f)
|
||
continue
|
||
exes.append(d + "/" + f)
|
||
|
||
# System runtime DLLs the exe needs on a bare tester machine (32-bit
|
||
# d3dx9 + MSVC runtimes -- carried in redist/, shipped next to the exe;
|
||
# the first zips included them and machines without the redists fail
|
||
# to load the exe otherwise).
|
||
redist = ["redist/" + f for f in sorted(os.listdir("redist"))
|
||
if f.lower().endswith(".dll")] if os.path.isdir("redist") else []
|
||
|
||
total = 0
|
||
with zipfile.ZipFile(zpath, "w", zipfile.ZIP_DEFLATED, compresslevel=6) as z:
|
||
# versioned tester README at the zip root
|
||
if os.path.exists("players/README.txt"):
|
||
readme = open("players/README.txt", encoding="utf-8").read()
|
||
readme = readme.replace("{VERSION}", name.replace("BT411_", ""))
|
||
lines = readme.splitlines(True)
|
||
if steam_on: # keep steam lines, drop marker
|
||
lines = [l.replace("{STEAM}", "") for l in lines]
|
||
else: # steam-free zip: strip them
|
||
lines = [l for l in lines if "{STEAM}" not in l]
|
||
if expire_on: # same treatment for expiry note
|
||
lines = [l.replace("{EXPIRE}", "") for l in lines]
|
||
else:
|
||
lines = [l for l in lines if "{EXPIRE}" not in l]
|
||
readme = "".join(lines)
|
||
z.writestr(name + "/README.txt", readme)
|
||
|
||
# The full controls reference travels with the game, so players get the
|
||
# 72-button panel map without the repo. Flattened to ASCII so it reads
|
||
# correctly in Notepad -- the markdown source keeps its typography.
|
||
if os.path.exists("docs/CONTROLS.md"):
|
||
controls = open("docs/CONTROLS.md", encoding="utf-8").read()
|
||
for bad, good in ((u"—", "-"), (u"–", "-"), (u"‘", "'"),
|
||
(u"’", "'"), (u"“", '"'), (u"”", '"'),
|
||
(u"×", "x"), (u"→", "->"), (u"…", "..."),
|
||
(u"↑", "Up"), (u"↓", "Down"),
|
||
(u"←", "Left"), (u"→", "Right"),
|
||
(u"⚠", "!"), (u"·", "-")):
|
||
controls = controls.replace(bad, good)
|
||
controls = controls.encode("ascii", "replace").decode("ascii")
|
||
z.writestr(name + "/CONTROLS.txt", controls)
|
||
print(" controls reference -> CONTROLS.txt")
|
||
for p in bats: # bats at the zip ROOT (next to
|
||
arc = os.path.basename(p) # content\ + build\, per the guard)
|
||
z.write(p, name + "/" + arc)
|
||
total += os.path.getsize(p)
|
||
for p in exes:
|
||
z.write(p, name + "/" + p)
|
||
total += os.path.getsize(p)
|
||
for p in redist: # next to the exe
|
||
z.write(p, name + "/build/Release/" + os.path.basename(p))
|
||
total += os.path.getsize(p)
|
||
for p in tracked:
|
||
if not os.path.exists(p):
|
||
print(" (skipping tracked-but-absent: %s)" % p)
|
||
continue
|
||
z.write(p, name + "/" + p)
|
||
total += os.path.getsize(p)
|
||
|
||
zsz = os.path.getsize(zpath)
|
||
print("wrote %s" % zpath)
|
||
print(" %d files, %.1f MB raw -> %.1f MB zipped"
|
||
% (len(bats) + len(exes) + len(tracked), total / 1048576.0,
|
||
zsz / 1048576.0))
|
||
|
||
# Crash-forensics: archive this build's PDB next to the zip (operator-side
|
||
# only, never shipped) so the [crash] btl4+0xNNNN offsets in player logs
|
||
# stay resolvable per distributed version.
|
||
pdb = "build/Release/btl4.pdb"
|
||
if os.path.exists(pdb):
|
||
import shutil
|
||
pdb_dest = zpath[:-4] + ".pdb"
|
||
shutil.copyfile(pdb, pdb_dest)
|
||
print(" symbols archived: %s" % pdb_dest)
|
||
|
||
if __name__ == "__main__":
|
||
main()
|