UI: LogBox overhaul (#508)

This commit is contained in:
David Walker 2023-05-26 05:07:37 -07:00 committed by GitHub
parent 113af6e711
commit 4503da6226
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 431 additions and 114 deletions

@ -84,6 +84,7 @@
| [Player](./bitburner.player.md) | | | [Player](./bitburner.player.md) | |
| [ProcessInfo](./bitburner.processinfo.md) | A single process on a server. | | [ProcessInfo](./bitburner.processinfo.md) | A single process on a server. |
| [Product](./bitburner.product.md) | Product in a warehouse | | [Product](./bitburner.product.md) | Product in a warehouse |
| [ReactElement](./bitburner.reactelement.md) | A stand-in for the real React.ReactElement, which API-extractor doesn't know about. Don't try to create one of these by hand; use React.createElement(). |
| [RecentScript](./bitburner.recentscript.md) | | | [RecentScript](./bitburner.recentscript.md) | |
| [ReputationFormulas](./bitburner.reputationformulas.md) | Reputation formulas | | [ReputationFormulas](./bitburner.reputationformulas.md) | Reputation formulas |
| [ResetInfo](./bitburner.resetinfo.md) | Various info about resets | | [ResetInfo](./bitburner.resetinfo.md) | Various info about resets |
@ -99,6 +100,7 @@
| [Stanek](./bitburner.stanek.md) | Stanek's Gift API. | | [Stanek](./bitburner.stanek.md) | Stanek's Gift API. |
| [StockOrder](./bitburner.stockorder.md) | <p>Return value of [getOrders](./bitburner.tix.getorders.md)</p><p>Keys are stock symbols, properties are arrays of [StockOrderObject](./bitburner.stockorderobject.md)</p> | | [StockOrder](./bitburner.stockorder.md) | <p>Return value of [getOrders](./bitburner.tix.getorders.md)</p><p>Keys are stock symbols, properties are arrays of [StockOrderObject](./bitburner.stockorderobject.md)</p> |
| [StockOrderObject](./bitburner.stockorderobject.md) | Value in map of [StockOrder](./bitburner.stockorder.md) | | [StockOrderObject](./bitburner.stockorderobject.md) | Value in map of [StockOrder](./bitburner.stockorder.md) |
| [TailProperties](./bitburner.tailproperties.md) | |
| [TIX](./bitburner.tix.md) | Stock market API | | [TIX](./bitburner.tix.md) | Stock market API |
| [UserInterface](./bitburner.userinterface.md) | User Interface API. | | [UserInterface](./bitburner.userinterface.md) | User Interface API. |
| [UserInterfaceTheme](./bitburner.userinterfacetheme.md) | Interface Theme | | [UserInterfaceTheme](./bitburner.userinterfacetheme.md) | Interface Theme |

@ -21,7 +21,7 @@ exec(
| Parameter | Type | Description | | Parameter | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| script | string | Filename of script to execute. | | script | string | Filename of script to execute. This file must already exist on the target server. |
| hostname | string | Hostname of the <code>target server</code> on which to execute the script. | | hostname | string | Hostname of the <code>target server</code> on which to execute the script. |
| threadOrOptions | number \| [RunOptions](./bitburner.runoptions.md) | _(Optional)_ Either an integer number of threads for new script, or a [RunOptions](./bitburner.runoptions.md) object. Threads defaults to 1. | | threadOrOptions | number \| [RunOptions](./bitburner.runoptions.md) | _(Optional)_ Either an integer number of threads for new script, or a [RunOptions](./bitburner.runoptions.md) object. Threads defaults to 1. |
| args | (string \| number \| boolean)\[\] | Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the third argument threadOrOptions must be filled in with a value. | | args | (string \| number \| boolean)\[\] | Additional arguments to pass into the new script that is being run. Note that if any arguments are being passed into the new script, then the third argument threadOrOptions must be filled in with a value. |
@ -36,7 +36,7 @@ Returns the PID of a successfully started script, and 0 otherwise.
RAM cost: 1.3 GB RAM cost: 1.3 GB
Run a script as a separate process on a specified server. This is similar to the function [run](./bitburner.ns.run.md) except that it can be used to run a script on any server, instead of just the current server. Run a script as a separate process on a specified server. This is similar to the function [run](./bitburner.ns.run.md) except that it can be used to run a script that already exists on any server, instead of just the current server.
If the script was successfully started, then this function returns the PID of that script. Otherwise, it returns 0. If the script was successfully started, then this function returns the PID of that script. Otherwise, it returns 0.

@ -152,6 +152,7 @@ export async function main(ns) {
| [scriptKill(script, host)](./bitburner.ns.scriptkill.md) | Kill all scripts with a filename. | | [scriptKill(script, host)](./bitburner.ns.scriptkill.md) | Kill all scripts with a filename. |
| [scriptRunning(script, host)](./bitburner.ns.scriptrunning.md) | Check if any script with a filename is running. | | [scriptRunning(script, host)](./bitburner.ns.scriptrunning.md) | Check if any script with a filename is running. |
| [serverExists(host)](./bitburner.ns.serverexists.md) | Returns a boolean denoting whether or not the specified server exists. | | [serverExists(host)](./bitburner.ns.serverexists.md) | Returns a boolean denoting whether or not the specified server exists. |
| [setTitle(title, pid)](./bitburner.ns.settitle.md) | Set the title of the tail window of a script. |
| [share()](./bitburner.ns.share.md) | Share the server's ram with your factions. | | [share()](./bitburner.ns.share.md) | Share the server's ram with your factions. |
| [sleep(millis)](./bitburner.ns.sleep.md) | Suspends the script for n milliseconds. | | [sleep(millis)](./bitburner.ns.sleep.md) | Suspends the script for n milliseconds. |
| [spawn(script, threadOrOptions, args)](./bitburner.ns.spawn.md) | Terminate current script and start another in 10 seconds. | | [spawn(script, threadOrOptions, args)](./bitburner.ns.spawn.md) | Terminate current script and start another in 10 seconds. |

@ -0,0 +1,39 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [NS](./bitburner.ns.md) &gt; [setTitle](./bitburner.ns.settitle.md)
## NS.setTitle() method
Set the title of the tail window of a script.
**Signature:**
```typescript
setTitle(title: string | ReactElement, pid?: number): void;
```
## Parameters
| Parameter | Type | Description |
| --- | --- | --- |
| title | string \| [ReactElement](./bitburner.reactelement.md) | |
| pid | number | _(Optional)_ Optional. PID of the script having its tail closed. If omitted, the current script is used. |
**Returns:**
void
## Remarks
RAM cost: 0 GB
This sets the title to the given string, and also forces an update of the tail window's contents.
The title is saved across restarts, but only if it is a simple string.
If the pid is unspecified, it will modify the current scripts logs.
Otherwise, the pid argument can be used to change the logs from another script.
It is possible to pass a React Element instead of a string. Get these by calling React.createElement() with appropriate parameters. You should either know or be willing to learn about the React UI library if you go down this route, and there will likely be rough edges.

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [ReactElement](./bitburner.reactelement.md) &gt; [key](./bitburner.reactelement.key.md)
## ReactElement.key property
**Signature:**
```typescript
key: string | number | null;
```

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [ReactElement](./bitburner.reactelement.md)
## ReactElement interface
A stand-in for the real React.ReactElement, which API-extractor doesn't know about. Don't try to create one of these by hand; use React.createElement().
**Signature:**
```typescript
interface ReactElement
```
## Properties
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [key](./bitburner.reactelement.key.md) | | string \| number \| null | |
| [props](./bitburner.reactelement.props.md) | | any | |
| [type](./bitburner.reactelement.type.md) | | string \| ((props: any) =&gt; [ReactElement](./bitburner.reactelement.md) \| null) \| (new (props: any) =&gt; object) | |

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [ReactElement](./bitburner.reactelement.md) &gt; [props](./bitburner.reactelement.props.md)
## ReactElement.props property
**Signature:**
```typescript
props: any;
```

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [ReactElement](./bitburner.reactelement.md) &gt; [type](./bitburner.reactelement.type.md)
## ReactElement.type property
**Signature:**
```typescript
type: string | ((props: any) => ReactElement | null) | (new (props: any) => object);
```

@ -27,6 +27,8 @@ interface RunningScript
| [pid](./bitburner.runningscript.pid.md) | | number | Process ID. Must be an integer | | [pid](./bitburner.runningscript.pid.md) | | number | Process ID. Must be an integer |
| [ramUsage](./bitburner.runningscript.ramusage.md) | | number | How much RAM this script uses for ONE thread | | [ramUsage](./bitburner.runningscript.ramusage.md) | | number | How much RAM this script uses for ONE thread |
| [server](./bitburner.runningscript.server.md) | | string | Hostname of the server on which this script runs | | [server](./bitburner.runningscript.server.md) | | string | Hostname of the server on which this script runs |
| [tailProperties](./bitburner.runningscript.tailproperties.md) | | [TailProperties](./bitburner.tailproperties.md) \| null | Properties of the tail window, or null if it is not shown |
| [temporary](./bitburner.runningscript.temporary.md) | | boolean | Whether this RunningScript is excluded from saves | | [temporary](./bitburner.runningscript.temporary.md) | | boolean | Whether this RunningScript is excluded from saves |
| [threads](./bitburner.runningscript.threads.md) | | number | Number of threads that this script runs with | | [threads](./bitburner.runningscript.threads.md) | | number | Number of threads that this script runs with |
| [title](./bitburner.runningscript.title.md) | | string \| [ReactElement](./bitburner.reactelement.md) | The title, as shown in the script's log box. Defaults to the name + args, but can be changed by the user. If it is set to a React element (only by the user), that will not be persisted, and will be restored to default on load. |

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [RunningScript](./bitburner.runningscript.md) &gt; [tailProperties](./bitburner.runningscript.tailproperties.md)
## RunningScript.tailProperties property
Properties of the tail window, or null if it is not shown
**Signature:**
```typescript
tailProperties: TailProperties | null;
```

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [RunningScript](./bitburner.runningscript.md) &gt; [title](./bitburner.runningscript.title.md)
## RunningScript.title property
The title, as shown in the script's log box. Defaults to the name + args, but can be changed by the user. If it is set to a React element (only by the user), that will not be persisted, and will be restored to default on load.
**Signature:**
```typescript
title: string | ReactElement;
```

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [TailProperties](./bitburner.tailproperties.md) &gt; [height](./bitburner.tailproperties.height.md)
## TailProperties.height property
Height of the log window content area
**Signature:**
```typescript
height: number;
```

@ -0,0 +1,22 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [TailProperties](./bitburner.tailproperties.md)
## TailProperties interface
**Signature:**
```typescript
interface TailProperties
```
## Properties
| Property | Modifiers | Type | Description |
| --- | --- | --- | --- |
| [height](./bitburner.tailproperties.height.md) | | number | Height of the log window content area |
| [width](./bitburner.tailproperties.width.md) | | number | Width of the log window content area |
| [x](./bitburner.tailproperties.x.md) | | number | X-coordinate of the log window |
| [y](./bitburner.tailproperties.y.md) | | number | Y-coordinate of the log window |

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [TailProperties](./bitburner.tailproperties.md) &gt; [width](./bitburner.tailproperties.width.md)
## TailProperties.width property
Width of the log window content area
**Signature:**
```typescript
width: number;
```

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [TailProperties](./bitburner.tailproperties.md) &gt; [x](./bitburner.tailproperties.x.md)
## TailProperties.x property
X-coordinate of the log window
**Signature:**
```typescript
x: number;
```

@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [bitburner](./bitburner.md) &gt; [TailProperties](./bitburner.tailproperties.md) &gt; [y](./bitburner.tailproperties.y.md)
## TailProperties.y property
Y-coordinate of the log window
**Signature:**
```typescript
y: number;
```

@ -236,6 +236,7 @@ v2.3.1 dev
GENERAL / MISC: GENERAL / MISC:
* Tail window overhaul, ability to set tail title with ns.setTitle, other tail bugfixes and improvements. (@d0sboots)
* Nerf noodle bar * Nerf noodle bar
`, `,
}; };

@ -1,3 +1,4 @@
import React from "react";
import { NetscriptContext } from "./APIWrapper"; import { NetscriptContext } from "./APIWrapper";
import { WorkerScript } from "./WorkerScript"; import { WorkerScript } from "./WorkerScript";
import { killWorkerScript } from "./killWorkerScript"; import { killWorkerScript } from "./killWorkerScript";
@ -38,6 +39,7 @@ import { isPositiveInteger, PositiveInteger, Unknownify } from "../types";
import { Engine } from "../engine"; import { Engine } from "../engine";
import { resolveFilePath, FilePath } from "../Paths/FilePath"; import { resolveFilePath, FilePath } from "../Paths/FilePath";
import { hasScriptExtension, ScriptFilePath } from "../Paths/ScriptFilePath"; import { hasScriptExtension, ScriptFilePath } from "../Paths/ScriptFilePath";
import { CustomBoundary } from "../ui/Components/CustomBoundary";
export const helpers = { export const helpers = {
string, string,
@ -736,6 +738,7 @@ function getCannotFindRunningScriptErrorMessage(ident: ScriptIdentifier): string
* @returns A sanitized, NS-facing copy of the RunningScript * @returns A sanitized, NS-facing copy of the RunningScript
*/ */
function createPublicRunningScript(runningScript: RunningScript): IRunningScript { function createPublicRunningScript(runningScript: RunningScript): IRunningScript {
const logProps = runningScript.tailProps;
return { return {
args: runningScript.args.slice(), args: runningScript.args.slice(),
filename: runningScript.filename, filename: runningScript.filename,
@ -749,6 +752,16 @@ function createPublicRunningScript(runningScript: RunningScript): IRunningScript
pid: runningScript.pid, pid: runningScript.pid,
ramUsage: runningScript.ramUsage, ramUsage: runningScript.ramUsage,
server: runningScript.server, server: runningScript.server,
tailProperties:
!logProps || !logProps.isVisible()
? null
: {
x: logProps.x,
y: logProps.y,
width: logProps.width,
height: logProps.height,
},
title: runningScript.title,
threads: runningScript.threads, threads: runningScript.threads,
temporary: runningScript.temporary, temporary: runningScript.temporary,
}; };
@ -800,3 +813,14 @@ export function handleUnknownError(e: unknown, ws: WorkerScript | ScriptDeath |
} }
dialogBoxCreate(initialText + e); dialogBoxCreate(initialText + e);
} }
// Incrementing value for custom element keys
let customElementKey = 0;
/**
* Wrap a user-provided React Element (or maybe invalid junk) in an Error-catching component,
* so the game won't crash and the user gets sensible messages.
*/
export function wrapUserNode(value: unknown) {
return <CustomBoundary key={`PlayerContent${customElementKey++}`} children={value as React.ReactNode} />;
}

@ -540,6 +540,7 @@ export const RamCosts: RamCostTree<NSFull> = {
moveTail: 0, moveTail: 0,
resizeTail: 0, resizeTail: 0,
closeTail: 0, closeTail: 0,
setTitle: 0,
clearPort: 0, clearPort: 0,
openDevMenu: 0, openDevMenu: 0,
alert: 0, alert: 0,

@ -37,7 +37,7 @@ import { runScriptFromScript } from "./NetscriptWorker";
import { killWorkerScript, killWorkerScriptByPid } from "./Netscript/killWorkerScript"; import { killWorkerScript, killWorkerScriptByPid } from "./Netscript/killWorkerScript";
import { workerScripts } from "./Netscript/WorkerScripts"; import { workerScripts } from "./Netscript/WorkerScripts";
import { WorkerScript } from "./Netscript/WorkerScript"; import { WorkerScript } from "./Netscript/WorkerScript";
import { helpers, assertObjectType } from "./Netscript/NetscriptHelpers"; import { helpers, assertObjectType, wrapUserNode } from "./Netscript/NetscriptHelpers";
import { import {
formatExp, formatExp,
formatNumberNoSuffix, formatNumberNoSuffix,
@ -49,7 +49,7 @@ import {
formatNumber, formatNumber,
} from "./ui/formatNumber"; } from "./ui/formatNumber";
import { convertTimeMsToTimeElapsedString } from "./utils/StringHelperFunctions"; import { convertTimeMsToTimeElapsedString } from "./utils/StringHelperFunctions";
import { LogBoxEvents, LogBoxCloserEvents, LogBoxPositionEvents, LogBoxSizeEvents } from "./ui/React/LogBoxManager"; import { LogBoxEvents, LogBoxCloserEvents } from "./ui/React/LogBoxManager";
import { arrayToString } from "./utils/helpers/ArrayHelpers"; import { arrayToString } from "./utils/helpers/ArrayHelpers";
import { NetscriptGang } from "./NetscriptFunctions/Gang"; import { NetscriptGang } from "./NetscriptFunctions/Gang";
import { NetscriptSleeve } from "./NetscriptFunctions/Sleeve"; import { NetscriptSleeve } from "./NetscriptFunctions/Sleeve";
@ -557,7 +557,12 @@ export const ns: InternalAPI<NSFull> = {
const x = helpers.number(ctx, "x", _x); const x = helpers.number(ctx, "x", _x);
const y = helpers.number(ctx, "y", _y); const y = helpers.number(ctx, "y", _y);
const pid = helpers.number(ctx, "pid", _pid); const pid = helpers.number(ctx, "pid", _pid);
LogBoxPositionEvents.emit({ pid, data: { x, y } }); const runningScriptObj = helpers.getRunningScript(ctx, pid);
if (runningScriptObj == null) {
helpers.log(ctx, () => helpers.getCannotFindRunningScriptErrorMessage(pid));
return;
}
runningScriptObj.tailProps?.setPosition(x, y);
}, },
resizeTail: resizeTail:
(ctx) => (ctx) =>
@ -565,7 +570,12 @@ export const ns: InternalAPI<NSFull> = {
const w = helpers.number(ctx, "w", _w); const w = helpers.number(ctx, "w", _w);
const h = helpers.number(ctx, "h", _h); const h = helpers.number(ctx, "h", _h);
const pid = helpers.number(ctx, "pid", _pid); const pid = helpers.number(ctx, "pid", _pid);
LogBoxSizeEvents.emit({ pid, data: { w, h } }); const runningScriptObj = helpers.getRunningScript(ctx, pid);
if (runningScriptObj == null) {
helpers.log(ctx, () => helpers.getCannotFindRunningScriptErrorMessage(pid));
return;
}
runningScriptObj.tailProps?.setSize(w, h);
}, },
closeTail: closeTail:
(ctx) => (ctx) =>
@ -574,6 +584,18 @@ export const ns: InternalAPI<NSFull> = {
//Emit an event to tell the game to close the tail window if it exists //Emit an event to tell the game to close the tail window if it exists
LogBoxCloserEvents.emit(pid); LogBoxCloserEvents.emit(pid);
}, },
setTitle:
(ctx) =>
(title, _pid = ctx.workerScript.scriptRef.pid) => {
const pid = helpers.number(ctx, "pid", _pid);
const runningScriptObj = helpers.getRunningScript(ctx, pid);
if (runningScriptObj == null) {
helpers.log(ctx, () => helpers.getCannotFindRunningScriptErrorMessage(pid));
return;
}
runningScriptObj.title = typeof title === "string" ? title : wrapUserNode(title);
runningScriptObj.tailProps?.rerender();
},
nuke: (ctx) => (_hostname) => { nuke: (ctx) => (_hostname) => {
const hostname = helpers.string(ctx, "hostname", _hostname); const hostname = helpers.string(ctx, "hostname", _hostname);

@ -1,16 +1,11 @@
import React from "react";
import { Player } from "../Player"; import { Player } from "../Player";
import { Exploit } from "../Exploits/Exploit"; import { Exploit } from "../Exploits/Exploit";
import * as bcrypt from "bcryptjs"; import * as bcrypt from "bcryptjs";
import { Apr1Events as devMenu } from "../ui/Apr1"; import { Apr1Events as devMenu } from "../ui/Apr1";
import { InternalAPI } from "../Netscript/APIWrapper"; import { InternalAPI } from "../Netscript/APIWrapper";
import { helpers } from "../Netscript/NetscriptHelpers"; import { helpers, wrapUserNode } from "../Netscript/NetscriptHelpers";
import { Terminal } from "../Terminal"; import { Terminal } from "../Terminal";
import { RamCostConstants } from "../Netscript/RamCostGenerator"; import { RamCostConstants } from "../Netscript/RamCostGenerator";
import { CustomBoundary } from "../ui/Components/CustomBoundary";
// Incrementing value for custom element keys
let customElementKey = 0;
export interface INetscriptExtra { export interface INetscriptExtra {
heart: { heart: {
@ -72,14 +67,10 @@ export function NetscriptExtra(): InternalAPI<INetscriptExtra> {
return true; return true;
}, },
tprintRaw: () => (value) => { tprintRaw: () => (value) => {
Terminal.printRaw( Terminal.printRaw(wrapUserNode(value));
<CustomBoundary key={`PlayerContent${customElementKey++}`} children={value as React.ReactNode} />,
);
}, },
printRaw: (ctx) => (value) => { printRaw: (ctx) => (value) => {
ctx.workerScript.print( ctx.workerScript.print(wrapUserNode(value));
<CustomBoundary key={`PlayerContent${customElementKey++}`} children={value as React.ReactNode} />,
);
}, },
}; };
} }

@ -17,6 +17,8 @@ import { getKeyList } from "../utils/helpers/getKeyList";
import { ScriptFilePath } from "../Paths/ScriptFilePath"; import { ScriptFilePath } from "../Paths/ScriptFilePath";
import { ScriptKey, scriptKey } from "../utils/helpers/scriptKey"; import { ScriptKey, scriptKey } from "../utils/helpers/scriptKey";
import type { LogBoxProperties } from "../ui/React/LogBoxManager";
export class RunningScript { export class RunningScript {
// Script arguments // Script arguments
args: ScriptArg[] = []; args: ScriptArg[] = [];
@ -65,6 +67,14 @@ export class RunningScript {
// Cached key for ByArgs lookups. Will be overwritten by a correct ScriptKey in fromJSON or constructor // Cached key for ByArgs lookups. Will be overwritten by a correct ScriptKey in fromJSON or constructor
scriptKey = "" as ScriptKey; scriptKey = "" as ScriptKey;
// Access to properties of the tail window. Can be used to get/set size, position, etc.
tailProps = null as LogBoxProperties | null;
// The title, as shown in the script's log box. Defaults to the name + args,
// but can be changed by the user. If it is set to a React element (only by the user),
// that will not be persisted, and will be restored to default on load.
title = "" as string | React.ReactElement;
// Number of threads that this script is running with // Number of threads that this script is running with
threads = 1 as PositiveInteger; threads = 1 as PositiveInteger;
@ -83,6 +93,7 @@ export class RunningScript {
this.server = script.server; this.server = script.server;
this.ramUsage = ramUsage; this.ramUsage = ramUsage;
this.dependencies = script.dependencies; this.dependencies = script.dependencies;
this.title = `${this.filename} ${args.join(" ")}`;
} }
log(txt: React.ReactNode): void { log(txt: React.ReactNode): void {
@ -140,7 +151,12 @@ export class RunningScript {
// Serialize the current object to a JSON save state // Serialize the current object to a JSON save state
toJSON(): IReviverValue { toJSON(): IReviverValue {
return Generic_toJSON("RunningScript", this, includedProperties); // Omit the title if it's a ReactNode, it will be filled in with the default on load.
return Generic_toJSON(
"RunningScript",
this,
typeof this.title === "string" ? includedProperties : includedPropsNoTitle,
);
} }
// Initializes a RunningScript Object from a JSON save state // Initializes a RunningScript Object from a JSON save state
@ -150,6 +166,9 @@ export class RunningScript {
return runningScript; return runningScript;
} }
} }
const includedProperties = getKeyList(RunningScript, { removedKeys: ["logs", "dependencies", "logUpd", "pid"] }); const includedProperties = getKeyList(RunningScript, {
removedKeys: ["logs", "dependencies", "logUpd", "pid", "tailProps"],
});
const includedPropsNoTitle = includedProperties.filter((x) => x !== "title");
constructorsForReviver.RunningScript = RunningScript; constructorsForReviver.RunningScript = RunningScript;

@ -169,6 +169,29 @@ interface Multipliers {
bladeburner_success_chance: number; bladeburner_success_chance: number;
} }
/** @public */
interface TailProperties {
/** X-coordinate of the log window */
x: number;
/** Y-coordinate of the log window */
y: number;
/** Width of the log window content area */
width: number;
/** Height of the log window content area */
height: number;
}
/**
* @public
* A stand-in for the real React.ReactElement, which API-extractor doesn't know about.
* Don't try to create one of these by hand; use React.createElement().
*/
interface ReactElement {
type: string | ((props: any) => ReactElement | null) | (new (props: any) => object);
props: any;
key: string | number | null;
}
/** @public */ /** @public */
interface RunningScript { interface RunningScript {
/** Arguments the script was called with */ /** Arguments the script was called with */
@ -198,6 +221,15 @@ interface RunningScript {
ramUsage: number; ramUsage: number;
/** Hostname of the server on which this script runs */ /** Hostname of the server on which this script runs */
server: string; server: string;
/** Properties of the tail window, or null if it is not shown */
tailProperties: TailProperties | null;
/**
* The title, as shown in the script's log box. Defaults to the name + args,
* but can be changed by the user. If it is set to a React element (only by
* the user), that will not be persisted, and will be restored to default on
* load.
*/
title: string | ReactElement;
/** Number of threads that this script runs with */ /** Number of threads that this script runs with */
threads: number; threads: number;
/** Whether this RunningScript is excluded from saves */ /** Whether this RunningScript is excluded from saves */
@ -5103,6 +5135,29 @@ export interface NS {
*/ */
closeTail(pid?: number): void; closeTail(pid?: number): void;
/**
* Set the title of the tail window of a script.
* @remarks
* RAM cost: 0 GB
*
* This sets the title to the given string, and also forces an update of the
* tail window's contents.
*
* The title is saved across restarts, but only if it is a simple string.
*
* If the pid is unspecified, it will modify the current scripts logs.
*
* Otherwise, the pid argument can be used to change the logs from another script.
*
* It is possible to pass a React Element instead of a string. Get these by calling
* React.createElement() with appropriate parameters. You should either know
* or be willing to learn about the React UI library if you go down this
* route, and there will likely be rough edges.
*
* @param pid - Optional. PID of the script having its tail closed. If omitted, the current script is used.
*/
setTitle(title: string | ReactElement, pid?: number): void;
/** /**
* Get the list of servers connected to a server. * Get the list of servers connected to a server.
* @remarks * @remarks

@ -4,13 +4,18 @@ import { RunningScript } from "../../Script/RunningScript";
import { killWorkerScriptByPid } from "../../Netscript/killWorkerScript"; import { killWorkerScriptByPid } from "../../Netscript/killWorkerScript";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import Draggable, { DraggableEvent } from "react-draggable"; import Draggable, { DraggableEvent } from "react-draggable";
import { ResizableBox, ResizeCallbackData } from "react-resizable"; import { ResizableBox, ResizeCallbackData } from "react-resizable";
import IconButton from "@mui/material/IconButton";
import makeStyles from "@mui/styles/makeStyles"; import makeStyles from "@mui/styles/makeStyles";
import createStyles from "@mui/styles/createStyles"; import createStyles from "@mui/styles/createStyles";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
import CloseIcon from "@mui/icons-material/Close";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
import StopCircleIcon from "@mui/icons-material/StopCircle";
import PlayCircleIcon from "@mui/icons-material/PlayCircle";
import { workerScripts } from "../../Netscript/WorkerScripts"; import { workerScripts } from "../../Netscript/WorkerScripts";
import { startWorkerScript } from "../../NetscriptWorker"; import { startWorkerScript } from "../../NetscriptWorker";
import { GetServer } from "../../Server/AllServers"; import { GetServer } from "../../Server/AllServers";
@ -18,7 +23,6 @@ import { findRunningScriptByPid } from "../../Script/ScriptHelpers";
import { debounce } from "lodash"; import { debounce } from "lodash";
import { Settings } from "../../Settings/Settings"; import { Settings } from "../../Settings/Settings";
import { ANSIITypography } from "./ANSIITypography"; import { ANSIITypography } from "./ANSIITypography";
import { ScriptArg } from "../../Netscript/ScriptArg";
import { useRerender } from "./hooks"; import { useRerender } from "./hooks";
import { dialogBoxCreate } from "./DialogBox"; import { dialogBoxCreate } from "./DialogBox";
@ -28,27 +32,47 @@ export const LogBoxEvents = new EventEmitter<[RunningScript]>();
export const LogBoxCloserEvents = new EventEmitter<[number]>(); export const LogBoxCloserEvents = new EventEmitter<[number]>();
export const LogBoxClearEvents = new EventEmitter<[]>(); export const LogBoxClearEvents = new EventEmitter<[]>();
interface LogBoxUIEvent<T> { // Dynamic properties (size, position) bound to a specific rendered instance of a LogBox
pid: number; export class LogBoxProperties {
data: T; x = window.innerWidth * 0.4;
y = window.innerHeight * 0.3;
width = 500;
height = 500;
rerender: () => void;
rootRef: React.RefObject<Draggable>;
constructor(rerender: () => void, rootRef: React.RefObject<Draggable>) {
this.rerender = rerender;
this.rootRef = rootRef;
} }
interface LogBoxPositionData { updateDOM(): void {
x: number; if (!this.rootRef.current) return;
y: number; const state = this.rootRef.current.state as { x: number; y: number };
state.x = this.x;
state.y = this.y;
} }
export const LogBoxPositionEvents = new EventEmitter<[LogBoxUIEvent<LogBoxPositionData>]>(); setPosition(x: number, y: number): void {
this.x = x;
interface LogBoxResizeData { this.y = y;
w: number; this.updateDOM();
h: number;
} }
export const LogBoxSizeEvents = new EventEmitter<[LogBoxUIEvent<LogBoxResizeData>]>(); setSize(width: number, height: number): void {
this.width = width;
this.height = height;
this.rerender();
}
isVisible(): boolean {
return this.rootRef.current !== null;
}
}
interface Log { interface Log {
id: string; id: number; // The PID of the script *when the window was first opened*
script: RunningScript; script: RunningScript;
} }
@ -59,10 +83,9 @@ export function LogBoxManager(): React.ReactElement {
useEffect( useEffect(
() => () =>
LogBoxEvents.subscribe((script: RunningScript) => { LogBoxEvents.subscribe((script: RunningScript) => {
const id = script.server + "-" + script.filename + script.args.map((x: ScriptArg): string => `${x}`).join("-"); if (logs.some((l) => l.script.pid === script.pid)) return;
if (logs.find((l) => l.id === id)) return;
logs.push({ logs.push({
id: id, id: script.pid,
script: script, script: script,
}); });
rerender(); rerender();
@ -87,21 +110,21 @@ export function LogBoxManager(): React.ReactElement {
); );
//Close tail windows by their id //Close tail windows by their id
function close(id: string): void { function close(id: number): void {
logs = logs.filter((l) => l.id !== id); logs = logs.filter((l) => l.id !== id);
rerender(); rerender();
} }
//Close tail windows by their pid //Close tail windows by their pid.
function closePid(pid: number): void { function closePid(pid: number): void {
logs = logs.filter((log) => log.script.pid != pid); logs = logs.filter((log) => log.script.pid !== pid);
rerender(); rerender();
} }
return ( return (
<> <>
{logs.map((log) => ( {logs.map((log) => (
<LogWindow key={log.id} script={log.script} id={log.id} onClose={() => close(log.id)} /> <LogWindow key={log.id} script={log.script} onClose={() => close(log.id)} />
))} ))}
</> </>
); );
@ -109,7 +132,6 @@ export function LogBoxManager(): React.ReactElement {
interface IProps { interface IProps {
script: RunningScript; script: RunningScript;
id: string;
onClose: () => void; onClose: () => void;
} }
@ -124,7 +146,11 @@ const useStyles = makeStyles(() =>
wordWrap: "break-word", wordWrap: "break-word",
}, },
titleButton: { titleButton: {
padding: "1px 0", borderWidth: "0 0 0 1px",
borderColor: Settings.theme.welllight,
borderStyle: "solid",
borderRadius: "0",
padding: "0",
height: "100%", height: "100%",
}, },
}), }),
@ -135,12 +161,13 @@ export const logBoxBaseZIndex = 1500;
function LogWindow(props: IProps): React.ReactElement { function LogWindow(props: IProps): React.ReactElement {
const draggableRef = useRef<HTMLDivElement>(null); const draggableRef = useRef<HTMLDivElement>(null);
const rootRef = useRef<Draggable>(null); const rootRef = useRef<Draggable>(null);
const [script, setScript] = useState(props.script); const script = props.script;
const classes = useStyles(); const classes = useStyles();
const container = useRef<HTMLDivElement>(null); const container = useRef<HTMLDivElement>(null);
const textArea = useRef<HTMLDivElement>(null); const textArea = useRef<HTMLDivElement>(null);
const rerender = useRerender(1000); const rerender = useRerender(1000);
const [size, setSize] = useState<[number, number]>([500, 500]); const propsRef = useRef(new LogBoxProperties(rerender, rootRef));
script.tailProps = propsRef.current;
const [minimized, setMinimized] = useState(false); const [minimized, setMinimized] = useState(false);
const textAreaKeyDown = (e: React.KeyboardEvent) => { const textAreaKeyDown = (e: React.KeyboardEvent) => {
@ -157,46 +184,17 @@ function LogWindow(props: IProps): React.ReactElement {
}; };
const onResize = (e: React.SyntheticEvent, { size }: ResizeCallbackData) => { const onResize = (e: React.SyntheticEvent, { size }: ResizeCallbackData) => {
setSize([size.width, size.height]); propsRef.current.setSize(size.width, size.height);
}; };
const setPosition = ({ x, y }: LogBoxPositionData) => {
const node = rootRef.current;
if (!node) return;
const state = node.state as { x: number; y: number };
state.x = x;
state.y = y;
};
// Listen to Logbox positioning events.
useEffect(
() =>
LogBoxPositionEvents.subscribe((e) => {
if (e.pid !== props.script.pid) return;
setPosition(e.data);
}),
[],
);
// Listen to Logbox resizing events.
useEffect(
() =>
LogBoxSizeEvents.subscribe((e) => {
if (e.pid !== props.script.pid) return;
setSize([e.data.w, e.data.h]);
}),
[],
);
// initial position if 40%/30%
useEffect(() => setPosition({ x: window.innerWidth * 0.4, y: window.innerHeight * 0.3 }), []);
useEffect(() => { useEffect(() => {
propsRef.current.updateDOM();
updateLayer(); updateLayer();
}, []); }, []);
function kill(): void { function kill(): void {
killWorkerScriptByPid(script.pid); killWorkerScriptByPid(script.pid);
rerender();
} }
function run(): void { function run(): void {
@ -214,10 +212,17 @@ function LogWindow(props: IProps): React.ReactElement {
if (!ramUsage) { if (!ramUsage) {
return dialogBoxCreate(`Could not calculate ram usage for ${script.filename} on ${server.hostname}.`); return dialogBoxCreate(`Could not calculate ram usage for ${script.filename} on ${server.hostname}.`);
} }
// Reset some things, because we're reusing the RunningScript instance
script.ramUsage = ramUsage; script.ramUsage = ramUsage;
script.dataMap = {};
script.onlineExpGained = 0;
script.onlineMoneyMade = 0;
script.onlineRunningTime = 0.01;
startWorkerScript(script, server); startWorkerScript(script, server);
rerender();
} else { } else {
setScript(s); console.warn(`Tried to rerun pid ${script.pid} that was already running!`);
} }
} }
@ -229,13 +234,17 @@ function LogWindow(props: IProps): React.ReactElement {
rerender(); rerender();
} }
function title(full = false): string { function title(): React.ReactElement {
const maxLength = 30; const title_str = script.title === "string" ? script.title : `${script.filename} ${script.args.join(" ")}`;
const t = `${script.filename} ${script.args.join(" ")}`; return (
if (full || t.length <= maxLength) { <Typography
return t; variant="h6"
} sx={{ marginRight: "auto", textOverflow: "ellipsis", whiteSpace: "nowrap", overflow: "hidden" }}
return t.slice(0, maxLength - 3) + "..."; title={title_str}
>
{script.title}
</Typography>
);
} }
function minimize(): void { function minimize(): void {
@ -271,7 +280,7 @@ function LogWindow(props: IProps): React.ReactElement {
if (!node) return; if (!node) return;
if (!isOnScreen(node)) { if (!isOnScreen(node)) {
setPosition({ x: 0, y: 0 }); propsRef.current.setPosition(0, 0);
} }
}, 100); }, 100);
@ -290,7 +299,7 @@ function LogWindow(props: IProps): React.ReactElement {
}; };
// Max [width, height] // Max [width, height]
const minConstraints: [number, number] = [250, 33]; const minConstraints: [number, number] = [150, 33];
return ( return (
<Draggable handle=".drag" onDrag={boundToBody} ref={rootRef} onMouseDown={updateLayer}> <Draggable handle=".drag" onDrag={boundToBody} ref={rootRef} onMouseDown={updateLayer}>
@ -316,8 +325,8 @@ function LogWindow(props: IProps): React.ReactElement {
ref={container} ref={container}
> >
<ResizableBox <ResizableBox
width={size[0]} width={propsRef.current.width}
height={size[1]} height={propsRef.current.height}
onResize={onResize} onResize={onResize}
minConstraints={minConstraints} minConstraints={minConstraints}
handle={ handle={
@ -336,30 +345,24 @@ function LogWindow(props: IProps): React.ReactElement {
> >
<> <>
<Paper className="drag" sx={{ display: "flex", alignItems: "center", cursor: "grab" }} ref={draggableRef}> <Paper className="drag" sx={{ display: "flex", alignItems: "center", cursor: "grab" }} ref={draggableRef}>
<Typography {title()}
variant="h6"
sx={{ marginRight: "auto", textOverflow: "ellipsis", whiteSpace: "nowrap", overflow: "hidden" }}
title={title(true)}
>
{title(true)}
</Typography>
<span style={{ minWidth: "fit-content", height: `${minConstraints[1]}px` }}> <span style={{ minWidth: "fit-content", height: `${minConstraints[1]}px` }}>
{!workerScripts.has(script.pid) ? ( {!workerScripts.has(script.pid) ? (
<Button className={classes.titleButton} onClick={run} onTouchEnd={run}> <IconButton className={classes.titleButton} onClick={run} onTouchEnd={run}>
Run <PlayCircleIcon />
</Button> </IconButton>
) : ( ) : (
<Button className={classes.titleButton} onClick={kill} onTouchEnd={kill}> <IconButton className={classes.titleButton} onClick={kill} onTouchEnd={kill}>
Kill <StopCircleIcon color="error" />
</Button> </IconButton>
)} )}
<Button className={classes.titleButton} onClick={minimize} onTouchEnd={minimize}> <IconButton className={classes.titleButton} onClick={minimize} onTouchEnd={minimize}>
{minimized ? "\u{1F5D6}" : "\u{1F5D5}"} {minimized ? <ExpandMoreIcon /> : <ExpandLessIcon />}
</Button> </IconButton>
<Button className={classes.titleButton} onClick={props.onClose} onTouchEnd={props.onClose}> <IconButton className={classes.titleButton} onClick={props.onClose} onTouchEnd={props.onClose}>
Close <CloseIcon />
</Button> </IconButton>
</span> </span>
</Paper> </Paper>

@ -77,6 +77,7 @@ function loadStandardServers() {
"ramUsage": 1.6, "ramUsage": 1.6,
"server": "home", "server": "home",
"scriptKey": "script.js*[]", "scriptKey": "script.js*[]",
"title": "Awesome Script",
"dependencies": [ "dependencies": [
{ {
"filename": "script.js", "filename": "script.js",

@ -74,6 +74,7 @@ exports[`load/saveAllServers 1`] = `
"ramUsage": 1.6, "ramUsage": 1.6,
"server": "home", "server": "home",
"scriptKey": "script.js*[]", "scriptKey": "script.js*[]",
"title": "Awesome Script",
"threads": 1, "threads": 1,
"temporary": false "temporary": false
} }