diff --git a/cmd/factorfactor/factor.go b/cmd/factorfactor/factor.go new file mode 100644 index 0000000..f1e74d6 --- /dev/null +++ b/cmd/factorfactor/factor.go @@ -0,0 +1,77 @@ +package main + +import ( + "log/slog" + + "git.sunturtle.xyz/zephyr/horse" + "git.sunturtle.xyz/zephyr/horse/umadump/ism" +) + +// For each gain type from inspiration (stats, caps, aptitudes, skills): +// find all activated sparks that can give that gain type. +// Then, find all subsets of effect groups among those sparks +// that have effects summing to those observed +// (or more if the .end_info result is maxed), +// with the restriction that one comes from each spark. +// Deduplicate permutations. +// Lastly, find the intersection of possible effects from sparks that appear +// multiple times. +// +// To some extent, we rely on there being no sparks with the random stats effect type, +// although we also don't know the distribution of stats from overcapped skill hints. + +type target struct { + spark *horse.Spark + group int + effect horse.SparkEffect +} + +func skilltargets(sparks []horse.Spark) map[horse.SkillID]map[horse.SparkID][]target { + r := make(map[horse.SkillID]map[horse.SparkID][]target) + for i, sp := range sparks { + for j, g := range sp.Effects { + for _, e := range g { + if e.Target != horse.SparkSkillHint { + continue + } + if r[horse.SkillID(e.Value1)] == nil { + r[horse.SkillID(e.Value1)] = make(map[horse.SparkID][]target) + } + r[horse.SkillID(e.Value1)][sp.ID] = append(r[horse.SkillID(e.Value1)][sp.ID], target{&sparks[i], j, e}) + } + } + } + return r +} + +func resolveSkills(tab tables, career *ism.WorkIdleSingleMode) + +// skillsparks appends all activated sparks that can yield a given skill to r. +// It assumes that no spark can give multiple skills. +func skillsparks(tab tables, sparks []ism.Factor, id horse.SkillID, r []*horse.Spark) []*horse.Spark { + for _, f := range sparks { + sp := tab.sparks[f.FactorID] + if sp == nil { + // ATM we do generally exclude Carnival Bonus from spark info. + // Better to check. + slog.Warn("skipped spark", slog.Int("id", int(f.FactorID))) + continue + } + if sparkHasSkill(sp, id) { + r = append(r, sp) + } + } + return r +} + +func sparkHasSkill(sp *horse.Spark, id horse.SkillID) bool { + for _, g := range sp.Effects { + for _, e := range g { + if e.Target != horse.SparkSkillHint { + continue + } + return id == horse.SkillID(e.Value1) + } + } + return false +} diff --git a/cmd/factorfactor/main.go b/cmd/factorfactor/main.go new file mode 100644 index 0000000..bd1d6f3 --- /dev/null +++ b/cmd/factorfactor/main.go @@ -0,0 +1,111 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "log/slog" + "os" + "os/signal" + "path/filepath" + "slices" + + "git.sunturtle.xyz/zephyr/horse/mdb" + "git.sunturtle.xyz/zephyr/horse/umadump/ism" + "golang.org/x/sync/errgroup" + "zombiezen.com/go/sqlite" + "zombiezen.com/go/sqlite/sqlitex" +) + +func main() { + var ( + mdbf string + dirs []string + files []string + ) + flag.StringVar(&mdbf, "-mdb", "", "`path` to master.mdb") + flag.Func("d", "idle_single_mode `dir`ectory (may be specified multiple times)", func(s string) error { + dirs = append(dirs, s) + return nil + }) + flag.Parse() + + files = slices.Clone(flag.Args()) + for _, dir := range dirs { + ds, err := os.ReadDir(dir) + if err != nil { + slog.Error("getting files in directory", slog.String("dir", dir), slog.Any("err", err)) + os.Exit(1) + } + for _, d := range ds { + if d.IsDir() { + continue + } + files = append(files, filepath.Join(dir, d.Name())) + } + } + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) + go func() { + <-ctx.Done() + stop() + }() + + db, err := sqlitex.NewPool(mdbf, sqlitex.PoolOptions{Flags: sqlite.OpenReadOnly}) + if err != nil { + slog.Error("mdb", slog.Any("err", err)) + os.Exit(1) + } + + slog.Info("load skills") + skills, err := mdb.Skills(ctx, db) + if err != nil { + slog.Error("skills", slog.Any("err", err)) + os.Exit(1) + } + slog.Info("load sparks") + sparks, err := mdb.Sparks(ctx, db) + if err != nil { + slog.Error("sparks", slog.Any("err", err)) + os.Exit(1) + } + slog.Info("load umas") + umas, err := mdb.Umas(ctx, db) + if err != nil { + slog.Error("umas", slog.Any("err", err)) + os.Exit(1) + } + slog.Info("mdb", + slog.Int("sparks", len(sparks)), + slog.Int("skills", len(skills)), + slog.Int("umas", len(umas)), + ) + tab := makeTables(skills, sparks, umas) + + g, ctx := errgroup.WithContext(ctx) + g.SetLimit(64) + for _, file := range files { + g.Go(func() error { + return dofile(ctx, file) + }) + } + if err := g.Wait(); err != nil { + slog.Error("process", slog.Any("err", err)) + os.Exit(1) + } +} + +func dofile(ctx context.Context, file string) error { + f, err := os.Open(file) + if err != nil { + return err + } + var career ism.WorkIdleSingleMode + d := json.NewDecoder(f) + d.DisallowUnknownFields() + if err := d.Decode(&career); err != nil { + return fmt.Errorf("%s: %w", file, err) + } + +} diff --git a/cmd/factorfactor/mdb.go b/cmd/factorfactor/mdb.go new file mode 100644 index 0000000..f0d8fdd --- /dev/null +++ b/cmd/factorfactor/mdb.go @@ -0,0 +1,37 @@ +package main + +import "git.sunturtle.xyz/zephyr/horse" + +type tables struct { + skills map[horse.SkillID]*horse.Skill + groups map[horse.SkillGroupID]map[int8]*horse.Skill + sparks map[horse.SparkID]*horse.Spark + umas map[horse.UmaID]*horse.Uma +} + +func makeTables(skills []horse.Skill, sparks []horse.Spark, umas []horse.Uma) tables { + tab := tables{ + skills: make(map[horse.SkillID]*horse.Skill, len(skills)), + groups: make(map[horse.SkillGroupID]map[int8]*horse.Skill, len(skills)/2), + sparks: make(map[horse.SparkID]*horse.Spark, len(sparks)), + umas: make(map[horse.UmaID]*horse.Uma, len(umas)), + } + for i, sk := range skills { + tab.skills[sk.ID] = &skills[i] + if tab.groups[sk.Group] == nil { + tab.groups[sk.Group] = make(map[int8]*horse.Skill, 4) + } + // Only add the skill to the groups table if they're rare, the base + // version of the common skill, or there isn't anything else there. + if sk.Rarity == 2 || sk.GroupRate == 1 || tab.groups[sk.Group][sk.Rarity] == nil { + tab.groups[sk.Group][sk.Rarity] = &skills[i] + } + } + for i, sp := range sparks { + tab.sparks[sp.ID] = &sparks[i] + } + for i, u := range umas { + tab.umas[u.ID] = &umas[i] + } + return tab +}