下载客户端

调整用户界面

2026-02-15 19:00:07
发布在Bitburner
转载

AI智能总结导读

这是一篇关于Bitburner游戏的用户界面自定义指南,作者分享了自制的文件资源管理器GUI及配套辅助脚本,包含安装方法、制作思路与代码,还提及未来优化计划,可帮助玩家自定义游戏UI功能。

想要用户界面提供更多实用功能?自己动手制作吧! 我会展示一个我制作的文件资源管理器窗口示例(你知道的,就是用于处理文件的图形用户界面,类似Windows资源管理器,不要和Internet Explorer混淆),以及我为简化制作过程所开发的工具(辅助脚本)。 前言 首先,我们要归功于应得之人:我的灵感来源于Temorok的一篇指南。我最初的一些用户界面编辑脚本就是基于他指南中的脚本制作的。 免责声明 我知道这么大篇幅的脚本在Steam的代码块中显示效果不佳,我可能会在有时间且愿意的情况下创建一个GitHub仓库,将脚本放在那里,以便于查看。虽然我已经测试并运行了这些脚本(它们确实能正常工作),但我还没有对其进行整理,也没有检查是否有更好的实现方法。我想分享这段代码,是为了向你展示利用这款游戏的用户界面可以实现哪些功能,同时也想说明你可以创建自己的小型框架,让编写这些修改用户界面的脚本变得更加容易。 关于本指南 本指南的目的并非是要教你具体的知识,更多的是想告诉你可以做哪些事情,并展示我所做的一些尝试(希望这能激励你自己动手制作类似的东西)。安装文件资源管理器 如果你有兴趣使用我编写的文件资源管理器程序,以下是快速安装指南: 1. 复制粘贴下方包含的所有脚本,并将它们命名为我使用的名称(脚本上方的标题行即为我使用的名称)。你也可以重命名,但之后需要编辑代码以匹配新的文件名。 2. 同样复制粘贴所有“.txt”文件(保持相同名称)。 3. 设置别名,这样每次运行程序时就无需输入完整脚本(否则会影响易用性),我会在下方代码块中提供我使用的别名。 alias explorer="run /css/main.js; run /programs/exe/explorer."我已包含运行主CSS脚本的步骤,因此你无需单独运行(尽管每个游戏会话只需运行一次主CSS脚本)。启动时,文件资源管理器的显示应大致如下:

