diff --git a/pokesay.go b/pokesay.go index 0d700912..651495c6 100644 --- a/pokesay.go +++ b/pokesay.go @@ -2,7 +2,11 @@ package main import ( "embed" + "encoding/json" "fmt" + "log" + "os" + "strconv" "strings" "github.com/pborman/getopt/v2" @@ -39,10 +43,12 @@ func parseFlags() pokesay.Args { // selection/filtering name := getopt.StringLong("name", 'n', "", "choose a pokemon from a specific name") + id := getopt.StringLong("id", 'i', "", "choose a pokemon from a specific ID (see `pokesay -l` for IDs)") category := getopt.StringLong("category", 'c', "", "choose a pokemon from a specific category") // list operations - listNames := getopt.BoolLong("list-names", 'l', "list all available names") + listNames := getopt.StringLong("list-names", 'l', "", "list all available names") + getopt.Lookup('l').SetOptional() listCategories := getopt.BoolLong("list-categories", 'L', "list all available categories") width := getopt.IntLong("width", 'w', 80, "the max speech bubble width") @@ -55,6 +61,7 @@ func parseFlags() pokesay.Args { // info box options japaneseName := getopt.BoolLong("japanese-name", 'j', "print the japanese name in the info box") + showId := getopt.BoolLong("id-info", 'I', "print the pokemon ID in the info box") noCategoryInfo := getopt.BoolLong("no-category-info", 'C', "do not print pokemon category information in the info box") drawInfoBorder := getopt.BoolLong("info-border", 'b', "draw a border around the info box") @@ -82,10 +89,13 @@ func parseFlags() pokesay.Args { NoTabSpaces: *noTabSpaces, NoCategoryInfo: *noCategoryInfo, ListCategories: *listCategories, - ListNames: *listNames, + ListNames: getopt.GetCount("list-names") > 0, + ListNameToken: *listNames, Category: *category, NameToken: *name, + IDToken: *id, JapaneseName: *japaneseName, + ShowID: *showId, BoxCharacters: pokesay.DetermineBoxCharacters(*unicodeBorders), DrawInfoBorder: *drawInfoBorder, Help: *help, @@ -106,25 +116,52 @@ func runListCategories() { // runListNames prints all available pokemon names // - This reads a struct of {name -> metadata indexes} from the embedded filesystem // - prints all the keys of the struct, and the total number of names -func runListNames() { - names := pokesay.ListNames( - pokedex.ReadStructFromBytes[map[string][]int](GOBAllNames), - ) - fmt.Printf("%s\n%d %s\n", strings.Join(names, " "), len(names), "total names") +func runListNames(token string) { + t := timer.NewTimer("runListNames", true) + + pokedex.ReadStructFromBytes[map[string][]int](GOBAllNames) + namesSorted := pokedex.GatherMapKeys(pokedex.ReadStructFromBytes[map[string][]int](GOBAllNames)) + t.Mark("read metadata") + + s := make(map[string]map[string]string) + exit := false + for i, name := range namesSorted { + metadata := pokedex.ReadMetadataFromEmbedded(GOBCowNames, pokedex.MetadataFpath(MetadataRoot, i)) + + entries := make(map[string]string, 0) + + for _, entry := range metadata.Entries { + k := fmt.Sprintf("%04d.%04d", i, entry.EntryIndex) + entries[k] = strings.Join(entry.Categories, ", ") + } + if token != "" && token == name { + json, _ := json.MarshalIndent(map[string]map[string]string{name: entries}, "", strings.Repeat(" ", 2)) + fmt.Fprintln(os.Stdout, string(json)) + exit = true + break + } + s[name] = entries + } + t.Mark("read metadata") + if exit { + return + } + json, _ := json.MarshalIndent(s, "", strings.Repeat(" ", 2)) + fmt.Fprintln(os.Stdout, string(json)) } // GenerateNames returns a list of names to print // - If the japanese name flag is set, it returns both the english and japanese names // - Otherwise, it returns just the english name -func GenerateNames(metadata pokedex.PokemonMetadata, args pokesay.Args) []string { +func GenerateNames(metadata pokedex.PokemonMetadata, args pokesay.Args, final pokedex.PokemonEntryMapping) []string { + nameParts := []string{metadata.Name} if args.JapaneseName { - return []string{ - metadata.Name, - fmt.Sprintf("%s (%s)", metadata.JapaneseName, metadata.JapanesePhonetic), - } - } else { - return []string{metadata.Name} + nameParts = append(nameParts, fmt.Sprintf("%s (%s)", metadata.JapaneseName, metadata.JapanesePhonetic)) + } + if args.ShowID { + nameParts = append(nameParts, fmt.Sprintf("%s.%04d", metadata.Idx, final.EntryIndex)) } + return nameParts } // runPrintByName prints a pokemon matched by a name @@ -141,7 +178,35 @@ func runPrintByName(args pokesay.Args) { metadata, final := pokesay.ChooseByName(names, args.NameToken, GOBCowNames, MetadataRoot) t.Mark("find/read metadata") - pokesay.Print(args, final.EntryIndex, GenerateNames(metadata, args), final.Categories, GOBCowData) + pokesay.Print(args, final.EntryIndex, GenerateNames(metadata, args, final), final.Categories, GOBCowData) + t.Mark("print") + + t.Stop() + t.PrintJson() +} + +// runPrintByID prints a pokemon corresponding to a specific ID +// - This reads a list of names from the embedded filesystem +// - It finds the name at alphabetical index `IDToken` +// - It matches the name to a metadata index, loads the corresponding metadata file, and then chooses a random entry +// - Finally, it prints the pokemon +func runPrintByID(args pokesay.Args) { + t := timer.NewTimer("runPrintByName", true) + + idxs := strings.Split(args.IDToken, ".") + + idx, _ := strconv.Atoi(idxs[0]) + subIdx, _ := strconv.Atoi(idxs[1]) + + t.Mark("format IDs") + + metadata, final, err := pokesay.ChooseByIndex(idx, subIdx, GOBCowNames, MetadataRoot) + if err != nil { + log.Fatal(err) + } + t.Mark("find/read metadata") + + pokesay.Print(args, subIdx, GenerateNames(metadata, args, final), final.Categories, GOBCowData) t.Mark("print") t.Stop() @@ -164,7 +229,7 @@ func runPrintByCategory(args pokesay.Args) { dir, _ := GOBCategories.ReadDir(dirPath) metadata, final := pokesay.ChooseByCategory(args.Category, dir, GOBCategories, CategoryRoot, GOBCowNames, MetadataRoot) - pokesay.Print(args, final.EntryIndex, GenerateNames(metadata, args), final.Categories, GOBCowData) + pokesay.Print(args, final.EntryIndex, GenerateNames(metadata, args, final), final.Categories, GOBCowData) t.Mark("print") t.Stop() @@ -184,7 +249,7 @@ func runPrintByNameAndCategory(args pokesay.Args) { metadata, final := pokesay.ChooseByNameAndCategory(names, args.NameToken, GOBCowNames, MetadataRoot, args.Category) t.Mark("find/read metadata") - pokesay.Print(args, final.EntryIndex, GenerateNames(metadata, args), final.Categories, GOBCowData) + pokesay.Print(args, final.EntryIndex, GenerateNames(metadata, args, final), final.Categories, GOBCowData) t.Mark("print") t.Stop() @@ -207,7 +272,7 @@ func runPrintRandom(args pokesay.Args) { final := metadata.Entries[pokesay.RandomInt(len(metadata.Entries))] t.Mark("choose entry") - pokesay.Print(args, final.EntryIndex, GenerateNames(metadata, args), final.Categories, GOBCowData) + pokesay.Print(args, final.EntryIndex, GenerateNames(metadata, args, final), final.Categories, GOBCowData) t.Mark("print") t.Stop() @@ -231,11 +296,13 @@ func main() { if args.ListCategories { runListCategories() } else if args.ListNames { - runListNames() + runListNames(args.ListNameToken) } else if args.NameToken != "" && args.Category != "" { runPrintByNameAndCategory(args) } else if args.NameToken != "" { runPrintByName(args) + } else if args.IDToken != "" { + runPrintByID(args) } else if args.Category != "" { runPrintByCategory(args) } else { diff --git a/src/bin/pokedex/pokedex.go b/src/bin/pokedex/pokedex.go index c43d8bea..55026424 100644 --- a/src/bin/pokedex/pokedex.go +++ b/src/bin/pokedex/pokedex.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path" + "sort" "strings" "github.com/tmck-code/pokesay/src/bin" @@ -98,27 +99,24 @@ func main() { fmt.Println("- Found", len(cowfileFpaths), "cowfiles") // Read pokemon names pokemonNames := pokedex.ReadNames(args.FromMetadataFname) - fmt.Println("- Read", len(pokemonNames), "pokemon names from", args.FromMetadataFname) - - fmt.Println("- Writing entries to file") - pbar := bin.NewProgressBar(len(cowfileFpaths)) - for i, fpath := range cowfileFpaths { - data, err := os.ReadFile(fpath) - pokedex.Check(err) + nameTokens := pokedex.GatherMapKeys(pokemonNames) + sort.Strings(nameTokens) + fmt.Println("names:", nameTokens) - pokedex.WriteBytesToFile(data, pokedex.EntryFpath(paths.EntryDirPath, i), true) - pbar.Add(1) - } + fmt.Println("- Read", len(pokemonNames), "pokemon names from", args.FromMetadataFname) // 1. For each pokemon name, write a metadata file, containing the name information, and // links to all of the matching cowfile indexes fmt.Println("- Writing metadata to file") pokemonMetadata := make([]pokedex.PokemonMetadata, 0) uniqueNames := make(map[string][]int) + nameVariants := make(map[string][]string) i := 0 - pbar = bin.NewProgressBar(len(pokemonNames)) - for key, name := range pokemonNames { - metadata := pokedex.CreateNameMetadata(i, key, name, args.FromDir, cowfileFpaths) + pbar := bin.NewProgressBar(len(pokemonNames)) + for i, key := range nameTokens { + name := pokemonNames[key] + // add variant + metadata := pokedex.CreateNameMetadata(fmt.Sprintf("%04d", i), key, name, args.FromDir, cowfileFpaths) pokedex.WriteStructToFile(metadata, pokedex.MetadataFpath(paths.MetadataDirPath, i)) pokemonMetadata = append(pokemonMetadata, *metadata) uniqueNames[name.Slug] = append(uniqueNames[name.Slug], i) @@ -126,6 +124,31 @@ func main() { pbar.Add(1) } + fmt.Println("- Writing entries to file") + pbar = bin.NewProgressBar(len(cowfileFpaths)) + for i, fpath := range cowfileFpaths { + data, err := os.ReadFile(fpath) + pokedex.Check(err) + entryFpath := pokedex.EntryFpath(paths.EntryDirPath, i) + fmt.Println("writing", fpath, ">", entryFpath) + + fpathParts := strings.Split(fpath, "/") + basename := fpathParts[len(fpathParts)-1] + name := strings.SplitN(strings.TrimSuffix(basename, ".cow"), "-", 2)[0] + fmt.Println("name", name) + for i, key := range nameTokens { + if name == key { + fmt.Println("found", name, key, i, pokemonNames[key], pokemonMetadata[i]) + nameVariants[name] = append(nameVariants[name], basename) + fmt.Println("name ++", name, fmt.Sprintf("%04d.%04d", i, len(nameVariants[name]))) + break + } + } + + pokedex.WriteBytesToFile(data, entryFpath, true) + pbar.Add(1) + } + pokedex.WriteStructToFile(uniqueNames, "build/assets/names.txt") // 2. Create the category struct using the cowfile paths, pokemon names and indexes\ diff --git a/src/pokedex/metadata.go b/src/pokedex/metadata.go index 8b64ebff..7a545ffa 100644 --- a/src/pokedex/metadata.go +++ b/src/pokedex/metadata.go @@ -13,13 +13,14 @@ type PokemonEntryMapping struct { } type PokemonMetadata struct { + Idx string Name string JapaneseName string JapanesePhonetic string Entries []PokemonEntryMapping } -func NewMetadata(name string, japaneseName string, japanesePhonetic string, entryMap map[int][][]string) *PokemonMetadata { +func NewMetadata(idx string, name string, japaneseName string, japanesePhonetic string, entryMap map[int][][]string) *PokemonMetadata { entries := make([]PokemonEntryMapping, 0) @@ -30,6 +31,7 @@ func NewMetadata(name string, japaneseName string, japanesePhonetic string, entr } return &PokemonMetadata{ + Idx: idx, Name: name, JapaneseName: japaneseName, JapanesePhonetic: japanesePhonetic, diff --git a/src/pokedex/pokedex.go b/src/pokedex/pokedex.go index ea7a5060..19f9f1ee 100644 --- a/src/pokedex/pokedex.go +++ b/src/pokedex/pokedex.go @@ -130,19 +130,21 @@ func Decompress(data []byte) []byte { return resB.Bytes() } -func CreateNameMetadata(idx int, key string, name PokemonName, rootDir string, fpaths []string) *PokemonMetadata { +func CreateNameMetadata(idx string, key string, name PokemonName, rootDir string, fpaths []string) *PokemonMetadata { entryCategories := make(map[int][][]string, 0) for i, fpath := range fpaths { - basename := strings.TrimPrefix(fpath, rootDir) - if strings.Contains(basename, strings.ToLower(name.Slug)) { + localFpath := strings.TrimPrefix(fpath, rootDir) + + if strings.Contains(localFpath, strings.ToLower(name.Slug)) { data, err := os.ReadFile(fpath) Check(err) - cats := createCategories(strings.TrimPrefix(fpath, rootDir), data) + cats := createCategories(localFpath, data) entryCategories[i] = append(entryCategories[i], cats) } } return NewMetadata( + idx, name.English, name.Japanese, name.JapanesePhonetic, @@ -169,10 +171,28 @@ func CreateCategoryStruct(rootDir string, metadata []PokemonMetadata, debug bool } func createCategories(fpath string, data []byte) []string { + // split the path into parts, e.g. "gen7x/shiny/" parts := strings.Split(fpath, "/") + // Get the height of the cowfile by counting the number of lines height := sizeCategory(len(strings.Split(string(data), "\n"))) - return append([]string{height}, parts[0:len(parts)-1]...) + // split the fname into parts, e.g. "charizard-mega-y.cow" + // fmt.Println(fpath) + fpathParts := strings.Split(fpath, "/") + basename := fpathParts[len(fpathParts)-1] + names := strings.SplitN(strings.TrimSuffix(basename, ".cow"), "-", 2) + // fmt.Printf("created names from %s: %v\n", fpath, names) + + // return a slice of the parts + // height, names from the 2nd to the 2nd to last, and parts from the 1st to the 2nd to last + + c := []string{height} + if len(names) > 1 { + c = append(c, names[1:]...) + } + c = append(c, parts[0:len(parts)-1]...) + fmt.Printf("fpath %s, names: %v, categories: %v\n", fpath, names, c) + return c } func sizeCategory(height int) string { diff --git a/src/pokesay/lookup.go b/src/pokesay/lookup.go index 99bb4a8f..fdf56d7b 100644 --- a/src/pokesay/lookup.go +++ b/src/pokesay/lookup.go @@ -2,6 +2,7 @@ package pokesay import ( "embed" + "errors" "fmt" "io/fs" "log" @@ -76,7 +77,19 @@ func fetchMetadataByName(names map[string][]int, nameToken string, metadataFiles pokedex.MetadataFpath(metadataRootDir, nameChoice), ) return metadata +} +func ChooseByIndex(idx int, entryIdx int, metadataFiles embed.FS, metadataRootDir string) (pokedex.PokemonMetadata, pokedex.PokemonEntryMapping, error) { + metadata := pokedex.ReadMetadataFromEmbedded( + metadataFiles, + pokedex.MetadataFpath(metadataRootDir, idx), + ) + for _, entry := range metadata.Entries { + if entry.EntryIndex == entryIdx { + return metadata, entry, nil + } + } + return pokedex.PokemonMetadata{}, pokedex.PokemonEntryMapping{}, errors.New("could not find pokemon by index") } func ChooseByName(names map[string][]int, nameToken string, metadataFiles embed.FS, metadataRootDir string) (pokedex.PokemonMetadata, pokedex.PokemonEntryMapping) { diff --git a/src/pokesay/print.go b/src/pokesay/print.go index 43f06030..e04b192b 100644 --- a/src/pokesay/print.go +++ b/src/pokesay/print.go @@ -33,9 +33,12 @@ type Args struct { NoCategoryInfo bool ListCategories bool ListNames bool + ListNameToken string Category string NameToken string + IDToken string JapaneseName bool + ShowID bool BoxCharacters *BoxCharacters DrawInfoBorder bool Help bool