/* trainerprpc * Parses teams in the competitive format (as used by Showdown) and * converts them into the internal team format. */ /* To add a new member to 'TrainerMon': * 1. Add the same member to 'Pokemon'. * 2. Parse that member in 'parse_trainer', probably in the 'parse_attribute' loop. * 3. Format that member in 'fprint_trainers'. */ #include #include #include #include #include #include #include #define MAX_TRAINER_AI_FLAGS 32 #define MAX_TRAINER_ITEMS 4 #define PARTY_SIZE 6 #define MAX_MON_MOVES 4 struct String { const unsigned char *string; int string_n; }; struct Stats { int hp, attack, defense, special_attack, special_defense, speed; }; enum Gender { GENDER_ANY, GENDER_MALE, GENDER_FEMALE, }; // TODO: Support Hidden Power. struct Pokemon { struct String nickname; struct String species; enum Gender gender; struct String item; int header_line; struct Stats evs; int evs_line; struct Stats ivs; int ivs_line; struct String ability; int ability_line; int level; int level_line; struct String ball; int ball_line; int friendship; int friendship_line; struct String nature; int nature_line; bool shiny; int shiny_line; int dynamax_level; int dynamax_level_line; bool gigantamax_factor; bool gigantamax_factor_line; struct String tera_type; int tera_type_line; struct String moves[MAX_MON_MOVES]; int moves_n; int move1_line; }; struct Trainer { struct String id; int id_line; struct String ai_flags[MAX_TRAINER_AI_FLAGS]; int ai_flags_n; int ai_flags_line; struct String items[MAX_TRAINER_ITEMS]; int items_n; int items_line; struct String class; int class_line; struct String encounter_music; int encounter_music_line; enum Gender gender; int gender_line; struct String pic; int pic_line; struct String name; int name_line; bool double_battle; int double_battle_line; struct Pokemon pokemon[PARTY_SIZE]; int pokemon_n; struct String mugshot; int mugshot_line; struct String starting_status; int starting_status_line; }; static bool is_empty_string(struct String s) { return s.string_n == 0; } static bool is_literal_string(struct String s1, const char *s2) { for (int i = 0;; i++) { if (i == s1.string_n && s2[i] == '\0') return true; else if (i == s1.string_n || s2[i] == '\0') return false; else if (s1.string[i] != s2[i]) return false; } } 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); if (n > s.string_n) { return false; } else { for (int i = 0; i < n; i++) { if (s.string[s.string_n - i - 1] != suffix[n - i - 1]) return false; } return true; } } static struct String literal_string(const char *s) { return (struct String) { .string = (const unsigned char *)s, .string_n = strlen(s), }; } struct Source { const char *path; const unsigned char *buffer; int buffer_n; }; struct SourceLocation { int line; int column; }; struct Token { const struct Source *source; struct SourceLocation location; int begin; int end; }; static bool is_empty_token(const struct Token *t) { return t->begin == t->end; } // TODO: Make case-insensitive? static bool is_literal_token(const struct Token *t, const char *s) { int i = t->begin; for (;;) { if (*s == '\0' && i == t->end) return true; else if (*s == '\0' || i == t->end) return false; else if (*s != t->source->buffer[i]) return false; i++; s++; } } struct Parser { struct Source *source; struct SourceLocation location; int offset; struct SourceLocation error_location; const char *error; bool fatal_error; }; struct Parsed { const struct Source *source; struct Trainer *trainers; int trainers_n; struct Stats default_ivs; int default_ivs_line; bool default_ivs_off; int default_level; int default_level_line; bool default_level_off; }; static bool set_parse_error(struct Parser *p, struct SourceLocation location, const char *error) { p->error = error; p->error_location = location; return false; } __attribute__((warn_unused_result)) static bool peek_char(struct Parser *p, unsigned char *c) { assert(p && c); if (p->offset == p->source->buffer_n) return false; *c = p->source->buffer[p->offset]; return true; } __attribute__((warn_unused_result)) static bool pop_char(struct Parser *p, unsigned char *c) { assert(p && c); if (p->offset == p->source->buffer_n) return false; *c = p->source->buffer[p->offset++]; if (*c == '\n') { p->location.line++; p->location.column = 1; } else { p->location.column++; } return true; } static void skip_whitespace(struct Parser *p) { assert(p); unsigned char c; for (;;) { if (!peek_char(p, &c)) break; if (c != ' ' && c != '\t') break; if (!pop_char(p, &c)) assert(false); } } static void skip_line(struct Parser *p) { assert(p); unsigned char c; for (;;) { if (!pop_char(p, &c)) break; if (c == '\n') break; } } __attribute__((warn_unused_result)) static bool match_eof(struct Parser *p) { return p->offset == p->source->buffer_n; } __attribute__((warn_unused_result)) static bool match_exact(struct Parser *p, const char *s) { assert(p && s); struct Parser p_ = *p; unsigned char c; for (; *s != '\0'; s++) { if (!pop_char(&p_, &c)) return false; if (*s != c) return false; } *p = p_; return true; } static void match_until_eol(struct Parser *p, struct Token *t) { assert(p && t); unsigned char c; skip_whitespace(p); t->source = p->source; t->location = p->location; t->begin = p->offset; t->end = p->offset; for (;;) { if (!peek_char(p, &c)) break; if (c == '\n') break; if (!pop_char(p, &c)) assert(false); if (c != ' ' && c != '\t') t->end = p->offset; } } __attribute__((warn_unused_result)) static bool match_eol(struct Parser *p) { struct Parser p_ = *p; unsigned char c; skip_whitespace(&p_); for (;;) { if (!pop_char(&p_, &c)) return false; if (c == '\n') break; else return false; } *p = p_; return true; } __attribute__((warn_unused_result)) static bool match_int(struct Parser *p, int *i) { assert(p && i); struct Parser p_ = *p; unsigned char c; *i = 0; for (;;) { if (!peek_char(&p_, &c)) break; if (!('0' <= c && c <= '9')) break; *i = *i * 10 + (c - '0'); if (!pop_char(&p_, &c)) assert(false); } if (p->offset == p_.offset) return false; *p = p_; return true; } __attribute__((warn_unused_result)) static bool match_empty_line(struct Parser *p) { struct Parser p_ = *p; if (match_exact(&p_, "# ")) { int line; if (match_int(&p_, &line)) { struct Token t; match_until_eol(&p_, &t); p_.location.line = line - 1; *p = p_; } } return match_eol(p); } __attribute__((warn_unused_result)) static bool match_identifier(struct Parser *p, struct Token *t) { assert(p && t); struct Parser p_ = *p; unsigned char c; t->source = p->source; t->location = p->location; t->begin = p->offset; for (;;) { if (!peek_char(&p_, &c)) break; if (('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || ('0' <= c && c <= '9') || c == '_') ; else break; if (!pop_char(&p_, &c)) assert(false); } t->end = p_.offset; if (t->begin == t->end) return false; *p = p_; return true; } __attribute__((warn_unused_result)) static bool match_human_identifier(struct Parser *p, struct Token *t) { assert(p && t); struct Parser p_ = *p, q = *p; unsigned char c; t->source = p->source; t->location = p->location; t->begin = p->offset; t->end = p->offset; for (;;) { if (!peek_char(&p_, &c)) break; if (('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || c == '_' || c == '-' || c == ' ') ; else if (p_.offset > t->begin && ('0' <= c && c <= '9')) ; else break; if (!pop_char(&p_, &c)) assert(false); if (c != ' ') q = p_; } t->end = q.offset; if (t->begin == t->end) return false; *p = q; return true; } // Like 'match_human_identifier' but parses ':', for 'Type: Null', among // other exceptions. __attribute__((warn_unused_result)) static bool match_species_identifier(struct Parser *p, struct Token *t) { assert(p && t); struct Parser p_ = *p, q = *p; unsigned char c; t->source = p->source; t->location = p->location; t->begin = p->offset; t->end = p->offset; for (;;) { if (!peek_char(&p_, &c)) break; if (('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || ('0' <= c && c <= '9') || c == '_' || c == '-' || c == ' ' || c == ':' || c == '.' || c == '\'' || c >= 0x80) ; else break; if (!pop_char(&p_, &c)) assert(false); if (c != ' ') q = p_; } t->end = q.offset; if (t->begin == t->end) return false; *p = q; return true; } // Like 'match_human_identifier' but parses '10,000,000 Volt Thunderbolt' // among other exceptions. __attribute__((warn_unused_result)) static bool match_move_identifier(struct Parser *p, struct Token *t) { assert(p && t); struct Parser p_ = *p, q = *p; unsigned char c; t->source = p->source; t->location = p->location; t->begin = p->offset; t->end = p->offset; for (;;) { if (!peek_char(&p_, &c)) break; if (('A' <= c && c <= 'Z') || ('a' <= c && c <= 'z') || ('0' <= c && c <= '9') || c == '_' || c == '-' || c == ' ' || c == ',') ; else break; if (!pop_char(&p_, &c)) assert(false); if (c != ' ') q = p_; } t->end = q.offset; if (t->begin == t->end) return false; *p = q; return true; } static bool show_parse_error(struct Parser *p) { // Print error message. int n = fprintf(stderr, "%s:%d: ", p->source->path, p->error_location.line); fprintf(stderr, "error: %s\n", p->error); struct Parser p_ = { .source = p->source, .location = { .line = 1, .column = 1 }, .offset = 0, }; for (;;) { if (p->error_location.line == p_.location.line) break; if (!match_empty_line(&p_)) skip_line(&p_); if (match_eof(&p_)) assert(false); } int begin = p_.offset; int end; for (end = begin; end < p->source->buffer_n; end++) { if (p->source->buffer[end] == '\n') break; } // Print the source line. fprintf(stderr, "%s:%d: %.*s\n", p->source->path, p->error_location.line, end - begin, &p->source->buffer[begin]); // Print caret pointing at the column. fprintf(stderr, "%*s", n, ""); for (int column = 1; column < p->error_location.column && begin + column < end; column++) { unsigned char c = p->source->buffer[begin + column]; fputc(c == '\t' ? c : ' ', stderr); } fprintf(stderr, "^\n"); p->error = NULL; p->fatal_error = true; return false; } static bool set_show_parse_error(struct Parser *p, struct SourceLocation location, const char *error) { set_parse_error(p, location, error); return show_parse_error(p); } __attribute__((warn_unused_result)) static bool parse_section(struct Parser *p, struct Token *section) { assert(p && section); struct Parser p_ = *p; if (!match_exact(&p_, "===")) return false; skip_whitespace(&p_); if (!match_identifier(&p_, section)) return set_parse_error(p, p_.location, "expected identifier"); skip_whitespace(&p_); if (!match_exact(&p_, "===")) return set_parse_error(p, p_.location, "expected '==='"); skip_whitespace(&p_); if (!match_eol(&p_)) return set_parse_error(p, p_.location, "unexpected character in section"); *p = p_; return true; } __attribute__((warn_unused_result)) static bool parse_pokemon_header(struct Parser *p, struct Token *nickname, struct Token *species, struct Token *gender, struct Token *item) { assert(p && nickname && species && gender && item); struct Parser p_ = *p; struct Token first = {}, second = {}, third = {}; if (!match_species_identifier(&p_, &first)) return false; skip_whitespace(&p_); if (match_exact(&p_, "(")) { if (!match_species_identifier(&p_, &second)) return set_parse_error(p, p_.location, "expected species or gender"); if (!match_exact(&p_, ")")) return set_parse_error(p, p_.location, "expected ')'"); skip_whitespace(&p_); if (match_exact(&p_, "(")) { if (!match_human_identifier(&p_, &third)) return set_parse_error(p, p_.location, "expected gender"); if (!match_exact(&p_, ")")) return set_parse_error(p, p_.location, "expected ')'"); *nickname = first; *species = second; *gender = third; } else { if (second.begin + 1 == second.end) // Single letter. { *nickname = (struct Token) {}; *species = first; *gender = second; } else { *nickname = first; *species = second; *gender = (struct Token) {}; } } } else { *nickname = (struct Token) {}; *species = first; *gender = (struct Token) {}; } skip_whitespace(&p_); if (match_exact(&p_, "@")) { skip_whitespace(&p_); if (!match_human_identifier(&p_, item)) return set_parse_error(p, p_.location, "expected item"); } else { *item = (struct Token) {}; } skip_whitespace(&p_); if (!match_eol(&p_)) return set_parse_error(p, p_.location, "unexpected character in Pokemon header"); *p = p_; return true; } __attribute__((warn_unused_result)) static bool parse_pokemon_nature(struct Parser *p, struct Token *nature) { assert(p && nature); struct Parser p_ = *p; if (!match_identifier(&p_, nature)) return false; skip_whitespace(&p_); if (!match_exact(&p_, "Nature")) return false; skip_whitespace(&p_); if (!match_eol(&p_)) return set_parse_error(p, p_.location, "unexpected character in nature"); *p = p_; return true; } __attribute__((warn_unused_result)) static bool parse_attribute(struct Parser *p, struct Token *key, struct Token *value) { assert(p && key && value); if (parse_pokemon_nature(p, value)) { static const struct Source nature_source = { .path=NULL, .buffer=(unsigned char *)"Nature", .buffer_n=6 }; key->source = &nature_source; key->location = p->location; key->begin = 0; key->end = nature_source.buffer_n; return true; } struct Parser p_ = *p; if (!match_human_identifier(&p_, key)) return false; skip_whitespace(&p_); if (match_exact(&p_, ":")) { skip_whitespace(&p_); match_until_eol(&p_, value); if (!match_eol(&p_)) return set_parse_error(p, p_.location, "unexpected character in attribute"); } else { return set_parse_error(p, p_.location, "expected ':' or 'Nature'"); } *p = p_; return true; } __attribute__((warn_unused_result)) static bool parse_pokemon_move(struct Parser *p, struct Token *move) { assert(p && move); struct Parser p_ = *p; if (!match_exact(&p_, "-")) return false; skip_whitespace(&p_); if (!match_move_identifier(&p_, move)) return set_parse_error(p, p_.location, "expected move"); skip_whitespace(&p_); if (!match_eol(&p_)) return set_parse_error(p, p_.location, "unexpected character in move"); *p = p_; return true; } static struct String token_string(const struct Token *t) { if (t->source) { return (struct String) { .string = &t->source->buffer[t->begin], .string_n = t->end - t->begin, }; } else { return (struct String) { .string = NULL, .string_n = 0, }; } } static bool token_gender(struct Parser *p, const struct Token *t, enum Gender *g) { if (is_empty_token(t)) { *g = GENDER_ANY; return true; } else if (is_literal_token(t, "M") || is_literal_token(t, "Male")) { *g = GENDER_MALE; return true; } else if (is_literal_token(t, "F") || is_literal_token(t, "Female")) { *g = GENDER_FEMALE; return true; } else { return set_parse_error(p, t->location, "invalid gender"); } } static bool token_stats(struct Parser *p, const struct Token *t, struct Stats *stats, bool require_all) { struct Source source = { .path = p->source->path, .buffer = p->source->buffer, .buffer_n = t->end, }; struct Parser p_ = { .source = &source, .location = t->location, .offset = t->begin, }; bool has_hp = false, has_atk = false, has_def = false, has_spa = false, has_spd = false, has_spe = false; for (;;) { int value; skip_whitespace(&p_); if (!match_int(&p_, &value)) return set_parse_error(p, p_.location, "expected integer"); struct Token stat; skip_whitespace(&p_); struct SourceLocation stat_location = p_.location; if (!match_identifier(&p_, &stat)) return set_parse_error(p, p_.location, "expected 'HP', 'Atk', 'Def', 'SpA', 'SpD', or 'Spe'"); if (is_literal_token(&stat, "HP")) { stats->hp = value; has_hp = true; } else if (is_literal_token(&stat, "Atk")) { stats->attack = value; has_atk = true; } else if (is_literal_token(&stat, "Def")) { stats->defense = value; has_def = true; } else if (is_literal_token(&stat, "SpA")) { stats->special_attack = value; has_spa = true; } else if (is_literal_token(&stat, "SpD")) { stats->special_defense = value; has_spd = true; } else if (is_literal_token(&stat, "Spe")) { stats->speed = value; has_spe = true; } else { return set_parse_error(p, stat_location, "expected 'HP', 'Atk', 'Def', 'SpA', 'SpD', or 'Spe'"); } skip_whitespace(&p_); if (match_eof(&p_)) break; else if (!match_exact(&p_, "/")) return set_parse_error(p, p_.location, "expected '/' or newline"); } if (require_all && !(has_hp && has_atk && has_def && has_spa && has_spd && has_spe)) return set_parse_error(p, p_.location, "expected all of 'HP', 'Atk', 'Def', 'SpA', 'SpD', and 'Spe'"); return true; } // '/'-separated strings. static bool token_human_identifiers(struct Parser *p, const struct Token *t, struct String *ids, int *ids_n, int max_ids_n) { struct Source source = { .path = p->source->path, .buffer = p->source->buffer, .buffer_n = t->end, }; struct Parser p_ = { .source = &source, .location = t->location, .offset = t->begin, }; int n = 0; while (*ids_n < max_ids_n) { skip_whitespace(&p_); struct Token id; if (!match_human_identifier(&p_, &id)) return set_parse_error(p, p_.location, "expected identifier"); ids[n++] = token_string(&id); skip_whitespace(&p_); if (match_eof(&p_)) break; else if (!match_exact(&p_, "/")) return set_parse_error(p, p_.location, "expected '/' or newline"); } *ids_n = n; return true; } static bool token_int(struct Parser *p, const struct Token *t, int *i) { char *end; long l = strtol((const char *)&t->source->buffer[t->begin], &end, 10); if ((unsigned char *)end != &t->source->buffer[t->end]) return set_parse_error(p, t->location, "invalid integer"); if (!(INT_MIN <= l && l <= INT_MAX)) return set_parse_error(p, t->location, "integer too large"); *i = l; return true; } static bool token_bool(struct Parser *p, const struct Token *t, bool *b) { if (is_literal_token(t, "Yes")) { *b = true; return true; } else if (is_literal_token(t, "No")) { *b = false; return true; } else { return set_parse_error(p, t->location, "invalid boolean"); } } static bool parse_pragma(struct Parser *p, struct Parsed *parsed) { assert(p && parsed); struct Parser p_ = *p; if (!match_exact(&p_, "#pragma")) return false; // TODO: Require _a_ space between #pragma and trainerproc. struct Token id; skip_whitespace(&p_); if (!match_identifier(&p_, &id)) return set_show_parse_error(p, p_.location, "expected 'trainerproc'"); if (!is_literal_token(&id, "trainerproc")) return set_show_parse_error(p, id.location, "expected 'trainerproc'"); skip_whitespace(&p_); if (!match_identifier(&p_, &id)) { return set_show_parse_error(p, p_.location, "expected identifier"); } else if (is_literal_token(&id, "ivs")) { if (parsed->default_ivs_line) return set_show_parse_error(p, p_.location, "duplicate #pragma trainerproc ivs"); parsed->default_ivs_line = p_.location.line; skip_whitespace(&p_); struct Token t; match_until_eol(&p_, &t); if (is_literal_token(&t, "explicit")) parsed->default_ivs_off = true; else if (!token_stats(p, &t, &parsed->default_ivs, true)) return show_parse_error(p); } else if (is_literal_token(&id, "level")) { if (parsed->default_level_line) return set_show_parse_error(p, p_.location, "duplicate #pragma trainerproc level"); parsed->default_level_line = p_.location.line; skip_whitespace(&p_); struct Token t; match_until_eol(&p_, &t); if (is_literal_token(&t, "explicit")) parsed->default_level_off = true; else if (!token_int(p, &t, &parsed->default_level)) return show_parse_error(p); } else { return set_show_parse_error(p, id.location, "unknown pragma"); } if (!match_eol(&p_)) return set_show_parse_error(p, p_.location, "expected newline"); *p = p_; return true; } // Species that are selected based on the explicit gender. // e.g. 'Nidoran (M)' becomes 'Nidoran-M'. static const struct { const char *species; const char *male_species; const char *female_species; } gendered_species[] = { { "Basculegion", "Basculegion-M", "Basculegion-F" }, { "Indeedee", "Indeedee-M", "Indeedee-F" }, { "Oinkologne", "Oinkologne-M", "Oinkologne-F" }, { "Meowstic", "Meowstic-M", "Meowstic-F" }, { "Nidoran", "Nidoran-M", "Nidoran-F" }, { NULL, NULL, NULL } }; // Items that are selected based on the explicit form. // e.g. 'Arceus-Fire' becomes 'Arceus @ Flame Plate'. static const struct { const char *form; const char *species; const char *item; } itemed_species[] = { { "Arceus-Bug", "Arceus", "Insect Plate" }, { "Arceus-Dark", "Arceus", "Dread Plate" }, { "Arceus-Dragon", "Arceus", "Draco Plate" }, { "Arceus-Electric", "Arceus", "Zap Plate" }, { "Arceus-Fairy", "Arceus", "Pixie Plate" }, { "Arceus-Fighting", "Arceus", "Fist Plate" }, { "Arceus-Fire", "Arceus", "Flame Plate" }, { "Arceus-Flying", "Arceus", "Sky Plate" }, { "Arceus-Ghost", "Arceus", "Spooky Plate" }, { "Arceus-Grass", "Arceus", "Meadow Plate" }, { "Arceus-Ground", "Arceus", "Earth Plate" }, { "Arceus-Ice", "Arceus", "Icicle Plate" }, { "Arceus-Poison", "Arceus", "Toxic Plate" }, { "Arceus-Psychic", "Arceus", "Mind Plate" }, { "Arceus-Rock", "Arceus", "Stone Plate" }, { "Arceus-Steel", "Arceus", "Iron Plate" }, { "Arceus-Water", "Arceus", "Splash Plate" }, { "Genesect-Burn", "Genesect", "Burn Drive" }, { "Genesect-Chill", "Genesect", "Chill Drive" }, { "Genesect-Douse", "Genesect", "Douse Drive" }, { "Genesect-Shock", "Genesect", "Shock Drive" }, { "Ogerpon-Cornerstone", "Ogerpon", "Cornerstone Mask" }, { "Ogerpon-Hearthflame", "Ogerpon", "Hearthflame Mask" }, { "Ogerpon-Wellspring", "Ogerpon", "Wellspring Mask" }, { "Silvally-Bug", "Silvally", "Bug Memory" }, { "Silvally-Dark", "Silvally", "Dark Memory" }, { "Silvally-Dragon", "Silvally", "Dragon Memory" }, { "Silvally-Electric", "Silvally", "Electric Memory" }, { "Silvally-Fairy", "Silvally", "Fairy Memory" }, { "Silvally-Fighting", "Silvally", "Fighting Memory" }, { "Silvally-Fire", "Silvally", "Fire Memory" }, { "Silvally-Flying", "Silvally", "Flying Memory" }, { "Silvally-Ghost", "Silvally", "Ghost Memory" }, { "Silvally-Grass", "Silvally", "Grass Memory" }, { "Silvally-Ground", "Silvally", "Ground Memory" }, { "Silvally-Ice", "Silvally", "Ice Memory" }, { "Silvally-Poison", "Silvally", "Poison Memory" }, { "Silvally-Psychic", "Silvally", "Psychic Memory" }, { "Silvally-Rock", "Silvally", "Rock Memory" }, { "Silvally-Steel", "Silvally", "Steel Memory" }, { "Silvally-Water", "Silvally", "Water Memory" }, { NULL, NULL, NULL } }; static bool parse_trainer(struct Parser *p, const struct Parsed *parsed, struct Trainer *trainer) { bool any_error = false; *trainer = (struct Trainer) {}; while (match_empty_line(p)) {} struct Token id; struct Token nickname, species, gender, item; if (!parse_section(p, &id)) { if (!p->error) set_parse_error(p, p->location, "expected '==='"); show_parse_error(p); return false; } trainer->id = token_string(&id); trainer->id_line = id.location.line; // Parse trainer attributes. struct Token key, value; while (parse_attribute(p, &key, &value)) { if (is_literal_token(&key, "AI")) { if (trainer->ai_flags_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'AI'"); trainer->ai_flags_line = value.location.line; if (!token_human_identifiers(p, &value, trainer->ai_flags, &trainer->ai_flags_n, MAX_TRAINER_AI_FLAGS)) any_error = !show_parse_error(p); } else if (is_literal_token(&key, "Items")) { if (trainer->items_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'Items'"); trainer->items_line = value.location.line; if (!token_human_identifiers(p, &value, trainer->items, &trainer->items_n, MAX_TRAINER_ITEMS)) any_error = !show_parse_error(p); } else if (is_literal_token(&key, "Class")) { if (trainer->class_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'Class'"); trainer->class_line = value.location.line; trainer->class = token_string(&value); } else if (is_literal_token(&key, "Music")) { if (trainer->encounter_music_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'Music'"); trainer->encounter_music_line = value.location.line; trainer->encounter_music = token_string(&value); } else if (is_literal_token(&key, "Gender")) { if (trainer->gender_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'Gender'"); trainer->gender_line = value.location.line; if (!token_gender(p, &value, &trainer->gender)) any_error = !show_parse_error(p); } else if (is_literal_token(&key, "Pic")) { if (trainer->pic_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'Pic'"); trainer->pic_line = value.location.line; trainer->pic = token_string(&value); } else if (is_literal_token(&key, "Name")) { if (trainer->name_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'Name'"); trainer->name_line = value.location.line; trainer->name = token_string(&value); } else if (is_literal_token(&key, "Double Battle")) { if (trainer->double_battle_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'Double Battle'"); trainer->double_battle_line = value.location.line; if (!token_bool(p, &value, &trainer->double_battle)) any_error = !show_parse_error(p); } else if (is_literal_token(&key, "Mugshot")) { if (trainer->mugshot_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'Mugshot'"); trainer->mugshot_line = value.location.line; trainer->mugshot = token_string(&value); } else if (is_literal_token(&key, "Starting Status")) { if (trainer->starting_status_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'Starting Status'"); trainer->starting_status_line = value.location.line; trainer->starting_status = token_string(&value); } else { any_error = !set_show_parse_error(p, key.location, "expected one of 'Name', 'Class', 'Pic', 'Gender', 'Music', 'Items', 'Double Battle', or 'AI'"); } } if (!trainer->pic_line) any_error = !set_show_parse_error(p, p->location, "expected 'Pic' before Pokemon"); if (!trainer->name_line) any_error = !set_show_parse_error(p, p->location, "expected 'Name' before Pokemon"); if (!match_empty_line(p)) { set_show_parse_error(p, p->location, "expected empty line"); // Continue if the line is the start of a new Pokemon. struct Parser p_ = *p; if (!parse_pokemon_header(&p_, &nickname, &species, &gender, &item)) return false; } for (int i = 0; i < PARTY_SIZE; i++) { struct Pokemon *pokemon = &trainer->pokemon[i]; // Parse first line. while (match_empty_line(p)) {} if (!parse_pokemon_header(p, &nickname, &species, &gender, &item)) { if (i > 0 || ends_with(trainer->id, "_NONE")) break; if (!p->error) set_parse_error(p, p->location, "expected nickname or species"); show_parse_error(p); // TODO: Continue if a Pokemon starts before the next section. // This is difficult to do, because 'Type: Null' is the name // of a species, and so 'X: Y' is being parsed as a Pokemon // header. return false; } trainer->pokemon_n++; pokemon->nickname = token_string(&nickname); pokemon->species = token_string(&species); if (!token_gender(p, &gender, &pokemon->gender)) any_error = !show_parse_error(p); pokemon->item = token_string(&item); pokemon->header_line = species.location.line; for (int i = 0; gendered_species[i].species; i++) { if (is_literal_string(pokemon->species, gendered_species[i].species)) { switch (pokemon->gender) { case GENDER_ANY: break; case GENDER_MALE: pokemon->species = literal_string(gendered_species[i].male_species); break; case GENDER_FEMALE: pokemon->species = literal_string(gendered_species[i].female_species); break; } pokemon->gender = GENDER_ANY; break; } } for (int i = 0; itemed_species[i].species; i++) { if (is_literal_string(pokemon->species, itemed_species[i].form) && is_empty_string(pokemon->item)) { pokemon->species = literal_string(itemed_species[i].species); pokemon->item = literal_string(itemed_species[i].item); } } // Parse Pokemon attributes. while (parse_attribute(p, &key, &value)) { if (is_literal_token(&key, "EVs")) { if (pokemon->evs_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'EVs'"); pokemon->evs_line = value.location.line; if (!token_stats(p, &value, &pokemon->evs, false)) any_error = !show_parse_error(p); } else if (is_literal_token(&key, "IVs")) { if (pokemon->ivs_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'IVs'"); pokemon->ivs_line = value.location.line; pokemon->ivs = parsed->default_ivs; if (!token_stats(p, &value, &pokemon->ivs, parsed->default_ivs_off)) any_error = !show_parse_error(p); } else if (is_literal_token(&key, "Ability")) { if (pokemon->ability_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'Ability'"); pokemon->ability_line = value.location.line; pokemon->ability = token_string(&value); } else if (is_literal_token(&key, "Level")) { if (pokemon->level_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'Level'"); pokemon->level_line = value.location.line; if (!token_int(p, &value, &pokemon->level)) any_error = !show_parse_error(p); } else if (is_literal_token(&key, "Ball")) { if (pokemon->ball_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'Ball'"); pokemon->ball_line = value.location.line; pokemon->ball = token_string(&value); } else if (is_literal_token(&key, "Happiness")) { if (pokemon->friendship_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'Happiness'"); pokemon->friendship_line = value.location.line; if (!token_int(p, &value, &pokemon->friendship)) any_error = !show_parse_error(p); } else if (is_literal_token(&key, "Nature")) { if (pokemon->nature_line) any_error = !set_show_parse_error(p, value.location, "duplicate 'Nature'"); pokemon->nature_line = value.location.line; pokemon->nature = token_string(&value); } else if (is_literal_token(&key, "Shiny")) { if (pokemon->shiny_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'Shiny'"); pokemon->shiny_line = value.location.line; if (!token_bool(p, &value, &pokemon->shiny)) any_error = !show_parse_error(p); } else if (is_literal_token(&key, "Dynamax Level")) { if (pokemon->dynamax_level_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'Dynamax Level'"); pokemon->dynamax_level_line = value.location.line; if (!token_int(p, &value, &pokemon->dynamax_level)) any_error = !show_parse_error(p); } else if (is_literal_token(&key, "Gigantamax")) { if (pokemon->gigantamax_factor_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'Gigantamax'"); pokemon->gigantamax_factor_line = value.location.line; if (!token_bool(p, &value, &pokemon->gigantamax_factor)) any_error = !show_parse_error(p); } else if (is_literal_token(&key, "Tera Type")) { if (pokemon->tera_type_line) any_error = !set_show_parse_error(p, key.location, "duplicate 'Tera Type'"); pokemon->tera_type_line = value.location.line; pokemon->tera_type = token_string(&value); } else { any_error = !set_show_parse_error(p, key.location, "expected one of 'EVs', 'IVs', 'Ability', 'Level', 'Ball', 'Happiness', 'Nature', 'Shiny', 'Dynamax Level', 'Gigantamax', or 'Tera Type'"); } } if (!pokemon->level_line) { if (!parsed->default_level_off) { pokemon->level = parsed->default_level; pokemon->level_line = p->location.line; } else { any_error = !set_show_parse_error(p, p->location, "expected 'Level' before moves"); } } if (!pokemon->ivs_line) { if (!parsed->default_ivs_off) { pokemon->ivs = parsed->default_ivs; pokemon->ivs_line = p->location.line; } else { any_error = !set_show_parse_error(p, p->location, "expected 'IVs' before moves"); } } // Parse moves. for (int j = 0; j < MAX_MON_MOVES; j++) { struct Token move; if (!parse_pokemon_move(p, &move)) { struct Parser p_ = *p; if (match_eof(&p_) || match_empty_line(&p_)) break; } else { if (pokemon->moves_n == 0) pokemon->move1_line = move.location.line; pokemon->moves[pokemon->moves_n] = token_string(&move); pokemon->moves_n++; } } if (match_eof(p)) break; if (!match_empty_line(p)) { set_show_parse_error(p, p->location, "expected empty line"); // Continue if the line is the start of a new Pokemon. struct Parser p_ = *p; if (!parse_pokemon_header(&p_, &nickname, &species, &gender, &item)) return false; } } return !any_error; } static void parse(struct Parser *p, struct Parsed *parsed) { parsed->source = p->source; int trainers_c = 256; parsed->trainers = malloc(sizeof(*parsed->trainers) * trainers_c); parsed->trainers_n = 0; assert(parsed->trainers); for (;;) { while (match_empty_line(p)) {} if (!parse_pragma(p, parsed)) break; } for (;;) { if (parsed->trainers_n == trainers_c) { trainers_c *= 2; struct Trainer *trainers_ = realloc(parsed->trainers, sizeof(*parsed->trainers) * trainers_c); assert(trainers_); parsed->trainers = trainers_; } struct Trainer *trainer = &parsed->trainers[parsed->trainers_n]; while (match_empty_line(p)) {} if (match_eof(p)) break; if (!parse_trainer(p, parsed, trainer)) { struct Token t; // Skip to the next trainer. for (;;) { struct Parser p_ = *p; if (match_eof(&p_) || parse_section(&p_, &t)) break; skip_line(p); } } else { parsed->trainers_n++; } } } static void fprint_string(FILE *f, struct String s) { fprintf(f, "%.*s", s.string_n, s.string); } static void fprint_bool(FILE *f, bool b) { fprintf(f, b ? "TRUE" : "FALSE"); } static void fprint_stats(FILE *f, const char *macro, struct Stats stats) { fprintf(f, "%s(%d, %d, %d, %d, %d, %d)", macro, stats.hp, stats.attack, stats.defense, stats.speed, stats.special_attack, stats.special_defense); } static bool is_constant(struct String s, const char *prefix) { for (int i = 0;; i++) { if (i == s.string_n) return false; else if (prefix[i] == '\0') return s.string[i] == '_'; else if (prefix[i] != s.string[i]) return false; } } static void fprint_constant(FILE *f, const char *prefix, struct String s) { if (!is_constant(s, prefix)) fprintf(f, "%s_", prefix); if (s.string_n > 0) { for (int i = 0; i < s.string_n; i++) { unsigned char c = s.string[i]; if (('A' <= c && c <= 'Z') || ('0' <= c && c <= '9')) fputc(c, f); else if ('a' <= c && c <= 'z') fputc(c - 'a' + 'A', f); else fputc('_', f); } } else { fprintf(f, "NONE"); } } // This is a really stupid helper for 'fprint_species'. static bool is_utf8_character(struct String s, int *i, const unsigned char *utf8) { for (int j = *i;; j++, utf8++) { if (*utf8 == '\0') { *i = j - 1; // '- 1' because of the '++' in 'fprint_species'. return true; } else if (j == s.string_n || s.string[j] != *utf8) { return false; } } } static void fprint_species(FILE *f, const char *prefix, struct String s) { if (!is_constant(s, prefix)) fprintf(f, "%s_", prefix); if (s.string_n == 0) { fprintf(f, "NONE"); } else { bool underscore = false; static const unsigned char *male = (unsigned char *)u8"♂"; static const unsigned char *female = (unsigned char *)u8"♀"; static const unsigned char *e_diacritic = (unsigned char *)u8"é"; static const unsigned char *right_single_quotation_mark = (unsigned char *)u8"’"; for (int i = 0; i < s.string_n; i++) { unsigned char c = s.string[i]; if (('A' <= c && c <= 'Z') || ('0' <= c && c <= '9')) { if (underscore) fputc('_', f); underscore = false; fputc(c, f); } else if ('a' <= c && c <= 'z') { if (underscore) fputc('_', f); underscore = false; fputc(c - 'a' + 'A', f); } else if (c == '\'' || c == '%' || is_utf8_character(s, &i, right_single_quotation_mark)) { // Do nothing. } else if (is_utf8_character(s, &i, male)) { underscore = false; fprintf(f, "_M"); } else if (is_utf8_character(s, &i, female)) { underscore = false; fprintf(f, "_F"); } else if (is_utf8_character(s, &i, e_diacritic)) { if (underscore) fputc('_', f); underscore = false; fputc('E', f); } else { underscore = true; } } } } static void fprint_trainers(const char *output_path, FILE *f, struct Parsed *parsed) { fprintf(f, "//\n"); fprintf(f, "// DO NOT MODIFY THIS FILE! It is auto-generated from %s\n", parsed->source->path); fprintf(f, "//\n"); fprintf(f, "// If you want to modify this file set COMPETITIVE_PARTY_SYNTAX to FALSE\n"); fprintf(f, "// in include/config/general.h and remove this notice.\n"); fprintf(f, "// Use sed -i '/^#line/d' '%s' to remove #line markers.\n", output_path); fprintf(f, "//\n"); fprintf(f, "\n"); fprintf(f, "#line 1 \"%s\"\n", parsed->source->path); fprintf(f, "\n"); for (int i = 0; i < parsed->trainers_n; i++) { struct Trainer *trainer = &parsed->trainers[i]; fprintf(f, "#line %d\n", trainer->id_line); fprintf(f, " ["); fprint_string(f, trainer->id); fprintf(f, "] =\n"); fprintf(f, " {\n"); if (!is_empty_string(trainer->name)) { fprintf(f, "#line %d\n", trainer->name_line); fprintf(f, " .trainerName = _(\""); fprint_string(f, trainer->name); fprintf(f, "\"),\n"); } if (!is_empty_string(trainer->class)) { fprintf(f, "#line %d\n", trainer->class_line); fprintf(f, " .trainerClass = "); fprint_constant(f, "TRAINER_CLASS", trainer->class); fprintf(f, ",\n"); } if (!is_empty_string(trainer->pic)) { fprintf(f, "#line %d\n", trainer->pic_line); fprintf(f, " .trainerPic = "); 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"); } fprintf(f, " .encounterMusic_gender = \n"); if (trainer->gender == GENDER_FEMALE) { fprintf(f, "#line %d\n", trainer->gender_line); fprintf(f, "F_TRAINER_FEMALE | \n"); } if (!is_empty_string(trainer->encounter_music)) { fprintf(f, "#line %d\n", trainer->encounter_music_line); fprintf(f, " "); fprint_constant(f, "TRAINER_ENCOUNTER_MUSIC", trainer->encounter_music); } else { fprintf(f, "0"); } fprintf(f, ",\n"); if (trainer->items_n > 0) { fprintf(f, "#line %d\n", trainer->items_line); fprintf(f, " .items = { "); for (int i = 0; i < trainer->items_n; i++) { if (i > 0) fprintf(f, ", "); fprint_constant(f, "ITEM", trainer->items[i]); } fprintf(f, " },\n"); } if (trainer->double_battle_line) { fprintf(f, "#line %d\n", trainer->double_battle_line); fprintf(f, " .doubleBattle = "); fprint_bool(f, trainer->double_battle); fprintf(f, ",\n"); } if (trainer->ai_flags_n > 0) { fprintf(f, "#line %d\n", trainer->ai_flags_line); fprintf(f, " .aiFlags = "); for (int i = 0; i < trainer->ai_flags_n; i++) { if (i > 0) fprintf(f, " | "); fprint_constant(f, "AI_FLAG", trainer->ai_flags[i]); } fprintf(f, ",\n"); } if (!is_empty_string(trainer->mugshot)) { fprintf(f, "#line %d\n", trainer->mugshot_line); fprintf(f, " .mugshotEnabled = TRUE,\n"); fprintf(f, " .mugshotColor = "); fprint_constant(f, "MUGSHOT_COLOR", trainer->mugshot); fprintf(f, ",\n"); } if (!is_empty_string(trainer->starting_status)) { fprintf(f, "#line %d\n", trainer->starting_status_line); fprintf(f, " .startingStatus = "); fprint_constant(f, "STARTING_STATUS", trainer->starting_status); fprintf(f, ",\n"); } fprintf(f, " .partySize = %d,\n", trainer->pokemon_n); fprintf(f, " .party = (const struct TrainerMon[])\n"); fprintf(f, " {\n"); for (int j = 0; j < trainer->pokemon_n; j++) { struct Pokemon *pokemon = &trainer->pokemon[j]; fprintf(f, " {\n"); if (!is_empty_string(pokemon->nickname)) { fprintf(f, "#line %d\n", pokemon->header_line); fprintf(f, " .nickname = COMPOUND_STRING(\""); fprint_string(f, pokemon->nickname); fprintf(f, "\"),\n"); } fprintf(f, "#line %d\n", pokemon->header_line); fprintf(f, " .species = "); fprint_species(f, "SPECIES", pokemon->species); fprintf(f, ",\n"); switch (pokemon->gender) { case GENDER_ANY: fprintf(f, " .gender = TRAINER_MON_RANDOM_GENDER,\n"); break; case GENDER_MALE: fprintf(f, "#line %d\n", pokemon->header_line); fprintf(f, " .gender = TRAINER_MON_MALE,\n"); break; case GENDER_FEMALE: fprintf(f, "#line %d\n", pokemon->header_line); fprintf(f, " .gender = TRAINER_MON_FEMALE,\n"); break; } if (!is_empty_string(pokemon->item)) { fprintf(f, "#line %d\n", pokemon->header_line); fprintf(f, " .heldItem = "); fprint_constant(f, "ITEM", pokemon->item); fprintf(f, ",\n"); } if (pokemon->evs_line) { fprintf(f, "#line %d\n", pokemon->evs_line); fprintf(f, " .ev = "); fprint_stats(f, "TRAINER_PARTY_EVS", pokemon->evs); fprintf(f, ",\n"); } if (pokemon->ivs_line) { fprintf(f, "#line %d\n", pokemon->ivs_line); fprintf(f, " .iv = "); fprint_stats(f, "TRAINER_PARTY_IVS", pokemon->ivs); fprintf(f, ",\n"); } if (pokemon->ability_line) { fprintf(f, "#line %d\n", pokemon->ability_line); fprintf(f, " .ability = "); fprint_constant(f, "ABILITY", pokemon->ability); fprintf(f, ",\n"); } if (pokemon->level_line) { fprintf(f, "#line %d\n", pokemon->level_line); fprintf(f, " .lvl = %d,\n", pokemon->level); } if (pokemon->ball_line) { fprintf(f, "#line %d\n", pokemon->ball_line); fprintf(f, " .ball = "); fprint_constant(f, "ITEM", pokemon->ball); fprintf(f, ",\n"); } if (pokemon->friendship_line) { fprintf(f, "#line %d\n", pokemon->friendship_line); fprintf(f, " .friendship = %d,\n", pokemon->friendship); } if (pokemon->nature_line) { fprintf(f, "#line %d\n", pokemon->nature_line); fprintf(f, " .nature = "); fprint_constant(f, "NATURE", pokemon->nature); fprintf(f, ",\n"); } else { fprintf(f, " .nature = NATURE_HARDY,\n"); } if (pokemon->shiny_line) { fprintf(f, "#line %d\n", pokemon->shiny_line); fprintf(f, " .isShiny = "); fprint_bool(f, pokemon->shiny); fprintf(f, ",\n"); } if (pokemon->dynamax_level_line) { fprintf(f, "#line %d\n", pokemon->dynamax_level_line); fprintf(f, " .dynamaxLevel = %d,\n", pokemon->dynamax_level); } else { fprintf(f, " .dynamaxLevel = MAX_DYNAMAX_LEVEL,\n"); } if (pokemon->gigantamax_factor_line) { fprintf(f, "#line %d\n", pokemon->gigantamax_factor_line); fprintf(f, " .gigantamaxFactor = "); fprint_bool(f, pokemon->gigantamax_factor); fprintf(f, ",\n"); } if (pokemon->dynamax_level_line || pokemon->gigantamax_factor_line) { fprintf(f, " .shouldUseDynamax = TRUE,\n"); } else if (pokemon->tera_type_line) { fprintf(f, "#line %d\n", pokemon->tera_type_line); fprintf(f, " .teraType = "); fprint_constant(f, "TYPE", pokemon->tera_type); fprintf(f, ",\n"); } if (pokemon->moves_n > 0) { fprintf(f, " .moves = {\n"); fprintf(f, "#line %d\n", pokemon->move1_line); for (int k = 0; k < pokemon->moves_n; k++) { fprintf(f, " "); fprint_constant(f, "MOVE", pokemon->moves[k]); fprintf(f, ",\n"); } fprintf(f, " },\n"); } fprintf(f, " },\n"); } fprintf(f, " },\n"); fprintf(f, " },\n"); } } static void usage(FILE *file, char *argv0) { fprintf(file, "Usage: %s -o \n", argv0); } int main(int argc, char *argv[]) { int status = 1; FILE *source_file = NULL; FILE *output_file = NULL; unsigned char *source_buffer = NULL; struct Parsed parsed = { .default_ivs = { 31, 31, 31, 31, 31, 31 }, .default_level = 100, }; const char *source_path = NULL; const char *output_path = NULL; const char *real_source_path = NULL; int opt; while ((opt = getopt(argc, argv, "i:o:")) != -1) { switch (opt) { case 'i': real_source_path = optarg; break; case 'o': output_path = optarg; break; default: fprintf(stderr, "unknown option '%c'\n", opt); usage(stderr, argv[0]); goto exit; } } if (!output_path) { usage(stderr, argv[0]); goto exit; } if (optind != argc - 1) { usage(stderr, argv[0]); goto exit; } source_path = argv[optind++]; int source_buffer_n; if (strcmp(source_path, "-") == 0) { source_file = stdin; source_path = ""; int source_buffer_c = 4096; source_buffer_n = 0; for (;;) { if (!(source_buffer = realloc(source_buffer, source_buffer_c))) { fprintf(stderr, "could not allocate %d bytes\n", source_buffer_c); goto exit; } source_buffer_n += fread(&source_buffer[source_buffer_n], 1, source_buffer_c - source_buffer_n, source_file); if (source_buffer_n < source_buffer_c) break; source_buffer_c += source_buffer_n / 2; // 1.5x growth rate. } } else { source_file = fopen(source_path, "r"); if (source_file == NULL) { fprintf(stderr, "could not open '%s' for reading\n", source_path); goto exit; } fseek(source_file, 0, SEEK_END); long source_buffer_n_ = ftell(source_file); if (source_buffer_n_ > INT_MAX) { fprintf(stderr, "could not read '%s': too big\n", source_path); goto exit; } source_buffer_n = source_buffer_n_; if (!(source_buffer = malloc(source_buffer_n))) { fprintf(stderr, "could not allocate %d bytes\n", source_buffer_n); goto exit; } rewind(source_file); if (fread(source_buffer, 1, source_buffer_n, source_file) < source_buffer_n) { fprintf(stderr, "could not read '%s'\n", source_path); goto exit; } } struct Source source = { .path = real_source_path ? real_source_path : source_path, .buffer = source_buffer, .buffer_n = source_buffer_n, }; struct Parser parser = { .source = &source, .location = { .line = 1, .column = 1 }, .offset = 0, }; parse(&parser, &parsed); if (parser.fatal_error) { goto exit; } if (strcmp(output_path, "-") == 0) { source_file = stdout; output_path = ""; } else { output_file = fopen(output_path, "w"); if (output_file == NULL) { fprintf(stderr, "could not open '%s' for writing\n", output_path); goto exit; } } fprint_trainers(output_path, output_file, &parsed); status = 0; exit: if (output_file) fclose(output_file); if (parsed.trainers) free(parsed.trainers); if (source_buffer) free(source_buffer); if (source_file) fclose(source_file); return status; }