package main import ( "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" "git.sunturtle.xyz/zephyr/horse/umadump/ism" ) const appID int32 = 0x157db //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, career *ism.WorkIdleSingleMode, trainerID int64) (err error) { log = log.With(slog.Int64("trainer", trainerID), slog.Int("id", int(career.End.Chara.SingleModeCharaID))) conn, err := db.Take(ctx) if err != nil { return err } defer db.Put(conn) defer sqlitex.Transaction(conn)(&err) // Do the first insert separately so we can check whether the career is // already in, and so that the join doesn't report the same error repeatedly // if there's something wrong with the db. if err := insertCareer(conn, log, career, trainerID); err != nil { code := sqlite.ErrCode(err) if code == sqlite.ResultConstraintUnique { log.InfoContext(ctx, "already loaded") return nil } return err } trainedChara := career.End.Chara.SingleModeCharaID err = errors.Join( insertConditions(conn, log, career.ProgressLog.CharaEffectLog, trainerID, trainedChara), insertGain(conn, log, &career.ProgressLog.EventGain, trainerID, trainedChara, 0), insertGainSkillHint(conn, log, career.ProgressLog.EventGain.SkillTips, trainerID, trainedChara, 0), insertGain(conn, log, &career.ProgressLog.SuccessionGain, trainerID, trainedChara, 1), insertGainSkillHint(conn, log, career.ProgressLog.SuccessionGain.SkillTips, trainerID, trainedChara, 1), ) for _, s := range career.ProgressLog.SupportCardGains { err = errors.Join( err, insertGain(conn, log, &s.GainInfo, trainerID, trainedChara, int32(s.SupportCardID)), insertGainSkillHint(conn, log, s.GainInfo.SkillTips, trainerID, trainedChara, int32(s.SupportCardID)), ) } err = errors.Join( err, insertInspirations(conn, log, career.ProgressLog.SuccessionFactorGains, trainerID, trainedChara), insertRaces(conn, log, career.ProgressLog.RaceHistory, trainerID, trainedChara), insertGainedSkills(conn, log, career.ProgressLog.GainSkillIDs, trainerID, trainedChara), insertCareerSkills(conn, log, career.End.Chara.Skills, trainerID, trainedChara), insertCareerSkillHints(conn, log, career.End.Chara.SkillTips, trainerID, trainedChara), insertSupportCards(conn, log, career.End.Chara.SupportCards, trainerID, trainedChara), insertStartSkills(conn, log, career.ProgressInfo.Chara.Skills, trainerID, trainedChara), insertStartSkillHints(conn, log, career.ProgressInfo.Chara.SkillTips, trainerID, trainedChara), ) return err } //go:embed insert-career.sql var insertCareerSQL string func insertCareer(conn *sqlite.Conn, log *slog.Logger, career *ism.WorkIdleSingleMode, trainerID int64) error { log.Debug("insert career") stmt := conn.Prep(insertCareerSQL) stmt.SetInt64("$trainer_id", trainerID) stmt.SetInt64("$trained_chara_id", int64(career.End.Chara.SingleModeCharaID)) stmt.SetInt64("$card_id", int64(career.End.Chara.CardID)) stmt.SetInt64("$scenario_id", int64(career.End.Chara.ScenarioID)) stmt.SetInt64("$route_id", int64(career.End.Chara.RouteID)) stmt.SetText("$start_time", career.ProgressInfo.StartTime) stmt.SetText("$end_time", career.ProgressInfo.EndTime) stmt.SetInt64("$chara_grade", int64(career.End.Chara.CharaGrade)) stmt.SetInt64("$star_level", int64(career.End.Chara.Rarity)) stmt.SetInt64("$potential_level", int64(career.End.Chara.TalentLevel)) stmt.SetInt64("$speed", int64(career.End.Chara.Speed)) stmt.SetInt64("$stamina", int64(career.End.Chara.Stamina)) stmt.SetInt64("$power", int64(career.End.Chara.Power)) stmt.SetInt64("$guts", int64(career.End.Chara.Guts)) stmt.SetInt64("$wit", int64(career.End.Chara.Wiz)) stmt.SetInt64("$energy", int64(career.End.Chara.Vital)) stmt.SetInt64("$max_speed", int64(career.End.Chara.MaxSpeed)) stmt.SetInt64("$max_stamina", int64(career.End.Chara.MaxStamina)) stmt.SetInt64("$max_power", int64(career.End.Chara.MaxPower)) stmt.SetInt64("$max_guts", int64(career.End.Chara.MaxGuts)) stmt.SetInt64("$max_wit", int64(career.End.Chara.MaxWiz)) stmt.SetInt64("$max_energy", int64(career.End.Chara.MaxVital)) stmt.SetInt64("$skill_point", int64(career.ProgressLog.TotalSkillPoint)) stmt.SetInt64("$mood", int64(career.End.Chara.Motivation)) stmt.SetInt64("$turf", int64(career.End.Chara.ProperGroundTurf)) stmt.SetInt64("$dirt", int64(career.End.Chara.ProperGroundDirt)) stmt.SetInt64("$sprint", int64(career.End.Chara.ProperDistanceShort)) stmt.SetInt64("$mile", int64(career.End.Chara.ProperDistanceMile)) stmt.SetInt64("$medium", int64(career.End.Chara.ProperDistanceMiddle)) stmt.SetInt64("$long", int64(career.End.Chara.ProperDistanceLong)) stmt.SetInt64("$front_runner", int64(career.End.Chara.ProperRunningStyleNige)) stmt.SetInt64("$pace_chaser", int64(career.End.Chara.ProperRunningStyleSenko)) stmt.SetInt64("$late_surger", int64(career.End.Chara.ProperRunningStyleSashi)) stmt.SetInt64("$end_closer", int64(career.End.Chara.ProperRunningStyleOikomi)) stmt.SetInt64("$start_speed", int64(career.ProgressInfo.Chara.Speed)) stmt.SetInt64("$start_stamina", int64(career.ProgressInfo.Chara.Stamina)) stmt.SetInt64("$start_power", int64(career.ProgressInfo.Chara.Power)) stmt.SetInt64("$start_guts", int64(career.ProgressInfo.Chara.Guts)) stmt.SetInt64("$start_wit", int64(career.ProgressInfo.Chara.Wiz)) stmt.SetInt64("$start_max_speed", int64(career.ProgressInfo.Chara.MaxSpeed)) stmt.SetInt64("$start_max_stamina", int64(career.ProgressInfo.Chara.MaxStamina)) stmt.SetInt64("$start_max_power", int64(career.ProgressInfo.Chara.MaxPower)) stmt.SetInt64("$start_max_guts", int64(career.ProgressInfo.Chara.MaxGuts)) stmt.SetInt64("$start_max_wit", int64(career.ProgressInfo.Chara.MaxWiz)) stmt.SetInt64("$start_turf", int64(career.ProgressInfo.Chara.ProperGroundTurf)) stmt.SetInt64("$start_dirt", int64(career.ProgressInfo.Chara.ProperGroundDirt)) stmt.SetInt64("$start_sprint", int64(career.ProgressInfo.Chara.ProperDistanceShort)) stmt.SetInt64("$start_mile", int64(career.ProgressInfo.Chara.ProperDistanceMile)) stmt.SetInt64("$start_medium", int64(career.ProgressInfo.Chara.ProperDistanceMiddle)) stmt.SetInt64("$start_long", int64(career.ProgressInfo.Chara.ProperDistanceLong)) stmt.SetInt64("$start_front_runner", int64(career.ProgressInfo.Chara.ProperRunningStyleNige)) stmt.SetInt64("$start_pace_chaser", int64(career.ProgressInfo.Chara.ProperRunningStyleSenko)) stmt.SetInt64("$start_late_surger", int64(career.ProgressInfo.Chara.ProperRunningStyleSashi)) stmt.SetInt64("$start_end_closer", int64(career.ProgressInfo.Chara.ProperRunningStyleOikomi)) stmt.SetInt64("$fans", int64(career.End.Chara.Fans)) stmt.SetInt64("$race_program_id", int64(career.End.Chara.RaceProgramID)) stmt.SetInt64("$reserve_race_program_id", int64(career.End.Chara.ReserveRaceProgramID)) stmt.SetInt64("$race_running_style", int64(career.End.Chara.RaceRunningStyle)) stmt.SetInt64("$is_short_race", int64(career.End.Chara.IsShortRace)) stmt.SetInt64("$succession_trained_chara_id_1", int64(career.End.Chara.SuccessionTrainedCharaID1)) stmt.SetInt64("$succession_trained_chara_id_2", int64(career.End.Chara.SuccessionTrainedCharaID2)) _, err := stmt.Step() err = errors.Join(err, stmt.Reset()) if err != nil { return fmt.Errorf("inserting career: %w", err) } return nil } func insertConditions(conn *sqlite.Conn, log *slog.Logger, cs []ism.CharaEffectLog, trainerID int64, trainedChara int32) error { log.Debug("insert conditions") const sql = `INSERT INTO condition VALUES (?, ?, ?, ?)` stmt := conn.Prep(sql) stmt.BindInt64(1, trainerID) stmt.BindInt64(2, int64(trainedChara)) for _, c := range cs { stmt.BindInt64(3, int64(c.CharaEffectID)) stmt.BindBool(4, c.IsActive) _, err := stmt.Step() err = errors.Join(err, stmt.Reset()) if err != nil { return fmt.Errorf("inserting condition: %w", err) } } return nil } //go:embed insert-gain.sql var insertGainSQL string func insertGain(conn *sqlite.Conn, log *slog.Logger, gain *ism.IdleSingleModeGainInfo, trainerID int64, trainedChara int32, source int32) error { log.Debug("insert gain", slog.Int("source", int(source))) stmt := conn.Prep(insertGainSQL) stmt.SetInt64("$trainer_id", trainerID) stmt.SetInt64("$trained_chara_id", int64(trainedChara)) stmt.SetInt64("$source", int64(source)) stmt.SetInt64("$gain_speed", int64(gain.Speed.Value)) stmt.SetInt64("$gain_stamina", int64(gain.Stamina.Value)) stmt.SetInt64("$gain_power", int64(gain.Power.Value)) stmt.SetInt64("$gain_guts", int64(gain.Guts.Value)) stmt.SetInt64("$gain_wit", int64(gain.Wiz.Value)) stmt.SetInt64("$gain_skill_point", int64(gain.SkillPoint)) stmt.SetInt64("$gain_max_speed", int64(gain.MaxSpeed)) stmt.SetInt64("$gain_max_stamina", int64(gain.MaxStamina)) stmt.SetInt64("$gain_max_power", int64(gain.MaxPower)) stmt.SetInt64("$gain_max_guts", int64(gain.MaxGuts)) stmt.SetInt64("$gain_max_wit", int64(gain.MaxWiz)) stmt.SetInt64("$gain_turf", int64(gain.ProperGroundTurf)) stmt.SetInt64("$gain_dirt", int64(gain.ProperGroundDirt)) stmt.SetInt64("$gain_sprint", int64(gain.ProperDistanceShort)) stmt.SetInt64("$gain_mile", int64(gain.ProperDistanceMile)) stmt.SetInt64("$gain_medium", int64(gain.ProperDistanceMiddle)) stmt.SetInt64("$gain_long", int64(gain.ProperDistanceLong)) stmt.SetInt64("$gain_front_runner", int64(gain.ProperRunningStyleNige)) stmt.SetInt64("$gain_pace_chaser", int64(gain.ProperRunningStyleSenko)) stmt.SetInt64("$gain_late_surger", int64(gain.ProperRunningStyleSashi)) stmt.SetInt64("$gain_end_closer", int64(gain.ProperRunningStyleOikomi)) _, err := stmt.Step() err = errors.Join(err, stmt.Reset()) if err != nil { return fmt.Errorf("inserting gain info for source %d: %w", source, err) } return nil } func insertGainSkillHint(conn *sqlite.Conn, log *slog.Logger, tips []ism.SkillTip, trainerID int64, trainedChara int32, source int32) error { log.Debug("insert gain skill hints", slog.Int("source", int(source))) stmt := conn.Prep(`INSERT INTO gain_skill_hint VALUES (?, ?, ?, ?, ?, ?)`) stmt.BindInt64(1, trainerID) stmt.BindInt64(2, int64(trainedChara)) stmt.BindInt64(3, int64(source)) for _, t := range tips { stmt.BindInt64(4, int64(t.GroupID)) stmt.BindInt64(5, int64(t.Rarity)) stmt.BindInt64(6, int64(t.Level)) _, err := stmt.Step() err = errors.Join(err, stmt.Reset()) if err != nil { return fmt.Errorf("inserting skill hint for group %d, rarity %d from source %d: %w", t.GroupID, t.Rarity, source, err) } } return nil } func insertInspirations(conn *sqlite.Conn, log *slog.Logger, inspos []ism.IdleSingleModeSuccessionFactorGain, trainerID int64, trainedChara int32) error { log.Debug("insert inspirations") stmt := conn.Prep(`INSERT INTO inspiration VALUES (?, ?, ?, ?, ?)`) stmt.BindInt64(1, trainerID) stmt.BindInt64(2, int64(trainedChara)) for _, yr := range inspos { stmt.BindInt64(3, int64(yr.Year)) for _, f := range yr.GainFactorInfo { stmt.BindInt64(4, int64(f.FactorID)) stmt.BindInt64(5, int64(f.Level)) _, err := stmt.Step() err = errors.Join(err, stmt.Reset()) if err != nil { return fmt.Errorf("inserting spark: %w", err) } } } return nil } func insertRaces(conn *sqlite.Conn, log *slog.Logger, races []ism.IdleSingleModeRaceHistory, trainerID int64, trainedChara int32) error { log.Debug("insert races") raceStmt := conn.Prep(`INSERT INTO race_history VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`) rewardStmt := conn.Prep(`INSERT INTO race_reward VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`) raceStmt.BindInt64(1, trainerID) raceStmt.BindInt64(2, int64(trainedChara)) rewardStmt.BindInt64(1, trainerID) rewardStmt.BindInt64(2, int64(trainedChara)) for _, r := range races { raceStmt.BindInt64(3, int64(r.RaceHistory.Turn)) raceStmt.BindInt64(4, int64(r.RaceHistory.ProgramID)) raceStmt.BindInt64(5, int64(r.RaceHistory.Weather)) raceStmt.BindInt64(6, int64(r.RaceHistory.GroundCondition)) raceStmt.BindInt64(7, int64(r.RaceHistory.RunningStyle)) raceStmt.BindInt64(8, int64(r.RaceHistory.ResultRank)) raceStmt.BindInt64(9, int64(r.RaceHistory.FrameOrder)) raceStmt.BindInt64(10, int64(r.RaceRewardInfo.GainedFans)) raceStmt.BindInt64(11, int64(r.LoseTipsID)) _, err := raceStmt.Step() err = errors.Join(err, raceStmt.Reset()) if err != nil { return fmt.Errorf("inserting race history: %w", err) } rewardStmt.BindInt64(3, int64(r.RaceHistory.Turn)) rewardStmt.BindBool(7, false) rewardStmt.BindBool(8, false) rewardStmt.BindBool(9, false) for _, reward := range r.RaceRewardInfo.RaceReward { rewardStmt.BindInt64(4, int64(reward.ItemType)) rewardStmt.BindInt64(5, int64(reward.ItemID)) rewardStmt.BindInt64(6, int64(reward.ItemNum)) _, err := rewardStmt.Step() err = errors.Join(err, rewardStmt.Reset()) if err != nil { return fmt.Errorf("inserting race reward: %w", err) } } rewardStmt.BindBool(7, true) for _, reward := range r.RaceRewardInfo.RaceRewardBonus { rewardStmt.BindInt64(4, int64(reward.ItemType)) rewardStmt.BindInt64(5, int64(reward.ItemID)) rewardStmt.BindInt64(6, int64(reward.ItemNum)) _, err := rewardStmt.Step() err = errors.Join(err, rewardStmt.Reset()) if err != nil { return fmt.Errorf("inserting race reward: %w", err) } } rewardStmt.BindBool(7, false) rewardStmt.BindBool(8, true) for _, reward := range r.RaceRewardInfo.RaceRewardPlusBonus { rewardStmt.BindInt64(4, int64(reward.ItemType)) rewardStmt.BindInt64(5, int64(reward.ItemID)) rewardStmt.BindInt64(6, int64(reward.ItemNum)) _, err := rewardStmt.Step() err = errors.Join(err, rewardStmt.Reset()) if err != nil { return fmt.Errorf("inserting race reward: %w", err) } } rewardStmt.BindBool(8, false) rewardStmt.BindBool(9, true) for _, reward := range r.RaceRewardInfo.RaceRewardBonusWin { rewardStmt.BindInt64(4, int64(reward.ItemType)) rewardStmt.BindInt64(5, int64(reward.ItemID)) rewardStmt.BindInt64(6, int64(reward.ItemNum)) _, err := rewardStmt.Step() err = errors.Join(err, rewardStmt.Reset()) if err != nil { return fmt.Errorf("inserting race reward: %w", err) } } } return nil } func insertGainedSkills(conn *sqlite.Conn, log *slog.Logger, skills []horse.SkillID, trainerID int64, trainedChara int32) error { log.Debug("insert gained skills") stmt := conn.Prep(`INSERT INTO gained_skill VALUES (?, ?, ?)`) stmt.BindInt64(1, trainerID) stmt.BindInt64(2, int64(trainedChara)) for _, sk := range skills { stmt.BindInt64(3, int64(sk)) _, err := stmt.Step() err = errors.Join(err, stmt.Reset()) if err != nil { return fmt.Errorf("inserting gain skill: %w", err) } } return nil } func insertCareerSkills(conn *sqlite.Conn, log *slog.Logger, skills []ism.Skill, trainerID int64, trainedChara int32) error { log.Debug("insert career skills") stmt := conn.Prep(`INSERT INTO career_skill VALUES (?, ?, ?, ?)`) stmt.BindInt64(1, trainerID) stmt.BindInt64(2, int64(trainedChara)) for _, sk := range skills { stmt.BindInt64(3, int64(sk.ID)) stmt.BindInt64(4, int64(sk.Level)) _, err := stmt.Step() err = errors.Join(err, stmt.Reset()) if err != nil { return fmt.Errorf("inserting skill: %w", err) } } return nil } func insertCareerSkillHints(conn *sqlite.Conn, log *slog.Logger, hints []ism.SkillTip, trainerID int64, trainedChara int32) error { log.Debug("insert career skill hints") stmt := conn.Prep(`INSERT INTO career_skill_hint VALUES (?, ?, ?, ?, ?)`) stmt.BindInt64(1, trainerID) stmt.BindInt64(2, int64(trainedChara)) for _, h := range hints { stmt.BindInt64(3, int64(h.GroupID)) stmt.BindInt64(4, int64(h.Rarity)) stmt.BindInt64(5, int64(h.Level)) _, err := stmt.Step() err = errors.Join(err, stmt.Reset()) if err != nil { return fmt.Errorf("inserting skill hint: %w", err) } } return nil } func insertSupportCards(conn *sqlite.Conn, log *slog.Logger, supps []ism.SingleModeSupportCard, trainerID int64, trainedChara int32) error { log.Debug("insert support cards") stmt := conn.Prep(`INSERT INTO career_support_card VALUES (?, ?, ?, ?, ?, ?, ?)`) stmt.BindInt64(1, trainerID) stmt.BindInt64(2, int64(trainedChara)) for _, s := range supps { stmt.BindInt64(3, int64(s.Position)) stmt.BindInt64(4, int64(s.ID)) stmt.BindInt64(5, int64(s.LimitBreak)) stmt.BindInt64(6, int64(s.Exp)) stmt.BindInt64(7, int64(s.OwnerID)) _, err := stmt.Step() err = errors.Join(err, stmt.Reset()) if err != nil { return fmt.Errorf("inserting support card: %w", err) } } return nil } func insertStartSkills(conn *sqlite.Conn, log *slog.Logger, skills []ism.Skill, trainerID int64, trainedChara int32) error { log.Debug("insert starting skills") stmt := conn.Prep(`INSERT INTO start_skill VALUES (?, ?, ?, ?)`) stmt.BindInt64(1, trainerID) stmt.BindInt64(2, int64(trainedChara)) for _, sk := range skills { stmt.BindInt64(3, int64(sk.ID)) stmt.BindInt64(4, int64(sk.Level)) _, err := stmt.Step() err = errors.Join(err, stmt.Reset()) if err != nil { return fmt.Errorf("inserting skill: %w", err) } } return nil } func insertStartSkillHints(conn *sqlite.Conn, log *slog.Logger, hints []ism.SkillTip, trainerID int64, trainedChara int32) error { log.Debug("insert starting skill hints") stmt := conn.Prep(`INSERT INTO start_skill_hint VALUES (?, ?, ?, ?, ?)`) stmt.BindInt64(1, trainerID) stmt.BindInt64(2, int64(trainedChara)) for _, h := range hints { stmt.BindInt64(3, int64(h.GroupID)) stmt.BindInt64(4, int64(h.Rarity)) stmt.BindInt64(5, int64(h.Level)) _, err := stmt.Step() err = errors.Join(err, stmt.Reset()) if err != nil { return fmt.Errorf("inserting skill hint: %w", err) } } return nil }