package main import ( "cmp" "context" "encoding/json" "flag" "fmt" "io" "log/slog" "net/http" "os" "os/signal" "path/filepath" "time" ) type DumpCard struct { ID int `json:"card_id"` Rarity int `json:"rarity"` // Level int `json:"talent_level"` // CreateTime int `json:"create_time"` } type DumpSupport struct { ID int `json:"support_card_id"` LB int `json:"limit_break_count"` // many other fields we don't need } type Collection struct { App string `json:"app"` // "gametora" Game string `json:"game"` // "umamusume" Type string `json:"type"` // "collection" Version int `json:"version"` // 4 Timestamp string `json:"timestamp"` // time.Now().String() Servers map[string]CollectionServer `json:"servers"` } type CollectionServer struct { CharCards map[string]int `json:"charCards"` Supports map[string]int `json:"supports"` Veterans struct{} `json:"-"` // `json:"veterans"` } type Localize struct { CardID int `json:"card_id"` // character card, or trainee SupportID int `json:"id"` // support card TID string `json:"tid"` // many other fields we don't need } func decode[T any](r io.Reader) ([]T, error) { var l []T d := json.NewDecoder(r) err := d.Decode(&l) return l, err } func tidMap(ctx context.Context, url string) (map[int]string, error) { req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } l, err := decode[Localize](resp.Body) m := make(map[int]string, len(l)) for _, v := range l { m[cmp.Or(v.CardID, v.SupportID)] = v.TID } return m, err } func filejson[T any](path string) ([]T, error) { f, err := os.Open(path) if err != nil { return nil, err } return decode[T](f) } func main() { ts := time.Now().UTC().Format(time.DateOnly) // can't use RFC3339 because :s in filename var ( dumpDir string out string ) flag.StringVar(&dumpDir, "dump", ".", "`dir`ectory containing umadump output files") flag.StringVar(&out, "o", fmt.Sprintf("uma-col-all-%s.json", ts), "output `file` (stdout if empty)") flag.Parse() ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) go func() { <-ctx.Done() stop() }() var err error w := os.Stdout if out != "" { w, err = os.Create(out) if err != nil { fatal("output", slog.Any("err", err)) } } charas, err := filejson[DumpCard](filepath.Join(dumpDir, "card_data.json")) if err != nil { fatal("character dump", slog.Any("err", err)) } supports, err := filejson[DumpSupport](filepath.Join(dumpDir, "support_card_data.json")) if err != nil { fatal("support card dump", slog.Any("err", err)) } charaTID, err := tidMap(ctx, "https://gametora.com/loc/umamusume/character_cards.json") if err != nil { fatal("character localization file", slog.Any("err", err)) } supportTID, err := tidMap(ctx, "https://gametora.com/loc/umamusume/support_cards.json") if err != nil { fatal("support card localization file", slog.Any("err", err)) } charaLevels := make(map[string]int, len(charas)) for _, v := range charas { charaLevels[charaTID[v.ID]] = v.Rarity } supportLevels := make(map[string]int, len(supports)) for _, v := range supports { supportLevels[supportTID[v.ID]] = v.LB } collection := Collection{ App: "gametora", Game: "umamusume", Type: "collection", Version: 4, Timestamp: ts, Servers: map[string]CollectionServer{ "en": { CharCards: charaLevels, Supports: supportLevels, }, }, } b, err := json.Marshal(collection) if err != nil { panic(err) } if _, err := w.Write(b); err != nil { fatal("writing output", slog.Any("err", err)) } } func fatal(msg string, args ...any) { slog.Error(msg, args...) os.Exit(1) }