horsegen: also generate go

This commit is contained in:
2026-01-14 12:18:54 -05:00
parent be41389006
commit c9a7e15f89
10 changed files with 16896 additions and 21 deletions

66
abilitytype_string.go Normal file
View File

@@ -0,0 +1,66 @@
// Code generated by "stringer -type AbilityType -trimprefix Ability"; DO NOT EDIT.
package horse
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[AbilityPassiveSpeed-1]
_ = x[AbilityPassiveStamina-2]
_ = x[AbilityPassivePower-3]
_ = x[AbilityPassiveGuts-4]
_ = x[AbilityPassiveWit-5]
_ = x[AbilityGreatEscape-6]
_ = x[AbilityVision-8]
_ = x[AbilityHP-9]
_ = x[AbilityGateDelay-10]
_ = x[AbilityFrenzy-13]
_ = x[AbilityCurrentSpeed-21]
_ = x[AbilityTargetSpeed-27]
_ = x[AbilityLaneSpeed-28]
_ = x[AbilityAccel-31]
_ = x[AbilityLaneChange-35]
}
const (
_AbilityType_name_0 = "PassiveSpeedPassiveStaminaPassivePowerPassiveGutsPassiveWitGreatEscape"
_AbilityType_name_1 = "VisionHPGateDelay"
_AbilityType_name_2 = "Frenzy"
_AbilityType_name_3 = "CurrentSpeed"
_AbilityType_name_4 = "TargetSpeedLaneSpeed"
_AbilityType_name_5 = "Accel"
_AbilityType_name_6 = "LaneChange"
)
var (
_AbilityType_index_0 = [...]uint8{0, 12, 26, 38, 49, 59, 70}
_AbilityType_index_1 = [...]uint8{0, 6, 8, 17}
_AbilityType_index_4 = [...]uint8{0, 11, 20}
)
func (i AbilityType) String() string {
switch {
case 1 <= i && i <= 6:
i -= 1
return _AbilityType_name_0[_AbilityType_index_0[i]:_AbilityType_index_0[i+1]]
case 8 <= i && i <= 10:
i -= 8
return _AbilityType_name_1[_AbilityType_index_1[i]:_AbilityType_index_1[i+1]]
case i == 13:
return _AbilityType_name_2
case i == 21:
return _AbilityType_name_3
case 27 <= i && i <= 28:
i -= 27
return _AbilityType_name_4[_AbilityType_index_4[i]:_AbilityType_index_4[i+1]]
case i == 31:
return _AbilityType_name_5
case i == 35:
return _AbilityType_name_6
default:
return "AbilityType(" + strconv.FormatInt(int64(i), 10) + ")"
}
}

264
character.go Normal file

File diff suppressed because one or more lines are too long

View File

@@ -166,13 +166,20 @@ ability types:
target types:
- 0 none (second and third abilities on skills that only have one)
- 1 self
- 2 others who have sympathy? only on autumn neo universe's rare green skill which triggers everyone's sympathy, and there's no target value
- 4 others in view
- 7 frontmost? only on daiichi ruby's unique, has a target value of 5 but the description says 先頭から4人まで
- 9 others ahead, target_value is number of targets (18 for all)
- 10 others behind, target_value is number of targets
- 11 all teammates
- 18 others in style, target_value is style (1=front, 2=pace, 3=late, 4=end)
- 19 rushing others ahead
- 20 rushing others behind
- 21 rushing others in style, target_value is style
- 22 specific character, target value is character id
- 23 other who triggered the skill
TODO target types only in jp: 2, 7, 11, 22, 23
ability_value_usage can be 1 for plain or 2-6 for aoharu stat skill stat scaling

View File

