depot/go/trains/cmd/train2livesplit/train2livesplit.go

257 lines
7.3 KiB
Go

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<int, object> timeFromSeconds = delegate (int seconds) {
if (seconds < 0) {
return defaultTime;
}
var time = Activator.CreateInstance(timeType);
time.RealTime = TimeSpan.FromSeconds(seconds);
return time;
};
Action<string, int, int> 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)
}
}