Add safe snapshot and segment assembly

Add envelope-aware snapshot and segment assembly for structured Monopoly and Arc history records.
This commit is contained in:
Golumpa 2026-07-15 19:48:11 +01:00
parent a5312520e7
commit 73bb100cd6
8 changed files with 601 additions and 23 deletions

View file

@ -22,7 +22,12 @@ 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.decoder.structured_protocol import StructuredRecord, parse_structured_records
from nte_history_exporter.decoder.structured_protocol import (
StructuredProtocolAssembler,
StructuredRecord,
parse_structured_blocks,
parse_structured_records,
)
def is_arc_history_request(content: bytes) -> bool:
@ -125,7 +130,7 @@ def _arc_metadata(arc_id: str) -> dict[str, Any]:
return next((meta for item_id, meta in ARC_META.items() if item_id.casefold() == folded), {})
def _structured_arc_rows(structured_rows: list[StructuredRecord]) -> list[dict[str, Any]]:
def structured_arc_rows(structured_rows: list[StructuredRecord]) -> list[dict[str, Any]]:
records = []
for structured in structured_rows:
meta = _arc_metadata(structured.item_id)
@ -149,6 +154,7 @@ def _structured_arc_rows(structured_rows: list[StructuredRecord]) -> list[dict[s
"decoder_mode": "structured_fallback",
"structured_pool_id": structured.pool_id,
"structured_protocol_view": structured.protocol_view,
"structured_generation_index": structured.generation_index,
}
)
return records
@ -176,10 +182,10 @@ def parse_arc_response(response: bytes) -> list[dict[str, Any]]:
legacy_rows = _parse_legacy_arc_response(response)
if legacy_rows:
return _enrich_legacy_arc_rows(legacy_rows, structured_rows)
return _structured_arc_rows(structured_rows)
return structured_arc_rows(structured_rows)
def build_arc_rows_from_pairs(pairs: list[tuple]) -> list[dict[str, Any]]:
def _build_primary_arc_rows_from_pairs(pairs: list[tuple]) -> list[dict[str, Any]]:
pool = POOL_META["arc_miracle_box"]
rows: list[dict[str, Any]] = []
for pair in pairs:
@ -209,6 +215,48 @@ def build_arc_rows_from_pairs(pairs: list[tuple]) -> list[dict[str, Any]]:
return rows
def build_arc_rows_from_pairs(pairs: list[tuple]) -> list[dict[str, Any]]:
primary_rows = _build_primary_arc_rows_from_pairs(pairs)
if primary_rows and any(row.get("decoder_mode") != "structured_fallback" for row in primary_rows):
return primary_rows
assembler = StructuredProtocolAssembler()
for source_index, pair in enumerate(pairs):
assembler.add_blocks(parse_structured_blocks(pair[6], "fork", source_index=source_index))
assembled = assembler.rows("fork")
if not assembled:
return primary_rows
pool = POOL_META["arc_miracle_box"]
rows = []
for row_index, (record, structured) in enumerate(
zip(structured_arc_rows(assembled), assembled), start=1
):
source_index = structured.source_index or 0
pair = pairs[source_index]
page, offset, req_i, req_ts, resp_i, resp_ts, response = pair[:7]
rows.append(
{
**record,
"page": page,
"offset": offset,
"row": row_index,
"pool_group_id": pool["id"],
"pool_group_name": pool["name"],
"request_msg": req_i,
"request_time_utc": fmt_packet_time(req_ts),
"response_msg": resp_i,
"response_time_utc": fmt_packet_time(resp_ts),
"response_len": len(response),
"record_count": len(assembled),
"structured_assembly": "snapshot_segments",
"structured_assembly_warning_count": len(assembler.warnings),
}
)
annotate_arc_groups(rows)
return rows
def make_arc_uid(timestamp_raw: str, ordinal: int) -> str:
source = "|".join([GAME_UID_PART, ARC_SYSTEM, ARC_BANNER_ID, timestamp_raw, str(ordinal)])
return hashlib.sha256(source.encode("utf-8")).hexdigest()[:32]