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.
155 lines
4.6 KiB
Python
155 lines
4.6 KiB
Python
from __future__ import annotations
|
|
|
|
import os
|
|
import sys
|
|
|
|
from nte_history_exporter.constants import EXPORTER_VERSION, GAME_NAME
|
|
|
|
WIDTH = 58
|
|
|
|
RESET = "\x1b[0m"
|
|
BOLD = "\x1b[1m"
|
|
DIM = "\x1b[2m"
|
|
CYAN = "\x1b[36m"
|
|
GREEN = "\x1b[32m"
|
|
YELLOW = "\x1b[33m"
|
|
|
|
_ansi: bool | None = None
|
|
|
|
|
|
def _enable_ansi() -> bool:
|
|
if not hasattr(sys.stdout, "isatty") or not sys.stdout.isatty():
|
|
return False
|
|
if os.name != "nt":
|
|
return True
|
|
try:
|
|
import ctypes
|
|
|
|
kernel32 = ctypes.windll.kernel32
|
|
handle = kernel32.GetStdHandle(-11)
|
|
mode = ctypes.c_uint32()
|
|
if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
|
|
return False
|
|
ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
|
return bool(kernel32.SetConsoleMode(handle, mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING))
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def ansi_enabled() -> bool:
|
|
global _ansi
|
|
if _ansi is None:
|
|
_ansi = _enable_ansi()
|
|
return _ansi
|
|
|
|
|
|
def style(text: str, *codes: str) -> str:
|
|
if not codes or not ansi_enabled():
|
|
return text
|
|
return "".join(codes) + text + RESET
|
|
|
|
|
|
def rule(char: str = "-") -> str:
|
|
return style(char * WIDTH, DIM)
|
|
|
|
|
|
def print_banner() -> None:
|
|
print()
|
|
print(rule("="))
|
|
print(style(f" NTE History Exporter v{EXPORTER_VERSION}", BOLD, CYAN))
|
|
print(style(f" {GAME_NAME} pull history -> tracker JSON", DIM))
|
|
print(rule("="))
|
|
|
|
|
|
def print_live_instructions(local_ip: str) -> None:
|
|
print()
|
|
print(style(" Listening on ", DIM) + style(local_ip, BOLD))
|
|
print()
|
|
print(style(" How to export your pull history", BOLD))
|
|
print(" 1. This tool must be running BEFORE you press Start on")
|
|
print(" the game's main menu, or the game connection cannot")
|
|
print(" be captured. Already in game? Log out to the main")
|
|
print(" menu and enter again.")
|
|
print(" 2. Open a supported history screen:")
|
|
print(style(" Monopoly > Standard Board history", CYAN))
|
|
print(style(" Monopoly > Limited Character Board history", CYAN))
|
|
print(style(" Gashapon > Arc Miracle Box history", CYAN))
|
|
print(" 3. Start at page 1 and scroll down through every page")
|
|
print(" you want exported.")
|
|
print(" 4. Scroll one page past where you plan to stop so the")
|
|
print(" last pull group can be confirmed as complete.")
|
|
print(" 5. You can open several boards in the same session.")
|
|
print()
|
|
print(style(" Waiting for history pages... press any key here when done.", BOLD, GREEN))
|
|
print(rule())
|
|
|
|
|
|
def print_page_captured(label: str, page: int | None) -> None:
|
|
print(style(" + ", GREEN, BOLD) + label + style(f" page {page}", DIM))
|
|
|
|
|
|
def print_results_header() -> None:
|
|
print()
|
|
print(rule())
|
|
print(style(" Results", BOLD))
|
|
print(rule())
|
|
|
|
|
|
def print_export_summary(name: str, decoded: int, exported: int, skipped: int) -> None:
|
|
counts = [
|
|
style(f"decoded {decoded}", DIM),
|
|
style(f"exported {exported}", GREEN, BOLD),
|
|
style(f"skipped {skipped}", YELLOW if skipped else DIM),
|
|
]
|
|
print(f" {name:<30}" + " ".join(counts))
|
|
|
|
|
|
def print_warning(code: str, reason: str, records: int | None = None) -> None:
|
|
suffix = f" ({records} records)" if records is not None else ""
|
|
print(style(f" ! {code}: {reason}{suffix}", YELLOW))
|
|
|
|
|
|
def print_note(text: str) -> None:
|
|
print(style(f" {text}", DIM))
|
|
|
|
|
|
def print_success(text: str) -> None:
|
|
print(style(f" {text}", GREEN, BOLD))
|
|
|
|
|
|
def print_problem(text: str) -> None:
|
|
print(style(f" {text}", YELLOW, BOLD))
|
|
|
|
|
|
def print_boxed(lines: list[str], *codes: str) -> None:
|
|
inner = WIDTH - 4
|
|
border = " +" + "-" * (WIDTH - 2) + "+"
|
|
print(style(border, *codes))
|
|
for line in lines:
|
|
print(style(" | " + line.ljust(inner) + " |", *codes))
|
|
print(style(border, *codes))
|
|
|
|
|
|
# Warning codes for an oldest timestamp group that was exported despite possibly
|
|
# being an unfinished 10-pull. The exported rows are stable, so the warning is
|
|
# informational and the user can ignore it once the rest is captured.
|
|
INCOMPLETE_EXPORT_CODES = {
|
|
"INCOMPLETE_TIMESTAMP_GROUP_EXPORTED",
|
|
"INCOMPLETE_ARC_10_PULL_EXPORTED",
|
|
}
|
|
|
|
|
|
def maybe_print_incomplete_hint(warnings: list[dict]) -> None:
|
|
if not any(w.get("code") in INCOMPLETE_EXPORT_CODES for w in warnings):
|
|
return
|
|
print()
|
|
print_boxed(
|
|
[
|
|
"The INCOMPLETE_..._EXPORTED warning above is safe",
|
|
"to ignore if the partial 10-pull it captured is",
|
|
"already in your tracker. Otherwise restart",
|
|
"the capture and go to a further page",
|
|
],
|
|
GREEN,
|
|
BOLD,
|
|
)
|