mirror of
https://github.com/bitburner-official/bitburner-src.git
synced 2026-04-21 08:42:53 +02:00
336 lines
12 KiB
TypeScript
336 lines
12 KiB
TypeScript
import React, {
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
useMemo,
|
|
useCallback,
|
|
type PointerEventHandler,
|
|
type WheelEventHandler,
|
|
} from "react";
|
|
import { Container, Typography, Button, Box, Tooltip } from "@mui/material";
|
|
import { ZoomIn, ZoomOut } from "@mui/icons-material";
|
|
import { throttle } from "lodash";
|
|
import { ServerStatusBox } from "./ServerStatusBox";
|
|
import { useRerender } from "../../ui/React/hooks";
|
|
import { DarknetEvents, DarknetState } from "../models/DarknetState";
|
|
import { SpecialServers } from "../../Server/data/SpecialServers";
|
|
import { drawOnCanvas, getPixelPosition } from "./networkCanvas";
|
|
import { dnetStyles, DWServerLogStyles } from "./dnetStyles";
|
|
import { getLabyrinthDetails, isLabyrinthServer } from "../effects/labyrinth";
|
|
import { DarknetServer } from "../../Server/DarknetServer";
|
|
import { getAllDarknetServers } from "../utils/darknetNetworkUtils";
|
|
import { ServerDetailsModal } from "./ServerDetailsModal";
|
|
import { AutoCompleteSearchBox } from "../../ui/AutoCompleteSearchBox";
|
|
import { getDarknetServerOrThrow } from "../utils/darknetServerUtils";
|
|
import { getServerLogs } from "../models/packetSniffing";
|
|
import { getTimeoutChance } from "../effects/offlineServerHandling";
|
|
import { DocumentationLink } from "../../ui/React/DocumentationLink";
|
|
import { Settings } from "../../Settings/Settings";
|
|
|
|
const DW_NET_WIDTH = 6000;
|
|
const DW_NET_HEIGHT = 12000;
|
|
const initialSearchLabel = `Search:`;
|
|
|
|
export function NetworkDisplayWrapper(): React.ReactElement {
|
|
const rerender = useRerender();
|
|
const draggableBackground = useRef<HTMLDivElement>(null);
|
|
const canvas = useRef<HTMLCanvasElement>(null);
|
|
const [zoomIndex, setZoomIndex] = useState(DarknetState.zoomIndex);
|
|
const [netDisplayDepth, setNetDisplayDepth] = useState<number>(1);
|
|
const [searchLabel, setSearchLabel] = useState<string>(initialSearchLabel);
|
|
const [serverOpened, setServerOpened] = useState<DarknetServer | null>(null);
|
|
const zoomOptions = useMemo(() => [0.12, 0.2, 0.25, 0.3, 0.4, 0.5, 0.6, 0.75, 1, 1.3], []);
|
|
const { classes } = dnetStyles({});
|
|
const instability = getTimeoutChance();
|
|
const instabilityText = instability > 0.01 ? `${(instability * 100).toFixed(1)}%` : "< 1%";
|
|
|
|
const scrollTo = useCallback(
|
|
(top: number, left: number) => {
|
|
DarknetState.netViewTopScroll = top;
|
|
DarknetState.netViewLeftScroll = left;
|
|
|
|
draggableBackground?.current?.scrollTo({
|
|
top: top,
|
|
left: left,
|
|
behavior: "instant",
|
|
});
|
|
},
|
|
[draggableBackground],
|
|
);
|
|
|
|
const updateDisplay = useCallback(() => {
|
|
if (!canvas.current) {
|
|
return;
|
|
}
|
|
const visibilityMargin = DarknetState.showFullNetwork ? 99 : 3;
|
|
const lab = getLabyrinthDetails().lab;
|
|
const startingDepth = lab && getServerLogs(lab, 1, true).length ? lab.depth : 0;
|
|
const deepestServerDepth = DarknetState.Network.flat().reduce(
|
|
(deepest, server) => (server?.hasAdminRights && server.depth > deepest ? server.depth : deepest),
|
|
startingDepth,
|
|
);
|
|
setNetDisplayDepth(deepestServerDepth + visibilityMargin);
|
|
|
|
rerender();
|
|
drawOnCanvas(canvas.current);
|
|
}, [rerender]);
|
|
|
|
useEffect(() => {
|
|
const clearSubscription = DarknetEvents.subscribe(() => updateDisplay());
|
|
draggableBackground.current?.addEventListener("wheel", (e) => e.preventDefault(), { passive: false });
|
|
scrollTo(DarknetState.netViewTopScroll, DarknetState.netViewLeftScroll);
|
|
updateDisplay();
|
|
|
|
return () => {
|
|
clearSubscription();
|
|
};
|
|
}, [updateDisplay, rerender, scrollTo]);
|
|
|
|
const allowAuth = (server: DarknetServer | null) =>
|
|
!!server &&
|
|
(server.hasAdminRights ||
|
|
server.serversOnNetwork.some((neighbor) => getDarknetServerOrThrow(neighbor).hasAdminRights));
|
|
|
|
const darkWebRoot = getDarknetServerOrThrow(SpecialServers.DarkWeb);
|
|
const labDetails = getLabyrinthDetails();
|
|
const labyrinth = labDetails.lab;
|
|
const depth = labDetails.depth;
|
|
|
|
const handleDragStart: PointerEventHandler<HTMLDivElement> = (pointerEvent) => {
|
|
const target = pointerEvent.target as HTMLDivElement;
|
|
const background = draggableBackground.current;
|
|
if (target.id === "draggableBackgroundTarget") {
|
|
background?.setPointerCapture(pointerEvent.pointerId);
|
|
}
|
|
};
|
|
|
|
const handleDragEnd: PointerEventHandler<HTMLDivElement> = (pointerEvent) => {
|
|
const target = pointerEvent.target as HTMLDivElement;
|
|
const background = draggableBackground.current;
|
|
if (target.id === "draggableBackgroundTarget") {
|
|
background?.releasePointerCapture(pointerEvent.pointerId);
|
|
}
|
|
DarknetEvents.emit();
|
|
};
|
|
|
|
const handleDrag: PointerEventHandler<HTMLDivElement> = (pointerEvent) => {
|
|
const background = draggableBackground.current;
|
|
if (background?.hasPointerCapture(pointerEvent.pointerId)) {
|
|
scrollTo(background?.scrollTop - pointerEvent.movementY, (background?.scrollLeft ?? 0) - pointerEvent.movementX);
|
|
}
|
|
};
|
|
|
|
const changeZoom = useCallback(
|
|
(out = true, mouseX?: number, mouseY?: number) => {
|
|
if (out && zoomIndex <= 0) return;
|
|
if (!out && zoomIndex >= zoomOptions.length - 1) return;
|
|
const newZoomIndex = out ? zoomIndex - 1 : zoomIndex + 1;
|
|
const oldZoom = zoomOptions[zoomIndex];
|
|
const newZoom = zoomOptions[newZoomIndex];
|
|
DarknetState.zoomIndex = newZoomIndex;
|
|
setZoomIndex(newZoomIndex);
|
|
const background = draggableBackground.current;
|
|
const mx = mouseX ?? (background?.clientWidth ?? 0) / 2;
|
|
const my = mouseY ?? (background?.clientHeight ?? 0) / 2;
|
|
scrollTo(
|
|
(((background?.scrollTop ?? 0) + my) / oldZoom) * newZoom - my,
|
|
(((background?.scrollLeft ?? 0) + mx) / oldZoom) * newZoom - mx,
|
|
);
|
|
},
|
|
[zoomIndex, setZoomIndex, zoomOptions, scrollTo],
|
|
);
|
|
|
|
const zoom = useCallback(
|
|
(wheelEvent: WheelEvent) => {
|
|
if (!draggableBackground.current || DarknetState.openServer) return;
|
|
const rect = draggableBackground.current.getBoundingClientRect();
|
|
const mouseX = wheelEvent.clientX - rect.left;
|
|
const mouseY = wheelEvent.clientY - rect.top;
|
|
changeZoom(wheelEvent.deltaY > 0, mouseX, mouseY);
|
|
},
|
|
[draggableBackground, changeZoom],
|
|
);
|
|
|
|
const zoomRef = useRef(zoom);
|
|
|
|
useEffect(() => {
|
|
zoomRef.current = zoom;
|
|
}, [zoom]);
|
|
|
|
// creating throttled callback only once - on mount
|
|
const throttledZoom = useMemo(() => {
|
|
const func = (wheelEvent: WheelEvent) => {
|
|
zoomRef.current?.(wheelEvent);
|
|
};
|
|
return throttle(func, 200);
|
|
}, []);
|
|
|
|
const handleZoom: WheelEventHandler<HTMLDivElement> = (wheelEvent) => {
|
|
wheelEvent.stopPropagation();
|
|
throttledZoom(wheelEvent as unknown as WheelEvent);
|
|
};
|
|
|
|
const search = (selection: string, options: string[], searchTerm: string) => {
|
|
if (searchTerm.length === 1) {
|
|
return;
|
|
} // Ignore single character searches
|
|
if (!searchTerm) {
|
|
setSearchLabel(initialSearchLabel);
|
|
return;
|
|
}
|
|
const servers = getAllDarknetServers();
|
|
const foundServer =
|
|
servers.find((s) => s.hostname.toLowerCase() === selection.toLowerCase()) ||
|
|
servers.find((s) => s.hostname.toLowerCase() === options[0]?.toLowerCase());
|
|
|
|
if (!foundServer) {
|
|
setSearchLabel(`(No results)`);
|
|
return;
|
|
} else {
|
|
setSearchLabel(initialSearchLabel);
|
|
}
|
|
|
|
const position = getPixelPosition(foundServer, true);
|
|
|
|
const background = draggableBackground.current;
|
|
scrollTo(
|
|
position.top * zoomOptions[zoomIndex] - ((background?.clientHeight ?? 0) / 2 - 100),
|
|
position.left * zoomOptions[zoomIndex] - (background?.clientWidth ?? 0) / 2,
|
|
);
|
|
|
|
if (allowAuth(foundServer)) {
|
|
setServerOpened(foundServer);
|
|
}
|
|
};
|
|
|
|
const getAutocompleteSuggestionList = (): string[] => {
|
|
const servers = getAllDarknetServers()
|
|
.filter((s) => s.depth < netDisplayDepth && !isLabyrinthServer(s.hostname))
|
|
.map((s) => s.hostname);
|
|
|
|
if (labyrinth && netDisplayDepth > depth) {
|
|
return [...servers, labyrinth.hostname];
|
|
}
|
|
|
|
return servers;
|
|
};
|
|
|
|
return (
|
|
<Container maxWidth={false} disableGutters>
|
|
{serverOpened ? (
|
|
<ServerDetailsModal
|
|
open={!!serverOpened}
|
|
onClose={() => setServerOpened(null)}
|
|
server={serverOpened}
|
|
classes={classes}
|
|
/>
|
|
) : (
|
|
""
|
|
)}
|
|
{DarknetState.mutationLock ? (
|
|
<Typography variant={"h6"} className={classes.gold}>
|
|
[WEBSTORM WARNING]
|
|
</Typography>
|
|
) : (
|
|
<Box className={`${classes.inlineFlexBox}`}>
|
|
<Typography variant={"h5"} sx={{ fontWeight: "bold" }}>
|
|
Dark Net
|
|
</Typography>
|
|
{instability > 0 && (
|
|
<Tooltip
|
|
title={
|
|
<>
|
|
If too many darknet servers are backdoored, it will increase the chance that authentication <br />
|
|
attempts will return a 408 Request Timeout error (even if the password is correct). <br />
|
|
Most servers will eventually restart or go offline, which removes backdoors over time.
|
|
</>
|
|
}
|
|
>
|
|
<Typography variant={"subtitle1"} sx={{ fontStyle: "italic" }}>
|
|
{" "}
|
|
Instability: {instabilityText}
|
|
</Typography>
|
|
</Tooltip>
|
|
)}
|
|
</Box>
|
|
)}
|
|
|
|
<div
|
|
className={classes.NetWrapper}
|
|
ref={draggableBackground}
|
|
onPointerDown={handleDragStart}
|
|
onPointerUp={handleDragEnd}
|
|
onPointerMove={handleDrag}
|
|
onWheel={handleZoom}
|
|
>
|
|
<div
|
|
style={{
|
|
position: "relative",
|
|
width: `${DW_NET_WIDTH}px`,
|
|
height: `${DW_NET_HEIGHT}px`,
|
|
zoom: zoomOptions[zoomIndex],
|
|
cursor: "grab",
|
|
}}
|
|
id={"draggableBackgroundTarget"}
|
|
>
|
|
<canvas
|
|
ref={canvas}
|
|
width={DW_NET_WIDTH}
|
|
height={DW_NET_HEIGHT}
|
|
style={{ position: "absolute", zIndex: -1 }}
|
|
></canvas>
|
|
{darkWebRoot && <ServerStatusBox server={darkWebRoot} enableAuth={true} classes={classes} />}
|
|
{DarknetState.Network.slice(0, netDisplayDepth).map((row) =>
|
|
row.map(
|
|
(server) =>
|
|
!!server && (
|
|
<ServerStatusBox server={server} key={server.ip} enableAuth={allowAuth(server)} classes={classes} />
|
|
),
|
|
),
|
|
)}
|
|
|
|
{!!labyrinth && netDisplayDepth > depth && (
|
|
<ServerStatusBox server={labyrinth} enableAuth={allowAuth(labyrinth)} classes={classes} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className={classes.zoomContainer}>
|
|
<Button className={classes.button} onClick={() => changeZoom(false)}>
|
|
<ZoomIn />
|
|
</Button>
|
|
<Button className={classes.button} onClick={() => changeZoom()}>
|
|
<ZoomOut />
|
|
</Button>
|
|
</div>
|
|
<Box className={`${classes.inlineFlexBox}`} style={{ justifyContent: "flex-start", gap: "10px" }}>
|
|
<Typography component="div" display="flex">
|
|
<Typography display="flex" alignItems="center" paddingRight="1em">
|
|
{searchLabel}
|
|
</Typography>
|
|
<AutoCompleteSearchBox
|
|
placeholder="Search for server"
|
|
maxSuggestions={6}
|
|
suggestionList={getAutocompleteSuggestionList}
|
|
ignoredTextRegex={/ /g}
|
|
onSelection={(_, selection, options, searchValue) => {
|
|
search(selection, options, searchValue);
|
|
}}
|
|
width={300}
|
|
/>
|
|
</Typography>
|
|
<DocumentationLink
|
|
page="programming/darknet.md"
|
|
style={{
|
|
...DWServerLogStyles,
|
|
fontSize: "18px",
|
|
padding: "2px 15px",
|
|
backgroundColor: Settings.theme.well,
|
|
}}
|
|
>
|
|
Darknet Docs
|
|
</DocumentationLink>
|
|
</Box>
|
|
</Container>
|
|
);
|
|
}
|