From 3d8616b3a7c3f389b8b24830acfcc66e9cf81650 Mon Sep 17 00:00:00 2001 From: Olivier Gagnon Date: Tue, 23 Aug 2022 17:50:31 -0400 Subject: [PATCH] Did some changes of the remote api and added documentation --- doc/source/index.rst | 1 + doc/source/remoteapi.rst | 203 ++++++++++++++++++++++ src/GameOptions/GameOptionsTab.ts | 1 + src/GameOptions/ui/ConnectionBauble.tsx | 23 ++- src/GameOptions/ui/CurrentOptionsPage.tsx | 30 ++-- src/GameOptions/ui/GameOptionsSidebar.tsx | 1 + src/RemoteFileAPI/MessageDefinitions.ts | 6 +- src/RemoteFileAPI/MessageHandlers.ts | 7 +- src/RemoteFileAPI/RFALogger.ts | 27 --- src/RemoteFileAPI/Remote.ts | 43 ++--- src/RemoteFileAPI/RemoteFileAPI.ts | 11 +- src/Settings/Settings.ts | 2 +- src/index.tsx | 2 +- 13 files changed, 281 insertions(+), 76 deletions(-) create mode 100644 doc/source/remoteapi.rst delete mode 100644 src/RemoteFileAPI/RFALogger.ts diff --git a/doc/source/index.rst b/doc/source/index.rst index 9e32ebff3..4d6a891c8 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -24,6 +24,7 @@ secrets that you've been searching for. Basic documentation Basic Gameplay Advanced Gameplay + Remote API Keyboard Shortcuts Game Frozen or Stuck? Guides & Tips diff --git a/doc/source/remoteapi.rst b/doc/source/remoteapi.rst new file mode 100644 index 000000000..06ad77d1a --- /dev/null +++ b/doc/source/remoteapi.rst @@ -0,0 +1,203 @@ + +Remote API +========== + +All versions of Bitburner can use websockets to connect to a server. +That server can then perform a number of actions. +Most commonly this is used in conjunction with an external text editor or API +in order to make writing scripts easier, or even use typescript. + +This API uses the JSON RCP 2.0 protocol. Inputs are in the following form: + + .. code-block:: javascript + + { + "jsonrpc": "2.0", + "id": number, + "method": string, + "params": any + } + +Outputs: + + .. code-block:: javascript + + { + "jsonrpc": "2.0", + "id": number, + "result": any, + "error": any + } + + +Methods +------- + +`pushFile` +^^^^^^^^^^ + Create or update a file. + + .. code-block:: javascript + + { + "jsonrpc": "2.0", + "id": number, + "method": "pushFile", + "params": { + filename: string; + content: string; + server: string; + } + } + + .. code-block:: javascript + + { + "jsonrpc": "2.0", + "id": number, + "result": "OK" + } + +`getFile` +^^^^^^^^^ + Read a file and it's content. + + .. code-block:: javascript + + { + "jsonrpc": "2.0", + "id": number, + "method": "getFile", + "params": { + filename: string; + server: string; + } + } + + .. code-block:: javascript + + { + "jsonrpc": "2.0", + "id": number, + "result": string + } + +`deleteFile` +^^^^^^^^^^^^ + Delete a file. + + .. code-block:: javascript + + { + "jsonrpc": "2.0", + "id": number, + "method": "deleteFile", + "params": { + filename: string; + server: string; + } + } + + .. code-block:: javascript + + { + "jsonrpc": "2.0", + "id": number, + "result": "OK" + } + +`getFileNames` +^^^^^^^^^^^^^^ + List all file names on a server. + + .. code-block:: javascript + + { + "jsonrpc": "2.0", + "id": number, + "method": "getFileNames", + "params": { + server: string; + } + } + + .. code-block:: javascript + + { + "jsonrpc": "2.0", + "id": number, + "result": string[] + } + +`getAllFiles` +^^^^^^^^^^^^^ + Get the content of all files on a server. + + .. code-block:: javascript + + { + "jsonrpc": "2.0", + "id": number, + "method": "getAllFiles", + "params": { + server: string; + } + } + + .. code-block:: javascript + + { + "jsonrpc": "2.0", + "id": number, + "result": { + filename: string, + content: string + }[] + } + +`calculateRam` +^^^^^^^^^^^^^^ + Calculate the in-game ram cost of a script. + + .. code-block:: javascript + + { + "jsonrpc": "2.0", + "id": number, + "method": "calculateRam", + "params": { + filename: string; + server: string; + } + } + + .. code-block:: javascript + + { + "jsonrpc": "2.0", + "id": number, + "result": number + } + + +`getDefinitionFile` +^^^^^^^^^^^^^^^^^^^ + Get the definition file of the API. + + .. code-block:: javascript + + { + "jsonrpc": "2.0", + "id": number, + "method": "getDefinitionFile" + } + + .. code-block:: javascript + + { + "jsonrpc": "2.0", + "id": number, + "result": string + } + + diff --git a/src/GameOptions/GameOptionsTab.ts b/src/GameOptions/GameOptionsTab.ts index 88b176f0a..ec1580552 100644 --- a/src/GameOptions/GameOptionsTab.ts +++ b/src/GameOptions/GameOptionsTab.ts @@ -3,4 +3,5 @@ export enum GameOptionsTab { INTERFACE, GAMEPLAY, MISC, + REMOTE_API, } diff --git a/src/GameOptions/ui/ConnectionBauble.tsx b/src/GameOptions/ui/ConnectionBauble.tsx index 75965764c..a2a62f200 100644 --- a/src/GameOptions/ui/ConnectionBauble.tsx +++ b/src/GameOptions/ui/ConnectionBauble.tsx @@ -1,5 +1,7 @@ import { Typography } from "@mui/material"; import React, { useState, useEffect } from "react"; +import WifiIcon from "@mui/icons-material/Wifi"; +import WifiOffIcon from "@mui/icons-material/WifiOff"; interface baubleProps { isConnected: () => boolean; @@ -15,5 +17,24 @@ export const ConnectionBauble = (props: baubleProps): React.ReactElement => { return () => clearInterval(timer); }); - return {connection ? "Connected" : "Disconnected"}; + return ( + <> + + Status:  + + {connection ? ( + <> + Online  + + + ) : ( + <> + Offline  + + + )} + + + + ); }; diff --git a/src/GameOptions/ui/CurrentOptionsPage.tsx b/src/GameOptions/ui/CurrentOptionsPage.tsx index b9eaf3029..171eaca17 100644 --- a/src/GameOptions/ui/CurrentOptionsPage.tsx +++ b/src/GameOptions/ui/CurrentOptionsPage.tsx @@ -1,4 +1,4 @@ -import { MenuItem, Select, SelectChangeEvent, TextField, Tooltip, Typography, Box } from "@mui/material"; +import { MenuItem, Select, SelectChangeEvent, TextField, Tooltip, Typography, Link } from "@mui/material"; import React, { useState } from "react"; import { IPlayer } from "../../PersonObjects/IPlayer"; import { isRemoteFileApiConnectionLive, newRemoteFileApiConnection } from "../../RemoteFileAPI/RemoteFileAPI"; @@ -370,12 +370,25 @@ export const CurrentOptionsPage = (props: IProps): React.ReactElement => { } /> + + ), + [GameOptionsTab.REMOTE_API]: ( + + + These settings control the Remote API for bitburner. This is typically used to write scripts using an external + text editor and then upload files to the home server. + + + + Documentation + + + - This port number is used to connect to a Remote File API port, please ensure that it matches with the port - the Remote File API server is publishing on (12525 by default). Click the reconnect button to try and - re-establish connection. The little colored bauble shows whether the connection is live or not. + This port number is used to connect to a Remote API port, please ensure that it matches with your Remote + API server port. Set to 0 to disable the feature. } > @@ -383,15 +396,10 @@ export const CurrentOptionsPage = (props: IProps): React.ReactElement => { InputProps={{ startAdornment: ( 0 && remoteFileApiPort <= 65535 ? "success" : "error"}> - Remote File API port: + Port:  ), - endAdornment: ( - - - - - ), + endAdornment: , }} value={remoteFileApiPort} onChange={handleRemoteFileApiPortChange} diff --git a/src/GameOptions/ui/GameOptionsSidebar.tsx b/src/GameOptions/ui/GameOptionsSidebar.tsx index 16ab2780f..cd71cc604 100644 --- a/src/GameOptions/ui/GameOptionsSidebar.tsx +++ b/src/GameOptions/ui/GameOptionsSidebar.tsx @@ -107,6 +107,7 @@ export const GameOptionsSidebar = (props: IProps): React.ReactElement => { + response interaction, undefined with notifications, defined with request/response - constructor(obj: { method?: string; result?: string; params?: FileMetadata; error?: string; id?: number } = {}) { + constructor( + obj: { method?: string; result?: string | number; params?: FileMetadata; error?: string; id?: number } = {}, + ) { this.method = obj.method; this.result = obj.result; this.params = obj.params; diff --git a/src/RemoteFileAPI/MessageHandlers.ts b/src/RemoteFileAPI/MessageHandlers.ts index 82297dbc1..068f7b1cb 100644 --- a/src/RemoteFileAPI/MessageHandlers.ts +++ b/src/RemoteFileAPI/MessageHandlers.ts @@ -12,12 +12,10 @@ import { FileLocation, isFileData, } from "./MessageDefinitions"; -//@ts-ignore: Complaint of import ending with .d.ts + import libSource from "!!raw-loader!../ScriptEditor/NetscriptDefinitions.d.ts"; -import { RFALogger } from "./RFALogger"; function error(errorMsg: string, { id }: RFAMessage): RFAMessage { - RFALogger.error((typeof id === "undefined" ? "" : `Request ${id}: `) + errorMsg); return new RFAMessage({ error: errorMsg, id: id }); } @@ -129,12 +127,11 @@ export const RFARequestHandler: Record void | R if (!script) return error("File doesn't exist", msg); const ramUsage = script.ramUsage; - return new RFAMessage({ result: String(ramUsage), id: msg.id }); + return new RFAMessage({ result: ramUsage, id: msg.id }); }, getDefinitionFile: function (msg: RFAMessage): RFAMessage { const source = (libSource + "").replace(/export /g, ""); - console.log(source); return new RFAMessage({ result: source, id: msg.id }); }, }; diff --git a/src/RemoteFileAPI/RFALogger.ts b/src/RemoteFileAPI/RFALogger.ts deleted file mode 100644 index 872d1dc26..000000000 --- a/src/RemoteFileAPI/RFALogger.ts +++ /dev/null @@ -1,27 +0,0 @@ -class RemoteFileAPILogger { - _enabled = true; - _prefix = "[RFA]"; - _error_prefix = "[RFA-ERROR]"; - - constructor(enabled: boolean) { - this._enabled = enabled; - } - - public error(...message: any[]): void { - if (this._enabled) console.error(this._error_prefix, ...message); - } - - public log(...message: any[]): void { - if (this._enabled) console.log(this._prefix, ...message); - } - - public disable(): void { - this._enabled = false; - } - - public enable(): void { - this._enabled = true; - } -} - -export const RFALogger = new RemoteFileAPILogger(true); diff --git a/src/RemoteFileAPI/Remote.ts b/src/RemoteFileAPI/Remote.ts index d864db8df..ed83fbc21 100644 --- a/src/RemoteFileAPI/Remote.ts +++ b/src/RemoteFileAPI/Remote.ts @@ -1,10 +1,10 @@ -import { RFALogger } from "./RFALogger"; import { RFAMessage } from "./MessageDefinitions"; import { RFARequestHandler } from "./MessageHandlers"; +import { SnackbarEvents, ToastVariant } from "../ui/React/Snackbar"; export class Remote { connection?: WebSocket; - protocol = "ws"; + static protocol = "ws"; ipaddr: string; port: number; @@ -18,32 +18,35 @@ export class Remote { } public startConnection(): void { - RFALogger.log("Trying to connect."); - this.connection = new WebSocket(this.protocol + "://" + this.ipaddr + ":" + this.port); + const address = Remote.protocol + "://" + this.ipaddr + ":" + this.port; + this.connection = new WebSocket(address); - this.connection.addEventListener("error", (e: Event) => RFALogger.error(e)); + this.connection.addEventListener("error", (e: Event) => + SnackbarEvents.emit(`Error with websocket ${address}, details: ${JSON.stringify(e)}`, ToastVariant.ERROR, 5000), + ); this.connection.addEventListener("message", handleMessageEvent); this.connection.addEventListener("open", () => - RFALogger.log("Connection established: ", this.ipaddr, ":", this.port), + SnackbarEvents.emit( + `Remote API connection established on ${this.ipaddr}:${this.port}`, + ToastVariant.SUCCESS, + 2000, + ), + ); + this.connection.addEventListener("close", () => + SnackbarEvents.emit("Remote API connection closed", ToastVariant.WARNING, 2000), ); - this.connection.addEventListener("close", () => RFALogger.log("Connection closed")); } } function handleMessageEvent(this: WebSocket, e: MessageEvent): void { const msg: RFAMessage = JSON.parse(e.data); - RFALogger.log("Message received:", msg); - if (msg.method) { - if (!RFARequestHandler[msg.method]) { - const response = new RFAMessage({ error: "Unknown message received", id: msg.id }); - this.send(JSON.stringify(response)); - return; - } - const response = RFARequestHandler[msg.method](msg); - RFALogger.log("Sending response: ", response); - if (response) this.send(JSON.stringify(response)); - } else if (msg.result) RFALogger.log("Somehow retrieved a result message."); - else if (msg.error) RFALogger.error("Received an error from server", msg); - else RFALogger.error("Incorrect Message", msg); + if (!msg.method || !RFARequestHandler[msg.method]) { + const response = new RFAMessage({ error: "Unknown message received", id: msg.id }); + this.send(JSON.stringify(response)); + return; + } + const response = RFARequestHandler[msg.method](msg); + if (!response) return; + this.send(JSON.stringify(response)); } diff --git a/src/RemoteFileAPI/RemoteFileAPI.ts b/src/RemoteFileAPI/RemoteFileAPI.ts index 1dccab591..c55ed6bfd 100644 --- a/src/RemoteFileAPI/RemoteFileAPI.ts +++ b/src/RemoteFileAPI/RemoteFileAPI.ts @@ -4,14 +4,9 @@ import { Remote } from "./Remote"; let server: Remote; export function newRemoteFileApiConnection(): void { - if (server == undefined) { - server = new Remote("localhost", Settings.RemoteFileApiPort); - server.startConnection(); - } else { - server.stopConnection(); - server = new Remote("localhost", Settings.RemoteFileApiPort); - server.startConnection(); - } + if (server) server.stopConnection(); + server = new Remote("localhost", Settings.RemoteFileApiPort); + server.startConnection(); } export function isRemoteFileApiConnectionLive(): boolean { diff --git a/src/Settings/Settings.ts b/src/Settings/Settings.ts index 3160dfa3c..29f7eef70 100644 --- a/src/Settings/Settings.ts +++ b/src/Settings/Settings.ts @@ -211,7 +211,7 @@ export const defaultSettings: IDefaultSettings = { MaxLogCapacity: 50, MaxPortCapacity: 50, MaxTerminalCapacity: 500, - RemoteFileApiPort: 12525, + RemoteFileApiPort: 0, SaveGameOnFileSave: true, SuppressBuyAugmentationConfirmation: false, SuppressFactionInvites: false, diff --git a/src/index.tsx b/src/index.tsx index 83b2692f6..d9e96cbc4 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -17,7 +17,7 @@ ReactDOM.render( document.getElementById("root"), ); -newRemoteFileApiConnection(); +setTimeout(newRemoteFileApiConnection, 2000); function rerender(): void { refreshTheme();