cmd/horsegen: generate using package mdb

This commit is contained in:
2026-06-02 13:39:10 -04:00
parent 2a4da0a73e
commit 20db674d25
2 changed files with 104 additions and 417 deletions

View File

@@ -2,44 +2,38 @@ package main
import (
"bufio"
"cmp"
"context"
_ "embed"
"encoding/json"
"errors"
"flag"
"fmt"
"log/slog"
"maps"
"os"
"os/signal"
"path/filepath"
"slices"
"strconv"
"strings"
"golang.org/x/sync/errgroup"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
"git.sunturtle.xyz/zephyr/horse"
"git.sunturtle.xyz/zephyr/horse/mdb"
)
func main() {
var (
mdb string
mdbf string
out string
region string
)
flag.StringVar(&mdb, "mdb", os.ExpandEnv(`$USERPROFILE\AppData\LocalLow\Cygames\Umamusume\master\master.mdb`), "`path` to Umamusume master.mdb")
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(&region, "region", "global", "region the database is for (global, jp)")
flag.Parse()
slog.Info("open", slog.String("mdb", mdb))
db, err := sqlitex.NewPool(mdb, sqlitex.PoolOptions{Flags: sqlite.OpenReadOnly})
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", mdb), slog.Any("err", err))
slog.Error("opening mdb", slog.String("mdb", mdbf), slog.Any("err", err))
os.Exit(1)
}
@@ -50,193 +44,16 @@ func main() {
}()
loadgroup, ctx1 := errgroup.WithContext(ctx)
charas := load(ctx1, loadgroup, db, "characters", characterSQL, func(s *sqlite.Stmt) horse.Character {
return horse.Character{
ID: horse.CharacterID(s.ColumnInt(0)),
Name: s.ColumnText(1),
}
})
aff := load(ctx1, loadgroup, db, "pair affinity", affinitySQL, func(s *sqlite.Stmt) horse.AffinityRelation {
return horse.AffinityRelation{
IDA: s.ColumnInt(0),
IDB: s.ColumnInt(1),
IDC: s.ColumnInt(2),
Affinity: s.ColumnInt(3),
}
})
umas := load(ctx1, loadgroup, db, "umas", umaSQL, func(s *sqlite.Stmt) horse.Uma {
return horse.Uma{
ID: horse.UmaID(s.ColumnInt(0)),
CharacterID: horse.CharacterID(s.ColumnInt(1)),
Name: s.ColumnText(2),
Variant: s.ColumnText(3),
Sprint: horse.AptitudeLevel(s.ColumnInt(4)),
Mile: horse.AptitudeLevel(s.ColumnInt(6)),
Medium: horse.AptitudeLevel(s.ColumnInt(7)),
Long: horse.AptitudeLevel(s.ColumnInt(8)),
Front: horse.AptitudeLevel(s.ColumnInt(9)),
Pace: horse.AptitudeLevel(s.ColumnInt(10)),
Late: horse.AptitudeLevel(s.ColumnInt(11)),
End: horse.AptitudeLevel(s.ColumnInt(12)),
Turf: horse.AptitudeLevel(s.ColumnInt(13)),
Dirt: horse.AptitudeLevel(s.ColumnInt(14)),
Unique: horse.SkillID(s.ColumnInt(15)),
Skill1: horse.SkillID(s.ColumnInt(16)),
Skill2: horse.SkillID(s.ColumnInt(17)),
Skill3: horse.SkillID(s.ColumnInt(18)),
SkillPL2: horse.SkillID(s.ColumnInt(19)),
SkillPL3: horse.SkillID(s.ColumnInt(20)),
SkillPL4: horse.SkillID(s.ColumnInt(21)),
SkillPL5: horse.SkillID(s.ColumnInt(22)),
}
})
sg := load(ctx1, loadgroup, db, "skill groups", skillGroupSQL, func(s *sqlite.Stmt) horse.SkillGroup {
return horse.SkillGroup{
ID: horse.SkillGroupID(s.ColumnInt(0)),
Skill1: horse.SkillID(s.ColumnInt(1)),
Skill2: horse.SkillID(s.ColumnInt(2)),
Skill3: horse.SkillID(s.ColumnInt(3)),
SkillBad: horse.SkillID(s.ColumnInt(4)),
}
})
skills := load(ctx1, loadgroup, db, "skills", skillSQL, func(s *sqlite.Stmt) horse.Skill {
return horse.Skill{
ID: horse.SkillID(s.ColumnInt(0)),
Name: s.ColumnText(1),
Description: s.ColumnText(2),
Group: horse.SkillGroupID(s.ColumnInt32(3)),
Rarity: int8(s.ColumnInt(5)),
GroupRate: int8(s.ColumnInt(6)),
GradeValue: s.ColumnInt32(7),
WitCheck: s.ColumnBool(8),
Activations: trimActivations([]horse.Activation{
{
Precondition: s.ColumnText(9),
Condition: s.ColumnText(10),
Duration: horse.TenThousandths(s.ColumnInt(11)),
DurScale: horse.DurScale(s.ColumnInt(12)),
Cooldown: horse.TenThousandths(s.ColumnInt(13)),
Abilities: trimAbilities([]horse.Ability{
{
Type: horse.AbilityType(s.ColumnInt(14)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(15)),
Value: horse.TenThousandths(s.ColumnInt(16)),
Target: horse.AbilityTarget(s.ColumnInt(17)),
TargetValue: s.ColumnInt32(18),
},
{
Type: horse.AbilityType(s.ColumnInt(19)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(20)),
Value: horse.TenThousandths(s.ColumnInt(21)),
Target: horse.AbilityTarget(s.ColumnInt(22)),
TargetValue: s.ColumnInt32(23),
},
{
Type: horse.AbilityType(s.ColumnInt(24)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(25)),
Value: horse.TenThousandths(s.ColumnInt(26)),
Target: horse.AbilityTarget(s.ColumnInt(27)),
TargetValue: s.ColumnInt32(28),
},
}),
},
{
Precondition: s.ColumnText(29),
Condition: s.ColumnText(30),
Duration: horse.TenThousandths(s.ColumnInt(31)),
DurScale: horse.DurScale(s.ColumnInt(32)),
Cooldown: horse.TenThousandths(s.ColumnInt(33)),
Abilities: trimAbilities([]horse.Ability{
{
Type: horse.AbilityType(s.ColumnInt(34)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(35)),
Value: horse.TenThousandths(s.ColumnInt(36)),
Target: horse.AbilityTarget(s.ColumnInt(37)),
TargetValue: s.ColumnInt32(38),
},
{
Type: horse.AbilityType(s.ColumnInt(39)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(40)),
Value: horse.TenThousandths(s.ColumnInt(41)),
Target: horse.AbilityTarget(s.ColumnInt(42)),
TargetValue: s.ColumnInt32(43),
},
{
Type: horse.AbilityType(s.ColumnInt(44)),
ValueUsage: horse.AbilityValueUsage(s.ColumnInt(45)),
Value: horse.TenThousandths(s.ColumnInt(46)),
Target: horse.AbilityTarget(s.ColumnInt(47)),
TargetValue: s.ColumnInt32(48),
},
}),
},
}),
UniqueOwner: s.ColumnText(52), // TODO(zeph): should be id, not name
Tags: parseTags(s.ColumnText(54)),
SPCost: s.ColumnInt(49),
IconID: s.ColumnInt(53),
}
})
races := load(ctx1, loadgroup, db, "races", raceSQL, func(s *sqlite.Stmt) horse.Race {
return horse.Race{
ID: horse.RaceID(s.ColumnInt(0)),
Name: s.ColumnText(1),
// TODO(zeph): grade
Thumbnail: s.ColumnInt(3),
Primary: horse.RaceID(s.ColumnInt(4)),
}
})
saddles := load(ctx1, loadgroup, db, "saddles", saddleSQL, func(s *sqlite.Stmt) horse.Saddle {
return horse.Saddle{
ID: horse.SaddleID(s.ColumnInt(0)),
Name: s.ColumnText(1),
Races: trimZeros(
horse.RaceID(s.ColumnInt(2)),
horse.RaceID(s.ColumnInt(3)),
horse.RaceID(s.ColumnInt(4)),
),
Type: horse.SaddleType(s.ColumnInt(5)),
Primary: horse.SaddleID(s.ColumnInt(6)),
}
})
scenarios := load(ctx1, loadgroup, db, "scenarios", scenarioSQL, func(s *sqlite.Stmt) horse.Scenario {
return horse.Scenario{
ID: horse.ScenarioID(s.ColumnInt(0)),
Name: s.ColumnText(1),
Title: s.ColumnText(2),
}
})
sparks := load(ctx1, loadgroup, db, "sparks", sparkSQL, func(s *sqlite.Stmt) horse.Spark {
return horse.Spark{
ID: horse.SparkID(s.ColumnInt(0)),
Name: s.ColumnText(1),
Description: s.ColumnText(2),
Group: horse.SparkGroupID(s.ColumnInt(3)),
Rarity: horse.SparkRarity(s.ColumnInt(4)),
Type: horse.SparkType(s.ColumnInt(5)),
// Effects filled in later.
}
})
sparkeffs := load(ctx1, loadgroup, db, "spark effects", sparkEffectSQL, func(s *sqlite.Stmt) SparkEffImm {
return SparkEffImm{
Group: horse.SparkGroupID(s.ColumnInt(0)),
Effect: s.ColumnInt(1),
Target: horse.SparkTarget(s.ColumnInt(2)),
Value1: s.ColumnInt32(3),
Value2: s.ColumnInt32(4),
}
})
convos := load(ctx1, loadgroup, db, "lobby conversations", conversationSQL, func(s *sqlite.Stmt) horse.Conversation {
return horse.Conversation{
CharacterID: horse.CharacterID(s.ColumnInt(0)),
Number: s.ColumnInt(1),
Location: horse.LobbyConversationLocationID(s.ColumnInt(2)),
Chara1: horse.CharacterID(s.ColumnInt(3)),
Chara2: horse.CharacterID(s.ColumnInt(4)),
Chara3: horse.CharacterID(s.ColumnInt(5)),
ConditionType: s.ColumnInt(6),
}
})
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))
@@ -252,7 +69,7 @@ func main() {
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", mergesparks(sparks, sparkeffs)) })
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))
@@ -262,56 +79,13 @@ func main() {
slog.InfoContext(ctx, "done")
}
var (
//go:embed sql/character.sql
characterSQL string
//go:embed sql/affinity.sql
affinitySQL string
//go:embed sql/uma.sql
umaSQL string
//go:embed sql/skill-group.sql
skillGroupSQL string
//go:embed sql/skill.sql
skillSQL string
//go:embed sql/race.sql
raceSQL string
//go:embed sql/saddle.sql
saddleSQL string
//go:embed sql/scenario.sql
scenarioSQL string
//go:embed sql/spark.sql
sparkSQL string
//go:embed sql/spark-effect.sql
sparkEffectSQL string
//go:embed sql/conversation.sql
conversationSQL string
)
func load[T any](ctx context.Context, group *errgroup.Group, db *sqlitex.Pool, kind, sql string, row func(*sqlite.Stmt) T) func() ([]T, error) {
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 {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return fmt.Errorf("couldn't get connection for %s: %w", kind, err)
}
stmt, _, err := conn.PrepareTransient(sql)
if err != nil {
return fmt.Errorf("couldn't prepare statement for %s: %w", kind, err)
}
for {
ok, err := stmt.Step()
if err != nil {
return fmt.Errorf("error stepping %s: %w", kind, err)
}
if !ok {
break
}
r = append(r, row(stmt))
}
return nil
got, err := get(ctx, db)
r = got
return err
})
return func() ([]T, error) {
err := group.Wait()
@@ -345,90 +119,3 @@ func write[T any](ctx context.Context, out, region, name string, v func() (T, er
slog.InfoContext(ctx, "marshaled", slog.String("path", p))
return err
}
func mergesparks(sparks func() ([]horse.Spark, error), effs func() ([]SparkEffImm, error)) func() ([]horse.Spark, error) {
return func() ([]horse.Spark, error) {
sp, err := sparks()
if err != nil {
return nil, err
}
ef, err := effs()
if err != nil {
return nil, err
}
// Spark effects are sorted by group ID, but groups apply to multiple
// sparks, and we don't rely on sparks and groups being in the same order.
// It is possible to merge in linear time, but not worth the effort:
// n log n is fine since this is an AOT step.
for i := range sp {
k, ok := slices.BinarySearchFunc(ef, sp[i].Group, func(e SparkEffImm, v horse.SparkGroupID) int { return cmp.Compare(e.Group, v) })
if !ok {
panic(fmt.Errorf("mergesparks: no spark group for %+v", &sp[i]))
}
// Back up to the first effect in the group.
for k > 0 && ef[k-1].Group == sp[i].Group {
k--
}
// Map effect IDs to the lists of their effects.
m := make(map[int][]horse.SparkEffect)
for _, e := range ef[k:] {
if e.Group != sp[i].Group {
// Done with this group.
break
}
m[e.Effect] = append(m[e.Effect], horse.SparkEffect{Target: e.Target, Value1: e.Value1, Value2: e.Value2})
}
// Now get effects in order.
keys := slices.Sorted(maps.Keys(m))
sp[i].Effects = make([][]horse.SparkEffect, 0, len(keys))
for _, key := range keys {
sp[i].Effects = append(sp[i].Effects, m[key])
}
}
return sp, nil
}
}
type SparkEffImm struct {
Group horse.SparkGroupID
Effect int
Target horse.SparkTarget
Value1 int32
Value2 int32
}
func parseTags(s string) []uint16 {
r := make([]uint16, 0, 8)
for s != "" {
t, u, _ := strings.Cut(s, "/")
s = u
v, err := strconv.ParseUint(t, 10, 16)
if err != nil {
panic(fmt.Errorf("parsing skill tags: %w", err))
}
r = append(r, uint16(v))
}
return trimZeros(r...)
}
func trimAbilities(s []horse.Ability) []horse.Ability {
for len(s) > 0 && s[len(s)-1].Type == 0 {
s = s[:len(s)-1]
}
return s
}
func trimActivations(s []horse.Activation) []horse.Activation {
for len(s) > 0 && s[len(s)-1].Condition == "" {
s = s[:len(s)-1]
}
return s
}
func trimZeros[T comparable](s ...T) []T {
var zero T
for len(s) > 0 && s[len(s)-1] == zero {
s = s[:len(s)-1]
}
return s
}

View File

@@ -4,7 +4,7 @@
"chara_id": 1001,
"name": "[Special Dreamer] Special Week",
"variant": "[Special Dreamer]",
"sprint": 0,
"sprint": 2,
"mile": 5,
"medium": 7,
"long": 7,
@@ -28,7 +28,7 @@
"chara_id": 1001,
"name": "[Hopp'n♪Happy Heart] Special Week",
"variant": "[Hopp'n♪Happy Heart]",
"sprint": 0,
"sprint": 2,
"mile": 5,
"medium": 7,
"long": 7,
@@ -52,7 +52,7 @@
"chara_id": 1002,
"name": "[Innocent Silence] Silence Suzuka",
"variant": "[Innocent Silence]",
"sprint": 0,
"sprint": 4,
"mile": 7,
"medium": 7,
"long": 3,
@@ -76,7 +76,7 @@
"chara_id": 1003,
"name": "[Peak Joy] Tokai Teio",
"variant": "[Peak Joy]",
"sprint": 0,
"sprint": 2,
"mile": 3,
"medium": 7,
"long": 6,
@@ -100,7 +100,7 @@
"chara_id": 1003,
"name": "[Beyond the Horizon] Tokai Teio",
"variant": "[Beyond the Horizon]",
"sprint": 0,
"sprint": 2,
"mile": 3,
"medium": 7,
"long": 6,
@@ -124,7 +124,7 @@
"chara_id": 1004,
"name": "[Formula R] Maruzensky",
"variant": "[Formula R]",
"sprint": 0,
"sprint": 6,
"mile": 7,
"medium": 6,
"long": 5,
@@ -148,7 +148,7 @@
"chara_id": 1004,
"name": "[Hot☆Summer Night] Maruzensky",
"variant": "[Hot☆Summer Night]",
"sprint": 0,
"sprint": 6,
"mile": 7,
"medium": 6,
"long": 5,
@@ -172,7 +172,7 @@
"chara_id": 1005,
"name": "[Shooting Star Revue] Fuji Kiseki",
"variant": "[Shooting Star Revue]",
"sprint": 0,
"sprint": 6,
"mile": 7,
"medium": 6,
"long": 3,
@@ -196,7 +196,7 @@
"chara_id": 1005,
"name": "[Succès Étoilé] Fuji Kiseki",
"variant": "[Succès Étoilé]",
"sprint": 0,
"sprint": 6,
"mile": 7,
"medium": 6,
"long": 3,
@@ -220,7 +220,7 @@
"chara_id": 1006,
"name": "[Starlight Beat] Oguri Cap",
"variant": "[Starlight Beat]",
"sprint": 0,
"sprint": 3,
"mile": 7,
"medium": 7,
"long": 6,
@@ -244,7 +244,7 @@
"chara_id": 1006,
"name": "[Ashen Miracle] Oguri Cap",
"variant": "[Ashen Miracle]",
"sprint": 0,
"sprint": 3,
"mile": 7,
"medium": 7,
"long": 6,
@@ -268,7 +268,7 @@
"chara_id": 1007,
"name": "[Red Strife] Gold Ship",
"variant": "[Red Strife]",
"sprint": 0,
"sprint": 1,
"mile": 5,
"medium": 7,
"long": 7,
@@ -292,7 +292,7 @@
"chara_id": 1008,
"name": "[Wild Top Gear] Vodka",
"variant": "[Wild Top Gear]",
"sprint": 0,
"sprint": 2,
"mile": 7,
"medium": 7,
"long": 2,
@@ -316,7 +316,7 @@
"chara_id": 1009,
"name": "[Peak Blue] Daiwa Scarlet",
"variant": "[Peak Blue]",
"sprint": 0,
"sprint": 2,
"mile": 7,
"medium": 7,
"long": 6,
@@ -340,7 +340,7 @@
"chara_id": 1010,
"name": "[Wild Frontier] Taiki Shuttle",
"variant": "[Wild Frontier]",
"sprint": 0,
"sprint": 7,
"mile": 7,
"medium": 3,
"long": 1,
@@ -364,7 +364,7 @@
"chara_id": 1011,
"name": "[Stone-Piercing Blue] Grass Wonder",
"variant": "[Stone-Piercing Blue]",
"sprint": 0,
"sprint": 1,
"mile": 7,
"medium": 6,
"long": 7,
@@ -388,7 +388,7 @@
"chara_id": 1011,
"name": "[Saintly Jade Cleric] Grass Wonder",
"variant": "[Saintly Jade Cleric ]",
"sprint": 0,
"sprint": 1,
"mile": 7,
"medium": 6,
"long": 7,
@@ -412,7 +412,7 @@
"chara_id": 1012,
"name": "[Azure Amazon] Hishi Amazon",
"variant": "[Azure Amazon]",
"sprint": 0,
"sprint": 4,
"mile": 7,
"medium": 7,
"long": 6,
@@ -436,7 +436,7 @@
"chara_id": 1013,
"name": "[Frontline Elegance] Mejiro McQueen",
"variant": "[Frontline Elegance]",
"sprint": 0,
"sprint": 1,
"mile": 2,
"medium": 7,
"long": 7,
@@ -460,7 +460,7 @@
"chara_id": 1013,
"name": "[End of the Skies] Mejiro McQueen",
"variant": "[End of the Skies]",
"sprint": 0,
"sprint": 1,
"mile": 2,
"medium": 7,
"long": 7,
@@ -484,7 +484,7 @@
"chara_id": 1014,
"name": "[El☆Número 1] El Condor Pasa",
"variant": "[El☆Número 1]",
"sprint": 0,
"sprint": 2,
"mile": 7,
"medium": 7,
"long": 6,
@@ -508,7 +508,7 @@
"chara_id": 1014,
"name": "[Kukulkan Warrior] El Condor Pasa",
"variant": "[Kukulkan Warrior]",
"sprint": 0,
"sprint": 2,
"mile": 7,
"medium": 7,
"long": 6,
@@ -532,7 +532,7 @@
"chara_id": 1015,
"name": "[O Sole Suo!] T.M. Opera O",
"variant": "[O Sole Suo!]",
"sprint": 0,
"sprint": 1,
"mile": 3,
"medium": 7,
"long": 7,
@@ -556,7 +556,7 @@
"chara_id": 1015,
"name": "[New Year, Same Radiance!] T.M. Opera O",
"variant": "[New Year, Same Radiance!]",
"sprint": 0,
"sprint": 1,
"mile": 3,
"medium": 7,
"long": 7,
@@ -580,7 +580,7 @@
"chara_id": 1016,
"name": "[Maverick] Narita Brian",
"variant": "[Maverick]",
"sprint": 0,
"sprint": 2,
"mile": 6,
"medium": 7,
"long": 7,
@@ -604,7 +604,7 @@
"chara_id": 1017,
"name": "[Emperor's Path] Symboli Rudolf",
"variant": "[Emperor's Path]",
"sprint": 0,
"sprint": 3,
"mile": 5,
"medium": 7,
"long": 7,
@@ -628,7 +628,7 @@
"chara_id": 1017,
"name": "[Archer by Moonlight] Symboli Rudolf",
"variant": "[Archer by Moonlight]",
"sprint": 0,
"sprint": 3,
"mile": 5,
"medium": 7,
"long": 7,
@@ -652,7 +652,7 @@
"chara_id": 1018,
"name": "[Empress Road] Air Groove",
"variant": "[Empress Road]",
"sprint": 0,
"sprint": 5,
"mile": 6,
"medium": 7,
"long": 3,
@@ -676,7 +676,7 @@
"chara_id": 1018,
"name": "[Quercus Civilis] Air Groove",
"variant": "[Quercus Civilis]",
"sprint": 0,
"sprint": 5,
"mile": 6,
"medium": 7,
"long": 3,
@@ -700,7 +700,7 @@
"chara_id": 1019,
"name": "[Full-Color Fangirling] Agnes Digital",
"variant": "[Full-Color Fangirling]",
"sprint": 0,
"sprint": 2,
"mile": 7,
"medium": 7,
"long": 1,
@@ -724,7 +724,7 @@
"chara_id": 1020,
"name": "[Reeling in the Big One] Seiun Sky",
"variant": "[Reeling in the Big One]",
"sprint": 0,
"sprint": 1,
"mile": 5,
"medium": 7,
"long": 7,
@@ -748,7 +748,7 @@
"chara_id": 1020,
"name": "[Soirée des Chatons] Seiun Sky",
"variant": "[Soirée des Chatons]",
"sprint": 0,
"sprint": 1,
"mile": 5,
"medium": 7,
"long": 7,
@@ -772,7 +772,7 @@
"chara_id": 1021,
"name": "[Fast as Lightning] Tamamo Cross",
"variant": "[Fast as Lightning]",
"sprint": 0,
"sprint": 1,
"mile": 3,
"medium": 7,
"long": 7,
@@ -796,7 +796,7 @@
"chara_id": 1022,
"name": "[Noble Seamair] Fine Motion",
"variant": "[Noble Seamair]",
"sprint": 0,
"sprint": 2,
"mile": 7,
"medium": 7,
"long": 5,
@@ -820,7 +820,7 @@
"chara_id": 1022,
"name": "[Titania] Fine Motion",
"variant": "[Titania]",
"sprint": 0,
"sprint": 2,
"mile": 7,
"medium": 7,
"long": 5,
@@ -844,7 +844,7 @@
"chara_id": 1023,
"name": "[pf. Winning Equation...] Biwa Hayahide",
"variant": "[pf. Winning Equation...]",
"sprint": 0,
"sprint": 2,
"mile": 5,
"medium": 7,
"long": 7,
@@ -868,7 +868,7 @@
"chara_id": 1023,
"name": "[Rouge Caroler] Biwa Hayahide",
"variant": "[Rouge Caroler]",
"sprint": 0,
"sprint": 2,
"mile": 5,
"medium": 7,
"long": 7,
@@ -892,7 +892,7 @@
"chara_id": 1024,
"name": "[Scramble☆Zone] Mayano Top Gun",
"variant": "[Scramble☆Zone]",
"sprint": 0,
"sprint": 4,
"mile": 4,
"medium": 7,
"long": 7,
@@ -916,7 +916,7 @@
"chara_id": 1024,
"name": "[Sunlight Bouquet] Mayano Top Gun",
"variant": "[Sunlight Bouquet]",
"sprint": 0,
"sprint": 4,
"mile": 4,
"medium": 7,
"long": 7,
@@ -940,7 +940,7 @@
"chara_id": 1025,
"name": "[Creeping Shadow] Manhattan Cafe",
"variant": "[Creeping Shadow]",
"sprint": 0,
"sprint": 1,
"mile": 2,
"medium": 6,
"long": 7,
@@ -964,7 +964,7 @@
"chara_id": 1026,
"name": "[MB-19890425] Mihono Bourbon",
"variant": "[MB-19890425]",
"sprint": 0,
"sprint": 5,
"mile": 6,
"medium": 7,
"long": 6,
@@ -988,7 +988,7 @@
"chara_id": 1026,
"name": "[CODE: ICING] Mihono Bourbon",
"variant": "[CODE: ICING]",
"sprint": 0,
"sprint": 5,
"mile": 6,
"medium": 7,
"long": 6,
@@ -1012,7 +1012,7 @@
"chara_id": 1027,
"name": "[Down the Line] Mejiro Ryan",
"variant": "[Down the Line]",
"sprint": 0,
"sprint": 3,
"mile": 5,
"medium": 7,
"long": 6,
@@ -1036,7 +1036,7 @@
"chara_id": 1028,
"name": "[Buono ☆ Alla Moda] Hishi Akebono",
"variant": "[Buono ☆ Alla Moda]",
"sprint": 0,
"sprint": 7,
"mile": 6,
"medium": 2,
"long": 1,
@@ -1060,7 +1060,7 @@
"chara_id": 1030,
"name": "[Rosy Dreams] Rice Shower",
"variant": "[Rosy Dreams]",
"sprint": 0,
"sprint": 3,
"mile": 5,
"medium": 7,
"long": 7,
@@ -1084,7 +1084,7 @@
"chara_id": 1030,
"name": "[Vampire Makeover!] Rice Shower",
"variant": "[Vampire Makeover!]",
"sprint": 0,
"sprint": 3,
"mile": 5,
"medium": 7,
"long": 7,
@@ -1108,7 +1108,7 @@
"chara_id": 1031,
"name": "[Always Electrifying] Ines Fujin",
"variant": "[Always Electrifying]",
"sprint": 0,
"sprint": 1,
"mile": 7,
"medium": 7,
"long": 5,
@@ -1132,7 +1132,7 @@
"chara_id": 1032,
"name": "[Tach-nology] Agnes Tachyon",
"variant": "[Tach-nology]",
"sprint": 0,
"sprint": 1,
"mile": 4,
"medium": 7,
"long": 6,
@@ -1156,7 +1156,7 @@
"chara_id": 1033,
"name": "[Starry Nocturne] Admire Vega",
"variant": "[Starry Nocturne]",
"sprint": 0,
"sprint": 2,
"mile": 5,
"medium": 7,
"long": 5,
@@ -1180,7 +1180,7 @@
"chara_id": 1034,
"name": "[Edomurasaki] Inari One",
"variant": "[Edomurasaki]",
"sprint": 0,
"sprint": 2,
"mile": 6,
"medium": 7,
"long": 7,
@@ -1204,7 +1204,7 @@
"chara_id": 1035,
"name": "[Get to Winning!] Winning Ticket",
"variant": "[Get to Winning!]",
"sprint": 0,
"sprint": 1,
"mile": 2,
"medium": 7,
"long": 6,
@@ -1228,7 +1228,7 @@
"chara_id": 1037,
"name": "[Meisterschaft] Eishin Flash",
"variant": "[Meisterschaft]",
"sprint": 0,
"sprint": 1,
"mile": 2,
"medium": 7,
"long": 7,
@@ -1252,7 +1252,7 @@
"chara_id": 1037,
"name": "[Precise Chocolatier] Eishin Flash",
"variant": "[Precise Chocolatier]",
"sprint": 0,
"sprint": 1,
"mile": 2,
"medium": 7,
"long": 7,
@@ -1276,7 +1276,7 @@
"chara_id": 1038,
"name": "[Fille Éclair] Curren Chan",
"variant": "[Fille Éclair]",
"sprint": 0,
"sprint": 7,
"mile": 4,
"medium": 1,
"long": 1,
@@ -1300,7 +1300,7 @@
"chara_id": 1038,
"name": "[Ma Chérie of the New Moon] Curren Chan",
"variant": "[Ma Chérie of the New Moon]",
"sprint": 0,
"sprint": 7,
"mile": 4,
"medium": 1,
"long": 1,
@@ -1324,7 +1324,7 @@
"chara_id": 1039,
"name": "[Princess of Pink] Kawakami Princess",
"variant": "[Princess of Pink]",
"sprint": 0,
"sprint": 4,
"mile": 6,
"medium": 7,
"long": 2,
@@ -1348,7 +1348,7 @@
"chara_id": 1040,
"name": "[Authentic / 1928] Gold City",
"variant": "[Authentic / 1928]",
"sprint": 0,
"sprint": 2,
"mile": 7,
"medium": 6,
"long": 6,
@@ -1372,7 +1372,7 @@
"chara_id": 1040,
"name": "[Autumn Cosmos] Gold City",
"variant": "[Autumn Cosmos]",
"sprint": 0,
"sprint": 2,
"mile": 7,
"medium": 6,
"long": 6,
@@ -1396,7 +1396,7 @@
"chara_id": 1041,
"name": "[Blossom in Learning] Sakura Bakushin O",
"variant": "[Blossom in Learning]",
"sprint": 0,
"sprint": 7,
"mile": 6,
"medium": 1,
"long": 1,
@@ -1444,7 +1444,7 @@
"chara_id": 1045,
"name": "[Murmuring Stream] Super Creek",
"variant": "[Murmuring Stream]",
"sprint": 0,
"sprint": 1,
"mile": 1,
"medium": 7,
"long": 7,
@@ -1468,7 +1468,7 @@
"chara_id": 1045,
"name": "[Chiffon-Wrapped Mummy] Super Creek",
"variant": "[Chiffon-Wrapped Mummy]",
"sprint": 0,
"sprint": 1,
"mile": 1,
"medium": 7,
"long": 7,
@@ -1492,7 +1492,7 @@
"chara_id": 1046,
"name": "[LOVE☆4EVER] Smart Falcon",
"variant": "[LOVE☆4EVER]",
"sprint": 0,
"sprint": 6,
"mile": 7,
"medium": 7,
"long": 3,
@@ -1516,7 +1516,7 @@
"chara_id": 1048,
"name": "[Jokester ☆ Vibes] Tosen Jordan",
"variant": "[Jokester ☆ Vibes]",
"sprint": 0,
"sprint": 1,
"mile": 2,
"medium": 7,
"long": 6,
@@ -1540,7 +1540,7 @@
"chara_id": 1050,
"name": "[Nevertheless] Narita Taishin",
"variant": "[Nevertheless]",
"sprint": 0,
"sprint": 2,
"mile": 4,
"medium": 7,
"long": 7,
@@ -1564,7 +1564,7 @@
"chara_id": 1051,
"name": "[Layered Petals] Nishino Flower",
"variant": "[Layered Petals]",
"sprint": 0,
"sprint": 7,
"mile": 7,
"medium": 3,
"long": 1,
@@ -1588,7 +1588,7 @@
"chara_id": 1052,
"name": "[Bestest Prize ♪] Haru Urara",
"variant": "[Bestest Prize ♪]",
"sprint": 0,
"sprint": 7,
"mile": 6,
"medium": 1,
"long": 1,
@@ -1612,7 +1612,7 @@
"chara_id": 1052,
"name": "[New Year ♪ New Urara!] Haru Urara",
"variant": "[New Year ♪ New Urara!]",
"sprint": 0,
"sprint": 7,
"mile": 7,
"medium": 1,
"long": 1,
@@ -1636,7 +1636,7 @@
"chara_id": 1056,
"name": "[Rising☆Fortune] Matikanefukukitaru",
"variant": "[Rising☆Fortune]",
"sprint": 0,
"sprint": 2,
"mile": 5,
"medium": 7,
"long": 7,
@@ -1660,7 +1660,7 @@
"chara_id": 1056,
"name": "[Lucky Tidings] Matikanefukukitaru",
"variant": "[Lucky Tidings]",
"sprint": 0,
"sprint": 2,
"mile": 5,
"medium": 7,
"long": 7,
@@ -1684,7 +1684,7 @@
"chara_id": 1058,
"name": "[Turbulent Blue] Meisho Doto",
"variant": "[Turbulent Blue]",
"sprint": 0,
"sprint": 1,
"mile": 2,
"medium": 7,
"long": 7,
@@ -1708,7 +1708,7 @@
"chara_id": 1059,
"name": "[Off the Line] Mejiro Dober",
"variant": "[Off the Line]",
"sprint": 0,
"sprint": 3,
"mile": 7,
"medium": 7,
"long": 2,
@@ -1732,7 +1732,7 @@
"chara_id": 1060,
"name": "[Poinsettia Ribbon] Nice Nature",
"variant": "[Poinsettia Ribbon]",
"sprint": 0,
"sprint": 1,
"mile": 5,
"medium": 7,
"long": 7,
@@ -1756,7 +1756,7 @@
"chara_id": 1060,
"name": "[Run & Win] Nice Nature",
"variant": "[Run & Win]",
"sprint": 0,
"sprint": 1,
"mile": 5,
"medium": 7,
"long": 7,
@@ -1780,7 +1780,7 @@
"chara_id": 1061,
"name": "[King of Emeralds] King Halo",
"variant": "[King of Emeralds]",
"sprint": 0,
"sprint": 7,
"mile": 6,
"medium": 6,
"long": 5,
@@ -1804,7 +1804,7 @@
"chara_id": 1061,
"name": "[Cheerleader in Noble White] King Halo",
"variant": "[Cheerleader in Noble White]",
"sprint": 0,
"sprint": 7,
"mile": 6,
"medium": 6,
"long": 5,
@@ -1828,7 +1828,7 @@
"chara_id": 1062,
"name": "[Clippety-Tippety-Clop] Matikanetannhauser",
"variant": "[Clippety-Tippety-Clop]",
"sprint": 0,
"sprint": 1,
"mile": 4,
"medium": 7,
"long": 7,
@@ -1852,7 +1852,7 @@
"chara_id": 1064,
"name": "[Line Breakthrough] Mejiro Palmer",
"variant": "[Line Breakthrough]",
"sprint": 0,
"sprint": 1,
"mile": 2,
"medium": 7,
"long": 7,
@@ -1876,7 +1876,7 @@
"chara_id": 1067,
"name": "[Natural Brilliance] Satono Diamond",
"variant": "[Natural Brilliance]",
"sprint": 0,
"sprint": 1,
"mile": 5,
"medium": 7,
"long": 7,
@@ -1900,7 +1900,7 @@
"chara_id": 1068,
"name": "[Gilded Shrine to Glory] Kitasan Black",
"variant": "[Gilded Shrine to Glory]",
"sprint": 0,
"sprint": 3,
"mile": 5,
"medium": 7,
"long": 7,
@@ -1924,7 +1924,7 @@
"chara_id": 1069,
"name": "[Strength in Full Bloom] Sakura Chiyono O",
"variant": "[Strength in Full Bloom]",
"sprint": 0,
"sprint": 3,
"mile": 7,
"medium": 7,
"long": 3,
@@ -1948,7 +1948,7 @@
"chara_id": 1071,
"name": "[Crystalline] Mejiro Ardan",
"variant": "[Crystalline]",
"sprint": 0,
"sprint": 3,
"mile": 6,
"medium": 7,
"long": 4,
@@ -1972,7 +1972,7 @@
"chara_id": 1072,
"name": "[Blazed Head, Covered Fists] Yaeno Muteki",
"variant": "[Blazed Head, Covered Fists]",
"sprint": 0,
"sprint": 1,
"mile": 6,
"medium": 7,
"long": 3,
@@ -1996,7 +1996,7 @@
"chara_id": 1074,
"name": "[Brunissage Line] Mejiro Bright",
"variant": "[Brunissage Line]",
"sprint": 0,
"sprint": 2,
"mile": 5,
"medium": 7,
"long": 7,