cmd/ismdb: tool to load auto mode umadumps into sqlite

This commit is contained in:
2026-10-24 23:00:30 -04:00
parent 7f1aa00be6
commit 74185d92d4
6 changed files with 966 additions and 0 deletions
+455
View File
@@ -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
}
+69
View File
@@ -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
)
+31
View File
@@ -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
)
+73
View File
@@ -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))
}
+250
View File
@@ -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;
+88
View File
@@ -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;