generate character.kk from game data

This commit is contained in:
2026-01-06 10:25:16 -05:00
parent bfc497b6fc
commit 0cbc0f93b5
12 changed files with 75647 additions and 2178 deletions

6
horsegen/README.md Normal file
View File

@@ -0,0 +1,6 @@
# gen
Go tool to generate the Koka source code from the game's SQLite database.
Code is generated using Go templates.
Templates use a `kkenum` function which converts a name `Mr. C.B.` to a Koka enumerant name `Mr-CB`.

View File

@@ -0,0 +1,34 @@
WITH uma_names AS (
SELECT
"index" AS "id",
"text" AS "name"
FROM text_data
WHERE category = 6 AND "index" BETWEEN 1000 AND 1999
-- Exclude characters who have no succession relations defined.
AND "index" IN (SELECT chara_id FROM succession_relation_member)
), pairs AS (
SELECT
a.id AS id_a,
a.name AS name_a,
b.id AS id_b,
b.name AS name_b
FROM uma_names a
JOIN uma_names b ON a.id != b.id -- exclude reflexive cases
), relation_pairs AS (
SELECT
ra.relation_type,
ra.chara_id AS id_a,
rb.chara_id AS id_b
FROM succession_relation_member ra
JOIN succession_relation_member rb ON ra.relation_type = rb.relation_type
), affinity AS (
SELECT
pairs.*,
SUM(IFNULL(relation_point, 0)) AS base_affinity
FROM pairs
LEFT JOIN relation_pairs rp ON pairs.id_a = rp.id_a AND pairs.id_b = rp.id_b
LEFT JOIN succession_relation sr ON rp.relation_type = sr.relation_type
GROUP BY pairs.id_a, pairs.id_b
)
SELECT * FROM affinity
ORDER BY id_a, id_b

View File

@@ -0,0 +1,41 @@
WITH uma_names AS (
SELECT
"index" AS "id",
"text" AS "name"
FROM text_data
WHERE category = 6 AND "index" BETWEEN 1000 AND 1999
-- Exclude characters who have no succession relations defined.
AND "index" IN (SELECT chara_id FROM succession_relation_member)
), trios AS (
SELECT
a.id AS id_a,
a.name AS name_a,
b.id AS id_b,
b.name AS name_b,
c.id AS id_c,
c.name AS name_c,
ra.relation_type
FROM uma_names a
JOIN uma_names b ON a.id != b.id
JOIN uma_names c ON a.id != c.id AND b.id != c.id
JOIN succession_relation_member ra ON a.id = ra.chara_id
JOIN succession_relation_member rb ON b.id = rb.chara_id
JOIN succession_relation_member rc ON c.id = rc.chara_id
WHERE
ra.relation_type = rb.relation_type
AND ra.relation_type = rc.relation_type
), affinity AS (
SELECT
id_a,
name_a,
id_b,
name_b,
id_c,
name_c,
SUM(relation_point) AS base_affinity
FROM trios
JOIN succession_relation sr ON trios.relation_type = sr.relation_type
GROUP BY id_a, id_b, id_c
)
SELECT * FROM affinity
ORDER BY id_a, id_b, id_c

View File

