diff --git a/markdown/bitburner.md b/markdown/bitburner.md
index 9be6a5a0c..b16f00ad7 100644
--- a/markdown/bitburner.md
+++ b/markdown/bitburner.md
@@ -84,6 +84,7 @@
| [Player](./bitburner.player.md) | |
| [ProcessInfo](./bitburner.processinfo.md) | A single process on a server. |
| [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) | |
| [ReputationFormulas](./bitburner.reputationformulas.md) | Reputation formulas |
| [ResetInfo](./bitburner.resetinfo.md) | Various info about resets |
@@ -99,6 +100,7 @@
| [Stanek](./bitburner.stanek.md) | Stanek's Gift API. |
| [StockOrder](./bitburner.stockorder.md) |
Return value of [getOrders](./bitburner.tix.getorders.md)
Keys are stock symbols, properties are arrays of [StockOrderObject](./bitburner.stockorderobject.md)
|
| [StockOrderObject](./bitburner.stockorderobject.md) | Value in map of [StockOrder](./bitburner.stockorder.md) |
+| [TailProperties](./bitburner.tailproperties.md) | |
| [TIX](./bitburner.tix.md) | Stock market API |
| [UserInterface](./bitburner.userinterface.md) | User Interface API. |
| [UserInterfaceTheme](./bitburner.userinterfacetheme.md) | Interface Theme |
diff --git a/markdown/bitburner.ns.exec.md b/markdown/bitburner.ns.exec.md
index b999d658c..5822647cc 100644
--- a/markdown/bitburner.ns.exec.md
+++ b/markdown/bitburner.ns.exec.md
@@ -21,7 +21,7 @@ exec(
| 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 target server 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. |
| 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
-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.
diff --git a/markdown/bitburner.ns.md b/markdown/bitburner.ns.md
index 7e27708d5..1c50a938f 100644
--- a/markdown/bitburner.ns.md
+++ b/markdown/bitburner.ns.md
@@ -152,6 +152,7 @@ export async function main(ns) {
| [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. |
| [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. |
| [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. |
diff --git a/markdown/bitburner.ns.settitle.md b/markdown/bitburner.ns.settitle.md
new file mode 100644
index 000000000..cc777850e
--- /dev/null
+++ b/markdown/bitburner.ns.settitle.md
@@ -0,0 +1,39 @@
+
+
+[Home](./index.md) > [bitburner](./bitburner.md) > [NS](./bitburner.ns.md) > [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 script’s 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.
+
diff --git a/markdown/bitburner.reactelement.key.md b/markdown/bitburner.reactelement.key.md
new file mode 100644
index 000000000..880a0d93b
--- /dev/null
+++ b/markdown/bitburner.reactelement.key.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [bitburner](./bitburner.md) > [ReactElement](./bitburner.reactelement.md) > [key](./bitburner.reactelement.key.md)
+
+## ReactElement.key property
+
+**Signature:**
+
+```typescript
+key: string | number | null;
+```
diff --git a/markdown/bitburner.reactelement.md b/markdown/bitburner.reactelement.md
new file mode 100644
index 000000000..44141fc32
--- /dev/null
+++ b/markdown/bitburner.reactelement.md
@@ -0,0 +1,22 @@
+
+
+[Home](./index.md) > [bitburner](./bitburner.md) > [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) => [ReactElement](./bitburner.reactelement.md) \| null) \| (new (props: any) => object) | |
+
diff --git a/markdown/bitburner.reactelement.props.md b/markdown/bitburner.reactelement.props.md
new file mode 100644
index 000000000..f2dfc8f5b
--- /dev/null
+++ b/markdown/bitburner.reactelement.props.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [bitburner](./bitburner.md) > [ReactElement](./bitburner.reactelement.md) > [props](./bitburner.reactelement.props.md)
+
+## ReactElement.props property
+
+**Signature:**
+
+```typescript
+props: any;
+```
diff --git a/markdown/bitburner.reactelement.type.md b/markdown/bitburner.reactelement.type.md
new file mode 100644
index 000000000..de14315d3
--- /dev/null
+++ b/markdown/bitburner.reactelement.type.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [bitburner](./bitburner.md) > [ReactElement](./bitburner.reactelement.md) > [type](./bitburner.reactelement.type.md)
+
+## ReactElement.type property
+
+**Signature:**
+
+```typescript
+type: string | ((props: any) => ReactElement | null) | (new (props: any) => object);
+```
diff --git a/markdown/bitburner.runningscript.md b/markdown/bitburner.runningscript.md
index adbaf477a..fac4b92f8 100644
--- a/markdown/bitburner.runningscript.md
+++ b/markdown/bitburner.runningscript.md
@@ -27,6 +27,8 @@ interface RunningScript
| [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 |
| [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 |
| [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. |
diff --git a/markdown/bitburner.runningscript.tailproperties.md b/markdown/bitburner.runningscript.tailproperties.md
new file mode 100644
index 000000000..63d733540
--- /dev/null
+++ b/markdown/bitburner.runningscript.tailproperties.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [bitburner](./bitburner.md) > [RunningScript](./bitburner.runningscript.md) > [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;
+```
diff --git a/markdown/bitburner.runningscript.title.md b/markdown/bitburner.runningscript.title.md
new file mode 100644
index 000000000..748055fd7
--- /dev/null
+++ b/markdown/bitburner.runningscript.title.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [bitburner](./bitburner.md) > [RunningScript](./bitburner.runningscript.md) > [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;
+```
diff --git a/markdown/bitburner.tailproperties.height.md b/markdown/bitburner.tailproperties.height.md
new file mode 100644
index 000000000..938aa64fe
--- /dev/null
+++ b/markdown/bitburner.tailproperties.height.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [bitburner](./bitburner.md) > [TailProperties](./bitburner.tailproperties.md) > [height](./bitburner.tailproperties.height.md)
+
+## TailProperties.height property
+
+Height of the log window content area
+
+**Signature:**
+
+```typescript
+height: number;
+```
diff --git a/markdown/bitburner.tailproperties.md b/markdown/bitburner.tailproperties.md
new file mode 100644
index 000000000..113d7d9f6
--- /dev/null
+++ b/markdown/bitburner.tailproperties.md
@@ -0,0 +1,22 @@
+
+
+[Home](./index.md) > [bitburner](./bitburner.md) > [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 |
+
diff --git a/markdown/bitburner.tailproperties.width.md b/markdown/bitburner.tailproperties.width.md
new file mode 100644
index 000000000..e531e472e
--- /dev/null
+++ b/markdown/bitburner.tailproperties.width.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [bitburner](./bitburner.md) > [TailProperties](./bitburner.tailproperties.md) > [width](./bitburner.tailproperties.width.md)
+
+## TailProperties.width property
+
+Width of the log window content area
+
+**Signature:**
+
+```typescript
+width: number;
+```
diff --git a/markdown/bitburner.tailproperties.x.md b/markdown/bitburner.tailproperties.x.md
new file mode 100644
index 000000000..21a29cb91
--- /dev/null
+++ b/markdown/bitburner.tailproperties.x.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [bitburner](./bitburner.md) > [TailProperties](./bitburner.tailproperties.md) > [x](./bitburner.tailproperties.x.md)
+
+## TailProperties.x property
+
+X-coordinate of the log window
+
+**Signature:**
+
+```typescript
+x: number;
+```
diff --git a/markdown/bitburner.tailproperties.y.md b/markdown/bitburner.tailproperties.y.md
new file mode 100644
index 000000000..5f08e85c7
--- /dev/null
+++ b/markdown/bitburner.tailproperties.y.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [bitburner](./bitburner.md) > [TailProperties](./bitburner.tailproperties.md) > [y](./bitburner.tailproperties.y.md)
+
+## TailProperties.y property
+
+Y-coordinate of the log window
+
+**Signature:**
+
+```typescript
+y: number;
+```
diff --git a/src/Constants.ts b/src/Constants.ts
index 0597176e0..08996cb9d 100644
--- a/src/Constants.ts
+++ b/src/Constants.ts
@@ -236,6 +236,7 @@ v2.3.1 dev
GENERAL / MISC:
+* Tail window overhaul, ability to set tail title with ns.setTitle, other tail bugfixes and improvements. (@d0sboots)
* Nerf noodle bar
`,
};
diff --git a/src/Netscript/NetscriptHelpers.ts b/src/Netscript/NetscriptHelpers.tsx
similarity index 97%
rename from src/Netscript/NetscriptHelpers.ts
rename to src/Netscript/NetscriptHelpers.tsx
index ddc7bb404..abae95b02 100644
--- a/src/Netscript/NetscriptHelpers.ts
+++ b/src/Netscript/NetscriptHelpers.tsx
@@ -1,3 +1,4 @@
+import React from "react";
import { NetscriptContext } from "./APIWrapper";
import { WorkerScript } from "./WorkerScript";
import { killWorkerScript } from "./killWorkerScript";
@@ -38,6 +39,7 @@ import { isPositiveInteger, PositiveInteger, Unknownify } from "../types";
import { Engine } from "../engine";
import { resolveFilePath, FilePath } from "../Paths/FilePath";
import { hasScriptExtension, ScriptFilePath } from "../Paths/ScriptFilePath";
+import { CustomBoundary } from "../ui/Components/CustomBoundary";
export const helpers = {
string,
@@ -736,6 +738,7 @@ function getCannotFindRunningScriptErrorMessage(ident: ScriptIdentifier): string
* @returns A sanitized, NS-facing copy of the RunningScript
*/
function createPublicRunningScript(runningScript: RunningScript): IRunningScript {
+ const logProps = runningScript.tailProps;
return {
args: runningScript.args.slice(),
filename: runningScript.filename,
@@ -749,6 +752,16 @@ function createPublicRunningScript(runningScript: RunningScript): IRunningScript
pid: runningScript.pid,
ramUsage: runningScript.ramUsage,
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,
temporary: runningScript.temporary,
};
@@ -800,3 +813,14 @@ export function handleUnknownError(e: unknown, ws: WorkerScript | ScriptDeath |
}
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 ;
+}
diff --git a/src/Netscript/RamCostGenerator.ts b/src/Netscript/RamCostGenerator.ts
index 1c92dbc21..65a0d6cc1 100644
--- a/src/Netscript/RamCostGenerator.ts
+++ b/src/Netscript/RamCostGenerator.ts
@@ -540,6 +540,7 @@ export const RamCosts: RamCostTree = {
moveTail: 0,
resizeTail: 0,
closeTail: 0,
+ setTitle: 0,
clearPort: 0,
openDevMenu: 0,
alert: 0,
diff --git a/src/NetscriptFunctions.ts b/src/NetscriptFunctions.ts
index 732042b2a..4e52d852f 100644
--- a/src/NetscriptFunctions.ts
+++ b/src/NetscriptFunctions.ts
@@ -37,7 +37,7 @@ import { runScriptFromScript } from "./NetscriptWorker";
import { killWorkerScript, killWorkerScriptByPid } from "./Netscript/killWorkerScript";
import { workerScripts } from "./Netscript/WorkerScripts";
import { WorkerScript } from "./Netscript/WorkerScript";
-import { helpers, assertObjectType } from "./Netscript/NetscriptHelpers";
+import { helpers, assertObjectType, wrapUserNode } from "./Netscript/NetscriptHelpers";
import {
formatExp,
formatNumberNoSuffix,
@@ -49,7 +49,7 @@ import {
formatNumber,
} from "./ui/formatNumber";
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 { NetscriptGang } from "./NetscriptFunctions/Gang";
import { NetscriptSleeve } from "./NetscriptFunctions/Sleeve";
@@ -557,7 +557,12 @@ export const ns: InternalAPI = {
const x = helpers.number(ctx, "x", _x);
const y = helpers.number(ctx, "y", _y);
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:
(ctx) =>
@@ -565,7 +570,12 @@ export const ns: InternalAPI = {
const w = helpers.number(ctx, "w", _w);
const h = helpers.number(ctx, "h", _h);
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:
(ctx) =>
@@ -574,6 +584,18 @@ export const ns: InternalAPI = {
//Emit an event to tell the game to close the tail window if it exists
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) => {
const hostname = helpers.string(ctx, "hostname", _hostname);
diff --git a/src/NetscriptFunctions/Extra.tsx b/src/NetscriptFunctions/Extra.ts
similarity index 83%
rename from src/NetscriptFunctions/Extra.tsx
rename to src/NetscriptFunctions/Extra.ts
index 6075d6ff1..cd11e30e1 100644
--- a/src/NetscriptFunctions/Extra.tsx
+++ b/src/NetscriptFunctions/Extra.ts
@@ -1,16 +1,11 @@
-import React from "react";
import { Player } from "../Player";
import { Exploit } from "../Exploits/Exploit";
import * as bcrypt from "bcryptjs";
import { Apr1Events as devMenu } from "../ui/Apr1";
import { InternalAPI } from "../Netscript/APIWrapper";
-import { helpers } from "../Netscript/NetscriptHelpers";
+import { helpers, wrapUserNode } from "../Netscript/NetscriptHelpers";
import { Terminal } from "../Terminal";
import { RamCostConstants } from "../Netscript/RamCostGenerator";
-import { CustomBoundary } from "../ui/Components/CustomBoundary";
-
-// Incrementing value for custom element keys
-let customElementKey = 0;
export interface INetscriptExtra {
heart: {
@@ -72,14 +67,10 @@ export function NetscriptExtra(): InternalAPI {
return true;
},
tprintRaw: () => (value) => {
- Terminal.printRaw(
- ,
- );
+ Terminal.printRaw(wrapUserNode(value));
},
printRaw: (ctx) => (value) => {
- ctx.workerScript.print(
- ,
- );
+ ctx.workerScript.print(wrapUserNode(value));
},
};
}
diff --git a/src/Script/RunningScript.ts b/src/Script/RunningScript.ts
index 094d69770..cd01a87df 100644
--- a/src/Script/RunningScript.ts
+++ b/src/Script/RunningScript.ts
@@ -17,6 +17,8 @@ import { getKeyList } from "../utils/helpers/getKeyList";
import { ScriptFilePath } from "../Paths/ScriptFilePath";
import { ScriptKey, scriptKey } from "../utils/helpers/scriptKey";
+import type { LogBoxProperties } from "../ui/React/LogBoxManager";
+
export class RunningScript {
// Script arguments
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
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
threads = 1 as PositiveInteger;
@@ -83,6 +93,7 @@ export class RunningScript {
this.server = script.server;
this.ramUsage = ramUsage;
this.dependencies = script.dependencies;
+ this.title = `${this.filename} ${args.join(" ")}`;
}
log(txt: React.ReactNode): void {
@@ -140,7 +151,12 @@ export class RunningScript {
// Serialize the current object to a JSON save state
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
@@ -150,6 +166,9 @@ export class 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;
diff --git a/src/ScriptEditor/NetscriptDefinitions.d.ts b/src/ScriptEditor/NetscriptDefinitions.d.ts
index 503d9eeab..2958cf869 100644
--- a/src/ScriptEditor/NetscriptDefinitions.d.ts
+++ b/src/ScriptEditor/NetscriptDefinitions.d.ts
@@ -169,6 +169,29 @@ interface Multipliers {
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 */
interface RunningScript {
/** Arguments the script was called with */
@@ -198,6 +221,15 @@ interface RunningScript {
ramUsage: number;
/** Hostname of the server on which this script runs */
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 */
threads: number;
/** Whether this RunningScript is excluded from saves */
@@ -5103,6 +5135,29 @@ export interface NS {
*/
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 script’s 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.
* @remarks
diff --git a/src/ui/React/LogBoxManager.tsx b/src/ui/React/LogBoxManager.tsx
index 0addf9a8d..a58fb9721 100644
--- a/src/ui/React/LogBoxManager.tsx
+++ b/src/ui/React/LogBoxManager.tsx
@@ -4,13 +4,18 @@ import { RunningScript } from "../../Script/RunningScript";
import { killWorkerScriptByPid } from "../../Netscript/killWorkerScript";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
-import Button from "@mui/material/Button";
import Paper from "@mui/material/Paper";
import Draggable, { DraggableEvent } from "react-draggable";
import { ResizableBox, ResizeCallbackData } from "react-resizable";
+import IconButton from "@mui/material/IconButton";
import makeStyles from "@mui/styles/makeStyles";
import createStyles from "@mui/styles/createStyles";
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 { startWorkerScript } from "../../NetscriptWorker";
import { GetServer } from "../../Server/AllServers";
@@ -18,7 +23,6 @@ import { findRunningScriptByPid } from "../../Script/ScriptHelpers";
import { debounce } from "lodash";
import { Settings } from "../../Settings/Settings";
import { ANSIITypography } from "./ANSIITypography";
-import { ScriptArg } from "../../Netscript/ScriptArg";
import { useRerender } from "./hooks";
import { dialogBoxCreate } from "./DialogBox";
@@ -28,27 +32,47 @@ export const LogBoxEvents = new EventEmitter<[RunningScript]>();
export const LogBoxCloserEvents = new EventEmitter<[number]>();
export const LogBoxClearEvents = new EventEmitter<[]>();
-interface LogBoxUIEvent {
- pid: number;
- data: T;
+// Dynamic properties (size, position) bound to a specific rendered instance of a LogBox
+export class LogBoxProperties {
+ x = window.innerWidth * 0.4;
+ y = window.innerHeight * 0.3;
+ width = 500;
+ height = 500;
+
+ rerender: () => void;
+ rootRef: React.RefObject;
+
+ constructor(rerender: () => void, rootRef: React.RefObject) {
+ this.rerender = rerender;
+ this.rootRef = rootRef;
+ }
+
+ updateDOM(): void {
+ if (!this.rootRef.current) return;
+ const state = this.rootRef.current.state as { x: number; y: number };
+ state.x = this.x;
+ state.y = this.y;
+ }
+
+ setPosition(x: number, y: number): void {
+ this.x = x;
+ this.y = y;
+ this.updateDOM();
+ }
+
+ setSize(width: number, height: number): void {
+ this.width = width;
+ this.height = height;
+ this.rerender();
+ }
+
+ isVisible(): boolean {
+ return this.rootRef.current !== null;
+ }
}
-interface LogBoxPositionData {
- x: number;
- y: number;
-}
-
-export const LogBoxPositionEvents = new EventEmitter<[LogBoxUIEvent]>();
-
-interface LogBoxResizeData {
- w: number;
- h: number;
-}
-
-export const LogBoxSizeEvents = new EventEmitter<[LogBoxUIEvent]>();
-
interface Log {
- id: string;
+ id: number; // The PID of the script *when the window was first opened*
script: RunningScript;
}
@@ -59,10 +83,9 @@ export function LogBoxManager(): React.ReactElement {
useEffect(
() =>
LogBoxEvents.subscribe((script: RunningScript) => {
- const id = script.server + "-" + script.filename + script.args.map((x: ScriptArg): string => `${x}`).join("-");
- if (logs.find((l) => l.id === id)) return;
+ if (logs.some((l) => l.script.pid === script.pid)) return;
logs.push({
- id: id,
+ id: script.pid,
script: script,
});
rerender();
@@ -87,21 +110,21 @@ export function LogBoxManager(): React.ReactElement {
);
//Close tail windows by their id
- function close(id: string): void {
+ function close(id: number): void {
logs = logs.filter((l) => l.id !== id);
rerender();
}
- //Close tail windows by their pid
+ //Close tail windows by their pid.
function closePid(pid: number): void {
- logs = logs.filter((log) => log.script.pid != pid);
+ logs = logs.filter((log) => log.script.pid !== pid);
rerender();
}
return (
<>
{logs.map((log) => (
- close(log.id)} />
+ close(log.id)} />
))}
>
);
@@ -109,7 +132,6 @@ export function LogBoxManager(): React.ReactElement {
interface IProps {
script: RunningScript;
- id: string;
onClose: () => void;
}
@@ -124,7 +146,11 @@ const useStyles = makeStyles(() =>
wordWrap: "break-word",
},
titleButton: {
- padding: "1px 0",
+ borderWidth: "0 0 0 1px",
+ borderColor: Settings.theme.welllight,
+ borderStyle: "solid",
+ borderRadius: "0",
+ padding: "0",
height: "100%",
},
}),
@@ -135,12 +161,13 @@ export const logBoxBaseZIndex = 1500;
function LogWindow(props: IProps): React.ReactElement {
const draggableRef = useRef(null);
const rootRef = useRef(null);
- const [script, setScript] = useState(props.script);
+ const script = props.script;
const classes = useStyles();
const container = useRef(null);
const textArea = useRef(null);
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 textAreaKeyDown = (e: React.KeyboardEvent) => {
@@ -157,46 +184,17 @@ function LogWindow(props: IProps): React.ReactElement {
};
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(() => {
+ propsRef.current.updateDOM();
updateLayer();
}, []);
function kill(): void {
killWorkerScriptByPid(script.pid);
+ rerender();
}
function run(): void {
@@ -214,10 +212,17 @@ function LogWindow(props: IProps): React.ReactElement {
if (!ramUsage) {
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.dataMap = {};
+ script.onlineExpGained = 0;
+ script.onlineMoneyMade = 0;
+ script.onlineRunningTime = 0.01;
+
startWorkerScript(script, server);
+ rerender();
} 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();
}
- function title(full = false): string {
- const maxLength = 30;
- const t = `${script.filename} ${script.args.join(" ")}`;
- if (full || t.length <= maxLength) {
- return t;
- }
- return t.slice(0, maxLength - 3) + "...";
+ function title(): React.ReactElement {
+ const title_str = script.title === "string" ? script.title : `${script.filename} ${script.args.join(" ")}`;
+ return (
+
+ {script.title}
+
+ );
}
function minimize(): void {
@@ -271,7 +280,7 @@ function LogWindow(props: IProps): React.ReactElement {
if (!node) return;
if (!isOnScreen(node)) {
- setPosition({ x: 0, y: 0 });
+ propsRef.current.setPosition(0, 0);
}
}, 100);
@@ -290,7 +299,7 @@ function LogWindow(props: IProps): React.ReactElement {
};
// Max [width, height]
- const minConstraints: [number, number] = [250, 33];
+ const minConstraints: [number, number] = [150, 33];
return (
@@ -316,8 +325,8 @@ function LogWindow(props: IProps): React.ReactElement {
ref={container}
>
<>
-
- {title(true)}
-
+ {title()}
{!workerScripts.has(script.pid) ? (
-
+
+
+
) : (
-
+
+
+
)}
-
-
+
+ {minimized ? : }
+
+
+
+
diff --git a/test/jest/Save.test.ts b/test/jest/Save.test.ts
index 7307088e9..9890f8305 100644
--- a/test/jest/Save.test.ts
+++ b/test/jest/Save.test.ts
@@ -77,6 +77,7 @@ function loadStandardServers() {
"ramUsage": 1.6,
"server": "home",
"scriptKey": "script.js*[]",
+ "title": "Awesome Script",
"dependencies": [
{
"filename": "script.js",
diff --git a/test/jest/__snapshots__/Save.test.ts.snap b/test/jest/__snapshots__/Save.test.ts.snap
index 210f6d3af..517b86d53 100644
--- a/test/jest/__snapshots__/Save.test.ts.snap
+++ b/test/jest/__snapshots__/Save.test.ts.snap
@@ -74,6 +74,7 @@ exports[`load/saveAllServers 1`] = `
"ramUsage": 1.6,
"server": "home",
"scriptKey": "script.js*[]",
+ "title": "Awesome Script",
"threads": 1,
"temporary": false
}