From 74185d92d4e34c99c227dd4a7518375c8f10bbb6 Mon Sep 17 00:00:00 2001 From: Branden J Brown Date: Fri, 16 Oct 2026 12:30:28 -0400 Subject: [PATCH] cmd/ismdb: tool to load auto mode umadumps into sqlite --- cmd/ismdb/db.go | 455 ++++++++++++++++++++++++++++++++++++ cmd/ismdb/insert-career.sql | 69 ++++++ cmd/ismdb/insert-gain.sql | 31 +++ cmd/ismdb/main.go | 73 ++++++ cmd/ismdb/schema-00.sql | 250 ++++++++++++++++++++ cmd/ismdb/schema-final.sql | 88 +++++++ 6 files changed, 966 insertions(+) create mode 100644 cmd/ismdb/db.go create mode 100644 cmd/ismdb/insert-career.sql create mode 100644 cmd/ismdb/insert-gain.sql create mode 100644 cmd/ismdb/main.go create mode 100644 cmd/ismdb/schema-00.sql create mode 100644 cmd/ismdb/schema-final.sql diff --git a/cmd/ismdb/db.go b/cmd/ismdb/db.go new file mode 100644 index 0000000..d4a505d --- /dev/null +++ b/cmd/ismdb/db.go @@ -0,0 +1,455 @@ +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 +} diff --git a/cmd/ismdb/insert-career.sql b/cmd/ismdb/insert-career.sql new file mode 100644 index 0000000..e08bb24 --- /dev/null +++ b/cmd/ismdb/insert-career.sql @@ -0,0 +1,69 @@ +INSERT INTO career VALUES ( + $trainer_id, + $trained_chara_id, + $card_id, + $scenario_id, + $route_id, + $start_time, + $end_time, + + $chara_grade, + $star_level, + $potential_level, + + $speed, + $stamina, + $power, + $guts, + $wit, + $energy, + $max_speed, + $max_stamina, + $max_power, + $max_guts, + $max_wit, + $max_energy, + $skill_point, + $mood, + + $turf, + $dirt, + $sprint, + $mile, + $medium, + $long, + $front_runner, + $pace_chaser, + $late_surger, + $end_closer, + + $start_speed, + $start_stamina, + $start_power, + $start_guts, + $start_wit, + $start_max_speed, + $start_max_stamina, + $start_max_power, + $start_max_guts, + $start_max_wit, + $start_turf, + $start_dirt, + $start_sprint, + $start_mile, + $start_medium, + $start_long, + $start_front_runner, + $start_pace_chaser, + $start_late_surger, + $start_end_closer, + + $fans, + $race_program_id, + $reserve_race_program_id, + $race_running_style, + $is_short_race, + + $succession_trained_chara_id_1, + $succession_trained_chara_id_2 +) \ No newline at end of file diff --git a/cmd/ismdb/insert-gain.sql b/cmd/ismdb/insert-gain.sql new file mode 100644 index 0000000..2703049 --- /dev/null +++ b/cmd/ismdb/insert-gain.sql @@ -0,0 +1,31 @@ +INSERT INTO gain VALUES ( + $trainer_id, + $trained_chara_id, + $source, + + $gain_speed, + $gain_stamina, + $gain_power, + $gain_guts, + $gain_wit, + $gain_skill_point, + + $gain_max_speed, + $gain_max_stamina, + $gain_max_power, + $gain_max_guts, + $gain_max_wit, + + $gain_turf, + $gain_dirt, + + $gain_sprint, + $gain_mile, + $gain_medium, + $gain_long, + + $gain_front_runner, + $gain_pace_chaser, + $gain_late_surger, + $gain_end_closer +) \ No newline at end of file diff --git a/cmd/ismdb/main.go b/cmd/ismdb/main.go new file mode 100644 index 0000000..3ab8777 --- /dev/null +++ b/cmd/ismdb/main.go @@ -0,0 +1,73 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "log/slog" + "os" + "os/signal" + "path/filepath" + + "git.sunturtle.xyz/zephyr/horse/umadump/ism" +) + +func main() { + var ( + uri, journalMode string + fs []string + trainer int64 + logLevel slog.Level + ) + flag.StringVar(&uri, "db", "idle_single_mode.sqlite3", "output database `URI` or filename") + flag.StringVar(&journalMode, "journal", "memory", "journal `mode` to apply to database connections") + flag.Int64Var(&trainer, "t", 0, "trainer `ID` to associate with the data") + flag.TextVar(&logLevel, "log", slog.LevelInfo, "log `level`") + flag.Func("d", "`dir`ectory containing idle single mode dumps to load (repeatable)", func(s string) error { + es, err := os.ReadDir(s) + for _, e := range es { + if e.IsDir() { + continue + } + fs = append(fs, filepath.Join(s, e.Name())) + } + return err + }) + flag.Parse() + fs = append(fs, flag.Args()...) + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + go func() { + <-ctx.Done() + stop() + }() + + log := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel})) + db := opendb(uri, journalMode) + ok := 0 + for _, n := range fs { + log := log.With(slog.String("file", n)) + log.Info("begin") + f, err := os.Open(n) + if err != nil { + log.Error("open", slog.Any("err", err)) + continue + } + var career ism.WorkIdleSingleMode + d := json.NewDecoder(f) + d.DisallowUnknownFields() + log.Debug("decode") + if err := d.Decode(&career); err != nil { + log.Error("load", slog.Any("err", err)) + continue + } + log.Debug("insert") + err = insert(ctx, db, log, &career, trainer) + if err != nil { + log.Error("insert", slog.Any("err", err)) + continue + } + ok++ + } + slog.Info("done", slog.Int("careers", ok)) +} diff --git a/cmd/ismdb/schema-00.sql b/cmd/ismdb/schema-00.sql new file mode 100644 index 0000000..94bcd3b --- /dev/null +++ b/cmd/ismdb/schema-00.sql @@ -0,0 +1,250 @@ +CREATE TABLE IF NOT EXISTS career ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + card_id INTEGER NOT NULL, + scenario_id INTEGER NOT NULL, + route_id INTEGER NOT NULL, + start_time TEXT NOT NULL, + end_time TEXT NOT NULL, + + chara_grade INTEGER NOT NULL, + star_level INTEGER NOT NULL, + potential_level INTEGER NOT NULL, + + speed INTEGER NOT NULL, + stamina INTEGER NOT NULL, + power INTEGER NOT NULL, + guts INTEGER NOT NULL, + wit INTEGER NOT NULL, + energy INTEGER NOT NULL, + max_speed INTEGER NOT NULL, + max_stamina INTEGER NOT NULL, + max_power INTEGER NOT NULL, + max_guts INTEGER NOT NULL, + max_wit INTEGER NOT NULL, + max_energy INTEGER NOT NULL, + skill_point INTEGER NOT NULL, + mood INTEGER NOT NULL, + + turf INTEGER NOT NULL, + dirt INTEGER NOT NULL, + sprint INTEGER NOT NULL, + mile INTEGER NOT NULL, + medium INTEGER NOT NULL, + long INTEGER NOT NULL, + front_runner INTEGER NOT NULL, + pace_chaser INTEGER NOT NULL, + late_surger INTEGER NOT NULL, + end_closer INTEGER NOT NULL, + + -- These fields are 0 if the career finished while it was not actively + -- displayed on the client used to dump it. + start_speed INTEGER NOT NULL, + start_stamina INTEGER NOT NULL, + start_power INTEGER NOT NULL, + start_guts INTEGER NOT NULL, + start_wit INTEGER NOT NULL, + start_max_speed INTEGER NOT NULL, + start_max_stamina INTEGER NOT NULL, + start_max_power INTEGER NOT NULL, + start_max_guts INTEGER NOT NULL, + start_max_wit INTEGER NOT NULL, + start_turf INTEGER NOT NULL, + start_dirt INTEGER NOT NULL, + start_sprint INTEGER NOT NULL, + start_mile INTEGER NOT NULL, + start_medium INTEGER NOT NULL, + start_long INTEGER NOT NULL, + start_front_runner INTEGER NOT NULL, + start_pace_chaser INTEGER NOT NULL, + start_late_surger INTEGER NOT NULL, + start_end_closer INTEGER NOT NULL, + + fans INTEGER NOT NULL, + race_program_id INTEGER NOT NULL, + reserve_race_program_id INTEGER NOT NULL, + race_running_style INTEGER NOT NULL, + is_short_race INTEGER NOT NULL, + + succession_trained_chara_id_1 INTEGER NOT NULL, + succession_trained_chara_id_2 INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id) +) STRICT; +CREATE INDEX IF NOT EXISTS career_by_scenario ON career (scenario_id); +CREATE INDEX IF NOT EXISTS career_by_start_time ON career (start_time); + +CREATE TABLE IF NOT EXISTS condition ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + condition INTEGER NOT NULL, + is_active INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, condition), + FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE +) STRICT; + +CREATE TABLE IF NOT EXISTS gain ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + source INTEGER NOT NULL, -- 0 for events, 1 for inspiration, otherwise support card id + + gain_speed INTEGER NOT NULL, + gain_stamina INTEGER NOT NULL, + gain_power INTEGER NOT NULL, + gain_guts INTEGER NOT NULL, + gain_wit INTEGER NOT NULL, + gain_skill_point INTEGER NOT NULL, + + gain_max_speed INTEGER NOT NULL, + gain_max_stamina INTEGER NOT NULL, + gain_max_power INTEGER NOT NULL, + gain_max_guts INTEGER NOT NULL, + gain_max_wit INTEGER NOT NULL, + + gain_turf INTEGER NOT NULL, + gain_dirt INTEGER NOT NULL, + + gain_sprint INTEGER NOT NULL, + gain_mile INTEGER NOT NULL, + gain_medium INTEGER NOT NULL, + gain_long INTEGER NOT NULL, + + gain_front_runner INTEGER NOT NULL, + gain_pace_chaser INTEGER NOT NULL, + gain_late_surger INTEGER NOT NULL, + gain_end_closer INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, source), + FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE +) STRICT; +CREATE INDEX IF NOT EXISTS gain_by_source ON gain (source); + +CREATE TABLE IF NOT EXISTS gain_skill_hint ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + source INTEGER NOT NULL, + + group_id INTEGER NOT NULL, + rarity INTEGER NOT NULL, + level INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, source, group_id, rarity), + FOREIGN KEY (trainer_id, trained_chara_id, source) REFERENCES gain ON DELETE CASCADE +) STRICT; + +CREATE TABLE IF NOT EXISTS inspiration ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + year INTEGER NOT NULL, + factor_id INTEGER NOT NULL, + level INTEGER NOT NULL, -- always 0 but just in case we get it in the future + + FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE +) STRICT; +CREATE INDEX IF NOT EXISTS inspiration_by_career ON inspiration (trainer_id, trained_chara_id); +CREATE INDEX IF NOT EXISTS inspiration_by_factor_id ON inspiration (factor_id); + +CREATE TABLE IF NOT EXISTS race_history ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + + turn INTEGER NOT NULL, + program_id INTEGER NOT NULL, + weather INTEGER NOT NULL, + ground_condition INTEGER NOT NULL, + running_style INTEGER NOT NULL, + result_rank INTEGER NOT NULL, + frame_order INTEGER NOT NULL, + gained_fans INTEGER NOT NULL, + lose_tips_id INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, turn), + FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE +) STRICT; + +CREATE TABLE IF NOT EXISTS race_reward ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + turn INTEGER NOT NULL, + + item_type INTEGER NOT NULL, + item_id INTEGER NOT NULL, + item_num INTEGER NOT NULL, + + is_bonus INTEGER NOT NULL, + is_plus_bonus INTEGER NOT NULL, + is_bonus_win INTEGER NOT NULL, + + FOREIGN KEY (trainer_id, trained_chara_id, turn) REFERENCES race_history ON DELETE CASCADE +) STRICT; +CREATE INDEX IF NOT EXISTS race_reward_by_career_turn ON race_reward (trainer_id, trained_chara_id, turn); +CREATE INDEX IF NOT EXISTS race_reward_by_item ON race_reward (item_id); + +CREATE TABLE IF NOT EXISTS gained_skill ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + skill_id INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, skill_id), + FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE +) STRICT; + +CREATE TABLE IF NOT EXISTS career_skill ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + skill_id INTEGER NOT NULL, + level INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, skill_id), + FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE +) STRICT; + +CREATE TABLE IF NOT EXISTS career_skill_hint ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + + group_id INTEGER NOT NULL, + rarity INTEGER NOT NULL, + level INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, group_id, rarity), + FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE +) STRICT; + +CREATE TABLE IF NOT EXISTS career_support_card ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + position INTEGER NOT NULL, + support_card_id INTEGER NOT NULL, + limit_break_count INTEGER NOT NULL, + exp INTEGER NOT NULL, + owner_viewer_id INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, position), + FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE, + UNIQUE (trainer_id, trained_chara_id, support_card_id) +) STRICT; +CREATE INDEX IF NOT EXISTS career_support_card_by_card ON career_support_card (support_card_id, exp); + +CREATE TABLE IF NOT EXISTS start_skill ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + skill_id INTEGER NOT NULL, + level INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, skill_id), + FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE +) STRICT; + +CREATE TABLE IF NOT EXISTS start_skill_hint ( + trainer_id INTEGER NOT NULL DEFAULT 0, + trained_chara_id INTEGER NOT NULL, + + group_id INTEGER NOT NULL, + rarity INTEGER NOT NULL, + level INTEGER NOT NULL, + + PRIMARY KEY (trainer_id, trained_chara_id, group_id, rarity), + FOREIGN KEY (trainer_id, trained_chara_id) REFERENCES career ON DELETE CASCADE +) STRICT; diff --git a/cmd/ismdb/schema-final.sql b/cmd/ismdb/schema-final.sql new file mode 100644 index 0000000..9c55cbf --- /dev/null +++ b/cmd/ismdb/schema-final.sql @@ -0,0 +1,88 @@ +DROP VIEW IF EXISTS gain_event; +DROP VIEW IF EXISTS gain_inspiration; +DROP VIEW IF EXISTS gain_support_card; + +CREATE VIEW gain_event AS +SELECT + trainer_id, + trained_chara_id, + gain_speed, + gain_stamina, + gain_power, + gain_guts, + gain_wit, + gain_skill_point, + gain_max_speed, + gain_max_stamina, + gain_max_power, + gain_max_guts, + gain_max_wit, + gain_turf, + gain_dirt, + gain_sprint, + gain_mile, + gain_medium, + gain_long, + gain_front_runner, + gain_pace_chaser, + gain_late_surger, + gain_end_closer +FROM gain +WHERE source = 0; + +CREATE VIEW gain_inspiration AS +SELECT + trainer_id, + trained_chara_id, + gain_speed, + gain_stamina, + gain_power, + gain_guts, + gain_wit, + gain_skill_point, + gain_max_speed, + gain_max_stamina, + gain_max_power, + gain_max_guts, + gain_max_wit, + gain_turf, + gain_dirt, + gain_sprint, + gain_mile, + gain_medium, + gain_long, + gain_front_runner, + gain_pace_chaser, + gain_late_surger, + gain_end_closer +FROM gain +WHERE source = 1; + +CREATE VIEW gain_support_card AS +SELECT + trainer_id, + trained_chara_id, + source AS support_card_id, + gain_speed, + gain_stamina, + gain_power, + gain_guts, + gain_wit, + gain_skill_point, + gain_max_speed, + gain_max_stamina, + gain_max_power, + gain_max_guts, + gain_max_wit, + gain_turf, + gain_dirt, + gain_sprint, + gain_mile, + gain_medium, + gain_long, + gain_front_runner, + gain_pace_chaser, + gain_late_surger, + gain_end_closer +FROM gain +WHERE source > 1;