예제 #1
0
 def add_linked_to_names_to_routine_ops(self):
     for _, r in self.routine_info:
         try:
             if r.type == SsbRoutineType.ACTOR:
                 r.linked_to_name = SsbConstant.create_for(self._scriptdata.level_entities__by_id[r.linked_to]).name
             elif r.type == SsbRoutineType.OBJECT:
                 r.linked_to_name = SsbConstant.create_for(self._scriptdata.objects__by_id[r.linked_to]).name
         except KeyError:
             pass
예제 #2
0
    def _assert_same_vertex(self, i, self_v: 'Vertex', other_v: 'Vertex'):
        if self_v is None or other_v is None:
            assert self_v == other_v, f"Both must be None {self._r_info(i)}"
            return

        self_op: SsbOperation = self_v['op']
        other_op: SsbOperation = other_v['op']
        # We can't really check foreign jumps
        if isinstance(self_op, SsbForeignLabel) or isinstance(
                other_op, SsbForeignLabel):
            assert isinstance(self_op, SsbForeignLabel) and isinstance(other_op, SsbForeignLabel), \
                f"If one is foreign label, both must be ({self._r_info(i)})."
            return
        # If this is a label jump, take root.
        if hasattr(self_op, 'root'):
            self_op = self_op.root
        if hasattr(other_op, 'root'):
            other_op = other_op.root

        # OPCODES EXCEPTIONS
        # We replace flag_CalcValue with the ASSIGN operator with flag_Set
        if self_op.op_code.name == OPS_FLAG__CALC_VALUE and self_op.params[
                1] == SsbCalcOperator.ASSIGN.value:
            self_op.op_code = SsbOpCode(-1, OPS_FLAG__SET)
            self_op.params = [self_op.params[0], self_op.params[2]]
        if other_op.op_code.name == OPS_FLAG__CALC_VALUE and other_op.params[
                1] == SsbCalcOperator.ASSIGN.value:
            other_op.op_code = SsbOpCode(-1, OPS_FLAG__SET)
            other_op.params = [other_op.params[0], other_op.params[2]]
        # We replace BranchBit + PERFORMANCE_PROGRESS_LIST with BranchPerformance
        if self_op.op_code.name == OP_BRANCH_BIT and self_op.params[
                0].name == SsbConstant.create_for(
                    self._variables_by_name['PERFORMANCE_PROGRESS_LIST']).name:
            self_op.op_code.name = SsbOpCode(-1, OP_BRANCH_PERFORMANCE)
            self_op.params = [self_op.params[1], 1]
        if other_op.op_code.name == OP_BRANCH_BIT and other_op.params[
                0].name == SsbConstant.create_for(
                    self._variables_by_name['PERFORMANCE_PROGRESS_LIST']).name:
            other_op.op_code.name = SsbOpCode(-1, OP_BRANCH_PERFORMANCE)
            other_op.params = [other_op.params[1], 1]

        assert self_op.op_code.name == other_op.op_code.name, f"Opcodes were not the same: {self_op.op_code.name} " \
                                                              f"vs. {other_op.op_code.name} [{self_v.index}," \
                                                              f"{other_v.index}] ({self._r_info(i)})."

        self_op_params = self_op.params
        other_op_params = other_op.params
        # PARAMETER EXCEPTIONS:
        # We force BranchVariation to be a boolean, because the game seems to treat it as such too.
        if self_op.op_code.name == OP_BRANCH_VARIATION:
            if self_op_params[0] > 1:
                self_op_params[0] = 1
            if other_op_params[0] > 1:
                other_op_params[0] = 1

        assert self_op_params == other_op_params, f"Parameters of opcode ({self_op.op_code.name}) [{self_v.index}," \
                                                  f"{other_v.index}] are not the same ({self._r_info(i)})."
예제 #3
0
 def to_explorerscript(self) -> Tuple[str, SourceMap]:
     self.add_linked_to_names_to_routine_ops()
     return ExplorerScriptSsbDecompiler(
         [x[1] for x in self.routine_info],
         self.get_filled_routine_ops(),
         self._scriptdata.common_routine_info__by_id,
         SsbConstant.create_for(self._scriptdata.game_variables__by_name['PERFORMANCE_PROGRESS_LIST']).name,
         SsbConstant.get_dungeon_mode_constants()
     ).convert()
