nte-exporter/src/nte_history_exporter/console.py
2026-08-19 13:00:09 +01:00

239 lines
8 KiB
Python

from __future__ import annotations
import os
import sys
from nte_history_exporter.constants import EXPORTER_VERSION, GAME_NAME
from nte_history_exporter.decoder.server_region import SERVER_REGIONS
from nte_history_exporter.live_capture.stop_key import wait_for_keypress
WIDTH = 58
REPOSITORY_URL = "https://github.com/Golumpa/nte-exporter"
RESET = "\x1b[0m"
BOLD = "\x1b[1m"
DIM = "\x1b[2m"
CYAN = "\x1b[36m"
GREEN = "\x1b[32m"
RED = "\x1b[31m"
YELLOW = "\x1b[33m"
BRIGHT_WHITE = "\x1b[97m"
_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} history and achievements -> JSON", DIM))
print(style(f" {REPOSITORY_URL}", DIM))
heart = "❤️" if (getattr(sys.stdout, "encoding", None) or "").lower().replace("-", "") == "utf8" else "<3"
print(style(" Created with ", DIM) + style(heart, RED) + style(" by Golumpa", DIM))
print(rule("="))
def print_live_instructions(local_ip: str, backend: str = "windows_raw", detail: str = "") -> None:
print()
print(style(" Listening on ", DIM) + style(local_ip, BOLD))
backend_detail = f" ({detail})" if detail and detail != local_ip else ""
print(style(f" Capture backend: {backend}{backend_detail}", DIM))
print()
print(style(" Before you begin", BOLD))
print(" Start this exporter before pressing Start on the game's")
print(" main menu. This lets it capture your achievements and")
print(" detect your account UID and server automatically.")
print()
print(" Already in game? Pull history still works, but achievement")
print(" capture requires restarting the game. Missing account details")
print(" will be requested when your exports are saved.")
print()
print(style(" Pull history", BOLD))
print(" Open a supported history screen, start at page 1, then")
print(" scroll through every page you want to export. Go one page")
print(" further so the exporter can confirm the final pull group.")
print()
print(style(" Monopoly:", DIM) + " Standard Board or Limited Character Board")
print(style(" Gashapon:", DIM) + " Arc Miracle Box or Mystery Box")
print(style(" You can capture more than one board in the same session.", DIM))
print()
print(style(" Achievements", BOLD))
print(" Your tracked achievements are captured automatically when")
print(" you log in after following the step above.")
print()
print(style(" Ready - use the game, then press any key here when finished.", BOLD, GREEN))
print(rule())
def print_page_captured(label: str, page: int | None, *, recaptured: bool = False) -> None:
action = "recaptured" if recaptured else "page"
print(style(" + ", GREEN, BOLD) + label + style(f" {action} {page}", DIM))
def print_achievements_captured(
in_game_completed: int,
in_game_in_progress: int,
playstation_completed: int,
playstation_in_progress: int,
) -> None:
print(style(" + ", GREEN, BOLD) + "Achievements captured")
print(
style(
f" In-game {in_game_completed} completed, "
f"{in_game_in_progress} in progress",
DIM,
)
)
print(
style(
f" PlayStation {playstation_completed} completed, "
f"{playstation_in_progress} in progress",
DIM,
)
)
def print_missing_pages(label: str, pages: list[int]) -> None:
page_list = ", ".join(str(page) for page in pages)
print(style(" ! ", YELLOW, BOLD) + style(f"{label} missing page(s): {page_list}.", YELLOW, BOLD))
print(
style(
" Close and reopen this history board, then scroll down until page gap recovered is shown.",
BRIGHT_WHITE,
BOLD,
)
)
def print_page_gap_recovered(label: str) -> None:
print(style(" + ", GREEN, BOLD) + style(f"{label} page gap recovered.", GREEN, BOLD))
def print_capture_stats(received: int, dropped: int, interface_dropped: int) -> None:
text = (
f"Capture stats: processed {received}, buffer dropped {dropped}, "
f"interface dropped {interface_dropped}"
)
if dropped or interface_dropped:
print(style(f" ! {text}", YELLOW, BOLD))
else:
print(style(f" {text}", DIM))
def print_capture_fallback(reason: str) -> None:
print(style(" ! Npcap unavailable; using Windows raw capture.", YELLOW))
print(style(f" {reason}", DIM))
print(style(" Raw capture may require running this terminal as Administrator.", 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_update_available(current_version: str, latest_version: str, release_url: str) -> None:
print()
print(style(f" Update available: v{current_version} -> {latest_version}", YELLOW, BOLD))
print(style(" Some reward or achievement mappings may be incomplete in this version.", YELLOW))
print(style(" Updating is recommended for the most accurate export labels.", YELLOW))
print(style(f" Download when ready: {release_url}", YELLOW))
def prompt_user_uid() -> str | None:
print()
print_problem("User UID was not detected in this capture.")
print_note("Enter your NTE user UID so the export can be named and linked correctly.")
print_note("Leaving this blank may prevent import on some trackers.")
value = input(" User UID: ").strip()
return value or None
def prompt_server_id() -> str | None:
print()
print_problem("Account server was not detected in this capture.")
print_note("Choose the server used by this account:")
choices = tuple(
(str(index), server_id, details["name"])
for index, (server_id, details) in enumerate(SERVER_REGIONS.items(), start=1)
)
for key, _server_id, name in choices:
print(f" {key}. {name}")
print_note("Leave this blank to omit server information.")
by_key = {key: server_id for key, server_id, _name in choices}
while True:
value = input(" Server [1-4]: ").strip()
if not value:
return None
if value in by_key:
return by_key[value]
print_problem("Enter 1, 2, 3, or 4; or leave it blank.")
def wait_for_close() -> None:
print()
print_success("Press any key to close the exporter.")
wait_for_keypress()