Files
BT411/tools/mkdist.py
T
arcattackandClaude Opus 4.8 c2ee7299b2 Generic joystick / HOTAS / pedals support: DirectInput 8 layer + capture wizard
Tester ask: configure non-Xbox controllers.  New L4JOY layer (DI8):
enumerates every non-XInput game device (up to 4), DIJOYSTATE2 polling
with range normalization, reacquire-on-loss, 3s hot-plug re-enum.
XInput devices are excluded via the documented RawInput IG_ VID/PID
check (live-verified skipping a real XBOX360 pad -- no double-feed).

bindings.txt grows joydev/joyaxis/joybutton/joyhat rows (device slots
by name substring or ordinal; axes X..RZ/SL0/SL1 with invert/slew/
deadzone; hats as 4-direction buttons with diagonal chords).  PadRIO
runs them through the existing channel/event machinery -- per-binding
edge arrays release automatically on detach (the #24 rule), and a
direct throttle axis maps full lever travel onto the 0..1 channel
while the gait detent still snaps a lever parked in the walk/run dead
band.

BT_JOYCONFIG=1 (joyconfig.bat) runs a console capture wizard: move
each control when prompted (stick/twist/throttle/fire), invert derived
from the move direction, hat look cluster auto-added, output written
between markers in bindings.txt with the rest of the file preserved.

Legacy L4DINPUT DIJoystick (L4CONTROLS=DIJOYSTICK) untouched.
Verified: grammar accept/reject per line, XInput-exclusion live,
30s in-mission no-device polling clean.  Awaiting a tester with real
stick hardware for axis verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:36:32 -05:00

147 lines
6.4 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)
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))
if __name__ == "__main__":
main()