예제 #4
0
    def compile_explorerscript(self, es_src: str, exps_absolue_path: str,
                               callback_after_parsing: Callable = None,
                               lookup_paths: List[str] = None) -> Tuple[Ssb, SourceMap]:
        """
        Compile ExplorerScript into a SSB model. Returns the Ssb model, the source map, and a list of macros
        that were used in the ExplorerScript file.

        lookup_paths is the list of include lookup paths.

        :raises: ParseError: On parsing errors
        :raises: SsbCompilerError: On logical compiling errors (eg. unknown opcodes)
        :raises: ValueError: On misc. logical compiling errors (eg. unknown constants)
        """
        logger.debug("Compiling ExplorerScript (size: %d, path: %s)...", len(es_src), exps_absolue_path)

        base_compiler = ExplorerScriptSsbCompiler(
            SsbConstant.create_for(self.rom_data.script_data.game_variables__by_name['PERFORMANCE_PROGRESS_LIST']).name,
            lookup_paths
        )
        base_compiler.compile(es_src, exps_absolue_path)

        # Profiling callback
        if callback_after_parsing:
            callback_after_parsing()

        return self.compile_structured(
            base_compiler.routine_infos, base_compiler.routine_ops, base_compiler.named_coroutines,
            base_compiler.source_map
        )
예제 #5
0
    def _parse_param(self, param: SsbOpParam, built_strings: Dict[str, List[str]], built_constants: List[str]) -> int:
        if isinstance(param, int):
            return param

        if isinstance(param, SsbOpParamConstant):
            try:
                return SsbConstant(param.name, self.rom_data.script_data).value.id
            except ValueError as err:
                raise SsbCompilerError(str(err)) from err

        if isinstance(param, SsbOpParamConstString):
            i = len(built_constants)
            built_constants.append(param.name)
            return i

        if isinstance(param, SsbOpParamLanguageString):
            i = len(built_strings[next(iter(built_strings.keys()))])
            if len(param.strings.keys()) == 1:
                # Single language convenience mode, apply this to all languages.
                only_value = param.strings[next(iter(param.strings.keys()))]
                for lang in built_strings.keys():
                    built_strings[lang].append(only_value)
            else:
                # Multi language regular case. All languages must be known.
                for lang, string in param.strings.items():
                    if lang not in built_strings:
                        raise SsbCompilerError(f(_("Unknown language for string: {lang}")))
                    built_strings[lang].append(string)
            return StringIndexPlaceholder(i)

        raise SsbCompilerError(f(_("Invalid parameter supplied for an operation: {param}")))
예제 #6
0
 def _get_special_words_uncached(self):
     pro = RomProject.get_current()
     yield from self.get_static_data().script_data.op_codes__by_name.keys()
     yield from (x.name.replace('$', '') for x in SsbConstant.collect_all(
         self.get_static_data().script_data))
     yield from EXPS_KEYWORDS
     yield from pro.get_string_provider().get_all(StringType.POKEMON_NAMES)
 def get_special_words(self) -> Iterable[str]:
     """
     Just returns the script operations and constants,
     more data is only supported by the main SkyTemple application
     """
     yield from self._static_data.script_data.op_codes__by_name.keys()
     yield from (
         x.name.replace('$', '')
         for x in SsbConstant.collect_all(self._static_data.script_data))
     yield from EXPS_KEYWORDS
