326 lines
12 KiB
Go
326 lines
12 KiB
Go
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
|
|
}
|