package main import ( "bufio" "context" "encoding/json" "flag" "fmt" "log" "net/http" "os" "strings" "text/template" "time" "hg.lukegb.com/lukegb/depot/go/trains/webapi" ) var ( header = template.Must(template.New("script").Parse(` startscript C# this["model"].Reset(); var run = this["state"].Run; run.CategoryName = "{{if .Service.Headcode}}{{.Service.Headcode}} {{end}}{{.FirstLocation.DepartureTiming.PublicScheduled.Format "1504"}} {{.FirstLocation.Location.Name}} to {{.LastLocation.Location.Name}}"; run.GameName = "{{.Service.StartDate}}"; run.AttemptCount = 0; run.AttemptHistory.Clear(); run.AutoSplitter = null; run.HasChanged = true; run.Clear(); var ass = run.GetType().Assembly; var timingMethodRealTime = Enum.GetValues(ass.GetType("LiveSplit.Model.TimingMethod"))[0]; this["state"].CurrentTimingMethod = timingMethodRealTime; var timeStampType = ass.GetType("LiveSplit.Model.TimeStamp"); var atomicDateTime = ass.GetType("LiveSplit.Model.AtomicDateTime"); // (System.DateTime, synced) var runExtensionsType = ass.GetType("LiveSplit.Model.RunExtensions"); var runExtensionsAddSegment = runExtensionsType.GetMethod("AddSegment"); var timeType = ass.GetType("LiveSplit.Model.Time"); var defaultTime = Activator.CreateInstance(timeType); var lastSplitTime = defaultTime; Func timeFromSeconds = delegate (int seconds) { if (seconds < 0) { return defaultTime; } var time = Activator.CreateInstance(timeType); time.RealTime = TimeSpan.FromSeconds(seconds); return time; }; Action addSplit = delegate (string name, int timetableSeconds, int realSeconds) { var timetableTime = Convert.ChangeType(timeFromSeconds(timetableSeconds), timeType); var lastTimetableTime = Convert.ChangeType(timeFromSeconds(0), timeType); if (this["state"].Run.Count > 0) { lastTimetableTime = this["state"].Run[this["state"].Run.Count-1].PersonalBestSplitTime; } var timetableSegmentTime = timetableTime - lastTimetableTime; var realTime = timeFromSeconds(realSeconds); runExtensionsAddSegment.Invoke(null, new object[] { run, name, timetableTime, timetableSegmentTime, null, realTime, null }); lastSplitTime = this["state"].Run[this["state"].Run.Count-1].SplitTime; }; // run, name, pbSplitTime, bestSegmentTime, icon, splitTime, segmentHistory {{.AddSplitFunctions}} endscript `)) trainId = flag.Int("train_id", -1, "Train ID") endpoint = flag.String("endpoint", "http://localhost:13974", "db2web endpoint") ) type templateVars struct { Service *webapi.ServiceData FirstLocation *webapi.ServiceLocation LastLocation *webapi.ServiceLocation AddSplitFunctions string } func fetchTrainOnce(ctx context.Context, id int) (*webapi.ServiceData, error) { req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/%d", *endpoint, id), nil) if err != nil { return nil, fmt.Errorf("constructing new request for %d: %w", id, err) } req.Header.Set("Accept", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("doing request for %d: %w", id, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("http status for %d was %d %v", id, resp.StatusCode, resp.Status) } var sd webapi.ServiceData if err := json.NewDecoder(resp.Body).Decode(&sd); err != nil { return nil, fmt.Errorf("decoding train %d: %w", id, err) } return &sd, nil } func generateLiveSplitScriptData(sd *webapi.ServiceData) (*templateVars, error) { asf := new(strings.Builder) currentSplit := -1 usingTime := func(td *webapi.TimingData) (time.Time, error) { if td.PublicScheduled != nil { return td.PublicScheduled.ToTime() } return td.WorkingScheduled.ToTime() } timetableDepartureTime, err := usingTime(sd.Locations[0].DepartureTiming) if err != nil { return nil, fmt.Errorf("parsing departure time: %w", err) } var lastHadActual bool for n, l := range sd.Locations { var usingData *webapi.TimingData switch { case l.DepartureTiming != nil: usingData = l.DepartureTiming case l.PassTiming != nil: usingData = l.PassTiming case l.ArrivalTiming != nil: usingData = l.ArrivalTiming } usingTimetableTime, err := usingTime(usingData) if err != nil { return nil, fmt.Errorf("parsing time for %q: %w", l.Location.Name, err) } var secondsOnTimetable, secondsActual int secondsOnTimetable = int((usingTimetableTime.Sub(timetableDepartureTime)) / time.Second) secondsActual = -1 if lastHadActual { currentSplit = n lastHadActual = false } if usingData.Actual != nil { timeActual, err := usingData.Actual.ToTime() if err != nil { return nil, fmt.Errorf("parsing actual time for %q: %w", l.Location.Name, err) } secondsActual = int((timeActual.Sub(timetableDepartureTime)) / time.Second) lastHadActual = true } fmt.Fprintf(asf, "addSplit(%q, %d, %d);\n", l.Location.Name, secondsOnTimetable, secondsActual) } if lastHadActual { fmt.Fprintf(asf, ` this["state"].Run.Offset = TimeSpan.FromSeconds(%d); this["model"].Start(); this["state"].CurrentSplitIndex = %d-1; this["model"].Split(); this["state"].Run[%d-1].SplitTime = lastSplitTime; `, (time.Now().Sub(timetableDepartureTime))/time.Second, len(sd.Locations), len(sd.Locations)) } else { fmt.Fprintf(asf, ` this["state"].Run.Offset = TimeSpan.FromSeconds(%d); this["model"].Start(); this["state"].CurrentSplitIndex = %d; `, (time.Now().Sub(timetableDepartureTime))/time.Second, currentSplit) } return &templateVars{ Service: sd, FirstLocation: sd.Locations[0], LastLocation: sd.Locations[len(sd.Locations)-1], AddSplitFunctions: asf.String(), }, nil } func fetchTrainAndOutputLiveSplit(ctx context.Context, id int) error { req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/?id=%d", *endpoint, id), nil) if err != nil { return fmt.Errorf("constructing new request for %d: %w", id, err) } req.Header.Set("Accept", "text/event-stream") resp, err := http.DefaultClient.Do(req) if err != nil { return fmt.Errorf("doing request for %d: %w", id, err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("http status for %d was %d %v", id, resp.StatusCode, resp.Status) } sc := bufio.NewScanner(resp.Body) var buffer []string for sc.Scan() { buffer = append(buffer, sc.Text()) if buffer[0] == "" { buffer = buffer[1:] } fmt.Fprintf(os.Stderr, "%#v\n\n", buffer) if len(buffer) < 2 || buffer[len(buffer)-1] != "" { continue } if strings.HasPrefix(buffer[0], "event:") { buffer = nil continue } var xs []string for _, s := range buffer { xs = append(xs, strings.TrimPrefix(s, "data: ")) } var sd webapi.ServiceData if err := json.Unmarshal([]byte(strings.Join(xs, "")), &sd); err != nil { return err } tv, err := generateLiveSplitScriptData(&sd) if err != nil { return err } if err := header.Execute(os.Stdout, tv); err != nil { return err } buffer = nil } return nil } func main() { flag.Parse() ctx, cancel := context.WithCancel(context.Background()) defer cancel() log.Fatal(fetchTrainAndOutputLiveSplit(ctx, *trainId)) sd, err := fetchTrainOnce(ctx, 33634) if err != nil { log.Fatal(err) } tv, err := generateLiveSplitScriptData(sd) if err != nil { log.Fatal(err) } if err := header.Execute(os.Stdout, tv); err != nil { log.Fatal(err) } }