예제 #8
0
 def get_filled_routine_ops(self):
     """Returns self.routine_ops, but with constant strings, strings and constants from scriptdata filled out"""
     logger.debug("Disassembling SSB model data...")
     rtns: List[List[SkyTempleSsbOperation]] = []
     pos_marker_increment = 0
     for rtn in self.routine_ops:
         rtn_ops = []
         for op in rtn:
             new_params = []
             skip_arguments = 0
             for i, param in enumerate(op.params):
                 if skip_arguments > 0:
                     skip_arguments -= 1
                     continue
                 argument_spec = self._get_argument_spec(op.op_code, i)
                 if argument_spec is not None:
                     if argument_spec.type == 'uint':
                         # TODO: Do unsigned parameters actually exist? If so are they also 14bit?
                         new_params.append(param)
                     elif argument_spec.type == 'sint':
                         # 14 bit signed int.
                         if param & 0x4000:
                             param = -0x8000 + param
                         new_params.append(param)
                     elif argument_spec.type == 'Entity':
                         new_params.append(SsbConstant.create_for(self._scriptdata.level_entities__by_id[param]))
                     elif argument_spec.type == 'Object':
                         new_params.append(SsbConstant.create_for(self._scriptdata.objects__by_id[param]))
                     elif argument_spec.type == 'Routine':
                         new_params.append(SsbConstant.create_for(self._scriptdata.common_routine_info__by_id[param]))
                     elif argument_spec.type == 'Face':
                         if param in self._scriptdata.face_names__by_id:
                             new_params.append(SsbConstant.create_for(self._scriptdata.face_names__by_id[param]))
                         else:
                             logger.warning(f"Unknown face id: {param}")
                             new_params.append(param)
                     elif argument_spec.type == 'FaceMode':
                         new_params.append(SsbConstant.create_for(self._scriptdata.face_position_modes__by_id[param]))
                     elif argument_spec.type == 'GameVar':
                         new_params.append(SsbConstant.create_for(self._scriptdata.game_variables__by_id[param]))
                     elif argument_spec.type == 'Level':
                         if param in self._scriptdata.level_list__by_id:
                             new_params.append(SsbConstant.create_for(self._scriptdata.level_list__by_id[param]))
                         else:
                             logger.warning(f"Unknown level id: {param}")
                             new_params.append(param)
                     elif argument_spec.type == 'Menu':
                         if param in self._scriptdata.menus__by_id:
                             new_params.append(SsbConstant.create_for(self._scriptdata.menus__by_id[param]))
                         else:
                             logger.warning(f"Unknown menu id: {param}")
                             new_params.append(param)
                     elif argument_spec.type == 'ProcessSpecial':
                         if param in self._scriptdata.process_specials__by_id:
                             new_params.append(SsbConstant.create_for(self._scriptdata.process_specials__by_id[param]))
                         else:
                             new_params.append(param)
                             logger.warning(f"Unknown special process id: {param}")
                     elif argument_spec.type == 'Direction':
                         if param in self._scriptdata.directions__by_ssb_id:
                             new_params.append(SsbConstant.create_for(self._scriptdata.directions__by_ssb_id[param]))
                         else:
                             new_params.append(param)
                             logger.warning(f"Unknown direction id: {param}")
                     elif argument_spec.type == 'Bgm':
                         if param in self._scriptdata.bgms__by_id:
                             new_params.append(SsbConstant.create_for(self._scriptdata.bgms__by_id[param]))
                         else:
                             logger.warning(f"Unknown BGM id: {param}")
                             new_params.append(param)
                     elif argument_spec.type == 'Effect':
                         if param in self._scriptdata.sprite_effects__by_id:
                             new_params.append(SsbConstant.create_for(self._scriptdata.sprite_effects__by_id[param]))
                         else:
                             logger.warning(f"Unknown effect id: {param}")
                             new_params.append(param)
                     elif argument_spec.type == 'String':
                         try:
                             new_params.append(SsbOpParamLanguageString(self.get_single_string(param - len(self.constants))))
                         except IndexError:
                             # Fall back to const table
                             new_params.append(SsbOpParamConstString(self.constants[param]))
                     elif argument_spec.type == 'ConstString':
                         try:
                             new_params.append(SsbOpParamConstString(self.constants[param]))
                         except IndexError:
                             # Fall back to lang string
                             new_params.append(SsbOpParamLanguageString(self.get_single_string(param - len(self.constants))))
                     elif argument_spec.type == 'PositionMark':
                         x_offset = y_offset = x_relative = y_relative = 0
                         try:
                             x_offset = param
                             y_offset = op.params[i + 1]
                             x_relative = op.params[i + 2]
                             y_relative = op.params[i + 3]
                         except IndexError:
                             logger.warning("SSB had wrong number of arguments for building a position marker.")
                         new_params.append(SsbOpParamPositionMarker(
                             f'm{pos_marker_increment}', x_offset, y_offset, x_relative, y_relative
                         ))
                         pos_marker_increment += 1
                         skip_arguments = 3
                     else:
                         raise RuntimeError(f"Unknown argument type '{argument_spec.type}'")
                 else:
                     raise RuntimeError(f"Missing argument spec for argument #{i} for OpCode {op.op_code.name}")
             new_op = SkyTempleSsbOperation(op.offset, op.op_code, new_params)
             rtn_ops.append(new_op)
         rtns.append(rtn_ops)
     return rtns
