diff --git a/src/Hacknet/HacknetServer.ts b/src/Hacknet/HacknetServer.ts index bd71cbb53..f72fea47b 100644 --- a/src/Hacknet/HacknetServer.ts +++ b/src/Hacknet/HacknetServer.ts @@ -53,6 +53,8 @@ export class HacknetServer extends BaseServer implements IHacknetNode { // Flag indicating whether this is a purchased server purchasedByPlayer = true; + isHacknetServer = true; + constructor(params: IConstructorParams = { hostname: "", ip: createRandomIp() }) { super(params); diff --git a/src/Server/BaseServer.ts b/src/Server/BaseServer.ts index 884d2d7d2..f371cd89d 100644 --- a/src/Server/BaseServer.ts +++ b/src/Server/BaseServer.ts @@ -122,6 +122,7 @@ export abstract class BaseServer implements IServer { openPortCount?: number; requiredHackingSkill?: number; serverGrowth?: number; + isHacknetServer?: boolean; constructor(params: IConstructorParams = { hostname: "", ip: createRandomIp() }) { this.ip = params.ip ? params.ip : createRandomIp(); @@ -367,6 +368,6 @@ export abstract class BaseServer implements IServer { // Customize a prune list for a subclass. static getIncludedKeys(ctor: new () => T): readonly (keyof T)[] { - return getKeyList(ctor, { removedKeys: ["runningScriptMap", "savedScripts", "ramUsed"] }); + return getKeyList(ctor, { removedKeys: ["runningScriptMap", "savedScripts", "ramUsed", "isHacknetServer"] }); } } diff --git a/src/ui/ActiveScripts/ActiveScriptsPage.tsx b/src/ui/ActiveScripts/ActiveScriptsPage.tsx index 88f3cf5f1..c7a2f8df8 100644 --- a/src/ui/ActiveScripts/ActiveScriptsPage.tsx +++ b/src/ui/ActiveScripts/ActiveScriptsPage.tsx @@ -1,4 +1,6 @@ import type { WorkerScript } from "../../Netscript/WorkerScript"; +import type { BaseServer } from "../../Server/BaseServer"; + import React, { useState } from "react"; import { MenuItem, Typography, Select, SelectChangeEvent, TextField, IconButton, List } from "@mui/material"; @@ -8,9 +10,9 @@ import { ScriptProduction } from "./ScriptProduction"; import { ServerAccordion } from "./ServerAccordion"; import { workerScripts } from "../../Netscript/WorkerScripts"; -import { getRecordEntries } from "../../Types/Record"; import { Settings } from "../../Settings/Settings"; import { isPositiveInteger } from "../../types"; +import { SpecialServers } from "../../Server/data/SpecialServers"; export function ActiveScriptsPage(): React.ReactElement { const [scriptsPerPage, setScriptsPerPage] = useState(Settings.ActiveScriptsScriptPageSize); @@ -31,24 +33,55 @@ export function ActiveScriptsPage(): React.ReactElement { setServersPerPage(n); } - const serverData: [string, WorkerScript[]][] = (() => { - const tempData: Record = {}; + // Creating and sorting the server data array is done here + const serverData: [BaseServer, WorkerScript[]][] = (() => { + const tempData: Map = new Map(); if (filter) { // Only check filtering if a filter exists (performance) for (const ws of workerScripts.values()) { if (!ws.hostname.includes(filter) && !ws.scriptRef.filename.includes(filter)) continue; - const hostname = ws.hostname; - if (tempData[hostname]) tempData[hostname].push(ws); - else tempData[hostname] = [ws]; + const server = ws.getServer(); + const serverScripts = tempData.get(server); + if (serverScripts) serverScripts.push(ws); + else tempData.set(server, [ws]); } } else { for (const ws of workerScripts.values()) { - const hostname = ws.hostname; - if (tempData[hostname]) tempData[hostname].push(ws); - else tempData[hostname] = [ws]; + const server = ws.getServer(); + const serverScripts = tempData.get(server); + if (serverScripts) serverScripts.push(ws); + else tempData.set(server, [ws]); } } - return getRecordEntries(tempData); + // serverData will be based on a sorted array from the temporary Map + return [...tempData].sort(([serverA], [serverB]) => { + // Servers not owned by the player are equal for sorting. Earliest return because it is the most common comparison. + if (!serverA.purchasedByPlayer && !serverB.purchasedByPlayer) return 0; + // Servers owned by the player come earlier in the sorting + if (serverA.purchasedByPlayer && !serverB.purchasedByPlayer) return -1; + if (!serverA.purchasedByPlayer && serverB.purchasedByPlayer) return 1; + // If we have reached this point, then both servers are player owned + // Home is at the top + if (serverA.hostname === SpecialServers.Home) return -1; + if (serverB.hostname === SpecialServers.Home) return 1; + // Hacknet servers shown after home + if (serverA.isHacknetServer && !serverB.isHacknetServer) return -1; + if (!serverA.isHacknetServer && serverB.isHacknetServer) return 1; + // Sorting for hacknet servers is based on the numbered suffix + if (serverA.isHacknetServer) { + if (serverA.hostname.length < serverB.hostname.length) return -1; + if (serverA.hostname.length > serverB.hostname.length) return 1; + // Get the numbered suffix from the end of the server names + const numA = Math.abs(parseInt(serverA.hostname.slice(-2))); + const numB = Math.abs(parseInt(serverB.hostname.slice(-2))); + if (numA < numB) return -1; + return 1; + } + // Sorting for other purchased servers is alphabetical. There's probably a better way to do this. + const fakeArray = [serverA.hostname, serverB.hostname].sort(); + if (serverA.hostname === fakeArray[0]) return -1; + return 1; + }); })(); const lastPage = Math.max(Math.ceil(serverData.length / serversPerPage) - 1, 0); @@ -112,8 +145,8 @@ export function ActiveScriptsPage(): React.ReactElement { - {dataToShow.map(([hostname, scripts]) => ( - + {dataToShow.map(([server, scripts]) => ( + ))} diff --git a/src/ui/ActiveScripts/ServerAccordion.tsx b/src/ui/ActiveScripts/ServerAccordion.tsx index f4af65d38..2f8e96608 100644 --- a/src/ui/ActiveScripts/ServerAccordion.tsx +++ b/src/ui/ActiveScripts/ServerAccordion.tsx @@ -1,45 +1,29 @@ -/** - * React Component for rendering the Accordion element for a single - * server in the 'Active Scripts' UI page - */ +import type { WorkerScript } from "../../Netscript/WorkerScript"; +import type { BaseServer } from "../../Server/BaseServer"; + import * as React from "react"; -import Typography from "@mui/material/Typography"; +import { Box, Collapse, ListItemText, ListItemButton, Paper, Typography } from "@mui/material"; +import { ExpandMore, ExpandLess } from "@mui/icons-material"; -import ListItemButton from "@mui/material/ListItemButton"; -import ListItemText from "@mui/material/ListItemText"; - -import Paper from "@mui/material/Paper"; -import Box from "@mui/material/Box"; -import Collapse from "@mui/material/Collapse"; -import ExpandMore from "@mui/icons-material/ExpandMore"; -import ExpandLess from "@mui/icons-material/ExpandLess"; import { ServerAccordionContent } from "./ServerAccordionContent"; -import { WorkerScript } from "../../Netscript/WorkerScript"; - import { createProgressBarText } from "../../utils/helpers/createProgressBarText"; -import { GetServer } from "../../Server/AllServers"; interface ServerAccordionProps { - hostname: string; + server: BaseServer; scripts: WorkerScript[]; } -export function ServerAccordion({ hostname, scripts }: ServerAccordionProps): React.ReactElement { +export function ServerAccordion({ server, scripts }: ServerAccordionProps): React.ReactElement { const [open, setOpen] = React.useState(false); - const server = GetServer(hostname); - if (!server) { - console.error(`Invalid server ${hostname} while displaying active scripts`); - return <>; - } // Accordion's header text // TODO: calculate the longest hostname length rather than hard coding it const longestHostnameLength = 18; - const paddedName = `${hostname}${" ".repeat(longestHostnameLength)}`.slice( + const paddedName = `${server.hostname}${" ".repeat(longestHostnameLength)}`.slice( 0, - Math.max(hostname.length, longestHostnameLength), + Math.max(server.hostname.length, longestHostnameLength), ); const barOptions = { progress: server.ramUsed / server.maxRam,