BitBurner settings.common.js
unknown
javascript
2 years ago
18 kB
36
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