Canonicalize reward IDs during case-insensitive mapping

This commit is contained in:
Golumpa 2026-08-19 13:10:33 +01:00
parent 5fe39b96ae
commit 7f7ad80b46
9 changed files with 64 additions and 38 deletions

View file

@ -21,7 +21,7 @@ from nte_history_exporter.constants import (
)
from nte_history_exporter.decoder.boundary import select_continuous_run_from_page_1
from nte_history_exporter.decoder.run import fmt_packet_time
from nte_history_exporter.mappings import ARC_META
from nte_history_exporter.mappings import ARC_META_BY_CASEFOLD
from nte_history_exporter.decoder.structured_protocol import (
StructuredProtocolAssembler,
StructuredRecord,
@ -95,7 +95,7 @@ def _parse_legacy_arc_response(response: bytes) -> list[dict[str, Any]]:
timestamp_raw = response[pos : pos + 8]
pos += 8
arc_id = decode_arc_key(name_raw) or name_raw.hex()
meta = ARC_META.get(arc_id, {})
canonical_id, meta = _resolve_arc_metadata(arc_id)
try:
ticks, unix_seconds, timestamp_decoded = decode_arc_timestamp(timestamp_raw)
except ValueError:
@ -107,7 +107,7 @@ def _parse_legacy_arc_response(response: bytes) -> list[dict[str, Any]]:
"record_len": pos - start,
"reward_key_hex": name_raw.hex(),
"reward_type": "arc",
"reward_id": arc_id,
"reward_id": canonical_id,
"reward_name": meta.get("name", "UNKNOWN"),
"reward_rank": meta.get("rank", ""),
"type_key_hex": type_raw.hex(),
@ -122,18 +122,15 @@ def _parse_legacy_arc_response(response: bytes) -> list[dict[str, Any]]:
return records
def _arc_metadata(arc_id: str) -> dict[str, Any]:
direct = ARC_META.get(arc_id)
if direct is not None:
return direct
folded = arc_id.casefold()
return next((meta for item_id, meta in ARC_META.items() if item_id.casefold() == folded), {})
def _resolve_arc_metadata(arc_id: str) -> tuple[str, dict[str, Any]]:
canonical_id, meta = ARC_META_BY_CASEFOLD.get(arc_id.casefold(), (arc_id, {}))
return canonical_id, meta
def structured_arc_rows(structured_rows: list[StructuredRecord]) -> list[dict[str, Any]]:
records = []
for structured in structured_rows:
meta = _arc_metadata(structured.item_id)
canonical_id, meta = _resolve_arc_metadata(structured.item_id)
records.append(
{
"record_start": structured.record_start,
@ -141,7 +138,7 @@ def structured_arc_rows(structured_rows: list[StructuredRecord]) -> list[dict[st
"record_len": structured.record_end - structured.record_start,
"reward_key_hex": "",
"reward_type": "arc",
"reward_id": structured.item_id,
"reward_id": canonical_id,
"reward_name": meta.get("name", "UNKNOWN"),
"reward_rank": meta.get("rank", ""),
"type_key_hex": "",

View file

@ -24,16 +24,11 @@ from nte_history_exporter.constants import (
from nte_history_exporter.decoder.boundary import select_continuous_run_from_page_1
from nte_history_exporter.decoder.protocol import infer_reward_type
from nte_history_exporter.decoder.run import fmt_packet_time
from nte_history_exporter.mappings import REWARDS_BY_ID
from nte_history_exporter.mappings import REWARDS_BY_CASEFOLD
DOTNET_TICKS_PER_SECOND = 10_000_000
MAX_RECORDS_PER_BLOCK = 100
MAX_REWARD_ID_LENGTH = 256
REWARDS_BY_CASEFOLDED_ID = {
reward_id.casefold(): reward for reward_id, reward in REWARDS_BY_ID.items()
}
def is_mystery_box_history_request(content: bytes) -> bool:
if len(content) < MYSTERY_BOX_HISTORY_REQUEST_LENGTH:
return False
@ -108,16 +103,15 @@ def _parse_view(data: bytes) -> list[dict[str, Any]]:
timestamp_raw = data[pos : pos + 8]
pos += 8
ticks, unix_seconds, timestamp_decoded = _decode_timestamp(timestamp_raw)
reward = REWARDS_BY_ID.get(reward_id) or REWARDS_BY_CASEFOLDED_ID.get(
reward_id.casefold(), {}
)
reward = REWARDS_BY_CASEFOLD.get(reward_id.casefold(), {})
canonical_reward_id = reward.get("id", reward_id)
rows.append(
{
"record_start": record_start,
"record_end": pos,
"record_len": pos - record_start,
"reward_type": reward.get("type") or infer_reward_type(reward_id),
"reward_id": reward_id,
"reward_id": canonical_reward_id,
"reward_name": reward.get("name", ""),
"reward_rank": reward.get("rank"),
"quantity": quantity,

View file

@ -15,7 +15,7 @@ from nte_history_exporter.constants import (
TIMESTAMP_TICKS_PER_SECOND,
VALID_DICE_FIELDS,
)
from nte_history_exporter.mappings import REWARDS_BY_ID
from nte_history_exporter.mappings import REWARDS_BY_CASEFOLD
from nte_history_exporter.decoder.structured_protocol import (
StructuredRecord,
parse_structured_records,
@ -61,11 +61,12 @@ def decode_reward_key(raw: bytes) -> str:
def infer_reward_type(reward_id: str) -> str:
if not reward_id:
return ""
if reward_id.startswith("fork_"):
folded = reward_id.casefold()
if folded.startswith("fork_"):
return "arc"
if reward_id.isdigit():
return "character"
if reward_id.startswith("Fashion_"):
if folded.startswith("fashion_"):
return "cosmetic"
return "item"
@ -165,7 +166,7 @@ def classify_result_type(
) -> tuple[str, int | None]:
if dice is None or dice_offset is None:
return "unknown", None
if reward_id == "Dice_ticket_01" and WARP_PIECE_CHASE_PATTERN in chunk_without_marker:
if reward_id.casefold() == "dice_ticket_01" and WARP_PIECE_CHASE_PATTERN in chunk_without_marker:
return "chase_reward", -4
if dice == 0:
return "points_gift", 0
@ -182,13 +183,14 @@ def classify_result_type(
def guess_quantity(chunk_hex: str, reward_id: str, result_type: str | None = None) -> int | None:
if reward_id == "Dice_ticket_01":
folded = reward_id.casefold()
if folded == "dice_ticket_01":
if result_type == "chase_reward":
return 30
return 4
if reward_id == "DiceNormal":
if folded == "dicenormal":
return 1
if reward_id == "Dice_ticket_02":
if folded == "dice_ticket_02":
if "c8b0d4c0" in chunk_hex:
return 50
if "c8b0ccc0" in chunk_hex:
@ -266,7 +268,8 @@ def _decode_aligned_response_records(response_content: bytes) -> list[dict[str,
elif result_type == "chase_reward":
dice = -4
dice_raw = -4
reward = REWARDS_BY_ID.get(reward_id, {})
reward = _reward_metadata(reward_id)
canonical_reward_id = reward.get("id", reward_id)
timestamp_raw = response_content[marker_offset + len(marker) : marker_offset + len(marker) + 8]
timestamp_ticks, timestamp_unix, timestamp_decoded = decode_history_timestamp(timestamp_raw)
chunk_hex = chunk.hex()
@ -288,7 +291,7 @@ def _decode_aligned_response_records(response_content: bytes) -> list[dict[str,
"dice_offset_in_record": dice_offset,
"reward_key_hex": key_hex,
"reward_type": reward.get("type") or infer_reward_type(reward_id),
"reward_id": reward_id,
"reward_id": canonical_reward_id,
"reward_name": reward.get("name", ""),
"reward_rank": reward.get("rank"),
"quantity": guess_quantity(chunk_hex, reward_id, result_type),
@ -335,8 +338,8 @@ def _enrich_heuristic_rows(
return heuristic_rows
for heuristic, structured in zip(heuristic_rows, structured_rows):
if not heuristic.get("reward_id"):
heuristic["reward_id"] = structured.item_id
reward = _reward_metadata(structured.item_id)
heuristic["reward_id"] = reward.get("id", structured.item_id)
heuristic["reward_type"] = reward.get("type") or infer_reward_type(structured.item_id)
heuristic["reward_name"] = reward.get("name", "")
heuristic["reward_rank"] = reward.get("rank")
@ -361,11 +364,7 @@ def _enrich_heuristic_rows(
def _reward_metadata(reward_id: str) -> dict[str, Any]:
direct = REWARDS_BY_ID.get(reward_id)
if direct is not None:
return direct
folded = reward_id.casefold()
return next((meta for item_id, meta in REWARDS_BY_ID.items() if item_id.casefold() == folded), {})
return REWARDS_BY_CASEFOLD.get(reward_id.casefold(), {})
def structured_monopoly_rows(structured_rows: list[StructuredRecord]) -> list[dict[str, Any]]:
@ -373,6 +372,7 @@ def structured_monopoly_rows(structured_rows: list[StructuredRecord]) -> list[di
for row_index, structured in enumerate(structured_rows, start=1):
dice, dice_raw, result_type, result_source = _structured_result(structured.roll_points_raw)
reward = _reward_metadata(structured.item_id)
canonical_reward_id = reward.get("id", structured.item_id)
rows.append(
{
"row": row_index,
@ -391,7 +391,7 @@ def structured_monopoly_rows(structured_rows: list[StructuredRecord]) -> list[di
"dice_offset_in_record": None,
"reward_key_hex": "",
"reward_type": reward.get("type") or infer_reward_type(structured.item_id),
"reward_id": structured.item_id,
"reward_id": canonical_reward_id,
"reward_name": reward.get("name", ""),
"reward_rank": reward.get("rank"),
"quantity": structured.count,

View file

@ -14,6 +14,9 @@ def load_mapping_file(filename: str) -> dict[str, Any]:
ARC_META: dict[str, dict[str, Any]] = load_mapping_file("arcs.json")
ARC_META_BY_CASEFOLD = {
arc_id.casefold(): (arc_id, meta) for arc_id, meta in ARC_META.items()
}
CHARACTERS: dict[str, dict[str, Any]] = load_mapping_file("characters.json")
ITEMS: dict[str, dict[str, Any]] = load_mapping_file("items.json")
ACHIEVEMENTS: dict[str, dict[str, Any]] = load_mapping_file("achievements.json")
@ -36,3 +39,6 @@ for _character_id, _meta in CHARACTERS.items():
}
for _item_id, _meta in ITEMS.items():
REWARDS_BY_ID[_item_id] = {"type": _meta["type"], "id": _item_id, "name": _meta["name"], "rank": _meta.get("rank")}
REWARDS_BY_CASEFOLD = {
reward_id.casefold(): reward for reward_id, reward in REWARDS_BY_ID.items()
}