diff --git a/src/components/RunMosaic/RunMosaic.tsx b/src/components/RunMosaic/RunMosaic.tsx index 3e4635e..baccd16 100644 --- a/src/components/RunMosaic/RunMosaic.tsx +++ b/src/components/RunMosaic/RunMosaic.tsx @@ -21,6 +21,7 @@ import "@blueprintjs/icons/lib/css/blueprint-icons.css"; import UpcomingDisplay from "../UpcomingDisplay/UpcomingDisplay"; import StorageManager from "../../utils/StorageManager"; +import liveSplitService from "../../services/LiveSplitWebSocket"; type RunParams = { routeUrl: string; @@ -30,6 +31,7 @@ const RunMosaic: React.FC = () => { const { routeUrl } = useParams(); const routeStatus = useSelector(selectRouteStatus); const dispatch = useAppDispatch(); + const liveSplit = liveSplitService; useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { @@ -67,6 +69,27 @@ const RunMosaic: React.FC = () => { } }, [dispatch, routeUrl, routeStatus]); + useEffect(() => { + function handleLoad() { + (async () => { + liveSplit.connect(); + })(); + } + if (document.readyState === "complete") { + handleLoad(); + } else { + window.addEventListener("load", handleLoad); + return () => { + window.removeEventListener("load", handleLoad); + }; + } + return () => { + if (liveSplit) { + liveSplit.disconnect(); + } + }; + }, [liveSplit]); + if (routeStatus !== "succeeded") { return
Loading...
; } diff --git a/src/services/LiveSplitWebSocket.ts b/src/services/LiveSplitWebSocket.ts new file mode 100644 index 0000000..5d5dd3d --- /dev/null +++ b/src/services/LiveSplitWebSocket.ts @@ -0,0 +1,109 @@ +class LiveSplitWebSocket { + private static instance: LiveSplitWebSocket; + + private socket: WebSocket | null = null; + private readonly url: string = "ws://localhost:16835/livesplit"; + + private pendingRequests: { + [id: string]: { + resolve: (value: string | PromiseLike) => void; + reject: (reason?: any) => void; + }; + } = {}; + + private constructor() {} + + public static getInstance(): LiveSplitWebSocket { + if (!LiveSplitWebSocket.instance) { + LiveSplitWebSocket.instance = new LiveSplitWebSocket(); + } + return LiveSplitWebSocket.instance; + } + + isConnected(): boolean { + return process.env.NODE_ENV !== "production" && this.socket?.readyState === WebSocket.OPEN; + } + + connect() { + this.socket = new WebSocket(this.url); + + this.socket.onopen = (event) => { + console.log("WebSocket Connected:", event); + }; + + this.socket.onerror = (error) => { + console.error("WebSocket Error:", error); + }; + + this.socket.onclose = (event) => { + console.log("WebSocket Closed:", event); + this.socket = null; + }; + + this.socket.onmessage = (event) => { + console.log("WebSocket Message Received:", event.data); + const response = JSON.parse(event.data); + + if (this.pendingRequests[response.name]) { + const { resolve } = this.pendingRequests[response.name]; + resolve(response.data); + delete this.pendingRequests[response.name]; + } + }; + } + + disconnect() { + if (this.socket) { + this.socket.close(); + } + } + + send(data: string) { + if (this.socket && this.socket.readyState === WebSocket.OPEN) { + this.socket.send(data); + } else { + console.error("WebSocket is not open. Unable to send data."); + } + } + + split() { + this.send("split"); + } + + unsplit() { + this.send("unsplit"); + } + getCurrentSplitName(): Promise { + return new Promise((resolve, reject) => { + this.pendingRequests["getcurrentsplitname"] = { resolve, reject }; + + this.send("getcurrentsplitname"); + + setTimeout(() => { + if (this.pendingRequests["getcurrentsplitname"]) { + resolve(""); + delete this.pendingRequests["getcurrentsplitname"]; + } + }, 100); + }); + } + + getPreviousSplitName(): Promise { + return new Promise((resolve, reject) => { + this.pendingRequests["getprevioussplitname"] = { resolve, reject }; + + this.send("getprevioussplitname"); + + setTimeout(() => { + if (this.pendingRequests["getprevioussplitname"]) { + resolve(""); + delete this.pendingRequests["getprevioussplitname"]; + } + }, 100); + }); + } +} + +const liveSplitService = LiveSplitWebSocket.getInstance(); + +export default liveSplitService; diff --git a/src/store/progressSlice.ts b/src/store/progressSlice.ts index a26042e..13c893c 100644 --- a/src/store/progressSlice.ts +++ b/src/store/progressSlice.ts @@ -1,5 +1,6 @@ import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; import { RootState } from "."; +import liveSplitService from "../services/LiveSplitWebSocket"; interface ProgressState { branchIndex: number; @@ -11,15 +12,34 @@ const initialState: ProgressState = { pointIndex: 0, }; -export const incrementProgress = createAsyncThunk("progress/increment", (_, { getState }) => { +export const incrementProgress = createAsyncThunk("progress/increment", async (_, { getState }) => { const state: RootState = getState() as RootState; const { progress } = state; + const liveSplit = liveSplitService; if (state.route.data) { + const point = state.route.data.branches[progress.branchIndex].points[progress.pointIndex]; + const thing = state.route.data.things[point.layerId][point.thingId]; + if ( + liveSplit.isConnected() && + ((thing.type === "Shrine" && point.action === "COMPLETE") || thing.type === "Lightroot") + ) { + const currentSplitName = await liveSplit.getCurrentSplitName(); + if (currentSplitName.endsWith(thing.name)) { + liveSplit.split(); + } + } + if ( progress.branchIndex < state.route.data.branches.length - 1 && progress.pointIndex === state.route.data.branches[progress.branchIndex].points.length - 1 ) { + if (liveSplit.isConnected()) { + const currentSplitName = await liveSplit.getCurrentSplitName(); + if (currentSplitName === state.route.data.branches[progress.branchIndex]?.name) { + liveSplit.split(); + } + } return { ...progress, branchIndex: progress.branchIndex + 1, pointIndex: 0 }; } else if (progress.pointIndex < state.route.data.branches[progress.branchIndex].points.length - 1) { return { ...progress, pointIndex: progress.pointIndex + 1 }; @@ -29,17 +49,40 @@ export const incrementProgress = createAsyncThunk("progress/increment", (_, { ge return progress; }); -export const decrementProgress = createAsyncThunk("progress/decrement", (_, { getState }) => { +export const decrementProgress = createAsyncThunk("progress/decrement", async (_, { getState }) => { const state: RootState = getState() as RootState; const { progress } = state; + const liveSplit = liveSplitService; if (state.route.data) { + let newProgress; if (progress.branchIndex > 0 && progress.pointIndex === 0) { const prevBranchLastPointIndex = state.route.data.branches[progress.branchIndex - 1].points.length - 1; - return { ...progress, branchIndex: progress.branchIndex - 1, pointIndex: prevBranchLastPointIndex }; + if (liveSplit.isConnected()) { + const previousSplitName = await liveSplit.getPreviousSplitName(); + if (previousSplitName === state.route.data.branches[progress.branchIndex - 1]?.name) { + liveSplit.unsplit(); + } + } + newProgress = { ...progress, branchIndex: progress.branchIndex - 1, pointIndex: prevBranchLastPointIndex }; } else if (progress.pointIndex > 0) { - return { ...progress, pointIndex: progress.pointIndex - 1 }; + newProgress = { ...progress, pointIndex: progress.pointIndex - 1 }; + } + if (newProgress) { + const point = state.route.data.branches[newProgress.branchIndex].points[newProgress.pointIndex]; + const thing = state.route.data.things[point.layerId][point.thingId]; + if ( + liveSplit.isConnected() && + ((thing.type === "Shrine" && point.action === "COMPLETE") || thing.type === "Lightroot") + ) { + const previousSplitName = await liveSplit.getPreviousSplitName(); + if (previousSplitName.endsWith(thing.name)) { + liveSplit.unsplit(); + } + } } + + return newProgress; } return progress;