Skip to content

Commit

Permalink
trainerproc: Competitive-formatted trainers and teams
Browse files Browse the repository at this point in the history
$ python3 migration_scripts/convert_parties.py src/data/trainers.h src/data/trainer_parties.h src/data/npc_trainers.party
Is available to convert Trainer Control-formatted trainers/parties into
Competitive-formatted ones.

Multiple '#include's can be placed in the trainer section of src/data.c
to support spreading the trainers across multiple .party files.

trainerproc does not interpret the values, leaving that job to the C
compiler, so we use '#line' to associate those errors with the lines in
the .party file(s). Because the columns don't make sense we use
-fno-show-column and -fno-diagostics-show-caret. We might want to move
gTrainers into its own file so that the rest of src/data.c isn't
affected by those flags.

Extensions (misfeatures, imo):
- .party files are passed through cpp, so '#define's are supported, and so
  are '// ...' and '/* ... */' comments.
- .party files also support writing, e.g. 'SPECIES_PIKACHU' instead of
  'Pikachu'. This allows people to write constants explicitly if they
  like.

Pragmas:
- '#pragma trainerproc ivs explicit' requires an explicit 'IVs:' line
  rather than defaulting to 31s.
- '#pragma trainerproc ivs <IVs>' changes the default IVs.
- '#pragma trainerproc level explicit' requires an explicit 'Level:'
  line rather than defaulting to 100.
- '#pragma trainerproc level <level>' changes the default level.
  • Loading branch information
mrgriffin committed Mar 29, 2024
1 parent 5a986c6 commit 7daed01
Show file tree
Hide file tree
Showing 15 changed files with 57,584 additions and 18,380 deletions.
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,12 @@ JSONPROC := tools/jsonproc/jsonproc$(EXE)
PATCHELF := tools/patchelf/patchelf$(EXE)
ROMTEST ?= $(shell { command -v mgba-rom-test || command -v tools/mgba/mgba-rom-test$(EXE); } 2>/dev/null)
ROMTESTHYDRA := tools/mgba-rom-test-hydra/mgba-rom-test-hydra$(EXE)
TRAINERPROC := tools/trainerproc/trainerproc$(EXE)

PERL := perl

# Inclusive list. If you don't want a tool to be built, don't add it here.
TOOLDIRS := tools/aif2pcm tools/bin2c tools/gbafix tools/gbagfx tools/jsonproc tools/mapjson tools/mid2agb tools/preproc tools/ramscrgen tools/rsfont tools/scaninc
TOOLDIRS := tools/aif2pcm tools/bin2c tools/gbafix tools/gbagfx tools/jsonproc tools/mapjson tools/mid2agb tools/preproc tools/ramscrgen tools/rsfont tools/scaninc tools/trainerproc
CHECKTOOLDIRS = tools/patchelf tools/mgba-rom-test-hydra
TOOLBASE = $(TOOLDIRS:tools/%=%)
TOOLS = $(foreach tool,$(TOOLBASE),tools/$(tool)/$(tool)$(EXE))
Expand Down Expand Up @@ -335,6 +336,10 @@ $(CRY_SUBDIR)/uncomp_%.bin: $(CRY_SUBDIR)/uncomp_%.aif ; $(AIF) $< $@
$(CRY_SUBDIR)/%.bin: $(CRY_SUBDIR)/%.aif ; $(AIF) $< $@ --compress
sound/%.bin: sound/%.aif ; $(AIF) $< $@

COMPETITIVE_PARTY_SYNTAX := $(shell echo 'COMPETITIVE_PARTY_SYNTAX' | $(CPP) $(CPPFLAGS) -imacros include/global.h | tail -n1)
ifeq ($(COMPETITIVE_PARTY_SYNTAX),1)
%.h: %.party tools ; $(CPP) $(CPPFLAGS) - < $< | sed '/#[^p]/d' | $(TRAINERPROC) -o $@ -i $< -
endif

ifeq ($(MODERN),0)
$(C_BUILDDIR)/libc.o: CC1 := tools/agbcc/bin/old_agbcc$(EXE)
Expand All @@ -354,6 +359,8 @@ $(C_BUILDDIR)/librfu_intr.o: CFLAGS := -O2 -mthumb-interwork -quiet
else
$(C_BUILDDIR)/librfu_intr.o: CFLAGS := -mthumb-interwork -O2 -mabi=apcs-gnu -mtune=arm7tdmi -march=armv4t -fno-toplevel-reorder -Wno-pointer-to-int-cast
$(C_BUILDDIR)/pokedex_plus_hgss.o: CFLAGS := -mthumb -mthumb-interwork -O2 -mabi=apcs-gnu -mtune=arm7tdmi -march=armv4t -Wno-pointer-to-int-cast -std=gnu17 -Werror -Wall -Wno-strict-aliasing -Wno-attribute-alias -Woverride-init
# Annoyingly we can't turn this on just for src/data/trainers.h
$(C_BUILDDIR)/data.o: CFLAGS += -fno-show-column -fno-diagnostics-show-caret
endif

