Skip to content

Commit

Permalink
Move Server Protocol to livesplit-core (#932)
Browse files Browse the repository at this point in the history
This moves the server protocol to `livesplit-core` and improves on it in
various ways:
- The protocol is based on JSON messages. This allows for example for
  more structured commands where it's easier to provide multiple
  arguments for a command and even have optional arguments.
- For each command, there is a corresponding response. It is either a
  `success` response with possibly the value that you requested, or an
  `error` response with an error `code`.
- On top of the responses you also get sent `event` messages that
  indicate changes to the timer. These can either be changes triggered
  via a command that you sent or by changes that happened through other
  sources, such as the user directly interacting with the timer or an
  auto splitter.

The protocol is still work in progress and we will evolve it into a
protocol that fully allows synchronizing timers over the network.

The event sink has now been renamed to command sink, because there is
now a clear distinction between incoming commands and events that are
the results of these commands.

Changelog: The protocol used for the server connection has been
significantly improved. There is a response for each command and clear
errors when something goes wrong. Additionally, there are now event
messages that indicate changes to the timer.
  • Loading branch information
CryZe authored Jun 22, 2024
1 parent e897dd8 commit f681701
Show file tree
Hide file tree
Showing 10 changed files with 631 additions and 471 deletions.
2 changes: 1 addition & 1 deletion livesplit-core
Submodule livesplit-core updated 41 files
+6 −6 benches/balanced_pb.rs
+2 −2 benches/layout_state.rs
+7 −7 benches/scene_management.rs
+7 −7 benches/software_rendering.rs
+7 −7 benches/svg_rendering.rs
+3 −2 capi/Cargo.toml
+93 −0 capi/bind_gen/src/typescript.ts
+250 −0 capi/src/command_sink.rs
+0 −110 capi/src/event_sink.rs
+6 −6 capi/src/hotkey_system.rs
+5 −2 capi/src/lib.rs
+29 −0 capi/src/server_protocol.rs
+44 −36 capi/src/timer.rs
+349 −0 capi/src/web_command_sink.rs
+0 −219 capi/src/web_event_sink.rs
+1 −1 src/analysis/pb_chance/tests.rs
+2 −2 src/analysis/sum_of_segments/tests.rs
+1 −1 src/analysis/tests/semantic_colors.rs
+14 −14 src/auto_splitting/mod.rs
+12 −12 src/component/detailed_timer/tests.rs
+1 −1 src/component/segment_time/tests.rs
+1 −1 src/component/splits/mod.rs
+43 −43 src/component/splits/tests/column.rs
+8 −8 src/component/splits/tests/mod.rs
+3 −3 src/component/title/tests.rs
+370 −106 src/event.rs
+37 −21 src/hotkey_system.rs
+3 −4 src/lib.rs
+3 −0 src/networking/mod.rs
+390 −0 src/networking/server_protocol.rs
+9 −7 src/timing/mod.rs
+19 −8 src/timing/timer/active_attempt.rs
+143 −95 src/timing/timer/mod.rs
+886 −0 src/timing/timer/tests/events.rs
+31 −21 src/timing/timer/tests/mark_as_modified.rs
+29 −30 src/timing/timer/tests/mod.rs
+3 −3 src/timing/timer/tests/variables.rs
+1 −1 src/timing/timer_phase.rs
+18 −11 src/util/tests_helper.rs
+7 −7 tests/rendering.rs
+2 −2 tests/run_files/livesplit1.0.lss
66 changes: 35 additions & 31 deletions src/api/LiveSplitServer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { toast } from "react-toastify";
import { LSOEventSink } from "../ui/LSOEventSink";
import { LSOCommandSink } from "../ui/LSOCommandSink";
import { ServerProtocol } from "../livesplit-core/livesplit_core";
import { Event } from "../livesplit-core";

export class LiveSplitServer {
private connection: WebSocket;
Expand All @@ -9,7 +11,7 @@ export class LiveSplitServer {
url: string,
private forceUpdate: () => void,
onServerConnectionClosed: () => void,
eventSink: LSOEventSink,
commandSink: LSOCommandSink,
) {
try {
this.connection = new WebSocket(url);
Expand All @@ -29,38 +31,31 @@ export class LiveSplitServer {
};

this.connection.onerror = () => {
// The onerror event does not contain any useful information.
toast.error("An error while communicating with the server occurred.");
if (wasConnected) {
// The onerror event does not contain any useful information.
toast.error("An error while communicating with the server occurred.");
}
};

let sendQueue = Promise.resolve();

this.connection.onmessage = (e) => {
if (typeof e.data === "string") {
const index = e.data.indexOf(" ");
let command = e.data;
let arg = "";
if (index >= 0) {
command = e.data.substring(0, index);
arg = e.data.substring(index + 1);
}
switch (command) {
case "start": eventSink.start(); break;
case "split": eventSink.split(); break;
case "splitorstart": eventSink.splitOrStart(); break;
case "reset": eventSink.reset(); break;
case "togglepause": eventSink.togglePauseOrStart(); break;
case "undo": eventSink.undoSplit(); break;
case "skip": eventSink.skipSplit(); break;
case "initgametime": eventSink.initializeGameTime(); break;
case "setgametime": eventSink.setGameTimeString(arg ?? ""); break;
case "setloadingtimes": eventSink.setLoadingTimesString(arg ?? ""); break;
case "pausegametime": eventSink.pauseGameTime(); break;
case "resumegametime": eventSink.resumeGameTime(); break;
case "setvariable": {
const [key, value] = JSON.parse(arg ?? "");
eventSink.setCustomVariable(key, value);
break;
// Handle and enqueue the command handling immediately, but send
// the response only after all previous responses have been
// sent.

const promise = ServerProtocol.handleCommand(e.data, commandSink.getCommandSink().ptr);
sendQueue = sendQueue.then(async () => {
const message = await promise;
if (this.connection.readyState === WebSocket.OPEN) {
this.connection.send(message);
}
}
});
} else {
sendQueue = sendQueue.then(() => {
this.connection.send('{"Err":{"code":"InvalidCommand"}}');
});
}
};

Expand All @@ -80,15 +75,24 @@ export class LiveSplitServer {
};
}

close(): void {
public close(): void {
if (this.connection.readyState === WebSocket.OPEN) {
this.wasIntendingToDisconnect = true;
this.connection.close();
this.forceUpdate();
}
}

getConnectionState(): number {
public getConnectionState(): number {
return this.connection.readyState;
}

public sendEvent(event: Event) {
if (this.connection.readyState === WebSocket.OPEN) {
const message = ServerProtocol.encodeEvent(event);
if (message !== undefined) {
this.connection.send(message);
}
}
}
}
Loading

0 comments on commit f681701

Please sign in to comment.