@@ -0,0 +1,145 @@
{{ define "go-character" -}}
package horse
// Automatically generated with horsegen; DO NOT EDIT
import (
"fmt"
"slices"
"strconv"
)
type Character struct {
ID int16
Name string
}
var characterIDs = []int16{
{{- range $c := $.Characters }}
{{ $c.ID }}, // {{ $c.Name }}
{{- end }}
}
var characterNames = []string{
{{- range $c := $.Characters }}
{{ printf "%q" $c.Name }},
{{- end }}
}
var characterNameToID = map[string]int16{
{{- range $c := $.Characters }}
{{ printf "%q" $c.Name }}: {{ $c.ID }},
{{- end }}
}
func characterIndex(id int16) (int, bool) {
return slices.BinarySearch(characterIDs, id)
}
func CharacterForID(id int16) Character {
i, ok := characterIndex(id)
if !ok {
return Character{}
}
return Character{
ID: id,
Name: characterNames[i],
}
}
func CharacterForName(name string) Character {
id, ok := characterNameToID[name]
if !ok {
return Character{}
}
return Character{
ID: id,
Name: name,
}
}
func (c *Character) MarshalJSON() ([]byte, error) {
// Only marshal legal or empty characters.
if c.ID == 0 {
return []byte{'0'}, nil
}
i, ok := characterIndex(c.ID)
if !ok {
return nil, fmt.Errorf("marshaling character %q with invalid ID %d", c.Name, c.ID)
}
if characterNames[i] != c.Name {
return nil, fmt.Errorf("marshaling character with ID %d: name is %q but should be %q", c.ID, c.Name, characterNames[i])
}
return strconv.AppendInt(nil, int64(c.ID), 10), nil
}
func (c *Character) UnmarshalJSON(b []byte) error {
if string(b) == "null" {
return nil
}
id, err := strconv.ParseInt(string(b), 10, 16)
if err != nil {
return fmt.Errorf("unmarshaling invalid character ID %q: %w", b, err)
}
if id == 0 {
*c = Character{}
return nil
}
i, ok := characterIndex(int16(id))
if !ok {
return fmt.Errorf("unmarshaling unrecognized character ID %d", id)
}
*c = Character{
ID: int16(id),
Name: characterNames[i],
}
return nil
}
var pairAffinity = []int8{
{{- range $a := $.Characters -}}
{{- range $b := $.Characters -}}
{{- index $.PairMaps $a.ID $b.ID -}},
{{- end -}}
{{- end -}}
}
var trioAffinity = []int8{
{{- range $a := $.Characters -}}
{{- range $b := $.Characters -}}
{{- range $c := $.Characters -}}
{{- index $.TrioMaps $a.ID $b.ID $c.ID -}},
{{- end -}}
{{- end -}}
{{- end -}}
}
func PairAffinity(a, b Character) int {
i, ok := characterIndex(a.ID)
if !ok {
return 0
}
j, ok := characterIndex(b.ID)
if !ok {
return 0
}
return int(pairAffinity[i*{{ $.Count }} + j])
}
func TrioAffinity(a, b, c Character) int {
i, ok := characterIndex(a.ID)
if !ok {
return 0
}
j, ok := characterIndex(b.ID)
if !ok {
return 0
}
k, ok := characterIndex(c.ID)
if !ok {
return 0
}
return int(trioAffinity[i*{{ $.Count }}*{{ $.Count }} + j*{{ $.Count }} + k])
}
{{ end }}

View File