The primary color should be the color from your own color scheme, but I did use some hard coded colors as well, so to change those you should look into the CSS file. The making ofThe rest of this guide is less of a guide and more of a diary/report on what I did to make this 'program'. Feel free to look at the code, copy the code, reuse the code and so on. Though it would be nice if you would credit me when using (parts of) my code and presenting it to others. Plans for the futureI plan on improving this program (because I find it a usefull program with lots of potential) and will probably update this guide with newer versions of the program from time to time. I do however not plan to give (at least not a lot of) support for this program (i.e. I don't plan to go fixing bugs for other people). A heads upI ran into one bug, while making this program, that I could not reproduce: I was no longer able to write to the terminal (I could still loop through older commands and execute them, I just couldn't type any new ones). I had to restart the game to get rid of the bug, I got the bug while I was debugging some things in the debug tool, and I don't know when exactly the bug was caused. If you run into it as well, let me know, I'm still trying to find out what happened. It is however very likely that I simply did something wrong with my debugging and the bug is not caused by the script itself. What are the helper scripts I wrote a couple of helper scripts / frameworking tools: A marker based template parser A collection class for storing 'placeholder => value' pairs to use in the template parser A css gatherer / injector Marker based templatingI wrote a basic marker based template parser (marker based templates use placeholders like '###MONEY###') which supports basic loops. I'm planning to expand on it in the future to give better loop support and to also support simple if statements, but I didn't need those for my file explorer program, so I haven't put them in yet. The marker based templating means that I can easily write a html file (though the game forced me to change the file extension to '.html.txt' because it only allows '.txt', '.js' and '.ns'), which I personally find way easier to work with than building all html from javascript. Collection classI wrote the Collection class to make working with the template parser easier. It doesn't really do a lot, apart from automatically grouping placeholders into either a 'single-valued' group or a 'multi-valued' group, which is usefull for the template parsers loop system. CSS gatherer / injectorI wrote (well more rewrote, since it's a modified version of some of the code from the guide I linked at the start of this guide) a script to gather all CSS files (once again with a different extension because the game wouldn't let me use '.css' so I'm using '.css.txt' as my file extension for CSS files). This script gathers all CSS files present on the home server and turns the contents into a single string value and then injects that css into the game by adding a <style> element to the document object. Running commands from the terminalThough I haven't seperated this part into its own script (yet), it is supporting code that you would need for a lot of utility scripts. The code for this was provided in the documentation[bitburner.readthedocs.io], though I may have renamed some things. Making the file explorer script/program One day I decided it would be nice to have a GUI for browsing the files on my home server, because there are a lot of them by now (I also thought about making a project sidebar for the editor, but gave up on that idea for the time being, because it sounded a lot harder to make than a file explorer). Getting a hook into the documentWith the 'ns.alert()' function and the string "<div id='explorer-window'></div>" passed to the function as the argument, I managed to get both an overlaying window and a hook into that window. After that the window could be accessed by using: document.getElementById("explorer-window"); And to combat the ram requirement of 'document' (a whopping 25GB), I simply replaced that by: let doc = eval("document"); Making the file structureWith the help of 'ns.ls()' and some string manipulation I made a file structure. The file structure is a tree with files as leave nodes, that way I could easily print all relevant files and folders to the file-explorer while keeping irrelevant files and folders from it. Adding click event handlers to files and foldersBecause I know what classes and ids I've given the files and folders when making the template, I could easily fetch them from the DOM document with: let files = doc.getElementsByClassName("explorer-file"); let dirs = doc.getElementsByClassName("explorer-directory"); And from there I could add the event listeners to the file and folder DOM elements with a simple 'for..of' loop: for (let dir of dirs) { dir.addEventListener('dblclick', function (e) { selectedFiles = []; currentPath.push(dir.id); update(); }); } And a similar 'for..of' loop for the files. Adding buttonsThe HTML for the buttons could simply be added to the HTML-template and be given functionality by the script by looking the buttons up in the DOM document and adding event listeners: doc.getElementById("explorer-remove").addEventListener('click', function (e) { removeFiles(); }); doc.getElementById("explorer-open").addEventListener('click', function (e) { openFiles(); }); Adding the path and backlinksAdding the path with the backlinks was done a bit sloppier, because I haven't implemented a proper for loop in the template parser (a for loop with room for more than one placeholder inside of it). So the path with backlinks was added fully using javascript and then passed to the template as a whole: let currentPathText = "/<span class='explorer-back-link' data-target='0'>" + "root</span>/"; for (let index in currentPath) { currentPathText += "<span class='explorer-back-link' data-target='" + (parseInt(index) + 1) + "'>" + currentPath[index] + "</span>/"; } data.add("PATH", currentPathText); Possible additionsI've been thinking about what things to add to the file-explorer that might be usefull. To name a few: Adding a list view with file sizes Adding drag and drop file renaming (moving files to folders) Adding file renaming (and possible folder renaming, a.k.a. mass file renaming) Adding minimizing and closing buttons to the window (probably means using a log window instead of an alert window) (code) Helper scripts / frameworking tools /programs/data/collection.js/** * Collection class * A collection of data for marker based templating */ export class Collection { #data; /** * Collection constructor */ constructor() { this.#data = {}; } /** * @return {{string: string|number}} */ get single() { let single = {}; for (let item in this.#data) { if (typeof this.#data[item] !== "object") { single[item] = this.#data[item]; } } return single; } /** * @return {{string: string[]|number[]}} */ get multi() { let multi = {}; for (let item in this.#data) { if (typeof this.#data[item] === "object") { multi[item] = this.#data[item]; } } return multi; } /** * Add a key-value pair to the data * Replaces existing key if it matches new key */ add(key, value) { this.#data[key] = value; } } /programs/template/parse.jsimport { Collection } from "/programs/data/collection.js"; /** * TemplateParse class * Parser for a marker based template */ export class TemplateParser { #html; /** * @param {NS} ns * @param {string} template */ constructor(ns, template) { this.#html = ns.read(template); } /** * Parse the template for the given data * @param {Collection} data * @return {string} */ parse(data) { let html = this.#html; // Parse all single-valued data items let single = data.single; for (let placeholder in single) { let regex = new RegExp(" # # #" + placeholder + " # # #", "g"); html = html.replace(regex, single[placeholder]).trim(); } // Parse all multi-valued data items let multi = data.multi; for (let placeholder in multi) { // Build a regular expression for the placeholder let regex = new RegExp(" # # #START LOOP " + placeholder + " # # #(.*) # # #END LOOP " + placeholder + " # # #", "s"); // Get the inner template for the placeholder let innerTemplate = html.match(regex)[1]; // Build the resulting html string let htmlResult = ""; for (let value of multi[placeholder]) { let regexInner = new RegExp(" # # #" + placeholder + " # # #", "g"); htmlResult += innerTemplate.replace(regexInner, value).trim(); } // Replace the inner template for the resulting // html string in the main html template html = html.replace(regex, htmlResult); } // Return the parsed template return html; } } /css/main.js/** @param {NS} ns **/ export async function main(ns) { // Get a reference to the document let doc = eval("document"); // Build the css let css = ""; for (let cssFile of ns.ls("home", ".css.txt")) { css += ns.read(cssFile); } // Add the css to the game let styleDiv = doc.getElementById('myCustomStyles'); if (!styleDiv) { // Make a new new div styleDiv = doc.createElement("div"); styleDiv.id = 'myCustomStyles'; doc.getElementsByTagName('head')[0].appendChild(styleDiv); } styleDiv.innerHTML = "<style>" + css + "</style>"; } (code) File explorer script /programs/exe/explorer.jsimport { TemplateParser } from "/programs/template/parse.js"; import { Collection } from "/programs/data/collection.js" /** @param {NS} ns **/ export async function main(ns) { var doc = eval("document"); let template = "/programs/html/explorer.html.txt"; var parser = new TemplateParser(ns, template); var fileStructure = { dirs: {}, files: [] }; let files = ns.ls("home"); for (let file of files) { let filePath = file.split("/"); let struct = fileStructure; while (filePath.length > 0) { let part = filePath.shift(); if (part !== "") { if (filePath.length > 0) { if (struct.dirs[part] === undefined) { struct.dirs[part] = { dirs: {}, files: [] }; } struct = struct.dirs[part]; } else { struct.files.push(part); } } } } ns.alert("<div id='explorer-window' class='explorer-window'></div>"); var explorerWindow = doc.getElementById("explorer-window"); var currentPath = []; var selectedFiles = []; function runCommandInTerminal(command) { // Get the terminal input field from the DOM const terminal = doc.getElementById("terminal-input"); // Print the command to the terminal input field terminal.value = command; // Get a reference to the React event handler. const handler = Object.keys(terminal)[1]; // Perform an onChange event to set some internal values. terminal[handler].onChange({ target: terminal }); // Simulate an enter press terminal[handler].onKeyDown({ keyCode: 13, preventDefault: () => null }); } function openFiles() { let command = "nano"; for (let file of selectedFiles.values()) { command += " " + file; } runCommandInTerminal(command); } async function removeFiles() { let command = ""; for (let file of selectedFiles) { command += "rm " + file + ";"; } runCommandInTerminal(command); } function toggleSelected(file) { if (currentPath.length > 0) { file = "/" + currentPath.join("/") + "/" + file; } else { file = "/" + file; } if (selectedFiles.includes(file)) { selectedFiles.splice( selectedFiles.indexOf(file), 1 ); } else { selectedFiles.push(file); } } function update() { // Make a new data collection for template parsing let data = new Collection(); // Find the currently selected folder let struct = fileStructure; for (let part of currentPath) { struct = struct.dirs[part]; } // Add data for parsing to collection data.add("FILE", struct.files); data.add("DIR", Object.keys(struct.dirs)); let currentPathText = "/<span class='explorer-back-link' data-target='0'>" + "root</span>/"; for (let index in currentPath) { currentPathText += "<span class='explorer-back-link' data-target='" + (parseInt(index) + 1) + "'>" + currentPath[index] + "</span>/"; } data.add("PATH", currentPathText); let selectedFilesText = ""; if (selectedFiles.length > 0) { selectedFilesText = "[" + selectedFiles.join(", ") + "]"; } data.add("FILES SELECTED", selectedFilesText); // Parse the template html let html = "Something went wrong!"; try { html = parser.parse(data); } catch (e) { console.log(e); } // Update the explorer window inner html explorerWindow.innerHTML = html; // Make new listeners let dirs = doc.getElementsByClassName("explorer-directory"); for (let dir of dirs) { dir.addEventListener('dblclick', function (e) { selectedFiles = []; currentPath.push(dir.id); update(); }); } let selectedFilesTextDiv = doc.getElementById("explorer-files-selected"); let files = doc.getElementsByClassName("explorer-file"); for (let file of files) { file.addEventListener('click', function (e) { file.classList.toggle("selected"); toggleSelected(file.id); let selectedFilesText = ""; if (selectedFiles.length > 0) { selectedFilesText = "[" + selectedFiles.join(", ") + "]"; } selectedFilesTextDiv.innerHTML = selectedFilesText; }); } let backLinks = doc.getElementsByClassName("explorer-back-link"); for (let backLink of backLinks) { backLink.addEventListener('click', function (e) { currentPath.splice(backLink.dataset.target, currentPath.length); update(); }); } doc.getElementById("explorer-remove").addEventListener('click', function (e) { removeFiles(); }); doc.getElementById("explorer-open").addEventListener('click', function (e) { openFiles(); }); } update(); } (code) HTML template and CSS file /programs/html/explorer.html.txt<div class='explorer-path'>###PATH###</div> <div class='explorer-content'> ###START LOOP DIR### <div class='explorer-directory' id='###DIR###'> <svg class='primary-color explorer-dir-image' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><!-- Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="currentColor" d="M527.9 224H480v-48c0-26.5-21.5-48-48-48H272l-64-64H48C21.5 64 0 85.5 0 112v288c0 26.5 21.5 48 48 48h400c16.5 0 31.9-8.5 40.7-22.6l79.9-128c20-31.9-3-73.4-40.7-73.4zM48 118c0-3.3 2.7-6 6-6h134.1l64 64H426c3.3 0 6 2.7 6 6v42H152c-16.8 0-32.4 8.8-41.1 23.2L48 351.4zm400 282H72l77.2-128H528z"/></svg> <span class='explorer-dir-name'>###DIR###</span> </div> ###END LOOP DIR### ###START LOOP FILE### <div class='explorer-file' id='###FILE###'> <svg class='primary-color explorer-file-image' xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!-- Font Awesome Free 5.15.3 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) --><path fill="currentColor" d="M369.9 97.9L286 14C277 5 264.8-.1 252.1-.1H48C21.5 0 0 21.5 0 48v416c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V131.9c0-12.7-5.1-25-14.1-34zM332.1 128H256V51.9l76.1 76.1zM48 464V48h160v104c0 13.3 10.7 24 24 24h104v288H48z"/></svg> <span class='explorer-file-name'>###FILE###</span> </div> ###END LOOP FILE### </div> <div class='explorer-files-selected' id='explorer-files-selected'> ###FILES SELECTED### </div> <div class='explorer-buttons'> <button id='explorer-remove' class='css-18nrgbo'>remove</button> <button id='explorer-open' class='css-18nrgbo'>open files</button> </div> /programs/css/explorer.css.txt.explorer-window { width: 800px; height: 100%; white-space: normal; } .explorer-path, .explorer-files-selected { min-height: 24px; margin: 10px 0; padding: 10px; border: 1px solid currentColor; } .explorer-dir-name, .explorer-file-name { width: 100%; text-align: center; display: block; overflow: auto; max-height: 22px; text-overflow: ellipsis; white-space: nowrap; font-family: "Lucida Console", "Lucida Sans Unicode", "Fira Mono", Consolas, "Courier New", Courier, monospace, "Times New Roman"; } .explorer-directory, .explorer-file { width: calc(20% - 20px); margin: 10px; height: 160px; padding: 0; background-color: #272727; cursor: pointer; } .explorer-directory:hover, .explorer-file:hover, .explorer-directory.selected, .explorer-file.selected { border: 1px solid currentColor; width: calc(20% - 22px); height: 158px; } .explorer-dir-image, .explorer-file-image { max-width: 80px; max-height: 80px; display: block; margin-left: auto; margin-right: auto; } .explorer-content { display: flex; justify-content: start; flex-wrap: wrap; height: 440px; overflow: scroll; border: 2px solid; width: calc(100% - 4px); white-space: pre-line; } .explorer-buttons { display: flex; } .explorer-buttons button { margin-right: 7px; } .explorer-back-link:hover { text-decoration: underline; } .explorer-back-link { cursor: pointer; }

评论

共0条评论
face
inputImg
相关阅读
最新更新

最新更新

  • Bitburner 新手合约指南 — A short guide and simple working scripts for solving Bitburner Coding Contracts.…
  • Bitburner 新手脚本指南 — 这是我为《网络黑客》创建/找到的中级脚本集合。它们基于我之前指南中的许多脚本。 查找元数据 元数据基本上是无法通过数据库或数学函数推导的数据。我构建了一个元数据…
  • 为HUD(平视显示器)添加自定义数据 — 使用内置的隐藏钩子和少量脚本自定义你的状态显示界面。 探索发现 本游戏鼓励你超越用户界面和文档进行探索。你可以检查文档对象模型,甚至查看源代码本身。当你开始跳出…
  • 我的《网络骇客》新手笔记、批处理脚本与部署方案 — Useful for beginners, this guide provides: -The Tutorial Commands in order -Cont…
  • 派系与强化 — I asked for this. ㅤ * acquired only from this faction, grafting and certain gang…
  • 基础黑客网络管理器 — 一个用于管理你的黑客网络的基础代码,同时尽量避免花费过多资金 使用方法: 创建一个.js文件或.ns文件,这两种都可以,.script文件使用不同的标准,所以不…
  • 实用脚本【备份】 — I need some place to keep my very useful scripts. Feel free to copy them! Some o…
  • 简易黑客网络管理器 — 一个简单的黑客网络管理器,可高效为你购买所有黑客网络升级。 设置步骤: 1. 创建一个.js文件并粘贴以下代码 2. 输入命令:nano nameOfYourF…
  • 一些实用的别名 — 早期游戏中的一些实用别名 别名 正如游戏内帮助所述,别名功能允许用一个字符串替换另一个单词。游戏还提供了示例:alias "nuke=run NUKE.exe"…
  • 《比特燃烧者》新手黑客指南 — A short & comprehensive guide including simple working scripts for beginning…