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 }