diff --git a/bemani/format/afp/decompile.py b/bemani/format/afp/decompile.py index dfbfa6f..d1f8a2b 100644 --- a/bemani/format/afp/decompile.py +++ b/bemani/format/afp/decompile.py @@ -288,6 +288,15 @@ class ExpressionStatement(Statement): return [f"{prefix}{self.expr.render(prefix, verbose=verbose)};"] +class StopSoundStatement(Statement): + # Stop all sounds, this is an actionscript-specific opcode. + def __repr__(self) -> str: + return "builtin_StopAllSounds()" + + def render(self, prefix: str, verbose: bool = False) -> List[str]: + return [f"{prefix}builtin_StopAllSounds();"] + + class StopMovieStatement(Statement): # Stop the movie, this is an actionscript-specific opcode. def __repr__(self) -> str: @@ -499,6 +508,12 @@ class GetTimeFunctionCall(FunctionCall): super().__init__("builtin_GetCurrentPlaybackPosition", []) +class GetPathFunctionCall(FunctionCall): + # Call the built-in 'get time' method which returns the current playback position. + def __init__(self, movieclip: Any) -> None: + super().__init__("builtin_GetPathOfMovie", [movieclip]) + + class MethodCall(Expression): # Call a method on an object. def __init__(self, objectref: Any, name: Union[str, int, Expression], params: List[Any]) -> None: @@ -2211,6 +2226,9 @@ class ByteCodeDecompiler(VerboseOutput): # exist before combining them, and we can't do that until we walk the stack, and the # stack walking algorithm both a) comes later and b) relies on all ifs being processed. # So, this stays as a beta for now, and will possibly be integrated at a later time. + # If we want to use this, we should probably reformat it to work on the finished + # statement list we get after fully rendering the stack, and use it in the optimization + # pass phase to rewrite code with fewer (possibly sometimes no) gotos. chunks_by_id: Dict[int, ArbitraryCodeChunk] = {chunk.id: chunk for chunk in chunks} chunks_examined: Set[int] = set() @@ -2540,6 +2558,10 @@ class ByteCodeDecompiler(VerboseOutput): chunk.actions[i] = PreviousFrameStatement() continue + if action.opcode == AP2Action.STOP_SOUND: + chunk.actions[i] = StopSoundStatement() + continue + if action.opcode == AP2Action.CLONE_SPRITE: depth = stack.pop() if not isinstance(depth, (int, Expression)): @@ -2556,35 +2578,6 @@ class ByteCodeDecompiler(VerboseOutput): chunk.actions[i] = RemoveSpriteStatement(obj) continue - if action.opcode == AP2Action.GET_VARIABLE: - variable_name = stack.pop() - 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 - - if action.opcode == AP2Action.DELETE: - member_name = stack.pop() - if not isinstance(member_name, (str, int, Expression)): - raise Exception("Logic error!") - obj_name = stack.pop() - - chunk.actions[i] = DeleteMemberStatement(obj_name, member_name) - 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.TO_NUMBER: obj_ref = stack.pop() stack.append(FunctionCall('int', [obj_ref])) @@ -2687,6 +2680,18 @@ class ByteCodeDecompiler(VerboseOutput): chunk.actions[i] = NopStatement() continue + if action.opcode == AP2Action.GET_VARIABLE: + variable_name = stack.pop() + 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 + if action.opcode == AP2Action.SET_VARIABLE: set_value = stack.pop() local_name = stack.pop() @@ -2699,6 +2704,33 @@ class ByteCodeDecompiler(VerboseOutput): continue + if action.opcode == AP2Action.DELETE: + member_name = stack.pop() + if not isinstance(member_name, (str, int, Expression)): + raise Exception("Logic error!") + obj_name = stack.pop() + + chunk.actions[i] = DeleteMemberStatement(obj_name, member_name) + 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.GET_MEMBER: + member_name = stack.pop() + if not isinstance(member_name, (str, int, Expression)): + raise Exception("Logic error!") + object_reference = stack.pop() + stack.append(Member(object_reference, member_name)) + + chunk.actions[i] = NopStatement() + continue + if action.opcode == AP2Action.SET_MEMBER: set_value = stack.pop() member_name = stack.pop() @@ -2709,6 +2741,30 @@ class ByteCodeDecompiler(VerboseOutput): chunk.actions[i] = SetMemberStatement(object_reference, member_name, set_value) continue + if action.opcode == AP2Action.GET_PROPERTY: + property_int = stack.pop() + if not isinstance(property_int, int): + # Its possible that code which uses this outdated SWF GET_PROPERTY call + # might dynamically calculate the integer which it wants to use to get + # a property on. But, probably not. I haven't seen any code use this or + # SET_PROPERTY so this is just here for documentation. + raise Exception("Logic error!") + object_reference = stack.pop() + stack.append(Member(object_reference, StringConstant(property_int + 0x100))) + + chunk.actions[i] = NopStatement() + continue + + if action.opcode == AP2Action.SET_PROPERTY: + set_value = stack.pop() + property_int = stack.pop() + if not isinstance(property_int, int): + raise Exception("Logic error!") + object_reference = stack.pop() + + chunk.actions[i] = SetMemberStatement(object_reference, StringConstant(property_int + 0x100), set_value) + continue + if action.opcode == AP2Action.DEFINE_LOCAL: set_value = stack.pop() local_name = stack.pop() @@ -2723,20 +2779,9 @@ class ByteCodeDecompiler(VerboseOutput): if not isinstance(local_name, (str, StringConstant)): raise Exception(f"Logic error, local name {local_name} is not a string!") - # 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, int, Expression)): - raise Exception("Logic error!") - object_reference = stack.pop() - stack.append(Member(object_reference, member_name)) - - chunk.actions[i] = NopStatement() - continue - if action.opcode == AP2Action.NEW_OBJECT: object_name = stack.pop() if not isinstance(object_name, (str, StringConstant)): @@ -2896,8 +2941,6 @@ class ByteCodeDecompiler(VerboseOutput): continue if action.opcode == AP2Action.PUSH_DUPLICATE: - # TODO: This might benefit from generating a temp variable assignment - # and pushing that onto the stack twice, instead of whatever's on the stack. dup = stack.pop() stack.append(dup) stack.append(dup) @@ -2910,6 +2953,57 @@ class ByteCodeDecompiler(VerboseOutput): chunk.actions[i] = NopStatement() continue + if action.opcode == AP2Action.TARGET_PATH: + clip = stack.pop() + stack.append(GetPathFunctionCall(clip)) + + chunk.actions[i] = NopStatement() + continue + + if action.opcode == AP2Action.CAST_OP: + obj_ref = stack.pop() + class_ref = stack.pop() + stack.append(FunctionCall('cast', [obj_ref, class_ref])) + + chunk.actions[i] = NopStatement() + continue + + if action.opcode == AP2Action.IMPLEMENTS_OP: + # This appears to be completely unimplemented/broken in + # Bishi so I have no idea what it intends to do. Probably + # I could look at the SWF spec and infer the functionality + # but there aren't any files that I've found in any games + # that use this opcode, so meh. + raise Exception(f"TODO: {action}") + + if action.opcode == AP2Action.STACK_SWAP: + first = stack.pop() + second = stack.pop() + stack.append(first) + stack.append(second) + + chunk.actions[i] = NopStatement() + continue + + # None of the below actions are understood outside of the fact + # that they operate entirely on the stack. They do not appear to + # be used in any game code I've come across and might be remnants + # of when the code was for playing SWF directly. + if action.opcode == AP2Action.ENUMERATE2: + raise Exception(f"TODO: {action}") + + if action.opcode == AP2Action.EXTENDS: + raise Exception(f"TODO: {action}") + + if action.opcode == AP2Action.END_DRAG: + raise Exception(f"TODO: {action}") + + if action.opcode == AP2Action.NEW_METHOD: + raise Exception(f"TODO: {action}") + + if action.opcode == AP2Action.GET_TARGET: + raise Exception(f"TODO: {action}") + if isinstance(action, NullReturnStatement): # We already handled this continue @@ -2939,8 +3033,7 @@ class ByteCodeDecompiler(VerboseOutput): ) continue - self.vprint(stack) - raise Exception(f"TODO: {action}") + raise Exception(f"Unexpected action {action}, the cases above should be exhaustive!") # Now, clean up code generation. new_actions: List[ConvertedAction] = [] @@ -3983,6 +4076,29 @@ class ByteCodeDecompiler(VerboseOutput): if not any_changed: break + # TODO: There's definitely a lot missing from this decompilation process. + # For one, function definitions do not include any mention of number of + # arguments. It appears that functions take arguments in the registers + # and when you call a function/method, the values that are popped from + # the stack for the function/method call are placed into registers for the + # function itself to access. However, there's some implicit parameters such + # as "_this" which is even checked for in some Bishi code. Ideally the code + # can be cross-referenced with function calls to determine the number of + # arguments and the decompilation can be improved in that regard, but we + # would need to nail down function call semantics better. The TRACE opcode + # is still active in Bishi and its output can be coaxed to appear in stdout + # so it would be possible to craft some bytecode and print out the register + # contents in a function call to nail this down, but it is left as a future + # enhancement. + + # TODO: If statements still don't support compound or properly, and resort + # to using nasty gotos. We have a prototype of an algorithm above with its + # own TODO section that can possibly fix this, but I haven't taken the time + # to try to fix it up and integrate it. It would produde far more readable + # code in some instances. We also would probably want to collapse some really + # long if chains to swithc statements or if/elif/else blocks for readability + # but that is also left as a future enhancement. + # Let's sanity check the code for a few things that might trip us up. self.__verify_balanced_labels(statements) self.__verify_no_empty_ifs(statements) diff --git a/bemani/format/afp/types/ap2.py b/bemani/format/afp/types/ap2.py index 06fdec2..cd5640c 100644 --- a/bemani/format/afp/types/ap2.py +++ b/bemani/format/afp/types/ap2.py @@ -210,10 +210,11 @@ class AP2Action: # variable with that name equal to the first object. SET_VARIABLE = 15 - # Similar to get variable. + # Similar to GET_MEMBER, but the member value is an integer in the range 0x0-0x15 which + # gets added to 0x100 and looked up in StringConstants. GET_PROPERTY = 16 - # Simiar to set variable. + # Similar to SET_MEMBER in exactly the same way GET_PROPERTY is similar to GET_MEMBER. SET_PROPERTY = 17 # Clone a sprite that's specified on the stack. @@ -656,7 +657,9 @@ class Register(Expression): class StringConstant(Expression): __PROPERTIES: List[Tuple[int, str]] = [ - # Seems to be properties on every object. + # Seems to be properties on every object. These also match the original + # SWF properties up to 0x115. GET_PROPERTY and SET_PROPERTY use these + # values to determine what to look up on an object. (0x100, '_x'), (0x101, '_y'), (0x102, '_xscale'),