@@ -0,0 +1,81 @@
{{ define "koka-character" -}}
module horse/character
// Automatically generated with the horsegen tool; DO NOT EDIT
// Character identity.
pub type character
{{- range $uma := $.Characters }}
{{ kkenum $uma.Name }}
{{- end }}
// The list of all characters.
val character/all = [
{{- range $uma := $.Characters }}
{{ kkenum $uma.Name }},
{{- end }}
]
// Get the character for a character ID.
// Generally, these are four digit numbers in the range 1000-1999.
pub fun character/from-id(id: int): maybe<character>
match id
{{- range $uma := $.Characters }}
{{ $uma.ID }} -> Just( {{- kkenum $uma.Name -}} )
{{- end }}
_ -> Nothing
// Get the ID for a character.
pub fun character/id(c: character): int
match c
{{- range $uma := $.Characters }}
{{ kkenum $uma.Name }} -> {{ $uma.ID }}
{{- end }}
// Get the name of a character.
pub fun character/show(c: character): string
match c
{{- range $uma := $.Characters }}
{{ kkenum $uma.Name }} -> {{ printf "%q" $uma.Name }}
{{- end }}
// Compare two characters.
pub fip fun character/order2(a: character, b: character): order2<character>
match (a, b)
{{- range $uma := $.Characters }}{{ $e := kkenum $uma.Name }}
( {{- $e }}, {{ $e -}} ) -> Eq2( {{- $e -}} )
{{- if ne $uma.ID $.MaxID }}
( {{- $e }}, b') -> Lt2( {{- $e }}, b')
(a', {{ $e -}} ) -> Gt2( {{- $e }}, a')
{{- end }}
{{- end }}
// Character equality.
pub fun character/(==)(a: character, b: character): bool
match (a, b)
{{- range $uma := $.Characters }}{{ $e := kkenum $uma.Name }}
( {{- $e }}, {{ $e -}} ) -> True
{{- end }}
_ -> False
// Base affinity between a pair using the global ruleset.
pub fun global/pair-affinity(a: character, b: character): int
match (a, b)
{{- range $pair := $.Pairs }}
( {{- kkenum $pair.NameA }}, {{ kkenum $pair.NameB -}} ) -> {{ $pair.Affinity }}
{{- end }}
// Reflexive cases are always 0.
{{- range $uma := $.Characters }}{{ $e := kkenum $uma.Name }}
( {{- $e }}, {{ $e -}} ) -> 0
{{- end }}
{{/* TODO: probably use a data structure instead of hoping the compilers fix this */ -}}
// Base affinity for a trio using the global ruleset.
pub fun global/trio-affinity(a: character, b: character, c: character): int
match (a, b, c)
{{- range $trio := $.Trios }}
( {{- kkenum $trio.NameA }}, {{ kkenum $trio.NameB }}, {{ kkenum $trio.NameC -}} ) -> {{ $trio.Affinity }}
{{- end }}
(_, _, _) -> 0
{{- end }}

7
horsegen/character.sql Normal file
View File

@@ -0,0 +1,7 @@
SELECT
"index" AS "id",
"text" AS "name"
FROM text_data
WHERE category = 6 AND "index" BETWEEN 1000 AND 1999
-- Exclude characters who have no succession relations defined.
AND "index" IN (SELECT chara_id FROM succession_relation_member)

46
horsegen/gen.go Normal file
View File

@@ -0,0 +1,46 @@
package main
import (
_ "embed"
"fmt"
"io"
"strings"
"text/template"
)
//go:embed character.kk.template
var characterKK string
// LoadTemplates sets up templates to render game data to source code.
func LoadTemplates() (*template.Template, error) {
t := template.New("root")
t.Funcs(template.FuncMap{
"kkenum": kkenum,
})
t, err := t.Parse(characterKK)
if err != nil {
return nil, fmt.Errorf("parsing characterKK: %w", err)
}
return t, nil
}
// ExecCharacterKK renders the Koka character module to w.
func ExecCharacterKK(t *template.Template, w io.Writer, c []Character, pairs, trios []AffinityRelation) error {
maxid := 0
for _, u := range c {
maxid = max(maxid, u.ID)
}
data := struct {
Characters []Character
Pairs []AffinityRelation
Trios []AffinityRelation
MaxID int
}{c, pairs, trios, maxid}
return t.ExecuteTemplate(w, "koka-character", &data)
}
func kkenum(name string) string {
name = strings.ReplaceAll(name, ".", "")
name = strings.ReplaceAll(name, " ", "-")
return name
}

131
horsegen/load.go Normal file
View File

