encounter journal base logic

This commit is contained in:
2026-06-06 19:51:26 +04:00
parent 5dcb8be3aa
commit 02d382c0f8
16 changed files with 236610 additions and 7 deletions
@@ -0,0 +1,164 @@
#!/usr/bin/env python3
"""
Build a trimmed item cache for the MoonWellClient EncounterJournal addon.
Reads one or more donor cache files (Sirus-style Generated_ItemsCache*.lua,
each holding rows like `[id] = { "name_en", "name_ru", quality, ..., "icon",
... }`), intersects them with the item ids referenced by
EncounterJournalData.json (the file produced by .ej export) and emits a
Lua file containing only the matching rows.
Output schema (single global, Lua):
MoonWellEncounterJournalItemCache = {
[<item_id>] = { name_en, name_ru, quality, icon },
...
}
Multiple donor files are merged left-to-right; later files override earlier
ones for duplicate ids. Items missing from every donor are reported on
stderr and skipped (the client falls back to GetItemInfo for those).
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
from typing import Dict, Iterable, Set, Tuple
# Row layout per Sirus convention:
# [id] = { "name_en", "name_ru", quality, classId, subClass,
# inventoryType, slot, stackable, idx, "icon", spellId },
ROW_RE = re.compile(
r"""^\s*\[(?P<id>\d+)\]\s*=\s*\{\s*
"(?P<en>(?:[^"\\]|\\.)*)"\s*,\s*
"(?P<ru>(?:[^"\\]|\\.)*)"\s*,\s*
(?P<quality>-?\d+)\s*,\s*
-?\d+\s*,\s* # field 4
-?\d+\s*,\s* # field 5
-?\d+\s*,\s* # field 6
-?\d+\s*,\s* # field 7
-?\d+\s*,\s* # field 8
-?\d+\s*,\s* # field 9
"(?P<icon>(?:[^"\\]|\\.)*)"\s*,\s*
-?\d+\s*
\}\s*,?\s*$""",
re.VERBOSE,
)
def parse_donor(path: Path) -> Dict[int, Tuple[str, str, int, str]]:
"""Returns {id: (name_en, name_ru, quality, icon)} from one donor file."""
out: Dict[int, Tuple[str, str, int, str]] = {}
bad = 0
with path.open("r", encoding="utf-8") as f:
for line in f:
m = ROW_RE.match(line)
if not m:
stripped = line.strip()
if stripped and not stripped.startswith(
("--", "Items", "ItemsCache", "}", "{")
):
bad += 1
continue
out[int(m.group("id"))] = (
m.group("en"),
m.group("ru"),
int(m.group("quality")),
m.group("icon"),
)
if bad:
print(f"[{path.name}] skipped {bad} unparseable lines", file=sys.stderr)
return out
def collect_needed_ids(journal_json: Path) -> Set[int]:
"""Returns the set of item ids referenced in EncounterJournalData.json."""
with journal_json.open("r", encoding="utf-8") as f:
data = json.load(f)
needed: Set[int] = set()
for rows in data.get("items", {}).values():
for row in rows:
# items[encounterID] row layout per docs/encounter_journal_format.md:
# [item_id, encounter_id, difficulty_mask, faction_mask, flags,
# id, class_mask]
if row:
needed.add(int(row[0]))
return needed
def lua_str(s: str) -> str:
s = s.replace("\\", "\\\\").replace("'", "\\'")
s = s.replace("\n", "\\n").replace("\r", "\\r")
return f"'{s}'"
def write_lua(out_path: Path, global_name: str,
cache: Dict[int, Tuple[str, str, int, str]]) -> None:
with out_path.open("w", encoding="utf-8") as f:
f.write("-- Generated by mod-encounter-journal/tools/"
"build_item_cache.py.\n")
f.write("-- Trimmed donor item cache for items referenced by "
"EncounterJournalData.\n\n")
f.write(f"{global_name} = {{\n")
for item_id in sorted(cache.keys()):
en, ru, quality, icon = cache[item_id]
f.write(
f" [{item_id}] = {{ {lua_str(en)}, {lua_str(ru)}, "
f"{quality}, {lua_str(icon)} }},\n"
)
f.write("}\n")
def merge_donors(donor_paths: Iterable[Path]
) -> Dict[int, Tuple[str, str, int, str]]:
merged: Dict[int, Tuple[str, str, int, str]] = {}
for p in donor_paths:
rows = parse_donor(p)
print(f"[{p.name}] parsed {len(rows)} rows", file=sys.stderr)
merged.update(rows)
return merged
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--journal", "-j", required=True, type=Path,
help="EncounterJournalData.json (from .ej export)")
parser.add_argument("--donor", "-d", required=True, type=Path, nargs="+",
help="One or more Generated_ItemsCache*.lua files")
parser.add_argument("--output", "-o", required=True, type=Path,
help="Where to write EncounterJournalItemCache.lua")
parser.add_argument("--global", dest="global_name",
default="MoonWellEncounterJournalItemCache",
help="Lua global name (default: %(default)s)")
args = parser.parse_args()
needed = collect_needed_ids(args.journal)
print(f"EJ references {len(needed)} unique item ids", file=sys.stderr)
donors = merge_donors(args.donor)
print(f"donor cache holds {len(donors)} unique item ids", file=sys.stderr)
trimmed = {i: donors[i] for i in needed if i in donors}
missing = sorted(needed - donors.keys())
write_lua(args.output, args.global_name, trimmed)
print(
f"wrote {len(trimmed)} rows to {args.output} "
f"(missing in donor: {len(missing)})",
file=sys.stderr,
)
if missing:
sample = ", ".join(str(i) for i in missing[:15])
more = "" if len(missing) <= 15 else f" ... and {len(missing) - 15} more"
print(f"missing item ids (client will fall back to GetItemInfo): "
f"{sample}{more}", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,577 @@
#!/usr/bin/env python3
"""
Import a Sirus-style Generated_EncounterJournal.lua dump into the
mod-encounter-journal custom_ej_* tables.
The dump holds seven top-level Lua tables that map 1:1 to our schema:
JOURNALINSTANCE -> custom_ej_instance
JOURNALENCOUNTER -> custom_ej_encounter
JOURNALENCOUNTERCREATURE -> custom_ej_creature
JOURNALENCOUNTERSECTION -> custom_ej_section
JOURNALENCOUNTERITEM -> custom_ej_loot
JOURNALTIER -> custom_ej_tier
JOURNALTIERXINSTANCE -> custom_ej_tier_instance
The script emits a single .sql file (or writes to stdout) that uses
REPLACE INTO so re-running is safe. Pipe it into your MySQL container:
python3 import_donor_journal.py -i Generated_EncounterJournal.lua \
-o donor_import.sql
docker exec -i ac-database mysql -uroot -p$PASS acore_world \
< donor_import.sql
Each row from the dump goes through a small tokenizer that understands
quoted strings ("..."), long-bracket strings ([[...]]), numbers, and
nested braces. The dump only uses single-line strings, so no multi-line
handling is needed.
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
from typing import Any, Iterable, List, Optional, TextIO, Tuple
# ---------------------------------------------------------------------------
# Tokeniser / value parser
# ---------------------------------------------------------------------------
class LuaParseError(RuntimeError):
pass
def _skip_ws(src: str, pos: int) -> int:
while pos < len(src) and src[pos] in " \t\r\n,":
pos += 1
return pos
def _parse_quoted_string(src: str, pos: int) -> Tuple[str, int]:
assert src[pos] == '"'
pos += 1
out: List[str] = []
while pos < len(src):
c = src[pos]
if c == "\\" and pos + 1 < len(src):
nxt = src[pos + 1]
# Lua escapes: \", \', \\, \n, \r, \t — keep verbatim semantics.
out.append({
'"': '"', "'": "'", "\\": "\\",
"n": "\n", "r": "\r", "t": "\t",
}.get(nxt, nxt))
pos += 2
continue
if c == '"':
return "".join(out), pos + 1
out.append(c)
pos += 1
raise LuaParseError("unterminated quoted string")
def _parse_long_bracket_string(src: str, pos: int) -> Tuple[str, int]:
assert src.startswith("[[", pos)
pos += 2
end = src.find("]]", pos)
if end < 0:
raise LuaParseError("unterminated long-bracket string")
return src[pos:end], end + 2
_NUM_RE = re.compile(r"-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?")
def _parse_number(src: str, pos: int) -> Tuple[float, int]:
m = _NUM_RE.match(src, pos)
if not m:
raise LuaParseError(f"expected number at {pos}: {src[pos:pos+20]!r}")
txt = m.group(0)
val: Any = float(txt) if ("." in txt or "e" in txt or "E" in txt) else int(txt)
return val, m.end()
def _parse_table(src: str, pos: int) -> Tuple[list, int]:
"""Parses `{ v1, v2, ... }` and returns the list of values."""
if src[pos] != "{":
raise LuaParseError(f"expected '{{' at {pos}")
pos += 1
out: list = []
while True:
pos = _skip_ws(src, pos)
if pos >= len(src):
raise LuaParseError("unterminated table literal")
if src[pos] == "}":
return out, pos + 1
value, pos = _parse_value(src, pos)
out.append(value)
def _parse_value(src: str, pos: int) -> Tuple[Any, int]:
pos = _skip_ws(src, pos)
c = src[pos]
if c == '"':
return _parse_quoted_string(src, pos)
if src.startswith("[[", pos):
return _parse_long_bracket_string(src, pos)
if c == "{":
return _parse_table(src, pos)
return _parse_number(src, pos)
# ---------------------------------------------------------------------------
# Top-level table extractor
# ---------------------------------------------------------------------------
def _find_top_level_tables(text: str) -> dict:
"""
Returns {table_name: dict_or_list}. Iterates the source once, splitting
at top-level `NAME = {` declarations and parsing the brace-balanced body.
"""
tables: dict = {}
pos = 0
pat = re.compile(r"^([A-Z_]+)\s*=\s*\{", re.MULTILINE)
while True:
m = pat.search(text, pos)
if not m:
break
name = m.group(1)
# Find brace-balanced end.
body_start = m.end() - 1 # position of '{'
depth = 0
i = body_start
in_str = None # '"' or '[' for long bracket
while i < len(text):
ch = text[i]
if in_str == '"':
if ch == "\\":
i += 2
continue
if ch == '"':
in_str = None
i += 1
continue
if in_str == "[":
if text.startswith("]]", i):
in_str = None
i += 2
continue
i += 1
continue
if ch == '"':
in_str = '"'
i += 1
continue
if text.startswith("[[", i):
in_str = "["
i += 2
continue
if ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
body = text[body_start:i + 1]
tables[name] = _parse_top_level_body(name, body)
pos = i + 1
break
i += 1
else:
raise LuaParseError(f"unterminated table {name}")
return tables
def _parse_top_level_body(name: str, body: str) -> Any:
"""
Parses the contents of one top-level table, which can have either
keyed entries `[id] = { ... }` or sequential entries `{ ... }`.
Returns dict for keyed entries, list for sequential.
"""
assert body.startswith("{") and body.endswith("}")
inner = body[1:-1]
i = 0
keyed: dict = {}
seq: list = []
while True:
i = _skip_ws(inner, i)
if i >= len(inner):
break
if inner[i] == "[":
# `[key] = value`
j = inner.index("]", i)
key_val, _ = _parse_value(inner, i + 1)
i = j + 1
i = _skip_ws(inner, i)
if inner[i] != "=":
raise LuaParseError(f"{name}: expected '=' at {i}")
i = _skip_ws(inner, i + 1)
value, i = _parse_value(inner, i)
keyed[key_val] = value
else:
value, i = _parse_value(inner, i)
seq.append(value)
if keyed and seq:
raise LuaParseError(f"{name}: mix of keyed/sequential entries")
return keyed if keyed else seq
# ---------------------------------------------------------------------------
# SQL emission
# ---------------------------------------------------------------------------
def _sql_str(s: Any) -> str:
s = str(s)
return "'" + s.replace("\\", "\\\\").replace("'", "''") + "'"
def _to_int(v: Any, default: int = 0) -> int:
if v is None:
return default
if isinstance(v, bool):
return int(v)
return int(v)
def _to_uint(v: Any) -> int:
"""Sirus uses -1 as 'all difficulties'; our schema is UNSIGNED → store 0."""
n = _to_int(v)
return n if n >= 0 else 0
def _to_signed(v: Any) -> int:
return _to_int(v)
def _to_float(v: Any) -> float:
if v is None:
return 0.0
return float(v)
def _load_id_set(path: Optional[Path]) -> Optional[set]:
"""Reads one id per line, returns a set of ints. None when path is None."""
if path is None:
return None
out: set = set()
with path.open("r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("#"):
continue
try:
out.add(int(line))
except ValueError:
pass
return out
def emit(tables: dict, out: TextIO,
known_maps: Optional[set] = None,
known_creatures: Optional[set] = None,
known_items: Optional[set] = None,
dropped: Optional[dict] = None) -> dict:
"""
Writes REPLACE INTO statements for every donor table. Returns per-table
insert counts for the run summary.
When @known_maps/@known_creatures/@known_items are provided, rows that
reference an id missing from the corresponding set are skipped:
- instance with map_id not in known_maps is dropped (and all of its
encounters/creatures/sections/loot/tier_instance go with it);
- creature whose creature_id != 0 and not in known_creatures dropped;
- loot whose item_id not in known_items dropped;
- tier with no surviving tier_instance entries dropped.
"""
counts = {
"tier": 0, "instance": 0, "tier_instance": 0,
"encounter": 0, "creature": 0, "section": 0, "loot": 0,
}
if dropped is None:
dropped = {}
for k in ("instance", "creature", "loot", "tier"):
dropped.setdefault(k, 0)
out.write("-- Generated by mod-encounter-journal/tools/"
"import_donor_journal.py.\n")
out.write("-- Source: Sirus-style Generated_EncounterJournal.lua dump.\n")
out.write("-- Safe to re-run: every INSERT uses REPLACE semantics on PK.\n")
out.write("SET NAMES utf8mb4;\n")
out.write("START TRANSACTION;\n\n")
# First pass: figure out which instance_ids survive the map filter so we
# can transitively drop their encounters/creatures/sections/loot.
surviving_instances: set = set()
for inst_id, row in tables.get("JOURNALINSTANCE", {}).items():
if len(row) < 12:
row = row + [0] * (12 - len(row))
map_id = _to_int(row[6])
if known_maps is not None and map_id not in known_maps:
dropped["instance"] += 1
continue
surviving_instances.add(_to_int(inst_id))
# encounter_id -> instance_id (used by creature/section/loot filters)
encounter_to_instance: dict = {}
for inst_id, rows in tables.get("JOURNALENCOUNTER", {}).items():
for row in rows:
if len(row) < 12:
continue
encounter_to_instance[_to_int(row[0])] = _to_int(inst_id)
def _enc_survives(enc_id: int) -> bool:
inst = encounter_to_instance.get(enc_id)
return inst is not None and inst in surviving_instances
# ---- tiers ----
# Build the set of tiers that retain at least one surviving instance
# via JOURNALTIERXINSTANCE so we don't write orphan tiers.
surviving_tiers: set = set()
for inst_id, tirow in tables.get("JOURNALTIERXINSTANCE", {}).items():
if _to_int(inst_id) in surviving_instances and len(tirow) >= 1:
surviving_tiers.add(_to_int(tirow[0]))
for row in tables.get("JOURNALTIER", []):
# {id, name, flags}
tid, name, flags = row[0], row[1], row[2] if len(row) > 2 else 0
if surviving_tiers and _to_int(tid) not in surviving_tiers:
dropped["tier"] += 1
continue
out.write(
"REPLACE INTO `custom_ej_tier` "
"(`id`, `name`, `flags`, `sort_order`) VALUES "
f"({_to_int(tid)}, {_sql_str(name)}, {_to_int(flags)}, "
f"{_to_int(tid)});\n"
)
counts["tier"] += 1
out.write("\n")
# ---- instances ----
for inst_id, row in tables.get("JOURNALINSTANCE", {}).items():
if _to_int(inst_id) not in surviving_instances:
continue
if len(row) < 12:
row = row + [0] * (12 - len(row))
(name, desc, btn, sbtn, bg, lbg, map_id, area, sort_o,
flags, src_id, wmaa) = row[:12]
out.write(
"REPLACE INTO `custom_ej_instance` "
"(`id`, `name`, `description`, `button_icon`, "
"`small_button_icon`, `background`, `lore_background`, "
"`map_id`, `area_id`, `world_map_area_id`, `flags`, `sort_order`) "
"VALUES ("
f"{_to_int(inst_id)}, {_sql_str(name)}, {_sql_str(desc)}, "
f"{_sql_str(btn)}, {_sql_str(sbtn)}, {_sql_str(bg)}, "
f"{_sql_str(lbg)}, {_to_int(map_id)}, {_to_int(area)}, "
f"{_to_int(wmaa)}, {_to_int(flags)}, {_to_int(sort_o)}"
");\n"
)
counts["instance"] += 1
out.write("\n")
# ---- tier_instance ----
# JOURNALTIERXINSTANCE[instance_id] = {tier_id, sort_order}
for inst_id, row in tables.get("JOURNALTIERXINSTANCE", {}).items():
if _to_int(inst_id) not in surviving_instances:
continue
tier_id = row[0]
sort_o = row[1] if len(row) > 1 else 0
out.write(
"REPLACE INTO `custom_ej_tier_instance` "
"(`tier_id`, `instance_id`, `sort_order`) VALUES "
f"({_to_int(tier_id)}, {_to_int(inst_id)}, {_to_int(sort_o)});\n"
)
counts["tier_instance"] += 1
out.write("\n")
# ---- encounters ----
# JOURNALENCOUNTER[instance_id] = list of rows
# {id, name, desc, map_x, map_y, floor, world_map_area, first_section,
# instance_id, difficulty_mask, flags, sort_order}
for inst_id, rows in tables.get("JOURNALENCOUNTER", {}).items():
if _to_int(inst_id) not in surviving_instances:
continue
for row in rows:
if len(row) < 12:
row = row + [0] * (12 - len(row))
(eid, name, desc, mx, my, floor, wmaa, first_sec, src_inst,
diff, flags, sort_o) = row[:12]
out.write(
"REPLACE INTO `custom_ej_encounter` "
"(`id`, `instance_id`, `name`, `description`, "
"`map_position_x`, `map_position_y`, `floor_index`, "
"`world_map_area_id`, `first_section_id`, `difficulty_mask`, "
"`flags`, `sort_order`) VALUES ("
f"{_to_int(eid)}, {_to_int(inst_id)}, {_sql_str(name)}, "
f"{_sql_str(desc)}, {_to_float(mx):.6f}, {_to_float(my):.6f}, "
f"{_to_uint(floor)}, {_to_uint(wmaa)}, {_to_int(first_sec)}, "
f"{_to_uint(diff)}, {_to_uint(flags)}, {_to_int(sort_o)}"
");\n"
)
counts["encounter"] += 1
out.write("\n")
# ---- creatures ----
# JOURNALENCOUNTERCREATURE[encounter_id] = list of rows
# {name, desc, display_id, icon, encounter_id, sort, id, creature_id,
# difficulty_mask}
for enc_id, rows in tables.get("JOURNALENCOUNTERCREATURE", {}).items():
if not _enc_survives(_to_int(enc_id)):
continue
for row in rows:
if len(row) < 9:
row = row + [0] * (9 - len(row))
(name, desc, display_id, icon, src_enc, sort_o, cid, cr_id,
diff) = row[:9]
cr_id_i = _to_int(cr_id)
if (known_creatures is not None and cr_id_i != 0
and cr_id_i not in known_creatures):
dropped["creature"] += 1
continue
out.write(
"REPLACE INTO `custom_ej_creature` "
"(`id`, `encounter_id`, `creature_id`, `name`, `description`, "
"`creature_display_id`, `icon`, `difficulty_mask`, "
"`sort_order`) VALUES ("
f"{_to_int(cid)}, {_to_int(enc_id)}, {_to_int(cr_id)}, "
f"{_sql_str(name)}, {_sql_str(desc)}, {_to_uint(display_id)}, "
f"{_sql_str(icon)}, {_to_uint(diff)}, {_to_int(sort_o)}"
");\n"
)
counts["creature"] += 1
out.write("\n")
# ---- sections ----
# JOURNALENCOUNTERSECTION[section_id] = single row
# {id, name, desc, display_id, desc_spell, icon_spell, encounter_id,
# next, sub, parent, flags, icon_flags, sort, type, difficulty,
# creature_id}
for sid, row in tables.get("JOURNALENCOUNTERSECTION", {}).items():
if len(row) < 16:
row = row + [0] * (16 - len(row))
(src_sid, name, desc, display_id, desc_spell, icon_spell, enc_id,
next_sid, sub_sid, parent_sid, flags, icon_flags, sort_o, stype,
diff, cr_id) = row[:16]
if not _enc_survives(_to_int(enc_id)):
continue
out.write(
"REPLACE INTO `custom_ej_section` "
"(`id`, `encounter_id`, `parent_section_id`, `next_section_id`, "
"`sub_section_id`, `name`, `description`, `creature_display_id`, "
"`description_spell_id`, `icon_spell_id`, `icon`, `flags`, "
"`icon_flags`, `sort_order`, `type`, `difficulty_mask`, "
"`creature_id`) VALUES ("
f"{_to_int(sid)}, {_to_int(enc_id)}, {_to_int(parent_sid)}, "
f"{_to_int(next_sid)}, {_to_int(sub_sid)}, {_sql_str(name)}, "
f"{_sql_str(desc)}, {_to_uint(display_id)}, "
f"{_to_uint(desc_spell)}, {_to_uint(icon_spell)}, NULL, "
f"{_to_uint(flags)}, {_to_uint(icon_flags)}, {_to_int(sort_o)}, "
f"{_to_uint(stype)}, {_to_uint(diff)}, {_to_uint(cr_id)}"
");\n"
)
counts["section"] += 1
out.write("\n")
# ---- loot ----
# JOURNALENCOUNTERITEM[encounter_id] = list of rows
# {item_id, encounter_id, difficulty_mask, faction_mask, flags, id,
# class_mask}
for enc_id, rows in tables.get("JOURNALENCOUNTERITEM", {}).items():
if not _enc_survives(_to_int(enc_id)):
continue
for row in rows:
if len(row) < 7:
row = row + [0] * (7 - len(row))
(item_id, src_enc, diff, faction, flags, lid, class_mask) = row[:7]
item_i = _to_int(item_id)
if known_items is not None and item_i not in known_items:
dropped["loot"] += 1
continue
out.write(
"REPLACE INTO `custom_ej_loot` "
"(`id`, `encounter_id`, `item_id`, `difficulty_mask`, "
"`faction_mask`, `class_mask`, `flags`, `sort_order`) "
"VALUES ("
f"{_to_int(lid)}, {_to_int(enc_id)}, {_to_int(item_id)}, "
f"{_to_uint(diff)}, {_to_signed(faction)}, "
f"{_to_signed(class_mask)}, {_to_uint(flags)}, "
f"{_to_int(lid)}"
");\n"
)
counts["loot"] += 1
out.write("\n")
out.write("COMMIT;\n")
return counts
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> int:
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--input", "-i", required=True, type=Path,
help="Generated_EncounterJournal.lua")
p.add_argument("--output", "-o", type=Path,
help="Where to write the .sql file (default: stdout)")
p.add_argument("--known-maps", type=Path,
help="File with allowed map ids (one per line). Donor "
"instances whose map_id is not listed are dropped, "
"together with their encounters/creatures/sections/"
"loot.")
p.add_argument("--known-creatures", type=Path,
help="File with allowed creature_template entries. Donor "
"creature cards with creature_id not in the list are "
"dropped.")
p.add_argument("--known-items", type=Path,
help="File with allowed item_template entries. Donor loot "
"rows referencing missing items are dropped.")
args = p.parse_args()
text = args.input.read_text(encoding="utf-8")
tables = _find_top_level_tables(text)
print("parsed tables:", file=sys.stderr)
for name, body in tables.items():
size = len(body) if isinstance(body, (dict, list)) else "?"
print(f" {name:30s} {size}", file=sys.stderr)
known_maps = _load_id_set(args.known_maps)
known_creatures = _load_id_set(args.known_creatures)
known_items = _load_id_set(args.known_items)
if known_maps is not None:
print(f"map whitelist: {len(known_maps)} ids", file=sys.stderr)
if known_creatures is not None:
print(f"creature whitelist: {len(known_creatures)} ids",
file=sys.stderr)
if known_items is not None:
print(f"item whitelist: {len(known_items)} ids", file=sys.stderr)
dropped: dict = {}
if args.output:
with args.output.open("w", encoding="utf-8") as f:
counts = emit(tables, f, known_maps, known_creatures,
known_items, dropped)
else:
counts = emit(tables, sys.stdout, known_maps, known_creatures,
known_items, dropped)
print("emitted rows:", file=sys.stderr)
for k, v in counts.items():
print(f" {k:14s} {v}", file=sys.stderr)
if any(dropped.values()):
print("dropped (custom Sirus entries):", file=sys.stderr)
for k, v in dropped.items():
if v:
print(f" {k:14s} {v}", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())