package replay import ( "compress/gzip" "encoding/base64" "encoding/binary" "errors" "fmt" "io" "math" "strings" ) type simdecoder struct { r *gzip.Reader err error } func readfull(r io.Reader, b []byte) error { _, err := io.ReadFull(r, b) if err == io.EOF { return io.ErrUnexpectedEOF } return err } func (r *simdecoder) f(name string) float32 { var b [4]byte err := readfull(r.r, b[:]) switch err { case nil: // do nothing case io.ErrUnexpectedEOF: // Check that the existing error doesn't already have this one // so we don't spam on short input. if r.err != nil && errors.Is(r.err, err) { break } fallthrough default: r.err = errors.Join(r.err, fmt.Errorf("scanning %s: %w", name, err)) } return math.Float32frombits(binary.LittleEndian.Uint32(b[:])) } func (r *simdecoder) i(name string) int32 { var b [4]byte err := readfull(r.r, b[:]) switch err { case nil: // do nothing case io.ErrUnexpectedEOF: // Check that the existing error doesn't already have this one // so we don't spam on short input. if r.err != nil && errors.Is(r.err, err) { break } fallthrough default: r.err = errors.Join(r.err, fmt.Errorf("scanning %s: %w", name, err)) } return int32(binary.LittleEndian.Uint32(b[:])) } func (r *simdecoder) i16(name string) int16 { var b [2]byte err := readfull(r.r, b[:]) switch err { case nil: // do nothing case io.ErrUnexpectedEOF: // Check that the existing error doesn't already have this one // so we don't spam on short input. if r.err != nil && errors.Is(r.err, err) { break } fallthrough default: r.err = errors.Join(r.err, fmt.Errorf("scanning %s: %w", name, err)) } return int16(binary.LittleEndian.Uint16(b[:])) } func (r *simdecoder) i8(name string) int8 { var b [1]byte err := readfull(r.r, b[:]) switch err { case nil: // do nothing case io.ErrUnexpectedEOF: // Check that the existing error doesn't already have this one // so we don't spam on short input. if r.err != nil && errors.Is(r.err, err) { break } fallthrough default: r.err = errors.Join(r.err, fmt.Errorf("scanning %s: %w", name, err)) } return int8(b[0]) } func DecodeSim(simDataBase64 string) (*RaceSimulateData, error) { r, err := gzip.NewReader(base64.NewDecoder(base64.StdEncoding, strings.NewReader(simDataBase64))) if err != nil { return nil, err } d := simdecoder{r: r} s := RaceSimulateData{ Header: RaceSimulateDataHeader{ MaxLength: d.i("RaceSimulateDataHeader.maxLength"), Version: d.i("RaceSimulateDataHeader.version"), }, DistanceDiffMax: d.f("distanceDiffMax"), HorseNum: d.i("horseNum"), HorseFrameSize: d.i("horseFrameSize"), HorseResultSize: d.i("horseResultSize"), PaddingSize1: d.i("PaddingSize1"), FrameCount: d.i("frameCount"), FrameSize: d.i("frameSize"), } s.Frame = make([]RaceSimulateFrameData, s.FrameCount) horseframes := make([]RaceSimulateHorseFrameData, s.FrameCount*s.HorseNum) for i := range s.Frame { s.Frame[i] = RaceSimulateFrameData{ Time: d.f("RaceSimulateFrameData.time"), HorseFrame: horseframes[i*int(s.HorseNum) : (i+1)*int(s.HorseNum)], } for j := range s.Frame[i].HorseFrame { s.Frame[i].HorseFrame[j] = RaceSimulateHorseFrameData{ Distance: d.f("RaceSimulateHorseFrameData.distance"), LanePosition: d.i16("RaceSimulateHorseFrameData.lanePosition"), Speed: d.i16("RaceSimulateHorseFrameData.speed"), HP: d.i16("RaceSimulateHorseFrameData.hp"), TemptationMode: TemptationMode(d.i8("RaceSimulateHorseFrameData.temptationMode")), BlockFrontHorseIndex: d.i8("RaceSimulateHorseFrameData.blockFrontHorseIndex"), } } } s.PaddingSize2 = d.i("PaddingSize2") s.HorseResult = make([]RaceSimulateHorseResultData, s.HorseNum) for i := range s.HorseResult { s.HorseResult[i] = RaceSimulateHorseResultData{ FinishOrder: d.i("RaceSimulateHorseResultData.finishOrder"), FinishTime: d.f("RaceSimulateHorseResultData.finishTime"), FinishDiffTime: d.f("RaceSimulateHorseResultData.finishDiffTime"), StartDelayTime: d.f("RaceSimulateHorseResultData.startDelayTime"), GutsOrder: d.i8("RaceSimulateHorseResultData.gutsOrder"), WizOrder: d.i8("RaceSimulateHorseResultData.wizOrder"), LastSpurtStartDistance: d.f("RaceSimulateHorseResultData.lastSpurtStartDistance"), RunningStyle: RunningStyle(d.i8("RaceSimulateHorseResultData.runningStyle")), Defeat: d.i("RaceSimulateHorseResultData.defeat"), FinishTimeRaw: d.f("RaceSimulateHorseResultData.finishTimeRaw"), } } s.PaddingSize3 = d.i("PaddingSize3") s.EventCount = d.i("eventCount") s.Event = make([]SizedEvent, s.EventCount) for i := range s.Event { s.Event[i] = SizedEvent{ EventSize: d.i16("SizedEvent.eventSize"), Event: RaceSimulateEventData{ FrameTime: d.f("RaceSimulateEventData.frameTime"), Type: SimulateEventType(d.i8("RaceSimulateEventData.type")), ParamCount: d.i8("RaceSimulateEventData.paramCount"), }, } s.Event[i].Event.Param = make([]int32, s.Event[i].Event.ParamCount) for j := range s.Event[i].Event.Param { s.Event[i].Event.Param[j] = d.i("RaceSimulateEventData.param") } } if d.err != nil { return nil, d.err } return &s, nil } type RaceSimulateData struct { Header RaceSimulateDataHeader `json:"header"` DistanceDiffMax float32 `json:"distanceDiffMax"` HorseNum int32 `json:"horseNum"` HorseFrameSize int32 `json:"horseFrameSize"` HorseResultSize int32 `json:"horseResultSize"` PaddingSize1 int32 `json:"PaddingSize1"` FrameCount int32 `json:"frameCount"` FrameSize int32 `json:"frameSize"` Frame []RaceSimulateFrameData `json:"frame"` PaddingSize2 int32 `json:"PaddingSize2"` HorseResult []RaceSimulateHorseResultData `json:"horseResult"` PaddingSize3 int32 `json:"PaddingSize3"` EventCount int32 `json:"eventCount"` Event []SizedEvent `json:"event"` } type RaceSimulateDataHeader struct { MaxLength int32 Version int32 } type RaceSimulateFrameData struct { Time float32 `json:"time"` HorseFrame []RaceSimulateHorseFrameData `json:"horseFrame"` } type RaceSimulateHorseFrameData struct { Distance float32 `json:"distance"` LanePosition int16 `json:"lanePosition"` Speed int16 `json:"speed"` HP int16 `json:"hp"` TemptationMode TemptationMode `json:"temptationMode"` BlockFrontHorseIndex int8 `json:"blockFrontHorseIndex"` } type TemptationMode int8 //go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type TemptationMode -linecomment const ( TemptationNull TemptationMode = iota // NULL TemptationPositionSashi // POSITION_SASHI TemptationPositionSenko // POSITION_SENKO TemptationPositionNige // POSITION_NIGE TemptationBoost // BOOST ) func (t TemptationMode) AppendText(b []byte) ([]byte, error) { return append(b, t.String()...), nil } func (t TemptationMode) MarshalText() ([]byte, error) { return t.AppendText(nil) } func (t *TemptationMode) UnmarshalText(b []byte) error { switch string(b) { case "NULL": *t = TemptationNull case "POSITION_SASHI": *t = TemptationPositionSashi case "POSITION_SENKO": *t = TemptationPositionSenko case "POSITION_NIGE": *t = TemptationPositionNige case "BOOST": *t = TemptationBoost default: return fmt.Errorf("invalid TemptationMode %q", b) } return nil } type RaceSimulateHorseResultData struct { FinishOrder int32 `json:"finishOrder"` FinishTime float32 `json:"finishTime"` FinishDiffTime float32 `json:"finishDiffTime"` StartDelayTime float32 `json:"startDelayTime"` GutsOrder int8 `json:"gutsOrder"` WizOrder int8 `json:"wizOrder"` LastSpurtStartDistance float32 `json:"lastSpurtStartDistance"` RunningStyle RunningStyle `json:"runningStyle"` Defeat int32 `json:"defeat"` FinishTimeRaw float32 `json:"finishTimeRaw"` } type RunningStyle int8 //go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type RunningStyle -linecomment const ( StyleNone RunningStyle = iota // NONE StyleNige // NIGE StyleSenko // SENKO StyleSashi // SASHI StyleOikomi // OIKOMI ) func (s RunningStyle) AppendText(b []byte) ([]byte, error) { return append(b, s.String()...), nil } func (s RunningStyle) MarshalText() ([]byte, error) { return s.AppendText(nil) } func (s *RunningStyle) UnmarshalText(b []byte) error { switch string(b) { case "NONE": *s = StyleNone case "NIGE": *s = StyleNige case "SENKO": *s = StyleSenko case "SASHI": *s = StyleSashi case "OIKOMI": *s = StyleOikomi default: return fmt.Errorf("invalid RunningStyle %q", b) } return nil } type SizedEvent struct { EventSize int16 `json:"eventSize"` Event RaceSimulateEventData `json:"event"` } type RaceSimulateEventData struct { FrameTime float32 `json:"frameTime"` Type SimulateEventType `json:"type"` ParamCount int8 `json:"paramCount"` Param []int32 `json:"param"` } type SimulateEventType int8 //go:generate go run golang.org/x/tools/cmd/stringer@v0.41.0 -type SimulateEventType -linecomment const ( EventScore SimulateEventType = iota // SCORE EventChallengeMatchPoint // CHALLENGE_MATCH_POINT EventNoUse2 // NOUSE_2 EventSkill // SKILL EventCompeteTop // COMPETE_TOP EventCompeteFight // COMPETE_FIGHT EventReleaseConservePower // RELEASE_CONSERVE_POWER ) func (t SimulateEventType) AppendText(b []byte) ([]byte, error) { return append(b, t.String()...), nil } func (t SimulateEventType) MarshalText() ([]byte, error) { return t.AppendText(nil) } func (t *SimulateEventType) UnmarshalText(b []byte) error { switch string(b) { case "SCORE": *t = EventScore case "CHALLENGE_MATCH_POINT": *t = EventChallengeMatchPoint case "NOUSE_2": *t = EventNoUse2 case "SKILL": *t = EventSkill case "COMPETE_TOP": *t = EventCompeteTop case "COMPETE_FIGHT": *t = EventCompeteFight case "RELEASE_CONSERVE_POWER": *t = EventReleaseConservePower default: return fmt.Errorf("invalid SimulateEventType %q", b) } return nil }