refactor: Modular Auto-image

This commit is contained in:
MiraclePatrickLumowa authored and GitHub committed 2025-09-18 04:22:00 +08:00
1 parent c52fb50c3d
commit a59aeb1351
12 files changed
+200852 -2180

No files matched your search

+124
View File
@@ -41,6 +41,130 @@ async function executeLocalScript(scriptName, tabId) {
try {
console.log(`Loading script: ${scriptName}`);
// Check if we need to inject dependencies first for Auto-Image.js
if (scriptName === 'Auto-Image.js') {
console.log('🔑 Auto-Image.js detected, ensuring dependencies are loaded first...');
try {
// First inject token-manager.js
const tokenManagerUrl = chrome.runtime.getURL('scripts/token-manager.js');
const tokenManagerResponse = await fetch(tokenManagerUrl);
if (tokenManagerResponse.ok) {
const tokenManagerCode = await tokenManagerResponse.text();
console.log('🔑 Token manager loaded, injecting first...');
// Execute token manager first
await chrome.scripting.executeScript({
target: { tabId: tabId },
world: "MAIN",
func: (code) => {
console.log('%c🔑 Loading WPlace Token Manager...', 'color: #4ade80; font-weight: bold;');
const script = document.createElement('script');
script.textContent = code;
document.head.appendChild(script);
script.remove();
console.log('%c✅ Token Manager loaded successfully', 'color: #4ade80; font-weight: bold;');
},
args: [tokenManagerCode]
});
console.log('✅ Token manager injected successfully');
} else {
console.warn('⚠️ Could not load token-manager.js, proceeding without it');
}
// Then inject image-processor.js
const imageProcessorUrl = chrome.runtime.getURL('scripts/image-processor.js');
const imageProcessorResponse = await fetch(imageProcessorUrl);
if (imageProcessorResponse.ok) {
const imageProcessorCode = await imageProcessorResponse.text();
console.log('🖼️ Image processor loaded, injecting second...');
// Execute image processor second
await chrome.scripting.executeScript({
target: { tabId: tabId },
world: "MAIN",
func: (code) => {
console.log('%c🖼️ Loading WPlace Image Processor...', 'color: #3b82f6; font-weight: bold;');
const script = document.createElement('script');
script.textContent = code;
document.head.appendChild(script);
script.remove();
console.log('%c✅ Image Processor loaded successfully', 'color: #3b82f6; font-weight: bold;');
},
args: [imageProcessorCode]
});
console.log('✅ Image processor injected successfully');
} else {
console.warn('⚠️ Could not load image-processor.js, proceeding without it');
}
// Then inject overlay-manager.js
const overlayManagerUrl = chrome.runtime.getURL('scripts/overlay-manager.js');
const overlayManagerResponse = await fetch(overlayManagerUrl);
if (overlayManagerResponse.ok) {
const overlayManagerCode = await overlayManagerResponse.text();
console.log('🎨 Overlay manager loaded, injecting third...');
// Execute overlay manager third
await chrome.scripting.executeScript({
target: { tabId: tabId },
world: "MAIN",
func: (code) => {
console.log('%c🎨 Loading WPlace Overlay Manager...', 'color: #8b5cf6; font-weight: bold;');
const script = document.createElement('script');
script.textContent = code;
document.head.appendChild(script);
script.remove();
console.log('%c✅ Overlay Manager loaded successfully', 'color: #8b5cf6; font-weight: bold;');
},
args: [overlayManagerCode]
});
console.log('✅ Overlay manager injected successfully');
} else {
console.warn('⚠️ Could not load overlay-manager.js, proceeding without it');
}
// Finally inject utils-manager.js
const utilsManagerUrl = chrome.runtime.getURL('scripts/utils-manager.js');
const utilsManagerResponse = await fetch(utilsManagerUrl);
if (utilsManagerResponse.ok) {
const utilsManagerCode = await utilsManagerResponse.text();
console.log('🛠️ Utils manager loaded, injecting fourth...');
// Execute utils manager fourth
await chrome.scripting.executeScript({
target: { tabId: tabId },
world: "MAIN",
func: (code) => {
console.log('%c🛠️ Loading WPlace Utils Manager...', 'color: #f59e0b; font-weight: bold;');
const script = document.createElement('script');
script.textContent = code;
document.head.appendChild(script);
script.remove();
console.log('%c✅ Utils Manager loaded successfully', 'color: #f59e0b; font-weight: bold;');
},
args: [utilsManagerCode]
});
console.log('✅ Utils manager injected successfully');
} else {
console.warn('⚠️ Could not load utils-manager.js, proceeding without it');
}
// Small delay to ensure dependencies are fully initialized
await new Promise(resolve => setTimeout(resolve, 200));
} catch (error) {
console.warn('⚠️ Error loading dependencies:', error);
}
}
// Determine script path - Script-manager files are in root, others in scripts/
let scriptUrl;
if (scriptName === 'Script-manager.js' || scriptName === 'Script-manager-fixed.js') {
+56
View File
@@ -0,0 +1,56 @@
import json
import base64
from PIL import Image
def extract_image_from_data(data, output_filename):
"""Extracts the original image from the imageData."""
image_data = data['imageData']
width = image_data['width']
height = image_data['height']
pixels = image_data['pixels']
# Create a new image with RGBA mode
img = Image.new('RGBA', (width, height))
# The pixel data is a flat list of R, G, B, A values
img.putdata([tuple(pixels[i:i+4]) for i in range(0, len(pixels), 4)])
img.save(output_filename)
print(f"Saved original image to {output_filename}")
def extract_painted_map(data, output_filename):
"""Extracts the painted map from the paintedMapPacked."""
painted_map = data['paintedMapPacked']
width = painted_map['width']
height = painted_map['height']
encoded_data = painted_map['data']
# Decode the base64 data
decoded_data = base64.b64decode(encoded_data)
# Create a color palette from the available colors
# The 'id' from the color list is the index for the color
color_palette = {color['id']: tuple(color['rgb']) for color in data['state']['availableColors']}
# Create a new image with RGB mode
img = Image.new('RGB', (width, height))
pixels = []
for byte in decoded_data:
# Get the color from the palette using the byte as the key
# Default to black if the color is not found
color = color_palette.get(byte, (0, 0, 0))
pixels.append(color)
img.putdata(pixels)
img.save(output_filename)
print(f"Saved painted map to {output_filename}")
if __name__ == "__main__":
# Load the JSON file
with open('wplace-bot-progress-2025-09-12T19-27-37.json', 'r') as f:
data = json.load(f)
# Extract and save the original image
extract_image_from_data(data, 'original_image.png')
# Extract and save the painted map
extract_painted_map(data, 'painted_map.png')
Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 B

File diff suppressed because it is too large. Load diff
+8388
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+26 -19
View File
@@ -1637,27 +1637,34 @@
const damagedPixels = [];
const { width, height, pixels } = state.imageData;
const ready = await overlayManager.waitForTiles(
state.region.x,
state.region.y,
width,
height,
state.startPosition.x,
state.startPosition.y,
state.autonomousMode ? 20000 : 15000
);
// Check if we're working with restored data - skip tile waiting for restored saves
const isRestoredData = state.availableColors && state.availableColors.length > 0 && state.colorsChecked;
if (!isRestoredData) {
const ready = await overlayManager.waitForTiles(
state.region.x,
state.region.y,
width,
height,
state.startPosition.x,
state.startPosition.y,
state.autonomousMode ? 20000 : 15000
);
if (!ready) {
Utils.addDebugLog('Failed to load required tiles for scanning', 'error');
if (state.autonomousMode) {
Utils.addDebugLog('Autonomous mode: will retry scan in 30 seconds', 'warning');
setTimeout(() => {
if (state.autoRepairEnabled && !state.stopFlag) {
scanForDamage();
}
}, 30000);
if (!ready) {
Utils.addDebugLog('Failed to load required tiles for scanning', 'error');
if (state.autonomousMode) {
Utils.addDebugLog('Autonomous mode: will retry scan in 30 seconds', 'warning');
setTimeout(() => {
if (state.autoRepairEnabled && !state.stopFlag) {
scanForDamage();
}
}, 30000);
}
return [];
}
return [];
} else {
Utils.addDebugLog('Using restored data - skipping tile wait for damage scan', 'info');
}
Utils.addDebugLog(`Scanning ${width}x${height} image for damage (transparent detection: ${state.paintTransparentPixels})...`, 'info');
+741
View File
@@ -0,0 +1,741 @@
// ==UserScript==
// @name WPlace Image Processor
// @namespace http://tampermonkey.net/
// @version 2025-09-16.1
// @description Image processing and color management for WPlace AutoBot
// @author Wbot
// @match https://wplace.live/*
// @grant none
// ==/UserScript==
/**
* ImageProcessor - Handles image loading, processing, color conversion, and dithering for WPlace AutoBot
* Extracted from Auto-Image.js for better modularity and reusability
*/
class ImageProcessor {
constructor(imageSrc = null) {
this.imageSrc = imageSrc;
this.img = null;
this.canvas = null;
this.ctx = null;
// Dithering buffers
this._ditherWorkBuf = null;
this._ditherEligibleBuf = null;
// Color cache for performance optimization
this._colorCache = new Map();
this._labCache = new Map();
// Configuration constants
this.TRANSPARENCY_THRESHOLD = 128;
this.WHITE_THRESHOLD = 230;
this.COLOR_CACHE_LIMIT = 15000;
}
/**
* Load image from source
* @returns {Promise<void>}
*/
async load() {
if (!this.imageSrc) {
throw new Error('No image source provided');
}
return new Promise((resolve, reject) => {
this.img = new Image();
this.img.crossOrigin = 'anonymous';
this.img.onload = () => {
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
this.canvas.width = this.img.width;
this.canvas.height = this.img.height;
this.ctx.drawImage(this.img, 0, 0);
resolve();
};
this.img.onerror = reject;
this.img.src = this.imageSrc;
});
}
/**
* Get image dimensions
* @returns {{width: number, height: number}}
*/
getDimensions() {
if (!this.canvas) {
throw new Error('Image not loaded. Call load() first.');
}
return {
width: this.canvas.width,
height: this.canvas.height,
};
}
/**
* Get pixel data from the image
* @returns {Uint8ClampedArray} RGBA pixel data
*/
getPixelData() {
if (!this.ctx) {
throw new Error('Image not loaded. Call load() first.');
}
return this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height).data;
}
/**
* Resize image to new dimensions
* @param {number} newWidth - Target width
* @param {number} newHeight - Target height
* @returns {Uint8ClampedArray} Resized image data
*/
resize(newWidth, newHeight) {
if (!this.canvas || !this.ctx) {
throw new Error('Image not loaded. Call load() first.');
}
const tempCanvas = document.createElement('canvas');
const tempCtx = tempCanvas.getContext('2d');
tempCanvas.width = newWidth;
tempCanvas.height = newHeight;
tempCtx.imageSmoothingEnabled = false;
tempCtx.drawImage(this.canvas, 0, 0, newWidth, newHeight);
this.canvas.width = newWidth;
this.canvas.height = newHeight;
this.ctx.imageSmoothingEnabled = false;
this.ctx.drawImage(tempCanvas, 0, 0);
return this.ctx.getImageData(0, 0, newWidth, newHeight).data;
}
/**
* Generate preview with resizing
* @param {number} width - Preview width
* @param {number} height - Preview height
* @returns {string} Base64 data URL of the preview
*/
generatePreview(width, height) {
if (!this.canvas) {
throw new Error('Image not loaded. Call load() first.');
}
const previewCanvas = document.createElement('canvas');
const previewCtx = previewCanvas.getContext('2d');
previewCanvas.width = width;
previewCanvas.height = height;
previewCtx.imageSmoothingEnabled = false;
previewCtx.drawImage(this.canvas, 0, 0, width, height);
return previewCanvas.toDataURL();
}
// =============================================
// COLOR PROCESSING METHODS
// =============================================
/**
* Convert RGB to LAB color space
* @param {number} r - Red value (0-255)
* @param {number} g - Green value (0-255)
* @param {number} b - Blue value (0-255)
* @returns {number[]} LAB values [L, a, b]
* @private
*/
_rgbToLab(r, g, b) {
// Normalize RGB values
let x = r / 255.0;
let y = g / 255.0;
let z = b / 255.0;
// Apply gamma correction
x = x > 0.04045 ? Math.pow((x + 0.055) / 1.055, 2.4) : x / 12.92;
y = y > 0.04045 ? Math.pow((y + 0.055) / 1.055, 2.4) : y / 12.92;
z = z > 0.04045 ? Math.pow((z + 0.055) / 1.055, 2.4) : z / 12.92;
// Convert to XYZ using sRGB matrix
let X = x * 0.4124564 + y * 0.3575761 + z * 0.1804375;
let Y = x * 0.2126729 + y * 0.7151522 + z * 0.0721750;
let Z = x * 0.0193339 + y * 0.1191920 + z * 0.9503041;
// Normalize for D65 illuminant
X /= 0.95047;
Y /= 1.00000;
Z /= 1.08883;
// Convert to LAB
X = X > 0.008856 ? Math.pow(X, 1/3) : (7.787 * X + 16/116);
Y = Y > 0.008856 ? Math.pow(Y, 1/3) : (7.787 * Y + 16/116);
Z = Z > 0.008856 ? Math.pow(Z, 1/3) : (7.787 * Z + 16/116);
const L = 116 * Y - 16;
const a = 500 * (X - Y);
const b2 = 200 * (Y - Z);
return [L, a, b2];
}
/**
* Get LAB values with caching
* @param {number} r - Red value
* @param {number} g - Green value
* @param {number} b - Blue value
* @returns {number[]} Cached LAB values
* @private
*/
_getLab(r, g, b) {
const key = (r << 16) | (g << 8) | b;
let v = this._labCache.get(key);
if (!v) {
v = this._rgbToLab(r, g, b);
this._labCache.set(key, v);
}
return v;
}
/**
* Find closest color in palette using specified algorithm
* @param {number} r - Red value
* @param {number} g - Green value
* @param {number} b - Blue value
* @param {Array} palette - Array of RGB arrays [[r,g,b], ...]
* @param {string} algorithm - 'legacy' or 'lab'
* @param {Object} options - Additional options for color matching
* @returns {number[]} Closest RGB color [r, g, b]
*/
findClosestPaletteColor(r, g, b, palette, algorithm = 'lab', options = {}) {
if (!palette || palette.length === 0) {
return [0, 0, 0];
}
const { enableChromaPenalty = false, chromaPenaltyWeight = 0.15 } = options;
if (algorithm === 'legacy') {
let menorDist = Infinity;
let cor = [0, 0, 0];
for (let i = 0; i < palette.length; i++) {
const [pr, pg, pb] = palette[i];
const rmean = (pr + r) / 2;
const rdiff = pr - r;
const gdiff = pg - g;
const bdiff = pb - b;
const dist = Math.sqrt(
(((512 + rmean) * rdiff * rdiff) >> 8) +
4 * gdiff * gdiff +
(((767 - rmean) * bdiff * bdiff) >> 8)
);
if (dist < menorDist) {
menorDist = dist;
cor = [pr, pg, pb];
}
}
return cor;
}
// LAB algorithm
const [Lt, at, bt] = this._getLab(r, g, b);
const targetChroma = Math.sqrt(at * at + bt * bt);
let best = null;
let bestDist = Infinity;
for (let i = 0; i < palette.length; i++) {
const [pr, pg, pb] = palette[i];
const [Lp, ap, bp] = this._getLab(pr, pg, pb);
const dL = Lt - Lp;
const da = at - ap;
const db = bt - bp;
let dist = dL * dL + da * da + db * db;
if (enableChromaPenalty && targetChroma > 20) {
const candChroma = Math.sqrt(ap * ap + bp * bp);
if (candChroma < targetChroma) {
const chromaDiff = targetChroma - candChroma;
dist += chromaDiff * chromaDiff * chromaPenaltyWeight;
}
}
if (dist < bestDist) {
bestDist = dist;
best = palette[i];
if (bestDist === 0) break;
}
}
return best || [0, 0, 0];
}
/**
* Check if a pixel is considered white based on threshold
* @param {number} r - Red value
* @param {number} g - Green value
* @param {number} b - Blue value
* @param {number} threshold - White threshold (default: 230)
* @returns {boolean} True if pixel is white
*/
isWhitePixel(r, g, b, threshold = this.WHITE_THRESHOLD) {
return r >= threshold && g >= threshold && b >= threshold;
}
/**
* Resolve target RGB to closest available color with caching
* @param {number[]} targetRgb - Target RGB array [r, g, b]
* @param {Array} availableColors - Available colors with id and rgb properties
* @param {Object} options - Matching options
* @returns {Object} {id: number|null, rgb: number[]}
*/
resolveColor(targetRgb, availableColors, options = {}) {
const {
exactMatch = false,
algorithm = 'lab',
enableChromaPenalty = false,
chromaPenaltyWeight = 0.15,
whiteThreshold = this.WHITE_THRESHOLD
} = options;
if (!availableColors || availableColors.length === 0) {
return { id: null, rgb: targetRgb };
}
const cacheKey = `${targetRgb[0]},${targetRgb[1]},${targetRgb[2]}|${algorithm}|${enableChromaPenalty ? 'c' : 'nc'}|${chromaPenaltyWeight}|${exactMatch ? 'exact' : 'closest'}`;
if (this._colorCache.has(cacheKey)) {
return this._colorCache.get(cacheKey);
}
// Check for exact match
if (exactMatch) {
const match = availableColors.find(
(c) => c.rgb[0] === targetRgb[0] && c.rgb[1] === targetRgb[1] && c.rgb[2] === targetRgb[2]
);
const result = match ? { id: match.id, rgb: [...match.rgb] } : { id: null, rgb: targetRgb };
this._colorCache.set(cacheKey, result);
return result;
}
// Check for white pixel matching
if (
targetRgb[0] >= whiteThreshold &&
targetRgb[1] >= whiteThreshold &&
targetRgb[2] >= whiteThreshold
) {
const whiteEntry = availableColors.find(
(c) => c.rgb[0] >= whiteThreshold && c.rgb[1] >= whiteThreshold && c.rgb[2] >= whiteThreshold
);
if (whiteEntry) {
const result = { id: whiteEntry.id, rgb: [...whiteEntry.rgb] };
this._colorCache.set(cacheKey, result);
return result;
}
}
// Find nearest color
let bestId = availableColors[0].id;
let bestRgb = [...availableColors[0].rgb];
let bestScore = Infinity;
if (algorithm === 'legacy') {
for (let i = 0; i < availableColors.length; i++) {
const c = availableColors[i];
const [r, g, b] = c.rgb;
const rmean = (r + targetRgb[0]) / 2;
const rdiff = r - targetRgb[0];
const gdiff = g - targetRgb[1];
const bdiff = b - targetRgb[2];
const dist = Math.sqrt(
(((512 + rmean) * rdiff * rdiff) >> 8) +
4 * gdiff * gdiff +
(((767 - rmean) * bdiff * bdiff) >> 8)
);
if (dist < bestScore) {
bestScore = dist;
bestId = c.id;
bestRgb = [...c.rgb];
if (dist === 0) break;
}
}
} else {
const [Lt, at, bt] = this._getLab(targetRgb[0], targetRgb[1], targetRgb[2]);
const targetChroma = Math.sqrt(at * at + bt * bt);
const penaltyWeight = enableChromaPenalty ? chromaPenaltyWeight : 0;
for (let i = 0; i < availableColors.length; i++) {
const c = availableColors[i];
const [r, g, b] = c.rgb;
const [L2, a2, b2] = this._getLab(r, g, b);
const dL = Lt - L2;
const da = at - a2;
const db = bt - b2;
let dist = dL * dL + da * da + db * db;
if (penaltyWeight > 0 && targetChroma > 20) {
const candChroma = Math.sqrt(a2 * a2 + b2 * b2);
if (candChroma < targetChroma) {
const cd = targetChroma - candChroma;
dist += cd * cd * penaltyWeight;
}
}
if (dist < bestScore) {
bestScore = dist;
bestId = c.id;
bestRgb = [...c.rgb];
if (dist === 0) break;
}
}
}
const result = { id: bestId, rgb: bestRgb };
this._colorCache.set(cacheKey, result);
// Limit cache size
if (this._colorCache.size > this.COLOR_CACHE_LIMIT) {
const firstKey = this._colorCache.keys().next().value;
this._colorCache.delete(firstKey);
}
return result;
}
// =============================================
// DITHERING METHODS (Floyd-Steinberg)
// =============================================
/**
* Ensure dithering buffers are properly sized
* @param {number} n - Number of pixels
* @returns {Object} {work: Float32Array, eligible: Uint8Array}
* @private
*/
_ensureDitherBuffers(n) {
if (!this._ditherWorkBuf || this._ditherWorkBuf.length !== n * 3) {
this._ditherWorkBuf = new Float32Array(n * 3);
}
if (!this._ditherEligibleBuf || this._ditherEligibleBuf.length !== n) {
this._ditherEligibleBuf = new Uint8Array(n);
}
return { work: this._ditherWorkBuf, eligible: this._ditherEligibleBuf };
}
/**
* Apply Floyd-Steinberg dithering to image data
* @param {Uint8ClampedArray} imageData - RGBA image data
* @param {number} width - Image width
* @param {number} height - Image height
* @param {Array} palette - Color palette for dithering
* @param {Object} options - Dithering options
* @returns {Object} {data: Uint8ClampedArray, totalValidPixels: number}
*/
applyFloydSteinbergDithering(imageData, width, height, palette, options = {}) {
const {
paintTransparentPixels = false,
paintWhitePixels = true,
transparencyThreshold = this.TRANSPARENCY_THRESHOLD,
whiteThreshold = this.WHITE_THRESHOLD,
algorithm = 'lab',
enableChromaPenalty = false,
chromaPenaltyWeight = 0.15,
mask = null
} = options;
const data = new Uint8ClampedArray(imageData);
const n = width * height;
const { work, eligible } = this._ensureDitherBuffers(n);
let totalValidPixels = 0;
// Initialize working buffers
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = y * width + x;
const i4 = idx * 4;
const r = data[i4];
const g = data[i4 + 1];
const b = data[i4 + 2];
const a = data[i4 + 3];
const masked = mask && mask[idx];
const isEligible =
!masked &&
(paintTransparentPixels || a >= transparencyThreshold) &&
(paintWhitePixels || !this.isWhitePixel(r, g, b, whiteThreshold));
eligible[idx] = isEligible ? 1 : 0;
work[idx * 3] = r;
work[idx * 3 + 1] = g;
work[idx * 3 + 2] = b;
if (!isEligible) {
data[i4 + 3] = 0; // Make ineligible pixels transparent
}
}
}
// Error diffusion function
const diffuse = (nx, ny, er, eg, eb, factor) => {
if (nx < 0 || nx >= width || ny < 0 || ny >= height) return;
const nidx = ny * width + nx;
if (!eligible[nidx]) return;
const base = nidx * 3;
work[base] = Math.min(255, Math.max(0, work[base] + er * factor));
work[base + 1] = Math.min(255, Math.max(0, work[base + 1] + eg * factor));
work[base + 2] = Math.min(255, Math.max(0, work[base + 2] + eb * factor));
};
// Apply Floyd-Steinberg dithering
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = y * width + x;
if (!eligible[idx]) continue;
const base = idx * 3;
const r0 = work[base];
const g0 = work[base + 1];
const b0 = work[base + 2];
const [nr, ng, nb] = this.findClosestPaletteColor(
r0, g0, b0, palette, algorithm, { enableChromaPenalty, chromaPenaltyWeight }
);
const i4 = idx * 4;
data[i4] = nr;
data[i4 + 1] = ng;
data[i4 + 2] = nb;
data[i4 + 3] = 255;
totalValidPixels++;
// Calculate and diffuse error
const er = r0 - nr;
const eg = g0 - ng;
const eb = b0 - nb;
diffuse(x + 1, y, er, eg, eb, 7 / 16);
diffuse(x - 1, y + 1, er, eg, eb, 3 / 16);
diffuse(x, y + 1, er, eg, eb, 5 / 16);
diffuse(x + 1, y + 1, er, eg, eb, 1 / 16);
}
}
return { data, totalValidPixels };
}
/**
* Apply simple color quantization without dithering
* @param {Uint8ClampedArray} imageData - RGBA image data
* @param {number} width - Image width
* @param {number} height - Image height
* @param {Array} palette - Color palette for quantization
* @param {Object} options - Quantization options
* @returns {Object} {data: Uint8ClampedArray, totalValidPixels: number}
*/
applySimpleQuantization(imageData, width, height, palette, options = {}) {
const {
paintTransparentPixels = false,
paintWhitePixels = true,
transparencyThreshold = this.TRANSPARENCY_THRESHOLD,
whiteThreshold = this.WHITE_THRESHOLD,
algorithm = 'lab',
enableChromaPenalty = false,
chromaPenaltyWeight = 0.15
} = options;
const data = new Uint8ClampedArray(imageData);
let totalValidPixels = 0;
for (let i = 0; i < data.length; i += 4) {
const r = data[i];
const g = data[i + 1];
const b = data[i + 2];
const a = data[i + 3];
const isEligible =
(paintTransparentPixels || a >= transparencyThreshold) &&
(paintWhitePixels || !this.isWhitePixel(r, g, b, whiteThreshold));
if (!isEligible) {
data[i + 3] = 0; // Make ineligible pixels transparent
continue;
}
const [nr, ng, nb] = this.findClosestPaletteColor(
r, g, b, palette, algorithm, { enableChromaPenalty, chromaPenaltyWeight }
);
data[i] = nr;
data[i + 1] = ng;
data[i + 2] = nb;
data[i + 3] = 255;
totalValidPixels++;
}
return { data, totalValidPixels };
}
// =============================================
// UTILITY METHODS
// =============================================
/**
* Extract available colors from DOM elements
* @returns {Array|null} Array of color objects with id and rgb properties
*/
extractAvailableColors() {
const colorElements = document.querySelectorAll('.color-option, [data-color-id]');
if (colorElements.length === 0) return null;
const colors = [];
colorElements.forEach((element) => {
const colorId = element.dataset.colorId || element.getAttribute('data-color-id');
const computedStyle = window.getComputedStyle(element);
const backgroundColor = computedStyle.backgroundColor;
if (backgroundColor && colorId) {
const rgbMatch = backgroundColor.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
if (rgbMatch) {
colors.push({
id: parseInt(colorId),
rgb: [parseInt(rgbMatch[1]), parseInt(rgbMatch[2]), parseInt(rgbMatch[3])]
});
}
}
});
return colors.length > 0 ? colors : null;
}
/**
* Create file uploader for images
* @returns {Promise<string>} Promise resolving to image data URL
*/
createImageUploader() {
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'image/png,image/jpeg';
input.onchange = () => {
if (input.files && input.files[0]) {
const fr = new FileReader();
fr.onload = () => resolve(fr.result);
fr.readAsDataURL(input.files[0]);
} else {
resolve(null);
}
};
input.click();
});
}
/**
* Extract available colors from DOM palette
* @param {Object} colorMap - The color map object (optional, will use window.CONFIG if not provided)
* @returns {Array|null} Array of available colors or null if none found
*/
extractAvailableColors(colorMap = null) {
const colorElements = document.querySelectorAll('.tooltip button[id^="color-"]');
if (colorElements.length === 0) {
console.log('❌ No color elements found on page');
return null;
}
// Use provided colorMap or fallback to window.CONFIG
const effectiveColorMap = colorMap || (window.CONFIG?.COLOR_MAP);
// Separate available and unavailable colors
const availableColors = [];
const unavailableColors = [];
Array.from(colorElements).forEach((el) => {
const id = Number.parseInt(el.id.replace('color-', ''));
if (id === 0) return; // Skip transparent color
const rgbStr = el.style.backgroundColor.match(/\d+/g);
if (!rgbStr || rgbStr.length < 3) {
console.warn(`Skipping color element ${el.id} — cannot parse RGB`);
return;
}
const rgb = rgbStr.map(Number);
// Find color name from COLOR_MAP
let name = `Unknown Color ${id}`;
if (effectiveColorMap) {
const colorInfo = Object.values(effectiveColorMap).find((color) => color.id === id);
name = colorInfo ? colorInfo.name : name;
}
const colorData = { id, name, rgb };
// Check if color is available (no SVG overlay means available)
if (!el.querySelector('svg')) {
availableColors.push(colorData);
} else {
unavailableColors.push(colorData);
}
});
// Console log detailed color information
console.log('=== CAPTURED COLORS STATUS ===');
console.log(`Total available colors: ${availableColors.length}`);
console.log(`Total unavailable colors: ${unavailableColors.length}`);
console.log(`Total colors scanned: ${availableColors.length + unavailableColors.length}`);
if (availableColors.length > 0) {
console.log('\n--- AVAILABLE COLORS ---');
availableColors.forEach((color, index) => {
console.log(
`${index + 1
}. ID: ${color.id}, Name: "${color.name}", RGB: (${color.rgb[0]}, ${color.rgb[1]}, ${color.rgb[2]})`
);
});
}
if (unavailableColors.length > 0) {
console.log('\n--- UNAVAILABLE COLORS ---');
unavailableColors.forEach((color, index) => {
console.log(
`${index + 1
}. ID: ${color.id}, Name: "${color.name}", RGB: (${color.rgb[0]}, ${color.rgb[1]}, ${color.rgb[2]}) [LOCKED]`
);
});
}
console.log('=== END COLOR STATUS ===');
return availableColors;
}
/**
* Clear all caches to free memory
*/
clearCaches() {
this._colorCache.clear();
this._labCache.clear();
this._ditherWorkBuf = null;
this._ditherEligibleBuf = null;
}
/**
* Clean up resources
*/
cleanup() {
this.clearCaches();
if (this.canvas) {
this.canvas.width = 0;
this.canvas.height = 0;
}
this.img = null;
this.canvas = null;
this.ctx = null;
}
}
// Create global instance
window.WPlaceImageProcessor = ImageProcessor;
// Create global instance for Auto-Image.js compatibility
window.globalImageProcessor = new ImageProcessor();
// Legacy compatibility - expose key methods globally for backward compatibility
window.ImageProcessor = ImageProcessor;
console.log('✅ WPlace Image Processor loaded and ready');
+837
View File
@@ -0,0 +1,837 @@
// ==UserScript==
// @name WPlace Overlay Manager
// @namespace http://tampermonkey.net/
// @version 2025-09-16.1
// @description Overlay management for WPlace AutoBot - handles tile processing, chunking, and rendering
// @author Wbot
// @match https://wplace.live/*
// @grant none
// ==/UserScript==
/**
* OverlayManager - Handles overlay processing, tile chunking, and canvas operations for WPlace AutoBot
* Extracted from Auto-Image.js for better modularity and reusability
*/
class OverlayManager {
constructor() {
this.isEnabled = false;
this.startCoords = null; // { region: {x, y}, pixel: {x, y} }
this.imageBitmap = null;
this.chunkedTiles = new Map(); // Map<"tileX,tileY", ImageBitmap>
this.originalTiles = new Map(); // Map<"tileX,ttileY", ImageBitmap> store latest original tile bitmaps
this.originalTilesData = new Map(); // Map<"tileX,tileY", {w,h,data:Uint8ClampedArray}> cache full ImageData for fast pixel reads
this.tileSize = 1000;
this.processPromise = null; // Track ongoing processing
this.lastProcessedHash = null; // Cache invalidation
this.workerPool = null; // Web worker pool for heavy processing
this.neededTilesForSave = null; // Set of tiles needed for save file restoration
}
/**
* Toggle overlay enabled state
* @returns {boolean} New enabled state
*/
toggle() {
this.isEnabled = !this.isEnabled;
console.log(`Overlay ${this.isEnabled ? 'enabled' : 'disabled'}.`);
return this.isEnabled;
}
/**
* Enable overlay
*/
enable() {
this.isEnabled = true;
// Force canvas refresh if script was loaded manually (not on startup)
// This ensures overlay works even when tiles are already cached
this._triggerCanvasRefreshIfNeeded();
// Also try direct canvas overlay approach as backup
this._attemptDirectCanvasOverlay();
}
/**
* Attempt to directly overlay on the existing canvas
* @private
*/
_attemptDirectCanvasOverlay() {
// Only try this if we have overlay data and it's been more than 5 seconds since page load
const timeSinceLoad = Date.now() - (window.performance?.timing?.navigationStart || 0);
if (timeSinceLoad > 5000 && this.chunkedTiles.size > 0) {
setTimeout(() => {
this._drawDirectOverlay();
}, 500);
}
}
/**
* Draw overlay directly on the main canvas
* @private
*/
_drawDirectOverlay() {
try {
const canvas = document.querySelector('canvas');
if (!canvas) {
return;
}
const ctx = canvas.getContext('2d');
if (!ctx) {
return;
}
// Store original composite operation
const originalComposite = ctx.globalCompositeOperation;
const originalAlpha = ctx.globalAlpha;
// Set overlay blending
ctx.globalCompositeOperation = 'source-over';
ctx.globalAlpha = window.state?.overlayOpacity || 0.6;
// Draw overlay chunks directly on canvas
this.chunkedTiles.forEach((bitmap, tileKey) => {
const [tileX, tileY] = tileKey.split(',').map(Number);
// Convert tile coordinates to canvas coordinates
// This assumes WPlace uses 1000px tiles
const canvasX = tileX * 1000;
const canvasY = tileY * 1000;
try {
ctx.drawImage(bitmap, canvasX, canvasY);
} catch (error) {
// Ignore individual tile draw errors
}
});
// Restore original settings
ctx.globalCompositeOperation = originalComposite;
ctx.globalAlpha = originalAlpha;
} catch (error) {
// Ignore direct overlay errors silently
}
}
/**
* Trigger canvas refresh for manual script execution
* @private
*/
_triggerCanvasRefreshIfNeeded() {
// Only trigger refresh if we have overlay data and it's been more than 5 seconds since page load
// (indicates manual execution rather than startup execution)
const timeSinceLoad = Date.now() - (window.performance?.timing?.navigationStart || 0);
if (timeSinceLoad > 5000 && this.chunkedTiles.size > 0) {
// Try multiple methods to refresh the canvas tiles
setTimeout(() => {
this._attemptCanvasRefresh();
}, 100);
}
}
/**
* Attempt to refresh canvas tiles using various methods
* @private
*/
_attemptCanvasRefresh() {
try {
// Method 1: Clear browser cache for tile URLs
if ('caches' in window) {
caches.keys().then(cacheNames => {
cacheNames.forEach(cacheName => {
caches.delete(cacheName);
});
});
}
// Method 2: Force browser to bypass cache by adding cache-busting parameter
// We need to intercept and modify tile requests to add cache-busting
const originalFetch = window.fetch;
let fetchOverrideActive = true;
window.fetch = async function(...args) {
const url = args[0] instanceof Request ? args[0].url : args[0];
if (fetchOverrideActive && typeof url === 'string' && url.match(/\/\d+\/\d+\.png/)) {
// Add cache-busting parameter
const separator = url.includes('?') ? '&' : '?';
const cacheBustUrl = `${url}${separator}_cb=${Date.now()}`;
if (args[0] instanceof Request) {
args[0] = new Request(cacheBustUrl, args[0]);
} else {
args[0] = cacheBustUrl;
}
}
return originalFetch.apply(this, args);
};
// Disable cache-busting after 10 seconds
setTimeout(() => {
fetchOverrideActive = false;
}, 10000);
// Method 3: Try to trigger a slight zoom change
const canvas = document.querySelector('canvas');
if (canvas) {
// Dispatch a wheel event to trigger tile refresh
const wheelEvent = new WheelEvent('wheel', {
deltaY: 1,
deltaMode: 0,
bubbles: true,
cancelable: true,
view: window
});
canvas.dispatchEvent(wheelEvent);
// Immediately counter it
setTimeout(() => {
const counterEvent = new WheelEvent('wheel', {
deltaY: -1,
deltaMode: 0,
bubbles: true,
cancelable: true,
view: window
});
canvas.dispatchEvent(counterEvent);
}, 50);
}
// Method 4: Try to trigger resize event
window.dispatchEvent(new Event('resize'));
} catch (error) {
console.warn('⚠️ Could not trigger canvas refresh:', error);
}
}
/**
* Disable overlay
*/
disable() {
this.isEnabled = false;
}
/**
* Clear all overlay data and disable
*/
clear() {
this.disable();
this.imageBitmap = null;
this.chunkedTiles.clear();
this.originalTiles.clear();
this.originalTilesData.clear();
this.lastProcessedHash = null;
if (this.processPromise) {
this.processPromise = null;
}
}
/**
* Set the image bitmap for overlay processing
* @param {ImageBitmap} imageBitmap - The image to use for overlay
*/
async setImage(imageBitmap) {
this.imageBitmap = imageBitmap;
this.lastProcessedHash = null; // Invalidate cache
if (this.imageBitmap && this.startCoords) {
await this.processImageIntoChunks();
}
}
/**
* Set the position for overlay rendering
* @param {Object} startPosition - Starting pixel position {x, y}
* @param {Object} region - Region coordinates {x, y}
*/
async setPosition(startPosition, region) {
if (!startPosition || !region) {
this.startCoords = null;
this.chunkedTiles.clear();
this.lastProcessedHash = null;
return;
}
this.startCoords = { region, pixel: startPosition };
this.lastProcessedHash = null; // Invalidate cache
if (this.imageBitmap) {
await this.processImageIntoChunks();
}
}
/**
* Generate hash for cache invalidation
* @returns {string|null} Hash string or null if no data
* @private
*/
_generateProcessHash() {
if (!this.imageBitmap || !this.startCoords) return null;
const { width, height } = this.imageBitmap;
const { x: px, y: py } = this.startCoords.pixel;
const { x: rx, y: ry } = this.startCoords.region;
// Access global state for overlay settings
const blueMarbleEnabled = window.state?.blueMarbleEnabled || false;
const overlayOpacity = window.state?.overlayOpacity || 0.6;
return `${width}x${height}_${px},${py}_${rx},${ry}_${blueMarbleEnabled}_${overlayOpacity}`;
}
/**
* Process image into chunks with caching and batch processing
*/
async processImageIntoChunks() {
if (!this.imageBitmap || !this.startCoords) {
console.warn('🚫 Cannot process chunks - missing imageBitmap or startCoords');
return;
}
console.log('🔄 Processing image into chunks...', {
imageSize: `${this.imageBitmap.width}x${this.imageBitmap.height}`,
startCoords: this.startCoords
});
// Check if we're already processing to avoid duplicate work
if (this.processPromise) {
console.log('⏳ Already processing chunks - waiting...');
return this.processPromise;
}
// Check cache validity
const currentHash = this._generateProcessHash();
if (this.lastProcessedHash === currentHash && this.chunkedTiles.size > 0) {
// Using cached overlay chunks
return;
}
// Start processing
console.log('🚀 Starting new chunk processing...');
this.processPromise = this._doProcessImageIntoChunks();
try {
await this.processPromise;
this.lastProcessedHash = currentHash;
console.log(`✅ Chunk processing complete - ${this.chunkedTiles.size} tiles created`);
} finally {
this.processPromise = null;
}
}
/**
* Internal method to process image into chunks
* @private
*/
async _doProcessImageIntoChunks() {
const startTime = performance.now();
this.chunkedTiles.clear();
const { width: imageWidth, height: imageHeight } = this.imageBitmap;
const { x: startPixelX, y: startPixelY } = this.startCoords.pixel;
const { x: startRegionX, y: startRegionY } = this.startCoords.region;
const { startTileX, startTileY, endTileX, endTileY } = this.calculateTileRange(
startRegionX,
startRegionY,
startPixelX,
startPixelY,
imageWidth,
imageHeight,
this.tileSize
);
const totalTiles = (endTileX - startTileX + 1) * (endTileY - startTileY + 1);
console.log(`🔄 Processing ${totalTiles} overlay tiles...`);
// Process tiles in batches to avoid blocking the main thread
const batchSize = 4; // Process 4 tiles at a time
const tilesToProcess = [];
for (let ty = startTileY; ty <= endTileY; ty++) {
for (let tx = startTileX; tx <= endTileX; tx++) {
tilesToProcess.push({ tx, ty });
}
}
// Process tiles in batches with yielding
for (let i = 0; i < tilesToProcess.length; i += batchSize) {
const batch = tilesToProcess.slice(i, i + batchSize);
await Promise.all(
batch.map(async ({ tx, ty }) => {
const tileKey = `${tx},${ty}`;
const chunkBitmap = await this._processTile(
tx,
ty,
imageWidth,
imageHeight,
startPixelX,
startPixelY,
startRegionX,
startRegionY
);
if (chunkBitmap) {
this.chunkedTiles.set(tileKey, chunkBitmap);
}
})
);
// Yield control to prevent blocking
if (i + batchSize < tilesToProcess.length) {
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
const processingTime = performance.now() - startTime;
console.log(
`✅ Overlay processed ${this.chunkedTiles.size} tiles in ${Math.round(processingTime)}ms`
);
}
/**
* Process a single tile
* @param {number} tx - Tile X coordinate
* @param {number} ty - Tile Y coordinate
* @param {number} imageWidth - Image width
* @param {number} imageHeight - Image height
* @param {number} startPixelX - Starting pixel X
* @param {number} startPixelY - Starting pixel Y
* @param {number} startRegionX - Starting region X
* @param {number} startRegionY - Starting region Y
* @returns {Promise<ImageBitmap|null>} Processed tile bitmap
* @private
*/
async _processTile(
tx,
ty,
imageWidth,
imageHeight,
startPixelX,
startPixelY,
startRegionX,
startRegionY
) {
const tileKey = `${tx},${ty}`;
// Calculate the portion of the image that overlaps with this tile
const imgStartX = (tx - startRegionX) * this.tileSize - startPixelX;
const imgStartY = (ty - startRegionY) * this.tileSize - startPixelY;
// Crop coordinates within the source image
const sX = Math.max(0, imgStartX);
const sY = Math.max(0, imgStartY);
const sW = Math.min(imageWidth - sX, this.tileSize - (sX - imgStartX));
const sH = Math.min(imageHeight - sY, this.tileSize - (sY - imgStartY));
if (sW <= 0 || sH <= 0) return null;
// Destination coordinates on the new chunk canvas
const dX = Math.max(0, -imgStartX);
const dY = Math.max(0, -imgStartY);
const chunkCanvas = new OffscreenCanvas(this.tileSize, this.tileSize);
const chunkCtx = chunkCanvas.getContext('2d');
chunkCtx.imageSmoothingEnabled = false;
chunkCtx.drawImage(this.imageBitmap, sX, sY, sW, sH, dX, dY, sW, sH);
// Blue marble effect with faster pixel manipulation
const blueMarbleEnabled = window.state?.blueMarbleEnabled || false;
if (blueMarbleEnabled) {
const imageData = chunkCtx.getImageData(dX, dY, sW, sH);
const data = imageData.data;
// Faster pixel manipulation using typed arrays
for (let i = 0; i < data.length; i += 4) {
const pixelIndex = i / 4;
const pixelY = Math.floor(pixelIndex / sW);
const pixelX = pixelIndex % sW;
if ((pixelX + pixelY) % 2 === 0 && data[i + 3] > 0) {
data[i + 3] = 0; // Set alpha to 0
}
}
chunkCtx.putImageData(imageData, dX, dY);
}
return await chunkCanvas.transferToImageBitmap();
}
/**
* Process and respond to tile requests with overlay compositing
* @param {Object} eventData - Event data containing endpoint, blobID, and blobData
*/
async processAndRespondToTileRequest(eventData) {
const { endpoint, blobID, blobData } = eventData;
let finalBlob = blobData;
if (this.isEnabled && this.chunkedTiles.size > 0) {
const tileMatch = endpoint.match(/(\d+)\/(\d+)\.png/);
if (tileMatch) {
const tileX = parseInt(tileMatch[1], 10);
const tileY = parseInt(tileMatch[2], 10);
const tileKey = `${tileX},${tileY}`;
const chunkBitmap = this.chunkedTiles.get(tileKey);
// Also store the original tile bitmap for later pixel color checks
try {
const originalBitmap = await createImageBitmap(blobData);
this.originalTiles.set(tileKey, originalBitmap);
// Cache full ImageData for fast pixel access (avoid repeated drawImage/getImageData)
try {
let canvas, ctx;
if (typeof OffscreenCanvas !== 'undefined') {
canvas = new OffscreenCanvas(originalBitmap.width, originalBitmap.height);
ctx = canvas.getContext('2d');
} else {
canvas = document.createElement('canvas');
canvas.width = originalBitmap.width;
canvas.height = originalBitmap.height;
ctx = canvas.getContext('2d');
}
ctx.imageSmoothingEnabled = false;
ctx.drawImage(originalBitmap, 0, 0);
const imgData = ctx.getImageData(0, 0, originalBitmap.width, originalBitmap.height);
// Store typed array copy to avoid retaining large canvas
this.originalTilesData.set(tileKey, {
w: originalBitmap.width,
h: originalBitmap.height,
data: new Uint8ClampedArray(imgData.data),
});
} catch (e) {
// If ImageData extraction fails, still keep the bitmap as fallback
console.warn('OverlayManager: could not cache ImageData for', tileKey, e);
}
} catch (e) {
console.warn('OverlayManager: could not create original bitmap for', tileKey, e);
}
if (chunkBitmap) {
try {
// Use faster compositing for better performance
finalBlob = await this._compositeTileOptimized(blobData, chunkBitmap);
} catch (e) {
console.error('Error compositing overlay:', e);
// Fallback to original tile on error
finalBlob = blobData;
}
}
}
}
// Send the (possibly modified) blob back to the injected script
window.postMessage(
{
source: 'auto-image-overlay',
blobID: blobID,
blobData: finalBlob,
},
'*'
);
}
/**
* Get pixel color from a tile
* @param {number} tileX - Tile X coordinate
* @param {number} tileY - Tile Y coordinate
* @param {number} pixelX - Pixel X within tile
* @param {number} pixelY - Pixel Y within tile
* @returns {Promise<Array|null>} [r,g,b,a] or null
*/
async getTilePixelColor(tileX, tileY, pixelX, pixelY) {
const tileKey = `${tileX},${tileY}`;
// Get transparency threshold from global state or config
const alphaThresh = window.state?.customTransparencyThreshold ||
window.CONFIG?.TRANSPARENCY_THRESHOLD || 10;
// 1. Prefer cached ImageData if available
const cached = this.originalTilesData.get(tileKey);
if (cached && cached.data && cached.w > 0 && cached.h > 0) {
const x = Math.max(0, Math.min(cached.w - 1, pixelX));
const y = Math.max(0, Math.min(cached.h - 1, pixelY));
const idx = (y * cached.w + x) * 4;
const d = cached.data;
const a = d[idx + 3];
const paintTransparentPixels = window.state?.paintTransparentPixels || false;
if (!paintTransparentPixels && a < alphaThresh) {
// Treat as transparent / unavailable
return null;
}
return [d[idx], d[idx + 1], d[idx + 2], a];
}
// 2. Fallback: use bitmap, with retry
const maxRetries = 3;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
const bitmap = this.originalTiles.get(tileKey);
if (!bitmap) {
if (attempt === maxRetries) {
console.warn('OverlayManager: no bitmap for', tileKey, 'after', maxRetries, 'attempts');
} else {
await this._sleep(50 * attempt); // exponential delay
}
continue;
}
try {
let canvas, ctx;
if (typeof OffscreenCanvas !== 'undefined') {
canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
ctx = canvas.getContext('2d');
} else {
canvas = document.createElement('canvas');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
ctx = canvas.getContext('2d');
}
ctx.imageSmoothingEnabled = false;
ctx.drawImage(bitmap, 0, 0);
const x = Math.max(0, Math.min(bitmap.width - 1, pixelX));
const y = Math.max(0, Math.min(bitmap.height - 1, pixelY));
const data = ctx.getImageData(x, y, 1, 1).data;
const a = data[3];
const paintTransparentPixels = window.state?.paintTransparentPixels || false;
if (!paintTransparentPixels && a < alphaThresh) {
return null;
}
return [data[0], data[1], data[2], a];
} catch (e) {
console.warn('OverlayManager: failed to read pixel (attempt', attempt, ')', tileKey, e);
if (attempt < maxRetries) {
await this._sleep(50 * attempt);
} else {
console.error(
'OverlayManager: failed to read pixel after',
maxRetries,
'attempts',
tileKey
);
}
}
}
// 3. If everything fails, return null
return null;
}
/**
* Composite tile with overlay using optimized rendering
* @param {Blob} originalBlob - Original tile blob
* @param {ImageBitmap} overlayBitmap - Overlay bitmap
* @returns {Promise<Blob>} Composited tile blob
* @private
*/
async _compositeTileOptimized(originalBlob, overlayBitmap) {
const originalBitmap = await createImageBitmap(originalBlob);
const canvas = new OffscreenCanvas(originalBitmap.width, originalBitmap.height);
const ctx = canvas.getContext('2d');
// Disable antialiasing for pixel-perfect rendering
ctx.imageSmoothingEnabled = false;
// Draw original tile first
ctx.drawImage(originalBitmap, 0, 0);
// Set opacity and draw overlay with optimized blend mode
const overlayOpacity = window.state?.overlayOpacity || 0.6;
ctx.globalAlpha = overlayOpacity;
ctx.globalCompositeOperation = 'source-over';
ctx.drawImage(overlayBitmap, 0, 0);
// Use faster blob conversion with compression settings
return await canvas.convertToBlob({
type: 'image/png',
quality: 0.95, // Slight compression for faster processing
});
}
/**
* Wait until all required tiles are loaded and cached
* @param {number} startRegionX - Starting region X
* @param {number} startRegionY - Starting region Y
* @param {number} pixelWidth - Image width in pixels
* @param {number} pixelHeight - Image height in pixels
* @param {number} startPixelX - Starting pixel X offset
* @param {number} startPixelY - Starting pixel Y offset
* @param {number} timeoutMs - Timeout in milliseconds
* @returns {Promise<boolean>} True if tiles are ready
*/
async waitForTiles(
startRegionX,
startRegionY,
pixelWidth,
pixelHeight,
startPixelX = 0,
startPixelY = 0,
timeoutMs = 10000
) {
const { startTileX, startTileY, endTileX, endTileY } = this.calculateTileRange(
startRegionX,
startRegionY,
startPixelX,
startPixelY,
pixelWidth,
pixelHeight,
this.tileSize
);
const requiredTiles = [];
for (let ty = startTileY; ty <= endTileY; ty++) {
for (let tx = startTileX; tx <= endTileX; tx++) {
requiredTiles.push(`${tx},${ty}`);
}
}
if (requiredTiles.length === 0) return true;
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
const stopFlag = window.state?.stopFlag || false;
if (stopFlag) {
console.log('waitForTiles: stopped by user');
return false;
}
const missing = requiredTiles.filter((key) => !this.originalTiles.has(key));
if (missing.length === 0) {
console.log(`✅ All ${requiredTiles.length} required tiles are loaded`);
return true;
}
await this._sleep(100);
}
console.warn(`❌ Timeout waiting for tiles: ${requiredTiles.length} required,
${requiredTiles.filter((k) => this.originalTiles.has(k)).length} loaded`);
return false;
}
/**
* Calculate tile range for given parameters
* @param {number} startRegionX - Starting region X
* @param {number} startRegionY - Starting region Y
* @param {number} startPixelX - Starting pixel X
* @param {number} startPixelY - Starting pixel Y
* @param {number} width - Width in pixels
* @param {number} height - Height in pixels
* @param {number} tileSize - Size of each tile (default 1000)
* @returns {{startTileX: number, startTileY: number, endTileX: number, endTileY: number}}
*/
calculateTileRange(
startRegionX,
startRegionY,
startPixelX,
startPixelY,
width,
height,
tileSize = 1000
) {
const endPixelX = startPixelX + width;
const endPixelY = startPixelY + height;
return {
startTileX: startRegionX + Math.floor(startPixelX / tileSize),
startTileY: startRegionY + Math.floor(startPixelY / tileSize),
endTileX: startRegionX + Math.floor((endPixelX - 1) / tileSize),
endTileY: startRegionY + Math.floor((endPixelY - 1) / tileSize),
};
}
/**
* Preload tiles for save file coordinates to populate originalTiles cache
* This fixes the "no bitmap" error when loading save files
* @param {Object} startPosition - The start position from save file
* @param {Object} region - The region from save file
* @param {number} width - Image width
* @param {number} height - Image height
*/
/**
* Preload tiles for save file coordinates to populate originalTiles cache
* This fixes the "no bitmap" error when loading save files
* For WPlace, we can't manually fetch tiles like r/place - we need to trigger natural tile loading
* @param {Object} startPosition - The start position from save file
* @param {Object} region - The region from save file
* @param {number} width - Image width
* @param {number} height - Image height
*/
async preloadTilesForSaveFile(startPosition, region, width, height) {
if (!startPosition || !region || !width || !height) {
console.warn('OverlayManager: Missing parameters for tile preload');
return;
}
try {
// Calculate tiles that need to be loaded based on image area
const tileSize = 1000; // WPlace uses 1000px tiles, not 256px like r/place
const tiles = this.calculateTileRange(
region.x, region.y,
startPosition.x, startPosition.y,
width, height,
tileSize
);
console.log(`🔄 WPlace: Need to preload ${(tiles.endTileX - tiles.startTileX + 1) * (tiles.endTileY - tiles.startTileY + 1)} tiles for save file...`);
// For WPlace, instead of manually fetching tiles, we need to wait for natural tile loading
// The browser will request tiles as the user navigates, and we intercept them
// For now, just log the tiles we need and let the overlay work with what's available
const neededTiles = [];
for (let tileX = tiles.startTileX; tileX <= tiles.endTileX; tileX++) {
for (let tileY = tiles.startTileY; tileY <= tiles.endTileY; tileY++) {
neededTiles.push(`${tileX},${tileY}`);
}
}
console.log(`📍 WPlace: Will use tiles when available: ${neededTiles.slice(0, 5).join(', ')}${neededTiles.length > 5 ? ` and ${neededTiles.length - 5} more...` : ''}`);
// Store the needed tiles list for reference
this.neededTilesForSave = new Set(neededTiles);
console.log('✅ WPlace: Tile preload setup complete - tiles will populate as user navigates');
} catch (error) {
console.error('❌ Failed to setup tile preload for save file:', error);
}
}
/**
* Sleep utility
* @param {number} ms - Milliseconds to sleep
* @returns {Promise<void>}
* @private
*/
_sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Clean up resources
*/
cleanup() {
this.clear();
if (this.workerPool) {
// Clean up worker pool if implemented
this.workerPool = null;
}
}
}
// Create global instance
window.WPlaceOverlayManager = OverlayManager;
// Create global instance for Auto-Image.js compatibility
window.globalOverlayManager = new OverlayManager();
// Legacy compatibility - expose key methods globally for backward compatibility
window.OverlayManager = OverlayManager;
console.log('✅ WPlace Overlay Manager loaded and ready');
+846
View File
@@ -0,0 +1,846 @@
// ==UserScript==
// @name WPlace Token Manager
// @namespace http://tampermonkey.net/
// @version 2025-09-16.1
// @description Turnstile token management for WPlace AutoBot
// @author Wbot
// @match https://wplace.live/*
// @grant none
// ==/UserScript==
/**
* TokenManager - Handles Turnstile token generation, caching, and validation for WPlace AutoBot
* Extracted from Auto-Image.js for better modularity and reusability
*/
class TokenManager {
constructor() {
// Token state
this.turnstileToken = null;
this.tokenExpiryTime = 0;
this.tokenGenerationInProgress = false;
this._resolveToken = null;
this.tokenPromise = new Promise((resolve) => {
this._resolveToken = resolve;
});
// Configuration constants
this.TOKEN_LIFETIME = 240000; // 4 minutes (tokens typically last 5 min, use 4 for safety)
this.MAX_RETRIES = 10;
this.MAX_BATCH_RETRIES = 10; // Maximum attempts for batch sending
// Turnstile widget state
this.turnstileLoaded = false;
this._turnstileContainer = null;
this._turnstileOverlay = null;
this._turnstileWidgetId = null;
this._lastSitekey = null;
this._cachedSitekey = null;
// Retry counter
this.retryCount = 0;
// Initialize message listener for token capture
this._initializeTokenCapture();
}
/**
* Initialize token capture from injected script
*/
_initializeTokenCapture() {
window.addEventListener('message', (event) => {
const { source, token } = event.data;
if (source === 'turnstile-capture' && token) {
this.setTurnstileToken(token);
// Show success message if UI is expecting it
if (document.querySelector('#statusText')?.textContent.includes('CAPTCHA')) {
this._showAlert('Token captured successfully', 'success');
}
}
});
}
/**
* Set a new Turnstile token with expiry
* @param {string} token - The Turnstile token
*/
setTurnstileToken(token) {
if (this._resolveToken) {
this._resolveToken(token);
this._resolveToken = null;
}
this.turnstileToken = token;
this.tokenExpiryTime = Date.now() + this.TOKEN_LIFETIME;
console.log('✅ Turnstile token set successfully');
}
/**
* Check if current token is valid
* @returns {boolean} True if token exists and not expired
*/
isTokenValid() {
return this.turnstileToken && Date.now() < this.tokenExpiryTime;
}
/**
* Invalidate current token
*/
invalidateToken() {
this.turnstileToken = null;
this.tokenExpiryTime = 0;
console.log('🗑️ Token invalidated, will force fresh generation');
}
/**
* Ensure a valid token is available, generating if necessary
* @param {boolean} forceRefresh - Force generation of new token
* @returns {Promise<string|null>} The token or null if failed
*/
async ensureToken(forceRefresh = false) {
// Return cached token if still valid and not forcing refresh
if (this.isTokenValid() && !forceRefresh) {
return this.turnstileToken;
}
// Invalidate token if forcing refresh
if (forceRefresh) this.invalidateToken();
// Avoid multiple simultaneous token generations
if (this.tokenGenerationInProgress) {
console.log('🔄 Token generation already in progress, waiting...');
await this._sleep(2000);
return this.isTokenValid() ? this.turnstileToken : null;
}
this.tokenGenerationInProgress = true;
try {
console.log('🔄 Token expired or missing, generating new one...');
const token = await this.handleCaptchaWithRetry();
if (token && token.length > 20) {
this.setTurnstileToken(token);
console.log('✅ Token captured and cached successfully');
return token;
}
console.log('⚠️ Invisible Turnstile failed, forcing browser automation...');
const fallbackToken = await this.handleCaptchaFallback();
if (fallbackToken && fallbackToken.length > 20) {
this.setTurnstileToken(fallbackToken);
console.log('✅ Fallback token captured successfully');
return fallbackToken;
}
console.log('❌ All token generation methods failed');
return null;
} finally {
this.tokenGenerationInProgress = false;
}
}
/**
* Handle captcha generation with retry logic
* @returns {Promise<string|null>} The token or null if failed
*/
async handleCaptchaWithRetry() {
const startTime = performance.now();
try {
const { sitekey, token: preGeneratedToken } = await this.obtainSitekeyAndToken();
if (!sitekey) {
throw new Error('No valid sitekey found');
}
console.log('🔑 Using sitekey:', sitekey);
if (typeof window !== 'undefined' && window.navigator) {
console.log(
'🧭 UA:',
window.navigator.userAgent.substring(0, 50) + '...',
'Platform:',
window.navigator.platform
);
}
let token = null;
if (
preGeneratedToken &&
typeof preGeneratedToken === 'string' &&
preGeneratedToken.length > 20
) {
console.log('♻️ Reusing pre-generated Turnstile token');
token = preGeneratedToken;
} else {
if (this.isTokenValid()) {
console.log('♻️ Using existing cached token (from previous session)');
token = this.turnstileToken;
} else {
console.log('🔐 Generating new token with executeTurnstile...');
token = await this.executeTurnstile(sitekey, 'paint');
if (token) this.setTurnstileToken(token);
}
}
if (token && typeof token === 'string' && token.length > 20) {
const elapsed = Math.round(performance.now() - startTime);
console.log(`✅ Turnstile token generated successfully in ${elapsed}ms`);
return token;
} else {
throw new Error(`Invalid or empty token received - Length: ${token?.length || 0}`);
}
} catch (error) {
const elapsed = Math.round(performance.now() - startTime);
console.error(`❌ Turnstile token generation failed after ${elapsed}ms:`, error);
throw error;
}
}
/**
* Fallback token generation method using pixel placement automation
* @returns {Promise<string|null>} The token or null if failed
*/
async handleCaptchaFallback() {
return new Promise(async (resolve, reject) => {
try {
// Ensure we have a fresh promise to await for a new token capture
if (!this._resolveToken) {
this.tokenPromise = new Promise((res) => {
this._resolveToken = res;
});
}
const timeoutPromise = this._sleep(20000).then(() =>
reject(new Error('Auto-CAPTCHA timed out.'))
);
const solvePromise = (async () => {
// Wait for main paint button
const mainPaintBtn = await this._waitForSelector(
'button.btn.btn-primary.btn-lg, button.btn.btn-primary.sm\\:btn-xl',
200,
10000
);
if (!mainPaintBtn) throw new Error('Could not find the main paint button.');
mainPaintBtn.click();
await this._sleep(500);
// Click transparent color button
const transBtn = await this._waitForSelector('button#color-0', 200, 5000);
if (!transBtn) throw new Error('Could not find the transparent color button.');
transBtn.click();
await this._sleep(500);
// Find canvas and simulate painting
const canvas = await this._waitForSelector('canvas', 200, 5000);
if (!canvas) throw new Error('Could not find the canvas element.');
canvas.setAttribute('tabindex', '0');
canvas.focus();
const rect = canvas.getBoundingClientRect();
const centerX = Math.round(rect.left + rect.width / 2);
const centerY = Math.round(rect.top + rect.height / 2);
// Simulate mouse movement and spacebar press
canvas.dispatchEvent(
new MouseEvent('mousemove', {
clientX: centerX,
clientY: centerY,
bubbles: true,
})
);
canvas.dispatchEvent(
new KeyboardEvent('keydown', {
key: ' ',
code: 'Space',
bubbles: true,
})
);
await this._sleep(50);
canvas.dispatchEvent(
new KeyboardEvent('keyup', {
key: ' ',
code: 'Space',
bubbles: true,
})
);
await this._sleep(500);
// Delay before sending confirmation
await this._sleep(800);
// Keep confirming until token is captured
const confirmLoop = async () => {
while (!this.turnstileToken) {
let confirmBtn = await this._waitForSelector(
'button.btn.btn-primary.btn-lg, button.btn.btn-primary.sm\\:btn-xl'
);
if (!confirmBtn) {
const allPrimary = Array.from(document.querySelectorAll('button.btn-primary'));
confirmBtn = allPrimary.length ? allPrimary[allPrimary.length - 1] : null;
}
if (confirmBtn) {
confirmBtn.click();
}
await this._sleep(500); // 500ms delay between confirmation attempts
}
};
// Start confirmation loop and wait for token
confirmLoop();
const token = await this.tokenPromise;
await this._sleep(300); // small delay after token is captured
resolve(token);
})();
await Promise.race([solvePromise, timeoutPromise]);
} catch (error) {
console.error('Auto-CAPTCHA process failed:', error);
reject(error);
}
});
}
/**
* Load Turnstile script if not already loaded
* @returns {Promise<void>}
*/
async loadTurnstile() {
// If Turnstile is already present, just resolve.
if (window.turnstile) {
this.turnstileLoaded = true;
return Promise.resolve();
}
return new Promise((resolve, reject) => {
// Avoid adding the script twice
if (
document.querySelector(
'script[src^="https://challenges.cloudflare.com/turnstile/v0/api.js"]'
)
) {
const checkReady = () => {
if (window.turnstile) {
this.turnstileLoaded = true;
resolve();
} else {
setTimeout(checkReady, 100);
}
};
return checkReady();
}
const script = document.createElement('script');
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
script.async = true;
script.defer = true;
script.onload = () => {
this.turnstileLoaded = true;
console.log('✅ Turnstile script loaded successfully');
resolve();
};
script.onerror = () => {
console.error('❌ Failed to load Turnstile script');
reject(new Error('Failed to load Turnstile'));
};
document.head.appendChild(script);
});
}
/**
* Create or reuse the turnstile container - completely hidden for token generation
* @returns {HTMLElement} The container element
*/
ensureTurnstileContainer() {
if (!this._turnstileContainer || !document.body.contains(this._turnstileContainer)) {
// Clean up old container if it exists
if (this._turnstileContainer) {
this._turnstileContainer.remove();
}
this._turnstileContainer = document.createElement('div');
this._turnstileContainer.className = 'wplace-turnstile-hidden';
this._turnstileContainer.setAttribute('aria-hidden', 'true');
this._turnstileContainer.id = 'turnstile-widget-container';
document.body.appendChild(this._turnstileContainer);
}
return this._turnstileContainer;
}
/**
* Interactive overlay container for visible widgets when needed
* @returns {HTMLElement} The overlay element
*/
ensureTurnstileOverlayContainer() {
if (this._turnstileOverlay && document.body.contains(this._turnstileOverlay)) {
return this._turnstileOverlay;
}
const overlay = document.createElement('div');
overlay.id = 'turnstile-overlay-container';
overlay.className = 'wplace-turnstile-overlay wplace-overlay-hidden';
const title = document.createElement('div');
title.textContent = this._getText('turnstileInstructions', 'Complete the verification');
title.className = 'wplace-turnstile-title';
const host = document.createElement('div');
host.id = 'turnstile-overlay-host';
host.className = 'wplace-turnstile-host';
const hideBtn = document.createElement('button');
hideBtn.textContent = this._getText('hideTurnstileBtn', 'Hide');
hideBtn.className = 'wplace-turnstile-hide-btn';
hideBtn.addEventListener('click', () => overlay.remove());
overlay.appendChild(title);
overlay.appendChild(host);
overlay.appendChild(hideBtn);
document.body.appendChild(overlay);
this._turnstileOverlay = overlay;
return overlay;
}
/**
* Execute Turnstile widget to generate token
* @param {string} sitekey - The site key
* @param {string} action - The action name (default: 'paint')
* @returns {Promise<string|null>} The token or null if failed
*/
async executeTurnstile(sitekey, action = 'paint') {
await this.loadTurnstile();
// Try reusing existing widget first if sitekey matches
if (this._turnstileWidgetId && this._lastSitekey === sitekey && window.turnstile?.execute) {
try {
console.log('🔄 Reusing existing Turnstile widget...');
const token = await Promise.race([
window.turnstile.execute(this._turnstileWidgetId, { action }),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Execute timeout')), 15000)
),
]);
if (token && token.length > 20) {
console.log('✅ Token generated via widget reuse');
return token;
}
} catch (error) {
console.log('⚠️ Widget reuse failed, will create a fresh widget:', error.message);
}
}
// Try invisible widget first
const invisibleToken = await this.createTurnstileWidget(sitekey, action);
if (invisibleToken && invisibleToken.length > 20) {
return invisibleToken;
}
console.log('⚠️ Falling back to interactive Turnstile (visible).');
return await this.createTurnstileWidgetInteractive(sitekey, action);
}
/**
* Create invisible Turnstile widget
* @param {string} sitekey - The site key
* @param {string} action - The action name
* @returns {Promise<string|null>} The token or null if failed
*/
async createTurnstileWidget(sitekey, action) {
return new Promise((resolve) => {
try {
// Force cleanup of any existing widget
if (this._turnstileWidgetId && window.turnstile?.remove) {
try {
window.turnstile.remove(this._turnstileWidgetId);
console.log('🧹 Cleaned up existing Turnstile widget');
} catch (e) {
console.warn('⚠️ Widget cleanup warning:', e.message);
}
}
const container = this.ensureTurnstileContainer();
container.innerHTML = '';
// Verify Turnstile is available
if (!window.turnstile?.render) {
console.error('❌ Turnstile not available for rendering');
resolve(null);
return;
}
console.log('🔧 Creating invisible Turnstile widget...');
const widgetId = window.turnstile.render(container, {
sitekey,
action,
size: 'invisible',
retry: 'auto',
'retry-interval': 8000,
callback: (token) => {
console.log('✅ Invisible Turnstile callback');
resolve(token);
},
'error-callback': () => resolve(null),
'timeout-callback': () => resolve(null),
});
this._turnstileWidgetId = widgetId;
this._lastSitekey = sitekey;
if (!widgetId) {
return resolve(null);
}
// Execute the widget and race with timeout
Promise.race([
window.turnstile.execute(widgetId, { action }),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Invisible execute timeout')), 12000)
),
])
.then(resolve)
.catch(() => resolve(null));
} catch (e) {
console.error('❌ Invisible Turnstile creation failed:', e);
resolve(null);
}
});
}
/**
* Create interactive Turnstile widget (visible)
* @param {string} sitekey - The site key
* @param {string} action - The action name
* @returns {Promise<string|null>} The token or null if failed
*/
async createTurnstileWidgetInteractive(sitekey, action) {
// Create a visible widget that users can interact with if needed
console.log('🔄 Creating interactive Turnstile widget (visible)');
return new Promise((resolve) => {
try {
// Force cleanup of any existing widget
if (this._turnstileWidgetId && window.turnstile?.remove) {
try {
window.turnstile.remove(this._turnstileWidgetId);
} catch (e) {
console.warn('⚠️ Widget cleanup warning:', e.message);
}
}
const overlay = this.ensureTurnstileOverlayContainer();
overlay.classList.remove('wplace-overlay-hidden');
overlay.style.display = 'block';
const host = overlay.querySelector('#turnstile-overlay-host');
host.innerHTML = '';
// Set a timeout for interactive mode
const timeout = setTimeout(() => {
console.warn('⏰ Interactive Turnstile widget timeout');
overlay.classList.add('wplace-overlay-hidden');
overlay.style.display = 'none';
resolve(null);
}, 60000); // 60 seconds for user interaction
const widgetId = window.turnstile.render(host, {
sitekey,
action,
size: 'normal',
theme: 'light',
callback: (token) => {
clearTimeout(timeout);
overlay.classList.add('wplace-overlay-hidden');
overlay.style.display = 'none';
console.log('✅ Interactive Turnstile completed successfully');
if (typeof token === 'string' && token.length > 20) {
resolve(token);
} else {
console.warn('❌ Invalid token from interactive widget');
resolve(null);
}
},
'error-callback': (error) => {
clearTimeout(timeout);
overlay.classList.add('wplace-overlay-hidden');
overlay.style.display = 'none';
console.warn('❌ Interactive Turnstile error:', error);
resolve(null);
},
});
this._turnstileWidgetId = widgetId;
this._lastSitekey = sitekey;
if (!widgetId) {
clearTimeout(timeout);
overlay.classList.add('wplace-overlay-hidden');
overlay.style.display = 'none';
console.warn('❌ Failed to create interactive Turnstile widget');
resolve(null);
} else {
console.log('✅ Interactive Turnstile widget created, waiting for user interaction...');
}
} catch (e) {
console.error('❌ Interactive Turnstile creation failed:', e);
resolve(null);
}
});
}
/**
* Cleanup method for when the script is disabled/reloaded
*/
cleanupTurnstile() {
if (this._turnstileWidgetId && window.turnstile?.remove) {
try {
window.turnstile.remove(this._turnstileWidgetId);
} catch (e) {
console.warn('Failed to cleanup Turnstile widget:', e);
}
}
if (this._turnstileContainer && document.body.contains(this._turnstileContainer)) {
this._turnstileContainer.remove();
}
if (this._turnstileOverlay && document.body.contains(this._turnstileOverlay)) {
this._turnstileOverlay.remove();
}
this._turnstileWidgetId = null;
this._turnstileContainer = null;
this._turnstileOverlay = null;
this._lastSitekey = null;
}
/**
* Obtain sitekey and token
* @param {string} fallback - Fallback sitekey
* @returns {Promise<{sitekey: string, token: string|null}>}
*/
async obtainSitekeyAndToken(fallback = '0x4AAAAAABpqJe8FO0N84q0F') {
// Cache sitekey to avoid repeated DOM queries
if (this._cachedSitekey) {
console.log('🔍 Using cached sitekey:', this._cachedSitekey);
return this.isTokenValid()
? {
sitekey: this._cachedSitekey,
token: this.turnstileToken,
}
: { sitekey: this._cachedSitekey, token: null };
}
// List of potential sitekeys to try
const potentialSitekeys = [
'0x4AAAAAABpqJe8FO0N84q0F', // WPlace common sitekey
'0x4AAAAAAAJ7xjKAp6Mt_7zw', // Alternative WPlace sitekey
'0x4AAAAAADm5QWx6Ov2LNF2g', // Another common sitekey
];
const trySitekey = async (sitekey, source) => {
if (!sitekey || sitekey.length < 10) return null;
console.log(`🔍 Testing sitekey from ${source}:`, sitekey);
const token = await this.executeTurnstile(sitekey);
if (token && token.length >= 20) {
console.log(`✅ Valid token generated from ${source} sitekey`);
this.setTurnstileToken(token);
this._cachedSitekey = sitekey;
return { sitekey, token };
} else {
console.log(`❌ Failed to get token from ${source} sitekey`);
return null;
}
};
try {
// 1️⃣ data-sitekey attribute
const sitekeySel = document.querySelector('[data-sitekey]');
if (sitekeySel) {
const sitekey = sitekeySel.getAttribute('data-sitekey');
const result = await trySitekey(sitekey, 'data attribute');
if (result) {
return result;
}
}
// 2️⃣ Turnstile element
const turnstileEl = document.querySelector('.cf-turnstile');
if (turnstileEl?.dataset?.sitekey) {
const sitekey = turnstileEl.dataset.sitekey;
const result = await trySitekey(sitekey, 'turnstile element');
if (result) {
return result;
}
}
// 3️⃣ Meta tags
const metaTags = document.querySelectorAll(
'meta[name*="turnstile"], meta[property*="turnstile"]'
);
for (const meta of metaTags) {
const content = meta.getAttribute('content');
const result = await trySitekey(content, 'meta tag');
if (result) {
return result;
}
}
// 4️⃣ Global variable
if (window.__TURNSTILE_SITEKEY) {
const result = await trySitekey(window.__TURNSTILE_SITEKEY, 'global variable');
if (result) {
return result;
}
}
// 5️⃣ Script tags
const scripts = document.querySelectorAll('script');
for (const script of scripts) {
const content = script.textContent || script.innerHTML;
const match = content.match(
/(?:sitekey|data-sitekey)['"\s\[\]:\=\(]*['"]?([0-9a-zA-Z_-]{20,})['"]?/i
);
if (match && match[1]) {
const extracted = match[1].replace(/['"]/g, '');
const result = await trySitekey(extracted, 'script content');
if (result) {
return result;
}
}
}
// 6️⃣ Known potential sitekeys
console.log('🔍 Testing known potential sitekeys...');
for (const testSitekey of potentialSitekeys) {
const result = await trySitekey(testSitekey, 'known list');
if (result) {
return result;
}
}
} catch (error) {
console.warn('⚠️ Error during sitekey detection:', error);
}
// 7️⃣ Fallback
console.log('🔧 Trying fallback sitekey:', fallback);
const fallbackResult = await trySitekey(fallback, 'fallback');
if (fallbackResult) {
return fallbackResult;
}
// 8️⃣ Ultimate fallback with no token
console.warn('❌ All sitekey detection methods failed, using fallback without token validation');
this._cachedSitekey = fallback;
return { sitekey: fallback, token: null };
}
// =============================================
// UTILITY METHODS
// =============================================
/**
* Sleep utility
* @param {number} ms - Milliseconds to sleep
* @returns {Promise<void>}
*/
_sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Wait for DOM element to appear
* @param {string} selector - CSS selector
* @param {number} pollInterval - Polling interval in ms
* @param {number} timeout - Timeout in ms
* @returns {Promise<Element|null>} The element or null if timeout
* @private
*/
_waitForSelector(selector, pollInterval = 200, timeout = 10000) {
return new Promise((resolve) => {
const endTime = Date.now() + timeout;
const poll = () => {
const element = document.querySelector(selector);
if (element) {
resolve(element);
} else if (Date.now() < endTime) {
setTimeout(poll, pollInterval);
} else {
resolve(null);
}
};
poll();
});
}
/**
* Simple translation helper (fallback to English)
* @param {string} key - Translation key
* @param {string} fallback - Fallback text
* @returns {string} Translated text or fallback
*/
_getText(key, fallback = '') {
// Try to use external translation function if available
if (typeof window.Utils?.t === 'function') {
return window.Utils.t(key);
}
// Fallback translations
const translations = {
turnstileInstructions: 'Complete the verification',
hideTurnstileBtn: 'Hide',
tokenCapturedSuccess: 'Token captured successfully'
};
return translations[key] || fallback || key;
}
/**
* Show alert helper (uses external alert if available)
* @param {string} message - Message to show
* @param {string} type - Alert type
*/
_showAlert(message, type = 'info') {
// Try to use external alert function if available
if (typeof window.Utils?.showAlert === 'function') {
window.Utils.showAlert(message, type);
} else {
console.log(`${type.toUpperCase()}: ${message}`);
}
}
/**
* Generate random string utility
* @param {number} len - Length of string
* @param {string} chars - Character set
* @returns {string} Random string
*/
_randStr(len, chars = 'abcdefghijklmnopqrstuvwxyz0123456789') {
return [...Array(len)]
.map(() =>
chars[
(crypto?.getRandomValues?.(new Uint32Array(1))[0] % chars.length) ||
Math.floor(Math.random() * chars.length)
]
)
.join('');
}
}
// Create global instance
window.WPlaceTokenManager = new TokenManager();
// Create global instance alias for Auto-Image.js compatibility
window.globalTokenManager = window.WPlaceTokenManager;
// Legacy compatibility - expose key methods globally for backward compatibility
window.setTurnstileToken = (token) => window.WPlaceTokenManager.setTurnstileToken(token);
window.ensureToken = (forceRefresh) => window.WPlaceTokenManager.ensureToken(forceRefresh);
window.isTokenValid = () => window.WPlaceTokenManager.isTokenValid();
window.invalidateToken = () => window.WPlaceTokenManager.invalidateToken();
console.log('✅ WPlace Token Manager loaded and ready');
File diff suppressed because it is too large. Load diff