encounter journal base logic
This commit is contained in:
@@ -85,11 +85,15 @@ console.
|
||||
|
||||
| Command | What it does |
|
||||
| ---------------- | ---------------------------------------------------------------------------- |
|
||||
| `.ej info` | Print row counts of the in-memory cache. |
|
||||
| `.ej reload` | Re-read all `custom_ej_*` tables. |
|
||||
| `.ej validate` | Run integrity checks (see "Validation" below). |
|
||||
| `.ej importloot` | Auto-populate `custom_ej_loot` from `creature_loot_template` (skips dupes). |
|
||||
| `.ej export` | Write `EncounterJournalData.json` (and `.lua`) to the configured path. |
|
||||
| Command | What it does |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------- |
|
||||
| `.ej info` | Print row counts of the in-memory cache. |
|
||||
| `.ej reload` | Re-read all `custom_ej_*` tables. |
|
||||
| `.ej validate` | Run integrity checks (see "Validation" below). |
|
||||
| `.ej importskeleton` | Generate instance/encounter/creature/section rows from `instance_template` + `instance_encounters`. |
|
||||
| `.ej importloot` | Auto-populate `custom_ej_loot` from `creature_loot_template` (skips dupes). |
|
||||
| `.ej importall` | Run `importskeleton` then `importloot` back-to-back. |
|
||||
| `.ej export` | Write `EncounterJournalData.json` (and `.lua`) to the configured path. |
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -1199,7 +1199,9 @@ namespace MoonWell::EncounterJournal
|
||||
|
||||
CreatureTemplate const* ct = sObjectMgr->GetCreatureTemplate(creature);
|
||||
std::string crName = (ct && !ct->Name.empty()) ? ct->Name : cmt;
|
||||
uint32 displayId = ct ? ct->Modelid1 : 0;
|
||||
uint32 displayId = 0;
|
||||
if (ct && !ct->Models.empty())
|
||||
displayId = ct->Models[0].CreatureDisplayID;
|
||||
|
||||
std::string crEsc;
|
||||
crEsc.reserve(crName.size());
|
||||
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user