172 lines
4.7 KiB
Go
172 lines
4.7 KiB
Go
package mdb
|
|
|
|
import (
|
|
"cmp"
|
|
"context"
|
|
"embed"
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
|
|
"zombiezen.com/go/sqlite"
|
|
"zombiezen.com/go/sqlite/sqlitex"
|
|
|
|
"git.sunturtle.xyz/zephyr/horse"
|
|
)
|
|
|
|
//go:embed sql/racecourse.sql
|
|
var racecourseSQL string
|
|
|
|
func Racecourses(ctx context.Context, db *sqlitex.Pool) ([]horse.Racecourse, error) {
|
|
return load(ctx, db, nil, racecourseSQL, func(s *sqlite.Stmt) horse.Racecourse {
|
|
c := horse.Racecourse{
|
|
ID: horse.RacecourseID(s.ColumnInt(0)),
|
|
TrackID: s.ColumnInt32(1),
|
|
TrackName: s.ColumnText(2),
|
|
Distance: float32(s.ColumnFloat(3)),
|
|
Ground: int8(s.ColumnInt(4)),
|
|
InOut: int8(s.ColumnInt(5)),
|
|
Turn: int8(s.ColumnInt(6)),
|
|
LaneMax: horse.TenThousandths(s.ColumnInt32(7)),
|
|
Threshold1: int8(s.ColumnInt(8)),
|
|
Threshold2: int8(s.ColumnInt(9)),
|
|
LaneStart: laneStarts[s.ColumnInt(10)-1],
|
|
}
|
|
ls := s.ColumnInt(10) - 1
|
|
if 0 <= ls && ls < len(laneStarts) {
|
|
c.LaneStart = laneStarts[ls]
|
|
}
|
|
evts := courseEvents(c.ID)
|
|
c.Spans, c.Slopes, c.MoveLanePoint = spansslopes(c.ID, evts, c.Distance)
|
|
return c
|
|
})
|
|
}
|
|
|
|
var laneStarts = [4][18]horse.TenThousandths{
|
|
{0, 555, 1111, 1666, 2222, 2777, 3333, 3888, 4444, 5333, 5888, 6444, 7000, 7555, 8111, 8666, 9222, 9777},
|
|
{0, 555, 1111, 1666, 2222, 2777, 3333, 3888, 4444, 5000, 5555, 6111, 6666, 7222, 7777, 8333, 8888, 9444},
|
|
{0, 555, 1111, 1666, 2222, 2777, 3333, 3888, 4444, 5000, 5555, 6111, 6666, 7222, 8811, 9366, 9922, 10477},
|
|
{0, 555, 1111, 1666, 2222, 2777, 3333, 3888, 4777, 5333, 5888, 6444, 7000, 7555, 8111, 8666, 9222, 9777},
|
|
}
|
|
|
|
//go:embed coursedata/*.json
|
|
var coursedataJSON embed.FS
|
|
|
|
type courseParams struct {
|
|
CourseParams []courseEvent `json:"courseParams"`
|
|
}
|
|
|
|
type courseEvent struct {
|
|
Type int8 `json:"_paramType"`
|
|
Values [2]int32 `json:"_values"`
|
|
Distance float32 `json:"_distance"`
|
|
}
|
|
|
|
func cmpEvents(a, b courseEvent) int {
|
|
if a.Distance == b.Distance {
|
|
return -cmp.Compare(a.Type, b.Type) // ensure straights are before corners
|
|
}
|
|
return cmp.Compare(a.Distance, b.Distance)
|
|
}
|
|
|
|
func courseEvents(id horse.RacecourseID) []courseEvent {
|
|
f, err := coursedataJSON.Open(fmt.Sprintf("coursedata/%d.json", id))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
d := json.NewDecoder(f)
|
|
d.DisallowUnknownFields()
|
|
var p courseParams
|
|
if err := d.Decode(&p); err != nil {
|
|
return nil
|
|
}
|
|
slices.SortFunc(p.CourseParams, cmpEvents)
|
|
return p.CourseParams
|
|
}
|
|
|
|
func spansslopes(id horse.RacecourseID, evts []courseEvent, length float32) ([]horse.RacecourseSpan, []horse.Slope, float32) {
|
|
// ensure spans and slopes are always non-nil for json
|
|
spans := []horse.RacecourseSpan{}
|
|
slopes := []horse.Slope{}
|
|
var ml float32
|
|
for _, evt := range evts {
|
|
var span horse.RacecourseSpan
|
|
switch evt.Type {
|
|
default:
|
|
panic(fmt.Errorf("horse: racecourse %d: unknown course event type %+v", id, evt))
|
|
case 1, 4, 5, 6, 8, 9, 10:
|
|
// Other events, e.g. jikkyo and bgm triggers.
|
|
// We aren't interested.
|
|
continue
|
|
case 3:
|
|
// Lane max change.
|
|
// We don't use this yet, but we might one day.
|
|
continue
|
|
|
|
case 7:
|
|
// Move lane point.
|
|
ml = evt.Distance
|
|
continue
|
|
|
|
case 11:
|
|
// Slope.
|
|
slope := horse.Slope{
|
|
Per: horse.TenThousandths(evt.Values[0]),
|
|
Start: evt.Distance,
|
|
End: evt.Distance + float32(evt.Values[1]),
|
|
}
|
|
slopes = append(slopes, slope)
|
|
continue
|
|
|
|
case 0:
|
|
// Corner.
|
|
span = horse.RacecourseSpan{
|
|
Start: evt.Distance,
|
|
End: evt.Distance + float32(evt.Values[1]),
|
|
Corner: int8(evt.Values[0]),
|
|
}
|
|
if ml == 0 {
|
|
ml = span.Start
|
|
}
|
|
// Continue out of the switch.
|
|
|
|
case 2:
|
|
// Straight.
|
|
if evt.Values[0] == 2 {
|
|
// End of the straight started by the previous event.
|
|
spans[len(spans)-1].End = evt.Distance
|
|
continue
|
|
}
|
|
// Start of the straight.
|
|
span = horse.RacecourseSpan{
|
|
Start: evt.Distance,
|
|
Straight: true,
|
|
FarStraight: evt.Values[1] == 2,
|
|
}
|
|
// Continue out of the switch.
|
|
}
|
|
// At this point, we are defining either a corner or a new straight.
|
|
var prev horse.RacecourseSpan
|
|
if len(spans) != 0 {
|
|
prev = spans[len(spans)-1]
|
|
}
|
|
if span.Start != prev.End {
|
|
// Longchamp 1000 (which doesn't appear to actually be used but w/e)
|
|
// defines straights by their start positions only, so check for the
|
|
// previous event being a straight with no end.
|
|
if prev.Straight && prev.End == 0 {
|
|
spans[len(spans)-1].End = span.Start
|
|
} else {
|
|
// Otherwise, insert a new span which is neither straight nor corner.
|
|
spans = append(spans, horse.RacecourseSpan{Start: prev.End, End: span.Start})
|
|
}
|
|
}
|
|
spans = append(spans, span)
|
|
}
|
|
// Always set the last span to end where the race does.
|
|
if len(spans) > 0 {
|
|
spans[len(spans)-1].End = length
|
|
}
|
|
return spans, slopes, ml
|
|
}
|