"""Reproduce Gitea #51 -- the coolant flush sound loops forever (SAURON, night 3). Uses the built-in scripted flush (BT_FLUSH_TEST=): holds the flush for ~60 sim frames, then releases. Instruments all four layers of the chain so the stuck stage is identifiable rather than guessed: [flush-tx] the input dispatch edge (mech4.cpp:5566 -- PRESS / RELEASE pair) [flush] the reservoir drain (heatfamily_reslice.cpp:772, while alarm==1) [aud-tail] OpenAL PLAY / STOP per source, with the LoopAtWill flag + release time [audio] PlayNote / StopNote calls PASS = one PRESS + one RELEASE, the [flush] drain STOPS after the release, and every CoolantDump source that got a PLAY also gets a STOP. A stuck sound shows up as either repeated PLAY lines with no STOP (per-frame re-trigger -- PlayNote restarts any source not currently AL_PLAYING) or a PLAY with no matching STOP at all. Runs well past the release so a late re-trigger is visible. Kills only its own PID. """ import os import re import subprocess import sys import time REPO = r"C:\git\bt411" LOG = os.path.join(REPO, "scratchpad_flushsnd.log") if os.path.exists(LOG): os.remove(LOG) env = dict(os.environ) env.update({ "BT_START_INSIDE": "1", "BT_DEV_GAUGES": "1", "BT_FLUSH_TEST": "300", # press at sim frame 300, release 60 frames later "BT_FLUSH_LOG": "1", "BT_AUD_TAIL": "1", "BT_AUDIO_LOG": "1", "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) try: deadline = time.time() + 260 released = False while time.time() < deadline: if os.path.exists(LOG): t = open(LOG, errors="replace").read() if "[flush-tx] InjectCoolant RELEASE" in t: released = True break time.sleep(2) # keep running well past the release -- a late re-trigger is the whole point time.sleep(20) finally: proc.terminate() time.sleep(1) t = open(LOG, errors="replace").read() if os.path.exists(LOG) else "" tx = re.findall(r"^\[flush-tx\].*$", t, re.M) drain = re.findall(r"^\[flush\].*$", t, re.M) tail = re.findall(r"^\[aud-tail\].*$", t, re.M) # pair PLAY/STOP per source, and flag which sources carry the LoopAtWill flag plays, stops, loopy = {}, {}, set() for l in tail: m = re.search(r"(PLAY|STOP) src=(\d+).*?file=(\S+)", l) if not m: continue kind, src, f = m.group(1), m.group(2), m.group(3) (plays if kind == "PLAY" else stops).setdefault((src, f), 0) (plays if kind == "PLAY" else stops)[(src, f)] += 1 if kind == "PLAY" and "loop=1" in l: loopy.add((src, f)) print("\n=============== RESULT ===============") print("reached the release edge:", released) print("\n[flush-tx] dispatch edges:", len(tx)) for l in tx: print(" ", l[:120]) print("\n[flush] drain samples:", len(drain), "(should stop at the release)") for l in drain[:4]: print(" ", l[:120]) if len(drain) > 4: print(" ... last:", drain[-1][:120]) print("\nOpenAL source pairing (coolant layers only):") allk = sorted(set(plays) | set(stops)) unbalanced = [] for k in allk: if "Coolant" not in k[1] and "coolant" not in k[1]: continue p, s = plays.get(k, 0), stops.get(k, 0) flag = " LoopAtWill" if k in loopy else "" print(" src=%-5s %-28s PLAY=%d STOP=%d%s" % (k[0], k[1], p, s, flag)) if p > s: unbalanced.append((k, p, s)) print("\nTotal [aud-tail] lines:", len(tail)) if unbalanced: print("\n*** UNBALANCED (sound left running / re-triggered):") for k, p, s in unbalanced: print(" %s %s PLAY=%d STOP=%d" % (k[0], k[1], p, s)) verdict = ("REPRODUCED -- a coolant source plays more than it stops" if unbalanced else ("clean pairing this run -- flush start/stop balanced" if tail else "NO [aud-tail] output -- audio path never fired (check the rig)")) print("\nVERDICT:", verdict) sys.exit(0)