239 lines
8 KiB
Python
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()
|