#!/usr/bin/env python3 """Slice the client's Player.log by wall-clock window and summarise it. The Unity log is one flat file per launch whose lines start with `[HH:MM:SS] [frame] [seconds] [Level] [Tag] ...`; anything that matters after a test is usually a handful of error lines inside a one-minute window. Usage: python tools/player-log-window.py --from 14:21 --to 14:23 [--errors] python tools/player-log-window.py --grep "studio|rrs" --from 14:22:30 python tools/player-log-window.py --log --from ... --to ... --show Options: --errors dedupe and count Error/Warning/Exception lines instead of printing all --show print every matching line (default when a --grep matches) --tail N additionally print the last N raw lines of the file """ import argparse import os import re import sys DEFAULT_LOG = os.path.join( os.environ.get("LOCALAPPDATA", ""), "..", "LocalLow", "Against Gravity", "Rec Room", "Player.log") LINE_RE = re.compile(r"^\[(\d\d:\d\d:\d\d(?:\.\d+)?)\]") def pad(value, tail): """`14:22` means the whole minute, `14:22:30` the whole second.""" if not value: return None return value if value.count(":") == 2 else value + tail def in_window(stamp, start, stop): if start and stamp < start: return False if stop and stamp > stop: return False return True def main(argv): parser = argparse.ArgumentParser() parser.add_argument("--log", default=os.path.normpath(DEFAULT_LOG)) parser.add_argument("--from", dest="start") parser.add_argument("--to", dest="stop") parser.add_argument("--grep") parser.add_argument("--errors", action="store_true") parser.add_argument("--show", action="store_true") parser.add_argument("--tail", type=int, default=0) args = parser.parse_args(argv[1:]) with open(args.log, "rb") as handle: blob = handle.read().decode("utf-8", "replace") lines = blob.split("\n") print(f"{args.log}: {len(lines)} lines, {len(blob)} bytes") pattern = re.compile(args.grep, re.I) if args.grep else None selected = [] for text in lines: match = LINE_RE.match(text) if not match: continue if not in_window(match.group(1), pad(args.start, ":00"), pad(args.stop, ":59")): continue if pattern and not pattern.search(text): continue if args.errors and not re.search(r"\[(Error|Warning)\]|Exception:|Error:", text): continue selected.append(text.rstrip()) if args.errors or not (args.show or pattern): counts = {} order = [] for text in selected: key = re.sub(r"\d+", "#", text[:220]) if key not in counts: counts[key] = 0 order.append(key) counts[key] += 1 for key in order: print(f" x{counts[key]:<4} {key}") else: for text in selected: print(" " + text[:300]) print(f"--- {len(selected)} selected line(s)") if args.tail: print(f"--- last {args.tail} lines ---") for text in [t for t in lines if t.strip()][-args.tail:]: print(" " + text[:300]) return 0 if __name__ == "__main__": sys.exit(main(sys.argv))