Anchor scans to page 1; export stable prefixes

Anchor page-run selection to start at page 1 (select_continuous_run_from_page_1) instead of picking the longest run, so newest pages are preserved and the newest timestamp ordinal 0 is always captured. Change boundary logic to always assign stable UIDs and export the captured prefix of an oldest timestamp group even when it may be an unfinished 10-pull, emitting informational warnings (INCOMPLETE_TIMESTAMP_GROUP_EXPORTED / INCOMPLETE_ARC_10_PULL_EXPORTED) instead of dropping rows. Apply the same policy to Arc groups and update arc group annotation and warnings accordingly. Add a new console module for improved CLI output and update cli, live_capture, adapters, and session code to use the new run selection and console helpers. Update docs, mappings, and tests to reflect the new boundary/export behavior and messaging.
This commit is contained in:
Golumpa 2026-06-11 12:22:34 +01:00
parent 0a1a19939b
commit 20b2086299
14 changed files with 402 additions and 200 deletions

View file

@ -7,9 +7,10 @@ import time
from datetime import datetime
from pathlib import Path
from nte_history_exporter import console
from nte_history_exporter.constants import POOL_META
from nte_history_exporter.decoder.arc import arc_stability_warnings, select_continuous_arc_run
from nte_history_exporter.decoder.boundary import annotate_groups, page_gap_warnings
from nte_history_exporter.decoder.arc import arc_stability_warnings
from nte_history_exporter.decoder.boundary import annotate_groups, select_continuous_run_from_page_1
from nte_history_exporter.export.csv_export import write_csv
from nte_history_exporter.export.json_export import build_export_json
from nte_history_exporter.live_capture.session import LiveHistorySession, UdpPacket
@ -52,8 +53,7 @@ def run_live_capture(
session = LiveHistorySession(local_ip)
sock = open_raw_udp_socket(local_ip)
print(f"Device 0 ready~! Listening on {local_ip}")
print("Open either Monopoly history board. Capture will stay open until you press any key.")
console.print_live_instructions(local_ip)
try:
for packet in read_packets(sock):
@ -75,7 +75,7 @@ def run_live_capture(
if matched:
kind = session.pairs[-1][7] if session.pairs else "permanent"
label = POOL_META.get(kind, POOL_META["permanent"])["name"]
print(f"Captured {label} page {session.last_page_seen}")
console.print_page_captured(label, session.last_page_seen)
finally:
try:
sock.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)
@ -86,14 +86,13 @@ def run_live_capture(
exports = []
for kind in session.kinds_seen():
pairs = session.pairs_for_kind(kind)
best_run = session.best_run(kind)
best_run, run_warnings = select_continuous_run_from_page_1(pairs)
rows = session.build_rows(kind)
if kind == "arc_miracle_box":
_arc_run, gap_warnings = select_continuous_arc_run(pairs)
warnings = gap_warnings + arc_stability_warnings(rows)
warnings = run_warnings + arc_stability_warnings(rows)
else:
rows, warnings = annotate_groups(rows, starts_from_page_1=bool(best_run and best_run[0][0] == 1))
warnings = page_gap_warnings(pairs, best_run) + warnings
rows, group_warnings = annotate_groups(rows)
warnings = run_warnings + group_warnings
pages_seen = [p[0] for p in best_run]
csv_path, json_path = export_paths(kind)
if write_debug_csv:
@ -116,21 +115,40 @@ def run_live_capture(
}
)
console.print_results_header()
if not exports:
print("No Monopoly history pages were captured.")
console.print_problem("No history pages were captured.")
console.print_note("This tool must already be running when you press Start on the")
console.print_note("game's main menu. Log out to the main menu, enter the game")
console.print_note("again, then reopen the history screen.")
return {"exports": []}
if copy_clipboard and len(exports) == 1:
payload = exports[0]["payload"]
copy_to_clipboard(payload)
print("Export copied to clipboard.")
elif len(exports) > 1:
print("Multiple banners captured; clipboard copy skipped.")
all_warnings = []
for item in exports:
scan = item["export"]["scan"]
console.print_export_summary(
item["export"]["banner"]["name"],
scan["decoded_records"],
scan["exported_records"],
scan["skipped_records"],
)
for warning in scan["warnings"]:
console.print_warning(warning["code"], warning["reason"], warning.get("records"))
all_warnings.extend(scan["warnings"])
console.maybe_print_incomplete_hint(all_warnings)
print()
for item in exports:
if item["csv_path"] is not None:
print(f"CSV written: {item['csv_path']}")
print(f"Export written: {item['json_path']}")
console.print_note(f"CSV written: {item['csv_path']}")
console.print_note(f"Export written: {item['json_path']}")
if copy_clipboard and len(exports) == 1:
copy_to_clipboard(exports[0]["payload"])
console.print_success("Export copied to clipboard - paste it straight into your tracker.")
elif len(exports) > 1:
console.print_note("Multiple banners captured; clipboard copy skipped so one export")
console.print_note("does not overwrite another.")
return {"exports": exports}

View file

@ -11,7 +11,7 @@ from nte_history_exporter.decoder.arc import (
parse_arc_response,
select_continuous_arc_run,
)
from nte_history_exporter.decoder.boundary import longest_monotonic_page_run
from nte_history_exporter.decoder.boundary import select_continuous_run_from_page_1
from nte_history_exporter.decoder.protocol import (
history_request_kind,
is_history_request,
@ -147,7 +147,5 @@ class LiveHistorySession:
def best_run(self, kind: str | None = None) -> list[tuple]:
pairs = self.pairs_for_kind(kind) if kind else self.pairs
if kind == "arc_miracle_box":
best_run, _warnings = select_continuous_arc_run(pairs)
return best_run
return longest_monotonic_page_run(pairs)
best_run, _warnings = select_continuous_run_from_page_1(pairs)
return best_run