@@ -0,0 +1,131 @@
package main
import (
"context"
_ "embed"
"fmt"
"zombiezen.com/go/sqlite/sqlitex"
)
//go:embed character.sql
var characterSQL string
//go:embed character.affinity2.sql
var characterAffinity2SQL string
//go:embed character.affinity3.sql
var characterAffinity3SQL string
type Character struct {
ID int
Name string
}
func Characters(ctx context.Context, db *sqlitex.Pool) ([]Character, error) {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, fmt.Errorf("couldn't get connection for characters: %w", err)
}
stmt, _, err := conn.PrepareTransient(characterSQL)
if err != nil {
return nil, fmt.Errorf("couldn't prepare statement for characters: %w", err)
}
defer stmt.Finalize()
var r []Character
for {
ok, err := stmt.Step()
if err != nil {
return nil, fmt.Errorf("error stepping characters: %w", err)
}
if !ok {
break
}
c := Character{
ID: stmt.ColumnInt(0),
Name: stmt.ColumnText(1),
}
r = append(r, c)
}
return r, nil
}
type AffinityRelation struct {
IDA int
NameA string
IDB int
NameB string
IDC int
NameC string
Affinity int
}
func CharacterPairs(ctx context.Context, db *sqlitex.Pool) ([]AffinityRelation, error) {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, fmt.Errorf("couldn't get connection for character pairs: %w", err)
}
stmt, _, err := conn.PrepareTransient(characterAffinity2SQL)
if err != nil {
return nil, fmt.Errorf("couldn't prepare statement for character pairs: %w", err)
}
defer stmt.Finalize()
var r []AffinityRelation
for {
ok, err := stmt.Step()
if err != nil {
return nil, fmt.Errorf("error stepping character pairs: %w", err)
}
if !ok {
break
}
p := AffinityRelation{
IDA: stmt.ColumnInt(0),
NameA: stmt.ColumnText(1),
IDB: stmt.ColumnInt(2),
NameB: stmt.ColumnText(3),
Affinity: stmt.ColumnInt(4),
}
r = append(r, p)
}
return r, nil
}
func CharacterTrios(ctx context.Context, db *sqlitex.Pool) ([]AffinityRelation, error) {
conn, err := db.Take(ctx)
defer db.Put(conn)
if err != nil {
return nil, fmt.Errorf("couldn't get connection for character trios: %w", err)
}
stmt, _, err := conn.PrepareTransient(characterAffinity3SQL)
if err != nil {
return nil, fmt.Errorf("couldn't prepare statement for character trios: %w", err)
}
defer stmt.Finalize()
var r []AffinityRelation
for {
ok, err := stmt.Step()
if err != nil {
return nil, fmt.Errorf("error stepping character trios: %w", err)
}
if !ok {
break
}
p := AffinityRelation{
IDA: stmt.ColumnInt(0),
NameA: stmt.ColumnText(1),
IDB: stmt.ColumnInt(2),
NameB: stmt.ColumnText(3),
IDC: stmt.ColumnInt(4),
NameC: stmt.ColumnText(5),
Affinity: stmt.ColumnInt(6),
}
r = append(r, p)
}
return r, nil
}

66
horsegen/main.go Normal file
View File

@@ -0,0 +1,66 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"zombiezen.com/go/sqlite"
"zombiezen.com/go/sqlite/sqlitex"
)
func main() {
var (
mdb string
out 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.Parse()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
go func() {
<-ctx.Done()
stop()
}()
t, err := LoadTemplates()
if err != nil {
fmt.Fprintf(os.Stderr, "loading templates: %s\n", err)
os.Exit(2)
}
db, err := sqlitex.NewPool(mdb, sqlitex.PoolOptions{Flags: sqlite.OpenReadOnly})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
charas, err := Characters(ctx, db)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
pairs, err := CharacterPairs(ctx, db)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
trios, err := CharacterTrios(ctx, db)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
cf, err := os.Create(filepath.Join(out, "character.kk"))
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
if err := ExecCharacterKK(t, cf, charas, pairs, trios); err != nil {
fmt.Fprintln(os.Stderr, err)
// continue on
}
}