rabukasim / engine /models /bytecode_readable.py
trioskosmos's picture
Upload folder using huggingface_hub
463f868 verified
import re
from typing import List
from engine.models.ability_filter import format_filter_attr
from engine.models.generated_metadata import (
CARD_TYPES,
CHARACTER_IDS,
COMPARISONS,
CONDITIONS,
COSTS,
COUNT_SOURCES,
GROUP_IDS,
HEART_COLOR_MAP,
META_RULE_TYPES,
OPCODES,
SLOT_INDICES,
TARGETS,
TRIGGERS,
UNIT_IDS,
ZONES,
)
from engine.models.generated_packer import unpack_a_heart_cost, unpack_s_standard, unpack_v_heart_counts, unpack_v_look_choose
OP = {name: int(value) for name, value in OPCODES.items()}
OPCODE_NAMES = {int(value): name for name, value in OPCODES.items()}
TRIGGER_NAMES = {int(value): name for name, value in TRIGGERS.items()}
CONDITION_NAMES = {int(value): name for name, value in CONDITIONS.items()}
COST_NAMES = {int(value): name for name, value in COSTS.items()}
TARGET_NAMES = {int(value): name for name, value in TARGETS.items()}
ZONE_NAMES = {int(value): name for name, value in ZONES.items()}
COMPARISON_NAMES = {int(value): name for name, value in COMPARISONS.items()}
COUNT_SOURCE_NAMES = {int(value): name for name, value in COUNT_SOURCES.items()}
META_RULE_NAMES = {int(value): name for name, value in META_RULE_TYPES.items()}
HEART_COLOR_NAMES = {int(value): name.lower() for name, value in HEART_COLOR_MAP.items()}
CHARACTER_NAMES = {int(value): name.title() for name, value in CHARACTER_IDS.items()}
GROUP_NAMES = {int(value): name.title() for name, value in GROUP_IDS.items()}
UNIT_NAMES = {int(value): name.title().replace("_", " ") for name, value in UNIT_IDS.items()}
CARD_TYPE_NAMES = {int(value): name.title() for name, value in CARD_TYPES.items()}
SPECIAL_ID_NAMES = {
0: "None",
1: "Self",
2: "Other than Self",
3: "Other than Activator",
}
SLOT_NAMES = {int(value): name for name, value in SLOT_INDICES.items()}
SLOT_NAMES.update(
{
0: "Left Stage (0)",
1: "Center Stage (1)",
2: "Right Stage (2)",
4: "Context Card",
6: "Hand (Zone)",
7: "Discard (Zone)",
10: "Choice Target",
13: "Live Slot 0",
14: "Live Slot 1",
15: "Live Slot 2",
20: "Player Select",
}
)
for name, value in CONDITIONS.items():
OPCODE_NAMES.setdefault(int(value), f"CHECK_{name}")
for name, value in COSTS.items():
OPCODE_NAMES.setdefault(int(value), f"COST_{name}")
# Manual overrides for unmapped or special opcodes
if "UNIQUE_NAMES_COUNT" not in CONDITIONS:
# Use 316 as a virtual opcode for documentation if needed,
# but the interpreter often sees 0 for unmapped conditions.
CONDITION_NAMES[316] = "UNIQUE_NAMES_COUNT"
OPCODE_NAMES[316] = "CHECK_UNIQUE_NAMES_COUNT"
def opcode_name(opcode: int, v: int = 0, a: int = 0, s: int = 0) -> str:
if opcode == 0 and (v != 0 or a != 0 or s != 0):
# In SUNNY DAY SONG and others, opcode 0 with condition-like slot params
# is used for conditions handled by the interpreter's higher-level logic.
return "CHECK_UNIQUE_NAMES_COUNT?"
return OPCODE_NAMES.get(int(opcode), f"OP_{opcode}")
def trigger_name(trigger: int) -> str:
return TRIGGER_NAMES.get(int(trigger), f"TRIGGER_{trigger}")
AREA_NAMES = {
0: "any",
1: "left",
2: "center",
3: "right",
}
_FLAG_25_BATON_OPS = {OP.get("PLAY_MEMBER_FROM_HAND"), OP.get("PLAY_MEMBER_FROM_DISCARD")}
_FLAG_25_CAPTURE_OPS = {OP.get("SELECT_MEMBER"), OP.get("MOVE_TO_DISCARD")}
_FLAG_25_REVEAL_OPS = {OP.get("REVEAL_UNTIL")}
RE_ID_REPLACEMENT = re.compile(r"(group|unit|char\d|type|special)=(\d+)")
def decode_filter(filter_attr: int) -> str:
if filter_attr == 0:
return "none"
raw_filter = format_filter_attr(filter_attr)
def replacer(match):
key = match.group(1)
val = int(match.group(2))
if key == "group":
return f"group={GROUP_NAMES.get(val, val)}"
if key == "unit":
return f"unit={UNIT_NAMES.get(val, val)}"
if key.startswith("char"):
return f"{key}={CHARACTER_NAMES.get(val, val)}"
if key == "type":
return f"type={CARD_TYPE_NAMES.get(val, val)}"
return match.group(0)
return RE_ID_REPLACEMENT.sub(replacer, raw_filter)
def _slot_name(slot_id: int) -> str:
if slot_id in SLOT_NAMES:
return SLOT_NAMES[slot_id]
target_name = TARGET_NAMES.get(slot_id)
if target_name:
return target_name.title()
return f"Slot_{slot_id}"
def _zone_name(zone_id: int) -> str:
zone_name = ZONE_NAMES.get(zone_id)
if zone_name:
zone_aliases = {
"DEFAULT": "Default",
"DECK_TOP": "Deck Top",
"DECK_BOTTOM": "Deck Bottom",
"ENERGY": "Energy",
"STAGE": "Stage",
"DECK": "Deck",
"HAND": "Hand",
"DISCARD": "Discard",
"LIVE_SET": "Live Set",
"SUCCESS_PILE": "Success Pile",
"YELL": "Yell",
}
return zone_aliases.get(zone_name, zone_name.title())
return f"Zone_{zone_id}"
def _comparison_name(comp_id: int) -> str:
comparison_name = COMPARISON_NAMES.get(comp_id)
if comparison_name:
return {
"EQ": "EQ (==)",
"GT": "GT (>)",
"LT": "LT (<)",
"GE": "GE (>=)",
"LE": "LE (<=)",
}.get(comparison_name, comparison_name)
return f"C_{comp_id}"
def _flag_25_label(opcode: int | None) -> str:
if opcode in _FLAG_25_BATON_OPS:
return "baton_slot"
if opcode in _FLAG_25_CAPTURE_OPS:
return "capture_value"
if opcode in _FLAG_25_REVEAL_OPS:
return "reveal_until_live"
return "flag25"
def decode_standard_slot(raw_slot: int, opcode: int | None = None) -> str:
if raw_slot == 0:
return "none"
slot = unpack_s_standard(raw_slot & 0xFFFFFFFF)
parts: List[str] = []
if slot["target_slot"]:
parts.append(f"target={_slot_name(slot['target_slot'])}")
if slot["remainder_zone"]:
if slot["is_dynamic"]:
parts.append(
f"multiplier_source={COUNT_SOURCE_NAMES.get(slot['remainder_zone'], _zone_name(slot['remainder_zone']))}"
)
else:
parts.append(f"remainder={_zone_name(slot['remainder_zone'])}")
if slot["source_zone"]:
parts.append(f"source={_zone_name(slot['source_zone'])}")
if slot["dest_zone"]:
parts.append(f"dest={_zone_name(slot['dest_zone'])}")
if slot["is_opponent"]:
parts.append("opponent")
if slot["is_reveal_until_live"]:
parts.append(_flag_25_label(opcode))
if slot["is_empty_slot"]:
parts.append("empty_slot")
if slot["is_wait"]:
parts.append("wait")
if slot["is_dynamic"]:
parts.append("dynamic")
if slot["area_idx"]:
parts.append(f"area={AREA_NAMES.get(slot['area_idx'], slot['area_idx'])}")
return ", ".join(parts)
def decode_condition_slot(raw_slot: int) -> str:
comp_val = (raw_slot >> 4) & 0x0F
base_slot = raw_slot & 0x0F
area_val = (raw_slot >> 29) & 0x07
parts = [
f"compare={_comparison_name(comp_val)}",
f"slot={_slot_name(base_slot)}",
]
if area_val:
parts.append(f"area={AREA_NAMES.get(area_val, area_val)}")
return ", ".join(parts)
def get_legend_str() -> str:
lines = ["\n--- BYTECODE LEGEND ---"]
lines.append("Zones: " + ", ".join([f"{k}:{_zone_name(k)}" for k in sorted(ZONE_NAMES)]))
lines.append("Slots: " + ", ".join([f"{k}:{_slot_name(k)}" for k in sorted(SLOT_NAMES)]))
lines.append("Comparisons: " + ", ".join([f"{k}:{_comparison_name(k)}" for k in sorted(COMPARISON_NAMES)]))
return "\n".join(lines)
def _decode_look_and_choose(value: int, attr: int, slot: int, opcode: int) -> str:
look_v = unpack_v_look_choose(value)
chars = []
if look_v["char_id_1"]:
chars.append(CHARACTER_NAMES.get(look_v["char_id_1"], str(look_v["char_id_1"])))
if look_v["char_id_2"]:
chars.append(CHARACTER_NAMES.get(look_v["char_id_2"], str(look_v["char_id_2"])))
if look_v["char_id_3"]:
chars.append(CHARACTER_NAMES.get(look_v["char_id_3"], str(look_v["char_id_3"])))
filter_str = decode_filter(attr)
filter_part = f"filter=[{filter_str}] " if attr else ""
return (
f"look={look_v['count']}, "
f"{filter_part}"
f"chars=[{'/'.join(chars)}], reveal={bool(look_v['reveal'])}, discard_remainder={bool(look_v['dest_discard'])}, "
f"slot=[{decode_standard_slot(slot, opcode)}]"
)
def _decode_meta_rule(value: int, attr: int, slot: int, opcode: int) -> str:
meta_name = META_RULE_NAMES.get(attr, f"META_{attr}")
parts = [f"type={meta_name}"]
if meta_name in {"HEART_RULE", "ALL_BLADE_AS_ANY_HEART"}:
source_name = {0: "none", 1: "all_blade", 2: "blade"}.get(value, str(value))
parts.append(f"source={source_name}")
elif value:
parts.append(f"value={value}")
if slot:
parts.append(f"slot=[{decode_standard_slot(slot, opcode)}]")
return ", ".join(parts)
def _decode_set_heart_cost(value: int, attr: int, slot: int, opcode: int) -> str:
hearts = unpack_v_heart_counts(value)
requirements = unpack_a_heart_cost(attr)
count_parts = [f"{color}={amount}" for color, amount in hearts.items() if amount]
req_parts = []
for idx in range(1, 9):
req = requirements[f"req_{idx}"]
if req:
req_parts.append(HEART_COLOR_NAMES.get(req, str(req)))
if requirements.get("unit_enabled"):
req_parts.append(f"unit={UNIT_NAMES.get(requirements.get('unit_id'), requirements.get('unit_id'))}")
parts = []
if count_parts:
parts.append("counts=[" + ", ".join(count_parts) + "]")
if req_parts:
parts.append("requirements=[" + ", ".join(req_parts) + "]")
if slot:
parts.append(f"slot=[{decode_standard_slot(slot, opcode)}]")
return ", ".join(parts) if parts else f"value={value}, attr={attr}, slot={slot}"
def _decode_heart_effect(count: int, attr: int, slot: int, opcode: int, label: str = "count") -> str:
parts = [f"{label}={count}"]
if 0 <= attr <= max(HEART_COLOR_NAMES):
parts.append(f"heart_type={HEART_COLOR_NAMES.get(attr, attr)}")
elif attr:
parts.append(f"filter=[{decode_filter(attr)}]")
if slot:
parts.append(f"slot=[{decode_standard_slot(slot, opcode)}]")
return ", ".join(parts)
def _decode_move_to_discard(value: int, attr: int, slot: int, opcode: int) -> str:
unsigned_value = value & 0xFFFFFFFF
parts = []
if unsigned_value & 0x80000000:
parts.append(f"until_size={unsigned_value & 0x7FFFFFFF}")
parts.append("mode=until_size")
else:
parts.append(f"count={value}")
parts.append(f"filter=[{decode_filter(attr)}]")
if slot:
parts.append(f"slot=[{decode_standard_slot(slot, opcode)}]")
return ", ".join(parts)
def decode_chunk(chunk: List[int]) -> str:
op, v, a_low, a_high, s = chunk
a = ((a_high & 0xFFFFFFFF) << 32) | (a_low & 0xFFFFFFFF)
is_negated = False
base_op = op
if 1000 <= op < 2000:
is_negated = True
base_op = op - 1000
op_name = opcode_name(base_op, v, a, s)
if is_negated:
op_name = f"NOT {op_name}"
# Determine descriptive parameter labels based on opcode
v_label = "value"
a_label = "filter"
s_label = "slot"
if base_op in (OP.get("DRAW"), OP.get("ADD_BLADES"), OP.get("ADD_HEARTS"),
OP.get("RECOVER_MEMBER"), OP.get("RECOVER_LIVE"),
OP.get("ACTIVATE_MEMBER"), OP.get("SET_TAPPED"),
OP.get("ACTIVATE_ENERGY"), OP.get("ENERGY_CHARGE"),
OP.get("REVEAL_UNTIL"), OP.get("SELECT_MEMBER"),
OP.get("SELECT_CARDS"), OP.get("SELECT_LIVE"),
OP.get("LOOK_DECK"), OP.get("ORDER_DECK")):
v_label = "count"
elif base_op in (OP.get("JUMP"), OP.get("JUMP_IF_FALSE")):
v_label = "offset"
elif base_op == OP.get("BUFF_POWER"):
v_label = "amount"
elif base_op == OP.get("PAY_ENERGY"):
v_label = "cost"
elif base_op == OP.get("GRANT_ABILITY"):
v_label = "granted_index"
elif base_op == OP.get("SELECT_MODE"):
v_label = "options"
elif base_op == OP.get("SET_HEARTS"):
v_label = "value"
# Default params string
params = f"{v_label}={v}, {a_label}=[{decode_filter(a)}], {s_label}=[{decode_standard_slot(s, base_op)}]"
# Specialized decoding logic for complex opcodes
if base_op == OP.get("LOOK_AND_CHOOSE"):
params = _decode_look_and_choose(v, a, s, base_op)
elif base_op == OP.get("META_RULE"):
params = _decode_meta_rule(v, a, s, base_op)
elif base_op == OP.get("SET_HEART_COST"):
params = _decode_set_heart_cost(v, a, s, base_op)
elif base_op == OP.get("MOVE_TO_DISCARD"):
params = _decode_move_to_discard(v, a, s, base_op)
elif base_op == OP.get("ADD_HEARTS") or base_op == OP.get("SET_HEARTS"):
params = _decode_heart_effect(v, a, s, base_op, label=v_label)
elif base_op == OP.get("MOVE_MEMBER"):
params = f"from={_slot_name(v)}, to={_slot_name(a)}, slot=[{decode_standard_slot(s, base_op)}]"
elif base_op == OP.get("SET_TARGET_SELF"):
params = "target_context=self"
elif base_op == OP.get("SET_TARGET_OPPONENT"):
params = "target_context=opponent"
elif base_op == OP.get("TRANSFORM_COLOR") or base_op == OP.get("TRANSFORM_HEART"):
params = f"source={HEART_COLOR_NAMES.get(v, v)}, target={HEART_COLOR_NAMES.get(a, a)}, slot=[{decode_standard_slot(s, base_op)}]"
elif base_op in (OP.get("JUMP"), OP.get("JUMP_IF_FALSE")):
params = f"offset={v}"
elif base_op == OP.get("RETURN"):
params = "done"
elif base_op == 0 and (v != 0 or a != 0 or s != 0):
# Fallback for unmapped condition parameters (like UNIQUE_NAMES_COUNT)
params = f"value={v}, filter=[{decode_filter(a)}], slot=[{decode_condition_slot(s)}]"
elif 100 <= base_op < 200:
# Action opcodes
params = f"value={v}, filter=[{decode_filter(a)}], slot=[{decode_standard_slot(s, base_op)}]"
elif 200 <= base_op < 400:
# Condition opcodes
params = f"value={v}, filter=[{decode_filter(a)}], slot=[{decode_condition_slot(s)}]"
return f"{op_name:<25} | {params}"
def decode_bytecode(bytecode: List[int]) -> str:
if not bytecode:
return "None"
chunks = [bytecode[i : i + 5] for i in range(0, len(bytecode), 5)]
lines = []
for i, chunk in enumerate(chunks):
if len(chunk) < 5:
chunk = chunk + [0] * (5 - len(chunk))
lines.append(f" {i * 5:02d}: {decode_chunk(chunk)}")
lines.append(get_legend_str())
return "\n".join(lines)
# ===== Version Gate Support =====
# Support for versioned bytecode decoding. Currently only v1 is implemented;
# v2 support is reserved for future extension.
def decode_bytecode_with_version(bytecode: List[int], layout_version: int = 1) -> str:
"""
Decode bytecode with explicit version specification.
Allows decoding different bytecode layout versions. Currently v1 and v2
are defined, but only v1 is active. This function enables gradual migration
when v2 layout is introduced.
Args:
bytecode: List of 32-bit integers comprising bytecode
layout_version: Bytecode layout version (default: 1)
Returns:
Formatted, legible bytecode trace with zone/slot legends
Raises:
ValueError: If layout_version is not supported
"""
if layout_version == 1:
return decode_bytecode(bytecode)
elif layout_version == 2:
# Future: Decode v2 layout (currently same as v1, placeholder for expansion)
return decode_bytecode_v2(bytecode)
else:
raise ValueError(f"Unsupported bytecode layout version: {layout_version}")
def decode_bytecode_v2(bytecode: List[int]) -> str:
"""
Decode bytecode using v2 layout (future extension).
Currently a placeholder that uses v1 logic. When v2 layout is finalized,
this function will be updated to handle the new format.
The v2 layout may:
- Expand certain fields for better expressiveness
- Add inline metadata markers
- Support wider immediate values
- Maintain backward compatibility with v1 decoders for common opcodes
For now, v2 is defined but inactive. Switch to v2 compilation via VersionGate.
"""
# Placeholder: Currently uses v1 decoding
# When v2 layout is implemented, replace this with v2-specific logic
if not bytecode:
return "None"
lines = [
f" [v2 layout - experimental, currently using v1 decoder]",
f" Bytecode length: {len(bytecode)} words",
]
lines.append("")
# For now, delegate to v1 decoder
v1_output = decode_bytecode(bytecode)
lines.append(v1_output)
return "\n".join(lines)