mirror of
https://github.com/tiennm99/WPlace-AutoBOT.git
synced 2026-10-13 08:15:08 +00:00
Fixed batch skipping + Optimized Overlay Processing
This commit is contained in:
1 parent
bacb7a7503
commit
03d0eddecf
1 file changed
+216
-113
+216
-113
@@ -209,7 +209,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// BILINGUAL TEXT STRINGS
|
||||
// BILINGUAL TEXT STRINGS:)
|
||||
const TEXT = {
|
||||
en: {
|
||||
title: "WPlace Auto-Image",
|
||||
@@ -992,7 +992,7 @@
|
||||
// Placeholder for the resize preview update function
|
||||
let _updateResizePreview = () => { };
|
||||
|
||||
// --- OVERLAY UPDATE: New OverlayManager class to handle all overlay logic ---
|
||||
// --- OVERLAY UPDATE: Optimized OverlayManager class with performance improvements ---
|
||||
class OverlayManager {
|
||||
constructor() {
|
||||
this.isEnabled = false;
|
||||
@@ -1000,6 +1000,9 @@
|
||||
this.imageBitmap = null;
|
||||
this.chunkedTiles = new Map(); // Map<"tileX,tileY", ImageBitmap>
|
||||
this.tileSize = 1000;
|
||||
this.processPromise = null; // Track ongoing processing
|
||||
this.lastProcessedHash = null; // Cache invalidation
|
||||
this.workerPool = null; // Web worker pool for heavy processing
|
||||
}
|
||||
|
||||
toggle() {
|
||||
@@ -1014,10 +1017,15 @@
|
||||
this.disable();
|
||||
this.imageBitmap = null;
|
||||
this.chunkedTiles.clear();
|
||||
this.lastProcessedHash = null;
|
||||
if (this.processPromise) {
|
||||
this.processPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
async setImage(imageBitmap) {
|
||||
this.imageBitmap = imageBitmap;
|
||||
this.lastProcessedHash = null; // Invalidate cache
|
||||
if (this.imageBitmap && this.startCoords) {
|
||||
await this.processImageIntoChunks();
|
||||
}
|
||||
@@ -1027,19 +1035,55 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// --- OVERLAY UPDATE: Simplified chunking logic for solid, semi-transparent overlay ---
|
||||
// Generate hash for cache invalidation
|
||||
_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;
|
||||
return `${width}x${height}_${px},${py}_${rx},${ry}_${state.blueMarbleEnabled}_${state.overlayOpacity}`;
|
||||
}
|
||||
|
||||
// --- OVERLAY UPDATE: Optimized chunking with caching and batch processing ---
|
||||
async processImageIntoChunks() {
|
||||
if (!this.imageBitmap || !this.startCoords) return;
|
||||
|
||||
// Check if we're already processing to avoid duplicate work
|
||||
if (this.processPromise) {
|
||||
return this.processPromise;
|
||||
}
|
||||
|
||||
// Check cache validity
|
||||
const currentHash = this._generateProcessHash();
|
||||
if (this.lastProcessedHash === currentHash && this.chunkedTiles.size > 0) {
|
||||
console.log(`📦 Using cached overlay chunks (${this.chunkedTiles.size} tiles)`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Start processing
|
||||
this.processPromise = this._doProcessImageIntoChunks();
|
||||
try {
|
||||
await this.processPromise;
|
||||
this.lastProcessedHash = currentHash;
|
||||
} finally {
|
||||
this.processPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -1052,63 +1096,89 @@
|
||||
const endTileX = startRegionX + Math.floor(endPixelX / this.tileSize);
|
||||
const endTileY = startRegionY + Math.floor(endPixelY / 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++) {
|
||||
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) continue;
|
||||
|
||||
// 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);
|
||||
|
||||
// --- NEW: BLUE MARBLE EFFECT ---
|
||||
if (state.blueMarbleEnabled) {
|
||||
const imageData = chunkCtx.getImageData(0, 0, this.tileSize, this.tileSize);
|
||||
const data = imageData.data;
|
||||
for (let pixelY = 0; pixelY < this.tileSize; pixelY++) {
|
||||
for (let pixelX = 0; pixelX < this.tileSize; pixelX++) {
|
||||
const canvasX = pixelX;
|
||||
const canvasY = pixelY;
|
||||
const imageX = canvasX - dX;
|
||||
const imageY = canvasY - dY;
|
||||
|
||||
if ((imageX + imageY) % 2 === 0) {
|
||||
const index = (canvasY * this.tileSize + canvasX) * 4;
|
||||
if (data[index + 3] > 0) {
|
||||
data[index + 3] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
chunkCtx.putImageData(imageData, 0, 0);
|
||||
}
|
||||
|
||||
const chunkBitmap = await chunkCanvas.transferToImageBitmap();
|
||||
this.chunkedTiles.set(tileKey, chunkBitmap);
|
||||
tilesToProcess.push({ tx, ty });
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Overlay processed into ${this.chunkedTiles.size} chunks.`);
|
||||
// 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`);
|
||||
}
|
||||
|
||||
// --- OVERLAY UPDATE: Simplified compositing logic for solid, semi-transparent overlay ---
|
||||
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);
|
||||
|
||||
// --- OPTIMIZED: Blue marble effect with faster pixel manipulation ---
|
||||
if (state.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();
|
||||
}
|
||||
|
||||
// --- OVERLAY UPDATE: Optimized compositing with caching ---
|
||||
async processAndRespondToTileRequest(eventData) {
|
||||
const { endpoint, blobID, blobData } = eventData;
|
||||
|
||||
@@ -1124,21 +1194,12 @@
|
||||
const chunkBitmap = this.chunkedTiles.get(tileKey);
|
||||
if (chunkBitmap) {
|
||||
try {
|
||||
const originalTileBitmap = await createImageBitmap(blobData);
|
||||
const canvas = new OffscreenCanvas(originalTileBitmap.width, originalTileBitmap.height);
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
|
||||
// Draw original tile first
|
||||
ctx.drawImage(originalTileBitmap, 0, 0);
|
||||
|
||||
// Set opacity and draw our solid overlay chunk on top
|
||||
ctx.globalAlpha = state.overlayOpacity;
|
||||
ctx.drawImage(chunkBitmap, 0, 0);
|
||||
|
||||
finalBlob = await canvas.convertToBlob({ type: 'image/png' });
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1151,6 +1212,29 @@
|
||||
blobData: finalBlob
|
||||
}, '*');
|
||||
}
|
||||
|
||||
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
|
||||
ctx.globalAlpha = state.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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const overlayManager = new OverlayManager();
|
||||
@@ -1163,6 +1247,7 @@
|
||||
let tokenPromise = new Promise((resolve) => { _resolveToken = resolve })
|
||||
let retryCount = 0
|
||||
const MAX_RETRIES = 10
|
||||
const MAX_BATCH_RETRIES = 10 // Maximum attempts for batch sending
|
||||
const TOKEN_LIFETIME = 240000 // 4 minutes (tokens typically last 5 min, use 4 for safety)
|
||||
|
||||
function setTurnstileToken(t) {
|
||||
@@ -5566,7 +5651,13 @@
|
||||
for (let x = y === startRow ? startCol : 0; x < width; x++) {
|
||||
if (state.stopFlag) {
|
||||
if (pixelBatch && pixelBatch.pixels.length > 0) {
|
||||
await sendPixelBatch(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY);
|
||||
console.log(`🎯 Sending final batch before stop with ${pixelBatch.pixels.length} pixels`);
|
||||
const success = await sendBatchWithRetry(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY);
|
||||
if (success) {
|
||||
pixelBatch.pixels.forEach(() => { state.paintedPixels++; });
|
||||
state.currentCharges -= pixelBatch.pixels.length;
|
||||
updateStats();
|
||||
}
|
||||
}
|
||||
state.lastPosition = { x, y }
|
||||
updateUI("paintingPaused", "warning", { x, y })
|
||||
@@ -5612,23 +5703,8 @@
|
||||
pixelBatch.regionY !== regionY + adderY) {
|
||||
|
||||
if (pixelBatch && pixelBatch.pixels.length > 0) {
|
||||
let success = await sendPixelBatch(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY);
|
||||
if (success === "token_error") {
|
||||
updateUI("captchaSolving", "warning");
|
||||
try {
|
||||
await handleCaptcha();
|
||||
success = await sendPixelBatch(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY);
|
||||
if (success === "token_error") {
|
||||
updateUI("captchaFailed", "error");
|
||||
state.stopFlag = true;
|
||||
break outerLoop;
|
||||
}
|
||||
} catch (e) {
|
||||
updateUI("captchaFailed", "error");
|
||||
state.stopFlag = true;
|
||||
break outerLoop;
|
||||
}
|
||||
}
|
||||
const success = await sendBatchWithRetry(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY);
|
||||
|
||||
if (success) {
|
||||
pixelBatch.pixels.forEach((p) => { state.paintedPixels++; });
|
||||
state.currentCharges -= pixelBatch.pixels.length;
|
||||
@@ -5648,12 +5724,11 @@
|
||||
}
|
||||
updateStats();
|
||||
} else {
|
||||
// If batch failed, don't mark pixels as painted so they can be retried
|
||||
console.warn(`⚠️ Batch failed for region ${pixelBatch.regionX},${pixelBatch.regionY} with ${pixelBatch.pixels.length} pixels. Will retry later.`);
|
||||
// Wait a bit before continuing to avoid rapid retries
|
||||
await Utils.sleep(1000);
|
||||
// If batch failed after all retries, stop painting to prevent infinite loops
|
||||
console.error(`❌ Batch failed permanently after retries. Stopping painting.`);
|
||||
state.stopFlag = true;
|
||||
break outerLoop;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
pixelBatch = {
|
||||
@@ -5672,23 +5747,7 @@
|
||||
});
|
||||
|
||||
if (pixelBatch.pixels.length >= Math.floor(state.currentCharges)) {
|
||||
let success = await sendPixelBatch(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY);
|
||||
if (success === "token_error") {
|
||||
updateUI("captchaSolving", "warning");
|
||||
try {
|
||||
await handleCaptcha();
|
||||
success = await sendPixelBatch(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY);
|
||||
if (success === "token_error") {
|
||||
updateUI("captchaFailed", "error");
|
||||
state.stopFlag = true;
|
||||
break outerLoop;
|
||||
}
|
||||
} catch (e) {
|
||||
updateUI("captchaFailed", "error");
|
||||
state.stopFlag = true;
|
||||
break outerLoop;
|
||||
}
|
||||
}
|
||||
const success = await sendBatchWithRetry(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY);
|
||||
|
||||
if (success) {
|
||||
pixelBatch.pixels.forEach((pixel) => {
|
||||
@@ -5712,10 +5771,10 @@
|
||||
await Utils.sleep(totalDelay)
|
||||
}
|
||||
} else {
|
||||
// If batch failed, don't mark pixels as painted so they can be retried
|
||||
console.warn(`⚠️ Batch failed with ${pixelBatch.pixels.length} pixels. Will retry later.`);
|
||||
// Wait a bit before continuing to avoid rapid retries
|
||||
await Utils.sleep(1000);
|
||||
// If batch failed after all retries, stop painting to prevent infinite loops
|
||||
console.error(`❌ Batch failed permanently after retries. Stopping painting.`);
|
||||
state.stopFlag = true;
|
||||
break outerLoop;
|
||||
}
|
||||
|
||||
pixelBatch.pixels = [];
|
||||
@@ -5745,7 +5804,7 @@
|
||||
}
|
||||
|
||||
if (pixelBatch && pixelBatch.pixels.length > 0 && !state.stopFlag) {
|
||||
const success = await sendPixelBatch(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY);
|
||||
const success = await sendBatchWithRetry(pixelBatch.pixels, pixelBatch.regionX, pixelBatch.regionY);
|
||||
if (success) {
|
||||
pixelBatch.pixels.forEach((pixel) => {
|
||||
state.paintedPixels++
|
||||
@@ -5757,8 +5816,8 @@
|
||||
await Utils.sleep(totalDelay)
|
||||
}
|
||||
} else {
|
||||
// If final batch failed, log it but don't retry to avoid infinite loops
|
||||
console.warn(`⚠️ Final batch failed with ${pixelBatch.pixels.length} pixels. These pixels will need to be painted on next run.`);
|
||||
// If final batch failed after retries, log it
|
||||
console.warn(`⚠️ Final batch failed with ${pixelBatch.pixels.length} pixels after all retries.`);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
@@ -5793,6 +5852,50 @@
|
||||
updateStats()
|
||||
}
|
||||
|
||||
// Helper function to retry batch until success with exponential backoff
|
||||
async function sendBatchWithRetry(pixels, regionX, regionY, maxRetries = MAX_BATCH_RETRIES) {
|
||||
let attempt = 0;
|
||||
while (attempt < maxRetries && !state.stopFlag) {
|
||||
attempt++;
|
||||
console.log(`🔄 Attempting to send batch (attempt ${attempt}/${maxRetries}) for region ${regionX},${regionY} with ${pixels.length} pixels`);
|
||||
|
||||
const result = await sendPixelBatch(pixels, regionX, regionY);
|
||||
|
||||
if (result === true) {
|
||||
console.log(`✅ Batch succeeded on attempt ${attempt}`);
|
||||
return true;
|
||||
} else if (result === "token_error") {
|
||||
console.log(`🔑 Token error on attempt ${attempt}, regenerating...`);
|
||||
updateUI("captchaSolving", "warning");
|
||||
try {
|
||||
await handleCaptcha();
|
||||
// Don't count token regeneration as a failed attempt
|
||||
attempt--;
|
||||
continue;
|
||||
} catch (e) {
|
||||
console.error(`❌ Token regeneration failed on attempt ${attempt}:`, e);
|
||||
updateUI("captchaFailed", "error");
|
||||
// Wait longer before retrying after token failure
|
||||
await Utils.sleep(5000);
|
||||
}
|
||||
} else {
|
||||
console.warn(`⚠️ Batch failed on attempt ${attempt}, retrying...`);
|
||||
// Exponential backoff with jitter
|
||||
const baseDelay = Math.min(1000 * Math.pow(2, attempt - 1), 30000); // Max 30s
|
||||
const jitter = Math.random() * 1000; // Add up to 1s random delay
|
||||
await Utils.sleep(baseDelay + jitter);
|
||||
}
|
||||
}
|
||||
|
||||
if (attempt >= maxRetries) {
|
||||
console.error(`❌ Batch failed after ${maxRetries} attempts (MAX_BATCH_RETRIES=${MAX_BATCH_RETRIES}). This will stop painting to prevent infinite loops.`);
|
||||
updateUI("paintingError", "error");
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async function sendPixelBatch(pixelBatch, regionX, regionY) {
|
||||
let token = turnstileToken;
|
||||
|
||||
|
||||
Reference in new issue
Block a user