check_names: add --fix-ids to repair wrong steam_ids in meta.cpp
ID_COLLISION status (previously NOT_ON_SERVER) now has its own label and shows the bad steam_id in the report output. --fix-ids cross-references comparison.json to find the correct publishedid for each colliding folder and rewrites it in-place in meta.cpp using regex, preserving all other content. New helpers: _lookup_detailed (returns server_name + local_sid tuple), _write_steam_id (regex-replace publishedid in meta.cpp), _build_comparison_id_map (builds normalized_name -> steam_id map from comparison.json). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
257
check_names.py
257
check_names.py
@@ -11,17 +11,21 @@ Status codes:
|
|||||||
OK disk name matches server name exactly
|
OK disk name matches server name exactly
|
||||||
MISMATCH disk name differs from server canonical name
|
MISMATCH disk name differs from server canonical name
|
||||||
NOT_ON_SERVER server has no folder for this mod (new mod? typo?)
|
NOT_ON_SERVER server has no folder for this mod (new mod? typo?)
|
||||||
|
ID_COLLISION local meta.cpp has wrong steam_id (belongs to a different mod)
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python check_names.py # report only
|
python check_names.py # report only
|
||||||
python check_names.py --fix # rename + fix junctions
|
python check_names.py --fix # rename mismatched folders + fix junctions
|
||||||
python check_names.py --group shared --fix
|
python check_names.py --fix-ids # fix wrong steam_ids in local meta.cpp files
|
||||||
|
python check_names.py --fix --fix-ids # both
|
||||||
|
python check_names.py --group shared --fix-ids
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import json
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -29,7 +33,7 @@ from arma_modlist_tools.compat import fix_console_encoding
|
|||||||
from arma_modlist_tools.config import load_config
|
from arma_modlist_tools.config import load_config
|
||||||
from arma_modlist_tools.fetcher import (
|
from arma_modlist_tools.fetcher import (
|
||||||
_normalize_name, _parse_meta_cpp,
|
_normalize_name, _parse_meta_cpp,
|
||||||
build_server_index, make_session,
|
build_server_index,
|
||||||
)
|
)
|
||||||
from arma_modlist_tools.linker import _is_junction, create_junction, remove_junction
|
from arma_modlist_tools.linker import _is_junction, create_junction, remove_junction
|
||||||
|
|
||||||
@@ -37,21 +41,21 @@ fix_console_encoding()
|
|||||||
|
|
||||||
# Column widths
|
# Column widths
|
||||||
W_DISK = 44
|
W_DISK = 44
|
||||||
W_SERVER = 44
|
W_SERVER = 50
|
||||||
W_GROUP = 24
|
W_GROUP = 24
|
||||||
|
|
||||||
|
_PUBLISHEDID_RE = re.compile(r"(publishedid\s*=\s*)\d+(\s*;)", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Helpers
|
# Helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _server_name_from_url(url: str) -> str:
|
def _server_name_from_url(url: str) -> str:
|
||||||
"""Extract the folder name from a trailing-slash server URL."""
|
|
||||||
return url.rstrip("/").split("/")[-1]
|
return url.rstrip("/").split("/")[-1]
|
||||||
|
|
||||||
|
|
||||||
def _read_local_steam_id(mod_dir: Path) -> str | None:
|
def _read_local_steam_id(mod_dir: Path) -> str | None:
|
||||||
"""Try to read publishedid from meta.cpp inside a local mod folder."""
|
|
||||||
for candidate in (mod_dir / "meta.cpp", mod_dir / "Meta.cpp"):
|
for candidate in (mod_dir / "meta.cpp", mod_dir / "Meta.cpp"):
|
||||||
if candidate.exists():
|
if candidate.exists():
|
||||||
try:
|
try:
|
||||||
@@ -61,28 +65,35 @@ def _read_local_steam_id(mod_dir: Path) -> str | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _lookup_server_name(local_name: str, mod_dir: Path, index: dict) -> str | None:
|
def _lookup_detailed(
|
||||||
|
local_name: str,
|
||||||
|
mod_dir: Path,
|
||||||
|
index: dict,
|
||||||
|
) -> tuple[str | None, str | None]:
|
||||||
"""
|
"""
|
||||||
Return the server's canonical folder name for this local mod, or None.
|
Return (server_name, local_steam_id_used).
|
||||||
Tries steam_id first, then normalized name.
|
|
||||||
"""
|
|
||||||
# 1. Steam ID from local meta.cpp (most reliable)
|
|
||||||
steam_id = _read_local_steam_id(mod_dir)
|
|
||||||
if steam_id:
|
|
||||||
url = index["by_steam_id"].get(steam_id)
|
|
||||||
if url:
|
|
||||||
return _server_name_from_url(url)
|
|
||||||
|
|
||||||
# 2. Normalized name fallback
|
- server_name: canonical server folder name, or None if not found
|
||||||
|
- local_steam_id_used: the steam_id that was read from local meta.cpp
|
||||||
|
(present regardless of whether the lookup succeeded, so callers can
|
||||||
|
report what the wrong ID is)
|
||||||
|
"""
|
||||||
|
local_sid = _read_local_steam_id(mod_dir)
|
||||||
|
|
||||||
|
if local_sid:
|
||||||
|
url = index["by_steam_id"].get(local_sid)
|
||||||
|
if url:
|
||||||
|
return _server_name_from_url(url), local_sid
|
||||||
|
|
||||||
|
# Name-based fallback
|
||||||
url = index["by_name"].get(_normalize_name(local_name))
|
url = index["by_name"].get(_normalize_name(local_name))
|
||||||
if url:
|
if url:
|
||||||
return _server_name_from_url(url)
|
return _server_name_from_url(url), local_sid
|
||||||
|
|
||||||
return None
|
return None, local_sid
|
||||||
|
|
||||||
|
|
||||||
def _collect_mod_folders(cfg, group_filter: str | None) -> list[tuple[str, Path]]:
|
def _collect_mod_folders(cfg, group_filter: str | None) -> list[tuple[str, Path]]:
|
||||||
"""Return list of (group_name, mod_path) for every @Mod in downloads/."""
|
|
||||||
result = []
|
result = []
|
||||||
if not cfg.downloads.is_dir():
|
if not cfg.downloads.is_dir():
|
||||||
return result
|
return result
|
||||||
@@ -98,26 +109,30 @@ def _collect_mod_folders(cfg, group_filter: str | None) -> list[tuple[str, Path]
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Classification helpers
|
# Classification
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _resolve_status(
|
def _resolve_status(
|
||||||
disk_name: str,
|
disk_name: str,
|
||||||
server_name: str | None,
|
server_name: str | None,
|
||||||
|
local_sid: str | None,
|
||||||
ok_disk_names: set[str],
|
ok_disk_names: set[str],
|
||||||
) -> tuple[str, str]:
|
) -> tuple[str, str]:
|
||||||
"""
|
"""
|
||||||
Return (status, display_col) for one mod, after false-positive filtering.
|
Return (status, display_col) after false-positive filtering.
|
||||||
|
|
||||||
A MISMATCH is downgraded to NOT_ON_SERVER when the proposed server name is
|
ID_COLLISION: steam_id lookup returned a name that's already correctly
|
||||||
already the exact disk name of another folder (steam_id collision on server).
|
held by a different folder — the local meta.cpp has the wrong publishedid.
|
||||||
"""
|
"""
|
||||||
if server_name is None:
|
if server_name is None:
|
||||||
return "NOT_ON_SERVER", "---"
|
return "NOT_ON_SERVER", "---"
|
||||||
if server_name == disk_name:
|
if server_name == disk_name:
|
||||||
return "OK", server_name
|
return "OK", server_name
|
||||||
if server_name in ok_disk_names:
|
if server_name in ok_disk_names:
|
||||||
return "NOT_ON_SERVER", "--- (steam_id collision)"
|
# This folder's local steam_id belongs to a mod that already has a
|
||||||
|
# correct folder on disk — the meta.cpp is wrong.
|
||||||
|
sid_info = f" (local id: {local_sid})" if local_sid else ""
|
||||||
|
return "ID_COLLISION", f"{server_name}{sid_info}"
|
||||||
return "MISMATCH", server_name
|
return "MISMATCH", server_name
|
||||||
|
|
||||||
|
|
||||||
@@ -131,17 +146,10 @@ def _fix_mismatch(
|
|||||||
server_name: str,
|
server_name: str,
|
||||||
cfg,
|
cfg,
|
||||||
) -> tuple[bool, str]:
|
) -> tuple[bool, str]:
|
||||||
"""
|
|
||||||
Rename mod_dir to server_name and update the arma_dir junction.
|
|
||||||
|
|
||||||
Returns (success, message).
|
|
||||||
"""
|
|
||||||
new_dir = mod_dir.parent / server_name
|
new_dir = mod_dir.parent / server_name
|
||||||
|
|
||||||
if new_dir.exists():
|
if new_dir.exists():
|
||||||
return False, f"target already exists: {new_dir}"
|
return False, f"target already exists: {new_dir}"
|
||||||
|
|
||||||
# --- Remove old junction in arma_dir (if present) ---
|
|
||||||
old_link = cfg.arma_dir / mod_dir.name
|
old_link = cfg.arma_dir / mod_dir.name
|
||||||
removed_old_link = False
|
removed_old_link = False
|
||||||
if _is_junction(old_link):
|
if _is_junction(old_link):
|
||||||
@@ -150,26 +158,83 @@ def _fix_mismatch(
|
|||||||
return False, f"could not remove old junction {old_link}: {err}"
|
return False, f"could not remove old junction {old_link}: {err}"
|
||||||
removed_old_link = True
|
removed_old_link = True
|
||||||
|
|
||||||
# --- Rename the download folder ---
|
|
||||||
try:
|
try:
|
||||||
mod_dir.rename(new_dir)
|
mod_dir.rename(new_dir)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
# Try to restore the junction we just removed
|
|
||||||
if removed_old_link:
|
if removed_old_link:
|
||||||
create_junction(old_link, mod_dir)
|
create_junction(old_link, mod_dir)
|
||||||
return False, f"rename failed: {exc}"
|
return False, f"rename failed: {exc}"
|
||||||
|
|
||||||
# --- Create new junction in arma_dir ---
|
|
||||||
new_link = cfg.arma_dir / server_name
|
new_link = cfg.arma_dir / server_name
|
||||||
if cfg.arma_dir.exists():
|
if cfg.arma_dir.exists():
|
||||||
if _is_junction(new_link) or new_link.exists():
|
if not (_is_junction(new_link) or new_link.exists()):
|
||||||
pass # already correct or non-junction exists; leave it
|
|
||||||
else:
|
|
||||||
create_junction(new_link, new_dir)
|
create_junction(new_link, new_dir)
|
||||||
|
|
||||||
return True, f"{mod_dir.name} -> {server_name}"
|
return True, f"{mod_dir.name} -> {server_name}"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fix: rewrite wrong steam_id in local meta.cpp
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _build_comparison_id_map(cfg) -> dict[str, tuple[str, str]]:
|
||||||
|
"""
|
||||||
|
Load comparison.json and return {normalized_name: (steam_id, display_name)}
|
||||||
|
for every mod that has a steam_id.
|
||||||
|
"""
|
||||||
|
if not cfg.comparison.exists():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
data = json.loads(cfg.comparison.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, json.JSONDecodeError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
result: dict[str, tuple[str, str]] = {}
|
||||||
|
all_mods = list(data.get("shared", {}).get("mods", []))
|
||||||
|
for preset_data in data.get("unique", {}).values():
|
||||||
|
all_mods.extend(preset_data.get("mods", []))
|
||||||
|
|
||||||
|
for mod in all_mods:
|
||||||
|
sid = mod.get("steam_id")
|
||||||
|
name = mod.get("name", "")
|
||||||
|
if sid and name:
|
||||||
|
result[_normalize_name(name)] = (sid, name)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _write_steam_id(mod_dir: Path, new_sid: str) -> tuple[bool, str]:
|
||||||
|
"""
|
||||||
|
Overwrite / insert publishedid in mod_dir/meta.cpp.
|
||||||
|
Returns (success, message).
|
||||||
|
"""
|
||||||
|
meta_path = mod_dir / "meta.cpp"
|
||||||
|
if meta_path.exists():
|
||||||
|
try:
|
||||||
|
text = meta_path.read_text(encoding="utf-8", errors="ignore")
|
||||||
|
except OSError as exc:
|
||||||
|
return False, str(exc)
|
||||||
|
|
||||||
|
old_sid_match = _parse_meta_cpp(text)
|
||||||
|
if _PUBLISHEDID_RE.search(text):
|
||||||
|
new_text = _PUBLISHEDID_RE.sub(rf"\g<1>{new_sid}\g<2>", text)
|
||||||
|
else:
|
||||||
|
new_text = text.rstrip() + f"\npublishedid = {new_sid};\n"
|
||||||
|
|
||||||
|
try:
|
||||||
|
meta_path.write_text(new_text, encoding="utf-8")
|
||||||
|
except OSError as exc:
|
||||||
|
return False, str(exc)
|
||||||
|
|
||||||
|
old_str = old_sid_match or "missing"
|
||||||
|
return True, f"meta.cpp: {old_str} -> {new_sid}"
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
meta_path.write_text(f"publishedid = {new_sid};\n", encoding="utf-8")
|
||||||
|
except OSError as exc:
|
||||||
|
return False, str(exc)
|
||||||
|
return True, f"meta.cpp created with id {new_sid}"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Main
|
# Main
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -178,12 +243,14 @@ def main() -> None:
|
|||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Check and optionally fix mod folder name mismatches."
|
description="Check and fix mod folder name mismatches and wrong steam IDs."
|
||||||
)
|
)
|
||||||
parser.add_argument("--group", "-g", metavar="GROUP",
|
parser.add_argument("--group", "-g", metavar="GROUP",
|
||||||
help="Only check this group (default: all)")
|
help="Only check this group (default: all)")
|
||||||
parser.add_argument("--fix", action="store_true",
|
parser.add_argument("--fix", action="store_true",
|
||||||
help="Rename mismatched folders and fix junctions")
|
help="Rename MISMATCH folders and fix junctions")
|
||||||
|
parser.add_argument("--fix-ids", action="store_true",
|
||||||
|
help="Rewrite wrong publishedid in meta.cpp using comparison.json")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
# ---- Build server index ----
|
# ---- Build server index ----
|
||||||
@@ -198,59 +265,71 @@ def main() -> None:
|
|||||||
print("No @Mod folders found in downloads/.\n")
|
print("No @Mod folders found in downloads/.\n")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
# ---- Pass 1: raw lookup for every mod ----
|
# ---- Pass 1: raw lookup ----
|
||||||
# result row: (group, mod_dir, server_name | None)
|
# (group, mod_dir, server_name | None, local_steam_id | None)
|
||||||
raw: list[tuple[str, Path, str | None]] = []
|
raw: list[tuple[str, Path, str | None, str | None]] = []
|
||||||
for group, mod_dir in mods:
|
for group, mod_dir in mods:
|
||||||
raw.append((group, mod_dir, _lookup_server_name(mod_dir.name, mod_dir, index)))
|
server_name, local_sid = _lookup_detailed(mod_dir.name, mod_dir, index)
|
||||||
|
raw.append((group, mod_dir, server_name, local_sid))
|
||||||
|
|
||||||
# ---- Pass 2: discard false positives caused by steam_id collisions ----
|
# ---- Pass 2: build ok_disk_names for collision detection ----
|
||||||
# If a server name is already an exact disk name (i.e. an OK match exists),
|
|
||||||
# any other folder whose steam_id lookup points to that same server name is a
|
|
||||||
# false positive — the server has a bad/shared publishedid in meta.cpp.
|
|
||||||
# Reclassify those entries as NOT_ON_SERVER.
|
|
||||||
ok_disk_names: set[str] = {
|
ok_disk_names: set[str] = {
|
||||||
mod_dir.name
|
mod_dir.name
|
||||||
for _, mod_dir, server_name in raw
|
for _, mod_dir, server_name, _ in raw
|
||||||
if server_name == mod_dir.name # exact match = genuinely OK
|
if server_name == mod_dir.name
|
||||||
}
|
}
|
||||||
def _classify(disk_name: str, server_name: str | None) -> tuple[str, str]:
|
|
||||||
return _resolve_status(disk_name, server_name, ok_disk_names)
|
|
||||||
|
|
||||||
# ---- Print table ----
|
# ---- Print table ----
|
||||||
ok_count = mismatch_count = unknown_count = 0
|
ok_count = mismatch_count = collision_count = unknown_count = 0
|
||||||
mismatches: list[tuple[str, Path, str]] = []
|
mismatches: list[tuple[str, Path, str]] = []
|
||||||
|
collisions: list[tuple[str, Path, str | None]] = [] # (group, mod_dir, local_sid)
|
||||||
|
|
||||||
print(f" {'Disk name':<{W_DISK}} {'Group':<{W_GROUP}} {'Server name':<{W_SERVER}} Status")
|
print(f" {'Disk name':<{W_DISK}} {'Group':<{W_GROUP}} {'Status / Server name':<{W_SERVER}}")
|
||||||
print(f" {'-'*W_DISK} {'-'*W_GROUP} {'-'*W_SERVER} ------")
|
print(f" {'-'*W_DISK} {'-'*W_GROUP} {'-'*W_SERVER}")
|
||||||
|
|
||||||
for group, mod_dir, server_name in raw:
|
for group, mod_dir, server_name, local_sid in raw:
|
||||||
status, server_col = _classify(mod_dir.name, server_name)
|
status, server_col = _resolve_status(mod_dir.name, server_name, local_sid, ok_disk_names)
|
||||||
if status == "OK":
|
if status == "OK":
|
||||||
ok_count += 1
|
ok_count += 1
|
||||||
|
detail = f"OK ({server_col})"
|
||||||
elif status == "MISMATCH":
|
elif status == "MISMATCH":
|
||||||
mismatch_count += 1
|
mismatch_count += 1
|
||||||
mismatches.append((group, mod_dir, server_name))
|
mismatches.append((group, mod_dir, server_name))
|
||||||
|
detail = f"MISMATCH -> {server_col}"
|
||||||
|
elif status == "ID_COLLISION":
|
||||||
|
collision_count += 1
|
||||||
|
collisions.append((group, mod_dir, local_sid))
|
||||||
|
detail = f"ID_COLLISION {server_col}"
|
||||||
else:
|
else:
|
||||||
unknown_count += 1
|
unknown_count += 1
|
||||||
|
detail = "NOT_ON_SERVER"
|
||||||
|
|
||||||
print(f" {mod_dir.name:<{W_DISK}} {group:<{W_GROUP}} {server_col:<{W_SERVER}} {status}")
|
print(f" {mod_dir.name:<{W_DISK}} {group:<{W_GROUP}} {detail}")
|
||||||
|
|
||||||
print(f"\n {ok_count} OK, {mismatch_count} mismatch, {unknown_count} not on server\n")
|
print(f"\n {ok_count} OK, {mismatch_count} mismatch, "
|
||||||
|
f"{collision_count} id_collision, {unknown_count} not on server\n")
|
||||||
|
|
||||||
if not mismatches:
|
any_action = args.fix or args.fix_ids
|
||||||
if mismatch_count == 0:
|
nothing_to_do = (not mismatches) and (not collisions)
|
||||||
print(" All folder names match the server. Safe to run full pipeline.\n")
|
|
||||||
|
if nothing_to_do:
|
||||||
|
if mismatch_count == 0 and collision_count == 0:
|
||||||
|
print(" All folder names match. Safe to run full pipeline.\n")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
# ---- Fix mode ----
|
if not any_action:
|
||||||
if not args.fix:
|
hints = []
|
||||||
print(" Run with --fix to rename mismatched folders and update junctions.\n")
|
if mismatches:
|
||||||
|
hints.append("--fix to rename mismatched folders")
|
||||||
|
if collisions:
|
||||||
|
hints.append("--fix-ids to correct wrong steam IDs in meta.cpp")
|
||||||
|
print(" Run with " + " and ".join(hints) + ".\n")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
print(f"Fixing {len(mismatches)} mismatch(es)...\n")
|
# ---- --fix: rename mismatched folders ----
|
||||||
|
if args.fix and mismatches:
|
||||||
|
print(f"Fixing {len(mismatches)} name mismatch(es)...\n")
|
||||||
fixed = failed = 0
|
fixed = failed = 0
|
||||||
|
|
||||||
for group, mod_dir, server_name in mismatches:
|
for group, mod_dir, server_name in mismatches:
|
||||||
ok, msg = _fix_mismatch(group, mod_dir, server_name, cfg)
|
ok, msg = _fix_mismatch(group, mod_dir, server_name, cfg)
|
||||||
if ok:
|
if ok:
|
||||||
@@ -259,13 +338,43 @@ def main() -> None:
|
|||||||
else:
|
else:
|
||||||
print(f" [X] {mod_dir.name} FAILED: {msg}")
|
print(f" [X] {mod_dir.name} FAILED: {msg}")
|
||||||
failed += 1
|
failed += 1
|
||||||
|
print(f"\n Done: {fixed} renamed" + (f", {failed} failed" if failed else "") + "\n")
|
||||||
|
|
||||||
print(f"\n Done: {fixed} renamed"
|
# ---- --fix-ids: rewrite wrong steam_ids ----
|
||||||
+ (f", {failed} failed" if failed else "")
|
if args.fix_ids and collisions:
|
||||||
+ "\n")
|
id_map = _build_comparison_id_map(cfg)
|
||||||
|
if not id_map:
|
||||||
|
print(" WARNING: comparison.json not found or empty. "
|
||||||
|
"Run 'python run.py --skip-fetch --skip-link' first.\n")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
if fixed:
|
print(f"Fixing {len(collisions)} steam_id collision(s)...\n")
|
||||||
print(" Run 'python run.py --skip-fetch' to verify links are correct.\n")
|
fixed = failed = unknown = 0
|
||||||
|
|
||||||
|
for group, mod_dir, bad_sid in collisions:
|
||||||
|
norm = _normalize_name(mod_dir.name)
|
||||||
|
match = id_map.get(norm)
|
||||||
|
if match is None:
|
||||||
|
print(f" [?] {mod_dir.name:<{W_DISK}} not in comparison.json — skipped")
|
||||||
|
unknown += 1
|
||||||
|
continue
|
||||||
|
correct_sid, display_name = match
|
||||||
|
if bad_sid == correct_sid:
|
||||||
|
print(f" [=] {mod_dir.name:<{W_DISK}} id already correct ({correct_sid})")
|
||||||
|
fixed += 1
|
||||||
|
continue
|
||||||
|
ok, msg = _write_steam_id(mod_dir, correct_sid)
|
||||||
|
if ok:
|
||||||
|
print(f" [+] {mod_dir.name:<{W_DISK}} {msg}")
|
||||||
|
fixed += 1
|
||||||
|
else:
|
||||||
|
print(f" [X] {mod_dir.name:<{W_DISK}} FAILED: {msg}")
|
||||||
|
failed += 1
|
||||||
|
|
||||||
|
print(f"\n Done: {fixed} fixed" +
|
||||||
|
(f", {unknown} skipped (not in comparison.json)" if unknown else "") +
|
||||||
|
(f", {failed} failed" if failed else "") + "\n")
|
||||||
|
print(" Re-run check_names.py to verify.\n")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
166
test_suite.py
166
test_suite.py
@@ -1064,7 +1064,11 @@ test("all exported symbols are callable or types", _test_exports_are_callable)
|
|||||||
|
|
||||||
group("check_names helpers")
|
group("check_names helpers")
|
||||||
|
|
||||||
from check_names import _server_name_from_url, _read_local_steam_id, _lookup_server_name, _resolve_status
|
from check_names import (
|
||||||
|
_server_name_from_url, _read_local_steam_id,
|
||||||
|
_lookup_detailed, _resolve_status, _write_steam_id,
|
||||||
|
_build_comparison_id_map,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _test_server_name_from_url():
|
def _test_server_name_from_url():
|
||||||
@@ -1091,7 +1095,7 @@ def _test_read_local_steam_id_no_id_in_file():
|
|||||||
assert _read_local_steam_id(d) is None
|
assert _read_local_steam_id(d) is None
|
||||||
|
|
||||||
|
|
||||||
def _test_lookup_server_name_by_steam_id():
|
def _test_lookup_detailed_by_steam_id():
|
||||||
index = {
|
index = {
|
||||||
"by_steam_id": {"463939057": "https://x.com/@ace3/"},
|
"by_steam_id": {"463939057": "https://x.com/@ace3/"},
|
||||||
"by_name": {},
|
"by_name": {},
|
||||||
@@ -1099,23 +1103,23 @@ def _test_lookup_server_name_by_steam_id():
|
|||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
d = Path(d)
|
d = Path(d)
|
||||||
(d / "meta.cpp").write_text("publishedid = 463939057;", encoding="utf-8")
|
(d / "meta.cpp").write_text("publishedid = 463939057;", encoding="utf-8")
|
||||||
result = _lookup_server_name("@ACE3", d, index)
|
server_name, local_sid = _lookup_detailed("@ACE3", d, index)
|
||||||
assert_eq(result, "@ace3")
|
assert_eq(server_name, "@ace3")
|
||||||
|
assert_eq(local_sid, "463939057")
|
||||||
|
|
||||||
|
|
||||||
def _test_lookup_server_name_by_name_fallback():
|
def _test_lookup_detailed_by_name_fallback():
|
||||||
index = {
|
index = {
|
||||||
"by_steam_id": {},
|
"by_steam_id": {},
|
||||||
"by_name": {"cbaa3": "https://x.com/@cba_a3/"},
|
"by_name": {"cbaa3": "https://x.com/@cba_a3/"},
|
||||||
}
|
}
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
# No meta.cpp — name fallback
|
server_name, local_sid = _lookup_detailed("@CBA_A3", Path(d), index)
|
||||||
result = _lookup_server_name("@CBA_A3", Path(d), index)
|
assert_eq(server_name, "@cba_a3")
|
||||||
assert_eq(result, "@cba_a3")
|
assert local_sid is None # no meta.cpp
|
||||||
|
|
||||||
|
|
||||||
def _test_lookup_server_name_steam_id_beats_name():
|
def _test_lookup_detailed_steam_id_beats_name():
|
||||||
"""steam_id lookup must take priority over name fallback."""
|
|
||||||
index = {
|
index = {
|
||||||
"by_steam_id": {"111": "https://x.com/@correct/"},
|
"by_steam_id": {"111": "https://x.com/@correct/"},
|
||||||
"by_name": {"mymod": "https://x.com/@wrong/"},
|
"by_name": {"mymod": "https://x.com/@wrong/"},
|
||||||
@@ -1123,55 +1127,149 @@ def _test_lookup_server_name_steam_id_beats_name():
|
|||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
d = Path(d)
|
d = Path(d)
|
||||||
(d / "meta.cpp").write_text("publishedid = 111;", encoding="utf-8")
|
(d / "meta.cpp").write_text("publishedid = 111;", encoding="utf-8")
|
||||||
result = _lookup_server_name("@MyMod", d, index)
|
server_name, local_sid = _lookup_detailed("@MyMod", d, index)
|
||||||
assert_eq(result, "@correct")
|
assert_eq(server_name, "@correct")
|
||||||
|
assert_eq(local_sid, "111")
|
||||||
|
|
||||||
|
|
||||||
def _test_lookup_server_name_not_found():
|
def _test_lookup_detailed_not_found():
|
||||||
index = {"by_steam_id": {}, "by_name": {}}
|
index = {"by_steam_id": {}, "by_name": {}}
|
||||||
with tempfile.TemporaryDirectory() as d:
|
with tempfile.TemporaryDirectory() as d:
|
||||||
result = _lookup_server_name("@Unknown", Path(d), index)
|
server_name, local_sid = _lookup_detailed("@Unknown", Path(d), index)
|
||||||
assert result is None
|
assert server_name is None
|
||||||
|
|
||||||
|
|
||||||
test("_server_name_from_url: extracts name from URL", _test_server_name_from_url)
|
def _test_lookup_detailed_returns_sid_even_when_not_on_server():
|
||||||
test("_read_local_steam_id: reads meta.cpp", _test_read_local_steam_id_present)
|
"""local_sid should be returned even when server lookup fails."""
|
||||||
test("_read_local_steam_id: no meta.cpp -> None", _test_read_local_steam_id_missing)
|
index = {"by_steam_id": {}, "by_name": {}}
|
||||||
test("_read_local_steam_id: meta.cpp without id -> None", _test_read_local_steam_id_no_id_in_file)
|
with tempfile.TemporaryDirectory() as d:
|
||||||
test("_lookup_server_name: matches by steam_id", _test_lookup_server_name_by_steam_id)
|
d = Path(d)
|
||||||
test("_lookup_server_name: falls back to name", _test_lookup_server_name_by_name_fallback)
|
(d / "meta.cpp").write_text("publishedid = 999;", encoding="utf-8")
|
||||||
test("_lookup_server_name: steam_id beats name fallback", _test_lookup_server_name_steam_id_beats_name)
|
server_name, local_sid = _lookup_detailed("@SomeMod", d, index)
|
||||||
test("_lookup_server_name: not found -> None", _test_lookup_server_name_not_found)
|
assert server_name is None
|
||||||
|
assert_eq(local_sid, "999")
|
||||||
|
|
||||||
# _resolve_status tests (the false-positive filter)
|
|
||||||
|
test("_lookup_detailed: matches by steam_id", _test_lookup_detailed_by_steam_id)
|
||||||
|
test("_lookup_detailed: falls back to name", _test_lookup_detailed_by_name_fallback)
|
||||||
|
test("_lookup_detailed: steam_id beats name fallback", _test_lookup_detailed_steam_id_beats_name)
|
||||||
|
test("_lookup_detailed: not found -> None", _test_lookup_detailed_not_found)
|
||||||
|
test("_lookup_detailed: returns local_sid even when not on server", _test_lookup_detailed_returns_sid_even_when_not_on_server)
|
||||||
|
|
||||||
|
# _resolve_status tests
|
||||||
|
|
||||||
def _test_resolve_status_ok():
|
def _test_resolve_status_ok():
|
||||||
status, col = _resolve_status("@ace", "@ace", ok_disk_names={"@ace"})
|
status, col = _resolve_status("@ace", "@ace", None, ok_disk_names={"@ace"})
|
||||||
assert_eq(status, "OK")
|
assert_eq(status, "OK")
|
||||||
assert_eq(col, "@ace")
|
assert_eq(col, "@ace")
|
||||||
|
|
||||||
def _test_resolve_status_mismatch():
|
def _test_resolve_status_mismatch():
|
||||||
status, col = _resolve_status("@ACE3", "@ace3", ok_disk_names={"@cba_a3"})
|
status, col = _resolve_status("@ACE3", "@ace3", None, ok_disk_names={"@cba_a3"})
|
||||||
assert_eq(status, "MISMATCH")
|
assert_eq(status, "MISMATCH")
|
||||||
assert_eq(col, "@ace3")
|
assert_eq(col, "@ace3")
|
||||||
|
|
||||||
def _test_resolve_status_not_found():
|
def _test_resolve_status_not_found():
|
||||||
status, col = _resolve_status("@Unknown", None, ok_disk_names=set())
|
status, col = _resolve_status("@Unknown", None, None, ok_disk_names=set())
|
||||||
assert_eq(status, "NOT_ON_SERVER")
|
assert_eq(status, "NOT_ON_SERVER")
|
||||||
|
|
||||||
def _test_resolve_status_steam_id_collision():
|
def _test_resolve_status_id_collision():
|
||||||
# server_name is already correctly held by another folder → false positive
|
|
||||||
ok = {"@Realistic Ragdoll Physics"}
|
ok = {"@Realistic Ragdoll Physics"}
|
||||||
status, col = _resolve_status("@NIArms All in One- ACE Compatibility",
|
status, col = _resolve_status(
|
||||||
|
"@NIArms All in One- ACE Compatibility",
|
||||||
"@Realistic Ragdoll Physics",
|
"@Realistic Ragdoll Physics",
|
||||||
ok_disk_names=ok)
|
"1234567",
|
||||||
assert_eq(status, "NOT_ON_SERVER", "Should be reclassified due to collision")
|
ok_disk_names=ok,
|
||||||
assert "collision" in col
|
)
|
||||||
|
assert_eq(status, "ID_COLLISION")
|
||||||
|
assert "1234567" in col # bad steam_id shown in output
|
||||||
|
|
||||||
|
def _test_resolve_status_collision_without_sid():
|
||||||
|
ok = {"@Realistic Ragdoll Physics"}
|
||||||
|
status, col = _resolve_status(
|
||||||
|
"@Some Mod", "@Realistic Ragdoll Physics", None, ok_disk_names=ok
|
||||||
|
)
|
||||||
|
assert_eq(status, "ID_COLLISION")
|
||||||
|
|
||||||
test("_resolve_status: OK match", _test_resolve_status_ok)
|
test("_resolve_status: OK match", _test_resolve_status_ok)
|
||||||
test("_resolve_status: genuine mismatch", _test_resolve_status_mismatch)
|
test("_resolve_status: genuine mismatch", _test_resolve_status_mismatch)
|
||||||
test("_resolve_status: not found", _test_resolve_status_not_found)
|
test("_resolve_status: not found", _test_resolve_status_not_found)
|
||||||
test("_resolve_status: steam_id collision reclassified as NOT_ON_SERVER", _test_resolve_status_steam_id_collision)
|
test("_resolve_status: ID_COLLISION shows bad steam_id", _test_resolve_status_id_collision)
|
||||||
|
test("_resolve_status: ID_COLLISION without local sid", _test_resolve_status_collision_without_sid)
|
||||||
|
|
||||||
|
# _write_steam_id tests
|
||||||
|
|
||||||
|
def _test_write_steam_id_updates_existing():
|
||||||
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
d = Path(d)
|
||||||
|
meta = d / "meta.cpp"
|
||||||
|
meta.write_text('publishedid = 111;\nname = "OldMod";\n', encoding="utf-8")
|
||||||
|
ok, msg = _write_steam_id(d, "999")
|
||||||
|
assert ok
|
||||||
|
assert "999" in meta.read_text(encoding="utf-8")
|
||||||
|
assert "111" not in meta.read_text(encoding="utf-8")
|
||||||
|
assert 'name = "OldMod"' in meta.read_text(encoding="utf-8") # preserved
|
||||||
|
|
||||||
|
def _test_write_steam_id_creates_if_missing():
|
||||||
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
d = Path(d)
|
||||||
|
ok, msg = _write_steam_id(d, "12345")
|
||||||
|
assert ok
|
||||||
|
meta = d / "meta.cpp"
|
||||||
|
assert meta.exists()
|
||||||
|
assert "12345" in meta.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
def _test_write_steam_id_appends_if_no_line():
|
||||||
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
d = Path(d)
|
||||||
|
meta = d / "meta.cpp"
|
||||||
|
meta.write_text('name = "NoId";\n', encoding="utf-8")
|
||||||
|
ok, msg = _write_steam_id(d, "777")
|
||||||
|
assert ok
|
||||||
|
content = meta.read_text(encoding="utf-8")
|
||||||
|
assert "777" in content
|
||||||
|
assert 'name = "NoId"' in content
|
||||||
|
|
||||||
|
test("_write_steam_id: updates existing publishedid", _test_write_steam_id_updates_existing)
|
||||||
|
test("_write_steam_id: creates meta.cpp if missing", _test_write_steam_id_creates_if_missing)
|
||||||
|
test("_write_steam_id: appends if no publishedid line", _test_write_steam_id_appends_if_no_line)
|
||||||
|
|
||||||
|
# _build_comparison_id_map tests
|
||||||
|
|
||||||
|
def _test_build_comparison_id_map_reads_json():
|
||||||
|
comparison = {
|
||||||
|
"compared_presets": ["A", "B"],
|
||||||
|
"shared": {"mod_count": 1, "mods": [
|
||||||
|
{"name": "CBA_A3", "steam_id": "450814997", "url": None, "source": "steam"},
|
||||||
|
]},
|
||||||
|
"unique": {
|
||||||
|
"A": {"mod_count": 1, "mods": [
|
||||||
|
{"name": "ACE3", "steam_id": "463939057", "url": None, "source": "steam"},
|
||||||
|
]},
|
||||||
|
"B": {"mod_count": 0, "mods": []},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
with tempfile.TemporaryDirectory() as d:
|
||||||
|
cfg_data = {
|
||||||
|
"server": {"base_url": "x", "username": "u", "password": "p"},
|
||||||
|
"paths": {"arma_dir": d, "downloads": d,
|
||||||
|
"modlist_html": d, "modlist_json": d},
|
||||||
|
}
|
||||||
|
import json as _json
|
||||||
|
cfg_file = Path(d) / "config.json"
|
||||||
|
cfg_file.write_text(_json.dumps(cfg_data), encoding="utf-8")
|
||||||
|
from arma_modlist_tools.config import load_config as _lc
|
||||||
|
cfg = _lc(str(cfg_file))
|
||||||
|
# Write a fake comparison.json
|
||||||
|
comp_path = Path(d) / "comparison.json"
|
||||||
|
comp_path.write_text(_json.dumps(comparison), encoding="utf-8")
|
||||||
|
id_map = _build_comparison_id_map(cfg)
|
||||||
|
|
||||||
|
assert_in("cbaa3", id_map)
|
||||||
|
assert_eq(id_map["cbaa3"][0], "450814997")
|
||||||
|
assert_in("ace3", id_map)
|
||||||
|
assert_eq(id_map["ace3"][0], "463939057")
|
||||||
|
|
||||||
|
test("_build_comparison_id_map: reads steam_ids from comparison.json", _test_build_comparison_id_map_reads_json)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user