Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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.

Expand Down Expand Up @@ -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(", ")}`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to update this example code and output. Please check my comment in help.ts. I recommend using something that is not data.servers. That list is too long after making the suggested change in that comment, so it's not really a good example.

].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 <li><ul>
<li> ./{data.command} {"<servers...>"} </li>
<li> Servers must be one of: {data.servers.join(", ")} </li>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here.

</ul></li>;
}
```

Outputs:
```plaintext
Usage for contracts.tsx:
- ./contracts.tsx <servers...>
- Servers must be one of: n00dles, foodnstuff, sigma-cosmetics, joesguns, hong-fang-tea, harakiri-sushi, iron-gym
```
2 changes: 1 addition & 1 deletion src/Documentation/doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 12 additions & 12 deletions src/Documentation/pages.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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<string, string> = {};
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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions src/Script/LoadedModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
92 changes: 88 additions & 4 deletions src/Terminal/commands/help.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,104 @@
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) {
Terminal.error("Incorrect usage of help command. Usage: help");
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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this? Does help not work for scripts with relative directories?

This doesn't feel like the right way to be checking for a script. I think Terminal.getFilepath is what you ought to be using here.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This warning is legitimate, although ultimately you will still end up doing the equivalent of dangling a promise.

But, the issue is that everything within here is asynchronous, whereas the help command ends immediately. So, multiple lines can end up appended to the terminal before the script comes back with the result of the help.

I think what you're going to need to do is Terminal.printRaw a reactnode here synchronously, but then capture that node inside the promise via closure capturing. That way, the space has already been allocated for the result, and you can asynchronously fill it in once it comes back. It could even start off as "..." or something to indicate it's computing. All the error messages that happen within the async part would work the same way.

I think you might be able to get fancy and still use Terminal.print/Terminal.error and thus avoid having to do ugly things for the ANSI support, by then directly mucking with the Terminal object and essentially swapping the placeholder object you created with printRaw for the new line, and removing the placeholder after. Since this all takes place synchronously (inside the resolution of the async then), it won't cause any flickering.

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 = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Surely there's a version of this already for autocomplete? They should be shared, unless there's a painful reason why they can't.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While refactoring, as d0sboots said, you should be careful when making any differences (e.g., servers).

hostname: localServer.hostname,
servers: GetAllServers()
.filter((server) => server.backdoorInstalled || localServer.serversOnNetwork.includes(server.hostname))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

servers is the list of all non-hidden servers' hostnames. You have to use filter((server) => server.serversOnNetwork.length !== 0) (src\Terminal\getTabCompletionPossibilities.ts).

If there is a reason that servers has to be a different list in help, you have to explain that reason in the TSDoc of AutocompleteData. I don't think there is one, though.

.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([""]),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
flags: 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);
}
}
1 change: 1 addition & 0 deletions src/Terminal/getTabCompletionPossibilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
Loading