generate character.kk from game data
This commit is contained in:
6
horsegen/README.md
Normal file
6
horsegen/README.md
Normal 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`.
|
||||
34
horsegen/character.affinity2.sql
Normal file
34
horsegen/character.affinity2.sql
Normal 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
|
||||
41
horsegen/character.affinity3.sql
Normal file
41
horsegen/character.affinity3.sql
Normal 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
|
||||
81
horsegen/character.kk.template
Normal file
81
horsegen/character.kk.template
Normal 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
7
horsegen/character.sql
Normal 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
46
horsegen/gen.go
Normal 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
131
horsegen/load.go
Normal 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
66
horsegen/main.go
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user