diff --git a/bemani/format/afp/__init__.py b/bemani/format/afp/__init__.py index db7e2fa..9135f95 100644 --- a/bemani/format/afp/__init__.py +++ b/bemani/format/afp/__init__.py @@ -2,7 +2,7 @@ from .geo import Shape, DrawParams from .swf import SWF, NamedTagReference from .container import TXP2File, PMAN, Texture, TextureRegion, Unknown1, Unknown2 from .render import AFPRenderer -from .types import Matrix, Color, Point, Rectangle, AP2Tag, AP2Action, AP2Object, AP2Pointer, AP2Property +from .types import Matrix, Color, Point, Rectangle, AP2Tag, AP2Action, AP2Object, AP2Pointer __all__ = [ @@ -25,5 +25,4 @@ __all__ = [ 'AP2Action', 'AP2Object', 'AP2Pointer', - 'AP2Property', ] diff --git a/bemani/format/afp/decompile.py b/bemani/format/afp/decompile.py index 7a49441..eba3a7e 100644 --- a/bemani/format/afp/decompile.py +++ b/bemani/format/afp/decompile.py @@ -1,7 +1,7 @@ import os from typing import Any, Dict, List, Sequence, Tuple, Set, Union, Optional, cast -from .types import AP2Action, JumpAction, IfAction, PushAction, GenericObject, DefineFunction2Action +from .types import AP2Action, JumpAction, IfAction, PushAction, Expression, Register, GenericObject, StringConstant, StoreRegisterAction, DefineFunction2Action from .util import VerboseOutput @@ -74,39 +74,66 @@ class ControlFlow: class ConvertedAction: # An action that has been analyzed and converted to an intermediate representation. + semi = False + + +class MultiStatementAction(ConvertedAction): + # An action that allows us to expand the number of lines we have to work with, for + # opcodes that perform more than one statement's worth of actions. + def __init__(self, actions: Sequence[ConvertedAction]) -> None: + self.actions = actions + + def __repr__(self) -> str: + # We should never emit one of these in printing. + return f"MultiStatementAction({self.actions})" + + +class Statement(ConvertedAction): + # This is just a type class for finished statements. semi = True - def _object_ref(self, obj: Any) -> str: - if isinstance(obj, (GenericObject, Variable, Member)): - return repr(obj) - else: - raise Exception(f"Unsupported objectref {obj}") - def _value_ref(self, param: Any) -> str: - if isinstance(param, (GenericObject, Variable, Member, CallFunctionStatement, CallMethodStatement)): - return repr(param) - elif isinstance(param, (str, int, float)): - return repr(param) - else: - raise Exception(f"Unsupported valueref {param} ({type(param)})") +def object_ref(obj: Any) -> str: + if isinstance(obj, (GenericObject, Variable, Member)): + return repr(obj) + else: + raise Exception(f"Unsupported objectref {obj} ({type(obj)})") + + +def value_ref(param: Any) -> str: + if isinstance(param, (Expression)): + return repr(param) + elif isinstance(param, (str, int, float)): + return repr(param) + else: + raise Exception(f"Unsupported valueref {param} ({type(param)})") + + +def name_ref(param: Any) -> str: + if isinstance(param, str): + return param + elif isinstance(param, StringConstant): + return repr(param) + else: + raise Exception(f"Unsupported nameref {param} ({type(param)})") ArbitraryOpcode = Union[AP2Action, ConvertedAction] -class BreakStatement(ConvertedAction): +class BreakStatement(Statement): # A break from a loop (forces execution to the next line after the loop). def __repr__(self) -> str: return "break" -class ContinueStatement(ConvertedAction): +class ContinueStatement(Statement): # A continue in a loop (forces execution to the top of the loop). def __repr__(self) -> str: return "continue" -class GotoStatement(ConvertedAction): +class GotoStatement(Statement): # A goto, including the ID of the chunk we want to jump to. def __init__(self, location: int) -> None: self.location = location @@ -115,63 +142,154 @@ class GotoStatement(ConvertedAction): return f"goto label_{self.location}" -class NullReturnStatement(ConvertedAction): +class NullReturnStatement(Statement): # A statement which directs the control flow to the end of the code, but # does not pop the stack to return def __repr__(self) -> str: return "return" -class NopStatement(ConvertedAction): +class NopStatement(Statement): # A literal no-op. We will get rid of these in an optimizing pass. def __repr__(self) -> str: return "nop" -class StopMovieStatement(ConvertedAction): +class ExpressionStatement(Statement): + # A statement which is an expression that discards its return. + def __init__(self, expr: Expression) -> None: + self.expr = expr + + def __repr__(self) -> str: + return f"{self.expr}" + + +class StopMovieStatement(Statement): # Stop the movie, this is an actionscript-specific opcode. def __repr__(self) -> str: return "builtin_StopPlaying()" -class CallFunctionStatement(ConvertedAction): +class PlayMovieStatement(Statement): + # Play the movie, this is an actionscript-specific opcode. + def __repr__(self) -> str: + return "builtin_StartPlaying()" + + +class FunctionCall(Expression): # Call a method on an object. - def __init__(self, name: str, params: List[Any]) -> None: + def __init__(self, name: Union[str, StringConstant], params: List[Any]) -> None: self.name = name self.params = params def __repr__(self) -> str: - params = [self._value_ref(param) for param in self.params] - return f"{self.name}({', '.join(params)})" + name = name_ref(self.name) + params = [value_ref(param) for param in self.params] + return f"{name}({', '.join(params)})" -class CallMethodStatement(ConvertedAction): +class MethodCall(Expression): # Call a method on an object. - def __init__(self, objectref: Any, name: str, params: List[Any]) -> None: + def __init__(self, objectref: Any, name: Union[str, StringConstant], params: List[Any]) -> None: self.objectref = objectref self.name = name self.params = params def __repr__(self) -> str: - obj = self._object_ref(self.objectref) - params = [self._value_ref(param) for param in self.params] - return f"{obj}.{self.name}({', '.join(params)})" + obj = object_ref(self.objectref) + name = name_ref(self.name) + params = [value_ref(param) for param in self.params] + return f"{obj}.{name}({', '.join(params)})" -class SetMemberStatement(ConvertedAction): +class NewFunction(Expression): + # Create a new function. + def __init__(self, funcname: Optional[str], flags: int, body: ByteCode) -> None: + self.funcname = funcname + self.flags = flags + self.body = body + + def __repr__(self) -> str: + return f"new function({repr(self.funcname) or ''}, {hex(self.flags)}, 'TODO: ByteCode')" + + +class NewObject(Expression): + # Create a new object of type. + def __init__(self, objname: Union[str, StringConstant], params: List[Any]) -> None: + self.objname = objname + self.params = params + + def __repr__(self) -> str: + objname = name_ref(self.objname) + params = [value_ref(param) for param in self.params] + return f"new {objname}({', '.join(params)})" + + +class SetMemberStatement(Statement): # Call a method on an object. - def __init__(self, objectref: Any, name: str, valueref: Any) -> None: + def __init__(self, objectref: Any, name: Union[str, StringConstant], valueref: Any) -> None: self.objectref = objectref self.name = name self.valueref = valueref def __repr__(self) -> str: - ref = self._object_ref(self.objectref) - val = self._value_ref(self.valueref) - return f"{ref}.{self.name} = {val}" + ref = object_ref(self.objectref) + name = name_ref(self.name) + val = value_ref(self.valueref) + return f"{ref}.{name} = {val}" -class IsUndefinedIfStatement(ConvertedAction): +class DeleteVariableStatement(Statement): + # Call a method on an object. + def __init__(self, name: Union[str, StringConstant]) -> None: + self.name = name + + def __repr__(self) -> str: + name = name_ref(self.name) + return f"del {name}" + + +class StoreRegisterStatement(Statement): + # Set a variable to a value. + def __init__(self, register: Register, valueref: Any) -> None: + self.register = register + self.valueref = valueref + + def __repr__(self) -> str: + val = value_ref(self.valueref) + return f"{self.register} = {val}" + + +class SetVariableStatement(Statement): + # Set a variable to a value. + def __init__(self, name: Union[str, StringConstant], valueref: Any) -> None: + self.name = name + self.valueref = valueref + + def __repr__(self) -> str: + name = name_ref(self.name) + val = value_ref(self.valueref) + return f"{name} = {val}" + + +class SetLocalStatement(Statement): + # Define a local variable with a value. + def __init__(self, name: Union[str, StringConstant], valueref: Any) -> None: + self.name = name + self.valueref = valueref + + def __repr__(self) -> str: + name = name_ref(self.name) + val = value_ref(self.valueref) + return f"local {name} = {val}" + + +class IfExpr(ConvertedAction): + # This is just for typing. + pass + + +class IsUndefinedIf(IfExpr): # No semicolon on this. semi = False @@ -180,30 +298,140 @@ class IsUndefinedIfStatement(ConvertedAction): self.negate = negate def __repr__(self) -> str: - val = self._value_ref(self.conditional) + val = value_ref(self.conditional) if self.negate: - return f"if ({val} !== UNDEFINED)" + return f"if ({val} is not UNDEFINED)" else: - return f"if ({val} === UNDEFINED)" + return f"if ({val} is UNDEFINED)" -class IntermediateIfStatement(ConvertedAction): - def __init__(self, parent_action: IfAction, true_actions: Sequence[ArbitraryOpcode], false_actions: Sequence[ArbitraryOpcode], negate: bool) -> None: +class IsBooleanIf(IfExpr): + # No semicolon on this. + semi = False + + def __init__(self, conditional: Any, negate: bool) -> None: + self.conditional = conditional + self.negate = negate + + def __repr__(self) -> str: + val = value_ref(self.conditional) + if self.negate: + return f"if ({val} is False)" + else: + return f"if ({val} is True)" + + +class IsEqualIf(IfExpr): + # No semicolon on this. + semi = False + + def __init__(self, conditional1: Any, conditional2: Any, negate: bool) -> None: + self.conditional1 = conditional1 + self.conditional2 = conditional2 + self.negate = negate + + def __repr__(self) -> str: + val1 = value_ref(self.conditional1) + val2 = value_ref(self.conditional2) + return f"if ({val1} {'!=' if self.negate else '=='} {val2})" + + +class IsStrictEqualIf(IfExpr): + # No semicolon on this. + semi = False + + def __init__(self, conditional1: Any, conditional2: Any, negate: bool) -> None: + self.conditional1 = conditional1 + self.conditional2 = conditional2 + self.negate = negate + + def __repr__(self) -> str: + val1 = value_ref(self.conditional1) + val2 = value_ref(self.conditional2) + return f"if ({val1} {'!==' if self.negate else '==='} {val2})" + + +class MagnitudeIf(IfExpr): + # No semicolon on this. + semi = False + + def __init__(self, conditional1: Any, conditional2: Any, negate: bool) -> None: + self.conditional1 = conditional1 + self.conditional2 = conditional2 + self.negate = negate + + def __repr__(self) -> str: + val1 = value_ref(self.conditional1) + val2 = value_ref(self.conditional2) + return f"if ({val1} {'<' if self.negate else '>'} {val2})" + + +class MagnitudeEqualIf(IfExpr): + # No semicolon on this. + semi = False + + def __init__(self, conditional1: Any, conditional2: Any, negate: bool) -> None: + self.conditional1 = conditional1 + self.conditional2 = conditional2 + self.negate = negate + + def __repr__(self) -> str: + val1 = value_ref(self.conditional1) + val2 = value_ref(self.conditional2) + return f"if ({val1} {'<=' if self.negate else '>='} {val2})" + + +class IfStatement(Statement): + # If statements aren't semicoloned. + semi = False + + def __init__(self, cond: IfExpr, true_statements: Sequence[Statement], false_statements: Sequence[Statement]) -> None: + self.cond = cond + self.true_statements = true_statements + self.false_statements = false_statements + + def __repr__(self) -> str: + true_entries: List[str] = [] + for statement in self.true_statements: + true_entries.extend([f" {s}" for s in (str(statement) + (';' if statement.semi else '')).split(os.linesep)]) + + false_entries: List[str] = [] + for statement in self.false_statements: + false_entries.extend([f" {s}" for s in (str(statement) + (';' if statement.semi else '')).split(os.linesep)]) + + if false_entries: + return os.linesep.join([ + f"{self.cond} {{", + os.linesep.join(true_entries), + "} else {", + os.linesep.join(false_entries), + "}" + ]) + else: + return os.linesep.join([ + f"{self.cond} {{", + os.linesep.join(true_entries), + "}" + ]) + + +class IntermediateIf(ConvertedAction): + def __init__(self, parent_action: IfAction, true_statements: Sequence[Statement], false_statements: Sequence[Statement], negate: bool) -> None: self.parent_action = parent_action - self.true_actions = list(true_actions) - self.false_actions = list(false_actions) + self.true_statements = list(true_statements) + self.false_statements = list(false_statements) self.negate = negate def __repr__(self) -> str: true_entries: List[str] = [] - for action in self.true_actions: + for action in self.true_statements: true_entries.extend([f" {s}" for s in str(action).split(os.linesep)]) false_entries: List[str] = [] - for action in self.false_actions: + for action in self.false_statements: false_entries.extend([f" {s}" for s in str(action).split(os.linesep)]) - if self.false_actions: + if self.false_statements: return os.linesep.join([ f"if <{'!' if self.negate else ''}{self.parent_action}> {{", os.linesep.join(true_entries), @@ -320,22 +548,23 @@ class IfBody: ) -class Variable(ConvertedAction): - def __init__(self, name: str) -> None: +class Variable(Expression): + def __init__(self, name: Union[str, StringConstant]) -> None: self.name = name def __repr__(self) -> str: - return self.name + return name_ref(self.name) -class Member(ConvertedAction): - def __init__(self, objectref: Any, member: str) -> None: +class Member(Expression): + def __init__(self, objectref: Any, member: Union[str, StringConstant]) -> None: self.objectref = objectref self.member = member def __repr__(self) -> str: - ref = self._object_ref(self.objectref) - return f"{ref}.{self.member}" + member = name_ref(self.member) + ref = object_ref(self.objectref) + return f"{ref}.{member}" class BitVector: @@ -724,7 +953,7 @@ class ByteCodeDecompiler(VerboseOutput): false_jump_point = false_jump_points[0] # Calculate true and false jump points, see if they are break/continue/goto. - true_action: Optional[ConvertedAction] = None + true_action: Optional[Statement] = None if true_jump_point == break_point: self.vprint("Converting jump if true to loop break into break statement.") true_action = BreakStatement() @@ -742,7 +971,7 @@ class ByteCodeDecompiler(VerboseOutput): true_action = GotoStatement(true_jump_point) chunk.next_chunks = [n for n in chunk.next_chunks if n != true_jump_point] - false_action: Optional[ConvertedAction] = None + false_action: Optional[Statement] = None if false_jump_point == break_point: self.vprint("Converting jump if false to loop break into break statement.") false_action = BreakStatement() @@ -771,7 +1000,7 @@ class ByteCodeDecompiler(VerboseOutput): # This is an internal-only if statement, we don't care. continue - chunk.actions[-1] = IntermediateIfStatement( + chunk.actions[-1] = IntermediateIf( cast(IfAction, last_action), [true_action], [false_action] if false_action else [], @@ -1133,9 +1362,35 @@ class ByteCodeDecompiler(VerboseOutput): # Return the tree, stripped of all dead code (most likely just the return sentinel). return new_chunks - def __eval_stack(self, chunk: ByteCodeChunk) -> None: + def __eval_stack(self, chunk: ByteCodeChunk, offset_map: Dict[int, int]) -> None: stack: List[Any] = [] + def make_if_expr(action: IfAction, negate: bool) -> IfExpr: + if action.comparison in ["IS DEFINED", "IS NOT UNDEFINED"]: + conditional = stack.pop() + return IsUndefinedIf(conditional, negate=negate != (action.comparison == "IS DEFINED")) + if action.comparison in ["IS TRUE", "IS FALSE"]: + conditional = stack.pop() + return IsBooleanIf(conditional, negate=negate != (action.comparison == "IS FALSE")) + if action.comparison in ["==", "!="]: + conditional2 = stack.pop() + conditional1 = stack.pop() + return IsEqualIf(conditional1, conditional2, negate=negate != (action.comparison == "!=")) + if action.comparison in ["STRICT ==", "STRICT !="]: + conditional2 = stack.pop() + conditional1 = stack.pop() + return IsStrictEqualIf(conditional1, conditional2, negate=negate != (action.comparison == "STRICT !=")) + if action.comparison in ["<", ">"]: + conditional2 = stack.pop() + conditional1 = stack.pop() + return MagnitudeIf(conditional1, conditional2, negate=negate != (action.comparison == "<")) + if action.comparison in ["<=", ">="]: + conditional2 = stack.pop() + conditional1 = stack.pop() + return MagnitudeEqualIf(conditional1, conditional2, negate=negate != (action.comparison == "<=")) + + raise Exception("TODO: {action}") + for i in range(len(chunk.actions)): action = chunk.actions[i] @@ -1146,33 +1401,73 @@ class ByteCodeDecompiler(VerboseOutput): chunk.actions[i] = NopStatement() continue + if isinstance(action, DefineFunction2Action): + # TODO: We need to recursively decompile this function and add its contents here. + stack.append(NewFunction(action.name, action.flags, action.body)) + + chunk.actions[i] = NopStatement() + 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 + # shenanigans, so we instead invent a new type of ConvertedAction that can contain + # multiple statements. + set_value = stack.pop() + if action.preserve_stack: + stack.append(set_value) + + store_actions: List[StoreRegisterStatement] = [] + + for reg in action.registers: + store_actions.append(StoreRegisterStatement(reg, set_value)) + + chunk.actions[i] = MultiStatementAction(store_actions) + continue + + if isinstance(action, JumpAction): + # This could possibly be a jump to the very next line, but we will wait for the + # optimization pass to figure that out. + chunk.actions[i] = GotoStatement(offset_map[action.jump_offset]) + continue + if isinstance(action, IfAction): - if action.comparison in ["IS DEFINED", "IS NOT UNDEFINED"]: - conditional = stack.pop() - chunk.actions[i] = IsUndefinedIfStatement(conditional, negate=(action.comparison == "IS DEFINED")) - continue + chunk.actions[i] = make_if_expr(action, False) + continue if isinstance(action, AP2Action): if action.opcode == AP2Action.STOP: chunk.actions[i] = StopMovieStatement() continue + if action.opcode == AP2Action.PLAY: + chunk.actions[i] = PlayMovieStatement() + continue + if action.opcode == AP2Action.END: chunk.actions[i] = NullReturnStatement() continue if action.opcode == AP2Action.GET_VARIABLE: variable_name = stack.pop() - if not isinstance(variable_name, str): + if not isinstance(variable_name, (str, StringConstant)): raise Exception("Logic error!") stack.append(Variable(variable_name)) chunk.actions[i] = NopStatement() continue + if action.opcode == AP2Action.DELETE2: + variable_name = stack.pop() + if not isinstance(variable_name, (str, StringConstant)): + raise Exception("Logic error!") + + chunk.actions[i] = DeleteVariableStatement(variable_name) + continue + if action.opcode == AP2Action.CALL_METHOD: method_name = stack.pop() - if not isinstance(method_name, str): + if not isinstance(method_name, (str, StringConstant)): raise Exception("Logic error!") object_reference = stack.pop() num_params = stack.pop() @@ -1181,14 +1476,14 @@ class ByteCodeDecompiler(VerboseOutput): params = [] for _ in range(num_params): params.append(stack.pop()) - stack.append(CallMethodStatement(object_reference, method_name, params)) + stack.append(MethodCall(object_reference, method_name, params)) chunk.actions[i] = NopStatement() continue if action.opcode == AP2Action.CALL_FUNCTION: function_name = stack.pop() - if not isinstance(function_name, str): + if not isinstance(function_name, (str, StringConstant)): raise Exception("Logic error!") num_params = stack.pop() if not isinstance(num_params, int): @@ -1196,7 +1491,7 @@ class ByteCodeDecompiler(VerboseOutput): params = [] for _ in range(num_params): params.append(stack.pop()) - stack.append(CallFunctionStatement(function_name, params)) + stack.append(FunctionCall(function_name, params)) chunk.actions[i] = NopStatement() continue @@ -1205,26 +1500,46 @@ class ByteCodeDecompiler(VerboseOutput): # This is a discard. Let's see if its discarding a function or method # call. If so, that means the return doesn't matter. discard = stack.pop() - if isinstance(discard, CallMethodStatement): + if isinstance(discard, MethodCall): # It is! Let's act on the statement. - chunk.actions[i] = discard + chunk.actions[i] = ExpressionStatement(discard) else: chunk.actions[i] = NopStatement() continue + 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!") + + 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): + if not isinstance(member_name, (str, StringConstant)): + self.vprint(chunk.actions) + self.vprint(stack) raise Exception("Logic error!") object_reference = stack.pop() chunk.actions[i] = SetMemberStatement(object_reference, member_name, set_value) continue + if action.opcode == AP2Action.DEFINE_LOCAL: + set_value = stack.pop() + local_name = stack.pop() + if not isinstance(local_name, (str, StringConstant)): + raise Exception("Logic error!") + + chunk.actions[i] = SetLocalStatement(local_name, set_value) + continue + if action.opcode == AP2Action.GET_MEMBER: member_name = stack.pop() - if not isinstance(member_name, str): + if not isinstance(member_name, (str, StringConstant)): raise Exception("Logic error!") object_reference = stack.pop() stack.append(Member(object_reference, member_name)) @@ -1232,8 +1547,32 @@ class ByteCodeDecompiler(VerboseOutput): chunk.actions[i] = NopStatement() continue + if action.opcode == AP2Action.NEW_OBJECT: + object_name = stack.pop() + if not isinstance(object_name, (str, StringConstant)): + raise Exception("Logic error!") + num_params = stack.pop() + if not isinstance(num_params, int): + raise Exception("Logic error!") + params = [] + for _ in range(num_params): + params.append(stack.pop()) + stack.append(NewObject(object_name, params)) + + chunk.actions[i] = NopStatement() + continue + if isinstance(action, NullReturnStatement): - # We alreadyf handled this + # We already handled this + continue + + if isinstance(action, IntermediateIf): + # A partially-converted if from loop detection. Let's hoist it out properly. + chunk.actions[i] = IfStatement( + make_if_expr(action.parent_action, action.negate), + action.true_statements, + action.false_statements, + ) continue self.vprint(chunk.actions) @@ -1250,11 +1589,14 @@ class ByteCodeDecompiler(VerboseOutput): if new_actions and isinstance(new_actions[-1], NullReturnStatement): # Filter out redundant return statements. continue + if isinstance(action, MultiStatementAction): + for new_action in action.actions: + new_actions.append(new_action) new_actions.append(action) chunk.actions = new_actions - def __eval_chunks(self, start_id: int, chunks: Sequence[ArbitraryCodeChunk]) -> None: + def __eval_chunks(self, start_id: int, chunks: Sequence[ArbitraryCodeChunk], offset_map: Dict[int, int]) -> None: chunks_by_id: Dict[int, ArbitraryCodeChunk] = {chunk.id: chunk for chunk in chunks} while True: @@ -1264,19 +1606,19 @@ class ByteCodeDecompiler(VerboseOutput): if isinstance(chunk, Loop): # Evaluate the loop self.vprint(f"Evaluating graph in Loop {chunk.id}") - self.__eval_chunks(chunk.id, chunk.chunks) + self.__eval_chunks(chunk.id, chunk.chunks, offset_map) elif isinstance(chunk, IfBody): # Evaluate the if body if chunk.true_chunks: self.vprint(f"Evaluating graph of IfBody {chunk.id} true case") true_start = self.__get_entry_block(chunk.true_chunks) - self.__eval_chunks(true_start, chunk.true_chunks) + self.__eval_chunks(true_start, chunk.true_chunks, offset_map) if chunk.false_chunks: self.vprint(f"Evaluating graph of IfBody {chunk.id} false case") false_start = self.__get_entry_block(chunk.false_chunks) - self.__eval_chunks(false_start, chunk.false_chunks) + self.__eval_chunks(false_start, chunk.false_chunks, offset_map) else: - self.__eval_stack(chunk) + self.__eval_stack(chunk, offset_map) # Go to the next chunk if not chunk.next_chunks: @@ -1358,7 +1700,7 @@ class ByteCodeDecompiler(VerboseOutput): chunks_loops_and_ifs = self.__check_graph(start_id, chunks_loops_and_ifs) # Now, its safe to start actually evaluating the stack. - self.__eval_chunks(start_id, chunks_loops_and_ifs) + self.__eval_chunks(start_id, chunks_loops_and_ifs, offset_map) # Finally, let's print the code! if self.main: diff --git a/bemani/format/afp/swf.py b/bemani/format/afp/swf.py index b7ccceb..c77a204 100644 --- a/bemani/format/afp/swf.py +++ b/bemani/format/afp/swf.py @@ -8,7 +8,6 @@ from .types import Matrix, Color, Point, Rectangle from .types import ( AP2Action, AP2Tag, - AP2Property, DefineFunction2Action, InitRegisterAction, StoreRegisterAction, @@ -22,6 +21,7 @@ from .types import ( StartDragAction, GotoFrame2Action, Register, + StringConstant, NULL, UNDEFINED, THIS, @@ -531,107 +531,107 @@ class SWF(TrackedCoverage, VerboseOutput): elif obj_to_create == 0x10: # Property constant with no alias. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0x100 - objects.append(AP2Property.property_to_name(propertyval)) + objects.append(StringConstant(propertyval)) offset_ptr += 1 - self.vprint(f"{prefix} PROPERTY CONST NAME: {AP2Property.property_to_name(propertyval)}") + self.vprint(f"{prefix} PROPERTY CONST NAME: {StringConstant.property_to_name(propertyval)}") elif obj_to_create == 0x11: # Property constant referencing a string table entry. propertyval, reference = struct.unpack(">BB", datachunk[offset_ptr:(offset_ptr + 2)]) propertyval += 0x100 - objects.append(AP2Property.property_to_name(propertyval)) referenceval = self.__get_string(string_offsets[reference]) + objects.append(StringConstant(propertyval, referenceval)) offset_ptr += 2 - self.vprint(f"{prefix} PROPERTY CONST NAME: {AP2Property.property_to_name(propertyval)}, ALIAS: {referenceval}") + self.vprint(f"{prefix} PROPERTY CONST NAME: {StringConstant.property_to_name(propertyval)}, ALIAS: {referenceval}") elif obj_to_create == 0x12: # Same as above, but with allowance for a 16-bit constant offset. propertyval, reference = struct.unpack(">BH", datachunk[offset_ptr:(offset_ptr + 3)]) propertyval += 0x100 - objects.append(AP2Property.property_to_name(propertyval)) referenceval = self.__get_string(string_offsets[reference]) + objects.append(StringConstant(propertyval, referenceval)) offset_ptr += 3 - self.vprint(f"{prefix} PROPERTY CONST NAME: {AP2Property.property_to_name(propertyval)}, ALIAS: {referenceval}") + self.vprint(f"{prefix} PROPERTY CONST NAME: {StringConstant.property_to_name(propertyval)}, ALIAS: {referenceval}") elif obj_to_create == 0x13: # Class property name. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0x300 - objects.append(AP2Property.property_to_name(propertyval)) + objects.append(StringConstant(propertyval)) offset_ptr += 1 - self.vprint(f"{prefix} CLASS CONST NAME: {AP2Property.property_to_name(propertyval)}") + self.vprint(f"{prefix} CLASS CONST NAME: {StringConstant.property_to_name(propertyval)}") elif obj_to_create == 0x14: # Class property constant with alias. propertyval, reference = struct.unpack(">BB", datachunk[offset_ptr:(offset_ptr + 2)]) propertyval += 0x300 - objects.append(AP2Property.property_to_name(propertyval)) referenceval = self.__get_string(string_offsets[reference]) + objects.append(StringConstant(propertyval, referenceval)) offset_ptr += 2 - self.vprint(f"{prefix} CLASS CONST NAME: {AP2Property.property_to_name(propertyval)}, ALIAS: {referenceval}") + self.vprint(f"{prefix} CLASS CONST NAME: {StringConstant.property_to_name(propertyval)}, ALIAS: {referenceval}") # One would expect 0x15 to be identical to 0x12 but for class properties instead. However, it appears # that this has been omitted from game binaries. elif obj_to_create == 0x16: # Func property name. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0x400 - objects.append(AP2Property.property_to_name(propertyval)) + objects.append(StringConstant(propertyval)) offset_ptr += 1 - self.vprint(f"{prefix} FUNC CONST NAME: {AP2Property.property_to_name(propertyval)}") + self.vprint(f"{prefix} FUNC CONST NAME: {StringConstant.property_to_name(propertyval)}") elif obj_to_create == 0x17: # Func property name referencing a string table entry. propertyval, reference = struct.unpack(">BB", datachunk[offset_ptr:(offset_ptr + 2)]) propertyval += 0x400 - objects.append(AP2Property.property_to_name(propertyval)) referenceval = self.__get_string(string_offsets[reference]) + objects.append(StringConstant(propertyval, referenceval)) offset_ptr += 2 - self.vprint(f"{prefix} FUNC CONST NAME: {AP2Property.property_to_name(propertyval)}, ALIAS: {referenceval}") + self.vprint(f"{prefix} FUNC CONST NAME: {StringConstant.property_to_name(propertyval)}, ALIAS: {referenceval}") # Same comment with 0x15 applies here with 0x18. elif obj_to_create == 0x19: # Other property name. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0x200 - objects.append(AP2Property.property_to_name(propertyval)) + objects.append(StringConstant(propertyval)) offset_ptr += 1 - self.vprint(f"{prefix} OTHER CONST NAME: {AP2Property.property_to_name(propertyval)}") + self.vprint(f"{prefix} OTHER CONST NAME: {StringConstant.property_to_name(propertyval)}") elif obj_to_create == 0x1a: # Other property name referencing a string table entry. propertyval, reference = struct.unpack(">BB", datachunk[offset_ptr:(offset_ptr + 2)]) propertyval += 0x200 - objects.append(AP2Property.property_to_name(propertyval)) referenceval = self.__get_string(string_offsets[reference]) + objects.append(StringConstant(propertyval, referenceval)) offset_ptr += 2 - self.vprint(f"{prefix} OTHER CONST NAME: {AP2Property.property_to_name(propertyval)}, ALIAS: {referenceval}") + self.vprint(f"{prefix} OTHER CONST NAME: {StringConstant.property_to_name(propertyval)}, ALIAS: {referenceval}") # Same comment with 0x15 and 0x18 applies here with 0x1b. elif obj_to_create == 0x1c: # Event property name. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0x500 - objects.append(AP2Property.property_to_name(propertyval)) + objects.append(StringConstant(propertyval)) offset_ptr += 1 - self.vprint(f"{prefix} EVENT CONST NAME: {AP2Property.property_to_name(propertyval)}") + self.vprint(f"{prefix} EVENT CONST NAME: {StringConstant.property_to_name(propertyval)}") elif obj_to_create == 0x1d: # Event property name referencing a string table entry. propertyval, reference = struct.unpack(">BB", datachunk[offset_ptr:(offset_ptr + 2)]) propertyval += 0x500 - objects.append(AP2Property.property_to_name(propertyval)) referenceval = self.__get_string(string_offsets[reference]) + objects.append(StringConstant(propertyval, referenceval)) offset_ptr += 2 - self.vprint(f"{prefix} EVENT CONST NAME: {AP2Property.property_to_name(propertyval)}, ALIAS: {referenceval}") + self.vprint(f"{prefix} EVENT CONST NAME: {StringConstant.property_to_name(propertyval)}, ALIAS: {referenceval}") # Same comment with 0x15, 0x18 and 0x1b applies here with 0x1e. elif obj_to_create == 0x1f: # Key constants. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0x600 - objects.append(AP2Property.property_to_name(propertyval)) + objects.append(StringConstant(propertyval)) offset_ptr += 1 - self.vprint(f"{prefix} KEY CONST NAME: {AP2Property.property_to_name(propertyval)}") + self.vprint(f"{prefix} KEY CONST NAME: {StringConstant.property_to_name(propertyval)}") elif obj_to_create == 0x20: # Key property name referencing a string table entry. propertyval, reference = struct.unpack(">BB", datachunk[offset_ptr:(offset_ptr + 2)]) propertyval += 0x600 - objects.append(AP2Property.property_to_name(propertyval)) referenceval = self.__get_string(string_offsets[reference]) + objects.append(StringConstant(propertyval, referenceval)) offset_ptr += 2 - self.vprint(f"{prefix} KEY CONST NAME: {AP2Property.property_to_name(propertyval)}, ALIAS: {referenceval}") + self.vprint(f"{prefix} KEY CONST NAME: {StringConstant.property_to_name(propertyval)}, ALIAS: {referenceval}") # Same comment with 0x15, 0x18, 0x1b and 0x1e applies here with 0x21. elif obj_to_create == 0x22: # Pointer to global object. @@ -644,41 +644,41 @@ class SWF(TrackedCoverage, VerboseOutput): elif obj_to_create == 0x24: # Some other property name. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0x700 - objects.append(AP2Property.property_to_name(propertyval)) + objects.append(StringConstant(propertyval)) offset_ptr += 1 - self.vprint(f"{prefix} ETC2 CONST NAME: {AP2Property.property_to_name(propertyval)}") + self.vprint(f"{prefix} ETC2 CONST NAME: {StringConstant.property_to_name(propertyval)}") # Possibly in newer binaries, 0x25 and 0x26 are implemented as 8-bit and 16-bit alias pointer # versions of 0x24. elif obj_to_create == 0x27: # Some other property name. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0x800 - objects.append(AP2Property.property_to_name(propertyval)) + objects.append(StringConstant(propertyval)) offset_ptr += 1 - self.vprint(f"{prefix} ORGFUNC2 CONST NAME: {AP2Property.property_to_name(propertyval)}") + self.vprint(f"{prefix} ORGFUNC2 CONST NAME: {StringConstant.property_to_name(propertyval)}") # Possibly in newer binaries, 0x28 and 0x29 are implemented as 8-bit and 16-bit alias pointer # versions of 0x27. elif obj_to_create == 0x2a: # Some other property name. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0x900 - objects.append(AP2Property.property_to_name(propertyval)) + objects.append(StringConstant(propertyval)) offset_ptr += 1 - self.vprint(f"{prefix} ETCFUNC2 CONST NAME: {AP2Property.property_to_name(propertyval)}") + self.vprint(f"{prefix} ETCFUNC2 CONST NAME: {StringConstant.property_to_name(propertyval)}") # Possibly in newer binaries, 0x2b and 0x2c are implemented as 8-bit and 16-bit alias pointer # versions of 0x2a. elif obj_to_create == 0x2d: # Some other property name. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0xa00 - objects.append(AP2Property.property_to_name(propertyval)) + objects.append(StringConstant(propertyval)) offset_ptr += 1 - self.vprint(f"{prefix} EVENT2 CONST NAME: {AP2Property.property_to_name(propertyval)}") + self.vprint(f"{prefix} EVENT2 CONST NAME: {StringConstant.property_to_name(propertyval)}") # Possibly in newer binaries, 0x2e and 0x2f are implemented as 8-bit and 16-bit alias pointer # versions of 0x2d. elif obj_to_create == 0x30: # Some other property name. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0xb00 - objects.append(AP2Property.property_to_name(propertyval)) + objects.append(StringConstant(propertyval)) offset_ptr += 1 - self.vprint(f"{prefix} EVENT METHOD CONST NAME: {AP2Property.property_to_name(propertyval)}") + self.vprint(f"{prefix} EVENT METHOD CONST NAME: {StringConstant.property_to_name(propertyval)}") # Possibly in newer binaries, 0x31 and 0x32 are implemented as 8-bit and 16-bit alias pointer # versions of 0x30. elif obj_to_create == 0x33: @@ -691,9 +691,9 @@ class SWF(TrackedCoverage, VerboseOutput): elif obj_to_create == 0x34: # Some other property names. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0xc00 - objects.append(AP2Property.property_to_name(propertyval)) + objects.append(StringConstant(propertyval)) offset_ptr += 1 - self.vprint(f"{prefix} GENERIC CONST NAME: {AP2Property.property_to_name(propertyval)}") + self.vprint(f"{prefix} GENERIC CONST NAME: {StringConstant.property_to_name(propertyval)}") # Possibly in newer binaries, 0x35 and 0x36 are implemented as 8-bit and 16-bit alias pointer # versions of 0x34. elif obj_to_create == 0x37: @@ -706,9 +706,9 @@ class SWF(TrackedCoverage, VerboseOutput): elif obj_to_create == 0x38: # Some other property names. propertyval = struct.unpack(">B", datachunk[offset_ptr:(offset_ptr + 1)])[0] + 0xd00 - objects.append(AP2Property.property_to_name(propertyval)) + objects.append(StringConstant(propertyval)) offset_ptr += 1 - self.vprint(f"{prefix} GENERIC2 CONST NAME: {AP2Property.property_to_name(propertyval)}") + self.vprint(f"{prefix} GENERIC2 CONST NAME: {StringConstant.property_to_name(propertyval)}") # Possibly in newer binaries, 0x39 and 0x3a are implemented as 8-bit and 16-bit alias pointer # versions of 0x38. else: @@ -752,7 +752,7 @@ class SWF(TrackedCoverage, VerboseOutput): self.vprint(f"{prefix} REGISTER NO: {register_no}") self.vprint(f"{prefix} END_{action_name}") - actions.append(StoreRegisterAction(lineno, store_registers)) + actions.append(StoreRegisterAction(lineno, store_registers, preserve_stack=True)) elif opcode == AP2Action.STORE_REGISTER2: register_no = struct.unpack(">B", datachunk[(offset_ptr + 1):(offset_ptr + 2)])[0] offset_ptr += 2 @@ -761,7 +761,7 @@ class SWF(TrackedCoverage, VerboseOutput): self.vprint(f"{prefix} REGISTER NO: {register_no}") self.vprint(f"{prefix} END_{action_name}") - actions.append(StoreRegisterAction(lineno, [Register(register_no)])) + actions.append(StoreRegisterAction(lineno, [Register(register_no)], preserve_stack=False)) elif opcode == AP2Action.IF: jump_if_true_offset = struct.unpack(">h", datachunk[(offset_ptr + 1):(offset_ptr + 3)])[0] jump_if_true_offset += (lineno + 3) diff --git a/bemani/format/afp/types/__init__.py b/bemani/format/afp/types/__init__.py index efbf112..875b55b 100644 --- a/bemani/format/afp/types/__init__.py +++ b/bemani/format/afp/types/__init__.py @@ -4,8 +4,8 @@ from .ap2 import ( AP2Action, AP2Object, AP2Pointer, - AP2Property, DefineFunction2Action, + Expression, GenericObject, NULL, UNDEFINED, @@ -15,6 +15,7 @@ from .ap2 import ( CLIP, GLOBAL, Register, + StringConstant, PushAction, InitRegisterAction, StoreRegisterAction, @@ -38,8 +39,8 @@ __all__ = [ 'AP2Action', 'AP2Object', 'AP2Pointer', - 'AP2Property', 'DefineFunction2Action', + 'Expression', 'GenericObject', 'NULL', 'UNDEFINED', @@ -49,6 +50,7 @@ __all__ = [ 'CLIP', 'GLOBAL', 'Register', + 'StringConstant', 'PushAction', 'InitRegisterAction', 'StoreRegisterAction', diff --git a/bemani/format/afp/types/ap2.py b/bemani/format/afp/types/ap2.py index 89fcc22..4e96709 100644 --- a/bemani/format/afp/types/ap2.py +++ b/bemani/format/afp/types/ap2.py @@ -614,8 +614,13 @@ class DefineFunction2Action(AP2Action): ]) +class Expression: + # This is just a type class for all things that can be expressions. + pass + + # A bunch of stuff for implementing PushAction -class GenericObject: +class GenericObject(Expression): def __init__(self, name: str) -> None: self.__name = name @@ -632,306 +637,15 @@ CLIP = GenericObject('CLIP') GLOBAL = GenericObject('GLOBAL') -class Register: +class Register(Expression): def __init__(self, no: int) -> None: self.no = no def __repr__(self) -> str: - return f"Register {self.no}" + return f"registers[{self.no}]" -class PushAction(AP2Action): - def __init__(self, offset: int, objects: List[Any]) -> None: - super().__init__(offset, AP2Action.PUSH) - self.objects = objects - - def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: - return { - **super().as_dict(*args, **kwargs), - # TODO: We need to do better than this when exporting objects, - # we should preserve their type. - 'objects': [repr(o) for o in self.objects], - } - - def __repr__(self) -> str: - objects = [f" {repr(obj)}" for obj in self.objects] - action_name = AP2Action.action_to_name(self.opcode) - return os.linesep.join([ - f"{self.offset}: {action_name}", - *objects, - f"END_{action_name}", - ]) - - -class InitRegisterAction(AP2Action): - def __init__(self, offset: int, registers: List[Register]) -> None: - super().__init__(offset, AP2Action.INIT_REGISTER) - self.registers = registers - - def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: - return { - **super().as_dict(*args, **kwargs), - 'registers': [r.no for r in self.registers], - } - - def __repr__(self) -> str: - registers = [f" {reg}" for reg in self.registers] - action_name = AP2Action.action_to_name(self.opcode) - return os.linesep.join([ - f"{self.offset}: {action_name}", - *registers, - f"END_{action_name}", - ]) - - -class StoreRegisterAction(AP2Action): - def __init__(self, offset: int, registers: List[Register]) -> None: - super().__init__(offset, AP2Action.STORE_REGISTER) - self.registers = registers - - def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: - return { - **super().as_dict(*args, **kwargs), - 'registers': [r.no for r in self.registers], - } - - def __repr__(self) -> str: - registers = [f" {reg}" for reg in self.registers] - action_name = AP2Action.action_to_name(self.opcode) - return os.linesep.join([ - f"{self.offset}: {action_name}", - *registers, - f"END_{action_name}", - ]) - - -class IfAction(AP2Action): - def __init__(self, offset: int, comparison: str, jump_if_true_offset: int) -> None: - super().__init__(offset, AP2Action.IF) - self.comparison = comparison - self.jump_if_true_offset = jump_if_true_offset - - def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: - return { - **super().as_dict(*args, **kwargs), - 'jump_if_true_offset': self.jump_if_true_offset, - } - - def __repr__(self) -> str: - return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Comparison: {self.comparison}, Offset To Jump To If True: {self.jump_if_true_offset}" - - -class JumpAction(AP2Action): - def __init__(self, offset: int, jump_offset: int) -> None: - super().__init__(offset, AP2Action.JUMP) - self.jump_offset = jump_offset - - def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: - return { - **super().as_dict(*args, **kwargs), - 'jump_offset': self.jump_offset, - } - - def __repr__(self) -> str: - return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Offset To Jump To: {self.jump_offset}" - - -class WithAction(AP2Action): - def __init__(self, offset: int, unknown: bytes) -> None: - super().__init__(offset, AP2Action.WITH) - self.unknown = unknown - - def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: - return { - **super().as_dict(*args, **kwargs), - # TODO: We need to do better than this, so I guess it comes down to having - # a better idea how WITH works. - 'unknown': str(self.unknown), - } - - def __repr__(self) -> str: - return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Unknown: {self.unknown!r}" - - -class GotoFrame2Action(AP2Action): - def __init__(self, offset: int, additional_frames: int, stop: bool) -> None: - super().__init__(offset, AP2Action.GOTO_FRAME2) - self.additional_frames = additional_frames - self.stop = stop - - def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: - return { - **super().as_dict(*args, **kwargs), - 'additiona_frames': self.additional_frames, - 'stop': self.stop, - } - - def __repr__(self) -> str: - return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Additional Frames: {self.additional_frames}, Stop On Arrival: {'yes' if self.stop else 'no'}" - - -class AddNumVariableAction(AP2Action): - def __init__(self, offset: int, amount_to_add: int) -> None: - super().__init__(offset, AP2Action.ADD_NUM_VARIABLE) - self.amount_to_add = amount_to_add - - def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: - return { - **super().as_dict(*args, **kwargs), - 'amount_to_add': self.amount_to_add, - } - - def __repr__(self) -> str: - return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Amount To Add: {self.amount_to_add}" - - -class AddNumRegisterAction(AP2Action): - def __init__(self, offset: int, register: Register, amount_to_add: int) -> None: - super().__init__(offset, AP2Action.ADD_NUM_REGISTER) - self.register = register - self.amount_to_add = amount_to_add - - def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: - return { - **super().as_dict(*args, **kwargs), - 'register': self.register.no, - 'amount_to_add': self.amount_to_add, - } - - def __repr__(self) -> str: - return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Register: {self.register}, Amount To Add: {self.amount_to_add}" - - -class GetURL2Action(AP2Action): - def __init__(self, offset: int, action: int) -> None: - super().__init__(offset, AP2Action.GET_URL2) - self.action = action - - def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: - return { - **super().as_dict(*args, **kwargs), - 'action': self.action, - } - - def __repr__(self) -> str: - return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Action: {self.action}" - - -class StartDragAction(AP2Action): - def __init__(self, offset: int, constrain: Optional[bool]) -> None: - super().__init__(offset, AP2Action.START_DRAG) - self.constrain = constrain - - def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: - return { - **super().as_dict(*args, **kwargs), - 'constrain': self.constrain, - } - - def __repr__(self) -> str: - if self.constrain is None: - cstr = "check stack" - else: - cstr = "yes" if self.constrain else "no" - return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Constrain Mouse: {cstr}" - - -class AP2Object: - UNDEFINED = 0x0 - NAN = 0x1 - BOOLEAN = 0x2 - INTEGER = 0x3 - S64 = 0x4 - FLOAT = 0x5 - DOUBLE = 0x6 - STRING = 0x7 - POINTER = 0x8 - OBJECT = 0x9 - INFINITY = 0xa - CONST_STRING = 0xb - BUILT_IN_FUNCTION = 0xc - - -class AP2Pointer: - # The type of the object if it is an AP2Object.POINTER or AP2Object.OBJECT - UNDEFINED = 0x0 - AFP_TEXT = 0x1 - AFP_RECT = 0x2 - AFP_SHAPE = 0x3 - DRAG = 0x4 - MATRIX = 0x5 - POINT = 0x6 - GETTER_SETTER_PROPERTY = 0x7 - FUNCTION_WITH_PROTOTYPE = 0x8 - ROW_DATA = 0x20 - - object_W = 0x50 - movieClip_W = 0x51 - sound_W = 0x52 - color_W = 0x53 - date_W = 0x54 - array_W = 0x55 - xml_W = 0x56 - xmlNode_W = 0x57 - textFormat_W = 0x58 - sharedObject_W = 0x59 - sharedObjectData_W = 0x5a - textField_W = 0x5b - xmlAttrib_W = 0x5c - bitmapdata_W = 0x5d - matrix_W = 0x5e - point_W = 0x5f - ColorMatrixFilter_W = 0x60 - String_W = 0x61 - Boolean_W = 0x62 - Number_W = 0x63 - function_W = 0x64 - prototype_W = 0x65 - super_W = 0x66 - transform_W = 0x68 - colorTransform_W = 0x69 - rectangle_W = 0x6a - - # All of these can have prototypes, not sure what the "C" stands for. - Object_C = 0x78 - MovieClip_C = 0x79 - Sound_C = 0x7a - Color_C = 0x7b - Date_C = 0x7c - Array_C = 0x7d - XML_C = 0x7e - XMLNode_C = 0x7f - TextFormat_C = 0x80 - TextField_C = 0x83 - BitmapData_C = 0x85 - matrix_C = 0x86 - point_C = 0x87 - String_C = 0x89 - Boolean_C = 0x8a - Number_C = 0x8b - Function_C = 0x8c - aplib_C = 0x8f - transform_C = 0x90 - colorTransform_C = 0x91 - rectangle_C = 0x92 - asdlib_C = 0x93 - XMLController_C = 0x94 - eManager_C = 0x95 - - stage_O = 0xa0 - math_O = 0xa1 - key_O = 0xa2 - mouse_O = 0xa3 - system_O = 0xa4 - sharedObject_O = 0xa5 - flash_O = 0xa6 - global_O = 0xa7 - display_P = 0xb4 - geom_P = 0xb5 - filtesr_P = 0xb6 - - -class AP2Property: +class StringConstant(Expression): __PROPERTIES: List[Tuple[int, str]] = [ # Seems to be properties on every object. (0x100, '_x'), @@ -2866,3 +2580,305 @@ class AP2Property: if i == propid: return p return f"" + + def __init__(self, const: int, alias: Optional[str] = None) -> None: + self.const = const + self.alias = alias + + def __repr__(self) -> str: + if self.alias: + return self.alias + else: + return StringConstant.property_to_name(self.const) + + +class PushAction(AP2Action): + def __init__(self, offset: int, objects: List[Any]) -> None: + super().__init__(offset, AP2Action.PUSH) + self.objects = objects + + def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: + return { + **super().as_dict(*args, **kwargs), + # TODO: We need to do better than this when exporting objects, + # we should preserve their type. + 'objects': [repr(o) for o in self.objects], + } + + def __repr__(self) -> str: + objects = [f" {repr(obj)}" for obj in self.objects] + action_name = AP2Action.action_to_name(self.opcode) + return os.linesep.join([ + f"{self.offset}: {action_name}", + *objects, + f"END_{action_name}", + ]) + + +class InitRegisterAction(AP2Action): + def __init__(self, offset: int, registers: List[Register]) -> None: + super().__init__(offset, AP2Action.INIT_REGISTER) + self.registers = registers + + def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: + return { + **super().as_dict(*args, **kwargs), + 'registers': [r.no for r in self.registers], + } + + def __repr__(self) -> str: + registers = [f" {reg}" for reg in self.registers] + action_name = AP2Action.action_to_name(self.opcode) + return os.linesep.join([ + f"{self.offset}: {action_name}", + *registers, + f"END_{action_name}", + ]) + + +class StoreRegisterAction(AP2Action): + def __init__(self, offset: int, registers: List[Register], preserve_stack: bool) -> None: + super().__init__(offset, AP2Action.STORE_REGISTER) + self.registers = registers + self.preserve_stack = preserve_stack + + def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: + return { + **super().as_dict(*args, **kwargs), + 'registers': [r.no for r in self.registers], + } + + def __repr__(self) -> str: + registers = [f" {reg}" for reg in self.registers] + action_name = AP2Action.action_to_name(self.opcode) + return os.linesep.join([ + f"{self.offset}: {action_name}, Preserve Stack: {self.preserve_stack}", + *registers, + f"END_{action_name}", + ]) + + +class IfAction(AP2Action): + def __init__(self, offset: int, comparison: str, jump_if_true_offset: int) -> None: + super().__init__(offset, AP2Action.IF) + self.comparison = comparison + self.jump_if_true_offset = jump_if_true_offset + + def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: + return { + **super().as_dict(*args, **kwargs), + 'jump_if_true_offset': self.jump_if_true_offset, + } + + def __repr__(self) -> str: + return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Comparison: {self.comparison}, Offset To Jump To If True: {self.jump_if_true_offset}" + + +class JumpAction(AP2Action): + def __init__(self, offset: int, jump_offset: int) -> None: + super().__init__(offset, AP2Action.JUMP) + self.jump_offset = jump_offset + + def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: + return { + **super().as_dict(*args, **kwargs), + 'jump_offset': self.jump_offset, + } + + def __repr__(self) -> str: + return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Offset To Jump To: {self.jump_offset}" + + +class WithAction(AP2Action): + def __init__(self, offset: int, unknown: bytes) -> None: + super().__init__(offset, AP2Action.WITH) + self.unknown = unknown + + def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: + return { + **super().as_dict(*args, **kwargs), + # TODO: We need to do better than this, so I guess it comes down to having + # a better idea how WITH works. + 'unknown': str(self.unknown), + } + + def __repr__(self) -> str: + return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Unknown: {self.unknown!r}" + + +class GotoFrame2Action(AP2Action): + def __init__(self, offset: int, additional_frames: int, stop: bool) -> None: + super().__init__(offset, AP2Action.GOTO_FRAME2) + self.additional_frames = additional_frames + self.stop = stop + + def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: + return { + **super().as_dict(*args, **kwargs), + 'additiona_frames': self.additional_frames, + 'stop': self.stop, + } + + def __repr__(self) -> str: + return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Additional Frames: {self.additional_frames}, Stop On Arrival: {'yes' if self.stop else 'no'}" + + +class AddNumVariableAction(AP2Action): + def __init__(self, offset: int, amount_to_add: int) -> None: + super().__init__(offset, AP2Action.ADD_NUM_VARIABLE) + self.amount_to_add = amount_to_add + + def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: + return { + **super().as_dict(*args, **kwargs), + 'amount_to_add': self.amount_to_add, + } + + def __repr__(self) -> str: + return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Amount To Add: {self.amount_to_add}" + + +class AddNumRegisterAction(AP2Action): + def __init__(self, offset: int, register: Register, amount_to_add: int) -> None: + super().__init__(offset, AP2Action.ADD_NUM_REGISTER) + self.register = register + self.amount_to_add = amount_to_add + + def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: + return { + **super().as_dict(*args, **kwargs), + 'register': self.register.no, + 'amount_to_add': self.amount_to_add, + } + + def __repr__(self) -> str: + return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Register: {self.register}, Amount To Add: {self.amount_to_add}" + + +class GetURL2Action(AP2Action): + def __init__(self, offset: int, action: int) -> None: + super().__init__(offset, AP2Action.GET_URL2) + self.action = action + + def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: + return { + **super().as_dict(*args, **kwargs), + 'action': self.action, + } + + def __repr__(self) -> str: + return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Action: {self.action}" + + +class StartDragAction(AP2Action): + def __init__(self, offset: int, constrain: Optional[bool]) -> None: + super().__init__(offset, AP2Action.START_DRAG) + self.constrain = constrain + + def as_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: + return { + **super().as_dict(*args, **kwargs), + 'constrain': self.constrain, + } + + def __repr__(self) -> str: + if self.constrain is None: + cstr = "check stack" + else: + cstr = "yes" if self.constrain else "no" + return f"{self.offset}: {AP2Action.action_to_name(self.opcode)}, Constrain Mouse: {cstr}" + + +class AP2Object: + UNDEFINED = 0x0 + NAN = 0x1 + BOOLEAN = 0x2 + INTEGER = 0x3 + S64 = 0x4 + FLOAT = 0x5 + DOUBLE = 0x6 + STRING = 0x7 + POINTER = 0x8 + OBJECT = 0x9 + INFINITY = 0xa + CONST_STRING = 0xb + BUILT_IN_FUNCTION = 0xc + + +class AP2Pointer: + # The type of the object if it is an AP2Object.POINTER or AP2Object.OBJECT + UNDEFINED = 0x0 + AFP_TEXT = 0x1 + AFP_RECT = 0x2 + AFP_SHAPE = 0x3 + DRAG = 0x4 + MATRIX = 0x5 + POINT = 0x6 + GETTER_SETTER_PROPERTY = 0x7 + FUNCTION_WITH_PROTOTYPE = 0x8 + ROW_DATA = 0x20 + + object_W = 0x50 + movieClip_W = 0x51 + sound_W = 0x52 + color_W = 0x53 + date_W = 0x54 + array_W = 0x55 + xml_W = 0x56 + xmlNode_W = 0x57 + textFormat_W = 0x58 + sharedObject_W = 0x59 + sharedObjectData_W = 0x5a + textField_W = 0x5b + xmlAttrib_W = 0x5c + bitmapdata_W = 0x5d + matrix_W = 0x5e + point_W = 0x5f + ColorMatrixFilter_W = 0x60 + String_W = 0x61 + Boolean_W = 0x62 + Number_W = 0x63 + function_W = 0x64 + prototype_W = 0x65 + super_W = 0x66 + transform_W = 0x68 + colorTransform_W = 0x69 + rectangle_W = 0x6a + + # All of these can have prototypes, not sure what the "C" stands for. + Object_C = 0x78 + MovieClip_C = 0x79 + Sound_C = 0x7a + Color_C = 0x7b + Date_C = 0x7c + Array_C = 0x7d + XML_C = 0x7e + XMLNode_C = 0x7f + TextFormat_C = 0x80 + TextField_C = 0x83 + BitmapData_C = 0x85 + matrix_C = 0x86 + point_C = 0x87 + String_C = 0x89 + Boolean_C = 0x8a + Number_C = 0x8b + Function_C = 0x8c + aplib_C = 0x8f + transform_C = 0x90 + colorTransform_C = 0x91 + rectangle_C = 0x92 + asdlib_C = 0x93 + XMLController_C = 0x94 + eManager_C = 0x95 + + stage_O = 0xa0 + math_O = 0xa1 + key_O = 0xa2 + mouse_O = 0xa3 + system_O = 0xa4 + sharedObject_O = 0xa5 + flash_O = 0xa6 + global_O = 0xa7 + display_P = 0xb4 + geom_P = 0xb5 + filtesr_P = 0xb6