import ApiManager from "./apiManager";
import { consoleError, consoleWarn, encodedToNumber, numberToEncoded, numberUnsignedTo32BitBooleanArray, set32BitPosition, sleep } from "./utils";
import WindowFilter from "./WindowFilter";
import WindowSettings from "./WindowSettings";
/** Documentation for `windowState` encoding.
* The variables are stored as concatnated, Big-Endian, base 92, substrings.
*
* Var 1 | Var 2 | Var 3
* 123456781234567812345678 (do not assume each variable is 8 bytes)
*
* Variable width is non-standard, but known (e.g. first variable is one byte, next variable is twenty bytes). Therefore, seperators are not used.
* These variables are listed from their first occurance, to their last occurance, in order.
* The first section ("All Windows") contains the variables which occur at the start of every window state.
* Variables unique to a window are stored *after* that section.
* The "Binary" type refers to variables that require additional decoding, as they are not stored as a `Number`.
* Storing a `Number` in the default, 13 unique bit flags is discouraged.
* Text should never be stored, because it will always bloat.
* Avoid collisions with other user storage settings. If a variable's state is managed elsewhere, don't add it here.
* If a variable is critical to non-display functionality, don't add it here.
* (i.e. Filter Window's displayed filtered colors is managed elsewhere, and is critical to templateManager for template rendering)
* (i.e. All of the Settings Window's variables are critical to functionality elsewhere)
* Nothing other than settingsManager should write to `windowState`, because there is no point. Just update the Manager's variable.
* "Why did you make this so hard?" because it reduces the file size by up to ~95% (with key-value pairs)
* For example, the main window is stored as 30 bytes in the JSON file, and the non-compressed version would be ~400 bytes (with key-value pairs).
*
* _____________________________________________________
* | |# of | Slice | |
* | Window Name |bytes| Value | Type | Description
* #-------------+-----+---------+---------+-------------
* | All Windows | 1 | 0, 1 | Number | Draw depth of the window. (0 is the window on the bottom, 91 is the window on the top)
* | All Windows | 1 | 1, 2 | Binary | 6 bit flags (Big-Endian, most-significant bit is reserved)
* | All Windows | | ^---0| Boolean | *Does the window exist in the DOM tree? (Is the window displayed to the user?)
* | All Windows | | /|\--1| Boolean | *Is the window minimized?
* | All Windows | | |---2| Boolean | *Has the window been moved/dragged by the user?
* | All Windows | | |---3| Boolean | *Is the X-axis shift translation negative? (This is a sign)
* | All Windows | | |---4| Boolean | *Is the Y-axis shift translation negative? (This is a sign)
* | All Windows | | |---5| Boolean | *Reserved for future extension. (True = the next byte contains 6 bit flags)
* | All Windows | 3 | 2, 5 | Number | Shift translation across the X-axis (zeroed if none)
* | All Windows | 3 | 5, 8 | Number | Shift translation across the Y-axis (zeroed if none)
* #-------------+-----+---------+---------+------------
* | Main Window | 2 | 8, 10 | Binary | 13 bit flags, which are unique to this window
* | Main Window | 4 | 10, 14 | Number | "Upload Template" input fields for the X coordinates (stored in the 2-coordinate system)
* | Main Window | 4 | 14, 18 | Number | "Upload Template" input fields for the Y coordinates (stored in the 2-coordinate system)
* | Settings | 2 | 8, 10 | Binary | 13 bit flags, which are unique to this window
* | Temp Wizard | 2 | 8, 10 | Binary | 13 bit flags, which are unique to this window
* | Credits | 2 | 8, 10 | Binary | 13 bit flags, which are unique to this window
* | Filter | 2 | 8, 10 | Binary | 13 bit flags, which are unique to this window
* | Filter | | ^---0| Boolean | *Is the window in "Windowed" mode?
* | Filter | | /|\--1| Boolean | *Are unused colors displayed to the user?
* | Filter | | |---2| Boolean | *Is the color filter sorted "Ascending"?
* | Filter | | |---3| Boolean | *Is the color filter sorted "Descending"?
* | Filter | | |---4| Boolean | *Is the color filter sorted by "Color IDs"?
* | Filter | | |---5| Boolean | *Is the color filter sorted by "Color Names"?
* | Filter | | |---6| Boolean | *Is the color filter sorted by "Premium Colors"?
* | Filter | | |---7| Boolean | *Is the color filter sorted by "Percentage"?
* | Filter | | |---8| Boolean | *Is the color filter sorted by "Correct Pixels"?
* | Filter | | |---9| Boolean | *Is the color filter sorted by "Incorrect Pixels"?
* | Filter | | |--10| Boolean | *Is the color filter sorted by "Total Pixels"?
* | Filter | | |--11| Boolean | *Reserved
* | Filter | | |--12| Boolean | *Reserved
*/
/** SettingsManager class for handling user settings and making them persist between sessions.
* Logic for {@link WindowSettings} is managed here.
* "Flags" should follow the same styling as `.classList()` and should not contain spaces.
* A flag should always be false by default.
* When a flag is false, it will not exist in the "flags" Array.
* (Therefore, "flags" should be `[]` by default)
* If it exists in the "flags" Array, then the flag is `true`.
* Windows are assumed to have only one `translate()` call in a `transform:` CSS in-line style.
* Aforementioned `translate()` is expected to be in pixel units. It can read non-pixel units, but will save/load as pixel units.
* @class SettingsManager
* @since 0.91.11
* @example
* {
* "uuid": "497dcba3-ecbf-4587-a2dd-5eb0665e6880",
* "telemetry": 1,
* "flags": ["hl-noTrans", "ftr-oWin", "te-noSkip"],
* "highlight": [[1,0,-1],[1,-1,0],[2,1,0],[1,0,1]],
* "filter": "!!#L3/kBp'Gb8]q"
* "windowStates": {"fltr": "!!$L4", "bm": "/kBp:"}
* }
*/
export default class SettingsManager extends WindowSettings {
#windowStatesObject;
#windowStatesObjectEncoded;
/** Constructor for the SettingsManager class
* @param {string} name - The name of the userscript
* @param {string} version - The version of the userscript
* @param {Object} userSettings - The user settings as an object
* @since 0.91.11
*/
constructor(name, version, userSettings) {
super(name, version); // Executes WindowSettings constructor
// The character representing zero and one in Blue Marble's default encoding alphabet
this.zerothEncodingAlphabetCharacter = numberToEncoded(0);
this.onethEncodingAlphabetCharacter = numberToEncoded(1);
this.windowMain = null; // The Main Blue Marble window
this.windowFilter = null; // The Color Filter window
this.windowCredits = null; // The Credits window
this.windowWizard = null; // The Template Wizard window
this.templateManager = null; // The template manager instance
this.apiManager = null; // The API manager
this.userSettings = userSettings; // User settings as an Object
this.userSettings.flags ??= []; // Makes sure the key "flags" always exists
this.userSettingsOld = structuredClone(this.userSettings); // Creates a duplicate of the user settings to store the old version of user settings from 5+ seconds ago
this.userSettingsSaveLocation = 'bmUserSettings'; // Storage save location
this.commonStatesByteLength = 8; // Sum of bytes of the common window state variables ("All Windows")
this.#windowStatesObjectEncoded = this.userSettings?.windowStates ?? {};
this.#windowStatesObject = this.#decodeWindowStateToObject(this.#windowStatesObjectEncoded) ?? {};
this.commonWindowStateTranslateRegEx = new RegExp(/translate\((-?\d*\.?\d*)\w*\s*,?\s*(-?\d*\.?\d*)/i); // RegEx for finding where the window is
this.updateFrequency = 2000; // Cooldown between saving to storage (throttle)
this.lastUpdateTime = 0; // When this unix timestamp is within the last 5 seconds, we should not save this.userSettings to storage
/** Enum for the bit flags of the Color Filter Window.
* This is used to convert `<select>` indexes into bit flag indexes without having to use conditional statements.
* This is also used so the list order of the UI options is not tied to the order of the bit flags.
* @type {Object<number, number>}
* @since 0.94.33
*/
this.wStateFilterVarsFlags = Object.freeze({
// <select> index: Bit Flag index
0: 0, // Is the window in "Windowed" mode?
1: 1, // Should unused colors be displayed?
2: 2, // Secondary Ascending
3: 3, // Secondary Descending
4: 4, // Primary Color IDs
5: 5, // Primary Color Names
6: 6, // Primary Premium Colors
7: 7, // Primary Percentage
8: 8, // Primary Correct Pixels
9: 9, // Primary Incorrect Pixels
10: 10, // Primary Total Pixels
// 11-12: Reserved
})
setInterval(this.#updateWindowState.bind(this), this.updateFrequency * 0.6);
setInterval(this.updateUserStorage.bind(this), this.updateFrequency); // Runs every X seconds (see updateFrequency)
}
/** Updates the user settings in userscript storage
* @since 0.91.39
*/
async updateUserStorage() {
//console.log('User Settings before attempted save: ', JSON.stringify(this.userSettings));
await this.#updateFilteredColors(); // Update the encoded string of filtered colors
this.userSettings['windowStates'] = this.#windowStatesObjectEncoded;
// Turns the objects into a string
const userSettingsCurrent = JSON.stringify(this.userSettings);
const userSettingsOld = JSON.stringify(this.userSettingsOld);
//console.log('Old user settings: ', userSettingsOld);
//console.log('New user settings: ', userSettingsCurrent);
// If the user settings have changed, AND the last update to user storage was over 2 seconds ago (2 sec throttle)...
if ((userSettingsCurrent != userSettingsOld) && ((Date.now() - this.lastUpdateTime) > this.updateFrequency)) {
await GM.setValue(this.userSettingsSaveLocation, userSettingsCurrent); // Updates user storage
this.userSettingsOld = structuredClone(this.userSettings); // Updates the old user settings with a duplicate of the current user settings
this.#windowStatesObject = this.#decodeWindowStateToObject(this.#windowStatesObjectEncoded) ?? {}; // Update the in-memory window state Object
this.lastUpdateTime = Date.now(); // Updates the variable that contains the last time updated
console.log(userSettingsCurrent);
}
}
/** Toggles a boolean flag to the state that was passed in.
* If no state was passed in, the flag will flip to the opposite state.
* The existence of the flag determines its state. If it exists, it is `true`.
* @param {string} flagName - The name of the flag to toggle
* @param {boolean} [state=undefined] - (Optional) The state to change the flag to
* @since 0.91.60
*/
toggleFlag(flagName, state = undefined) {
console.log('Flag Settings:', this.userSettings?.flags);
const flagIndex = this.userSettings?.flags?.indexOf(flagName) ?? -1; // Is the flag `true`?
console.log(`Flag '${flagName}' is requested to become '${state}' (currently ${flagIndex})`);
// If the flag is enabled, AND the user does not want to force the flag to be true...
if ((flagIndex != -1) && (state !== true)) {
console.log(`Setting flag '${flagName}' to false!`);
this.userSettings?.flags?.splice(flagIndex, 1); // Remove the flag (makes it false)
} else if ((flagIndex == -1) && (state !== false)) {
console.log(`Setting flag '${flagName}' to true! (Adding to storage)`);
// Else if the flag is disabled, AND the user does not want to force the flag to be false...
this.userSettings?.flags?.push(flagName); // Add the flag (makes it true)
}
console.log('Flag Settings Final: ', this.userSettings?.flags);
}
// This is one of the most insane OOP setups I have ever laid my eyes on
/** Builds the "highlight" category of the settings window
* @since 0.91.18
* @see WindowSettings#buildHighlight
*/
buildHighlight() {
const highlightPresetOff = '<svg viewBox="0 0 3 3"><path d="M0,0H3V3H0ZM0,1H3M0,2H3M1,0V3M2,0V3" fill="#fff"/><path d="M1,1H2V2H1Z" fill="#2f4f4f"/></svg>';
const highlightPresetCross = '<svg viewBox="0 0 3 3"><path d="M0,0H3V3H0Z" fill="#fff"/><path d="M1,0H2V1H3V2H2V3H1V2H0V1H1Z" fill="brown"/><path d="M1,1H2V2H1Z" fill="#2f4f4f"/></svg>';
// Obtains user settings for highlight from storage, or the default array if nothing was found
const storedHighlight = this.userSettings?.highlight ?? [[1, 0, 1], [2, 0, 0], [1, -1, 0], [1, 1, 0], [1, 0, -1]];
// Constructs the category and adds it to the window
this.window = this.addDiv({'class': 'bm-container'})
.addHeader(2, {'textContent': 'Pixel Highlight'}).buildElement()
.addHr().buildElement()
.addDiv({'class': 'bm-container', 'style': 'margin-left: 1.5ch;'})
.addCheckbox({'textContent': 'Highlight transparent pixels'}, (instance, label, checkbox) => {
checkbox.checked = !this.userSettings?.flags?.includes('hl-noTrans'); // Makes the checkbox match the last stored user setting
checkbox.onchange = (event) => this.toggleFlag('hl-noTrans', !event.target.checked); // Forces the flag to be the opposite state as the checkbox. E.g. "Checked" means 'hl-noTrans' is false (does not exist).
}).buildElement()
.addP({'id': 'bm-highlight-preset-label', 'textContent': 'Choose a preset:', 'style': 'font-weight: 700;'}).buildElement()
.addDiv({'class': 'bm-flex-center', 'role': 'group', 'aria-labelledby': 'bm-highlight-preset-label'})
.addDiv({'class': 'bm-highlight-preset-container'})
.addSpan({'textContent': 'None'}).buildElement()
.addButton({'innerHTML': highlightPresetOff, 'aria-label': 'Preset "None"'}, (instance, button) => {button.onclick = () => this.#updateHighlightToPreset('None')}).buildElement()
.buildElement()
.addDiv({'class': 'bm-highlight-preset-container'})
.addSpan({'textContent': 'Cross'}).buildElement()
.addButton({'innerHTML': highlightPresetCross, 'aria-label': 'Preset "Cross Shape"'}, (instance, button) => {button.onclick = () => this.#updateHighlightToPreset('Cross')}).buildElement()
.buildElement()
.addDiv({'class': 'bm-highlight-preset-container'})
.addSpan({'textContent': 'X'}).buildElement()
.addButton({'innerHTML': highlightPresetCross.replace('d="M1,0H2V1H3V2H2V3H1V2H0V1H1Z"', 'd="M0,0V1H3V0H2V3H3V2H0V3H1V0Z"'), 'aria-label': 'Preset "X Shape"'}, (instance, button) => {button.onclick = () => this.#updateHighlightToPreset('X')}).buildElement()
.buildElement()
.addDiv({'class': 'bm-highlight-preset-container'})
.addSpan({'textContent': 'Full'}).buildElement()
.addButton({'innerHTML': highlightPresetOff.replace('#fff', '#2f4f4f'), 'aria-label': 'Preset "Full Template"'}, (instance, button) => {button.onclick = () => this.#updateHighlightToPreset('Full')}).buildElement()
.buildElement()
.buildElement()
.addP({'id': 'bm-highlight-grid-label', 'textContent': 'Create a custom pattern:', 'style': 'font-weight: 700;'}).buildElement()
.addDiv({'class': 'bm-highlight-grid', 'role': 'group', 'aria-labelledby': 'bm-highlight-grid-label'});
// We leave this open so we can add buttons
// For each of the 9 buttons...
for (let buttonY = -1; buttonY <= 1; buttonY++) {
for (let buttonX = -1; buttonX <= 1; buttonX++) {
const buttonState = storedHighlight[storedHighlight.findIndex(([, x, y]) => ((x == buttonX) && (y == buttonY)))]?.[0] ?? 0;
let buttonStateName = 'Disabled';
if (buttonState == 1) {
buttonStateName = 'Incorrect';
} else if (buttonState == 2) {
buttonStateName = 'Template';
}
this.window = this.addButton({
'data-status': buttonStateName,
'aria-label': `Sub-pixel ${buttonStateName.toLowerCase()}`
}, (instance, button) => {
button.onclick = () => this.#updateHighlightSettings(button, [buttonX, buttonY])
}).buildElement();
}
}
// Resumes from where we left off before we added buttons
this.window = this.buildElement()
.buildElement()
.buildElement();
}
/** Updates the display of the highlight buttons in the settings window.
* Additionally, it will update user settings with the new selection.
* @param {HTMLButtonElement} button - The button that was pressed
* @param {Array<number, number>} coords - The relative coordinates of the button
* @since 0.91.46
*/
#updateHighlightSettings(button, coords) {
button.disabled = true; // Disabled the button until we are done
const status = button.dataset['status']; // Obtains the current status of the button
/** Obtains the old highlight storage, or sets it to default. @type {Array<number[]>} */
const userStorageOld = this.userSettings?.highlight ?? [[1, 0, 1], [2, 0, 0], [1, -1, 0], [1, 1, 0], [1, 0, -1]];
let userStorageChange = [2, 0, 0]; // The new change to the user storage
const userStorageNew = userStorageOld; // The old storage with the new change
// For each different type of status...
switch (status) {
// If the button was in the "Disabled" state
case 'Disabled':
// Change to "Incorrect"
button.dataset['status'] = 'Incorrect';
button.ariaLabel = 'Sub-pixel incorrect';
userStorageChange = [1, ...coords];
break;
// If the button was in the "Incorrect" state
case 'Incorrect':
// Change to "Template"
button.dataset['status'] = 'Template';
button.ariaLabel = 'Sub-pixel template';
userStorageChange = [2, ...coords];
break;
// If the button was in the "Template" state
case 'Template':
// Change to "Disabled"
button.dataset['status'] = 'Disabled';
button.ariaLabel = 'Sub-pixel disabled';
userStorageChange = [0, ...coords];
break;
}
// Finds the index of the pixel to change
const indexOfChange = userStorageOld.findIndex(([, x, y]) => ((x == userStorageChange[1]) && (y == userStorageChange[2])));
// If the new sub-pixel state is NOT disabled
if (userStorageChange[0] != 0) {
// If a sub-pixel was found...
if (indexOfChange != -1) {
userStorageNew[indexOfChange] = userStorageChange;
} else {
userStorageNew.push(userStorageChange);
}
} else if (indexOfChange != -1) {
// Else, it is disabled. We want to remove it if it exists.
userStorageNew.splice(indexOfChange, 1); // Removes 1 index from the array at the index of the pixel change
}
console.log('New Highlight Settings: ', userStorageNew);
this.userSettings['highlight'] = userStorageNew;
// TODO: Add timer update here
button.disabled = false; // Reenables the button since we are done
}
/** Changes the highlight buttons to the clicked preset.
* @param {string} preset - The name of the preset
* @since 0.91.49
*/
async #updateHighlightToPreset(preset) {
// Obtains all preset buttons as a NodeList
const presetButtons = document.querySelectorAll('.bm-highlight-preset-container button');
// For each preset...
for (const button of presetButtons) {
button.disabled = true; // Disables the button
}
let presetArray = [0,0,0,0,2,0,0,0,0]; // The preset "None"
// Selects the preset passed in
switch (preset) {
case 'Cross':
presetArray = [0,1,0,1,2,1,0,1,0]; // The preset "Cross"
break;
case 'X':
presetArray = [1,0,1,0,2,0,1,0,1]; // The preset "X"
break;
case 'Full':
presetArray = [2,2,2,2,2,2,2,2,2]; // The preset "Full"
break;
}
// Obtains the buttons to click as a NodeList
const buttons = document.querySelector('.bm-highlight-grid')?.childNodes ?? [];
// For each button...
for (let buttonIndex = 0; buttonIndex < buttons.length; buttonIndex++) {
const button = buttons[buttonIndex]; // Gets the current button to check
// Gets the state of the button as a number
let buttonState = button.dataset['status'];
buttonState = (buttonState != 'Disabled') ? ((buttonState != 'Incorrect') ? 2 : 1) : 0;
// Finds the difference between the preset and the button
let buttonStateDelta = presetArray[buttonIndex] - buttonState;
// Since there is no difference, the button matches, so we skip it
if (buttonStateDelta == 0) {continue;}
// Makes the difference positive
buttonStateDelta += (buttonStateDelta < 0) ? 3 : 0;
/** At this point, these are the possible options:
* 1. The preset is zero and the button is two (-2) so we need to click once
* 2. The preset is one and the button is two (-1) so we need to click twice
* 3. The preset is one ahead of the button (1) so we need to click once
* 4. The preset is two ahead of the button (2) so we need to click twice
* Due to the addition of three in the line above, options 1 & 3 combine, and options 2 & 4 combine.
* Now the only options we have are:
* 1. If (1) then click once
* 2. If (2) then click twice
* Also due to the addition of three in the line above, our two options are POSITIVE numbers
*/
button.click(); // Clicks once
// Clicks a second time if needed
if (buttonStateDelta == 2) {
// For 0.2 seconds, or when the button is NOT disabled, wait for 10 milliseconds before attempting to continue
for (let timeWaited = 0; timeWaited < 200; timeWaited += 10) {
if (!button.disabled) {break;} // Breaks early once the button is enabled
await sleep(10);
}
button.click(); // Clicks again
}
}
// For each preset...
for (const button of presetButtons) {
button.disabled = false; // Re-enables the button
}
}
/** Updates filtered colors in user storage.
* @since 0.92.15
*/
async #updateFilteredColors() {
//const timer = performance.now();
// The current color filter Map
const filteredColorMap = this.templateManager.shouldFilterColor;
// If no colors are to be filtered, return early
if (!filteredColorMap.size) {
this.userSettings.filter = this.zerothEncodingAlphabetCharacter.repeat(15);
return; // Returns early
}
let mutableBitFlagsNegSmall = 0; // Colors -1 to -31 in Blue Marble's palette
let mutableBitFlagsPosSmall = 0; // Colors 0 to 31 in Wplace's palette
let mutableBitFlagsPosLarge = 0; // Colors 32 to 63 in Wplace's palette
// For each color passed in...
for (const [id, value] of filteredColorMap) {
if (id >= -32 && id <= -1) {
mutableBitFlagsNegSmall = set32BitPosition(mutableBitFlagsNegSmall, id + 32, value);
} else if (id >= 0 && id <= 31) {
mutableBitFlagsPosSmall = set32BitPosition(mutableBitFlagsPosSmall, id, value);
} else if (id >= 32 && id <= 63) {
mutableBitFlagsPosLarge = set32BitPosition(mutableBitFlagsPosLarge, id - 32, value);
} else {
consoleError(`Attempted to store filter color with ID #${id} but this ID number is out of bounds (-32 to 63)! The color will not be stored.`);
}
}
const encodedBitFlags = numberToEncoded(mutableBitFlagsNegSmall).padStart(5, this.zerothEncodingAlphabetCharacter)
+ numberToEncoded(mutableBitFlagsPosSmall).padStart(5, this.zerothEncodingAlphabetCharacter)
+ numberToEncoded(mutableBitFlagsPosLarge).padStart(5, this.zerothEncodingAlphabetCharacter);
this.userSettings.filter = encodedBitFlags; // Stores the encoded bit flags
//console.log(`Finished updating filter color storage in ${(performance.now() - timer).toFixed(3) / 1000} seconds!\nThere are ${filteredColorMap.size} hidden colors.`);
}
/** Decodes the filtered color bit flags that came from user storage.
* @param {string} encodedString - The filtered color save-state from user storage
* @returns {Map<number, boolean>} A map containing only entries of colors to filter
* @since 0.92.18
*/
decodeFilteredColorBitFlags(encodedString) {
const shouldColorBeFiltered = new Map(); // Will contain colors to be filtered
// If encodedString is in an unexpected state...
if (typeof encodedString !== 'string') {
consoleWarn('Could not decode filtered colors from user storage! Either the filtered colors are not stored as a string, or the user storage does not exist. Assuming no colors are filtered...');
return shouldColorBeFiltered; // Return early
}
// Return early if no colors are filtered
if (!encodedString || encodedString == this.zerothEncodingAlphabetCharacter.repeat(15)) {return shouldColorBeFiltered;}
const minSupportedBitFlag = -32; // Minimum supported color ID (Reserved Blue Marble color)
const maxSupportedBitFlag = 63; // Maximum supported color ID (Light Stone Wplace color)
const supportedEncodedBitFlags = encodedString.slice(0, 15); // This version of Blue Marble can not support more than 15 encoded characters, so we ignore them.
// The bit flags
const bitFlagsNegSmall = encodedToNumber(supportedEncodedBitFlags.slice(0, 5));
const bitFlagsPosSmall = encodedToNumber(supportedEncodedBitFlags.slice(5, 10));
const bitFlagsPosLarge = encodedToNumber(supportedEncodedBitFlags.slice(10, 15));
// It is assumed that `encodedToNumber` outputs an unsigned number (0 to 4294967295)
// For all supported color IDs...
for (let id = minSupportedBitFlag; id <= maxSupportedBitFlag; id++) {
let isBitTrue = false;
// Find if the bit is one, and if it is, mark that ID as a filtered color
if (id >= -32 && id <= -1) {
isBitTrue = (bitFlagsNegSmall & (1 << (id + 32))) !== 0;
} else if (id >= 0 && id <= 31) {
isBitTrue = (bitFlagsPosSmall & (1 << id)) !== 0;
} else if (id >= 32 && id <= 63) {
isBitTrue = (bitFlagsPosLarge & (1 << (id - 32))) !== 0;
}
if (isBitTrue) {
shouldColorBeFiltered.set(id, true);
}
}
return shouldColorBeFiltered;
}
/** Retrieves all window states, and *overrides* the user storage version stored in `this.userStorage.windowStates`.
* This encodes window states.
* @since 0.92.23
*/
#updateWindowState() {
/** Obtains common window states, which are non-unique to the window.
* (Every window can have these states)
* @param {HTMLElement} windowElement - The ID (DOM attribute) of the window.
* @param {string} userStorageID - The UID (key) used in user storage to signify this window
* @since 0.92.23
* @returns {string} Encoded string, which contains the common state variables.
*/
const obtainCommonStates = (windowElement, userStorageID) => {
// Retrieves the hottest stored common state for this window.
// This is the memory version, as opposed to disk version, which is cold
// If it can't retrieve the common state, we use zeros, because either the window is new, or something went VERY wrong somewhere else, so a little data loss here is fine compared to the alternative (crashing)
//console.log(this.#windowStatesObjectEncoded?.[userStorageID]?.slice(0, this.commonStatesByteLength) ?? this.zerothEncodingAlphabetCharacter.repeat(this.commonStatesByteLength));
const commonStatesOld = this.#windowStatesObjectEncoded?.[userStorageID]?.slice(0, this.commonStatesByteLength) ?? this.zerothEncodingAlphabetCharacter.repeat(this.commonStatesByteLength);
// This is ONLY the common states of the window
// Returns the previously stored window state variables...
// ...but sets the "is window shown" variable to `false`
if (!windowElement) {return commonStatesOld.slice(0, 1) + numberToEncoded(set32BitPosition(encodedToNumber(commonStatesOld.slice(1, 2)), 0, false)) + commonStatesOld.slice(2);}
// Obtain the draw depth, or if it does not exist, (cause a collision) by setting it to zero
// Refuses to set a draw depth greater than 91
const drawDepth = Math.max(0, Math.min(Number(windowElement.dataset['drawDepth'] ?? 0), 91));
// Declares the dynamically constructed bit flag byte
// We immediately set the "is window shown" bit to `true`
let bitFlagsMutable = set32BitPosition(0, 0, true); // Use only the 6 least-significant bits
// Figures out if the window is minimized, and sets the cooresponding bit
const windowMinimizationButton = windowElement.querySelector('button[data-button-status]');
const isWindowMinimized = (windowMinimizationButton?.dataset['buttonStatus'] == 'collapsed');
bitFlagsMutable = set32BitPosition(bitFlagsMutable, 1, isWindowMinimized);
const windowStyle = windowElement.style; // The in-line style DOM attribute for the window
/** RegEx matches, which contain the coordinate strings.
* Matches against the `transform` style property
* @type {string[] | null}
*/
const matches = this.commonWindowStateTranslateRegEx.exec(windowStyle.getPropertyValue('transform') ?? '');
// If matches exists, then the window has been moved,
// so we update the corresponding bit
bitFlagsMutable = set32BitPosition(bitFlagsMutable, 2, !!matches);
const xTransCoord = Number(matches?.[1] ?? 0); // X Coordinate, or zero
const yTransCoord = Number(matches?.[2] ?? 0); // Y Coordinate, or zero
// Is the [axis] shift translation negative?
// A really cool trick, which turns the sign of a number into a boolean. (Negative zero becomes positive)
// !!(Math.sign(number) + 1)
// But since `negative = true` here, we invert the boolean
// and assign the boolean to the corresponding bit
bitFlagsMutable = set32BitPosition(bitFlagsMutable, 3, !(Math.sign(xTransCoord) + 1));
bitFlagsMutable = set32BitPosition(bitFlagsMutable, 4, !(Math.sign(yTransCoord) + 1));
const windowCoordinateMaximum = 778687; // Ones, for three encoded characters (92^3)-1
// Encodes the X & Y coordinates, clamped to the farthest supported coordinate
const windowTransX = numberToEncoded(Math.min(Math.abs(xTransCoord), windowCoordinateMaximum));
const windowTransY = numberToEncoded(Math.min(Math.abs(yTransCoord), windowCoordinateMaximum));
// Stores one/true because the window always exists if this code reaches this point
return numberToEncoded(drawDepth).slice(-1) // Clamp to 1 character
+ numberToEncoded(bitFlagsMutable).slice(-1) // Clamp to 1 character
+ windowTransX.padStart(3, this.zerothEncodingAlphabetCharacter).slice(-3) // Clamp to 3 characters
+ windowTransY.padStart(3, this.zerothEncodingAlphabetCharacter).slice(-3); // Clamp to 3 characters
};
// Obtains the window ID for the main window
const windowMainID = this.windowMain?.windowID;
// Obtains the main window element itself
const windowMainElement = windowMainID ? document.querySelector('#' + this.windowMain?.windowID) : undefined;
// Obtains the most-up-to-date common window state for the main window
const windowMainCommonStates = obtainCommonStates(windowMainElement, 'bm');
// Stores 13 bit flags unique to this window
let windowMainUniqueStatesMutable = 0; // Currently there are none, so this is the final verison
// Stores the X coordinates for the "Upload Template" coordinate input fields. Fallback is zero. Note: This is a user-specified field
const windowMainTemplateCoordinateX = Math.min(2047999, Math.max(0, (Number(windowMainElement?.querySelector('#bm-input-tx')?.value ?? 0) * 1000) + Number(windowMainElement?.querySelector('#bm-input-px')?.value ?? 0)));
// Stores the Y coordinates for the "Upload Template" coordinate input fields. Fallback is zero. Note: This is a user-specified field
const windowMainTemplateCoordinateY = Math.min(2047999, Math.max(0, (Number(windowMainElement?.querySelector('#bm-input-ty')?.value ?? 0) * 1000) + Number(windowMainElement?.querySelector('#bm-input-py')?.value ?? 0)));
// Save the window state, or fallback to zeros
const windowMainState = windowMainCommonStates
+ numberToEncoded(windowMainUniqueStatesMutable).padStart(2, this.zerothEncodingAlphabetCharacter).slice(-2) // Ensures this is always two characters
+ numberToEncoded(windowMainTemplateCoordinateX).padStart(4, this.zerothEncodingAlphabetCharacter).slice(-4) // Ensures this is always four characters
+ numberToEncoded(windowMainTemplateCoordinateY).padStart(4, this.zerothEncodingAlphabetCharacter).slice(-4); // Ensures this is always four characters
this.#windowStatesObjectEncoded['bm'] = windowMainState ?? this.zerothEncodingAlphabetCharacter.repeat(18);
// Obtains the window ID for the Credits window
const windowCreditsID = this.windowCredits?.windowID;
// Obtains the Credits window element itself
const windowCreditsElement = windowCreditsID ? document.querySelector('#' + this.windowCredits?.windowID) : undefined;
// Obtains the most-up-to-date common window state for the Credits window
const windowCreditsCommonStates = obtainCommonStates(windowCreditsElement, 'crdt');
// Stores 13 bit flags unique to this window
let windowCreditsUniqueStatesMutable = 0; // Currently there are none, so this is the final verison
// Save the window state, or fallback to zeros
const windowCreditsState = windowCreditsCommonStates
+ numberToEncoded(windowCreditsUniqueStatesMutable).padStart(2, this.zerothEncodingAlphabetCharacter).slice(-2);
this.#windowStatesObjectEncoded['crdt'] = windowCreditsState ?? this.zerothEncodingAlphabetCharacter.repeat(10);
// Obtains the window ID for the Template Wizard window
const windowWizardID = this.windowWizard?.windowID;
// Obtains the Template Wizard window element itself
const windowWizardElement = windowWizardID ? document.querySelector('#' + this.windowWizard?.windowID) : undefined;
// Obtains the most-up-to-date common window state for the Template Wizard window
const windowWizardCommonStates = obtainCommonStates(windowWizardElement, 'wzrd');
// Stores 13 bit flags unique to this window
let windowWizardUniqueStatesMutable = 0; // Currently there are none, so this is the final version
// Save the window state, or fallback to zeros
const windowWizardState = windowWizardCommonStates
+ numberToEncoded(windowWizardUniqueStatesMutable).padStart(2, this.zerothEncodingAlphabetCharacter).slice(-2);
this.#windowStatesObjectEncoded['wzrd'] = windowWizardState ?? this.zerothEncodingAlphabetCharacter.repeat(10);
// Obtains the window ID for the Settings Wizard window
const windowSettingsID = this.windowID;
// Obtains the Settings window element itself
const windowSettingsElement = windowSettingsID ? document.querySelector('#' + this?.windowID) : undefined; // Is there any point in doing optional chaining on `this`?
// Obtains the most-up-to-date common window states for the Settings window
const windowSettingsCommonStates = obtainCommonStates(windowSettingsElement, 'sett');
// Stores 13 bit flags unique to this window
let windowSettingsUniqueStatesMutable = 0; // Currently there are none, so this is the final version
// Save the window state, or fallback to zeros
const windowSettingsState = windowSettingsCommonStates
+ numberToEncoded(windowSettingsUniqueStatesMutable).padStart(2, this.zerothEncodingAlphabetCharacter).slice(-2);
this.#windowStatesObjectEncoded['sett'] = windowSettingsState ?? this.zerothEncodingAlphabetCharacter.repeat(10);
// Obtains the window ID for the Color Filter window
const windowFilterID = this.windowFilter?.windowID;
// Obtains the Color Filter window element itself
const windowFilterElement = windowFilterID ? document.querySelector('#' + this.windowFilter?.windowID) : undefined;
// Obtains the most-up-to-date common window state for the Color Filter window
const windowFilterCommonStates = obtainCommonStates(windowFilterElement, 'fltr');
// Stores 13 bit flags unique to this window
let windowFilterUniqueStatesMutable = 0;
// Obtains if the window is in "Windowed" mode
const windowFilterIsWindowed = windowFilterElement?.classList?.contains('bm-windowed');
// Modifies the bit flag for "Windowed" mode
windowFilterUniqueStatesMutable = set32BitPosition(windowCreditsUniqueStatesMutable, 0, windowFilterIsWindowed);
// Obtains if "Show Unused Colors" checkbox is `true`
let showUnusedColors = document.querySelector('#bm-filter-show-unused')?.checked ?? this.windowFilter?.showUnused ?? false;
// Modifies the bit flag for displaying unused colors
windowFilterUniqueStatesMutable = set32BitPosition(windowFilterUniqueStatesMutable, 1, showUnusedColors);
// Obtains the secondary sort selected by the user, or falls back to the previous choice
const selectedSortSecondaryIndex = document.querySelector('#bm-filter-sort-secondary')?.selectedIndex ?? this.windowFilter?.WStateSortFlagsToValues[this.windowFilter?.sortSecondary] - 11 ?? 0;
// Modifies the bit flag for the selected secondary sort
windowFilterUniqueStatesMutable = set32BitPosition(windowFilterUniqueStatesMutable, this.wStateFilterVarsFlags[selectedSortSecondaryIndex + 2], true);
// Obtains the primary sort selected by the user, or falls back to the previous choice
const selectedSortPrimaryIndex = document.querySelector('#bm-filter-sort-primary')?.selectedIndex ?? this.windowFilter?.WStateSortFlagsToValues[this.windowFilter?.sortPrimary] - 13 ?? 0;
// Modifies the bit flag for the selected primary sort
windowFilterUniqueStatesMutable = set32BitPosition(windowFilterUniqueStatesMutable, this.wStateFilterVarsFlags[selectedSortPrimaryIndex + 4], true);
// Save the window state, or fallback to zeros
const windowFilterState = windowFilterCommonStates
+ numberToEncoded(windowFilterUniqueStatesMutable).padStart(2, this.zerothEncodingAlphabetCharacter).slice(-2);
this.#windowStatesObjectEncoded['fltr'] = windowFilterState ?? this.zerothEncodingAlphabetCharacter.repeat(10);
}
/** Decodes & builds the window state object.
* This function parses user storage into a readable format,
* then passes it to the {@link SettingsManager}, which is the owner of the windows state object.
* @param {Object} windowState - The encoded state of all windows saved in user storage
* @since 0.92.23
*/
#decodeWindowStateToObject(windowState) {
console.log('Recieved window state to decode: ', windowState);
/** Decodes the common header data in each encoded value.
* This is an arrow function so code inside the function can easily access class-level variables (using `this`).
* @param {string} encodedString - The encoded value
* @returns {Array<number, number>}
* @since 0.92.23
*/
const decodeCommonStates = (encodedString) => {
// If the passed in encodedString is invalid...
if ((typeof encodedString !== 'string') || (encodedString.length == 0)) {
consoleWarn(`Could not decode common states of a window! Expected a 'string' that is ${this.commonStatesByteLength} bytes long, but recieved a '${typeof encodedString}' with value: ${encodedString}\nAssuming all common states are zeros...`);
encodedString = this.zerothEncodingAlphabetCharacter.repeat(this.commonStatesByteLength);
} // The data we are supposed to read is corrupt, so we can zero all bytes and continue as normal.
// Stores the "layer" this window is at, compared to zero
const drawDepth = encodedToNumber(encodedString.slice(0, 1));
// The bit flags 0 to 5
const bitFlags = encodedToNumber(encodedString.slice(1, 2));
const isWindowInDOM = (bitFlags & (1 << 0)) !== 0; // Stores the value of the 0th bit, as a Boolean
const isWindowMinimized = (bitFlags & (1 << 1)) !== 0; // Stores the value of the 1st bit, as a Boolean
const hasWindowBeenMoved = (bitFlags & (1 << 2)) !== 0; // Stores the value of the 2nd bit, as a Boolean
const xAxisSignIsNegative = (bitFlags & (1 << 3)) !== 0; // Stores the value of the 3rd bit, as a Boolean
const yAxisSignIsNegative = (bitFlags & (1 << 4)) !== 0; // Stores the value of the 4th bit, as a Boolean
const reservedCommonFlag = false; // Reserved
// Shift Translations
const xAxisShiftTrans = encodedToNumber(encodedString.slice(2, 5));
const yAxisShiftTrans = encodedToNumber(encodedString.slice(5, 8));
const commonStates = [drawDepth, isWindowInDOM, isWindowMinimized, hasWindowBeenMoved, xAxisSignIsNegative, yAxisSignIsNegative, reservedCommonFlag, xAxisShiftTrans, yAxisShiftTrans];
//console.log(commonStates);
return commonStates;
};
const mainWindowStateDefault = '!#!!!!!!!!!!!!!!!!'; // Default state of the main window
const creditsWindowStateDefault = this.zerothEncodingAlphabetCharacter.repeat(10); // Default state of the Credits Window
const wizardWindowStateDefault = this.zerothEncodingAlphabetCharacter.repeat(10); // Default state of the Template Wizard Window
const settingsWindowStateDefault = this.zerothEncodingAlphabetCharacter.repeat(10); // Default state of the Settings Window
const filterWindowStateDefault = '!!!!!!!!!6'; // Default state of the Color Filter Window
// Main Window
const mainWindowEncodedState = windowState['bm'] ?? mainWindowStateDefault; // The entire encoded window state. Fallback to default
const mainWindowEncodedCommon = mainWindowEncodedState?.slice(0, this.commonStatesByteLength); // The encoded window state for common variables
const mainWindowEncodedFlags = mainWindowEncodedState?.slice(this.commonStatesByteLength, 10); // The encoded window state for bit flags
const mainWindowTemplateCoordX = encodedToNumber(mainWindowEncodedState?.slice(10, 14)); // The numbers to store in the "Upload Template" input fields
const mainWindowTemplateCoordY = encodedToNumber(mainWindowEncodedState?.slice(14, 18)); // The numbers to store in the "Upload Template" input fields
const mainWindowDecoded32BitBooleanArray = numberUnsignedTo32BitBooleanArray(encodedToNumber(mainWindowEncodedFlags) >>> 0);
const mainWindowState =
decodeCommonStates(mainWindowEncodedCommon).concat(
mainWindowDecoded32BitBooleanArray.slice(0, 13), // If we don't clamp to the first 13 flags, we will return 19 additional flags that don't exist
mainWindowTemplateCoordX, mainWindowTemplateCoordY
);
// mainWindowState is an Array where each index is variable. The order is preserved.
// Credits Window
const creditsWindowEncodedState = windowState['crdt'] ?? creditsWindowStateDefault; // The entire encoded window state. Fallback to default
const creditsWindowEncodedCommon = creditsWindowEncodedState?.slice(0, this.commonStatesByteLength); // The encoded window state for common variables
const creditsWindowEncodedFlags = creditsWindowEncodedState?.slice(this.commonStatesByteLength, 10); // The encoded window state for bit flags
const creditsWindowDecoded32BitBooleanArray = numberUnsignedTo32BitBooleanArray(encodedToNumber(creditsWindowEncodedFlags) >>> 0);
const creditsWindowState =
decodeCommonStates(creditsWindowEncodedCommon).concat(
creditsWindowDecoded32BitBooleanArray.slice(0, 13) // If we don't clamp to the first 13 flags, we will return 19 additional flags that don't exist
);
// creditsWindowState is an Array where each index is a variable. The order is preserved.
// Template Wizard Window
const wizardWindowEncodedState = windowState['wzrd'] ?? wizardWindowStateDefault; // The entire encoded window state. Fallback to default
const wizardWindowEncodedCommon = wizardWindowEncodedState?.slice(0, this.commonStatesByteLength); // The encoded window state for common variables
const wizardWindowEncodedFlags = wizardWindowEncodedState?.slice(this.commonStatesByteLength, 10); // The encoded window state for bit flags
const wizardWindowDecoded32BitBooleanArray = numberUnsignedTo32BitBooleanArray(encodedToNumber(wizardWindowEncodedFlags) >>> 0);
const wizardWindowState =
decodeCommonStates(wizardWindowEncodedCommon).concat(
wizardWindowDecoded32BitBooleanArray.slice(0, 13) // If we don't clamp to the first 13 flags, we will return 19 additional flags that don't exist
);
// wizardWindowState is an Array where each index is a variable. The order is preserved.
// Settings Window
const settingsWindowEncodedState = windowState['sett'] ?? settingsWindowStateDefault; // The entire encoded window state. Fallback to default
const settingsWindowEncodedCommon = settingsWindowEncodedState?.slice(0, this.commonStatesByteLength); // The encoded window state for common variables
const settingsWindowEncodedFlags = settingsWindowEncodedState?.slice(this.commonStatesByteLength, 10); // The encoded window state for bit flags
const settingsWindowDecoded32BitBooleanArray = numberUnsignedTo32BitBooleanArray(encodedToNumber(settingsWindowEncodedFlags) >>> 0);
const settingsWindowState =
decodeCommonStates(settingsWindowEncodedCommon).concat(
settingsWindowDecoded32BitBooleanArray.slice(0, 13) // If we don't clamp to the first 13 flags, we will return 19 additional flags that don't exist
);
// settingsWindowState is an Array where each index is a variable. The order is preserved.
// Color Filter Window
const filterWindowEncodedState = windowState['fltr'] ?? filterWindowStateDefault; // The entire encoded window state. Fallback to default
const filterWindowEncodedCommon = filterWindowEncodedState?.slice(0, this.commonStatesByteLength); // The encoded window state for common variables
const filterWindowEncodedFlags = filterWindowEncodedState?.slice(this.commonStatesByteLength, 10); // The encoded window state for bit flags
const filterWindowDecoded32BitBooleanArray = numberUnsignedTo32BitBooleanArray(encodedToNumber(filterWindowEncodedFlags) >>> 0);
const filterWindowState =
decodeCommonStates(filterWindowEncodedCommon).concat(
filterWindowDecoded32BitBooleanArray.slice(0, 13) // If we don't clamp to the first 13 flags, we will return 19 additional flags that don't exist
);
// filterWindowState is an Array where each index is a variable. The order is preserved.
return {
'bm': mainWindowState,
'crdt': creditsWindowState,
'wzrd': wizardWindowState,
'sett': settingsWindowState,
'fltr': filterWindowState,
};
}
/** Build the "template" category of settings window
* @since 0.91.68
* @see WindowSettings#buildTemplate
*/
buildTemplate() {
this.window = this.addDiv({'class': 'bm-container'})
.addHeader(2, {'textContent': 'Template'}).buildElement()
.addHr().buildElement()
.addDiv({'class': 'bm-container', 'style': 'margin-left: 1.5ch;'})
.addCheckbox({'textContent': 'Template creation should skip transparent tiles'}, (instance, label, checkbox) => {
checkbox.checked = !this.userSettings?.flags?.includes('hl-noSkip'); // Makes the checkbox match the last stored user setting
checkbox.onchange = (event) => this.toggleFlag('hl-noSkip', !event.target.checked); // If the user wants to skip, then the checkbox is NOT checked
}).buildElement()
.addCheckbox({'innerHTML': 'Experimental: Template creation should <em>aggressively</em> skip transparent tiles'}, (instance, label, checkbox) => {
checkbox.checked = this.userSettings?.flags?.includes('hl-agSkip'); // Makes the checkbox match the last stored user setting
checkbox.onchange = (event) => this.toggleFlag('hl-agSkip', event.target.checked); // If the user wants to aggressively skip, then the checkbox is checked
}).buildElement()
.buildElement()
.buildElement()
}
/** Returns the decoded window states
* @since 0.92.69
* @returns {Object} An object containing window states
*/
getWindowStatesObject() {
console.log('#windowStatesObject: ', this.#windowStatesObject);
return this.#windowStatesObject;
}
/** Returns the corresponding variable's value from the window state.
* This was specifically so an enum value could be passed in as the `index`.
* @param {string} tinyID - The ID for the window that is ONLY used inside user storage
* @param {number} index - The Array index that contains the value
* @since 0.92.77
* @returns {number | boolean}
*/
getWindowStateVariable(tinyID, index) {
// If the passed in arguments are invalid
if ((typeof tinyID !== 'string') || (typeof index !== 'number')) {
consoleError(`Attempted to get window state variable with type (string, number), but recieved type (${typeof tinyID}, ${typeof index}) instead! Value: (${tinyID}, ${index})\nReturning zero...`);
return 0;
}
const windowState = this.#windowStatesObject?.[tinyID];
// If the passed in arguments are valid types, but an invalid Array index
if (!Number.isInteger(index) || (index < 0) || (index > windowState.length - 1)) {
consoleError(`Attempted to retrieve index ${index} in '${tinyID}' window state, but the index is out-of-bounds! Valid: 0 - ${windowState.length - 1}\n Returning zero...`);
return 0;
}
return windowState[index]; // Returns the value
}
/** Populates the windowMain variable with the WindowMain class.
* @param {WindowMain} windowMain - The WindowMain class instance
* @since 0.92.23
*/
setWindowMain(windowMain) {this.windowMain = windowMain;}
/** Populates the windowFilter variable with the WindowFilter class.
* @param {WindowFilter} windowFilter - The windowFilter class instance
* @since 0.94.17
*/
setWindowFilter(windowFilter) {this.windowFilter = windowFilter;}
/** Populates the windowCredits variable with the WindowCredits class.
* @param {WindowCredits} windowCredits - The windowCredits class instance
* @since 0.94.17
*/
setWindowCredits(windowCredits) {this.windowCredits = windowCredits;}
/** Populates the windowWizard variable with the WindowWizard class.
* @param {WindowWizard} windowWizard - The windowWizard class instance
* @since 0.94.17
*/
setWindowWizard(windowWizard) {this.windowWizard = windowWizard;}
/** Populates the templateManager variable with the templateManager class.
* @param {TemplateManager} templateManager - The templateManager class instance
* @since 0.92.22
*/
setTemplateManager(templateManager) {this.templateManager = templateManager;}
/** Populates the apiManager variable with the apiManager class.
* @param {ApiManager} apiManager - The apiManager class instance
* @since 0.92.23
*/
setApiManager(apiManager) {this.apiManager = apiManager;}
}