6 Commits
Author SHA1 Message Date
zephyr 381df5bd35 umadump/ism: new end info fields
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-10-07 16:35:31 -04:00
zephyr e5ebd1894c umadump/ism: new reward summary fields
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-10-07 13:13:30 -04:00
zephyr c43f6b8292 cmd/racedb: program to load races into an sqlite db
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-10-06 21:00:37 -04:00
zephyr cccf0cc838 umadump/replay: allow fan count in race horse data 2026-10-06 21:00:18 -04:00
zephyr 64f82136cb umadump/replay: trainer name is null for npcs 2026-10-04 09:08:25 -04:00
zephyr 53c7079f46 umadump/replay: decode race replays
ci/woodpecker/push/horsebot Pipeline was successful
ci/woodpecker/push/zenno Pipeline was successful
2026-10-03 22:34:02 -04:00
31 changed files with 19840 additions and 6 deletions
+325
View File
@@ -0,0 +1,325 @@
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
}
+49
View File
@@ -0,0 +1,49 @@
INSERT OR ROLLBACK INTO race (
config_id,
seed,
distance_diff_max,
frame_count,
horse_1,
horse_2,
horse_3,
horse_4,
horse_5,
horse_6,
horse_7,
horse_8,
horse_9,
horse_10,
horse_11,
horse_12,
horse_13,
horse_14,
horse_15,
horse_16,
horse_17,
horse_18
)
VALUES (
$config_id,
$seed,
$distance_diff_max,
$frame_count,
$horse_1,
$horse_2,
$horse_3,
$horse_4,
$horse_5,
$horse_6,
$horse_7,
$horse_8,
$horse_9,
$horse_10,
$horse_11,
$horse_12,
$horse_13,
$horse_14,
$horse_15,
$horse_16,
$horse_17,
$horse_18
)
RETURNING id
+4
View File
@@ -0,0 +1,4 @@
INSERT INTO race_config (type, course_set_id, ground_condition, weather, season, time, num_horse)
VALUES ($type, $course_set_id, $ground_condition, $weather, $season, $time, $num_horse)
ON CONFLICT DO UPDATE SET id=id
RETURNING id
+12
View File
@@ -0,0 +1,12 @@
INSERT INTO race_horse
VALUES (
$race, $horse,
$horse_index, $post,
$finish_order, $finish_time_raw, $finish_time_scaled, $finish_diff_time_from_prev,
$running_style, $running_style_ex,
$active_distance_aptitude, $active_surface_aptitude,
$mood, $mood_coef,
$base_speed, $base_stamina, $base_power, $base_guts, $base_wit,
$popularity, $popularity_rank_left, $popularity_rank_center, $popularity_rank_right,
$start_delay, $spurt_start_distance
)
+19
View File
@@ -0,0 +1,19 @@
INSERT INTO trained_chara (
trainer, trainer_id, trained_chara_id,
chara_id, chara_name, card_id, rarity, mob_id,
raw_speed, raw_stamina, raw_power, raw_guts, raw_wit,
turf, dirt,
sprint, mile, medium, long,
front_runner, pace_chaser, late_surger, end_closer,
fans, rank, rank_score
) VALUES (
$trainer, $trainer_id, $trained_chara_id,
$chara_id, $chara_name, $card_id, $rarity, $mob_id,
$raw_speed, $raw_stamina, $raw_power, $raw_guts, $raw_wit,
$turf, $dirt,
$sprint, $mile, $medium, $long,
$front_runner, $pace_chaser, $late_surger, $end_closer,
$fans, $rank, $rank_score
)
ON CONFLICT DO UPDATE SET id=id
RETURNING id
+74
View File
@@ -0,0 +1,74 @@
package main
import (
"context"
"encoding/json"
"flag"
"log/slog"
"os"
"os/signal"
"path/filepath"
"git.sunturtle.xyz/zephyr/horse/umadump/replay"
)
func main() {
var (
uri, journalMode string
fs []string
)
flag.StringVar(&uri, "db", "race.sqlite3", "output database `URI` or filename")
flag.StringVar(&journalMode, "journal", "memory", "journal `mode` to apply to database connections")
flag.Func("d", "`dir`ectory containing race 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()
}()
db := opendb(uri, journalMode)
ok := 0
for _, n := range fs {
log := slog.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 race replay.Race
d := json.NewDecoder(f)
d.DisallowUnknownFields()
log.Debug("decode")
if err := d.Decode(&race); err != nil {
log.Error("load", slog.Any("err", err))
continue
}
log.Debug("simdata")
simdata, err := replay.DecodeSim(race.SimDataBase64)
if err != nil {
log.Error("simdata", slog.Any("err", err))
continue
}
log.Debug("insert")
err = insert(ctx, db, log, &race, simdata)
if err != nil {
log.Error("insert", slog.Any("err", err))
continue
}
ok++
}
slog.Info("done", slog.Int("races", ok))
}
+234
View File
@@ -0,0 +1,234 @@
-- trained_chara lists horses who appear in races.
CREATE TABLE IF NOT EXISTS trained_chara (
-- id is the DB-internal ID of the Uma.
-- It does not come from the game and should not be supplied in DDL.
id INTEGER PRIMARY KEY,
trainer TEXT NOT NULL,
trainer_id INTEGER NOT NULL, -- 0 for NPC umas e.g. in practice races
trained_chara_id INTEGER NOT NULL,
chara_id INTEGER NOT NULL,
chara_name TEXT NOT NULL,
-- card_id is the Uma herself, e.g. 100201 for [Innocent Silence] Silence Suzuka.
card_id INTEGER NOT NULL,
rarity INTEGER NOT NULL,
mob_id INTEGER NOT NULL, -- 0 for Umas that are not mobs
-- Raw stats.
raw_speed INTEGER NOT NULL,
raw_stamina INTEGER NOT NULL,
raw_power INTEGER NOT NULL,
raw_guts INTEGER NOT NULL,
raw_wit INTEGER NOT NULL,
-- Aptitudes.
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,
-- Other data.
fans INTEGER NOT NULL,
rank INTEGER NOT NULL,
rank_score INTEGER NOT NULL,
UNIQUE (trainer_id, trained_chara_id)
) STRICT;
-- Insert trained chara ID 0 to represent no Uma.
INSERT OR IGNORE INTO trained_chara (
id,
trainer,
trainer_id,
trained_chara_id,
chara_id, chara_name,
card_id,
mob_id,
raw_speed, raw_stamina, raw_power, raw_guts, raw_wit,
turf, dirt,
sprint, mile, medium, long,
front_runner, pace_chaser, late_surger, end_closer,
fans,
rank, rank_score
)
VALUES (0, '', 0, 0, 0, '', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
-- trained_chara_skill lists the skills each observed horse owns.
CREATE TABLE IF NOT EXISTS trained_chara_skill (
trained_chara INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
skill_id INTEGER NOT NULL,
level INTEGER NOT NULL,
UNIQUE (trained_chara, skill_id)
) STRICT;
CREATE INDEX IF NOT EXISTS trained_chara_skill_by_trained_chara ON trained_chara_skill (trained_chara);
-- race_course_set is the accumulated information of observed race courses.
CREATE TABLE IF NOT EXISTS race_course_set (
id INTEGER PRIMARY KEY,
race_track_id INTEGER NOT NULL,
distance INTEGER NOT NULL,
ground INTEGER NOT NULL,
inout INTEGER NOT NULL,
turn INTEGER NOT NULL,
fence_set INTEGER NOT NULL,
float_lane_max INTEGER NOT NULL,
course_set_status_id INTEGER NOT NULL,
finish_time_min INTEGER NOT NULL,
finish_time_min_random_range INTEGER NOT NULL,
finish_time_max INTEGER NOT NULL,
finish_time_max_random_range INTEGER NOT NULL,
section_distance REAL NOT NULL,
distance_type INTEGER NOT NULL,
furlong_number INTEGER NOT NULL,
base_speed REAL NOT NULL
) STRICT;
-- race_config is observed configurations of races including the race type.
CREATE TABLE IF NOT EXISTS race_config (
-- id is the DB-internal ID of the race config.
id INTEGER PRIMARY KEY,
type INTEGER NOT NULL,
course_set_id INTEGER NOT NULL REFERENCES race_course_set (id) ON DELETE RESTRICT,
ground_condition INTEGER NOT NULL,
weather INTEGER NOT NULL,
season INTEGER NOT NULL,
time INTEGER NOT NULL,
num_horse INTEGER NOT NULL,
UNIQUE (type, course_set_id, ground_condition, weather, season, time, num_horse)
) STRICT;
CREATE INDEX IF NOT EXISTS race_config_by_type ON race_config (type);
-- race is all observed races.
CREATE TABLE IF NOT EXISTS race (
-- id is the DB-internal ID of the race.
id INTEGER PRIMARY KEY,
config_id INTEGER NOT NULL REFERENCES race_config (id),
seed INTEGER NOT NULL,
distance_diff_max REAL NOT NULL,
frame_count INTEGER NOT NULL,
-- There is no consistent ID for races, and even race seeds have been found
-- to collide well above the expected rate for a uniform distribution.
-- In order to prevent adding duplicates of races each time the tool is run,
-- we include the list of all horse IDs present in the race.
-- Analysis on the horses in the race should instead use the race_horse table.
-- Horse entries here are 0 where there is actually no horse at that gate.
horse_1 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_2 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_3 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_4 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_5 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_6 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_7 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_8 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_9 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_10 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_11 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_12 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_13 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_14 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_15 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_16 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_17 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_18 INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
UNIQUE (
config_id,
seed,
horse_1,
horse_2,
horse_3,
horse_4,
horse_5,
horse_6,
horse_7,
horse_8,
horse_9,
horse_10,
horse_11,
horse_12,
horse_13,
horse_14,
horse_15,
horse_16,
horse_17,
horse_18
)
) STRICT;
CREATE INDEX IF NOT EXISTS race_by_config ON race (config_id);
-- race_horse pairs each instance of a horse with its condition and performance
-- in a given race.
CREATE TABLE IF NOT EXISTS race_horse (
race INTEGER NOT NULL REFERENCES race (id) ON DELETE CASCADE,
horse INTEGER NOT NULL REFERENCES trained_chara (id) ON DELETE CASCADE,
horse_index INTEGER NOT NULL,
post INTEGER NOT NULL,
finish_order INTEGER NOT NULL,
finish_time_raw REAL NOT NULL,
finish_time_scaled REAL NOT NULL,
finish_diff_time_from_prev REAL NOT NULL,
running_style INTEGER NOT NULL,
running_style_ex INTEGER NOT NULL,
active_distance_aptitude INTEGER NOT NULL,
active_surface_aptitude INTEGER NOT NULL,
mood INTEGER NOT NULL,
mood_coef REAL NOT NULL,
base_speed REAL NOT NULL,
base_stamina REAL NOT NULL,
base_power REAL NOT NULL,
base_guts REAL NOT NULL,
base_wit REAL NOT NULL,
popularity INTEGER NOT NULL,
popularity_rank_left INTEGER NOT NULL,
popularity_rank_center INTEGER NOT NULL,
popularity_rank_right INTEGER NOT NULL,
start_delay REAL NOT NULL,
spurt_start_distance REAL NOT NULL,
PRIMARY KEY (race, horse),
UNIQUE (race, horse_index),
CHECK (horse != 0)
) STRICT;
-- race_frame holds the actual received frames.
CREATE TABLE IF NOT EXISTS race_frame (
race INTEGER NOT NULL REFERENCES race (id) ON DELETE CASCADE,
frame_time REAL NOT NULL,
horse_index INTEGER NOT NULL,
distance REAL NOT NULL,
lane INTEGER NOT NULL, -- scale -4
speed INTEGER NOT NULL, -- scale -2
hp INTEGER NOT NULL,
temptation INTEGER NOT NULL,
block_front_horse_index INTEGER NOT NULL,
PRIMARY KEY (race, frame_time, horse_index)
) STRICT;
-- race_event holds raw received race events.
-- Views such as race_event_skill_activate, race_event_spot_struggle_activate, &c.
-- interpret events per type.
CREATE TABLE IF NOT EXISTS race_event (
race INTEGER NOT NULL REFERENCES race (id) ON DELETE CASCADE,
frame_time REAL NOT NULL,
type INTEGER NOT NULL,
param_0 INTEGER,
param_1 INTEGER,
param_2 INTEGER,
param_3 INTEGER,
param_4 INTEGER,
param_5 INTEGER
) STRICT;
CREATE INDEX IF NOT EXISTS race_event_by_race ON race_event (race);
CREATE INDEX IF NOT EXISTS race_event_by_type ON race_event (type);
+36
View File
@@ -0,0 +1,36 @@
DROP VIEW IF EXISTS race_event_skill_activate;
DROP VIEW IF EXISTS race_event_spot_struggle_activate;
DROP VIEW IF EXISTS race_event_duel_activate;
DROP VIEW IF EXISTS race_event_release_conserve_power;
CREATE VIEW race_event_skill_activate AS
SELECT
race,
frame_time,
param_0 AS user_index,
param_1 AS skill,
param_2 * 0.0001 AS duration,
param_3 AS branch,
param_4 AS hit_bitmask
FROM race_event WHERE type = 3;
CREATE VIEW race_event_spot_struggle_activate AS
SELECT
race,
frame_time,
param_0 AS user_index
FROM race_event WHERE type = 4;
CREATE VIEW race_event_duel_activate AS
SELECT
race,
frame_time,
param_0 AS user_index
FROM race_event WHERE type = 5;
CREATE VIEW race_event_release_conserve_power AS
SELECT
race,
frame_time,
param_0 AS user_index
FROM race_event WHERE type = 6;
+97 -1
View File
@@ -1,6 +1,9 @@
package ism
import "git.sunturtle.xyz/zephyr/horse"
import (
"git.sunturtle.xyz/zephyr/horse"
"git.sunturtle.xyz/zephyr/horse/umadump"
)
type Skill struct {
ID horse.SkillID `json:"skill_id"`
@@ -225,13 +228,106 @@ type AddItem struct {
Number int32 `json:"number"`
}
type PieceData struct {
PieceID int32 `json:"piece_id"`
PieceNum int32 `json:"piece_num"`
}
type UserCard struct {
CardID horse.UmaID `json:"card_id"`
Rarity int8 `json:"rarity"`
TalentLevel int8 `json:"talent_level"`
Skills []Skill `json:"skill_data_array"`
CreateTime string `json:"create_time"`
}
type UserSupportCard struct {
ViewerID int64 `json:"viewer_id"`
SupportCardID horse.SupportCardID `json:"support_card_id"`
Exp int32 `json:"exp"`
LimitBreak int32 `json:"limit_break_count"`
FavoriteFlag int32 `json:"favorite_flag"`
Stock int32 `json:"stock"`
PossessTime string `json:"possess_time"`
}
type RewardAddSupportCardNum struct {
SupportCardID horse.SupportCardID `json:"support_card_id"`
Number int32 `json:"number"`
ItemType int32 `json:"item_type"`
}
type HonorData struct {
HonorID int32 `json:"honor_id"`
Step int32 `json:"step"`
CreateTime string `json:"create_time"`
}
type UserChara struct {
CharaID horse.CharacterID `json:"chara_id"`
TrainingNum int32 `json:"training_num"`
LovePoint int32 `json:"love_point"`
LovePointPool int32 `json:"love_point_pool"`
Fan uint64 `json:"fan"`
}
type UserCloth struct {
ClothID int32 `json:"cloth_id"`
}
type UserMusic struct {
MusicID int32 `json:"music_id"`
AcquisitionTime string `json:"acquisition_time"`
}
type CharaProfileData struct {
CharaID horse.CharacterID `json:"chara_id"`
DataID int32 `json:"data_id"`
NewFlag int32 `json:"new_flag"`
}
type RewardSummary struct {
AddItem []AddItem `json:"add_item_list"`
AddPiece []PieceData `json:"add_piece_list"`
AddCard []UserCard `json:"add_card_list"`
AddCardBonus *RewardSummary `json:"add_card_bonus_info"`
AddSupportCard []UserSupportCard `json:"add_support_card_list"`
AddSupportCardNum []RewardAddSupportCardNum `json:"add_support_card_num_array"`
AddHonor []HonorData `json:"add_honor_list"`
AddChara []UserChara `json:"add_chara_list"`
AddCloth []UserCloth `json:"add_cloth_list"`
AddMusic []UserMusic `json:"add_music_list"`
AddStoryIDs []umadump.Never `json:"add_story_id_array"` // not in my disassembly
AddFCoin int32 `json:"add_fcoin"` // carats
AddPresents int32 `json:"add_present_num"`
AddTotalFan int32 `json:"add_total_fan"`
NewCharaProfiles []CharaProfileData `json:"new_chara_profile_array"`
ForceUpdateHonorID int32 `json:"force_update_honor_id"`
}
type End struct {
Chara SingleModeChara `json:"chara_info"`
RaceConditions []umadump.Never `json:"race_condition_array"`
UncheckedEvents []umadump.Never `json:"unchecked_event_array"`
HomeInfo *umadump.Never `json:"home_info"`
WinSaddleIDs []umadump.Never `json:"win_saddle_id_array"`
EffectedFactors []umadump.Never `json:"effected_factor_array"`
RaceStartInfo *umadump.Never `json:"race_start_info"`
RaceScenario *umadump.Never `json:"race_scenario"`
AddTrophyInfo *umadump.Never `json:"add_trophy_info"`
TrophyRewardInfo *umadump.Never `json:"trophy_reward_info"`
PrevCharaGrade *umadump.Never `json:"prev_chara_grade"`
RaceAddRewardInfo []umadump.Never `json:"race_add_reward_info"`
Missions []umadump.Never `json:"mission_list"`
StoryEventMissions []umadump.Never `json:"story_event_mission_list"`
StoryEventCharaBonuses []umadump.Never `json:"story_event_chara_bonus_list"`
StartDressInfo []umadump.Never `json:"start_dress_info"`
ResumeFactorSelect *umadump.Never `json:"resume_factor_select"`
RaceRandomPrograms []umadump.Never `json:"race_random_program_array"`
RaceRewardLimitMore []umadump.Never `json:"race_reward_limit_more_list"`
SkillFilterSettings []umadump.Never `json:"skill_filter_setting_array"`
RewardSummary RewardSummary `json:"reward_summary_info"`
IsUmaplan bool `json:"is_umaplan"`
}
type ProgressInfo struct {
+13
View File
@@ -0,0 +1,13 @@
package umadump
type Never struct{}
func (Never) AppendText(b []byte) ([]byte, error) {
panic("umadump: attempt to encode value that should never exist")
}
func (Never) MarshalText() ([]byte, error) {
panic("umadump: attempt to encode value that should never exist")
}
func (Never) UnmarshalText(b []byte) error {
panic("umadump: attempt to decode value that should never exist")
}
File diff suppressed because one or more lines are too long
+29
View File
@@ -0,0 +1,29 @@
// Code generated by "stringer -type CardRarity -linecomment -trimprefix Card"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[CardNone-0]
_ = x[CardRare1-1]
_ = x[CardRare2-2]
_ = x[CardRare3-3]
_ = x[CardRare4-4]
_ = x[CardRare5-5]
}
const _CardRarity_name = "NONERare1Rare2Rare3Rare4Rare5"
var _CardRarity_index = [...]uint8{0, 4, 9, 14, 19, 24, 29}
func (i CardRarity) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_CardRarity_index)-1 {
return "CardRarity(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _CardRarity_name[_CardRarity_index[idx]:_CardRarity_index[idx+1]]
}
+35
View File
@@ -0,0 +1,35 @@
// Code generated by "stringer -type CharaGradeType -linecomment -trimprefix CharaGrade"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[CharaGradeNone-0]
_ = x[CharaGradeDebut-1]
_ = x[CharaGradeNoWin-2]
_ = x[CharaGradeOpen-3]
_ = x[CharaGradeG3Silver-4]
_ = x[CharaGradeG3Gold-5]
_ = x[CharaGradeG2Silver-6]
_ = x[CharaGradeG2Gold-7]
_ = x[CharaGradeG1Bronze-8]
_ = x[CharaGradeG1Silver-9]
_ = x[CharaGradeG1Gold-10]
_ = x[charaGradeMax-11]
}
const _CharaGradeType_name = "NONEDebutNoWinOpenG3SilverG3GoldG2SilverG2GoldG1BronzeG1SilverG1GoldcharaGradeMax"
var _CharaGradeType_index = [...]uint8{0, 4, 9, 14, 18, 26, 32, 40, 46, 54, 62, 68, 81}
func (i CharaGradeType) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_CharaGradeType_index)-1 {
return "CharaGradeType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _CharaGradeType_name[_CharaGradeType_index[idx]:_CharaGradeType_index[idx+1]]
}
@@ -0,0 +1,27 @@
// Code generated by "stringer -type CourseDistanceType -linecomment -trimprefix Distance"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[DistanceSprint-1]
_ = x[DistanceMile-2]
_ = x[DistanceMedium-3]
_ = x[DistanceLong-4]
}
const _CourseDistanceType_name = "ShortMileMiddleLong"
var _CourseDistanceType_index = [...]uint8{0, 5, 9, 15, 19}
func (i CourseDistanceType) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_CourseDistanceType_index)-1 {
return "CourseDistanceType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _CourseDistanceType_name[_CourseDistanceType_index[idx]:_CourseDistanceType_index[idx+1]]
}
+27
View File
@@ -0,0 +1,27 @@
// Code generated by "stringer -type GroundCondition -linecomment"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[GroundFirm-1]
_ = x[GroundGood-2]
_ = x[GroundSoft-3]
_ = x[GroundHeavy-4]
}
const _GroundCondition_name = "GoodSoftHardBad"
var _GroundCondition_index = [...]uint8{0, 4, 8, 12, 15}
func (i GroundCondition) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_GroundCondition_index)-1 {
return "GroundCondition(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _GroundCondition_name[_GroundCondition_index[idx]:_GroundCondition_index[idx+1]]
}
+28
View File
@@ -0,0 +1,28 @@
// Code generated by "stringer -type InitialLaneType -trimprefix InitialLane"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[InitialLaneExtraSpaceAfter9-1]
_ = x[InitialLaneEquidistant-2]
_ = x[InitialLaneExtraSpaceAfter14-3]
_ = x[InitialLaneExtraSpaceAfter8-4]
_ = x[initialLaneMax-5]
}
const _InitialLaneType_name = "ExtraSpaceAfter9EquidistantExtraSpaceAfter14ExtraSpaceAfter8initialLaneMax"
var _InitialLaneType_index = [...]uint8{0, 16, 27, 44, 60, 74}
func (i InitialLaneType) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_InitialLaneType_index)-1 {
return "InitialLaneType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _InitialLaneType_name[_InitialLaneType_index[idx]:_InitialLaneType_index[idx+1]]
}
@@ -0,0 +1,26 @@
// Code generated by "stringer -type MainStoryRaceGimmickType -linecomment"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[GimmickNone-0]
_ = x[GimmickSpecial00-1]
_ = x[gimmickMax-2]
}
const _MainStoryRaceGimmickType_name = "NONESpecial_00gimmickMax"
var _MainStoryRaceGimmickType_index = [...]uint8{0, 4, 14, 24}
func (i MainStoryRaceGimmickType) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_MainStoryRaceGimmickType_index)-1 {
return "MainStoryRaceGimmickType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _MainStoryRaceGimmickType_name[_MainStoryRaceGimmickType_index[idx]:_MainStoryRaceGimmickType_index[idx+1]]
}
+29
View File
@@ -0,0 +1,29 @@
// Code generated by "stringer -type Motivation -linecomment"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[MotivationNone-0]
_ = x[MotivationAwful-1]
_ = x[MotivationBad-2]
_ = x[MotivationNormal-3]
_ = x[MotivationGood-4]
_ = x[MotivationGreat-5]
}
const _Motivation_name = "NoneMinLowMiddleHighMax"
var _Motivation_index = [...]uint8{0, 4, 7, 10, 16, 20, 23}
func (i Motivation) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_Motivation_index)-1 {
return "Motivation(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Motivation_name[_Motivation_index[idx]:_Motivation_index[idx+1]]
}
+32
View File
@@ -0,0 +1,32 @@
// Code generated by "stringer -type ProperGrade -trimprefix Aptitude"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[AptitudeNull-0]
_ = x[AptitudeG-1]
_ = x[AptitudeF-2]
_ = x[AptitudeE-3]
_ = x[AptitudeD-4]
_ = x[AptitudeC-5]
_ = x[AptitudeB-6]
_ = x[AptitudeA-7]
_ = x[AptitudeS-8]
}
const _ProperGrade_name = "NullGFEDCBAS"
var _ProperGrade_index = [...]uint8{0, 4, 5, 6, 7, 8, 9, 10, 11, 12}
func (i ProperGrade) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_ProperGrade_index)-1 {
return "ProperGrade(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _ProperGrade_name[_ProperGrade_index[idx]:_ProperGrade_index[idx+1]]
}
+29
View File
@@ -0,0 +1,29 @@
// Code generated by "stringer -type RaceDifficulty -trimprefix Difficulty"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[DifficultyEasy-1]
_ = x[DifficultyNormal-2]
_ = x[DifficultyHard-3]
_ = x[DifficultyVeryHard-4]
_ = x[DifficultyExtreme-5]
_ = x[difficultyMax-6]
}
const _RaceDifficulty_name = "EasyNormalHardVeryHardExtremedifficultyMax"
var _RaceDifficulty_index = [...]uint8{0, 4, 10, 14, 22, 29, 42}
func (i RaceDifficulty) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_RaceDifficulty_index)-1 {
return "RaceDifficulty(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _RaceDifficulty_name[_RaceDifficulty_index[idx]:_RaceDifficulty_index[idx+1]]
}
+29
View File
@@ -0,0 +1,29 @@
// Code generated by "stringer -type RaceTime -trimprefix Time"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[TimeMin-0]
_ = x[TimeMorning-1]
_ = x[TimeDaytime-2]
_ = x[TimeEvening-3]
_ = x[TimeNight-4]
_ = x[TimeMax-5]
}
const _RaceTime_name = "MinMorningDaytimeEveningNightMax"
var _RaceTime_index = [...]uint8{0, 3, 10, 17, 24, 29, 32}
func (i RaceTime) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_RaceTime_index)-1 {
return "RaceTime(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _RaceTime_name[_RaceTime_index[idx]:_RaceTime_index[idx+1]]
}
+48
View File
@@ -0,0 +1,48 @@
// Code generated by "stringer -type RaceType -linecomment -trimprefix RaceType"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[RaceTypeNone-0]
_ = x[RaceTypePvP-1]
_ = x[RaceTypeTutorial-2]
_ = x[RaceTypeStory-3]
_ = x[RaceTypeStoryCondition-4]
_ = x[RaceTypeChampions-5]
_ = x[RaceTypeSingle-6]
_ = x[RaceTypeSingleModeScenarioTeamRace-7]
_ = x[RaceTypeRoomMatch-8]
_ = x[RaceTypePractice-9]
_ = x[RaceTypeDaily-10]
_ = x[RaceTypeTeamBuilding-11]
_ = x[RaceTypeLegend-12]
_ = x[RaceTypeChallengeMatch-13]
_ = x[RaceTypeTeamStadium-14]
_ = x[RaceTypeHeroes-16]
_ = x[raceTypeMaxInclusive-16]
}
const (
_RaceType_name_0 = "NONEPvPTutorialStoryStoryConditionChampionsSingleSingleModeScenarioTeamRaceRoomMatchPracticeDailyTeamBuildingLegendChallengeMatchTeamStadium"
_RaceType_name_1 = "Heroes"
)
var (
_RaceType_index_0 = [...]uint8{0, 4, 7, 15, 20, 34, 43, 49, 75, 84, 92, 97, 109, 115, 129, 140}
)
func (i RaceType) String() string {
switch {
case 0 <= i && i <= 14:
return _RaceType_name_0[_RaceType_index_0[i]:_RaceType_index_0[i+1]]
case i == 16:
return _RaceType_name_1
default:
return "RaceType(" + strconv.FormatInt(int64(i), 10) + ")"
}
}
+29
View File
@@ -0,0 +1,29 @@
// Code generated by "stringer -type RaceWeather -trimprefix Weather"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[WeatherMin-0]
_ = x[WeatherSunny-1]
_ = x[WeatherCloudy-2]
_ = x[WeatherRainy-3]
_ = x[WeatherSnow-4]
_ = x[WeatherMax-5]
}
const _RaceWeather_name = "MinSunnyCloudyRainySnowMax"
var _RaceWeather_index = [...]uint8{0, 3, 8, 14, 19, 23, 26}
func (i RaceWeather) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_RaceWeather_index)-1 {
return "RaceWeather(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _RaceWeather_name[_RaceWeather_index[idx]:_RaceWeather_index[idx+1]]
}
File diff suppressed because it is too large Load Diff
+182
View File
@@ -0,0 +1,182 @@
package replay_test
import (
_ "embed"
"encoding/json"
"strings"
"testing"
"git.sunturtle.xyz/zephyr/horse/umadump/replay"
)
//go:embed Mihono_Bourbon-74.2421s-20260829.json
var testRace string
func TestRaceUnmarshal(t *testing.T) {
// A replay is simply too large to bother converting by hand for a unit test.
// Just test that we can unmarshal and certain important fields are populated.
d := json.NewDecoder(strings.NewReader(testRace))
d.DisallowUnknownFields()
var got replay.Race
if err := d.Decode(&got); err != nil {
t.Error(err)
}
if got.RaceType != replay.RaceTypeChampions {
t.Errorf("wrong race type: want %v, got %v", replay.RaceTypeChampions, got.RaceType)
}
if got.RandomSeed != 1667907263 {
t.Errorf("wrong random seed: want 1667907263, got %d", got.RandomSeed)
}
if got.RaceCourseSet.ID != 10903 {
t.Errorf("wrong race course: want 10903, got %d", got.RaceCourseSet.ID)
}
if got.GroundCondition != replay.GroundFirm {
t.Errorf("wrong ground condition: want %v, got %v", replay.GroundFirm, got.GroundCondition)
}
if got.Weather != replay.WeatherCloudy {
t.Errorf("wrong weather: want %v, got %v", replay.WeatherCloudy, got.Weather)
}
if got.NumRaceHorses != 9 {
t.Errorf("wrong number of race horses: want 9, got %d", got.NumRaceHorses)
}
if len(got.PlayerTeamMemberArray) != 3 {
t.Errorf("wrong number of team members: want 3, got %d", len(got.PlayerTeamMemberArray))
}
for _, m := range got.PlayerTeamMemberArray {
if m.CharaID == 0 {
t.Error("missing team member chara id")
}
if m.FinishTimeRaw == 0 {
t.Error("missing team member finish time raw")
}
if m.RaceParam.RawSpeed == 0 {
t.Error("missing team member race param raw speed")
}
if m.RaceParam.BaseSpeed == 0 {
t.Error("missing team member race param base speed")
}
if m.ResponseHorseData.ViewerID == 0 {
t.Error("missing team member response horse data viewer id")
}
if m.ResponseHorseData.TrainedCharaID == 0 {
t.Error("missing team member response horse data trained chara id")
}
if m.ResponseHorseData.CardID == 0 {
t.Error("missing team member response horse data card id")
}
if len(m.ResponseHorseData.SkillArray) == 0 {
t.Error("missing team member response horse data skill array")
}
if m.ResponseHorseData.Speed == 0 {
t.Error("missing team member response horse data speed")
}
if m.ResponseHorseData.RunningStyle == 0 {
t.Error("missing team member response horse data running style")
}
if m.ActiveProperDistance == 0 {
t.Error("missing team member active distance aptitude")
}
if m.ActiveProperGroundType == 0 {
t.Error("missing team member active surface aptitude")
}
if m.TrainedCharaData.ID == 0 {
t.Error("missing team member trained chara id")
}
if m.TrainedCharaData.CardID == 0 {
t.Error("missing team member trained chara card id")
}
if m.TrainedCharaData.Speed == 0 {
t.Error("missing team member trained chara speed")
}
if m.TrainedCharaData.RunningStyle == 0 {
t.Error("missing team member trained chara running style")
}
if m.TrainedCharaData.CreateTime == "" {
t.Error("missing team member trained chara create time")
}
if len(m.TrainedCharaData.AcquiredSkillArray) == 0 {
t.Error("missing team member trained chara acquired skill array")
}
}
if len(got.RaceHorse) != int(got.NumRaceHorses) {
t.Errorf("wrong number of race horses: want %d, got %d", got.NumRaceHorses, len(got.RaceHorse))
}
zeroFinish, zeroDelta := 0, 0
for _, m := range got.RaceHorse {
if m.CharaID == 0 {
t.Error("missing race horse chara id")
}
if m.FinishOrder == 0 {
zeroFinish++
}
if m.FinishTimeRaw == 0 {
t.Error("missing race horse finish time raw")
}
if m.FinishDiffTimeFromPrev == 0 {
zeroDelta++
}
if m.RaceParam.RawSpeed == 0 {
t.Error("missing race horse race param raw speed")
}
if m.RaceParam.BaseSpeed == 0 {
t.Error("missing race horse race param base speed")
}
if m.ResponseHorseData.ViewerID == 0 {
t.Error("missing race horse response horse data viewer id")
}
if m.ResponseHorseData.TrainedCharaID == 0 {
t.Error("missing race horse response horse data trained chara id")
}
if m.ResponseHorseData.CardID == 0 {
t.Error("missing race horse response horse data card id")
}
if len(m.ResponseHorseData.SkillArray) == 0 {
t.Error("missing race horse response horse data skill array")
}
if m.ResponseHorseData.Speed == 0 {
t.Error("missing race horse response horse data speed")
}
if m.ResponseHorseData.RunningStyle == 0 {
t.Error("missing race horse response horse data running style")
}
if m.ActiveProperDistance == 0 {
t.Error("missing race horse active distance aptitude")
}
if m.ActiveProperGroundType == 0 {
t.Error("missing race horse active surface aptitude")
}
if m.TrainedCharaData.ID == 0 {
t.Error("missing race horse trained chara id")
}
if m.TrainedCharaData.CardID == 0 {
t.Error("missing race horse trained chara card id")
}
if m.TrainedCharaData.Speed == 0 {
t.Error("missing race horse trained chara speed")
}
if m.TrainedCharaData.RunningStyle == 0 {
t.Error("missing race horse trained chara running style")
}
if m.TrainedCharaData.CreateTime == "" {
t.Error("missing race horse trained chara create time")
}
if len(m.TrainedCharaData.AcquiredSkillArray) == 0 {
t.Error("missing race horse trained chara acquired skill array")
}
}
if zeroFinish != 1 {
t.Errorf("%d horses in index 0 finish", zeroFinish)
}
if zeroDelta != 1 {
t.Errorf("%d horses with zero finish time delta", zeroDelta)
}
if got.SimDataBase64 != encodedSimdata {
t.Errorf("wrong simdata:\nwant %s\n got %s", encodedSimdata, got.SimDataBase64)
}
if len(got.HorseIndexByFinishOrder) != len(got.RaceHorse) {
t.Errorf("length of horseIndexByFinishOrder (%d) doesn't match length of raceHorse (%d)", len(got.HorseIndexByFinishOrder), len(got.RaceHorse))
}
if len(got.HorseIndexByPopularity) != len(got.RaceHorse) {
t.Errorf("length of horseIndexByPopularity (%d) doesn't match length of raceHorse (%d)", len(got.HorseIndexByPopularity), len(got.RaceHorse))
}
}
@@ -0,0 +1,27 @@
// Code generated by "stringer -type ResultBoardConditionType -linecomment"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[ResultBoardTurfNone-1]
_ = x[ResultBoardTurfDirt-2]
_ = x[ResultBoardDirtNone-3]
_ = x[ResultBoardDirtTurf-4]
}
const _ResultBoardConditionType_name = "Turf_NoneTurf_DirtDirt_NoneDirt_Turf"
var _ResultBoardConditionType_index = [...]uint8{0, 9, 18, 27, 36}
func (i ResultBoardConditionType) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_ResultBoardConditionType_index)-1 {
return "ResultBoardConditionType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _ResultBoardConditionType_name[_ResultBoardConditionType_index[idx]:_ResultBoardConditionType_index[idx+1]]
}
+27
View File
@@ -0,0 +1,27 @@
// Code generated by "stringer -type Rotation -trimprefix Rotation"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[RotationRight-1]
_ = x[RotationLeft-2]
_ = x[RotationStraightRight-3]
_ = x[RotationStraightLeft-4]
}
const _Rotation_name = "RightLeftStraightRightStraightLeft"
var _Rotation_index = [...]uint8{0, 5, 9, 22, 34}
func (i Rotation) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_Rotation_index)-1 {
return "Rotation(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Rotation_name[_Rotation_index[idx]:_Rotation_index[idx+1]]
}
+25
View File
@@ -0,0 +1,25 @@
// Code generated by "stringer -type RunningStyleEx -linecomment"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[StyleExNone-0]
_ = x[StyleExRunaway-1]
}
const _RunningStyleEx_name = "NONEOonige"
var _RunningStyleEx_index = [...]uint8{0, 4, 10}
func (i RunningStyleEx) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_RunningStyleEx_index)-1 {
return "RunningStyleEx(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _RunningStyleEx_name[_RunningStyleEx_index[idx]:_RunningStyleEx_index[idx+1]]
}
+30
View File
@@ -0,0 +1,30 @@
// Code generated by "stringer -type Season -trimprefix Season"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[SeasonMin-0]
_ = x[SeasonSpring-1]
_ = x[SeasonSummer-2]
_ = x[SeasonFall-3]
_ = x[SeasonWinter-4]
_ = x[SeasonCherryBlossom-5]
_ = x[SeasonMax-6]
}
const _Season_name = "MinSpringSummerFallWinterCherryBlossomMax"
var _Season_index = [...]uint8{0, 3, 9, 15, 19, 25, 38, 41}
func (i Season) String() string {
idx := int(i) - 0
if i < 0 || idx >= len(_Season_index)-1 {
return "Season(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Season_name[_Season_index[idx]:_Season_index[idx+1]]
}
File diff suppressed because one or more lines are too long
+27
View File
@@ -0,0 +1,27 @@
// Code generated by "stringer -type TurfVisionType -trimprefix TurfVision"; DO NOT EDIT.
package replay
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[TurfVisionURA-1]
_ = x[TurfVisionNAU-2]
_ = x[TurfVisionStand-3]
_ = x[turfVisionMax-4]
}
const _TurfVisionType_name = "URANAUStandturfVisionMax"
var _TurfVisionType_index = [...]uint8{0, 3, 6, 11, 24}
func (i TurfVisionType) String() string {
idx := int(i) - 1
if i < 1 || idx >= len(_TurfVisionType_index)-1 {
return "TurfVisionType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _TurfVisionType_name[_TurfVisionType_index[idx]:_TurfVisionType_index[idx+1]]
}