"""Gitea #51 part 2 -- catch the STUCK looping coolant source red-handed. Part 1 (flushsnd.py) showed the flush picks one of five CoolantDump variants and that the note-off does arrive. But three of the five variants contain a zone authored **LoopAtWill**, and SetupPatch (L4AUDLVL.cpp:290-296) maps anything that is not ForceStatic to **AL_LOOPING = 1** -- an OpenAL source that never ends on its own. The enum's own comment says LoopAtWill means "will play once OR loop as desired" (L4AUDLVL.h:16), i.e. permission to loop, not a command. So: drive several flushes (variant selection differs per trigger), then sit idle and use the engine's own BT_AUDIO_DUMP once-a-second playing-source dump to see whether anything is STILL playing with loop=1 long after the last release. PASS (bug absent) = no [playing] lines with loop=1 after the idle period. FAIL (bug present) = a coolant source still playing with loop=1 forever. Kills only the PID it spawns. """ import ctypes import os import re import subprocess import sys import time KEYUP = 0x0002 VK_H = 0x48 user32 = ctypes.windll.user32 REPO = r"C:\git\bt411" LOG = os.path.join(REPO, "scratchpad_flushsnd2.log") if os.path.exists(LOG): os.remove(LOG) env = dict(os.environ) env.update({ "BT_START_INSIDE": "1", "BT_KEY_NOFOCUS": "1", "BT_DEV_GAUGES": "1", "BT_FLUSH_LOG": "1", "BT_AUD_TAIL": "1", "BT_AUDIO_DUMP": "1", # once-a-second dump of every PLAYING source + its loop flag "BT_LOG": LOG, }) proc = subprocess.Popen( [os.path.join(REPO, "build", "Release", "btl4.exe"), "-egg", "LAST.EGG"], cwd=os.path.join(REPO, "content"), env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) print("pid", proc.pid) def tap(hold=0.35): user32.keybd_event(VK_H, 0, 0, 0) time.sleep(hold) user32.keybd_event(VK_H, 0, KEYUP, 0) try: deadline = time.time() + 240 while time.time() < deadline: if os.path.exists(LOG) and "first frame" in open(LOG, errors="replace").read(): break time.sleep(2) else: print("FAIL: never reached the mission") proc.terminate() sys.exit(2) time.sleep(6) # six flushes, spaced -- the variant selection differs per trigger for i in range(6): tap() print(" flush %d dispatched" % (i + 1)) time.sleep(3.0) print(" idling 25s -- anything still playing now is STUCK") idle_mark = len(open(LOG, errors="replace").read()) time.sleep(25) finally: proc.terminate() time.sleep(1) t = open(LOG, errors="replace").read() if os.path.exists(LOG) else "" tail_t = t[idle_mark:] if "idle_mark" in dir() else t tx = re.findall(r"^\[flush-tx\].*$", t, re.M) variants = sorted(set(re.findall(r"file=(CoolantDump\d+_z\d+\.wav)", t))) # playing-source dumps recorded DURING the idle window (post-release) idle_play = re.findall(r"^\[playing\].*$", tail_t, re.M) stuck = [l for l in idle_play if "loop=1" in l] stuck_coolant = [l for l in stuck if "oolant" in l] print("\n=============== RESULT ===============") print("[flush-tx] edges:", len(tx), "(expect 12 = 6 press + 6 release)") print("coolant zones that actually played:", len(variants)) for v in variants: print(" ", v) print("\n[playing] lines during the 25s IDLE window:", len(idle_play)) seen = {} for l in idle_play: m = re.search(r"src=(\d+) (\S+).*loop=(\d+)", l) if m: seen[(m.group(1), m.group(2), m.group(3))] = seen.get((m.group(1), m.group(2), m.group(3)), 0) + 1 for (src, nm, lp), n in sorted(seen.items(), key=lambda kv: -kv[1]): print(" src=%-4s %-28s loop=%s seen %dx" % (src, nm, lp, n)) print("\nSTILL PLAYING with loop=1 after all releases:", len(stuck)) print(" ...of which coolant:", len(stuck_coolant)) for l in stuck_coolant[:5]: print(" ", l[:120]) print("\nVERDICT:", "REPRODUCED -- a looping coolant source outlives its release (#51)" if stuck_coolant else ("looping sources stuck, but not coolant -- same class, different sample" if stuck else "no stuck looping source in this run")) sys.exit(0)