@@ -2,6 +2,7 @@ package main
import (
"embed"
"errors"
"fmt"
"io"
"regexp"
@@ -10,7 +11,7 @@ import (
"unicode"
)
//go:embed character.kk.template skill.kk.template
//go:embed character.kk.template skill.kk.template character.go.template skill.go.template
var templates embed.FS
// LoadTemplates sets up templates to render game data to source code.
@@ -18,12 +19,14 @@ func LoadTemplates() (*template.Template, error) {
t := template.New("root")
t.Funcs(template.FuncMap{
"kkenum": kkenum,
"goenum": goenum,
})
return t.ParseFS(templates, "*")
}
// ExecCharacterKK renders the Koka character module to w.
func ExecCharacterKK(t *template.Template, w io.Writer, c []NamedID[Character], pairs, trios []AffinityRelation) error {
// ExecCharacter renders the Koka character module to kk and the Go character file to g.
// If either is nil, it is skipped.
func ExecCharacter(t *template.Template, kk, g io.Writer, c []NamedID[Character], pairs, trios []AffinityRelation) error {
if len(pairs) != len(c)*len(c) {
return fmt.Errorf("there are %d pairs but there must be %d for %d characters", len(pairs), len(c)*len(c), len(c))
}
@@ -58,13 +61,20 @@ func ExecCharacterKK(t *template.Template, w io.Writer, c []NamedID[Character],
Count int
MaxID int
}{c, pairs, trios, pm, tm, len(c), maxid}
return t.ExecuteTemplate(w, "koka-character", &data)
var err error
if kk != nil {
err = errors.Join(t.ExecuteTemplate(kk, "koka-character", &data))
}
if g != nil {
err = errors.Join(t.ExecuteTemplate(g, "go-character", &data))
}
return err
}
func ExecSkillKK(t *template.Template, w io.Writer, g []NamedID[SkillGroup], s []Skill) error {
m := make(map[int][]Skill, len(g))
u := make(map[int]int, len(g))
for _, t := range s {
func ExecSkill(t *template.Template, kk, g io.Writer, groups []NamedID[SkillGroup], skills []Skill) error {
m := make(map[int][]Skill, len(groups))
u := make(map[int]int, len(groups))
for _, t := range skills {
m[t.GroupID] = append(m[t.GroupID], t)
if t.Rarity >= 4 {
// Add inheritable uniques to u so we can add inherited versions to groups.
@@ -72,7 +82,7 @@ func ExecSkillKK(t *template.Template, w io.Writer, g []NamedID[SkillGroup], s [
}
}
// Now that u is set up, iterate through again and add in inherited skills.
for _, t := range s {
for _, t := range skills {
if t.InheritID != 0 {
m[u[t.InheritID]] = append(m[u[t.InheritID]], t)
}
@@ -81,8 +91,15 @@ func ExecSkillKK(t *template.Template, w io.Writer, g []NamedID[SkillGroup], s [
Groups []NamedID[SkillGroup]
Skills []Skill
Related map[int][]Skill
}{g, s, m}
return t.ExecuteTemplate(w, "koka-skill", &data)
}{groups, skills, m}
var err error
if kk != nil {
err = errors.Join(t.ExecuteTemplate(kk, "koka-skill", &data))
}
if g != nil {
err = errors.Join(t.ExecuteTemplate(g, "go-skill-data", &data))
}
return err
}
func ExecSkillGroupKK(t *template.Template, w io.Writer, g []NamedID[SkillGroup], s []Skill) error {
@@ -93,7 +110,7 @@ func ExecSkillGroupKK(t *template.Template, w io.Writer, g []NamedID[SkillGroup]
return t.ExecuteTemplate(w, "koka-skill-group", &data)
}
const replaceDash = " ,!?/+();#○☆♡'=♪∀゚∴"
const wordSeps = " ,!?/-+();#○☆♡'=♪∀゚∴"
var (
kkReplace = func() *strings.Replacer {
@@ -111,13 +128,33 @@ var (
"×", "x",
"◎", "Lv2",
}
for _, c := range replaceDash {
for _, c := range wordSeps {
r = append(r, string(c), "-")
}
return strings.NewReplacer(r...)
}()
kkMultidash = regexp.MustCompile(`-+`)
kkDashNonletter = regexp.MustCompile(`-[^A-Za-z]`)
goReplace = func() *strings.Replacer {
r := []string{
"Triple 7s", "TripleSevens",
"1,500,000 CC", "OneMillionCC",
"15,000,000 CC", "FifteenMillionCC",
"1st", "First",
"♡ 3D Nail Art", "NailArt",
".", "",
"&", "And",
"'s", "s",
"∞", "Infinity",
"×", "X",
"◎", "Lv2",
}
for _, c := range wordSeps {
r = append(r, string(c), "")
}
return strings.NewReplacer(r...)
}()
)
func kkenum(name string) string {
@@ -144,3 +181,14 @@ func kkenum(name string) string {
}
return name
}
func goenum(name string) string {
// go names are a bit more lax, so we need fewer checks
orig := name
name = goReplace.Replace(name)
if len(name) == 0 {
panic(fmt.Errorf("%q became empty as Go enum variant", orig))
}
name = strings.ToUpper(name[:1]) + name[1:]
return name
}

View File

@@ -15,11 +15,12 @@ import (
func main() {
var (
mdb string
out string
mdb string
kkOut, goOut string
)
flag.StringVar(&mdb, "mdb", os.ExpandEnv(`$USERPROFILE\AppData\LocalLow\Cygames\Umamusume\master\master.mdb`), "`path` to Umamusume master.mdb")
flag.StringVar(&out, "kk", `.\horse`, "existing `dir`ectory for output Koka files")
flag.StringVar(&kkOut, "kk", `.\horse`, "existing `dir`ectory for output Koka files")
flag.StringVar(&goOut, "go", `.`, "existing `dir`ectory for output Go files")
flag.Parse()
pctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
@@ -86,23 +87,28 @@ func main() {
eg, ctx = errgroup.WithContext(pctx)
eg.Go(func() error {
cf, err := os.Create(filepath.Join(out, "character.kk"))
cf, err := os.Create(filepath.Join(kkOut, "character.kk"))
if err != nil {
return err
}
gf, err := os.Create(filepath.Join(goOut, "character.go"))
if err != nil {
return err
}
slog.Info("write characters")
return ExecCharacterKK(t, cf, charas, pairs, trios)
return ExecCharacter(t, cf, gf, charas, pairs, trios)
})
eg.Go(func() error {
sf, err := os.Create(filepath.Join(out, "skill.kk"))
sf, err := os.Create(filepath.Join(kkOut, "skill.kk"))
if err != nil {
return err
}
gf, err := os.Create(filepath.Join(goOut, "skill_data.go"))
slog.Info("write skills")
return ExecSkillKK(t, sf, sg, skills)
return ExecSkill(t, sf, gf, sg, skills)
})
eg.Go(func() error {
sf, err := os.Create(filepath.Join(out, "skill-group.kk"))
sf, err := os.Create(filepath.Join(kkOut, "skill-group.kk"))
if err != nil {
return err
}

View File

@@ -0,0 +1,70 @@
{{- define "go-skill-data" -}}
package horse
// Automatically generated with horsegen; DO NOT EDIT
//go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type SkillID -trimprefix Skill -linecomment
const (
{{- range $s := $.Skills }}
Skill{{ goenum $s.Name }}{{ if ne $s.InheritID 0 }}Inherit{{ end }} SkillID = {{ $s.ID }} // {{ $s.Name }}
{{- end }}
)
var AllSkills = map[SkillID]Skill{
{{- range $s := $.Skills }}
Skill{{ goenum $s.Name }}{{ if ne $s.InheritID 0 }}Inherit{{ end }}: {
{{ $s.ID }},
{{ printf "%q" $s.Name }},
{{ printf "%q" $s.Description }},
{{ $s.GroupID }},
{{ $s.Rarity }},
{{ $s.GroupRate }},
{{ $s.GradeValue }},
{{ $s.WitCheck }},
[]Activation{
{{- range $a := $s.Activations }}
{{- if ne $a.Condition "" }}
{
{{ printf "%q" $a.Precondition }},
{{ printf "%q" $a.Condition }},
{{ $a.Duration }},
{{ $a.Cooldown }},
[]Ability{
{{- range $abil := $a.Abilities }}
{{- if ne $abil.Type 0 }}
{
{{ if eq $abil.Type 1 -}}AbilityPassiveSpeed,
{{ else if eq $abil.Type 2 -}}AbilityPassiveStamina,
{{ else if eq $abil.Type 3 -}}AbilityPassivePower,
{{ else if eq $abil.Type 4 -}}AbilityPassiveGuts,
{{ else if eq $abil.Type 5 -}}AbilityPassiveWit,
{{ else if eq $abil.Type 6 -}}AbilityGreatEscape,
{{ else if eq $abil.Type 8 -}}AbilityVision,
{{ else if eq $abil.Type 9 -}}AbilityHP,
{{ else if eq $abil.Type 10 -}}AbilityGateDelay,
{{ else if eq $abil.Type 13 -}}AbilityFrenzy,
{{ else if eq $abil.Type 21 -}}AbilityCurrentSpeed,
{{ else if eq $abil.Type 27 -}}AbilityTargetSpeed,
{{ else if eq $abil.Type 28 -}}AbilityLaneSpeed,
{{ else if eq $abil.Type 31 -}}AbilityAccel,
{{ else if eq $abil.Type 35 -}}AbilityLaneChange,
{{ else }}??? $abil.Type={{$abil.Type}}
{{ end -}}
{{ $abil.ValueUsage }},
{{ $abil.Value }},
{{ $abil.Target }},
{{ $abil.TargetValue }},
},
{{- end }}
{{- end }}
},
},
{{- end }}
{{- end }}
},
{{ $s.SPCost }},
{{ $s.IconID }},
},
{{- end }}
}
{{ end }}

57
skill.go Normal file
View File

@@ -0,0 +1,57 @@
package horse
type SkillID int32
// Skill is the internal data about a skill.
type Skill struct {
ID SkillID
Name string
Description string
Group int32
Rarity int8
GroupRate int8
GradeValue int32
WitCheck bool
Activations []Activation
SPCost int
IconID int
}
// Activation is the parameters controlling when a skill activates.
type Activation struct {
Precondition string
Condition string
Duration int // 1e4 scale
Cooldown int // 1e4 scale
Abilities []Ability
}
// Ability is an individual effect applied by a skill.
type Ability struct {
Type AbilityType
ValueUsage int8
Value int32
Target int8
TargetValue int32
}
type AbilityType int8
//go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type AbilityType -trimprefix Ability
const (
AbilityPassiveSpeed AbilityType = 1
AbilityPassiveStamina AbilityType = 2
AbilityPassivePower AbilityType = 3
AbilityPassiveGuts AbilityType = 4
AbilityPassiveWit AbilityType = 5
AbilityGreatEscape AbilityType = 6
AbilityVision AbilityType = 8
AbilityHP AbilityType = 9
AbilityGateDelay AbilityType = 10
AbilityFrenzy AbilityType = 13
AbilityCurrentSpeed AbilityType = 21
AbilityTargetSpeed AbilityType = 27
AbilityLaneSpeed AbilityType = 28
AbilityAccel AbilityType = 31
AbilityLaneChange AbilityType = 35
)

15217
skill_data.go Normal file

File diff suppressed because it is too large Load Diff

995
skillid_string.go Normal file

File diff suppressed because one or more lines are too long