From e6fa98f70719563b3c5e6fac270e3aabb51efe11 Mon Sep 17 00:00:00 2001 From: Branden J Brown Date: Wed, 6 May 2026 16:24:34 -0400 Subject: [PATCH] initial commit --- go.mod | 3 + umadump-gametora.go | 165 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 go.mod create mode 100644 umadump-gametora.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..48fef60 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.sunturtle.xyz/zephyr/umadump-gametora + +go 1.25.6 diff --git a/umadump-gametora.go b/umadump-gametora.go new file mode 100644 index 0000000..610a7c4 --- /dev/null +++ b/umadump-gametora.go @@ -0,0 +1,165 @@ +package main + +import ( + "cmp" + "context" + "encoding/json" + "flag" + "io" + "log/slog" + "net/http" + "os" + "os/signal" + "path/filepath" + "time" +) + +type DumpCard struct { + ID int `json:"card_id"` + Level int `json:"talent_level"` + Rarity int `json:"rarity"` + CreateTime int `json:"create_time"` +} + +type DumpSupport struct { + ID int `json:"supportCardId"` + Level int `json:"level"` + LB int `json:"limitBreakCount"` + MaxLevel int `json:"maxLevel"` + CreateTime int `json:"createTime"` + Exp int `json:"exp"` + Stock int `json:"stock"` + Favorite bool `json:"isFavoriteLock"` + BestTraining int `json:"bestTraining"` +} + +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() { + var ( + dumpDir string + out string + ) + flag.StringVar(&dumpDir, "dump", ".", "`dir`ectory containing umadump output files") + flag.StringVar(&out, "o", "", "output `file` (default stdout)") + 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: time.Now().String(), + 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) +}