cmd/racedb: program to load races into an sqlite db
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
|
||||
"zombiezen.com/go/sqlite"
|
||||
"zombiezen.com/go/sqlite/sqlitemigration"
|
||||
"zombiezen.com/go/sqlite/sqlitex"
|
||||
|
||||
"git.sunturtle.xyz/zephyr/horse/umadump/replay"
|
||||
)
|
||||
|
||||
const appID int32 = 0x4acedb
|
||||
|
||||
//go:embed schema-00.sql
|
||||
var schema0 string
|
||||
|
||||
//go:embed schema-final.sql
|
||||
var schemaFinal string
|
||||
|
||||
func opendb(uri, journalMode string) *sqlitemigration.Pool {
|
||||
schema := sqlitemigration.Schema{
|
||||
Migrations: []string{
|
||||
schema0,
|
||||
},
|
||||
AppID: appID,
|
||||
RepeatableMigration: schemaFinal,
|
||||
}
|
||||
j := "PRAGMA journal_mode=" + journalMode
|
||||
opts := sqlitemigration.Options{
|
||||
Flags: sqlite.OpenReadWrite | sqlite.OpenCreate | sqlite.OpenURI,
|
||||
PrepareConn: func(conn *sqlite.Conn) error {
|
||||
if journalMode == "" {
|
||||
return nil
|
||||
}
|
||||
stmt, _, err := conn.PrepareTransient(j)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = stmt.Step()
|
||||
return errors.Join(err, stmt.Finalize())
|
||||
},
|
||||
}
|
||||
db := sqlitemigration.NewPool(uri, schema, opts)
|
||||
return db
|
||||
}
|
||||
|
||||
func insert(ctx context.Context, db *sqlitemigration.Pool, log *slog.Logger, race *replay.Race, simdata *replay.RaceSimulateData) (err error) {
|
||||
conn, err := db.Take(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Put(conn)
|
||||
|
||||
defer sqlitex.Transaction(conn)(&err)
|
||||
|
||||
log.DebugContext(ctx, "horses",
|
||||
slog.Int("num", int(race.NumRaceHorses)),
|
||||
slog.Int("actual", len(race.RaceHorse)),
|
||||
)
|
||||
var trainedCharas [18]int64
|
||||
for i := range race.RaceHorse {
|
||||
c := &race.RaceHorse[i]
|
||||
id, err := insertTrainedChara(conn, c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
trainedCharas[i] = id
|
||||
if err := insertTrainedCharaSkill(conn, c, id); err != nil {
|
||||
return err
|
||||
}
|
||||
log.DebugContext(ctx, "horse",
|
||||
slog.String("trainer", c.TrainerName),
|
||||
slog.Int("trained", int(c.ResponseHorseData.TrainedCharaID)),
|
||||
slog.Int("card", int(c.ResponseHorseData.CardID)),
|
||||
slog.Int64("id", id),
|
||||
)
|
||||
}
|
||||
if err = insertRaceCourseSet(conn, race); err != nil {
|
||||
return err
|
||||
}
|
||||
log.DebugContext(ctx, "course", slog.Int("id", int(race.RaceCourseSet.ID)))
|
||||
configID, err := insertRaceConfig(conn, race)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.DebugContext(ctx, "config", slog.Int64("id", configID))
|
||||
raceID, err := insertRace(conn, race, simdata, &trainedCharas, configID)
|
||||
if err != nil {
|
||||
code := sqlite.ErrCode(err)
|
||||
if code == sqlite.ResultConstraintUnique {
|
||||
log.InfoContext(ctx, "already loaded")
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
log.DebugContext(ctx, "race", slog.Int64("id", raceID))
|
||||
err = errors.Join(
|
||||
insertRaceHorses(ctx, log, conn, race, simdata, &trainedCharas, raceID),
|
||||
insertRaceFrames(ctx, log, conn, simdata, raceID),
|
||||
insertRaceEvents(ctx, log, conn, simdata, raceID),
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
//go:embed insert-trained_chara.sql
|
||||
var insertTrainedCharaSQL string
|
||||
|
||||
func insertTrainedChara(conn *sqlite.Conn, c *replay.HorseData) (int64, error) {
|
||||
stmt := conn.Prep(insertTrainedCharaSQL)
|
||||
trainer := cmp.Or(c.ResponseHorseData.ViewerID, c.ResponseHorseData.OwnerViewerID)
|
||||
stmt.SetText("$trainer", c.TrainerName)
|
||||
stmt.SetInt64("$trainer_id", trainer)
|
||||
stmt.SetInt64("$trained_chara_id", int64(c.ResponseHorseData.TrainedCharaID))
|
||||
stmt.SetInt64("$chara_id", int64(c.CharaID))
|
||||
stmt.SetText("$chara_name", c.CharaName)
|
||||
stmt.SetInt64("$card_id", int64(c.ResponseHorseData.CardID))
|
||||
stmt.SetInt64("$rarity", int64(c.Rarity))
|
||||
stmt.SetInt64("$mob_id", int64(c.MobID))
|
||||
stmt.SetInt64("$raw_speed", int64(c.ResponseHorseData.Speed))
|
||||
stmt.SetInt64("$raw_stamina", int64(c.ResponseHorseData.Stamina))
|
||||
stmt.SetInt64("$raw_power", int64(c.ResponseHorseData.Pow))
|
||||
stmt.SetInt64("$raw_guts", int64(c.ResponseHorseData.Guts))
|
||||
stmt.SetInt64("$raw_wit", int64(c.ResponseHorseData.Wiz))
|
||||
stmt.SetInt64("$turf", int64(c.ResponseHorseData.ProperGroundTurf))
|
||||
stmt.SetInt64("$dirt", int64(c.ResponseHorseData.ProperGroundDirt))
|
||||
stmt.SetInt64("$sprint", int64(c.ResponseHorseData.ProperDistanceShort))
|
||||
stmt.SetInt64("$mile", int64(c.ResponseHorseData.ProperDistanceMile))
|
||||
stmt.SetInt64("$medium", int64(c.ResponseHorseData.ProperDistanceMiddle))
|
||||
stmt.SetInt64("$long", int64(c.ResponseHorseData.ProperDistanceLong))
|
||||
stmt.SetInt64("$front_runner", int64(c.ResponseHorseData.ProperRunningStyleNige))
|
||||
stmt.SetInt64("$pace_chaser", int64(c.ResponseHorseData.ProperRunningStyleSenko))
|
||||
stmt.SetInt64("$late_surger", int64(c.ResponseHorseData.ProperRunningStyleSashi))
|
||||
stmt.SetInt64("$end_closer", int64(c.ResponseHorseData.ProperRunningStyleOikomi))
|
||||
stmt.SetInt64("$fans", int64(c.TrainedCharaData.Fans))
|
||||
stmt.SetInt64("$rank", int64(c.TrainedCharaData.Rank))
|
||||
stmt.SetInt64("$rank_score", int64(c.TrainedCharaData.RankScore))
|
||||
r, err := sqlitex.ResultInt64(stmt)
|
||||
err = errors.Join(err, stmt.Reset())
|
||||
if err != nil {
|
||||
err = fmt.Errorf("inserting trained chara: %w", err)
|
||||
}
|
||||
return r, err
|
||||
}
|
||||
|
||||
func insertTrainedCharaSkill(conn *sqlite.Conn, c *replay.HorseData, id int64) error {
|
||||
const sql = `INSERT OR IGNORE INTO trained_chara_skill VALUES (?, ?, ?)`
|
||||
stmt := conn.Prep(sql)
|
||||
stmt.BindInt64(1, id)
|
||||
for _, sk := range c.ResponseHorseData.SkillArray {
|
||||
stmt.BindInt64(2, int64(sk.SkillID))
|
||||
stmt.BindInt64(3, int64(sk.Level))
|
||||
_, err := stmt.Step()
|
||||
err = errors.Join(err, stmt.Reset())
|
||||
if err != nil {
|
||||
return fmt.Errorf("inserting trained chara id %d skill %d: %w", id, sk.SkillID, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertRaceCourseSet(conn *sqlite.Conn, race *replay.Race) error {
|
||||
const sql = `INSERT OR IGNORE INTO race_course_set VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
stmt := conn.Prep(sql)
|
||||
stmt.BindInt64(1, int64(race.RaceCourseSet.ID))
|
||||
stmt.BindInt64(2, int64(race.RaceCourseSet.RaceTrackID))
|
||||
stmt.BindInt64(3, int64(race.RaceCourseSet.Distance))
|
||||
stmt.BindInt64(4, int64(race.RaceCourseSet.Ground))
|
||||
stmt.BindInt64(5, int64(race.RaceCourseSet.Inout))
|
||||
stmt.BindInt64(6, int64(race.RaceCourseSet.Turn))
|
||||
stmt.BindInt64(7, int64(race.RaceCourseSet.FenceSet))
|
||||
stmt.BindInt64(8, int64(race.RaceCourseSet.FloatLaneMax))
|
||||
stmt.BindInt64(9, int64(race.RaceCourseSet.CourseSetStatusID))
|
||||
stmt.BindInt64(10, int64(race.RaceCourseSet.FinishTimeMin))
|
||||
stmt.BindInt64(11, int64(race.RaceCourseSet.FinishTimeMinRandomRange))
|
||||
stmt.BindInt64(12, int64(race.RaceCourseSet.FinishTimeMax))
|
||||
stmt.BindInt64(13, int64(race.RaceCourseSet.FinishTimeMaxRandomRange))
|
||||
stmt.BindFloat(14, float64(race.CourseSectionDistance))
|
||||
stmt.BindInt64(15, int64(race.CourseDistanceType))
|
||||
stmt.BindInt64(16, int64(race.CourseFurlongNum))
|
||||
stmt.BindFloat(17, float64(race.BaseSpeed))
|
||||
_, err := stmt.Step()
|
||||
err = errors.Join(err, stmt.Reset())
|
||||
if err != nil {
|
||||
return fmt.Errorf("inserting race course: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:embed insert-race_config.sql
|
||||
var insertRaceConfigSQL string
|
||||
|
||||
func insertRaceConfig(conn *sqlite.Conn, race *replay.Race) (int64, error) {
|
||||
stmt := conn.Prep(insertRaceConfigSQL)
|
||||
stmt.SetInt64("$type", int64(race.RaceType))
|
||||
stmt.SetInt64("$course_set_id", int64(race.RaceCourseSet.ID))
|
||||
stmt.SetInt64("$ground_condition", int64(race.GroundCondition))
|
||||
stmt.SetInt64("$weather", int64(race.Weather))
|
||||
stmt.SetInt64("$season", int64(race.Season))
|
||||
stmt.SetInt64("$time", int64(race.Time))
|
||||
stmt.SetInt64("$num_horse", int64(race.NumRaceHorses))
|
||||
r, err := sqlitex.ResultInt64(stmt)
|
||||
err = errors.Join(err, stmt.Reset())
|
||||
if err != nil {
|
||||
err = fmt.Errorf("inserting race config: %w", err)
|
||||
}
|
||||
return r, err
|
||||
}
|
||||
|
||||
//go:embed insert-race.sql
|
||||
var insertRaceSQL string
|
||||
|
||||
func insertRace(conn *sqlite.Conn, race *replay.Race, simdata *replay.RaceSimulateData, trainedCharas *[18]int64, configID int64) (int64, error) {
|
||||
stmt := conn.Prep(insertRaceSQL)
|
||||
stmt.SetInt64("$config_id", configID)
|
||||
stmt.SetInt64("$seed", int64(race.RandomSeed))
|
||||
stmt.SetFloat("$distance_diff_max", float64(simdata.DistanceDiffMax))
|
||||
stmt.SetInt64("$frame_count", int64(simdata.FrameCount))
|
||||
// zombiezen/sqlite doesn't seem to expose an API to find the index of a
|
||||
// named parameter, so even though we were using names before, we switch
|
||||
// to indices for trained chara IDs.
|
||||
for i, id := range trainedCharas {
|
||||
stmt.BindInt64(5+i, id)
|
||||
}
|
||||
r, err := sqlitex.ResultInt64(stmt)
|
||||
err = errors.Join(err, stmt.Reset())
|
||||
if err != nil {
|
||||
err = fmt.Errorf("inserting race: %w", err)
|
||||
}
|
||||
return r, err
|
||||
}
|
||||
|
||||
//go:embed insert-race_horse.sql
|
||||
var insertRaceHorseSQL string
|
||||
|
||||
func insertRaceHorses(ctx context.Context, log *slog.Logger, conn *sqlite.Conn, race *replay.Race, simdata *replay.RaceSimulateData, trainedCharas *[18]int64, raceID int64) error {
|
||||
stmt := conn.Prep(insertRaceHorseSQL)
|
||||
stmt.SetInt64("$race", raceID)
|
||||
for i, c := range race.RaceHorse {
|
||||
log.DebugContext(ctx, "race horse", slog.Int64("trained", trainedCharas[i]), slog.Int("index", int(c.HorseIndex)))
|
||||
stmt.SetInt64("$horse", trainedCharas[i])
|
||||
stmt.SetInt64("$horse_index", int64(c.HorseIndex))
|
||||
stmt.SetInt64("$post", int64(c.PostNumber))
|
||||
stmt.SetInt64("$finish_order", int64(c.FinishOrder))
|
||||
stmt.SetFloat("$finish_time_raw", float64(c.FinishTimeRaw))
|
||||
stmt.SetFloat("$finish_time_scaled", float64(c.FinishTimeScaled))
|
||||
stmt.SetFloat("$finish_diff_time_from_prev", float64(c.FinishDiffTimeFromPrev))
|
||||
stmt.SetInt64("$running_style", int64(c.ResponseHorseData.RunningStyle))
|
||||
stmt.SetInt64("$running_style_ex", int64(c.RunningStyleEx))
|
||||
stmt.SetInt64("$active_distance_aptitude", int64(c.ActiveProperDistance))
|
||||
stmt.SetInt64("$active_surface_aptitude", int64(c.ActiveProperGroundType))
|
||||
stmt.SetInt64("$mood", int64(c.RaceParam.Motivation))
|
||||
stmt.SetFloat("$mood_coef", float64(c.RaceParam.MotivationCoef))
|
||||
stmt.SetFloat("$base_speed", float64(c.RaceParam.BaseSpeed))
|
||||
stmt.SetFloat("$base_stamina", float64(c.RaceParam.BaseStamina))
|
||||
stmt.SetFloat("$base_power", float64(c.RaceParam.BasePow))
|
||||
stmt.SetFloat("$base_guts", float64(c.RaceParam.BaseGuts))
|
||||
stmt.SetFloat("$base_wit", float64(c.RaceParam.BaseWiz))
|
||||
stmt.SetInt64("$popularity", int64(c.Popularity))
|
||||
stmt.SetInt64("$popularity_rank_left", int64(c.PopularityRankLeft))
|
||||
stmt.SetInt64("$popularity_rank_center", int64(c.PopularityRankCenter))
|
||||
stmt.SetInt64("$popularity_rank_right", int64(c.PopularityRankRight))
|
||||
stmt.SetFloat("$start_delay", float64(simdata.HorseResult[i].StartDelayTime))
|
||||
stmt.SetFloat("$spurt_start_distance", float64(simdata.HorseResult[i].LastSpurtStartDistance))
|
||||
_, err := stmt.Step()
|
||||
err = errors.Join(err, stmt.Reset())
|
||||
if err != nil {
|
||||
return fmt.Errorf("inserting race horses: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertRaceFrames(ctx context.Context, log *slog.Logger, conn *sqlite.Conn, simdata *replay.RaceSimulateData, raceID int64) error {
|
||||
const sql = `INSERT INTO race_frame VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
stmt := conn.Prep(sql)
|
||||
stmt.BindInt64(1, raceID)
|
||||
for _, f := range simdata.Frame {
|
||||
log.DebugContext(ctx, "frame", slog.Float64("time", float64(f.Time)))
|
||||
stmt.BindFloat(2, float64(f.Time))
|
||||
for i, h := range f.HorseFrame {
|
||||
stmt.BindInt64(3, int64(i))
|
||||
stmt.BindFloat(4, float64(h.Distance))
|
||||
stmt.BindInt64(5, int64(h.LanePosition))
|
||||
stmt.BindInt64(6, int64(h.Speed))
|
||||
stmt.BindInt64(7, int64(h.HP))
|
||||
stmt.BindInt64(8, int64(h.TemptationMode))
|
||||
stmt.BindInt64(9, int64(h.BlockFrontHorseIndex))
|
||||
_, err := stmt.Step()
|
||||
err = errors.Join(err, stmt.Reset())
|
||||
if err != nil {
|
||||
return fmt.Errorf("inserting race frames: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertRaceEvents(ctx context.Context, log *slog.Logger, conn *sqlite.Conn, simdata *replay.RaceSimulateData, raceID int64) error {
|
||||
const sql = `INSERT INTO race_event VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
||||
stmt := conn.Prep(sql)
|
||||
for _, e := range simdata.Event {
|
||||
log.DebugContext(ctx, "event", slog.Float64("time", float64(e.Event.FrameTime)), slog.Int("type", int(e.Event.Type)))
|
||||
// Easier to clear all bindings every time now
|
||||
// than to bind nulls to all the parameters that need them below.
|
||||
stmt.ClearBindings()
|
||||
stmt.BindInt64(1, raceID)
|
||||
stmt.BindFloat(2, float64(e.Event.FrameTime))
|
||||
stmt.BindInt64(3, int64(e.Event.Type))
|
||||
for i, p := range e.Event.Param {
|
||||
stmt.BindInt64(4+i, int64(p))
|
||||
}
|
||||
_, err := stmt.Step()
|
||||
err = errors.Join(err, stmt.Reset())
|
||||
if err != nil {
|
||||
return fmt.Errorf("inserting race events: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user