basic noita parsing
This commit is contained in:
@@ -0,0 +1,291 @@
|
||||
"""
|
||||
Decode a polylan_mod_log.txt.
|
||||
|
||||
Hash format: sha1(seed|timestamp|id) — timestamp is bound into the hash so
|
||||
it cannot be altered without breaking the signature.
|
||||
|
||||
Seed-less entries (INIT, DEBUG, mod checks) use sha1(id) with no seed or
|
||||
timestamp — those are resolved via a static lookup.
|
||||
|
||||
Usage:
|
||||
python decode_log.py [path/to/polylan_mod_log.txt]
|
||||
|
||||
Default path: ~/.local/share/Steam/steamapps/common/Noita/polylan_mod_log.txt
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
from functools import cache
|
||||
from noita.services.spells import ALL_SPELLS, ALL_PERKS
|
||||
|
||||
|
||||
SEED_POOL = [
|
||||
# General good seeds
|
||||
3154823,
|
||||
3718311,
|
||||
10064758,
|
||||
123156801,
|
||||
1024089369,
|
||||
1026967166,
|
||||
# Pacifist seeds
|
||||
177795258,
|
||||
520542929,
|
||||
10600249,
|
||||
25300740,
|
||||
21875589,
|
||||
24085389,
|
||||
59775105,
|
||||
44190726,
|
||||
1039649471,
|
||||
1072607354,
|
||||
# Perk combo seeds
|
||||
839747651,
|
||||
839844768,
|
||||
840909713,
|
||||
839959129,
|
||||
840016192,
|
||||
840039886,
|
||||
840398606,
|
||||
840439045,
|
||||
840457463,
|
||||
840492754,
|
||||
840507802,
|
||||
840513742,
|
||||
840542079,
|
||||
840574169,
|
||||
840610974,
|
||||
840626894,
|
||||
840872436,
|
||||
840894605,
|
||||
841221188,
|
||||
]
|
||||
|
||||
DEATH_PENALTY = 1
|
||||
|
||||
# All scoreable events: base 1 pt for every spell and perk, overrides below.
|
||||
POINTS = {
|
||||
# ── Spells ───────────────────────────────────────────────────────────────
|
||||
**{sid: 1 for sid in ALL_SPELLS},
|
||||
"ADD_TRIGGER": 10,
|
||||
"NOLLA": 10,
|
||||
"CHAOTIC_TRANSMUTATION": 10,
|
||||
"DUPLICATE": 5,
|
||||
"OMEGA": 10,
|
||||
"HEALING_BOLT": 5,
|
||||
# ── Perks ─────────────────────────────────────────────────────────────────
|
||||
**{pid: 1 for pid in ALL_PERKS},
|
||||
"EDIT_WANDS_EVERYWHERE": 10,
|
||||
# ── Spell combos ─────────────────────────────────────────────────────────
|
||||
"PING_PONG_DRILL": 10,
|
||||
"HEAVY_SHOT_DISC": 10,
|
||||
"TOUCH_OF_ANY": 5,
|
||||
"TWO_ARC_MODIFIERS": 10,
|
||||
"TWO_TRAIL_MODIFIERS": 10,
|
||||
# ── Perk combos ──────────────────────────────────────────────────────────
|
||||
"CRIMSON_ALCHEMIST": 15,
|
||||
"GREEDY_GOBLIN_KING": 15,
|
||||
"STORM_TOUCHED_ASCENDANT": 15,
|
||||
"ARCHMAGE_OF_CONTROL": 15,
|
||||
"HAUNTED_MAGE": 15,
|
||||
"GLASS_CANNON_MESSIAH": 15,
|
||||
"INFINITE_ENGINE": 15,
|
||||
"PERFECT_ACCURACY_LOOP": 15,
|
||||
"HOMING_DEATH_SWARM": 15,
|
||||
"UNTOUCHABLE_FIELD": 15,
|
||||
"ELECTRIC_SUSTAIN_LOOP": 15,
|
||||
"IMMORTAL_LEECH_CORE": 15,
|
||||
"PROJECTILE_OVERLOAD": 15,
|
||||
"CLOSE_RANGE_DEATH_MACHINE": 15,
|
||||
"CRITICAL_MASS": 15,
|
||||
"HOLY_MOUNTAIN_ABUSER": 15,
|
||||
"DEFLECTOR_MATRIX": 15,
|
||||
"STORMBORNE_LEVITATOR": 15,
|
||||
# ── Objectives ───────────────────────────────────────────────────────────
|
||||
"HP_200": 5,
|
||||
"HP_500": 5,
|
||||
"HP_2000": 5,
|
||||
"HP_5000": 5,
|
||||
"GOLD_1000": 5,
|
||||
"GOLD_10000": 5,
|
||||
"GOLD_100000": 5,
|
||||
"GOLD_1000000": 5,
|
||||
**{f"ORB_{i}": 5 for i in range(34)},
|
||||
"FIND_AMBROSIA": 10,
|
||||
"WAND_MANA_500": 10,
|
||||
"WAND_MANA_1000": 10,
|
||||
"WAND_MANA_1500": 10,
|
||||
"WAND_CAPACITY_10": 10,
|
||||
"WAND_CAPACITY_20": 10,
|
||||
"BOSS_KILL": 100,
|
||||
"CRIMSON_ALCHEMIST-BOSS_KILL": 100,
|
||||
"GREEDY_GOBLIN_KING-BOSS_KILL": 100,
|
||||
"STORM_TOUCHED_ASCENDANT-BOSS_KILL": 100,
|
||||
"ARCHMAGE_OF_CONTROL-BOSS_KILL": 100,
|
||||
"HAUNTED_MAGE-BOSS_KILL": 100,
|
||||
"GLASS_CANNON_MESSIAH-BOSS_KILL": 100,
|
||||
"INFINITE_ENGINE-BOSS_KILL": 100,
|
||||
"PERFECT_ACCURACY_LOOP-BOSS_KILL": 100,
|
||||
"HOMING_DEATH_SWARM-BOSS_KILL": 100,
|
||||
"UNTOUCHABLE_FIELD-BOSS_KILL": 100,
|
||||
"ELECTRIC_SUSTAIN_LOOP-BOSS_KILL": 100,
|
||||
"IMMORTAL_LEECH_CORE-BOSS_KILL": 100,
|
||||
"PROJECTILE_OVERLOAD-BOSS_KILL": 100,
|
||||
"CLOSE_RANGE_DEATH_MACHINE-BOSS_KILL": 100,
|
||||
"CRITICAL_MASS-BOSS_KILL": 100,
|
||||
"HOLY_MOUNTAIN_ABUSER-BOSS_KILL": 100,
|
||||
"DEFLECTOR_MATRIX-BOSS_KILL": 100,
|
||||
"STORMBORNE_LEVITATOR-BOSS_KILL": 100,
|
||||
}
|
||||
|
||||
# Perk-combo IDs — used to award a per-combo boss-kill bonus.
|
||||
PERK_COMBO_IDS = {
|
||||
"CRIMSON_ALCHEMIST",
|
||||
"GREEDY_GOBLIN_KING",
|
||||
"STORM_TOUCHED_ASCENDANT",
|
||||
"ARCHMAGE_OF_CONTROL",
|
||||
"HAUNTED_MAGE",
|
||||
"GLASS_CANNON_MESSIAH",
|
||||
"INFINITE_ENGINE",
|
||||
"PERFECT_ACCURACY_LOOP",
|
||||
"HOMING_DEATH_SWARM",
|
||||
"UNTOUCHABLE_FIELD",
|
||||
"ELECTRIC_SUSTAIN_LOOP",
|
||||
"IMMORTAL_LEECH_CORE",
|
||||
"PROJECTILE_OVERLOAD",
|
||||
"CLOSE_RANGE_DEATH_MACHINE",
|
||||
"CRITICAL_MASS",
|
||||
"HOLY_MOUNTAIN_ABUSER",
|
||||
"DEFLECTOR_MATRIX",
|
||||
"STORMBORNE_LEVITATOR",
|
||||
}
|
||||
|
||||
BOSS_KILL_COMBO_BONUS = 100 # default bonus per active perk combo on boss kill
|
||||
|
||||
# IDs tried with the seed+timestamp scheme.
|
||||
_ALL_IDS = list(POINTS.keys()) + ["DEATH"]
|
||||
|
||||
# Seed-less hashes (sha1(id) only, no seed, no timestamp).
|
||||
_STATIC_LOOKUP = {
|
||||
hashlib.sha1(k.encode()).hexdigest(): k
|
||||
for k in ["-", "DEBUG", "polylan-mod", "cheatgui", "alchemy_recipes_display"]
|
||||
}
|
||||
|
||||
|
||||
def _sha1(text: str) -> str:
|
||||
return hashlib.sha1(text.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
@cache
|
||||
def resolve(
|
||||
hash_val: str, ts: str, preferred_seed: int | None = None
|
||||
) -> tuple[str | None, int | None]:
|
||||
"""Return (name, seed) or (None, None). preferred_seed is tried first."""
|
||||
if hash_val in _STATIC_LOOKUP:
|
||||
return _STATIC_LOOKUP[hash_val], None
|
||||
|
||||
seeds = (
|
||||
[preferred_seed] + [s for s in SEED_POOL if s != preferred_seed]
|
||||
if preferred_seed is not None
|
||||
else SEED_POOL
|
||||
)
|
||||
|
||||
for seed in seeds:
|
||||
prefix = f"{seed}|{ts}|"
|
||||
for name in _ALL_IDS:
|
||||
if _sha1(prefix + name) == hash_val:
|
||||
return name, seed
|
||||
|
||||
return None, None
|
||||
|
||||
|
||||
def parse_log(file) -> list:
|
||||
entries = []
|
||||
for il, line in enumerate(file.split("\n")):
|
||||
m = re.match(r"\[(.+?)\] ([0-9a-f]{40})", line.rstrip())
|
||||
if m:
|
||||
entries.append({"ts": m.group(1), "hash": m.group(2)})
|
||||
continue
|
||||
|
||||
print(f"Unable to parse line number {il:>4}: {line.strip()}??")
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def decode(file) -> None:
|
||||
entries = parse_log(file)
|
||||
|
||||
if not entries:
|
||||
print("No entries found in log.")
|
||||
return
|
||||
|
||||
seen = set()
|
||||
total = 0
|
||||
deaths = 0
|
||||
have_init = False
|
||||
have_debug = False
|
||||
known_seed = None # cached once first seeded entry is resolved
|
||||
current_run_perk_combos = set() # perk combos seen since the last "-" entry
|
||||
|
||||
for entry in entries:
|
||||
name, seed = resolve(entry["hash"], entry["ts"], known_seed)
|
||||
|
||||
if name is None:
|
||||
print(f" ???? (unresolved) {entry['hash']} ts: {entry['ts']}")
|
||||
continue
|
||||
|
||||
if seed is not None and known_seed is None:
|
||||
known_seed = seed
|
||||
|
||||
if name == "-":
|
||||
have_init = True
|
||||
current_run_perk_combos = set()
|
||||
continue
|
||||
|
||||
if name == "DEBUG":
|
||||
have_debug = True
|
||||
continue
|
||||
|
||||
if name == "DEATH":
|
||||
deaths += 1
|
||||
continue
|
||||
|
||||
# skip mod-presence entries
|
||||
if name not in POINTS:
|
||||
continue
|
||||
|
||||
if name in seen:
|
||||
continue
|
||||
|
||||
if name not in {"BOSS_KILL"}:
|
||||
seen.add(name)
|
||||
|
||||
if name in PERK_COMBO_IDS:
|
||||
current_run_perk_combos.add(name)
|
||||
|
||||
pts = POINTS.get(name, 0)
|
||||
if pts:
|
||||
print(f" +{pts:>4} {name:<40} first seen: {entry['ts']}")
|
||||
total += pts
|
||||
|
||||
if name == "BOSS_KILL":
|
||||
for combo_id in sorted(current_run_perk_combos):
|
||||
bonus_key = f"{combo_id}-BOSS_KILL"
|
||||
if bonus_key not in seen:
|
||||
seen.add(bonus_key)
|
||||
bonus_pts = POINTS.get(bonus_key, BOSS_KILL_COMBO_BONUS)
|
||||
print(f" +{bonus_pts:>4} {bonus_key:<40} combo bonus")
|
||||
total += bonus_pts
|
||||
|
||||
death_deduction = deaths * DEATH_PENALTY
|
||||
if deaths:
|
||||
print(
|
||||
f" -{death_deduction:>4} ({deaths} death{'s' if deaths > 1 else ''} × {DEATH_PENALTY} pts)"
|
||||
)
|
||||
|
||||
if have_init:
|
||||
print(
|
||||
f"\nTotal: {total - death_deduction} pts ({total} earned − {death_deduction} penalty)"
|
||||
)
|
||||
|
||||
if have_debug:
|
||||
print("Note: DEBUG mode was active during at least one session.")
|
||||
Reference in New Issue
Block a user