ifeq ($(DINFO),1)
Expand Down
1 change: 1 addition & 0 deletions include/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
#define POKEDEX_PLUS_HGSS FALSE // If TRUE, enables the custom HGSS style Pokedex.
#define SUMMARY_SCREEN_NATURE_COLORS TRUE // If TRUE, nature-based stat boosts and reductions will be red and blue in the summary screen.
#define HQ_RANDOM TRUE // If TRUE, replaces the default RNG with an implementation of SFC32 RNG. May break code that relies on RNG.
#define COMPETITIVE_PARTY_SYNTAX TRUE // If TRUE, parties are defined in "competitive syntax".

// Measurement system constants to be used for UNITS
#define UNITS_IMPERIAL 0 // Inches, feet, pounds
Expand Down
5 changes: 3 additions & 2 deletions include/constants/trainers.h
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,8 @@
#define F_TRAINER_FEMALE (1 << 7)

// Trainer party defines
#define TRAINER_MON_MALE 1
#define TRAINER_MON_FEMALE 2
#define TRAINER_MON_MALE 1
#define TRAINER_MON_FEMALE 2
#define TRAINER_MON_RANDOM_GENDER 3

#endif // GUARD_TRAINERS_H
301 changes: 301 additions & 0 deletions migration_scripts/convert_parties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
# 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+)')

species_replacements = {
"HO_OH": "Ho-Oh",
"PORYGON_Z": "Porygon-Z",
"TYPE_NULL": "Type: Null",
"JANGMO_O": "Jangmo-o",
"HAKAMO_O": "Hakamo-o",
"KOMMO_O": "Kommo-o",
"WO_CHIEN": "Wo-Chien",
"CHIEN_PAO": "Chien-Pao",
"TING_LU": "Ting-Lu",
"CHI_YU": "Chi-Yu",
"_ALOLAN": "-Alola",
"_GALARIAN": "-Galar",
"_HISUIAN": "-Hisui",
}

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 gTrainers\[\] = \{)|(^ \{$)|(\.partySize =)|(\.party = NULL)|(\.mugshotEnabled = TRUE)|(\};)')

begin_trainer_definition = re.compile(r' \[(TRAINER_\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_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_double_battle_definition = re.compile(r'\.doubleBattle = (\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.double_battle = None
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_double_battle_definition.search(line):
[double_battle] = m.groups()
if double_battle == 'TRUE':
trainer.double_battle = "Yes"
elif double_battle == 'FALSE':
trainer.double_battle = "No"
else:
raise Exception(f"unknown doubleBattle: '{double_battle}'")
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")
out_party.write(f"Double Battle: {trainer.double_battle}\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]} <trainers.h> <trainer_parties.h> <out>")
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)
6 changes: 5 additions & 1 deletion src/battle_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -2180,7 +2180,9 @@ void ModifyPersonalityForNature(u32 *personality, u32 newNature)
u32 GeneratePersonalityForGender(u32 gender, u32 species)
{
const struct SpeciesInfo *speciesInfo = &gSpeciesInfo[species];
if (gender == MON_MALE)
if (gender == MON_GENDERLESS)
return 0;
else if (gender == MON_MALE)
return ((255 - speciesInfo->genderRatio) / 2) + speciesInfo->genderRatio;
else
return speciesInfo->genderRatio / 2;
Expand Down Expand Up @@ -2254,6 +2256,8 @@ u8 CreateNPCTrainerPartyFromTrainer(struct Pokemon *party, const struct Trainer
personalityValue = (personalityValue & 0xFFFFFF00) | GeneratePersonalityForGender(MON_MALE, partyData[i].species);
else if (partyData[i].gender == TRAINER_MON_FEMALE)
personalityValue = (personalityValue & 0xFFFFFF00) | GeneratePersonalityForGender(MON_FEMALE, partyData[i].species);
else if (partyData[i].gender == TRAINER_MON_RANDOM_GENDER)
personalityValue = (personalityValue & 0xFFFFFF00) | GeneratePersonalityForGender(Random() & 1 ? MON_MALE : MON_FEMALE, partyData[i].species);
ModifyPersonalityForNature(&personalityValue, partyData[i].nature);
if (partyData[i].isShiny)
{
Expand Down
4 changes: 4 additions & 0 deletions src/data.c
Original file line number Diff line number Diff line change
Expand Up @@ -226,4 +226,8 @@ const union AnimCmd *const sAnims_Trainer[] ={
};

#include "data/trainer_parties.h"

const struct Trainer gTrainers[] =
{
#include "data/trainers.h"
};
Loading

0 comments on commit 7daed01

Please sign in to comment.