package main import ( "bufio" "context" _ "embed" "encoding/json" "errors" "flag" "log/slog" "os" "os/signal" "path/filepath" "golang.org/x/sync/errgroup" "zombiezen.com/go/sqlite" "zombiezen.com/go/sqlite/sqlitex" "git.sunturtle.xyz/zephyr/horse/mdb" ) func main() { var ( mdbf string out string region string ) flag.StringVar(&mdbf, "mdb", os.ExpandEnv(`$USERPROFILE\AppData\LocalLow\Cygames\Umamusume\master\master.mdb`), "`path` to Umamusume master.mdb") flag.StringVar(&out, "o", `.`, "`dir`ectory for output files") flag.StringVar(®ion, "region", "global", "region the database is for (global, jp)") flag.Parse() slog.Info("open", slog.String("mdb", mdbf)) db, err := sqlitex.NewPool(mdbf, sqlitex.PoolOptions{Flags: sqlite.OpenReadOnly}) if err != nil { slog.Error("opening mdb", slog.String("mdb", mdbf), slog.Any("err", err)) os.Exit(1) } ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) go func() { <-ctx.Done() stop() }() loadgroup, ctx1 := errgroup.WithContext(ctx) charas := load(ctx1, loadgroup, db, "characters", mdb.Characters) aff := load(ctx1, loadgroup, db, "pair affinity", mdb.AffinitySummary) umas := load(ctx1, loadgroup, db, "umas", mdb.Umas) sg := load(ctx1, loadgroup, db, "skill groups", mdb.SkillGroups) skills := load(ctx1, loadgroup, db, "skills", mdb.Skills) races := load(ctx1, loadgroup, db, "races", mdb.Races) saddles := load(ctx1, loadgroup, db, "saddles", mdb.Saddles) scenarios := load(ctx1, loadgroup, db, "scenarios", mdb.Scenarios) sparks := load(ctx1, loadgroup, db, "sparks", mdb.Sparks) convos := load(ctx1, loadgroup, db, "lobby conversations", mdb.Conversations) if err := os.MkdirAll(filepath.Join(out, region), 0775); err != nil { slog.Error("create output dir", slog.Any("err", err)) os.Exit(1) } writegroup, ctx2 := errgroup.WithContext(ctx) writegroup.Go(func() error { return write(ctx2, out, region, "character.json", charas) }) writegroup.Go(func() error { return write(ctx2, out, region, "affinity.json", aff) }) writegroup.Go(func() error { return write(ctx2, out, region, "uma.json", umas) }) writegroup.Go(func() error { return write(ctx2, out, region, "skill-group.json", sg) }) writegroup.Go(func() error { return write(ctx2, out, region, "skill.json", skills) }) writegroup.Go(func() error { return write(ctx2, out, region, "race.json", races) }) writegroup.Go(func() error { return write(ctx2, out, region, "saddle.json", saddles) }) writegroup.Go(func() error { return write(ctx2, out, region, "scenario.json", scenarios) }) writegroup.Go(func() error { return write(ctx2, out, region, "spark.json", sparks) }) writegroup.Go(func() error { return write(ctx2, out, region, "conversation.json", convos) }) if err := writegroup.Wait(); err != nil { slog.ErrorContext(ctx, "write", slog.Any("err", err)) os.Exit(1) } slog.InfoContext(ctx, "done") } func load[T any](ctx context.Context, group *errgroup.Group, db *sqlitex.Pool, kind string, get func(context.Context, *sqlitex.Pool) ([]T, error)) func() ([]T, error) { slog.InfoContext(ctx, "load", slog.String("kind", kind)) var r []T group.Go(func() error { got, err := get(ctx, db) r = got return err }) return func() ([]T, error) { err := group.Wait() if err == context.Canceled { // After the first wait, all future ones return context.Canceled. // We want to be able to wait any number of times, so hide it. err = nil } return r, err } } func write[T any](ctx context.Context, out, region, name string, v func() (T, error)) error { p := filepath.Join(out, region, name) r, err := v() if err != nil { return err } slog.InfoContext(ctx, "write", slog.String("path", p)) f, err := os.Create(p) if err != nil { return err } defer f.Close() w := bufio.NewWriter(f) enc := json.NewEncoder(w) enc.SetEscapeHTML(false) enc.SetIndent("", "\t") err = enc.Encode(r) err = errors.Join(err, w.Flush()) slog.InfoContext(ctx, "marshaled", slog.String("path", p)) return err }