diff --git a/bemani/format/afp/decompile.py b/bemani/format/afp/decompile.py index fe70061..02ac3cb 100644 --- a/bemani/format/afp/decompile.py +++ b/bemani/format/afp/decompile.py @@ -13,6 +13,9 @@ from .types import ( StringConstant, StoreRegisterAction, DefineFunction2Action, + GotoFrame2Action, + UNDEFINED, + GLOBAL, ) from .util import VerboseOutput @@ -107,7 +110,7 @@ class Statement(ConvertedAction): def object_ref(obj: Any) -> str: - if isinstance(obj, (GenericObject, Variable, Member)): + if isinstance(obj, (GenericObject, Variable, Member, MethodCall, FunctionCall)): return obj.render(nested=True) else: raise Exception(f"Unsupported objectref {obj} ({type(obj)})") @@ -222,6 +225,54 @@ class PlayMovieStatement(Statement): return [f"{prefix}builtin_StartPlaying();"] +class DebugTraceStatement(Statement): + # Print a debug trace if supported. + def __init__(self, trace: Any) -> None: + self.trace = trace + + def __repr__(self) -> str: + trace = value_ref(self.trace) + return f"builtin_DebugTrace({trace})" + + def render(self, prefix: str) -> List[str]: + trace = value_ref(self.trace) + return [f"{prefix}builtin_DebugTrace({trace});"] + + +class GotoFrameStatement(Statement): + # Go to a specified frame in the animation. + def __init__(self, frame: Any) -> None: + self.frame = frame + + def __repr__(self) -> str: + frame = value_ref(self.frame) + return f"builtin_GotoFrame({frame})" + + def render(self, prefix: str) -> List[str]: + frame = value_ref(self.frame) + return [f"{prefix}builtin_GotoFrame({frame});"] + + +class CloneSpriteStatement(Statement): + # Clone a sprite. + def __init__(self, obj_to_clone: Any, name: Union[str, Expression], depth: Union[int, Expression]) -> None: + self.obj_to_clone = obj_to_clone + self.name = name + self.depth = depth + + def __repr__(self) -> str: + obj = object_ref(self.obj_to_clone) + name = value_ref(self.name) + depth = value_ref(self.depth) + return f"builtin_CloneSprite({obj}, {name}, {depth})" + + def render(self, prefix: str) -> List[str]: + obj = object_ref(self.obj_to_clone) + name = value_ref(self.name) + depth = value_ref(self.depth) + return [f"{prefix}builtin_CloneSprite({obj}, {name}, {depth});"] + + class ArithmeticExpression(Expression): def __init__(self, left: Any, op: str, right: Any) -> None: self.left = left @@ -236,7 +287,24 @@ class ArithmeticExpression(Expression): def render(self, nested: bool = False) -> str: left = value_ref(self.left, parens=True) right = value_ref(self.right, parens=True) - return f"{left} {self.op} {right}" + + if nested and self.op == '-': + return f"({left} {self.op} {right})" + else: + return f"{left} {self.op} {right}" + + +class Array(Expression): + # Call a method on an object. + def __init__(self, params: List[Any]) -> None: + self.params = params + + def __repr__(self) -> str: + return self.render() + + def render(self, nested: bool = False) -> str: + params = [value_ref(param) for param in self.params] + return f"[{', '.join(params)}]" class FunctionCall(Expression): @@ -310,7 +378,7 @@ class NewObject(Expression): class SetMemberStatement(Statement): # Call a method on an object. - def __init__(self, objectref: Any, name: Union[str, Expression], valueref: Any) -> None: + def __init__(self, objectref: Any, name: Union[str, int, Expression], valueref: Any) -> None: self.objectref = objectref self.name = name self.valueref = valueref @@ -353,7 +421,7 @@ class DeleteVariableStatement(Statement): def render(self, prefix: str) -> List[str]: name = name_ref(self.name) - return [f"{prefix}delete {name};"] + return [f"{prefix}del {name};"] class StoreRegisterStatement(Statement): @@ -555,7 +623,8 @@ class DoWhileStatement(Statement): return os.linesep.join([ "do {", os.linesep.join(entries), - "} while(True);" + "}", + "while(True);" ]) def render(self, prefix: str) -> List[str]: @@ -567,7 +636,9 @@ class DoWhileStatement(Statement): f"{prefix}do", f"{prefix}{{", *entries, - f"{prefix}}} while(True);", + f"{prefix}}}", + f"{prefix}while(True);", + "", ] @@ -716,7 +787,9 @@ class Variable(Expression): class Member(Expression): - def __init__(self, objectref: Any, member: Union[str, Expression]) -> None: + # A member can be an array entry in an array, or an object member as accessed + # in array lookup syntax or dot notation. + def __init__(self, objectref: Any, member: Union[str, int, Expression]) -> None: self.objectref = objectref self.member = member @@ -1562,9 +1635,9 @@ class ByteCodeDecompiler(VerboseOutput): stack: List[Any] = [] def make_if_expr(action: IfAction, negate: bool) -> IfExpr: - if action.comparison in ["IS DEFINED", "IS NOT UNDEFINED"]: + if action.comparison in ["IS UNDEFINED", "IS NOT UNDEFINED"]: conditional = stack.pop() - return IsUndefinedIf(conditional, negate=negate != (action.comparison == "IS DEFINED")) + return IsUndefinedIf(conditional, negate=negate != (action.comparison == "IS UNDEFINED")) if action.comparison in ["IS TRUE", "IS FALSE"]: conditional = stack.pop() return IsBooleanIf(conditional, negate=negate != (action.comparison == "IS FALSE")) @@ -1585,7 +1658,7 @@ class ByteCodeDecompiler(VerboseOutput): conditional1 = stack.pop() return MagnitudeEqualIf(conditional1, conditional2, negate=negate != (action.comparison == "<=")) - raise Exception("TODO: {action}") + raise Exception(f"TODO: {action}") for i in range(len(chunk.actions)): action = chunk.actions[i] @@ -1604,6 +1677,24 @@ class ByteCodeDecompiler(VerboseOutput): chunk.actions[i] = NopStatement() continue + if isinstance(action, GotoFrame2Action): + after: Statement + + if action.stop: + after = StopMovieStatement() + else: + after = PlayMovieStatement() + + frame = stack.pop() + if action.additional_frames: + frame = ArithmeticExpression(frame, '+', action.additional_frames) + + chunk.actions[i] = MultiAction([ + GotoFrameStatement(frame), + after, + ]) + continue + if isinstance(action, StoreRegisterAction): # This one's fun, because a store register can generate zero or more statements. # So we need to expand the stack. But we can't mid-iteration without a lot of @@ -1659,11 +1750,25 @@ class ByteCodeDecompiler(VerboseOutput): chunk.actions[i] = NullReturnStatement() continue + if action.opcode == AP2Action.CLONE_SPRITE: + depth = stack.pop() + if not isinstance(depth, (int, Expression)): + raise Exception("Logic error!") + name = stack.pop() + if not isinstance(name, (str, Expression)): + raise Exception("Logic error!") + obj = stack.pop() + chunk.actions[i] = CloneSpriteStatement(obj, name, depth) + continue + if action.opcode == AP2Action.GET_VARIABLE: variable_name = stack.pop() - if not isinstance(variable_name, (str, StringConstant)): - raise Exception("Logic error!") - stack.append(Variable(variable_name)) + if isinstance(variable_name, (str, StringConstant)): + stack.append(Variable(variable_name)) + else: + # This is probably a reference to a variable by + # string concatenation. + stack.append(Member(GLOBAL, variable_name)) chunk.actions[i] = NopStatement() continue @@ -1676,6 +1781,34 @@ class ByteCodeDecompiler(VerboseOutput): chunk.actions[i] = DeleteVariableStatement(variable_name) continue + if action.opcode == AP2Action.TO_NUMBER: + obj_ref = stack.pop() + stack.append(FunctionCall('int', [obj_ref])) + + chunk.actions[i] = NopStatement() + continue + + if action.opcode == AP2Action.TO_STRING: + obj_ref = stack.pop() + stack.append(FunctionCall('str', [obj_ref])) + + chunk.actions[i] = NopStatement() + continue + + if action.opcode == AP2Action.INCREMENT: + obj_ref = stack.pop() + stack.append(ArithmeticExpression(obj_ref, '+', 1)) + + chunk.actions[i] = NopStatement() + continue + + if action.opcode == AP2Action.DECREMENT: + obj_ref = stack.pop() + stack.append(ArithmeticExpression(obj_ref, '-', 1)) + + chunk.actions[i] = NopStatement() + continue + if action.opcode == AP2Action.CALL_METHOD: method_name = stack.pop() if not isinstance(method_name, (str, StringConstant)): @@ -1721,16 +1854,19 @@ class ByteCodeDecompiler(VerboseOutput): if action.opcode == AP2Action.SET_VARIABLE: set_value = stack.pop() local_name = stack.pop() - if not isinstance(local_name, (str, StringConstant)): - raise Exception("Logic error!") + if isinstance(local_name, (str, StringConstant)): + chunk.actions[i] = SetVariableStatement(local_name, set_value) + else: + # This is probably a reference to a variable by + # string concatenation. + chunk.actions[i] = SetMemberStatement(GLOBAL, local_name, set_value) - chunk.actions[i] = SetVariableStatement(local_name, set_value) continue if action.opcode == AP2Action.SET_MEMBER: set_value = stack.pop() member_name = stack.pop() - if not isinstance(member_name, (str, Expression)): + if not isinstance(member_name, (str, int, Expression)): raise Exception("Logic error!") object_reference = stack.pop() @@ -1746,9 +1882,18 @@ class ByteCodeDecompiler(VerboseOutput): chunk.actions[i] = SetLocalStatement(local_name, set_value) continue + if action.opcode == AP2Action.DEFINE_LOCAL2: + local_name = stack.pop() + if not isinstance(local_name, (str, StringConstant)): + raise Exception("Logic error!") + + # TODO: Should this be NULL? + chunk.actions[i] = SetLocalStatement(local_name, UNDEFINED) + continue + if action.opcode == AP2Action.GET_MEMBER: member_name = stack.pop() - if not isinstance(member_name, (str, Expression)): + if not isinstance(member_name, (str, int, Expression)): raise Exception("Logic error!") object_reference = stack.pop() stack.append(Member(object_reference, member_name)) @@ -1771,6 +1916,23 @@ class ByteCodeDecompiler(VerboseOutput): chunk.actions[i] = NopStatement() continue + if action.opcode == AP2Action.INIT_ARRAY: + num_entries = stack.pop() + if not isinstance(num_entries, int): + raise Exception("Logic error!") + params = [] + for _ in range(num_entries): + params.append(stack.pop()) + stack.append(Array(params)) + + chunk.actions[i] = NopStatement() + continue + + if action.opcode == AP2Action.TRACE: + trace_obj = stack.pop() + chunk.actions[i] = DebugTraceStatement(trace_obj) + continue + if action.opcode == AP2Action.ADD2: expr1 = stack.pop() expr2 = stack.pop() @@ -1779,6 +1941,54 @@ class ByteCodeDecompiler(VerboseOutput): chunk.actions[i] = NopStatement() continue + if action.opcode == AP2Action.SUBTRACT: + expr2 = stack.pop() + expr1 = stack.pop() + stack.append(ArithmeticExpression(expr1, "-", expr2)) + + chunk.actions[i] = NopStatement() + continue + + if action.opcode == AP2Action.MULTIPLY: + expr2 = stack.pop() + expr1 = stack.pop() + stack.append(ArithmeticExpression(expr1, "*", expr2)) + + chunk.actions[i] = NopStatement() + continue + + if action.opcode == AP2Action.MODULO: + expr2 = stack.pop() + expr1 = stack.pop() + stack.append(ArithmeticExpression(expr1, "%", expr2)) + + chunk.actions[i] = NopStatement() + continue + + if action.opcode == AP2Action.BIT_OR: + expr2 = stack.pop() + expr1 = stack.pop() + stack.append(ArithmeticExpression(expr1, "|", expr2)) + + chunk.actions[i] = NopStatement() + continue + + if action.opcode == AP2Action.BIT_AND: + expr2 = stack.pop() + expr1 = stack.pop() + stack.append(ArithmeticExpression(expr1, "&", expr2)) + + chunk.actions[i] = NopStatement() + continue + + if action.opcode == AP2Action.BIT_XOR: + expr2 = stack.pop() + expr1 = stack.pop() + stack.append(ArithmeticExpression(expr1, "^", expr2)) + + chunk.actions[i] = NopStatement() + continue + if isinstance(action, NullReturnStatement): # We already handled this continue diff --git a/bemani/format/afp/types/ap2.py b/bemani/format/afp/types/ap2.py index e69e76d..888b491 100644 --- a/bemani/format/afp/types/ap2.py +++ b/bemani/format/afp/types/ap2.py @@ -274,7 +274,8 @@ class AP2Action: # Define a variable in the local movieclip or function, without a value. DEFINE_LOCAL2 = 34 - # Init an array from the stack. I haven't figured out what it needs to push and pop. + # Init an array from the stack. Pops the array's number of items, and then an item each + # to add to the array. Then it adds the array to the stack. INIT_ARRAY = 35 # Init an object from the stack.