Set up skeleton of Template Wizard

This commit is contained in:
SwingTheVine 2026-02-24 23:00:00 -05:00
parent 9885a100c7
commit 34d3447138
10 changed files with 911 additions and 657 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -51,7 +51,7 @@
<a href="https://discord.gg/tpeBPy46hf" target="_blank" rel="noopener noreferrer"><img alt="Contact Me" src="https://img.shields.io/badge/Contact_Me-gray?style=flat&logo=Discord&logoColor=white&logoSize=auto&labelColor=cornflowerblue"></a>
<a href="https://bluemarble.lol/" target="_blank" rel="noopener noreferrer"><img alt="Blue Marble Website" src="https://img.shields.io/badge/Blue_Marble_Website-crqch-blue?style=flat&logo=globe&logoColor=white"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="WakaTime" src="https://img.shields.io/badge/Coding_Time-169hrs_20mins-blue?style=flat&logo=wakatime&logoColor=black&logoSize=auto&labelColor=white"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Patches" src="https://img.shields.io/badge/Total_Patches-931-black?style=flat"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Patches" src="https://img.shields.io/badge/Total_Patches-938-black?style=flat"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Lines of Code" src="https://img.shields.io/badge/Lines_Of_Code-498-blue?style=flat"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Total Comments" src="https://img.shields.io/badge/Lines_Of_Comments-498-blue?style=flat"></a>
<a href="" target="_blank" rel="noopener noreferrer"><img alt="Compression" src="https://img.shields.io/badge/Compression-70.19%25-blue"></a>

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "wplace-bluemarble",
"version": "0.88.433",
"version": "0.88.440",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wplace-bluemarble",
"version": "0.88.433",
"version": "0.88.440",
"devDependencies": {
"esbuild": "^0.25.0",
"jsdoc": "^4.0.5",

View file

@ -1,6 +1,6 @@
{
"name": "wplace-bluemarble",
"version": "0.88.433",
"version": "0.88.440",
"type": "module",
"homepage": "https://bluemarble.lol/",
"repository": {

View file

@ -2,7 +2,7 @@
// @name Blue Marble
// @name:en Blue Marble
// @namespace https://github.com/SwingTheVine/
// @version 0.88.433
// @version 0.88.440
// @description A userscript to automate and/or enhance the user experience on Wplace.live. Make sure to comply with the site's Terms of Service, and rules! This script is not affiliated with Wplace.live in any way, use at your own risk. This script is not affiliated with TamperMonkey. The author of this userscript is not responsible for any damages, issues, loss of data, or punishment that may occur as a result of using this script. This script is provided "as is" under the MPL-2.0 license. The "Blue Marble" icon is licensed under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication. The image is owned by NASA.
// @description:en A userscript to automate and/or enhance the user experience on Wplace.live. Make sure to comply with the site's Terms of Service, and rules! This script is not affiliated with Wplace.live in any way, use at your own risk. This script is not affiliated with TamperMonkey. The author of this userscript is not responsible for any damages, issues, loss of data, or punishment that may occur as a result of using this script. This script is provided "as is" under the MPL-2.0 license. The "Blue Marble" icon is licensed under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication. The image is owned by NASA.
// @author SwingTheVine

122
src/WindowWizard.js Normal file
View file

@ -0,0 +1,122 @@
import Overlay from "./Overlay";
import { escapeHTML } from "./utils";
/** Wizard that manages template updates & recovery
* @class WindowWizard
* @since 0.88.434
* @see {@link Overlay} for examples
*/
export default class WindowWizard extends Overlay {
/** Constructor for the Template Wizard window
* @param {string} name - The name of the userscript
* @param {string} version - The version of the userscript
* @param {string} schemaVersionBleedingEdge - The bleeding edge of schema versions for Blue Marble
* @since 0.88.434
* @see {@link Overlay#constructor} for examples
*/
constructor(name, version, schemaVersionBleedingEdge) {
super(name, version); // Executes the code in the Overlay constructor
this.window = null; // Contains the *window* DOM tree
this.windowID = 'bm-window-wizard'; // The ID attribute for this window
this.windowParent = document.body; // The parent of the window DOM tree
// Retrieves data from storage
this.currentJSON = JSON.parse(GM_getValue('bmTemplates', '{}')); // The current Blue Marble storage
this.scriptVersion = this.currentJSON?.scriptVersion; // Script version when template was created
this.schemaVersion = this.currentJSON?.schemaVersion; // Schema version when template was created
this.schemaHealth = undefined; // Current schema health. This is: 'Good', 'Poor', 'Bad', or 'Dead' for full match, MINOR mismatch, MAJOR mismatch, and unknown, respectively.
this.schemaVersionBleedingEdge = schemaVersionBleedingEdge; // Latest schema version
}
/** Spawns a Template Wizard window.
* If another template wizard window already exists, we DON'T spawn another!
* Parent/child relationships in the DOM structure below are indicated by indentation.
* @since 0.88.434
*/
buildWindow() {
// If a template wizard window already exists, throw an error and return early
if (document.querySelector(`#${this.windowID}`)) {
this.handleDisplayError('Template Wizard window already exists!');
return;
}
// Creates a new template wizard window
this.window = this.addDiv({'id': this.windowID, 'class': 'bm-window', 'style': 'z-index: 9001;'})
.addDragbar()
.addButton({'class': 'bm-button-circle', 'textContent': '▼', 'aria-label': 'Minimize window "Template Wizard"', 'data-button-status': 'expanded'}, (instance, button) => {
button.onclick = () => instance.handleMinimization(button);
button.ontouchend = () => {button.click()}; // Needed only to negate weird interaction with dragbar
}).buildElement()
.addDiv().buildElement() // Contains the minimized h1 element
.addButton({'class': 'bm-button-circle', 'textContent': '🞪', 'aria-label': 'Close window "Template Wizard"'}, (instance, button) => {
button.onclick = () => {document.querySelector(`#${this.windowID}`)?.remove();};
button.ontouchend = () => {button.click();}; // Needed only to negate weird interaction with dragbar
}).buildElement()
.buildElement()
.addDiv({'class': 'bm-window-content'})
.addDiv({'class': 'bm-container bm-center-vertically'})
.addHeader(1, {'textContent': 'Template Wizard'}).buildElement()
.buildElement()
.addHr().buildElement()
.addDiv({'class': 'bm-container'})
.addP({'id': 'bm-wizard-status', 'textContent': 'Loading template storage status...'}).buildElement()
.buildElement()
.addDiv({'class': 'bm-container bm-scrollable'})
.addSpan({'textContent': 'Detected templates:'}).buildElement()
// Detected templates will show up here
.buildElement()
.buildElement()
.buildElement().buildOverlay(this.windowParent);
this.#displaySchemaHealth();
}
/** Determines how "healthy" the template storage is.
* @since 0.88.436
*/
#displaySchemaHealth() {
// SemVer -> string[]
const schemaVersionArray = this.schemaVersion.split(/[-\.\+]/);
const schemaVersionBleedingEdgeArray = this.schemaVersionBleedingEdge.split(/[-\.\+]/);
// Calculates the health that is displayed as a banner
let schemaHealthBanner = '';
if (schemaVersionArray[0] == schemaVersionBleedingEdgeArray[0]) {
if (schemaVersionArray[1] == schemaVersionBleedingEdgeArray[1]) {
schemaHealthBanner = 'Template storage health: <b>Healthy!</b><br>No futher action required. (Reason: Semantic version matches)';
this.schemaHealth = 'Good';
} else {
schemaHealthBanner = 'Template storage health: <b>Poor!</b><br>You can still use your template, but some features may not work. It is recommended that you update Blue Marble\'s template storage. (Reason: MINOR version mismatch)';
this.schemaHealth = 'Poor';
}
} else {
schemaHealthBanner = 'Template storage health: <b>Dead!</b><br>Blue Marble can not load the template storage. (Reason: MAJOR version mismatch)';
this.schemaHealth = 'Dead';
}
// Display schema health to user
this.updateInnerHTML('#bm-wizard-status', `${schemaHealthBanner}<br>The current schema version (<b>${escapeHTML(this.schemaVersion)}</b>) was created during Blue Marble version <b>${escapeHTML(this.scriptVersion)}</b>.<br>The current Blue Marble version (<b>${escapeHTML(this.version)}</b>) requires schema version <b>${escapeHTML(this.schemaVersionBleedingEdge)}</b>.<br>If you don't want to upgrade the template storage (schema), then downgrade Blue Marble to version <b>${escapeHTML(this.scriptVersion)}</b>.`);
// If the schema health is Poor or Bad, then show update options
if ((this.schemaHealth == 'Poor') || (this.schemaHealth == 'Bad')) {
const buttonOptions = new Overlay(this.name, this.version);
buttonOptions.addDiv({'class': 'bm-container bm-flex-center bm-center-vertically'})
.addButton({'textContent': 'Recover (download) templates'}, (instance, button) => {
button.onclick = () => {
}
}).buildElement()
.addButton({'textContent': `Update template storage to ${this.schemaVersionBleedingEdge}`}, (instance, button) => {
button.onclick = () => {
}
}).buildElement()
.buildElement().buildOverlay(document.querySelector('#bm-wizard-status').parentNode)
}
}
}

View file

@ -1,5 +1,6 @@
import Template from "./Template";
import { base64ToUint8, colorpaletteForBlueMarble, numberToEncoded } from "./utils";
import WindowWizard from "./WindowWizard";
/** Manages the template system.
* This class handles all external requests for template modification, creation, and analysis.
@ -60,7 +61,7 @@ export default class TemplateManager {
this.name = name; // Name of userscript
this.version = version; // Version of userscript
this.overlay = overlay; // The main instance of the Overlay class
this.templatesVersion = '1.0.0'; // Version of JSON schema
this.schemaVersion = '1.1.0'; // Version of JSON schema
this.userID = null; // The ID of the current user
this.encodingBase = '!#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~'; // Characters to use for encoding/decoding
this.tileSize = 1000; // The number of pixels in a tile. Assumes the tile is square
@ -88,7 +89,7 @@ export default class TemplateManager {
return {
"whoami": this.name.replace(' ', ''), // Name of userscript without spaces
"scriptVersion": this.version, // Version of userscript
"schemaVersion": this.templatesVersion, // Version of JSON schema
"schemaVersion": this.schemaVersion, // Version of JSON schema
"templates": {} // The templates
};
}
@ -374,68 +375,102 @@ export default class TemplateManager {
console.log(`BlueMarble length: ${Object.keys(templates).length}`);
// Run only if there are templates saved
if (Object.keys(templates).length > 0) {
const schemaVersion = json?.schemaVersion;
const schemaVersionArray = schemaVersion.split(/[-\.\+]/); // SemVer -> string[]
const schemaVersionBleedingEdge = this.schemaVersion.split(/[-\.\+]/); // SemVer -> string[]
const scriptVersion = json?.scriptVersion;
// For each template...
for (const template in templates) {
console.log(`BlueMarble Template Schema: ${schemaVersion}; Script Version: ${scriptVersion}`);
const templateKey = template; // The identification key for the template. E.g., "0 $Z"
const templateValue = templates[template]; // The actual content of the template
console.log(`Template Key: ${templateKey}`);
// If 1.x.x
if (schemaVersionArray[0] == schemaVersionBleedingEdge[0]) {
if (templates.hasOwnProperty(template)) {
// If 1.1.x
if (schemaVersionArray[1] == schemaVersionBleedingEdge[1]) {
const templateKeyArray = templateKey.split(' '); // E.g., "0 $Z" -> ["0", "$Z"]
const sortID = Number(templateKeyArray?.[0]); // Sort ID of the template
const authorID = templateKeyArray?.[1] || '0'; // User ID of the person who exported the template
const displayName = templateValue.name || `Template ${sortID || ''}`; // Display name of the template
//const coords = templateValue?.coords?.split(',').map(Number); // "1,2,3,4" -> [1, 2, 3, 4]
loadSchemaVersion_1_x_x(); // Load 1.1.x schema
} else {
const pixelCount = {
total: templateValue.pixels.total,
colors: new Map(Object.entries(templateValue.pixels.colors).map(([key, value]) => [Number(key), value]))
};
// Spawns a new Template Wizard
const windowWizard = new WindowWizard(this.name, this.version, this.schemaVersion);
windowWizard.buildWindow();
const tilesbase64 = templateValue.tiles;
const templateTiles = {}; // Stores the template bitmap tiles for each tile.
const templateTiles32 = {}; // Stores the template Uint32Array tiles for each tile.
loadSchemaVersion_1_x_x(); // Load 1.x.x schema with 1.1.x loader, expecting some things to not function
}
} else {
// We don't know what the schema is. Unsupported?
const actualTileSize = this.tileSize * this.drawMult;
this.overlay.handleDisplayError(`Template version ${schemaVersion} is unsupported.\nUse Blue Marble version ${scriptVersion} or load a new template.`);
}
for (const tile in tilesbase64) {
console.log(tile);
if (tilesbase64.hasOwnProperty(tile)) {
const encodedTemplateBase64 = tilesbase64[tile];
const templateUint8Array = base64ToUint8(encodedTemplateBase64); // Base 64 -> Uint8Array
/** Loads version 1.0.0 of Blue Marble template storage
* @since 0.88.434
*/
async function loadSchemaVersion_1_x_x() {
const templateBlob = new Blob([templateUint8Array], { type: "image/png" }); // Uint8Array -> Blob
const templateBitmap = await createImageBitmap(templateBlob) // Blob -> Bitmap
templateTiles[tile] = templateBitmap;
// Converts to Uint32Array
const canvas = new OffscreenCanvas(actualTileSize, actualTileSize);
const context = canvas.getContext('2d');
context.drawImage(templateBitmap, 0, 0);
const imageData = context.getImageData(0, 0, templateBitmap.width, templateBitmap.height);
templateTiles32[tile] = new Uint32Array(imageData.data.buffer);
// Run only if there are templates saved
if (Object.keys(templates).length > 0) {
// For each template...
for (const template in templates) {
const templateKey = template; // The identification key for the template. E.g., "0 $Z"
const templateValue = templates[template]; // The actual content of the template
console.log(`Template Key: ${templateKey}`);
if (templates.hasOwnProperty(template)) {
const templateKeyArray = templateKey.split(' '); // E.g., "0 $Z" -> ["0", "$Z"]
const sortID = Number(templateKeyArray?.[0]); // Sort ID of the template
const authorID = templateKeyArray?.[1] || '0'; // User ID of the person who exported the template
const displayName = templateValue.name || `Template ${sortID || ''}`; // Display name of the template
//const coords = templateValue?.coords?.split(',').map(Number); // "1,2,3,4" -> [1, 2, 3, 4]
const pixelCount = {
total: templateValue.pixels.total,
colors: new Map(Object.entries(templateValue.pixels.colors).map(([key, value]) => [Number(key), value]))
};
const tilesbase64 = templateValue.tiles;
const templateTiles = {}; // Stores the template bitmap tiles for each tile.
const templateTiles32 = {}; // Stores the template Uint32Array tiles for each tile.
const actualTileSize = this.tileSize * this.drawMult;
for (const tile in tilesbase64) {
console.log(tile);
if (tilesbase64.hasOwnProperty(tile)) {
const encodedTemplateBase64 = tilesbase64[tile];
const templateUint8Array = base64ToUint8(encodedTemplateBase64); // Base 64 -> Uint8Array
const templateBlob = new Blob([templateUint8Array], { type: "image/png" }); // Uint8Array -> Blob
const templateBitmap = await createImageBitmap(templateBlob) // Blob -> Bitmap
templateTiles[tile] = templateBitmap;
// Converts to Uint32Array
const canvas = new OffscreenCanvas(actualTileSize, actualTileSize);
const context = canvas.getContext('2d');
context.drawImage(templateBitmap, 0, 0);
const imageData = context.getImageData(0, 0, templateBitmap.width, templateBitmap.height);
templateTiles32[tile] = new Uint32Array(imageData.data.buffer);
}
}
// Creates a new Template class instance
const template = new Template({
displayName: displayName,
sortID: sortID || this.templatesArray?.length || 0,
authorID: authorID || '',
//coords: coords,
});
template.pixelCount = pixelCount;
template.chunked = templateTiles;
template.chunked32 = templateTiles32;
this.templatesArray.push(template);
console.log(this.templatesArray);
console.log(`^^^ This ^^^`);
}
// Creates a new Template class instance
const template = new Template({
displayName: displayName,
sortID: sortID || this.templatesArray?.length || 0,
authorID: authorID || '',
//coords: coords,
});
template.pixelCount = pixelCount;
template.chunked = templateTiles;
template.chunked32 = templateTiles32;
this.templatesArray.push(template);
console.log(this.templatesArray);
console.log(`^^^ This ^^^`);
}
}
}