3 Commits

Author SHA1 Message Date
zephyr aca6da187d output support card lb/exp and gold skill counts
Fixes #1.
Fixes #2.
2026-07-29 15:46:58 -04:00
zephyr f5d102718d handle new umadump output shapes 2026-07-29 15:19:17 -04:00
zephyr 6864472fdd use base name in output 2026-07-28 07:31:59 -04:00
7 changed files with 10746 additions and 808 deletions
+219 -22
View File
@@ -1,14 +1,35 @@
package main package main
import "git.sunturtle.xyz/zephyr/horse" import (
"bytes"
"encoding/json"
"io"
"time"
"git.sunturtle.xyz/zephyr/horse"
)
type SupportCardID int32 // package horse doesn't have this yet type SupportCardID int32 // package horse doesn't have this yet
type OldWorkSingleIdleMode struct {
Chara SingleModeChara `json:"charaInfo"`
StartTime int64 `json:"startTime"`
EndTime int64 `json:"endTime"`
ProgressLog OldIdleSingleModeProgressLog `json:"progressLogInfo"`
}
type ProgressInfo struct {
Chara SingleModeChara `json:"chara_info"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
DressID int32 `json:"dress_id"`
}
type WorkIdleSingleMode struct { type WorkIdleSingleMode struct {
Chara SingleModeChara `json:"charaInfo"` ProgressInfo ProgressInfo `json:"progress_info"`
StartTime int64 `json:"startTime"` ProgressLog IdleSingleModeProgressLog `json:"progress_log_info"`
EndTime int64 `json:"endTime"` End End `json:"end_info"`
ProgressLog IdleSingleModeProgressLog `json:"progressLogInfo"` OldChara *SingleModeChara `json:"chara_info"`
} }
type Skill struct { type Skill struct {
@@ -40,6 +61,7 @@ type GroupOuting struct {
type Evaluation struct { type Evaluation struct {
TargetID int32 `json:"target_id"` TargetID int32 `json:"target_id"`
TrainingPartner int32 `json:"training_partner_id"`
Evaluation int32 `json:"evaluation"` Evaluation int32 `json:"evaluation"`
IsOuting int32 `json:"is_outing"` IsOuting int32 `json:"is_outing"`
StoryStep int32 `json:"story_step"` StoryStep int32 `json:"story_step"`
@@ -126,17 +148,22 @@ type SingleModeChara struct {
SkillUpgrades []SingleModeSkillUpgrade `json:"skill_upgrade_info_array"` SkillUpgrades []SingleModeSkillUpgrade `json:"skill_upgrade_info_array"`
} }
type CharaEffectLog struct { type OldCharaEffectLog struct {
CharaEffectID int32 `json:"charaEffectId"` CharaEffectID int32 `json:"charaEffectId"`
IsActive bool `json:"isActive"` IsActive bool `json:"isActive"`
} }
type CharaEffectLog struct {
CharaEffectID int32 `json:"chara_effect_id"`
IsActive bool `json:"is_active"`
}
type IdleSingleModeSignedInt struct { type IdleSingleModeSignedInt struct {
Sign int32 `json:"sign"` Sign int32 `json:"sign"`
Value int32 `json:"value"` Value int32 `json:"value"`
} }
type IdleSingleModeGainInfo struct { type OldIdleSingleModeGainInfo struct {
Speed IdleSingleModeSignedInt `json:"speed"` Speed IdleSingleModeSignedInt `json:"speed"`
Stamina IdleSingleModeSignedInt `json:"stamina"` Stamina IdleSingleModeSignedInt `json:"stamina"`
Power IdleSingleModeSignedInt `json:"power"` Power IdleSingleModeSignedInt `json:"power"`
@@ -161,19 +188,59 @@ type IdleSingleModeGainInfo struct {
SkillTips []SkillTip `json:"skillTips"` SkillTips []SkillTip `json:"skillTips"`
} }
type IdleSingleModeSupportCardGain struct { type IdleSingleModeGainInfo struct {
SupportCardID SupportCardID `json:"supportCardId"` Speed IdleSingleModeSignedInt `json:"speed"`
GainInfo IdleSingleModeGainInfo `json:"gainInfo"` Stamina IdleSingleModeSignedInt `json:"stamina"`
Power IdleSingleModeSignedInt `json:"power"`
Wiz IdleSingleModeSignedInt `json:"wiz"`
Guts IdleSingleModeSignedInt `json:"guts"`
MaxSpeed int32 `json:"max_speed"`
MaxStamina int32 `json:"max_stamina"`
MaxPower int32 `json:"max_power"`
MaxWiz int32 `json:"max_wiz"`
MaxGuts int32 `json:"max_guts"`
ProperDistanceShort horse.AptitudeLevel `json:"proper_distance_short"`
ProperDistanceMile horse.AptitudeLevel `json:"proper_distance_mile"`
ProperDistanceMiddle horse.AptitudeLevel `json:"proper_distance_middle"`
ProperDistanceLong horse.AptitudeLevel `json:"proper_distance_long"`
ProperRunningStyleNige horse.AptitudeLevel `json:"proper_running_style_nige"`
ProperRunningStyleSenko horse.AptitudeLevel `json:"proper_running_style_senko"`
ProperRunningStyleSashi horse.AptitudeLevel `json:"proper_running_style_sashi"`
ProperRunningStyleOikomi horse.AptitudeLevel `json:"proper_running_style_oikomi"`
ProperGroundTurf horse.AptitudeLevel `json:"proper_ground_turf"`
ProperGroundDirt horse.AptitudeLevel `json:"proper_ground_dirt"`
SkillPoint int32 `json:"skill_point"`
SkillTips []SkillTip `json:"skill_tips_array"`
} }
type Factor struct { type OldIdleSingleModeSupportCardGain struct {
SupportCardID SupportCardID `json:"supportCardId"`
GainInfo OldIdleSingleModeGainInfo `json:"gainInfo"`
}
type IdleSingleModeSupportCardGain struct {
SupportCardID SupportCardID `json:"support_card_id"`
GainInfo IdleSingleModeGainInfo `json:"gain_info"`
}
type OldFactor struct {
FactorID horse.SparkID `json:"factorId"` FactorID horse.SparkID `json:"factorId"`
Level int32 `json:"level"` Level int32 `json:"level"`
} }
type Factor struct {
FactorID horse.SparkID `json:"factor_id"`
Level int32 `json:"level"`
}
type OldIdleSingleModeSuccessionFactorGain struct {
Year int32 `json:"year"`
GainFactorInfo []OldFactor `json:"gainFactorInfoArray"`
}
type IdleSingleModeSuccessionFactorGain struct { type IdleSingleModeSuccessionFactorGain struct {
Year int32 `json:"year"` Year int32 `json:"year"`
GainFactorInfo []Factor `json:"gainFactorInfoArray"` GainFactorInfo []Factor `json:"gain_factor_info_array"`
} }
type SingleRaceHistory struct { type SingleRaceHistory struct {
@@ -184,7 +251,7 @@ type SingleRaceHistory struct {
RunningStyle int32 `json:"running_style"` RunningStyle int32 `json:"running_style"`
ResultRank int32 `json:"result_rank"` ResultRank int32 `json:"result_rank"`
FrameOrder int32 `json:"frame_order"` FrameOrder int32 `json:"frame_order"`
NpcCount int32 `json:"npc_count"` NPCCount int32 `json:"npc_count"`
} }
type RaceReward struct { type RaceReward struct {
@@ -219,13 +286,143 @@ type IdleSingleModeRaceHistory struct {
LoseTipsID int32 `json:"lose_tips_id"` LoseTipsID int32 `json:"lose_tips_id"`
} }
type IdleSingleModeProgressLog struct { type OldIdleSingleModeProgressLog struct {
CharaEffectLog []CharaEffectLog `json:"charaEffectLog"` CharaEffectLog []OldCharaEffectLog `json:"charaEffectLog"`
SupportCardGains []IdleSingleModeSupportCardGain `json:"supportCardGainInfo"` SupportCardGains []OldIdleSingleModeSupportCardGain `json:"supportCardGainInfo"`
EventGain IdleSingleModeGainInfo `json:"eventGainInfo"` EventGain OldIdleSingleModeGainInfo `json:"eventGainInfo"`
SuccessionGain IdleSingleModeGainInfo `json:"successionGainInfo"` SuccessionGain OldIdleSingleModeGainInfo `json:"successionGainInfo"`
SuccessionFactorGains []IdleSingleModeSuccessionFactorGain `json:"successionFactorGainArray"` SuccessionFactorGains []OldIdleSingleModeSuccessionFactorGain `json:"successionFactorGainArray"`
RaceHistory []IdleSingleModeRaceHistory `json:"raceHistoryArray"` RaceHistory []IdleSingleModeRaceHistory `json:"raceHistoryArray"`
GainSkillIDs []horse.SkillID `json:"gainSkillIdArray"` GainSkillIDs []horse.SkillID `json:"gainSkillIdArray"`
TotalSkillPoint int32 `json:"totalSkillPoint"` TotalSkillPoint int32 `json:"totalSkillPoint"`
}
type IdleSingleModeProgressLog struct {
CharaEffectLog []CharaEffectLog `json:"chara_effect_log_array"`
SupportCardGains []IdleSingleModeSupportCardGain `json:"support_card_gain_info_array"`
EventGain IdleSingleModeGainInfo `json:"event_gain_info"`
SuccessionGain IdleSingleModeGainInfo `json:"succession_gain_info"`
SuccessionFactorGains []IdleSingleModeSuccessionFactorGain `json:"succession_factor_gain_array"`
RaceHistory []IdleSingleModeRaceHistory `json:"race_history_array"`
GainSkillIDs []horse.SkillID `json:"gain_skill_id_array"`
TotalSkillPoint int32 `json:"total_skill_point"`
}
type AddItem struct {
ItemID int32 `json:"item_id"`
Number int32 `json:"number"`
}
type RewardSummary struct {
AddItem []AddItem `json:"add_item_list"`
}
type End struct {
Chara SingleModeChara `json:"chara_info"`
CharaButWerseterIsOverworked *SingleModeChara `json:"chara_starting_info"`
RewardSummary RewardSummary `json:"reward_summary_info"`
}
func loadISM(r io.Reader) (WorkIdleSingleMode, error) {
// Since we have two potential types to read at the top level,
// decode one message and try it both ways.
var b json.RawMessage
var err error
if err = json.NewDecoder(r).Decode(&b); err != nil {
return WorkIdleSingleMode{}, err
}
{
var ism WorkIdleSingleMode
d := json.NewDecoder(bytes.NewReader(b))
d.DisallowUnknownFields()
err = d.Decode(&ism)
if err == nil {
// NOTE(zeph): this is for no error
// If old fields are populated, move them to the correct ones.
if ism.End.CharaButWerseterIsOverworked != nil {
ism.End.Chara = *ism.End.CharaButWerseterIsOverworked
ism.End.CharaButWerseterIsOverworked = nil
}
if ism.OldChara != nil {
ism.ProgressInfo.Chara = *ism.OldChara
ism.OldChara = nil
}
return ism, nil
}
}
{
var ism OldWorkSingleIdleMode
d := json.NewDecoder(bytes.NewReader(b))
d.DisallowUnknownFields()
// Use a new error variable here.
// If this fails, we want to return the reason we didn't use the API type instead.
if err := d.Decode(&ism); err == nil {
// NOTE(zeph): this is for no error
return unold(ism), nil
}
}
// NOTE(zeph): this is the error from decoding the API type, not the validation type.
return WorkIdleSingleMode{}, err
}
func unold(old OldWorkSingleIdleMode) WorkIdleSingleMode {
return WorkIdleSingleMode{
ProgressInfo: ProgressInfo{
Chara: old.Chara,
StartTime: time.Unix(old.StartTime, 0).Format(time.DateTime),
EndTime: time.Unix(old.EndTime, 0).Format(time.DateTime),
},
ProgressLog: IdleSingleModeProgressLog{
CharaEffectLog: unoldEffects(old.ProgressLog.CharaEffectLog),
SupportCardGains: unoldCardGains(old.ProgressLog.SupportCardGains),
EventGain: IdleSingleModeGainInfo(old.ProgressLog.EventGain),
SuccessionGain: IdleSingleModeGainInfo(old.ProgressLog.SuccessionGain),
SuccessionFactorGains: unoldFactorGains(old.ProgressLog.SuccessionFactorGains),
RaceHistory: old.ProgressLog.RaceHistory,
GainSkillIDs: old.ProgressLog.GainSkillIDs,
TotalSkillPoint: old.ProgressLog.TotalSkillPoint,
},
// We could try to roll up starting numbers plus gains,
// but only a few of these files exist anyway.
// Not worth the effort.
End: End{},
}
}
func unoldEffects(olds []OldCharaEffectLog) []CharaEffectLog {
new := make([]CharaEffectLog, len(olds))
for i, old := range olds {
new[i] = CharaEffectLog(old)
}
return new
}
func unoldCardGains(olds []OldIdleSingleModeSupportCardGain) []IdleSingleModeSupportCardGain {
new := make([]IdleSingleModeSupportCardGain, len(olds))
for i, old := range olds {
new[i] = IdleSingleModeSupportCardGain{
SupportCardID: old.SupportCardID,
GainInfo: IdleSingleModeGainInfo(old.GainInfo),
}
}
return new
}
func unoldFactorGains(olds []OldIdleSingleModeSuccessionFactorGain) []IdleSingleModeSuccessionFactorGain {
new := make([]IdleSingleModeSuccessionFactorGain, len(olds))
for i, old := range olds {
new[i] = IdleSingleModeSuccessionFactorGain{
Year: old.Year,
GainFactorInfo: unoldFactors(old.GainFactorInfo),
}
}
return new
}
func unoldFactors(olds []OldFactor) []Factor {
new := make([]Factor, len(olds))
for i, old := range olds {
new[i] = Factor(old)
}
return new
} }
+4761 -776
View File
File diff suppressed because it is too large Load Diff
+4 -6
View File
@@ -3,10 +3,10 @@ package main
import ( import (
"bufio" "bufio"
"encoding/csv" "encoding/csv"
"encoding/json"
"flag" "flag"
"log/slog" "log/slog"
"os" "os"
"path/filepath"
"slices" "slices"
) )
@@ -45,15 +45,13 @@ func dofile(w *csv.Writer, name string) {
} }
defer f.Close() defer f.Close()
var ism WorkIdleSingleMode ism, err := loadISM(f)
d := json.NewDecoder(f) if err != nil {
d.DisallowUnknownFields()
if err := d.Decode(&ism); err != nil {
log.Error("decode", slog.Any("err", err)) log.Error("decode", slog.Any("err", err))
return return
} }
for row := range ismRows(name, ism) { for row := range ismRows(filepath.Base(name), ism) {
if err := w.Write(row.csv()); err != nil { if err := w.Write(row.csv()); err != nil {
log.Error("write row", slog.Any("err", err)) log.Error("write row", slog.Any("err", err))
} }
+61 -4
View File
@@ -2,9 +2,9 @@ package main
import ( import (
"iter" "iter"
"log/slog"
"reflect" "reflect"
"strconv" "strconv"
"strings"
) )
type Row struct { type Row struct {
@@ -20,13 +20,26 @@ type Row struct {
Scenario int Scenario int
Character int Character int
Deck1 int Deck1 int
Deck1LB int
Deck1Exp int
Deck2 int Deck2 int
Deck2LB int
Deck2Exp int
Deck3 int Deck3 int
Deck3LB int
Deck3Exp int
Deck4 int Deck4 int
Deck4LB int
Deck4Exp int
Deck5 int Deck5 int
Deck5LB int
Deck5Exp int
Deck6 int Deck6 int
Deck6LB int
Deck6Exp int
Races int Races int
Wins int Wins int
GoldSkills int
NightOwl bool // chara effect id 1 NightOwl bool // chara effect id 1
Slacker bool // 2 Slacker bool // 2
SkinOutbreak bool // 3 SkinOutbreak bool // 3
@@ -38,6 +51,7 @@ type Row struct {
HotTopic bool // 9 HotTopic bool // 9
PracticePerfect bool // 10 PracticePerfect bool // 10
VeryGoodAtTraining bool // 11 VeryGoodAtTraining bool // 11
OtherConds []string
} }
var headers []string var headers []string
@@ -49,6 +63,14 @@ func init() {
} }
} }
func elem[T any, S ~[]T](s S, i int) T {
if i < 0 || i >= len(s) {
var zero T
return zero
}
return s[i]
}
func ismRows(key string, ism WorkIdleSingleMode) iter.Seq[Row] { func ismRows(key string, ism WorkIdleSingleMode) iter.Seq[Row] {
return func(yield func(Row) bool) { return func(yield func(Row) bool) {
pl := ism.ProgressLog pl := ism.ProgressLog
@@ -60,25 +82,45 @@ func ismRows(key string, ism WorkIdleSingleMode) iter.Seq[Row] {
} }
} }
var conditions [11]bool var conditions [11]bool
var otherconds []string
for _, v := range pl.CharaEffectLog { for _, v := range pl.CharaEffectLog {
if v.CharaEffectID < 1 || v.CharaEffectID > 11 { if v.CharaEffectID < 1 || v.CharaEffectID > 11 {
slog.Warn("skipping unknown charaEffectID", slog.Int("id", int(v.CharaEffectID)), slog.Bool("active", v.IsActive)) otherconds = append(otherconds, strconv.Itoa(int(v.CharaEffectID)))
continue continue
} }
conditions[v.CharaEffectID-1] = true conditions[v.CharaEffectID-1] = true
} }
var golds int
for _, t := range ism.End.Chara.SkillTips {
if t.Rarity == 2 {
golds++
}
}
row := Row{ row := Row{
File: key, File: key,
Scenario: int(ism.Chara.ScenarioID), Scenario: int(ism.ProgressInfo.Chara.ScenarioID),
Character: int(ism.Chara.CardID), Character: int(ism.ProgressInfo.Chara.CardID),
Deck1: int(pl.SupportCardGains[0].SupportCardID), Deck1: int(pl.SupportCardGains[0].SupportCardID),
Deck1LB: int(elem(ism.End.Chara.SupportCards, 0).LimitBreakCount),
Deck1Exp: int(elem(ism.End.Chara.SupportCards, 0).Exp),
Deck2: int(pl.SupportCardGains[1].SupportCardID), Deck2: int(pl.SupportCardGains[1].SupportCardID),
Deck2LB: int(elem(ism.End.Chara.SupportCards, 1).LimitBreakCount),
Deck2Exp: int(elem(ism.End.Chara.SupportCards, 1).Exp),
Deck3: int(pl.SupportCardGains[2].SupportCardID), Deck3: int(pl.SupportCardGains[2].SupportCardID),
Deck3LB: int(elem(ism.End.Chara.SupportCards, 2).LimitBreakCount),
Deck3Exp: int(elem(ism.End.Chara.SupportCards, 2).Exp),
Deck4: int(pl.SupportCardGains[3].SupportCardID), Deck4: int(pl.SupportCardGains[3].SupportCardID),
Deck4LB: int(elem(ism.End.Chara.SupportCards, 3).LimitBreakCount),
Deck4Exp: int(elem(ism.End.Chara.SupportCards, 3).Exp),
Deck5: int(pl.SupportCardGains[4].SupportCardID), Deck5: int(pl.SupportCardGains[4].SupportCardID),
Deck5LB: int(elem(ism.End.Chara.SupportCards, 4).LimitBreakCount),
Deck5Exp: int(elem(ism.End.Chara.SupportCards, 4).Exp),
Deck6: int(pl.SupportCardGains[5].SupportCardID), Deck6: int(pl.SupportCardGains[5].SupportCardID),
Deck6LB: int(elem(ism.End.Chara.SupportCards, 5).LimitBreakCount),
Deck6Exp: int(elem(ism.End.Chara.SupportCards, 5).Exp),
Races: races, Races: races,
Wins: wins, Wins: wins,
GoldSkills: golds,
NightOwl: conditions[0], NightOwl: conditions[0],
Slacker: conditions[1], Slacker: conditions[1],
SkinOutbreak: conditions[2], SkinOutbreak: conditions[2],
@@ -90,6 +132,7 @@ func ismRows(key string, ism WorkIdleSingleMode) iter.Seq[Row] {
HotTopic: conditions[8], HotTopic: conditions[8],
PracticePerfect: conditions[9], PracticePerfect: conditions[9],
VeryGoodAtTraining: conditions[10], VeryGoodAtTraining: conditions[10],
OtherConds: otherconds,
} }
row.GainSource = "Events" row.GainSource = "Events"
@@ -146,13 +189,26 @@ func (r Row) csv() []string {
strconv.Itoa(r.Scenario), strconv.Itoa(r.Scenario),
strconv.Itoa(r.Character), strconv.Itoa(r.Character),
strconv.Itoa(r.Deck1), strconv.Itoa(r.Deck1),
strconv.Itoa(r.Deck1LB),
strconv.Itoa(r.Deck1Exp),
strconv.Itoa(r.Deck2), strconv.Itoa(r.Deck2),
strconv.Itoa(r.Deck2LB),
strconv.Itoa(r.Deck2Exp),
strconv.Itoa(r.Deck3), strconv.Itoa(r.Deck3),
strconv.Itoa(r.Deck3LB),
strconv.Itoa(r.Deck3Exp),
strconv.Itoa(r.Deck4), strconv.Itoa(r.Deck4),
strconv.Itoa(r.Deck4LB),
strconv.Itoa(r.Deck4Exp),
strconv.Itoa(r.Deck5), strconv.Itoa(r.Deck5),
strconv.Itoa(r.Deck5LB),
strconv.Itoa(r.Deck5Exp),
strconv.Itoa(r.Deck6), strconv.Itoa(r.Deck6),
strconv.Itoa(r.Deck6LB),
strconv.Itoa(r.Deck6Exp),
strconv.Itoa(r.Races), strconv.Itoa(r.Races),
strconv.Itoa(r.Wins), strconv.Itoa(r.Wins),
strconv.Itoa(r.GoldSkills),
strconv.FormatBool(r.NightOwl), strconv.FormatBool(r.NightOwl),
strconv.FormatBool(r.Slacker), strconv.FormatBool(r.Slacker),
strconv.FormatBool(r.SkinOutbreak), strconv.FormatBool(r.SkinOutbreak),
@@ -164,5 +220,6 @@ func (r Row) csv() []string {
strconv.FormatBool(r.HotTopic), strconv.FormatBool(r.HotTopic),
strconv.FormatBool(r.PracticePerfect), strconv.FormatBool(r.PracticePerfect),
strconv.FormatBool(r.VeryGoodAtTraining), strconv.FormatBool(r.VeryGoodAtTraining),
strings.Join(r.OtherConds, ", "),
} }
} }
View File
+2808
View File
File diff suppressed because it is too large Load Diff
+2893
View File
File diff suppressed because it is too large Load Diff