diff --git a/migration_scripts/convert_partner_parties.py b/migration_scripts/convert_partner_parties.py new file mode 100644 index 0000000000..e726dcc723 --- /dev/null +++ b/migration_scripts/convert_partner_parties.py @@ -0,0 +1,319 @@ +# If you have extra members in 'TrainerMon': +# 1. Add a regular expression which matches that member (e.g. 'shadow_definition'). +# 2. Match that regular expression in 'convert' and write into 'attributes' with the key that 'trainerproc' should parse. +# 3. Add the key used in 'attributes' to 'pokemon_attribute_order'. +# 4. Update 'trainerproc.c' to parse the new key. + +import re +import sys + +is_blank = re.compile(r'^[ \t]*(//.*)?$') + +begin_party_definition = re.compile(r'struct TrainerMon (\w+)\[\] =') +end_party_definition = re.compile(r'^};') +begin_pokemon_definition = re.compile(r'^ { *$') +end_pokemon_definition = re.compile(r'^ },? *$') +level_definition = re.compile(r'\.lvl = (\d+)') +species_definition = re.compile(r'\.species = SPECIES_(\w+)') +gender_definition = re.compile(r'\.gender = TRAINER_MON_(\w+)') +nickname_definition = re.compile(r'\.nickname = COMPOUND_STRING\("([^"]+)"\)') +item_definition = re.compile(r'\.heldItem = ITEM_(\w+)') +ball_definition = re.compile(r'\.ball = ITEM_(\w+)') +ability_definition = re.compile(r'\.ability = ABILITY_(\w+)') +friendship_definition = re.compile(r'\.friendship = (\d+)') +shiny_definition = re.compile(r'\.isShiny = (\w+)') +ivs_definition = re.compile(r'\.iv = TRAINER_PARTY_IVS\(([0-9 ]+),([0-9 ]+),([0-9 ]+),([0-9 ]+),([0-9 ]+),([0-9 ]+)\)') +evs_definition = re.compile(r'\.ev = TRAINER_PARTY_EVS\(([0-9 ]+),([0-9 ]+),([0-9 ]+),([0-9 ]+),([0-9 ]+),([0-9 ]+)\)') +moves_definition = re.compile(r'\.moves = \{([^}]+)\}') +move_definition = re.compile(r'MOVE_(\w+)') +nature_definition = re.compile(r'\.nature = NATURE_(\w+)') + +# NOTE: These are just for aesthetics, the Pokemon would still compile +# without them. +species_replacements = { + "CHIEN_PAO": "Chien-Pao", + "CHI_YU": "Chi-Yu", + "HAKAMO_O": "Hakamo-o", + "HO_OH": "Ho-Oh", + "JANGMO_O": "Jangmo-o", + "KOMMO_O": "Kommo-o", + "PORYGON_Z": "Porygon-Z", + "ROTOM_": "Rotom-", + "TING_LU": "Ting-Lu", + "TYPE_NULL": "Type: Null", + "WO_CHIEN": "Wo-Chien", + + "_ALOLAN": "-Alola", + "_AQUA_BREED": "-Aqua", + "_BATTLE_BOND": "-Bond", + "_BLAZE_BREED": "-Blaze", + "_CAP": "", + "_CLOAK": "", + "_COMBAT_BREED": "-Combat", + "_CROWED_SHIELD": "-Crowned", + "_CROWED_SWORD": "-Crowned", + "_DRIVE": "", + "_EAST_SEA": "-East", + "_FAMILY_OF_FOUR": "-Four", + "_FEMALE": "-F", + "_FLOWER": "", + "_GALARIAN": "-Galar", + "_GIGANTAMAX": "-Gmax", + "_HISUIAN": "-Hisui", + "_ICE_RIDER": "-Ice", + "_NOICE_FACE": "-Noice", + "_ORIGIN": "-Origin", + "_ORIGINAL_COLOR": "-Original", + "_PALDEAN": "-Paldea", + "_PLUMAGE": "", + "_POKE_BALL": "-Pokeball", + "_SHADOW_RIDER": "-Shadow", + "_STRIKE_STYLE": "-Style", + "_TOTEM": "-Totem", + "_ZEN_MODE": "-Zen", +} + +pokemon_attribute_order = ['Level', 'Ability', 'IVs', 'EVs', 'Happiness', 'Shiny', 'Ball'] + +class Pokemon: + def __init__(self): + self.nickname = None + self.species = None + self.gender = None + self.item = None + self.nature = None + self.attributes = {} + self.attributes['IVs'] = "0 HP / 0 Atk / 0 Def / 0 SpA / 0 SpD / 0 Spe" + self.moves = [] + +def convert_parties(in_path, in_h): + party_identifier = None + party = None + pokemon = None + parties = {} + + for line_no, line in enumerate(in_h, 1): + try: + line = line[:-1] + if m := begin_party_definition.search(line): + if party: + raise Exception(f"unexpected start of party") + [identifier] = m.groups() + party_identifier = identifier + party = [] + elif end_party_definition.search(line): + if not party: + raise Exception(f"unexpected end of party") + parties[party_identifier] = party + party = None + elif begin_pokemon_definition.search(line): + if pokemon: + raise Exception(f"unexpected start of Pokemon") + pokemon = Pokemon() + elif end_pokemon_definition.search(line): + if not pokemon: + raise Exception(f"unexpected end of Pokemon") + else: + party.append(pokemon) + pokemon = None + elif m := level_definition.search(line): + [level] = m.groups() + pokemon.attributes['Level'] = level + elif m := species_definition.search(line): + [species_] = m.groups() + for match, replacement in species_replacements.items(): + species_ = species_.replace(match, replacement) + pokemon.species = species_.replace("_", " ").title() + elif m := gender_definition.search(line): + [gender_] = m.groups() + if gender_ == 'MALE': + pokemon.gender = 'M' + elif gender_ == 'FEMALE': + pokemon.gender = 'F' + else: + raise Exception(f"unknown gender: '{gender_}'") + elif m := nickname_definition.search(line): + [nickname] = m.groups() + pokemon.nickname = nickname + elif m := item_definition.search(line): + [item_] = m.groups() + pokemon.item = item_.replace("_", " ").title() + elif m := ball_definition.search(line): + [ball] = m.groups() + pokemon.attributes['Ball'] = ball.replace("_", " ").title() + elif m := ability_definition.search(line): + [ability] = m.groups() + pokemon.attributes['Ability'] = ability.replace("_", " ").title() + elif m := friendship_definition.search(line): + [friendship] = m.groups() + pokemon.attributes['Happiness'] = friendship + elif m := shiny_definition.search(line): + [shiny] = m.groups() + if shiny == 'TRUE': + pokemon.attributes['Shiny'] = 'Yes' + elif shiny == 'FALSE': + pokemon.attributes['Shiny'] = 'No' + else: + raise Exception(f"unknown isShiny: '{shiny}'") + elif m := ivs_definition.search(line): + [hp, attack, defense, speed, special_attack, special_defense] = [stat.strip() for stat in m.groups()] + stats = {"HP": hp, "Atk": attack, "Def": defense, "SpA": special_attack, "SpD": special_defense, "Spe": speed} + pokemon.attributes['IVs'] = ' / '.join(f"{value} {key}" for key, value in stats.items()) + elif m := evs_definition.search(line): + [hp, attack, defense, speed, special_attack, special_defense] = [stat.strip() for stat in m.groups()] + stats = {"HP": hp, "Atk": attack, "Def": defense, "SpA": special_attack, "SpD": special_defense, "Spe": speed} + pokemon.attributes['EVs'] = ' / '.join(f"{value} {key}" for key, value in stats.items() if value != '0') + elif m := moves_definition.search(line): + [moves_] = m.groups() + pokemon.moves = [move.replace("_", " ").title() for move in move_definition.findall(moves_) if move != "NONE"] + elif m := nature_definition.search(line): + [nature_] = m.groups() + pokemon.nature = nature_.replace("_", " ").title() + elif is_blank.search(line): + pass + else: + raise Exception(f"could not parse '{line.strip()}'") + except Exception as e: + print(f"{in_path}:{line_no}: {e}") + return parties + +is_trainer_skip = re.compile(r'(const struct Trainer gBattlePartners\[\] = \{)|(^ \{$)|(\.partySize =)|(\.party = NULL)|(\.mugshotEnabled = TRUE)|(\};)') + +begin_trainer_definition = re.compile(r' \[(PARTNER_\w+)\] =') +end_trainer_definition = re.compile(r' }') +trainer_class_definition = re.compile(r'\.trainerClass = TRAINER_CLASS_(\w+)') +encounter_music_gender_definition = re.compile(r'\.encounterMusic_gender = (F_TRAINER_FEMALE \| )?TRAINER_ENCOUNTER_MUSIC_(\w+)') +trainer_pic_definition = re.compile(r'\.trainerPic = TRAINER_BACK_PIC_(\w+)') +trainer_name_definition = re.compile(r'\.trainerName = _\("([^"]*)"\)') +trainer_items_definition = re.compile(r'\.items = \{([^}]*)\}') +trainer_item_definition = re.compile(r'ITEM_(\w+)') +trainer_ai_flags_definition = re.compile(r'\.aiFlags = (.*)') +trainer_ai_flag_definition = re.compile(r'AI_FLAG_(\w+)') +trainer_party_definition = re.compile(r'\.party = TRAINER_PARTY\((\w+)\)') +trainer_mugshot_definition = re.compile(r'\.mugshotColor = MUGSHOT_COLOR_(\w+)') +trainer_starting_status_definition = re.compile(r'\.startingStatus = STARTING_STATUS_(\w+)') + +class_fixups = { + "Rs": "RS", +} + +pic_fixups = { + "Rs": "RS", +} + +class Trainer: + def __init__(self, id_): + self.id = id_ + self.class_ = None + self.encounter_music = None + self.gender = None + self.pic = None + self.name = None + self.items = [] + self.ai_flags = None + self.mugshot = None + self.starting_status = None + self.party = None + +def convert_trainers(in_path, in_h, parties, out_party): + newlines = 0 + trainer = None + for line_no, line in enumerate(in_h, 1): + try: + line = line[:-1] + if m := begin_trainer_definition.search(line): + if trainer: + raise Exception(f"unexpected start of trainer") + [id_] = m.groups() + trainer = Trainer(id_) + elif m := trainer_class_definition.search(line): + [class_] = m.groups() + class_ = class_.replace("_", " ").title() + for match, replacement in class_fixups.items(): + class_ = class_.replace(match, replacement) + trainer.class_ = class_ + elif m := encounter_music_gender_definition.search(line): + [is_female, music] = m.groups() + trainer.gender = 'Female' if is_female else 'Male' + trainer.encounter_music = music.replace("_", " ").title() + elif m := trainer_pic_definition.search(line): + [pic] = m.groups() + pic = pic.replace("_", " ").title() + for match, replacement in pic_fixups.items(): + pic = pic.replace(match, replacement) + trainer.pic = pic + elif m := trainer_name_definition.search(line): + [name] = m.groups() + trainer.name = name + elif m := trainer_items_definition.search(line): + [items] = m.groups() + trainer.items = " / ".join(item.replace("_", " ").title() for item in trainer_item_definition.findall(items) if item != "NONE") + elif m := trainer_ai_flags_definition.search(line): + [ai_flags] = m.groups() + trainer.ai_flags = " / ".join(ai_flag.replace("_", " ").title() for ai_flag in trainer_ai_flag_definition.findall(ai_flags)) + elif m := trainer_mugshot_definition.search(line): + [color] = m.groups() + trainer.mugshot = color.title() + elif m := trainer_starting_status_definition.search(line): + [starting_status] = m.groups() + trainer.starting_status = starting_status.replace("_", " ").title() + elif m := trainer_party_definition.search(line): + [party] = m.groups() + trainer.party = parties[party] + elif end_trainer_definition.search(line): + if not trainer: + raise Exception(f"unexpected end of trainer") + while newlines > 0: + out_party.write(f"\n") + newlines -= 1 + newlines = 1 + out_party.write(f"=== {trainer.id} ===\n") + out_party.write(f"Name: {trainer.name}\n") + out_party.write(f"Class: {trainer.class_}\n") + out_party.write(f"Pic: {trainer.pic}\n") + out_party.write(f"Gender: {trainer.gender}\n") + out_party.write(f"Music: {trainer.encounter_music}\n") + if trainer.items: + out_party.write(f"Items: {trainer.items}\n") + if trainer.ai_flags: + out_party.write(f"AI: {trainer.ai_flags}\n") + if trainer.mugshot: + out_party.write(f"Mugshot: {trainer.mugshot}\n") + if trainer.starting_status: + out_party.write(f"Starting Status: {trainer.starting_status}\n") + if trainer.party: + for i, pokemon in enumerate(trainer.party): + out_party.write(f"\n") + if pokemon.nickname: + out_party.write(f"{pokemon.nickname} ({pokemon.species})") + else: + out_party.write(f"{pokemon.species}") + if pokemon.gender: + out_party.write(f" ({pokemon.gender})") + if pokemon.item and pokemon.item != 'None': + out_party.write(f" @ {pokemon.item}") + out_party.write(f"\n") + if pokemon.nature: + out_party.write(f"{pokemon.nature} Nature\n") + for key in pokemon_attribute_order: + if key in pokemon.attributes: + out_party.write(f"{key}: {pokemon.attributes[key]}\n") + for move in pokemon.moves: + out_party.write(f"- {move}\n") + trainer = None + elif is_blank.search(line) or is_trainer_skip.search(line): + pass + else: + raise Exception(f"could not parse '{line.strip()}'") + except Exception as e: + print(f"{in_path}:{line_no}: {e}") + +if __name__ == '__main__': + try: + [argv0, trainers_in_path, parties_in_path, out_path] = sys.argv + except: + print(f"usage: python3 {sys.argv[0]} ") + else: + with open(trainers_in_path, "r") as trainers_in_h, open(parties_in_path, "r") as parties_in_h, open(out_path, "w") as out_party: + parties = convert_parties(parties_in_path, parties_in_h) + trainers = convert_trainers(trainers_in_path, trainers_in_h, parties, out_party) diff --git a/migration_scripts/convert_parties.py b/migration_scripts/convert_trainer_parties.py similarity index 100% rename from migration_scripts/convert_parties.py rename to migration_scripts/convert_trainer_parties.py diff --git a/src/battle_tower.c b/src/battle_tower.c index eea9473778..59e9a8aeaf 100644 --- a/src/battle_tower.c +++ b/src/battle_tower.c @@ -699,7 +699,10 @@ static const u8 *const *const sPartnerApprenticeTextTables[NUM_APPRENTICES] = #include "data/battle_frontier/battle_tent.h" #include "data/partner_parties.h" +const struct Trainer gBattlePartners[] = +{ #include "data/battle_partners.h" +}; static void (* const sBattleTowerFuncs[])(void) = { diff --git a/src/data/battle_partners.h b/src/data/battle_partners.h index 39bb91132f..ae0b753cb3 100644 --- a/src/data/battle_partners.h +++ b/src/data/battle_partners.h @@ -1,20 +1,105 @@ -const struct Trainer gBattlePartners[] = { +// +// DO NOT MODIFY THIS FILE! It is auto-generated from src/data/battle_partners.party +// +// If you want to modify this file set COMPETITIVE_PARTY_SYNTAX to FALSE +// in include/config.h and remove this notice. +// Use sed -i '/^#line/d' 'src/data/battle_partners.h' to remove #line markers. +// + +#line 1 "src/data/battle_partners.party" + +#line 1 [PARTNER_NONE] = { - .party = NULL, +#line 3 .trainerClass = TRAINER_CLASS_PKMN_TRAINER_1, - .encounterMusic_gender = TRAINER_ENCOUNTER_MUSIC_MALE, - .trainerPic = TRAINER_PIC_HIKER, - .trainerName = _(""), - .items = {}, +#line 4 + .trainerPic = TRAINER_BACK_PIC_BRENDAN, + .encounterMusic_gender = +#line 6 + TRAINER_ENCOUNTER_MUSIC_MALE, + .partySize = 0, + .party = (const struct TrainerMon[]) + { + }, }, - +#line 8 [PARTNER_STEVEN] = { - .party = TRAINER_PARTY(sParty_StevenPartner), - .trainerClass = TRAINER_CLASS_RIVAL, - .encounterMusic_gender = TRAINER_ENCOUNTER_MUSIC_MALE, - .trainerPic = TRAINER_BACK_PIC_STEVEN, +#line 9 .trainerName = _("STEVEN"), +#line 10 + .trainerClass = TRAINER_CLASS_RIVAL, +#line 11 + .trainerPic = TRAINER_BACK_PIC_STEVEN, + .encounterMusic_gender = +#line 13 + TRAINER_ENCOUNTER_MUSIC_MALE, + .partySize = 3, + .party = (const struct TrainerMon[]) + { + { +#line 15 + .species = SPECIES_METANG, + .gender = TRAINER_MON_RANDOM_GENDER, +#line 19 + .ev = TRAINER_PARTY_EVS(0, 252, 252, 0, 6, 0), +#line 18 + .iv = TRAINER_PARTY_IVS(31, 31, 31, 31, 31, 31), +#line 17 + .lvl = 42, +#line 16 + .nature = NATURE_BRAVE, + .dynamaxLevel = MAX_DYNAMAX_LEVEL, + .moves = { +#line 20 + MOVE_LIGHT_SCREEN, + MOVE_PSYCHIC, + MOVE_REFLECT, + MOVE_METAL_CLAW, + }, + }, + { +#line 25 + .species = SPECIES_SKARMORY, + .gender = TRAINER_MON_RANDOM_GENDER, +#line 29 + .ev = TRAINER_PARTY_EVS(252, 0, 0, 0, 6, 252), +#line 28 + .iv = TRAINER_PARTY_IVS(31, 31, 31, 31, 31, 31), +#line 27 + .lvl = 43, +#line 26 + .nature = NATURE_IMPISH, + .dynamaxLevel = MAX_DYNAMAX_LEVEL, + .moves = { +#line 30 + MOVE_TOXIC, + MOVE_AERIAL_ACE, + MOVE_PROTECT, + MOVE_STEEL_WING, + }, + }, + { +#line 35 + .species = SPECIES_AGGRON, + .gender = TRAINER_MON_RANDOM_GENDER, +#line 39 + .ev = TRAINER_PARTY_EVS(0, 252, 0, 0, 252, 6), +#line 38 + .iv = TRAINER_PARTY_IVS(31, 31, 31, 31, 31, 31), +#line 37 + .lvl = 44, +#line 36 + .nature = NATURE_ADAMANT, + .dynamaxLevel = MAX_DYNAMAX_LEVEL, + .moves = { +#line 40 + MOVE_THUNDER, + MOVE_PROTECT, + MOVE_SOLAR_BEAM, + MOVE_DRAGON_CLAW, + }, + }, + }, }, -}; diff --git a/src/data/battle_partners.party b/src/data/battle_partners.party new file mode 100644 index 0000000000..e1ecfe35fa --- /dev/null +++ b/src/data/battle_partners.party @@ -0,0 +1,43 @@ +=== PARTNER_NONE === +Name: +Class: Pkmn Trainer 1 +Pic: Brendan +Gender: Male +Music: Male + +=== PARTNER_STEVEN === +Name: STEVEN +Class: Rival +Pic: Steven +Gender: Male +Music: Male + +Metang +Brave Nature +Level: 42 +IVs: 31 HP / 31 Atk / 31 Def / 31 SpA / 31 SpD / 31 Spe +EVs: 252 Atk / 252 Def / 6 SpA +- Light Screen +- Psychic +- Reflect +- Metal Claw + +Skarmory +Impish Nature +Level: 43 +IVs: 31 HP / 31 Atk / 31 Def / 31 SpA / 31 SpD / 31 Spe +EVs: 252 HP / 6 SpA / 252 SpD +- Toxic +- Aerial Ace +- Protect +- Steel Wing + +Aggron +Adamant Nature +Level: 44 +IVs: 31 HP / 31 Atk / 31 Def / 31 SpA / 31 SpD / 31 Spe +EVs: 252 Atk / 252 SpA / 6 SpD +- Thunder +- Protect +- Solar Beam +- Dragon Claw diff --git a/src/data/partner_parties.h b/src/data/partner_parties.h index 1b071ec28e..8b13789179 100644 --- a/src/data/partner_parties.h +++ b/src/data/partner_parties.h @@ -1,26 +1 @@ -static const struct TrainerMon sParty_StevenPartner[] = { - { - .species = SPECIES_METANG, - .lvl = 42, - .nature = NATURE_BRAVE, - .iv = TRAINER_PARTY_IVS(31, 31, 31, 31, 31, 31), - .ev = TRAINER_PARTY_EVS(0, 252, 252, 0, 6, 0), - .moves = {MOVE_LIGHT_SCREEN, MOVE_PSYCHIC, MOVE_REFLECT, MOVE_METAL_CLAW}, - }, - { - .species = SPECIES_SKARMORY, - .lvl = 43, - .nature = NATURE_IMPISH, - .iv = TRAINER_PARTY_IVS(31, 31, 31, 31, 31, 31), - .ev = TRAINER_PARTY_EVS(252, 0, 0, 0, 6, 252), - .moves = {MOVE_TOXIC, MOVE_AERIAL_ACE, MOVE_PROTECT, MOVE_STEEL_WING}, - }, - { - .species = SPECIES_AGGRON, - .lvl = 44, - .nature = NATURE_ADAMANT, - .iv = TRAINER_PARTY_IVS(31, 31, 31, 31, 31, 31), - .ev = TRAINER_PARTY_EVS(0, 252, 0, 0, 252, 6), - .moves = {MOVE_THUNDER, MOVE_PROTECT, MOVE_SOLAR_BEAM, MOVE_DRAGON_CLAW}, - } -}; + diff --git a/tools/trainerproc/main.c b/tools/trainerproc/main.c index 5b0fbfc3f4..51a2df8968 100644 --- a/tools/trainerproc/main.c +++ b/tools/trainerproc/main.c @@ -143,6 +143,12 @@ static bool is_literal_string(struct String s1, const char *s2) } } +static bool starts_with(struct String s, const char *prefix) +{ + int n = strlen(prefix); + return strncmp((const char *)s.string, prefix, n) == 0; +} + static bool ends_with(struct String s, const char *suffix) { int n = strlen(suffix); @@ -1194,7 +1200,7 @@ static bool parse_trainer(struct Parser *p, const struct Parsed *parsed, struct while (match_empty_line(p)) {} if (!parse_pokemon_header(p, &nickname, &species, &gender, &item)) { - if (i > 0 || is_literal_string(trainer->id, "TRAINER_NONE")) + if (i > 0 || ends_with(trainer->id, "_NONE")) break; if (!p->error) set_parse_error(p, p->location, "expected nickname or species"); @@ -1614,7 +1620,10 @@ static void fprint_trainers(const char *output_path, FILE *f, struct Parsed *par { fprintf(f, "#line %d\n", trainer->pic_line); fprintf(f, " .trainerPic = "); - fprint_constant(f, "TRAINER_PIC", trainer->pic); + if (starts_with(trainer->id, "PARTNER_")) + fprint_constant(f, "TRAINER_BACK_PIC", trainer->pic); + else + fprint_constant(f, "TRAINER_PIC", trainer->pic); fprintf(f, ",\n"); }