예제 #9
0
    def compile_structured(
            self,
            routine_infos: List[SsbRoutineInfo],
            routine_ops: List[List[SsbOperation]],
            named_coroutines: List[str],
            original_source_map: SourceMap
    ) -> Tuple[Ssb, SourceMap]:
        """Compile the structured data from a base compiler for SsbScript or ExplorerScript into an SSB model."""
        logger.debug("Assembling SSB model...")

        model = Ssb.create_empty(self.rom_data.script_data)
        if len(routine_ops) != len(routine_ops) != len(named_coroutines):
            raise SsbCompilerError(_("The routine data lists for the decompiler must have the same lengths."))

        # Build routines and opcodes.
        if len(routine_ops) > 0:
            header_class = SsbHeaderUs
            if self.rom_data.game_region == GAME_REGION_EU:
                header_class = SsbHeaderEu

            built_strings: Dict[str, List[str]] = {lang: [] for lang in header_class.supported_langs()}
            built_constants: List[str] = []

            for i, r in enumerate(routine_infos):
                if r is None:
                    raise SsbCompilerError(f(_("Routine {i} not found.")))

            input_routine_structure: List[
                Tuple[SsbRoutineInfo, str, List[SsbOperation]]
            ] = list(zip(routine_infos, named_coroutines, routine_ops))

            # The cursor position of the written routine opcodes.
            # The opcodes start after the routine info, which has a fixed length, based on the number of routines.
            opcode_cursor = SSB_LEN_ROUTINE_INFO_ENTRY * len(input_routine_structure) + SSB_PADDING_BEFORE_ROUTINE_INFO
            # If it has any coroutines, they all have to be.
            has_coroutines = routine_infos[0].type == SsbRoutineType.COROUTINE

            # Run coroutine checks and sortings.
            if has_coroutines:
                # Assert, that the data contains all coroutines from the ROM schema and sort all three lists by this
                if len(input_routine_structure) != len(self.rom_data.script_data.common_routine_info):
                    raise SsbCompilerError(
                        f(_("The script must contain exactly {len(self.rom_data.script_data.common_routine_info)} coroutines."))
                    )
                if len(routine_infos) != len(set(named_coroutines)):
                    raise SsbCompilerError(f(_("The script must not contain any duplicate coroutines.")))
                try:
                    input_routine_structure = sorted(
                        input_routine_structure, key=lambda k: self.rom_data.script_data.common_routine_info__by_name[k[1]].id
                    )
                except KeyError as err:
                    raise SsbCompilerError(f(_("Unknown coroutine {err}"))) from err

            # Build Routine Infos
            built_routine_info_with_offset: List[Tuple[int, SsbRoutineInfo]] = []
            built_routine_ops: List[List[SsbOperation]] = []
            # A list of lists for ALL opcodes that maps all opcode indices to their memory address.
            opcode_index_mem_offset_mapping: Dict[int, int] = {}
            bytes_written_last_rtn = 0

            for i, (input_info, __, input_ops) in enumerate(input_routine_structure):
                if (
                        has_coroutines and input_info.type != SsbRoutineType.COROUTINE
                ) or (
                        not has_coroutines and input_info.type == SsbRoutineType.COROUTINE
                ):
                    raise SsbCompilerError(f(_("Coroutines and regular routines can not be mixed in a script file.")))

                routine_start_cursor = opcode_cursor
                # Build OPs
                built_ops: List[SkyTempleSsbOperation] = []
                if len(input_ops) == 0:
                    # ALIAS ROUTINE. This alias the PREVIOUS routine
                    routine_start_cursor = opcode_cursor - bytes_written_last_rtn
                else:
                    bytes_written_last_rtn = 0
                    for in_op in input_ops:
                        if in_op.op_code.name not in self.rom_data.script_data.op_codes__by_name:
                            raise SsbCompilerError(f(_("Unknown operation {in_op.op_code.name}.")))
                        op_codes: List[Pmd2ScriptOpCode] = self.rom_data.script_data.op_codes__by_name[in_op.op_code.name]
                        if len(op_codes) > 1:
                            # Can be either a variable length opcode or the "normal" variant.
                            var_len_op_code = next(o for o in op_codes if o.params == -1)
                            normal_op_code = next(o for o in op_codes if o.params != -1)
                            if self._correct_param_list_len(in_op.params) == normal_op_code.params:
                                op_code = normal_op_code
                            elif self._correct_param_list_len(in_op.params) > normal_op_code.params:
                                op_code = var_len_op_code
                            else:
                                raise SsbCompilerError(f(_("The number of parameters for {normal_op_code.name} "
                                                          "must be at least {normal_op_code.params}, is {self._correct_param_list_len(in_op.params)}.")))
                        else:
                            op_code = op_codes[0]
                        new_params: List[int] = []
                        op_len = 2
                        if op_code.params == -1:
                            # Handle variable length opcode by inserting the number of opcodes as the first argument.
                            # ... nothing to do here! Writing the first "meta-argument" for the number of arguments
                            # is the job of the writer later!
                            op_len += 2
                            pass
                        elif self._correct_param_list_len(in_op.params) != op_code.params:
                            # TODO: This might be a confusing count for end users in the case of position markers.
                            raise SsbCompilerError(f(_("The number of parameters for {op_code.name} "
                                                       "must be {op_code.params}, is {self._correct_param_list_len(in_op.params)}.")))
                        for param in in_op.params:
                            if isinstance(param, SsbOpParamPositionMarker):
                                # Handle multi-argument case position markers
                                new_params.append(param.x_offset)
                                new_params.append(param.y_offset)
                                new_params.append(param.x_relative)
                                new_params.append(param.y_relative)
                                op_len += 8
                            else:
                                # Handle the rest
                                new_params.append(self._parse_param(param, built_strings, built_constants))
                                op_len += 2
                        built_ops.append(SkyTempleSsbOperation(opcode_cursor, op_code, new_params))

                        # Create actual offset mapping for this opcode and update source map
                        opcode_index_mem_offset_mapping[in_op.offset] = int(opcode_cursor / 2)

                        bytes_written_last_rtn += op_len
                        opcode_cursor += op_len

                # Find out the target for this routine if it's specified by name
                if input_info.linked_to == -1:
                    input_info.linked_to = SsbConstant(input_info.linked_to_name, self.rom_data.script_data).value.id

                built_routine_info_with_offset.append((routine_start_cursor, input_info))
                built_routine_ops.append(built_ops)

            # Second pass: Update all jumps to their correct place and update string index positions
            for built_routine in built_routine_ops:
                for op in built_routine:
                    if op.op_code.name in OPS_WITH_JUMP_TO_MEM_OFFSET:
                        param_id = OPS_WITH_JUMP_TO_MEM_OFFSET[op.op_code.name]
                        index_to_jump_to = op.params[param_id]
                        op.params[param_id] = opcode_index_mem_offset_mapping[index_to_jump_to]
                    for i, param in enumerate(op.params):
                        if isinstance(param, StringIndexPlaceholder):
                            # If the parameter is a reference to a language string, the length of the constants
                            # has to be added, because the language strings are after the const strings.
                            op.params[i] = len(built_constants) + int(param)

            # Fill the model
            model.routine_info = built_routine_info_with_offset
            model.routine_ops = built_routine_ops
            model.constants = built_constants
            model.strings = built_strings

            # Update the source map
            original_source_map.rewrite_offsets(opcode_index_mem_offset_mapping)

        return model, original_source_map
예제 #10
0
 def all_constants(self):
     return list(SsbConstant.collect_all(self.constant_source))
예제 #11
0
 def __init__(self, rom_data: Pmd2Data):
     super().__init__()
     self.all_constants = list(SsbConstant.collect_all(rom_data.script_data))