diff --git a/src/Documentation/doc/advanced/autocomplete.md b/src/Documentation/doc/advanced/autocomplete.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Documentation/doc/advanced/inject_html.md b/src/Documentation/doc/advanced/inject_html.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Documentation/doc/advanced/netscripthacknetnodeapi.md b/src/Documentation/doc/advanced/netscripthacknetnodeapi.md new file mode 100644 index 0000000000..31c387cd8a --- /dev/null +++ b/src/Documentation/doc/advanced/netscripthacknetnodeapi.md @@ -0,0 +1,123 @@ +# Netscript Hacknet Node API + +## Warning + +Not all functions in the Hacknet Node API are immediately available. For this reason, the documentation for this API may contain spoilers for the game. + +Netscript provides the following API for accessing and upgrading your Hacknet Nodes through scripts. + +Note that none of these functions will write to the script's logs. If you want to see what your script is doing, you will have to print to the logs yourself. + +Hacknet Node API functions must be accessed through the `hacknet` namespace. + +### In Netscript 1.0: +```javascript +hacknet.purchaseNode(); +hacknet.getNodeStats(3).level; +``` + +### In NetscriptJS: +```javascript +ns.hacknet.purchaseNode(); +ns.hacknet.getNodeStats(3).level; +``` + +## Hacknet Nodes API Functions + +- [`numNodes()`](hacknetnodeapi/numNodes) +- [`purchaseNode()`](hacknetnodeapi/purchaseNode) +- [`getPurchaseNodeCost()`](hacknetnodeapi/getPurchaseNodeCost) +- [`getNodeStats()`](hacknetnodeapi/getNodeStats) +- [`upgradeLevel()`](hacknetnodeapi/upgradeLevel) +- [`upgradeRam()`](hacknetnodeapi/upgradeRam) +- [`upgradeCore()`](hacknetnodeapi/upgradeCore) +- [`getLevelUpgradeCost()`](hacknetnodeapi/getLevelUpgradeCost) +- [`getRamUpgradeCost()`](hacknetnodeapi/getRamUpgradeCost) +- [`getCoreUpgradeCost()`](hacknetnodeapi/getCoreUpgradeCost) + +## Referencing a Hacknet Node + +Most of the functions in the Hacknet Node API perform an operation on a single node. Therefore, a numeric index is used to identify and specify which Hacknet Node a function should act on. This index number corresponds to the number at the end of the name of the Hacknet Node. + +For example: +- The first Hacknet Node you purchase will have the name `hacknet-node-0` and is referenced using index `0`. +- The fifth Hacknet Node you purchase will have the name `hacknet-node-4` and is referenced using index `4`. + +## RAM Cost + +Accessing the `hacknet` namespace incurs a one-time cost of **4 GB of RAM**. In other words, using multiple Hacknet Node API functions in a script will not cost more than 4 GB of RAM. + +## Utilities + +The following function is not officially part of the Hacknet Node API, but it can be useful when writing Hacknet Node-related scripts. Since it is not part of the API, it does not need to be accessed using the `hacknet` namespace. + +- `getHacknetMultipliers` + +## Example Script + +The following is an example of one way a script can be used to automate the purchasing and upgrading of Hacknet Nodes. + +This script attempts to purchase Hacknet Nodes until the player has a total of 8. Then, it gradually upgrades those nodes to **level 80**, **16 GB RAM**, and **8 cores**. + +```javascript +export async function main(ns) { + function myMoney() { + return ns.getServerMoneyAvailable("home"); + } + + ns.disableLog("getServerMoneyAvailable"); + ns.disableLog("sleep"); + + const cnt = 8; + + while (ns.hacknet.numNodes() < cnt) { + let res = ns.hacknet.purchaseNode(); + if (res != -1) ns.print("Purchased Hacknet Node with index " + res); + await ns.sleep(1000); + } + + ns.tprint("All " + cnt + " nodes purchased"); + + for (let i = 0; i < cnt; i++) { + while (ns.hacknet.getNodeStats(i).level <= 80) { + let cost = ns.hacknet.getLevelUpgradeCost(i, 1); + while (myMoney() < cost) { + ns.print("Need $" + cost + " . Have $" + myMoney()); + await ns.sleep(3000); + } + ns.hacknet.upgradeLevel(i, 1); + } + } + + ns.tprint("All nodes upgraded to level 80"); + + for (let i = 0; i < cnt; i++) { + while (ns.hacknet.getNodeStats(i).ram < 16) { + let cost = ns.hacknet.getRamUpgradeCost(i, 1); + while (myMoney() < cost) { + ns.print("Need $" + cost + " . Have $" + myMoney()); + await ns.sleep(3000); + } + ns.hacknet.upgradeRam(i, 1); + } + } + + ns.tprint("All nodes upgraded to 16GB RAM"); + + for (let i = 0; i < cnt; i++) { + while (ns.hacknet.getNodeStats(i).cores < 8) { + let cost = ns.hacknet.getCoreUpgradeCost(i, 1); + while (myMoney() < cost) { + ns.print("Need $" + cost + " . Have $" + myMoney()); + await ns.sleep(3000); + } + ns.hacknet.upgradeCore(i, 1); + } + } + + ns.tprint("All nodes upgraded to 8 cores"); +} +``` + +This script ensures that the player's Hacknet Nodes are upgraded efficiently while maintaining sufficient funds. + diff --git a/src/Documentation/doc/advanced/netscriptmisc.md b/src/Documentation/doc/advanced/netscriptmisc.md new file mode 100644 index 0000000000..9d126ccb0d --- /dev/null +++ b/src/Documentation/doc/advanced/netscriptmisc.md @@ -0,0 +1,228 @@ +# Netscript Miscellaneous + +## Netscript Ports + +Netscript Ports are endpoints that can be used to communicate between scripts and across servers. A port is implemented as a serialized queue, where you can only write and read one element at a time from the port. Only string and number types may be written to ports. When you read data from a port, the element that is read is removed from the port. + +The following Netscript functions can be used to interact with ports: + +- `read` +- `write` +- `tryWrite` +- `clear` +- `peek` + +Ports are specified by passing the port number as the first argument and the value as the second. The default maximum capacity of a port is 50, but this can be changed in **Options > System**. Setting this too high can cause the game to use a lot of memory. + +### Important + +The data inside ports are **not saved**! If you close and re-open the game or reload the page, all data in the ports will be lost. + +### Example Usage + +Here's a brief example of how ports work. For simplicity, we'll only deal with port 1. + +Let's assume Port 1 starts out empty: + +```plaintext +[] +``` + +Now assume we run the following script: + +```javascript +export async function main(ns) { + for (var i = 0; i < 10; ++i) { + ns.writePort(1, i); // Writes the value of i to port 1 + } +} +``` + +After execution, Port 1 will contain: + +```plaintext +[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +``` + +Now, running the following script: + +```javascript +export async function main(ns) { + for (var i = 0; i < 3; ++i) { + ns.print(ns.readPort(1)); // Reads a value from port 1 and prints it + } +} +``` + +Will print: + +```plaintext +0 +1 +2 +``` + +And the remaining data in Port 1 will be: + +```plaintext +[3, 4, 5, 6, 7, 8, 9] +``` + +### Warning + +In **NetscriptJS**, do not try writing base Promises to a port. + +## Port Handles + +### Warning + +Port Handles only work in **NetscriptJS**. They do not work in **Netscript1**. + +The `getPortHandle` Netscript function can be used to get a handle to a Netscript Port. This handle allows access to several new port-related functions: + +### Methods + +#### `NetscriptPort.writePort(data)` + +- **Parameter:** `data` - Data to write to the port +- **Returns:** If the port is full, the item that is removed from the port is returned. Otherwise, `null` is returned. +- Works the same as the Netscript function `write`. + +#### `NetscriptPort.tryWritePort(data)` + +- **Parameter:** `data` - Data to try to write to the port +- **Returns:** `true` if the data is successfully written, `false` otherwise. +- If the port is full, the data will **not** be written. + +#### `NetscriptPort.full()` + +- **Returns:** `true` if the Netscript Port is full, `false` otherwise. + +#### `NetscriptPort.empty()` + +- **Returns:** `true` if the Netscript Port is empty, `false` otherwise. + +#### `NetscriptPort.clear()` + +- Clears all data from the port. +- Works the same as the Netscript function `clear`. + +### Port Handle Example + +```javascript +export async function main(ns) { + port = ns.getPortHandle(5); + back = port.data.pop(); // Get and remove last element in port + + // Wait for port data before reading + while (port.empty()) { + await ns.sleep(10000); + } + res = port.read(); + + // Wait for room in the port before writing + while (!port.tryWrite(5)) { + await ns.sleep(5000); + } + + // Successfully wrote to port! +} +``` + +## Comments + +Netscript supports comments using JavaScript syntax. Comments are ignored by the interpreter and can be used to document code: + +```javascript +// This is a comment and will not get executed +/* Multi-line + * comment */ +ns.print("This code will be executed"); +``` + +## Importing Functions + +In Netscript, you can import functions declared in other scripts. The script will incur the RAM usage of all imported functions. There are two ways to do this: + +```javascript +import * as namespace from "script filename"; // Import all functions +import {fn1, fn2, ...} from "script filename"; // Import specific functions +``` + +### Example + +Consider a library script called `testlibrary.js`: + +```javascript +export function foo1(args) { + // function definition... +} + +export function foo2(args) { + // function definition... +} + +export async function foo3(args) { + // function definition... +} + +export function foo4(args) { + // function definition... +} + +export async function main(ns) { + // main function definition, can be empty but must exist... +} +``` + +To use these functions in another script: + +```javascript +import * as testlib from "testlibrary.js"; + +export async function main(ns) { + const values = [1,2,3]; + + // Use imported functions with the namespace + const someVal1 = await testlib.foo3(...values); + const someVal2 = testlib.foo1(values[0]); + + if (someVal1 > someVal2) { + // ... + } else { + // ... + } +} +``` + +To import only certain functions and save RAM: + +```javascript +import {foo1, foo3} from "testlibrary.js"; // Saves RAM + +export async function main(ns) { + const values = [1,2,3]; + + // No namespace needed + const someVal1 = await foo3(...values); + const someVal2 = foo1(values[1]); + + if (someVal1 > someVal2) { + // ... + } else { + // ... + } +} +``` + +### Warning + +The `export` keyword **cannot** be used in **Netscript1** as it's not supported. It can, however, be used in **NetscriptJS** (but it's not required). + +## Standard JavaScript Objects + +Standard built-in JavaScript objects such as `Math`, `Date`, `Number`, and others are supported based on the Netscript version you are using: + +- **Netscript1**: Supports built-in objects defined in ES5. +- **NetscriptJS**: Supports objects based on your browser's JavaScript engine. + diff --git a/src/Documentation/doc/advanced/netscriptscriptarguments.md b/src/Documentation/doc/advanced/netscriptscriptarguments.md new file mode 100644 index 0000000000..d114c7940c --- /dev/null +++ b/src/Documentation/doc/advanced/netscriptscriptarguments.md @@ -0,0 +1,55 @@ +# Netscript Script Arguments + +Arguments passed into a script can be accessed in Netscript using a special array called `args`. The arguments can be accessed using a normal array using the `[]` operator (`args[0]`, `args[1]`, etc.). These arguments can be of type `string`, `number`, or `boolean`. + +For example, let's say we want to make a generic script `generic-run.script` and we plan to pass two arguments into that script. The first argument will be the name of another script, and the second argument will be a number. This generic script will run the script specified in the first argument with the number of threads specified in the second argument. The code would look like: + +```javascript +var fileName = args[0]; +var threads = args[1]; +run(fileName, threads); +``` + +And it could be run from the terminal like: + +``` +run generic-run.script myscript.script 7 +``` + +### Netscript 2 (.js / ns2) Version + +In `.js / ns2`, the above script would look like: + +```javascript +export async function main(ns) { + let fileName = ns.args[0]; + let threads = ns.args[1]; + ns.run(fileName, threads); +} +``` + +### Getting the Number of Arguments + +It is also possible to get the number of arguments that were passed into a script using `args.length`. + +For example, if we want to create a script `foo.js` that takes 2 arguments: +- A string to print +- A number of times to print that string + +The code would look like: + +```javascript +export async function main(ns) { + for (let i = 0; i < ns.args[1]; i++) { + ns.tprint(ns.args[0]); + } +} +``` + +Then we can have another script launch `foo.js` with the two arguments like: + +```javascript +export async function main(ns) { + ns.exec("foo.js", "n00dles", 1, "this will be printed twice", 2); +} +``` diff --git a/src/Documentation/doc/advanced/shortcuts.md b/src/Documentation/doc/advanced/shortcuts.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Terminal/ui/TerminalInput.tsx b/src/Terminal/ui/TerminalInput.tsx index 77de9882c8..3d12a27c11 100644 --- a/src/Terminal/ui/TerminalInput.tsx +++ b/src/Terminal/ui/TerminalInput.tsx @@ -13,6 +13,16 @@ import { longestCommonStart } from "../../utils/StringHelperFunctions"; const useStyles = makeStyles()((theme: Theme) => ({ input: { backgroundColor: theme.colors.backgroundprimary, + border: "2px solid transparent", + borderRadius: "4px", + padding: theme.spacing(1), + fontSize: "1rem", + color: '#04ce04', + transition: "border-color 0.2s ease, box-shadow 0.2s ease", + }, + focusedInput: { + borderColor: theme.palette.primary.main, // Highlights color when focused + boxShadow: `0 0 5px ${theme.palette.primary.main}`, // Subtle glow effect }, nopadding: { padding: theme.spacing(0), @@ -44,6 +54,7 @@ export function TerminalInput(): React.ReactElement { const [searchResults, setSearchResults] = useState([]); const [searchResultsIndex, setSearchResultsIndex] = useState(0); const [autofilledValue, setAutofilledValue] = useState(false); + const [isFocused, setIsFocused] = useState(false); // New state for focus feedback const { classes } = useStyles(); // If we have no data in the current terminal history, let's initialize it from the player save @@ -70,6 +81,14 @@ export function TerminalInput(): React.ReactElement { } } + function handleFocus(): void { + setIsFocused(true); // Set focus state to true + } + + function handleBlur(): void { + setIsFocused(false); // Set focus state to false + } + function handleValueChange(event: React.ChangeEvent): void { saveValue(event.target.value); setPossibilities([]); @@ -77,10 +96,11 @@ export function TerminalInput(): React.ReactElement { setAutofilledValue(false); } - function resetSearch(isAutofilled = false) { + function resetSearch(clearFocus?: boolean): void { + //Edited to ensure resetSearch clears all autocomplete and search states setSearchResults([]); - setAutofilledValue(isAutofilled); setSearchResultsIndex(0); + if (clearFocus) setIsFocused(false); } function getSearchSuggestionPrespace() { @@ -209,16 +229,35 @@ export function TerminalInput(): React.ReactElement { async function onKeyDown(event: React.KeyboardEvent): Promise { const ref = terminalInput.current; - // Run command or insert newline + // Execute command or insert newline when Enter is pressed if (event.key === KEY.ENTER) { event.preventDefault(); + if (!ref) return; + const command = searchResults.length ? searchResults[searchResultsIndex] : value; + + // Print the command to the terminal Terminal.print(`[${Player.getCurrentServer().hostname} /${Terminal.cwd()}]> ${command}`); - if (command) { - Terminal.executeCommands(command); - saveValue(""); - resetSearch(); + + if (command.trim()) { + try { + // Execute the command + Terminal.executeCommands(command); + + // Clear the input field after execution + saveValue(""); + resetSearch(); + } catch (error) { + console.error("Error executing terminal command:", error); + Terminal.print("Error executing command. Check console for details."); + } + } else { + // Handle empty command, for example display a hint or do nothing + Terminal.print("No command entered."); } + + // Ensure terminal remains focused after execution + if (ref) ref.focus(); return; } @@ -251,6 +290,9 @@ export function TerminalInput(): React.ReactElement { if (event.key === KEY.L && event.ctrlKey) { event.preventDefault(); Terminal.clear(); + + // Ensure focus feedback remains after clearing + if (ref) ref.focus(); } // Select previous command. @@ -405,34 +447,39 @@ export function TerminalInput(): React.ReactElement { <> - [{Player.getCurrentServer().hostname} /{Terminal.cwd()}]>  - - ), - spellCheck: false, - onBlur: () => { - setPossibilities([]); - resetSearch(); - }, - onKeyDown: (event) => { - onKeyDown(event).catch((error) => { - console.error(error); - }); - }, - }} + color={Terminal.action === null ? "primary" : "secondary"} + autoFocus + disabled={Terminal.action !== null} + autoComplete="off" + value={value} + classes={{ root: `${classes.input} ${isFocused ? classes.focusedInput : ""}` }} + onChange={handleValueChange} + inputRef={terminalInput} + InputProps={{ + // for players to hook in + id: "terminal-input", + className: classes.input, + startAdornment: ( + + [{Player.getCurrentServer().hostname} /{Terminal.cwd()}]>  + + ), + spellCheck: false, + onBlur: () => { + setIsFocused(false); // Clear focus feedback on blur + setPossibilities([]); + resetSearch(); + }, + onFocus: () => { + setIsFocused(true); // Indicate terminal is focused + }, + onKeyDown: (event) => { + onKeyDown(event).catch((error) => { + console.error(error); + }); + }, + tabIndex: 0, + }} > 0} diff --git a/src/ui/ActiveScripts/ActiveScriptsPage.tsx b/src/ui/ActiveScripts/ActiveScriptsPage.tsx index c7a2f8df83..7f874005a5 100644 --- a/src/ui/ActiveScripts/ActiveScriptsPage.tsx +++ b/src/ui/ActiveScripts/ActiveScriptsPage.tsx @@ -146,7 +146,7 @@ export function ActiveScriptsPage(): React.ReactElement { {dataToShow.map(([server, scripts]) => ( - + ))} diff --git a/src/ui/ActiveScripts/ServerAccordion.tsx b/src/ui/ActiveScripts/ServerAccordion.tsx index 2f8e966086..0af7798449 100644 --- a/src/ui/ActiveScripts/ServerAccordion.tsx +++ b/src/ui/ActiveScripts/ServerAccordion.tsx @@ -13,13 +13,13 @@ import { createProgressBarText } from "../../utils/helpers/createProgressBarText interface ServerAccordionProps { server: BaseServer; scripts: WorkerScript[]; + tabIndex?: number; // Ensure tabIndex is part of the props } -export function ServerAccordion({ server, scripts }: ServerAccordionProps): React.ReactElement { +export function ServerAccordion({ server, scripts, tabIndex }: ServerAccordionProps): React.ReactElement { const [open, setOpen] = React.useState(false); // Accordion's header text - // TODO: calculate the longest hostname length rather than hard coding it const longestHostnameLength = 18; const paddedName = `${server.hostname}${" ".repeat(longestHostnameLength)}`.slice( 0, @@ -33,13 +33,15 @@ export function ServerAccordion({ server, scripts }: ServerAccordionProps): Reac return ( - setOpen((old) => !old)}> + {/* Make ListItemButton focusable with tabIndex */} + setOpen((old) => !old)} tabIndex={tabIndex}> {headerTxt}} /> {open ? : } - + {/* Pass tabIndex to ServerAccordionContent */} + diff --git a/src/ui/ActiveScripts/ServerAccordionContent.tsx b/src/ui/ActiveScripts/ServerAccordionContent.tsx index a21316c6af..e4987d4090 100644 --- a/src/ui/ActiveScripts/ServerAccordionContent.tsx +++ b/src/ui/ActiveScripts/ServerAccordionContent.tsx @@ -9,16 +9,19 @@ import { FirstPage, KeyboardArrowLeft, KeyboardArrowRight, LastPage } from "@mui interface ServerActiveScriptsProps { scripts: WorkerScript[]; + tabIndex?: number; // Added tabIndex prop here } export function ServerAccordionContent({ scripts }: ServerActiveScriptsProps): React.ReactElement { const [page, setPage] = useState(0); + if (scripts.length === 0) { console.error(`Attempted to display a server in active scripts when there were no matching scripts to show`); return <>; } const scriptsPerPage = Settings.ActiveScriptsScriptPageSize; const lastPage = Math.ceil(scripts.length / scriptsPerPage) - 1; + function changePage(n: number) { if (!Number.isInteger(n) || n > lastPage || n < 0) return; setPage(n); @@ -35,7 +38,9 @@ export function ServerAccordionContent({ scripts }: ServerActiveScriptsProps): R component="span" marginRight="auto" >{`Displaying scripts ${firstScriptNumber}-${lastScriptNumber} of ${scripts.length}`} - changePage(0)} disabled={page === 0}> + + {/* Navigation buttons */} + changePage(0)} disabled={page === 0} tabIndex={0} aria-label="First Page"> changePage(page - 1)} disabled={page === 0}> @@ -48,9 +53,11 @@ export function ServerAccordionContent({ scripts }: ServerActiveScriptsProps): R + + {/* Scripts list */} {scripts.slice(page * scriptsPerPage, page * scriptsPerPage + scriptsPerPage).map((ws) => ( - + ))} diff --git a/src/ui/ActiveScripts/WorkerScriptAccordion.tsx b/src/ui/ActiveScripts/WorkerScriptAccordion.tsx index bf9311d797..497e41408d 100644 --- a/src/ui/ActiveScripts/WorkerScriptAccordion.tsx +++ b/src/ui/ActiveScripts/WorkerScriptAccordion.tsx @@ -42,6 +42,7 @@ const useStyles = makeStyles()({ interface IProps { workerScript: WorkerScript; + tabIndex?: number; // Added this prop to accept tabIndex } export function WorkerScriptAccordion(props: IProps): React.ReactElement { @@ -65,7 +66,8 @@ export function WorkerScriptAccordion(props: IProps): React.ReactElement { return ( <> - setOpen((old) => !old)} component={Paper}> + {/* Pass tabIndex to ListItemButton to make it focusable via keyboard */} + setOpen((old) => !old)} component={Paper} tabIndex={props.tabIndex}>