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 }