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
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)})."
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()
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 )
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}")))
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
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
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
def all_constants(self): return list(SsbConstant.collect_all(self.constant_source))
def __init__(self, rom_data: Pmd2Data): super().__init__() self.all_constants = list(SsbConstant.collect_all(rom_data.script_data))