diff --git a/src/GameOptions/ui/RemoteAPIPage.tsx b/src/GameOptions/ui/RemoteAPIPage.tsx index 43fdca73e..9563cfc57 100644 --- a/src/GameOptions/ui/RemoteAPIPage.tsx +++ b/src/GameOptions/ui/RemoteAPIPage.tsx @@ -13,9 +13,14 @@ export const RemoteAPIPage = (): React.ReactElement => { ); const [remoteFileApiPort, setRemoteFileApiPort] = useState(Settings.RemoteFileApiPort.toString()); const [portError, setPortError] = useState(isValidConnectionPort(Settings.RemoteFileApiPort).message ?? ""); + const [remoteFileApiReconnectionDelay, setRemoteFileApiReconnectionDelay] = useState( + Settings.RemoteFileApiReconnectionDelay.toString(), + ); + const [reconnectionDelayError, setReconnectionDelayError] = useState(""); const isValidHostname = hostnameError === ""; const isValidPort = portError === ""; + const isValidReconnectionDelay = reconnectionDelayError === ""; function handleRemoteFileApiHostnameChange(event: React.ChangeEvent): void { const newValue = event.target.value.trim(); @@ -32,7 +37,7 @@ export const RemoteAPIPage = (): React.ReactElement => { function handleRemoteFileApiPortChange(event: React.ChangeEvent): void { const newValue = event.target.value.trim(); setRemoteFileApiPort(newValue); - const port = Number.parseInt(newValue); + const port = Number(newValue); const result = isValidConnectionPort(port); if (!result.success) { setPortError(result.message); @@ -42,6 +47,18 @@ export const RemoteAPIPage = (): React.ReactElement => { setPortError(""); } + function handleRemoteFileApiReconnectionDelayChange(event: React.ChangeEvent): void { + const newValue = event.target.value.trim(); + setRemoteFileApiReconnectionDelay(newValue); + const reconnectionDelay = Number(newValue); + if (!Number.isFinite(reconnectionDelay) || reconnectionDelay < 0) { + setReconnectionDelayError("Invalid reconnection delay"); + return; + } + Settings.RemoteFileApiReconnectionDelay = reconnectionDelay; + setReconnectionDelayError(""); + } + return ( @@ -73,7 +90,7 @@ export const RemoteAPIPage = (): React.ReactElement => { Hostname: , + startAdornment: Hostname: , }} value={remoteFileApiHostname} onChange={handleRemoteFileApiHostnameChange} @@ -97,7 +114,11 @@ export const RemoteAPIPage = (): React.ReactElement => { Port: , + startAdornment: ( + + Port:  + + ), }} value={remoteFileApiPort} onChange={handleRemoteFileApiPortChange} @@ -107,6 +128,33 @@ export const RemoteAPIPage = (): React.ReactElement => { {portError && {portError}} + + When the connection is closed, Bitburner will automatically reconnect after this delay. +
+ The value must be in seconds. Set it to 0 to disable the feature. + + } + > +
+ + Reconnection delay:  + + ), + }} + value={remoteFileApiReconnectionDelay} + onChange={handleRemoteFileApiReconnectionDelayChange} + placeholder="0" + size={"medium"} + /> + {reconnectionDelayError && {reconnectionDelayError}} +
+
(Settings.UseWssForRemoteFileApi = newValue)} diff --git a/src/RemoteFileAPI/Remote.ts b/src/RemoteFileAPI/Remote.ts index 0b5649b8c..8df90c378 100644 --- a/src/RemoteFileAPI/Remote.ts +++ b/src/RemoteFileAPI/Remote.ts @@ -8,6 +8,8 @@ function showErrorMessage(address: string, detail: string) { SnackbarEvents.emit(`Error with websocket ${address}, details: ${detail}`, ToastVariant.ERROR, 5000); } +const eventCodeWhenIntentionallyStoppingConnection = 3000; + export class Remote { connection?: WebSocket; ipaddr: string; @@ -19,7 +21,7 @@ export class Remote { } public stopConnection(): void { - this.connection?.close(); + this.connection?.close(eventCodeWhenIntentionallyStoppingConnection); } public startConnection(): void { @@ -40,9 +42,23 @@ export class Remote { 2000, ), ); - this.connection.addEventListener("close", () => - SnackbarEvents.emit("Remote API connection closed", ToastVariant.WARNING, 2000), - ); + this.connection.addEventListener("close", (event) => { + /** + * On Bitburner side, we may intentionally close the connection. For example, we do that before starting a new + * connection. In this event handler, we do things that are only necessary when the connection is closed + * unexpectedly (e.g., show a warning, reconnect after a delay), so we need to check whether the close event is + * unexpected. + */ + if (event.code === eventCodeWhenIntentionallyStoppingConnection) { + return; + } + SnackbarEvents.emit(`Remote API connection closed. Code: ${event.code}.`, ToastVariant.WARNING, 2000); + if (Settings.RemoteFileApiReconnectionDelay > 0) { + setTimeout(() => { + this.startConnection(); + }, Settings.RemoteFileApiReconnectionDelay * 1000); + } + }); } } diff --git a/src/Settings/Settings.ts b/src/Settings/Settings.ts index e76a68e6b..e8685d020 100644 --- a/src/Settings/Settings.ts +++ b/src/Settings/Settings.ts @@ -114,6 +114,8 @@ export const Settings = { RemoteFileApiAddress: "localhost", /** Port the Remote File API client will try to connect to. 0 to disable. */ RemoteFileApiPort: 0, + /** Automatically reconnect to the Remote File API client after this delay. Set it 0 to disable. */ + RemoteFileApiReconnectionDelay: 0, /** Use wss instead of ws when connecting to RFA clients */ UseWssForRemoteFileApi: false, /** Whether to save the game when the player saves any file. */