"""Deterministically digest the tester logs into compact per-session event timelines. The logs are 1.4-5.8 MB each and mostly per-frame telemetry. Everything that matters for the ghost/desync triage is a small set of tagged event lines. This extracts them in order (so causality survives) and counts the rest, turning each log into a few KB an agent can reason over in one read instead of 90 greps. """ import os import re import sys DIR = os.path.dirname(os.path.abspath(__file__)) FILES = { "vex_LEGION.log": "VexUbiquity (LEGION)", "elengil_ALIA.log": "Elengil (ALIA)", "rajel_GAMERSLAB.log": "RajelAran (GAMERSLAB)", "sauron_XIAOLONG.log": "SAURON (XIAOLONG) -- HOST of match 2", "connman_MSFIREFLY.log": "Conn Man (MS-FIREFLY)", } # Ordered-timeline events: anything bearing on death, respawn, entity identity, # replication, or a reported symptom. KEEP = re.compile( r"\[respawn\]|\[damage\]|\[deathfx\]|\[paint\]|DESTROYED|SWALLOWED|RECOVERED" r"|\[matchlog\]|PLAYER_LINK|\[score\]|\[kd\]|KILL|DEATH" r"|\[steamnet\]|PEER|ROUTE|HELLO|JOIN|join|LOBBY|relay" r"|MakeMessage|make message|entity create|CreateEntity|DynamicEntity|interest" r"|collision volume|CollisionVolume|\[warp\]|translocat|drop zone|dropzone" r"|WARNING|ERROR|FAIL|Fail\(|assert|Assert|crash|EXCEPTION" r"|acquireFails|source census|pool exhaust|DROP transient" r"|\[gimp|limp|gaitSM state=2[2-7]|\[exp\]|\[tick\]|BUG \(Gitea" r"|SESSION build=", re.I) # High-frequency telemetry we only COUNT (never list). NOISE = re.compile(r"\[syncF\]|\[gaitSM\]|\[drive\]|\[mppr|\[spatial\]|\[audio\]|\[startreq\]" r"|\[mrec-rx\]|\[rx0\]|\[wire\]|\[gait\]|\[target\]|\[zone-armor\]" r"|\[animind\]|\[seqev\]|SetupPatch", re.I) MAXPERSESS = 260 # ordered events per session (last sessions get the budget) def digest(path, who): raw = open(path, encoding="latin-1", errors="replace").read().splitlines() # split into sessions bounds = [i for i, l in enumerate(raw) if "BT411 SESSION build=" in l] if not bounds: bounds = [0] bounds.append(len(raw)) out = [f"##### {who} ({os.path.basename(path)}, {len(raw)} lines, " f"{len(bounds)-1} sessions)"] sess = list(zip(bounds, bounds[1:])) # only the last 3 sessions in detail; earlier ones get one summary line for idx, (s, e) in enumerate(sess): head = raw[s][:150] if s < len(raw) else "" if idx < len(sess) - 3: out.append(f"-- session {idx+1}: {head} [{e-s} lines, skipped: pre-match]") continue out.append("") out.append(f"===== session {idx+1} of {len(sess)} lines {s+1}-{e} {head}") kept, noise = [], 0 for i in range(s, e): l = raw[i] if NOISE.search(l): noise += 1 continue if KEEP.search(l): kept.append(f" {i+1}: {l[:190]}") if len(kept) > MAXPERSESS: half = MAXPERSESS // 2 kept = kept[:half] + [f" ... {len(kept)-MAXPERSESS} events elided ..."] + kept[-half:] out.extend(kept) out.append(f" [telemetry lines suppressed in this session: {noise}]") return "\n".join(out) total = 0 for f, who in FILES.items(): p = os.path.join(DIR, f) if not os.path.exists(p): print("MISSING", p) continue d = digest(p, who) o = os.path.join(DIR, "digest_" + f.replace(".log", ".txt")) open(o, "w", encoding="utf-8").write(d) total += len(d) print("%-34s %7.1f KB -> %6.1f KB (%s)" % ( f, os.path.getsize(p)/1024, len(d)/1024, os.path.basename(o))) print("total digest size: %.1f KB" % (total/1024))