diff --git a/src/Documentation/doc/basic/autocomplete.md b/src/Documentation/doc/basic/autocomplete_help.md
similarity index 51%
rename from src/Documentation/doc/basic/autocomplete.md
rename to src/Documentation/doc/basic/autocomplete_help.md
index de924f0ba3..4b1fb1e6cd 100644
--- a/src/Documentation/doc/basic/autocomplete.md
+++ b/src/Documentation/doc/basic/autocomplete_help.md
@@ -1,8 +1,14 @@
-# Autocomplete
+# Autocomplete and Help
-The BitBurner terminal offers tab-completion, where pressing tab after typing a command offers suggestions for arguments to pass. You can customize this behavior for your scripts.
+Beyond the scope of executing your [scripts](scripts.md) in BitBurner, you have extra functionality that may be **exported** out of your files.
-This relies on an exported function named "autocomplete" that is placed _outside_ of main, in the base scope of the script.
+You have the capability of implementing _autocomplete_ for your scripts terminal interaction, and custom _help_ instructions that are shown when you use the `help` command.
+
+These rely on exported functions named `autocomplete()` and `help()`, that must be placed _outside_ of main, in the base scope of the script.
+
+## Autocomplete
+
+The BitBurner terminal offers tab-completion, where pressing `tab` after typing a command offers suggestions for arguments to pass. You can customize this behavior for your scripts.
This function must return an array, the contents of which make up the autocomplete options.
@@ -27,7 +33,7 @@ export function main(ns) {
Running this script from the terminal like `run script.js` or `./script.js` and pressing tab, would offer "argument0", "argument1" and "argument2" as autocomplete options.
-## AutocompleteData
+### AutocompleteData
To make this feature more useful, an [AutocompleteData](https://github.com/bitburner-official/bitburner-src/blob/stable/markdown/bitburner.autocompletedata.md) object is provided to the autocomplete function that holds information commonly passed as arguments to scripts, such as server names and filenames.
@@ -90,8 +96,103 @@ export function autocomplete(data, args) {
In that example typing `run script.js` and pressing tab would initially suggest every server for autocomplete. Then if "n00dles" is added to the arguments and tab is pressed again, "n00dles" would no longer be suggested in subsequent autocomplete calls.
-# Notes
+# Help
+
+The `help` terminal command offers detailed information about existing terminal commands. It can also display custom help messages defined inside a script file.
-- The autocomplete function in the file is called each time the tab key is pressed following `run file.js` or `./file.js` in the terminal.
-- The autocomplete function is separate from `main`, and does not receive an `ns` context as a parameter. This means no `ns` game commands will work in autocomplete functions.
-- If a multi-element array is returned then multiple options are displayed. If a single-element array is returned then that element is auto-filled to the terminal. This is handy for the "--tail" run argument, for example.
+This function's return type can be either a simple string, or if you prefer a little bit more customization, a [ReactNode](../programming/react.md).
+
+For example:
+
+```javascript
+/**
+ * @param {AutocompleteData} data - context about the game, may be useful to list argument documentation
+ * @returns {string|ReactNode} - Outputted to the Terminal
+ */
+export function help(data) {
+ return ["This is a simple script.", " ", "This script will output foo."].join("\n");
+}
+
+/** @param {NS} ns */
+export function main(ns) {
+ ns.tprint("foo");
+}
+```
+
+Running `help` on the terminal as `help script.js` would display the provided strings line-by-line, as shown below:
+
+```plaintext
+Usage for script.js:
+This is a simple script.
+
+This script will output foo.
+```
+
+This function supports ANSI escape codes, in case you'd like a bit of customization in your text.
+
+```javascript
+/**
+ * @param {AutocompleteData} data - context about the game, may be useful to list argument documentation
+ * @returns {string|ReactNode} - Outputted to the Terminal
+ */
+export function help() {
+ return `${"\x1b[2m"}This is fancy bold text!`;
+}
+
+/** @param {NS} ns */
+export function main(ns) {
+ // ...
+}
+```
+Which would show "**This is fancy bold text!**" in the Terminal.
+
+## Advanced Use
+
+For more advanced uses of this function, you might consider using _TypeScript_ files.
+
+### Context clues through AutocompleteData
+
+AutocompleteData may be passed into the help function to give custom messages based on context. This may be helpful when you have a script that takes in game-specific arguments, which synergize with the autocomplete function:
+
+A few notable differences with this and autocomplete is that the `.command` property contains the file path and name, the `.flags` property is empty.
+
+_Example: contracts.ts_
+
+```ts
+export function help(data: AutocompleteData): string {
+ return [
+ "Finds and solves contracts in a server.",
+ "Possible arguments: ",
+ ` ${data.servers.join(", ")}`
+ ].join("\n");
+}
+
+// ...
+```
+
+```plaintext
+Usage for contracts.ts:
+Finds and solves contracts in a server.
+Possible arguments:
+ n00dles, foodnstuff, sigma-cosmetics, joesguns, hong-fang-tea, harakiri-sushi, iron-gym
+```
+
+
+The function can also return a React element. For this, it's highly recommended to write a `*.tsx` script file.
+The example above, written in `.tsx`:
+
+```tsx
+export function help(data: AutocompleteData): ReactNode {
+ return
+ - ./{data.command} {""}
+ - Servers must be one of: {data.servers.join(", ")}
+
;
+}
+```
+
+Outputs:
+```plaintext
+Usage for contracts.tsx:
+ - ./contracts.tsx
+ - Servers must be one of: n00dles, foodnstuff, sigma-cosmetics, joesguns, hong-fang-tea, harakiri-sushi, iron-gym
+```
diff --git a/src/Documentation/doc/index.md b/src/Documentation/doc/index.md
index 5c502b4913..76071f69b4 100644
--- a/src/Documentation/doc/index.md
+++ b/src/Documentation/doc/index.md
@@ -23,7 +23,7 @@
- [Stock market](basic/stockmarket.md)
- [World](basic/world.md)
- [Coding contracts](basic/codingcontracts.md)
-- [Autocomplete](basic/autocomplete.md)
+- [Autocomplete and Help](basic/autocomplete_help.md)
## Advanced Mechanics
diff --git a/src/Documentation/pages.ts b/src/Documentation/pages.ts
index ee1cfde4e2..d028711acf 100644
--- a/src/Documentation/pages.ts
+++ b/src/Documentation/pages.ts
@@ -1,7 +1,7 @@
// THIS FILE IS AUTOGENERATED. DO NOT EDIT IT MANUALLY.
-import file0 from "./doc/advanced/bitnodes.md?raw";
-import file1 from "./doc/advanced/bitnode_recommendation_comprehensive_guide.md?raw";
-import file2 from "./doc/advanced/bitnode_recommendation_short_guide.md?raw";
+import file0 from "./doc/advanced/bitnode_recommendation_comprehensive_guide.md?raw";
+import file1 from "./doc/advanced/bitnode_recommendation_short_guide.md?raw";
+import file2 from "./doc/advanced/bitnodes.md?raw";
import file3 from "./doc/advanced/bladeburners.md?raw";
import file4 from "./doc/advanced/corporation/basic-gameplay-and-term.md?raw";
import file5 from "./doc/advanced/corporation/boost-material.md?raw";
@@ -31,7 +31,7 @@ import file28 from "./doc/advanced/sleeves.md?raw";
import file29 from "./doc/advanced/sourcefiles.md?raw";
import file30 from "./doc/advanced/stanek.md?raw";
import file31 from "./doc/basic/augmentations.md?raw";
-import file32 from "./doc/basic/autocomplete.md?raw";
+import file32 from "./doc/basic/autocomplete_help.md?raw";
import file33 from "./doc/basic/codingcontracts.md?raw";
import file34 from "./doc/basic/companies.md?raw";
import file35 from "./doc/basic/crimes.md?raw";
@@ -63,6 +63,7 @@ import file60 from "./doc/programming/hackingalgorithms.md?raw";
import file61 from "./doc/programming/learn.md?raw";
import file62 from "./doc/programming/react.md?raw";
import file63 from "./doc/programming/remote_api.md?raw";
+import nsDoc_bitburner__valueof_md from "../../markdown/bitburner._valueof.md?raw";
import nsDoc_bitburner_activefragment_highestcharge_md from "../../markdown/bitburner.activefragment.highestcharge.md?raw";
import nsDoc_bitburner_activefragment_id_md from "../../markdown/bitburner.activefragment.id.md?raw";
import nsDoc_bitburner_activefragment_md from "../../markdown/bitburner.activefragment.md?raw";
@@ -994,8 +995,8 @@ import nsDoc_bitburner_ns_infiltration_md from "../../markdown/bitburner.ns.infi
import nsDoc_bitburner_ns_islogenabled_md from "../../markdown/bitburner.ns.islogenabled.md?raw";
import nsDoc_bitburner_ns_isrunning_md from "../../markdown/bitburner.ns.isrunning.md?raw";
import nsDoc_bitburner_ns_kill_md from "../../markdown/bitburner.ns.kill.md?raw";
-import nsDoc_bitburner_ns_killall_md from "../../markdown/bitburner.ns.killall.md?raw";
import nsDoc_bitburner_ns_kill_1_md from "../../markdown/bitburner.ns.kill_1.md?raw";
+import nsDoc_bitburner_ns_killall_md from "../../markdown/bitburner.ns.killall.md?raw";
import nsDoc_bitburner_ns_ls_md from "../../markdown/bitburner.ns.ls.md?raw";
import nsDoc_bitburner_ns_md from "../../markdown/bitburner.ns.md?raw";
import nsDoc_bitburner_ns_mv_md from "../../markdown/bitburner.ns.mv.md?raw";
@@ -1494,13 +1495,12 @@ import nsDoc_bitburner_workstats_md from "../../markdown/bitburner.workstats.md?
import nsDoc_bitburner_workstats_money_md from "../../markdown/bitburner.workstats.money.md?raw";
import nsDoc_bitburner_workstats_reputation_md from "../../markdown/bitburner.workstats.reputation.md?raw";
import nsDoc_bitburner_workstats_strexp_md from "../../markdown/bitburner.workstats.strexp.md?raw";
-import nsDoc_bitburner__valueof_md from "../../markdown/bitburner._valueof.md?raw";
import nsDoc_index_md from "../../markdown/index.md?raw";
export const AllPages: Record = {};
-AllPages["advanced/bitnodes.md"] = file0;
-AllPages["advanced/bitnode_recommendation_comprehensive_guide.md"] = file1;
-AllPages["advanced/bitnode_recommendation_short_guide.md"] = file2;
+AllPages["advanced/bitnode_recommendation_comprehensive_guide.md"] = file0;
+AllPages["advanced/bitnode_recommendation_short_guide.md"] = file1;
+AllPages["advanced/bitnodes.md"] = file2;
AllPages["advanced/bladeburners.md"] = file3;
AllPages["advanced/corporation/basic-gameplay-and-term.md"] = file4;
AllPages["advanced/corporation/boost-material.md"] = file5;
@@ -1530,7 +1530,7 @@ AllPages["advanced/sleeves.md"] = file28;
AllPages["advanced/sourcefiles.md"] = file29;
AllPages["advanced/stanek.md"] = file30;
AllPages["basic/augmentations.md"] = file31;
-AllPages["basic/autocomplete.md"] = file32;
+AllPages["basic/autocomplete_help.md"] = file32;
AllPages["basic/codingcontracts.md"] = file33;
AllPages["basic/companies.md"] = file34;
AllPages["basic/crimes.md"] = file35;
@@ -1562,6 +1562,7 @@ AllPages["programming/hackingalgorithms.md"] = file60;
AllPages["programming/learn.md"] = file61;
AllPages["programming/react.md"] = file62;
AllPages["programming/remote_api.md"] = file63;
+AllPages["nsDoc/bitburner._valueof.md"] = nsDoc_bitburner__valueof_md;
AllPages["nsDoc/bitburner.activefragment.highestcharge.md"] = nsDoc_bitburner_activefragment_highestcharge_md;
AllPages["nsDoc/bitburner.activefragment.id.md"] = nsDoc_bitburner_activefragment_id_md;
AllPages["nsDoc/bitburner.activefragment.md"] = nsDoc_bitburner_activefragment_md;
@@ -2493,8 +2494,8 @@ AllPages["nsDoc/bitburner.ns.infiltration.md"] = nsDoc_bitburner_ns_infiltration
AllPages["nsDoc/bitburner.ns.islogenabled.md"] = nsDoc_bitburner_ns_islogenabled_md;
AllPages["nsDoc/bitburner.ns.isrunning.md"] = nsDoc_bitburner_ns_isrunning_md;
AllPages["nsDoc/bitburner.ns.kill.md"] = nsDoc_bitburner_ns_kill_md;
-AllPages["nsDoc/bitburner.ns.killall.md"] = nsDoc_bitburner_ns_killall_md;
AllPages["nsDoc/bitburner.ns.kill_1.md"] = nsDoc_bitburner_ns_kill_1_md;
+AllPages["nsDoc/bitburner.ns.killall.md"] = nsDoc_bitburner_ns_killall_md;
AllPages["nsDoc/bitburner.ns.ls.md"] = nsDoc_bitburner_ns_ls_md;
AllPages["nsDoc/bitburner.ns.md"] = nsDoc_bitburner_ns_md;
AllPages["nsDoc/bitburner.ns.mv.md"] = nsDoc_bitburner_ns_mv_md;
@@ -2993,7 +2994,6 @@ AllPages["nsDoc/bitburner.workstats.md"] = nsDoc_bitburner_workstats_md;
AllPages["nsDoc/bitburner.workstats.money.md"] = nsDoc_bitburner_workstats_money_md;
AllPages["nsDoc/bitburner.workstats.reputation.md"] = nsDoc_bitburner_workstats_reputation_md;
AllPages["nsDoc/bitburner.workstats.strexp.md"] = nsDoc_bitburner_workstats_strexp_md;
-AllPages["nsDoc/bitburner._valueof.md"] = nsDoc_bitburner__valueof_md;
AllPages["nsDoc/index.md"] = nsDoc_index_md;
export const nsApiPages = Object.keys(AllPages)
diff --git a/src/Script/LoadedModule.ts b/src/Script/LoadedModule.ts
index 6c39806300..8f4dd6c701 100644
--- a/src/Script/LoadedModule.ts
+++ b/src/Script/LoadedModule.ts
@@ -8,6 +8,7 @@ export type ScriptURL = string & { __type: "ScriptURL" };
export interface ScriptModule {
main?: (ns: NSFull, ...args: ScriptArg[]) => unknown;
autocomplete?: (data: AutocompleteData, flags: string[]) => unknown;
+ help?: (data: AutocompleteData) => string | React.ReactElement;
}
export class LoadedModule {
diff --git a/src/Terminal/commands/help.ts b/src/Terminal/commands/help.ts
index 6bdf2a0d5e..112b4a6611 100644
--- a/src/Terminal/commands/help.ts
+++ b/src/Terminal/commands/help.ts
@@ -1,5 +1,14 @@
-import { Terminal } from "../../Terminal";
+import { Player } from "@player";
+import type { AutocompleteData } from "@nsdefs";
import { TerminalHelpText, HelpTexts } from "../HelpText";
+import { Terminal } from "../../Terminal";
+import { compile } from "../../NetscriptJSEvaluator";
+import { wrapUserNode } from "../../Netscript/NetscriptHelpers";
+import { enums } from "../../NetscriptFunctions";
+import { GetAllServers } from "../../Server/AllServers";
+import { hasScriptExtension, resolveScriptFilePath, ScriptFilePath } from "../../Paths/ScriptFilePath";
+import { Flags } from "../../NetscriptFunctions/Flags";
+import { hasTextExtension } from "../../Paths/TextFilePath";
export function help(args: (string | number | boolean)[]): void {
if (args.length !== 0 && args.length !== 1) {
@@ -7,14 +16,89 @@ export function help(args: (string | number | boolean)[]): void {
return;
}
if (args.length === 0) {
- TerminalHelpText.forEach((line) => Terminal.print(line));
+ const flushedOut = TerminalHelpText.join("\n");
+ Terminal.print(flushedOut);
} else {
const cmd = args[0] + "";
const txt = HelpTexts[cmd];
+
if (txt == null) {
- Terminal.error("No help topics match '" + cmd + "'");
+ // Here is where flow lands if we have a player-implemented command
+
+ // Input sanitization
+ const cmdCopy = String(cmd).replace(/^[/.]+/, "") as ScriptFilePath;
+ const filePath = resolveScriptFilePath(cmdCopy);
+
+ const localServer = Player.getCurrentServer();
+
+ if (filePath == null) {
+ if (hasScriptExtension(cmdCopy)) {
+ Terminal.error(`Could not find script '${cmdCopy}'\nMake sure this file exists in this server.`);
+ } else if (hasTextExtension(cmdCopy)) {
+ Terminal.error(
+ `'${cmdCopy}' needs to be either a *.js, *.ts, *.jsx or *.tsx file to have detailed help information.`,
+ );
+ } else {
+ Terminal.error(`No help entry for '${cmdCopy}'.`);
+ }
+ return;
+ }
+
+ const script = localServer.scripts.get(filePath);
+
+ try {
+ if (script == null) {
+ throw new Error("Script pathname has no valid script object");
+ }
+
+ // Deal with help() here
+
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
+ compile(script, localServer.scripts).then((compiledModule) => {
+ if (compiledModule.help == null) {
+ Terminal.error("No help text for '" + cmdCopy + "'. Implement it by exporting a help() function.");
+ return;
+ }
+
+ const autocompleteData: AutocompleteData = {
+ hostname: localServer.hostname,
+ servers: GetAllServers()
+ .filter((server) => server.backdoorInstalled || localServer.serversOnNetwork.includes(server.hostname))
+ .map((server) => server.hostname),
+ scripts: [...localServer.scripts.keys()],
+ txts: [...localServer.textFiles.keys()],
+ enums: enums,
+ // Pass no flags as the help command does not use flags
+ flags: Flags([""]),
+ filename: script.filename,
+ processes: Array.from(localServer.runningScriptMap.values(), (m) =>
+ Array.from(m.values(), (r) => ({
+ pid: r.pid,
+ filename: r.filename,
+ threads: r.threads,
+ args: r.args.slice(),
+ temporary: r.temporary,
+ })),
+ ).flat(),
+ // Pass the command as the script filename
+ command: `${cmdCopy}`,
+ };
+
+ const helpObj = compiledModule.help(autocompleteData);
+ Terminal.print(`Usage for ${cmdCopy}:`);
+
+ if (typeof helpObj === "string") {
+ Terminal.print(helpObj);
+ } else {
+ Terminal.printRaw(wrapUserNode(helpObj));
+ }
+ });
+ } catch (err) {
+ Terminal.error("Failed to get information for '" + cmdCopy + "'. Check if the script has any syntax errors.");
+ }
return;
}
- txt.forEach((t) => Terminal.print(t));
+ const flushedOut: string = txt.join("\n");
+ Terminal.print(flushedOut);
}
}
diff --git a/src/Terminal/getTabCompletionPossibilities.ts b/src/Terminal/getTabCompletionPossibilities.ts
index ae32343f21..3280c62270 100644
--- a/src/Terminal/getTabCompletionPossibilities.ts
+++ b/src/Terminal/getTabCompletionPossibilities.ts
@@ -199,6 +199,7 @@ export async function getTabCompletionPossibilities(terminalText: string, baseDi
if (onFirstCommandArg) {
addGeneric({ iterable: Object.keys(HelpTexts), usePathing: false });
}
+ addScripts();
return possibilities;
case "nano":