mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2024-11-10 09:43:54 +01:00
Merge pull request #3972 from Hoekstraa/rfa
RFA: New Remote File API addition for transmitting files to the game
This commit is contained in:
commit
895944aa19
18
src/GameOptions/ui/ConnectionBauble.tsx
Normal file
18
src/GameOptions/ui/ConnectionBauble.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
interface baubleProps {
|
||||
callback: () => boolean;
|
||||
}
|
||||
|
||||
export const ConnectionBauble = (props: baubleProps): React.ReactElement => {
|
||||
const [connection, setConnection] = useState(props.callback());
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setConnection(props.callback());
|
||||
}, 1000);
|
||||
return () => clearInterval(timer);
|
||||
});
|
||||
|
||||
return <div className="ConnectionBauble">{connection ? "Connected" : "Disconnected"}</div>;
|
||||
};
|
@ -1,12 +1,15 @@
|
||||
import { MenuItem, Select, SelectChangeEvent, TextField, Tooltip, Typography } from "@mui/material";
|
||||
import { MenuItem, Select, SelectChangeEvent, TextField, Tooltip, Typography, Box } from "@mui/material";
|
||||
import React, { useState } from "react";
|
||||
import { IPlayer } from "../../PersonObjects/IPlayer";
|
||||
import { isRemoteFileApiConnectionLive, newRemoteFileApiConnection } from "../../RemoteFileAPI/RemoteFileAPI";
|
||||
import { Settings } from "../../Settings/Settings";
|
||||
import { OptionSwitch } from "../../ui/React/OptionSwitch";
|
||||
import { formatTime } from "../../utils/helpers/formatTime";
|
||||
import { GameOptionsTab } from "../GameOptionsTab";
|
||||
import { GameOptionsPage } from "./GameOptionsPage";
|
||||
import { OptionsSlider } from "./OptionsSlider";
|
||||
import Button from "@mui/material/Button";
|
||||
import { ConnectionBauble } from "./ConnectionBauble";
|
||||
|
||||
interface IProps {
|
||||
currentTab: GameOptionsTab;
|
||||
@ -21,6 +24,7 @@ export const CurrentOptionsPage = (props: IProps): React.ReactElement => {
|
||||
const [terminalSize, setTerminalSize] = useState(Settings.MaxTerminalCapacity);
|
||||
const [autosaveInterval, setAutosaveInterval] = useState(Settings.AutosaveInterval);
|
||||
const [timestampFormat, setTimestampFormat] = useState(Settings.TimestampsFormat);
|
||||
const [remoteFileApiPort, setRemoteFileApiPort] = useState(Settings.RemoteFileApiPort);
|
||||
const [locale, setLocale] = useState(Settings.Locale);
|
||||
|
||||
function handleExecTimeChange(
|
||||
@ -81,6 +85,11 @@ export const CurrentOptionsPage = (props: IProps): React.ReactElement => {
|
||||
Settings.TimestampsFormat = event.target.value;
|
||||
}
|
||||
|
||||
function handleRemoteFileApiPortChange(event: React.ChangeEvent<HTMLInputElement>): void {
|
||||
setRemoteFileApiPort(Number(event.target.value) as number);
|
||||
Settings.RemoteFileApiPort = Number(event.target.value);
|
||||
}
|
||||
|
||||
const pages = {
|
||||
[GameOptionsTab.SYSTEM]: (
|
||||
<GameOptionsPage title="System">
|
||||
@ -361,6 +370,34 @@ export const CurrentOptionsPage = (props: IProps): React.ReactElement => {
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Tooltip
|
||||
title={
|
||||
<Typography>
|
||||
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.
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<TextField
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<Typography color={remoteFileApiPort > 0 && remoteFileApiPort <= 65535 ? "success" : "error"}>
|
||||
Remote File API port:
|
||||
</Typography>
|
||||
),
|
||||
endAdornment: (
|
||||
<Box>
|
||||
<Button onClick={newRemoteFileApiConnection}>Reconnect</Button>
|
||||
<ConnectionBauble callback={isRemoteFileApiConnectionLive} />
|
||||
</Box>
|
||||
),
|
||||
}}
|
||||
value={remoteFileApiPort}
|
||||
onChange={handleRemoteFileApiPortChange}
|
||||
placeholder="12525"
|
||||
/>
|
||||
</Tooltip>
|
||||
</GameOptionsPage>
|
||||
),
|
||||
};
|
||||
|
58
src/RemoteFileAPI/MessageDefinitions.ts
Normal file
58
src/RemoteFileAPI/MessageDefinitions.ts
Normal file
@ -0,0 +1,58 @@
|
||||
export class RFAMessage {
|
||||
jsonrpc = "2.0"; // Transmits version of JSON-RPC. Compliance maybe allows some funky interaction with external tools?
|
||||
public method?: string; // Is defined when it's a request/notification, otherwise undefined
|
||||
public result?: string; // Is defined when it's a response, otherwise undefined
|
||||
public params?: FileMetadata; // Optional parameters to method
|
||||
public error?: string; // Only defined on error
|
||||
public id?: number; // ID to keep track of request -> response interaction, undefined with notifications, defined with request/response
|
||||
|
||||
constructor(obj: { method?: string; result?: string; params?: FileMetadata; error?: string; id?: number } = {}) {
|
||||
this.method = obj.method;
|
||||
this.result = obj.result;
|
||||
this.params = obj.params;
|
||||
this.error = obj.error;
|
||||
this.id = obj.id;
|
||||
}
|
||||
}
|
||||
|
||||
type FileMetadata = FileData | FileContent | FileLocation | FileServer;
|
||||
|
||||
export interface FileData {
|
||||
filename: string;
|
||||
content: string;
|
||||
server: string;
|
||||
}
|
||||
|
||||
export interface FileContent {
|
||||
filename: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface FileLocation {
|
||||
filename: string;
|
||||
server: string;
|
||||
}
|
||||
|
||||
export interface FileServer {
|
||||
server: string;
|
||||
}
|
||||
|
||||
export function isFileData(p: unknown): p is FileData {
|
||||
const pf = p as FileData;
|
||||
return typeof pf.server === "string" && typeof pf.filename === "string" && typeof pf.content === "string";
|
||||
}
|
||||
|
||||
export function isFileLocation(p: unknown): p is FileLocation {
|
||||
const pf = p as FileLocation;
|
||||
return typeof pf.server === "string" && typeof pf.filename === "string";
|
||||
}
|
||||
|
||||
export function isFileContent(p: unknown): p is FileContent {
|
||||
const pf = p as FileContent;
|
||||
return typeof pf.filename === "string" && typeof pf.content === "string";
|
||||
}
|
||||
|
||||
export function isFileServer(p: unknown): p is FileServer {
|
||||
const pf = p as FileServer;
|
||||
return typeof pf.server === "string";
|
||||
}
|
140
src/RemoteFileAPI/MessageHandlers.ts
Normal file
140
src/RemoteFileAPI/MessageHandlers.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { Player } from "../Player";
|
||||
import { isScriptFilename } from "../Script/isScriptFilename";
|
||||
import { GetServer } from "../Server/AllServers";
|
||||
import { isValidFilePath } from "../Terminal/DirectoryHelpers";
|
||||
import { TextFile } from "../TextFile";
|
||||
import {
|
||||
RFAMessage,
|
||||
FileData,
|
||||
FileContent,
|
||||
isFileServer,
|
||||
isFileLocation,
|
||||
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 });
|
||||
}
|
||||
|
||||
export const RFARequestHandler: Record<string, (message: RFAMessage) => void | RFAMessage> = {
|
||||
pushFile: function (msg: RFAMessage): RFAMessage {
|
||||
if (!isFileData(msg.params)) return error("Misses parameters", msg);
|
||||
|
||||
const fileData: FileData = msg.params;
|
||||
if (!isValidFilePath(fileData.filename)) return error("Invalid filename", msg);
|
||||
|
||||
const server = GetServer(fileData.server);
|
||||
if (!server) return error("Server hostname invalid", msg);
|
||||
|
||||
if (isScriptFilename(fileData.filename)) server.writeToScriptFile(Player, fileData.filename, fileData.content);
|
||||
// Assume it's a text file
|
||||
else server.writeToTextFile(fileData.filename, fileData.content);
|
||||
|
||||
// If and only if the content is actually changed correctly, send back an OK.
|
||||
const savedCorrectly =
|
||||
server.getScript(fileData.filename)?.code === fileData.content ||
|
||||
server.textFiles.filter((t: TextFile) => t.filename == fileData.filename).at(0)?.text === fileData.content;
|
||||
|
||||
if (!savedCorrectly) return error("File wasn't saved correctly", msg);
|
||||
|
||||
return new RFAMessage({ result: "OK", id: msg.id });
|
||||
},
|
||||
|
||||
getFile: function (msg: RFAMessage): RFAMessage {
|
||||
if (!isFileLocation(msg.params)) return error("Message misses parameters", msg);
|
||||
|
||||
const fileData: FileLocation = msg.params;
|
||||
if (!isValidFilePath(fileData.filename)) return error("Invalid filename", msg);
|
||||
|
||||
const server = GetServer(fileData.server);
|
||||
if (!server) return error("Server hostname invalid", msg);
|
||||
|
||||
if (isScriptFilename(fileData.filename)) {
|
||||
const scriptContent = server.getScript(fileData.filename);
|
||||
if (!scriptContent) return error("File doesn't exist", msg);
|
||||
return new RFAMessage({ result: scriptContent.code, id: msg.id });
|
||||
} else {
|
||||
// Assume it's a text file
|
||||
const file = server.textFiles.filter((t: TextFile) => t.filename == fileData.filename).at(0);
|
||||
if (!file) return error("File doesn't exist", msg);
|
||||
return new RFAMessage({ result: file.text, id: msg.id });
|
||||
}
|
||||
},
|
||||
|
||||
deleteFile: function (msg: RFAMessage): RFAMessage {
|
||||
if (!isFileLocation(msg.params)) return error("Message misses parameters", msg);
|
||||
const fileData: FileLocation = msg.params;
|
||||
if (!isValidFilePath(fileData.filename)) return error("Invalid filename", msg);
|
||||
|
||||
const server = GetServer(fileData.server);
|
||||
if (!server) return error("Server hostname invalid", msg);
|
||||
|
||||
const fileExists = (): boolean =>
|
||||
!!server.getScript(fileData.filename) || server.textFiles.some((t: TextFile) => t.filename === fileData.filename);
|
||||
|
||||
if (!fileExists()) return error("File doesn't exist", msg);
|
||||
server.removeFile(fileData.filename);
|
||||
if (fileExists()) return error("Failed to delete the file", msg);
|
||||
|
||||
return new RFAMessage({ result: "OK", id: msg.id });
|
||||
},
|
||||
|
||||
getFileNames: function (msg: RFAMessage): RFAMessage {
|
||||
if (!isFileServer(msg.params)) return error("Message misses parameters", msg);
|
||||
|
||||
const server = GetServer(msg.params.server);
|
||||
if (!server) return error("Server hostname invalid", msg);
|
||||
|
||||
const fileNameList: string[] = [
|
||||
...server.textFiles.map((txt): string => txt.filename),
|
||||
...server.scripts.map((scr): string => scr.filename),
|
||||
];
|
||||
|
||||
return new RFAMessage({ result: JSON.stringify(fileNameList), id: msg.id });
|
||||
},
|
||||
|
||||
getAllFiles: function (msg: RFAMessage): RFAMessage {
|
||||
if (!isFileServer(msg.params)) return error("Message misses parameters", msg);
|
||||
|
||||
const server = GetServer(msg.params.server);
|
||||
if (!server) return error("Server hostname invalid", msg);
|
||||
|
||||
const fileList: FileContent[] = [
|
||||
...server.textFiles.map((txt): FileContent => {
|
||||
return { filename: txt.filename, content: txt.text };
|
||||
}),
|
||||
...server.scripts.map((scr): FileContent => {
|
||||
return { filename: scr.filename, content: scr.code };
|
||||
}),
|
||||
];
|
||||
|
||||
return new RFAMessage({ result: JSON.stringify(fileList), id: msg.id });
|
||||
},
|
||||
|
||||
calculateRam: function (msg: RFAMessage): RFAMessage {
|
||||
if (!isFileLocation(msg.params)) return error("Message misses parameters", msg);
|
||||
const fileData: FileLocation = msg.params;
|
||||
if (!isValidFilePath(fileData.filename)) return error("Invalid filename", msg);
|
||||
|
||||
const server = GetServer(fileData.server);
|
||||
if (!server) return error("Server hostname invalid", msg);
|
||||
|
||||
if (!isScriptFilename(fileData.filename)) return error("Filename isn't a script filename", msg);
|
||||
const script = server.getScript(fileData.filename);
|
||||
if (!script) return error("File doesn't exist", msg);
|
||||
const ramUsage = script.ramUsage;
|
||||
|
||||
return new RFAMessage({ result: String(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 });
|
||||
},
|
||||
};
|
27
src/RemoteFileAPI/RFALogger.ts
Normal file
27
src/RemoteFileAPI/RFALogger.ts
Normal file
@ -0,0 +1,27 @@
|
||||
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);
|
49
src/RemoteFileAPI/Remote.ts
Normal file
49
src/RemoteFileAPI/Remote.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { RFALogger } from "./RFALogger";
|
||||
import { RFAMessage } from "./MessageDefinitions";
|
||||
import { RFARequestHandler } from "./MessageHandlers";
|
||||
|
||||
export class Remote {
|
||||
connection?: WebSocket;
|
||||
protocol = "ws";
|
||||
ipaddr: string;
|
||||
port: number;
|
||||
|
||||
constructor(ip: string, port: number) {
|
||||
this.ipaddr = ip;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public stopConnection(): void {
|
||||
this.connection?.close();
|
||||
}
|
||||
|
||||
public startConnection(): void {
|
||||
RFALogger.log("Trying to connect.");
|
||||
this.connection = new WebSocket(this.protocol + "://" + this.ipaddr + ":" + this.port);
|
||||
|
||||
this.connection.addEventListener("error", (e: Event) => RFALogger.error(e));
|
||||
this.connection.addEventListener("message", handleMessageEvent);
|
||||
this.connection.addEventListener("open", () =>
|
||||
RFALogger.log("Connection established: ", this.ipaddr, ":", this.port),
|
||||
);
|
||||
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);
|
||||
}
|
19
src/RemoteFileAPI/RemoteFileAPI.ts
Normal file
19
src/RemoteFileAPI/RemoteFileAPI.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Settings } from "../Settings/Settings";
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
export function isRemoteFileApiConnectionLive(): boolean {
|
||||
return server.connection != undefined && server.connection.readyState == 1;
|
||||
}
|
@ -84,6 +84,11 @@ interface IDefaultSettings {
|
||||
*/
|
||||
MaxTerminalCapacity: number;
|
||||
|
||||
/**
|
||||
* Port the Remote File API client will try to connect to.
|
||||
*/
|
||||
RemoteFileApiPort: number;
|
||||
|
||||
/**
|
||||
* Save the game when you save any file.
|
||||
*/
|
||||
@ -206,6 +211,7 @@ export const defaultSettings: IDefaultSettings = {
|
||||
MaxLogCapacity: 50,
|
||||
MaxPortCapacity: 50,
|
||||
MaxTerminalCapacity: 500,
|
||||
RemoteFileApiPort: 12525,
|
||||
SaveGameOnFileSave: true,
|
||||
SuppressBuyAugmentationConfirmation: false,
|
||||
SuppressFactionInvites: false,
|
||||
@ -248,6 +254,7 @@ export const Settings: ISettings & ISelfInitializer & ISelfLoading = {
|
||||
MaxTerminalCapacity: defaultSettings.MaxTerminalCapacity,
|
||||
OwnedAugmentationsOrder: OwnedAugmentationsOrderSetting.AcquirementTime,
|
||||
PurchaseAugmentationsOrder: PurchaseAugmentationsOrderSetting.Default,
|
||||
RemoteFileApiPort: defaultSettings.RemoteFileApiPort,
|
||||
SaveGameOnFileSave: defaultSettings.SaveGameOnFileSave,
|
||||
SuppressBuyAugmentationConfirmation: defaultSettings.SuppressBuyAugmentationConfirmation,
|
||||
SuppressFactionInvites: defaultSettings.SuppressFactionInvites,
|
||||
|
@ -4,6 +4,9 @@ import ReactDOM from "react-dom";
|
||||
import { TTheme as Theme, ThemeEvents, refreshTheme } from "./Themes/ui/Theme";
|
||||
import { LoadingScreen } from "./ui/LoadingScreen";
|
||||
import { initElectron } from "./Electron";
|
||||
|
||||
import { newRemoteFileApiConnection } from "./RemoteFileAPI/RemoteFileAPI";
|
||||
|
||||
initElectron();
|
||||
globalThis["React"] = React;
|
||||
globalThis["ReactDOM"] = ReactDOM;
|
||||
@ -14,6 +17,8 @@ ReactDOM.render(
|
||||
document.getElementById("root"),
|
||||
);
|
||||
|
||||
newRemoteFileApiConnection();
|
||||
|
||||
function rerender(): void {
|
||||
refreshTheme();
|
||||
ReactDOM.render(
|
||||
|
Loading…
Reference in New Issue
Block a user