BitBurner settings.common.js
unknown
javascript
2 years ago
18 kB
29
Indexable
import { SettingsDomElement } from "settings.react.common.js" // This script uses globalThis["document"] to get the Document instance, which we use to create HTML elements. // We do it this way, because BitBurner already has the capability to create DOM elements through React (see React.createElement and ns.printRaw). // If you consider this as cheating just uncomment the next line: //document; // Set this to 0 to trigger instant reloads without a setInterval FileWatcher // This will use the globalThis["document"] Document instance to dispatch an event. const autoLoadCheckInterval = 100; export class SettingsInfo { constructor(name, schema) { this._name = name; this._schema = schema; } get schema() { return this._schema; } get name() { return this._name; } create(ns) { return Settings.fromSchema(this.schema, `settingsData/${this.name}.txt`, ns); } } export class Settings extends EventTarget { /** @param {NS} ns */ static fromSchema(schema, settingsFilePathOrNs, ns) { let settingsFilePath; if (typeof settingsFilePathOrNs === "object") ns = settingsFilePathOrNs; else settingsFilePath = settingsFilePathOrNs; settingsFilePath = settingsFilePath ?? schema.settingsFilePath; const result = new Settings({ properties: schema, }); if (ns) { result._ns = ns; result.getLastChangeTimeCallback = (settingsName) => { const date = ns.fileExists(settingsName + ".date.txt") ? parseInt(ns.read(settingsName + ".date.txt")) : 0; return date; }; result.loadSettingsCallback = (settingsName) => { return ns.fileExists(settingsName) ? JSON.parse(ns.read(settingsName)) : {}; }; result.saveSettingsCallback = (settingsName, data) => { ns.write(settingsName, JSON.stringify(data), "w"); ns.write(settingsName + ".date.txt", Date.now().toString(), "w"); }; } if (settingsFilePath) result.startAutoLoadSave(settingsFilePath); return result; } descriptor; name; parent; _value = null; _descriptionElement = null; _properties = []; _proxy; _lastLoaded = 0; _autoLoadHandle; _save; _onValueChangedInternal = () => { }; constructor(descriptor, name, parent) { super(); this.descriptor = descriptor; this.name = name; this.parent = parent; this._type = { get hasValue() { return this.valueType !== undefined; }, get valueType() { if (this.isButton) return "number"; // because we increment the value to indicate the button was clicked else if (this.isLabel) return "string"; return descriptor.default === undefined ? undefined : typeof descriptor.default; }, get isButton() { return typeof descriptor.default === "function"; }, get isObject() { return "properties" in descriptor; }, get isRange() { return "min" in descriptor || "max" in descriptor || "step" in descriptor; }, get isSelect() { return "options" in descriptor; }, get isLabel() { return descriptor.default === undefined && !this.isObject; }, get isBoolean() { return this.valueType === "boolean"; }, get isNumber() { return this.valueType === "number"; }, get isString() { return this.valueType === "string"; }, get isHidden() { return !!descriptor.hidden; }, get isReadonly() { return !!descriptor.readonly; }, get typeName() { if (this.isButton) return "button"; else if (this.isObject) return "object"; else if (this.isRange) return "range"; else if (this.isSelect) return "select"; else if (this.isLabel) return "label"; else return "value"; }, get range() { if (!this.isRange) return undefined; return { min: descriptor.min ?? 0, max: descriptor.max ?? 1, step: descriptor.step, }; }, get options() { if (!this.isSelect) return undefined; return descriptor.options; } }; this._value = this._type.isButton ? 0 : descriptor.default; if (this.type.isObject) this._properties = Object.entries(descriptor.properties) .map(([name, descriptor]) => new Settings(descriptor, name, this)); } get ns() { return this.root._ns; } getLastChangeTimeCallback = (settingsName) => { return parseInt(localStorage.getItem(settingsName + "_date") ?? "0"); }; loadSettingsCallback = (settingsName) => { return JSON.parse(localStorage.getItem(settingsName) ?? "{}"); }; saveSettingsCallback = (settingsName, data) => { localStorage.setItem(settingsName, JSON.stringify(data)); localStorage.setItem(settingsName + "_date", Date.now().toString()); }; get type() { return this._type; } get root() { return this.parent?.root ?? this; } get value() { return this._value; } set value(value) { if (!this.root._isLoading && this.root._load) this.root._load(); if (this._value === value) return; if (this.type.isButton) { const times = value - this._value; for (let i = 0; i < times; ++i) this._onTriggered([], this.name); } this._value = value; this._onValueChanged([], this.name, value, this.type.isButton); this._onValueChangedInternal(value); } get proxy() { return this._createProxy(); } get reactElement() { return this.createReactElement(); } createReactElement(labelOverride = undefined) { this._reactElement = this._reactElement ?? SettingsDomElement.create(this, labelOverride); return this._reactElement; } startAutoSave(filePath) { if (!this.type.isObject || this._save !== undefined) return; this._save = () => { this.saveSettingsCallback(filePath, this.toObject()); if (autoLoadCheckInterval === 0) { globalThis["document"].dispatchEvent(new BitBurnerSettingsChangedEvent(filePath)); } }; this.addEventListener("internal-change", this._save); } stopAutoSave() { if (this._save === undefined) return; this.removeEventListener("internal-change", this._save); this._save = undefined; } startAutoLoad(filePath) { if (!this.type.isObject || this._load !== undefined) return; this._load = (e) => { if (e && e.settingsFilePath !== filePath) return; const lastLoaded = this.getLastChangeTimeCallback(filePath); if (lastLoaded > this._lastLoaded) this.fromObject(this.loadSettingsCallback(filePath), lastLoaded); }; if (autoLoadCheckInterval === 0) { globalThis["document"].addEventListener("BitBurnerSettingsChanged", this._load); } else { this._autoLoadHandle = setInterval(this._load, autoLoadCheckInterval); } this._load(); } stopAutoLoad() { if (this._load === undefined) return; if (this._autoLoadHandle) { clearInterval(this._autoLoadHandle); this._autoLoadHandle = undefined; } else { globalThis["document"].removeEventListener("BitBurnerSettingsChanged", this._load); } this._load = undefined; } startAutoLoadSave(filePath) { this.startAutoLoad(filePath); this.startAutoSave(filePath); } stopAutoLoadSave() { this.stopAutoLoad(); this.stopAutoSave(); } get _path() { if (!this.name) return []; if (!this.parent) return [this.name]; return this.parent._path.concat([this.name]); } _onTriggered(path, name) { this.dispatchEvent(new ClickEvent(path.join("."))); this.parent?._onTriggered([this.name, ...path], name); } _onValueChanged(path, name, value, wasFromButton) { this.dispatchEvent(new InternalChangeEvent(path.join("."), value)); if (!wasFromButton) this.dispatchEvent(new ChangeEvent(path.join("."), value)); this.parent?._onValueChanged([this.name, ...path], name, value, wasFromButton); this._updateDescriptionText(); this._updateText(); this._updateHint(); } get label() { return this._getDynamicPropertyValue("label") ?? (this.name ? toHumanReadableName(this.name) : undefined); function toHumanReadableName(name) { return name.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase()); } } get hint() { return this._getDynamicPropertyValue("hint"); } get description() { return this._getDynamicPropertyValue("description"); } get text() { return this._getDynamicPropertyValue("text"); } _getDynamicPropertyValue(propertyName) { const reboundGetter = Object.getOwnPropertyDescriptor(this.descriptor, propertyName)?.get?.bind(this); if (reboundGetter) return reboundGetter.call(); const result = this.descriptor[propertyName]; if (result === undefined) return undefined; if (typeof result === "function") return result.bind(this)(this.value, this.ns); return result; } _updateHint() { if (!this._control) return; const hint = this.hint; if (hint !== undefined) this._control.title = hint.toString(); } _updateDescriptionText() { if (!this._descriptionElement) return; const text = this.description ?? this.value; if (text !== undefined) this._descriptionElement.textContent = text.toString(); } _updateLabel() { if (!this._labelElement) return; const label = this.label; if (label !== undefined) this._labelElement.textContent = label; } _updateText() { if (!this._textElement) return; const text = this.text; if (text !== undefined) this._textElement.textContent = text; else if (this.type.isLabel) this._textElement.textContent = this.value; } _createProxy() { if (this._proxy) return this._proxy; if (!this.type.isButton && !this.type.isObject) throw new Error("Can only create a proxy for an object or button"); this._proxy = new Proxy({}, { get: (target, name) => { if (name === "addEventListener") return this.addEventListener.bind(this); else if (name === "removeEventListener") return this.removeEventListener.bind(this); else if (this.type.isObject && name === "toObject") return this.toObject.bind(this); else if (this.type.isObject && name === "fromObject") return this.fromObject.bind(this); const property = this._properties?.find((property) => property.name === name); if (!property) throw new Error(`Property ${name} not found`); return property.type.isObject || property.type.isButton ? property.proxy : property.value; }, set: (target, name, value) => { const property = this._properties?.find((property) => property.name === name); if (!property) throw new Error(`Property ${name} not found`); property.value = value; return true; }, }); return this._proxy; } fromObject(data, timeStamp = Date.now()) { this._lastLoaded = timeStamp; this._isLoading = true; if (!this.type.isObject) throw new Error("Can only load an object"); this._properties.forEach((property) => { if (property.type.isObject) { property.fromObject(data[property.name]); } else if (property.type.hasValue) { property._isLoading = true; property.value = data[property.name] ?? property.value; property._isLoading = false; } }); this._isLoading = false; } toObject() { if (!this.type.isObject) throw new Error("Can only save an object"); const result = {}; this._properties.forEach((property) => { if (property.type.isObject) { result[property.name] = property.toObject(); return; } else if (property.type.hasValue) { result[property.name] = property.value; } }); return result; } createDom(labelOverride = undefined) { if (this._controlElement) return this._controlElement; let containerElement; // only for type object let valueContainerElement; // contains an input element and maybe a description span const doc = globalThis["document"]; const hasLabel = labelOverride !== undefined || this.label !== undefined; if (this.type.isObject && hasLabel) { this._controlElement = doc.createElement("details"); this._controlElement.open = true; } else this._controlElement = doc.createElement("div"); if (this.type.isObject) { containerElement = doc.createElement("div"); containerElement.classList.add("container"); this._controlElement.appendChild(containerElement); } if (this.descriptor.hint) this._controlElement.title = this.descriptor.hint; (containerElement ?? this._controlElement).toggleAttribute("disabled", !!this.type.isReadonly); this._controlElement.classList.add("control"); this._controlElement.classList.add(this.type.typeName); if (this.hasValue && !this.type.isButton) this._controlElement.classList.add(this.type.valueType); if (!this.type.isButton && hasLabel) { this._labelElement = doc.createElement(this.type.isObject ? "summary" : "label"); if (labelOverride !== undefined) this._labelElement.textContent = labelOverride; else this._labelElement.textContent = this.label; this._controlElement.appendChild(this._labelElement); } let input; if (this.type.isRange) { input = doc.createElement("input"); input.type = "range"; input.min = this.type.range.min.toString(); input.max = this.type.range.max.toString(); if (this.type.range.step !== undefined) input.step = this.descriptor.step.toString(); } else if (this.type.isSelect) { input = doc.createElement("select"); this.type.options.forEach((option) => { const optionElement = doc.createElement("option"); optionElement.value = option; optionElement.textContent = option; input.appendChild(optionElement); }); } else if (this.type.isButton) { input = doc.createElement("button"); this._textElement = this._labelElement = input; } else if (this.type.isLabel) { this._textElement = input = doc.createElement("span"); //nop } else if (this.type.isObject) { // no input, but more controls this._properties.filter(p => !p.type.isHidden).forEach((property) => { containerElement.appendChild(property.createDom()); }); } else if (this.type.isBoolean) { input = doc.createElement("input"); input.type = "checkbox"; } else if (this.type.isNumber) { input = doc.createElement("input"); input.type = "number"; } else if (this.type.isString) { input = doc.createElement("input"); input.type = "text"; } if (input) { valueContainerElement = doc.createElement("div"); valueContainerElement.classList.add("valueContainer"); valueContainerElement.appendChild(input); this._controlElement.appendChild(valueContainerElement); if (this.description !== undefined || this.type.isRange) { this._descriptionElement = doc.createElement("span"); this._descriptionElement.classList.add("description"); valueContainerElement.appendChild(this._descriptionElement); } if (this.type.isButton) { input.addEventListener("click", () => { if (typeof this.descriptor.default === "function" && this.descriptor.default !== Function) this.descriptor.default.call(this); this.value++; }); } else if (this.type.isLabel) { // nop } else if (this.type.isBoolean) { input.checked = this.value; this._onValueChangedInternal = (newValue) => { input.checked = newValue; }; input.addEventListener("change", () => { if (!this.root._isLoading) this.value = input.checked; }); } else if (this.type.isNumber) { input.value = this.value; this._onValueChangedInternal = (newValue) => { input.value = newValue; }; input.addEventListener(this.type.isRange ? "input" : "change", () => { if (!this.root._isLoading) this.value = parseFloat(input.value); }); } else if (this.type.isString) { input.value = this.value; this._onValueChangedInternal = (newValue) => { input.value = newValue; }; input.addEventListener("change", () => { if (!this.root._isLoading) this.value = input.value; }); } this._updateDescriptionText(); } this._updateLabel(); this._updateText(); this._updateHint(); return this._controlElement; } } class ChangeEvent extends Event { constructor(path, value) { super('change', { bubbles: false, cancelable: false, composed: false }); this.path = path; this.value = value; } } class InternalChangeEvent extends Event { constructor(path, value) { super('internal-change', { bubbles: false, cancelable: false, composed: false }); this.path = path; this.value = value; } } class ClickEvent extends Event { constructor(path) { super('click', { bubbles: false, cancelable: false, composed: false }); this.path = path; } } class BitBurnerSettingsChangedEvent extends Event { constructor(settingsFilePath) { super('BitBurnerSettingsChanged', { bubbles: false, cancelable: false, composed: false }); this.settingsFilePath = settingsFilePath; } }
Editor is loading...
Leave a Comment