257 lines
7.3 KiB
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)
|
|
}
|
|
|
|
}
|