// ==UserScript== // @name OWOP Image to Pixel By Agents_K – [draggable + FR/EN] // @namespace owop-autopixel // @version 2.1 // @description Pose une image locale sur OWOP à partir (X,Y) avec blocs N×N, débit en px/s, réessais, passe de vérif. Queue: max 5000, refill sous 500. Panneau à droite, déplaçable, FR/EN. // @match *://ourworldofpixels.com/* // @match *://*.ourworldofpixels.com/* // @run-at document-idle // @license MIT // @grant none // @noframes // @downloadURL none // ==/UserScript== (function () { 'use strict'; if (window.top !== window.self) return; // ---------- UI ---------- const css = ` #apxPanel{ position:fixed;top:12px;right:12px;left:auto; z-index:2147483647;background:#111a;border:1px solid #333;border-radius:12px; padding:10px 12px;color:#eee;font:13px/1.35 system-ui,Segoe UI,Roboto,Arial; width:330px;box-shadow:0 8px 24px rgba(0,0,0,.35) } #apxTitle{ margin:0 0 8px;font-size:14px;font-weight:700; cursor:move; user-select:none; -webkit-user-select:none; padding:4px 2px; border-bottom:1px solid #2a2a2a; } #apxPanel .row{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px} #apxPanel .wide{grid-column:1 / -1} #apxPanel input[type="number"],#apxPanel input[type="file"],#apxPanel button,#apxPanel input[type="range"]{ width:100%;box-sizing:border-box;border-radius:8px;border:1px solid #444;background:#17181a;color:#eee;padding:6px 8px } #apxPanel label{display:flex;flex-direction:column;gap:6px} #apxPanel .small{color:#bbb;font-size:12px} #apxStatus{margin-top:6px;font-size:12px;color:#bbb} #apxLed{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px;background:#d33;vertical-align:middle} #apxLed.on{background:#2ecc71} `; const style = document.createElement('style'); style.textContent = css; document.head.append(style); const panel = document.createElement('div'); panel.id = 'apxPanel'; panel.innerHTML = `

Image to Pixel By Agents_K

En attente d'une image… / Waiting for an image…
`; document.body.append(panel); // ---------- Draggable panel ---------- (function makeDraggable(){ const title = panel.querySelector('#apxTitle'); let dragging = false, startX = 0, startY = 0, startLeft = 0, startTop = 0; try { const save = localStorage.getItem('apxPanelPos'); if (save) { const {left, top} = JSON.parse(save); panel.style.left = left + 'px'; panel.style.top = top + 'px'; panel.style.right = 'auto'; } } catch(_) {} const onMouseMove = (e) => { if (!dragging) return; const nx = startLeft + (e.clientX - startX); const ny = startTop + (e.clientY - startY); panel.style.left = nx + 'px'; panel.style.top = ny + 'px'; panel.style.right = 'auto'; }; const onMouseUp = () => { if (!dragging) return; dragging = false; window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mouseup', onMouseUp); const rect = panel.getBoundingClientRect(); try { localStorage.setItem('apxPanelPos', JSON.stringify({ left: rect.left, top: rect.top })); } catch(_){} }; title.addEventListener('mousedown', (e) => { dragging = true; const rect = panel.getBoundingClientRect(); startX = e.clientX; startY = e.clientY; startLeft = rect.left; startTop = rect.top; window.addEventListener('mousemove', onMouseMove); window.addEventListener('mouseup', onMouseUp); e.preventDefault(); }); })(); // ---------- Helpers & elements ---------- const $ = (sel) => panel.querySelector(sel); const led = $('#apxLed'), msg = $('#apxMsg'); const scaleEl = $('#apxScale'), scaleInfo = $('#apxScaleInfo'); const blockEl = $('#apxBlock'), blockInfo = $('#apxBlockInfo'); const rateEl = $('#apxRate'), rateInfo = $('#apxRateInfo'); const setStatus = (on, text) => { led.classList.toggle('on', !!on); msg.textContent = text || ''; }; // ---------- Image state ---------- let srcBmp = null, srcW = 0, srcH = 0; let scalePct = 100; let tgtW = 0, tgtH = 0; let imgData = null; const imgC = document.createElement('canvas'); const imgCtx = imgC.getContext('2d', { willReadFrequently: true }); // ---------- Parameters ---------- const state = { x0: 0, y0: 0, block: 3, rate: 15, running: false, pass: 1, // 1 = scan/place, 2 = verify }; // ---------- Queue with watermarks ---------- // Task = { wx, wy, r, g, b, due, tries } const queue = []; const Q_MAX_FEED = 5000; // hard cap const Q_LOW_WATERMARK = 500; // refill threshold const BACKOFF_MS = 600; // <— déclaré UNE SEULE fois ici let tickTimer = null; function rgbEq(a,b){ return a && b && a[0]===b[0] && a[1]===b[1] && a[2]===b[2]; } function loadBitmap(file){ return file.arrayBuffer().then(buf => createImageBitmap(new Blob([buf]))); } function rebuildScaled() { if (!srcBmp) return; tgtW = Math.max(1, Math.round(srcW * scalePct / 100)); tgtH = Math.max(1, Math.round(srcH * scalePct / 100)); imgC.width = tgtW; imgC.height = tgtH; imgCtx.imageSmoothingEnabled = false; imgCtx.clearRect(0,0,tgtW,tgtH); imgCtx.drawImage(srcBmp, 0, 0, srcW, srcH, 0, 0, tgtW, tgtH); imgData = imgCtx.getImageData(0,0,tgtW,tgtH); scaleInfo.textContent = `Échelle: ${scalePct}% • Taille posée: ${tgtW}×${tgtH} / Scale: ${scalePct}% • Placed size: ${tgtW}×${tgtH}`; } function getImgRGB(ix, iy) { if (!imgData) return null; if (ix<0 || iy<0 || ix>=tgtW || iy>=tgtH) return null; const p = (iy*tgtW + ix) * 4, d = imgData.data; if (d[p+3] < 10) return null; return [d[p], d[p+1], d[p+2]]; } // ---------- OWOP access ---------- function waitForOWOP(maxMs = 30000) { return new Promise((resolve, reject) => { const t0 = Date.now(); const iv = setInterval(() => { if (window.OWOP && OWOP.world) { clearInterval(iv); resolve(); } else if (Date.now() - t0 > maxMs) { clearInterval(iv); reject(new Error('OWOP timeout')); } }, 100); }); } let placeMode = null; function placePixel(wx, wy, rgb) { try { if (placeMode === 'array') return OWOP.world.setPixel(wx, wy, rgb); if (placeMode === 'args') return OWOP.world.setPixel(wx, wy, rgb[0], rgb[1], rgb[2]); try { const r1 = OWOP.world.setPixel(wx, wy, rgb); placeMode = 'array'; return r1; } catch(_) { const r2 = OWOP.world.setPixel(wx, wy, rgb[0], rgb[1], rgb[2]); placeMode = 'args'; return r2; } } catch(_) { return false; } } function getBoardPixel(wx, wy) { try { const p = OWOP.world.getPixel(wx, wy); if (!p) return null; if (Array.isArray(p)) return p; if (typeof p.r === 'number') return [p.r, p.g, p.b]; } catch(_) {} return null; } // ---------- Incremental feeders with watermarks ---------- // Pass 1 scanning pointers: let scanX = 0, scanY = 0, scannedAll = false; function feedQueue() { if (!imgData || scannedAll) return; while (queue.length < Q_MAX_FEED && !scannedAll) { const bx = state.block, by = state.block; for (let dy=0; dy=tgtW || iy>=tgtH) continue; const rgb = getImgRGB(ix, iy); if (!rgb) continue; const wx = state.x0 + ix, wy = state.y0 + iy; queue.push({ wx, wy, r: rgb[0], g: rgb[1], b: rgb[2], due: 0, tries: 0 }); if (queue.length >= Q_MAX_FEED) break; } if (queue.length >= Q_MAX_FEED) break; } scanX += state.block; if (scanX >= tgtW) { scanX = 0; scanY += state.block; } if (scanY >= tgtH) { scannedAll = true; } } } // Pass 2 verify pointers: let vScanX = 0, vScanY = 0, verifyDone = false; function beginVerify() { vScanX = 0; vScanY = 0; verifyDone = false; } function feedVerifyQueue() { if (!imgData || verifyDone) return; while (queue.length < Q_MAX_FEED && !verifyDone) { const rgb = getImgRGB(vScanX, vScanY); if (rgb) { const wx = state.x0 + vScanX, wy = state.y0 + vScanY; const cur = getBoardPixel(wx, wy); if (!rgbEq(cur, rgb)) { queue.push({ wx, wy, r: rgb[0], g: rgb[1], b: rgb[2], due: 0, tries: 0 }); } } // advance verify pointer vScanX++; if (vScanX >= tgtW) { vScanX = 0; vScanY++; } if (vScanY >= tgtH) { verifyDone = true; } } } // ---------- Placement loop ---------- function tick() { if (!state.running) return; const now = performance.now(); let taskIdx = -1; for (let i = 0; i < queue.length; i++) { if (queue[i].due <= now) { taskIdx = i; break; } } if (taskIdx === -1) return; const t = queue.splice(taskIdx, 1)[0]; const desired = [t.r, t.g, t.b]; const cur = getBoardPixel(t.wx, t.wy); if (rgbEq(cur, desired)) { updateStatus(); return; } const ok = placePixel(t.wx, t.wy, desired); if (!ok) { t.tries++; t.due = now + BACKOFF_MS * Math.min(6, t.tries); queue.push(t); } updateStatus(); } function updateStatus() { const progA = scannedAll ? 100 : Math.floor(((scanY*tgtW + scanX) / (tgtW*tgtH)) * 100); const passInfo = state.pass === 1 ? 'Passe 1/2 / Pass 1/2' : 'Passe 2/2 / Pass 2/2'; setStatus(true, `${passInfo} • queue: ${queue.length} • scan: ${progA}%`); } function setRateTimer() { if (tickTimer) clearInterval(tickTimer); const interval = Math.max(10, Math.round(1000 / state.rate)); tickTimer = setInterval(tick, interval); } // Watcher: transition between passes & refill under low watermark const endWatcher = setInterval(() => { if (!state.running) return; if (state.pass === 1) { if (!scannedAll && queue.length < Q_LOW_WATERMARK) feedQueue(); if (queue.length === 0 && scannedAll) { state.pass = 2; beginVerify(); setStatus(true, 'Vérification (passe 2/2)… / Verifying (pass 2/2)…'); if (queue.length < Q_LOW_WATERMARK) feedVerifyQueue(); } } else { if (!verifyDone && queue.length < Q_LOW_WATERMARK) feedVerifyQueue(); if (verifyDone && queue.length === 0) { stopAll(); setStatus(false, 'Terminé ✅ / Done ✅'); } } }, 200); function startAll() { if (state.running) return; state.running = true; state.pass = 1; scanX = scanY = 0; scannedAll = false; beginVerify(); verifyDone = true; queue.length = 0; feedQueue(); // prime jusqu’à 5000 setRateTimer(); // cadence px/s setStatus(true, 'Démarré (passe 1/2)… / Started (pass 1/2)…'); } function stopAll() { state.running = false; if (tickTimer) clearInterval(tickTimer); setStatus(false, 'Arrêté. / Stopped.'); } // ---------- Events UI ---------- $('#apxFile').addEventListener('change', async (e) => { const f = e.target.files && e.target.files[0]; if (!f) return; try { setStatus(false, 'Chargement image… / Loading image…'); const bmp = await loadBitmap(f); srcBmp = bmp; srcW = bmp.width; srcH = bmp.height; rebuildScaled(); setStatus(false, `Image ${srcW}×${srcH} chargée. Ajuste échelle/bloc/taux, mets X/Y, puis Démarrer. / Image ${srcW}×${srcH} loaded. Adjust scale/block/rate, set X/Y, then Start.`); } catch (err) { console.error('[AutoPixel] load error', err); setStatus(false, 'Échec chargement image. / Failed to load image.'); alert('Échec du chargement de l’image (voir console). / Failed to load image (see console).'); } }); $('#apxSetMouse').addEventListener('click', () => { if (window.OWOP && OWOP.mouse) { $('#apxX').value = OWOP.mouse.tileX; $('#apxY').value = OWOP.mouse.tileY; } else { alert('OWOP pas prêt. Attends que la carte soit chargée. / OWOP not ready. Wait for the map to load.'); } }); $('#apxStart').addEventListener('click', async () => { try { await waitForOWOP(); if (!imgData) { alert('Charge d’abord une image. / Please load an image first.'); return; } state.x0 = parseInt($('#apxX').value || '0', 10) || 0; state.y0 = parseInt($('#apxY').value || '0', 10) || 0; startAll(); } catch(_) { alert('OWOP n’est pas prêt (timeout). Recharge la page. / OWOP not ready (timeout). Reload the page.'); } }); $('#apxStop').addEventListener('click', stopAll); $('#apxReset').addEventListener('click', () => { stopAll(); queue.length = 0; scanX = scanY = 0; scannedAll = false; beginVerify(); verifyDone = true; setStatus(false, 'Progression réinitialisée. / Progress reset.'); }); blockEl.addEventListener('input', () => { const v = parseInt(blockEl.value, 10) || 1; state.block = (v % 2 === 0) ? (v + 1) : v; // 1,3,5,... blockEl.value = String(state.block); blockInfo.textContent = `Bloc: ${state.block}×${state.block} (${state.block*state.block} px) / Block: ${state.block}×${state.block} (${state.block*state.block} px)`; }); scaleEl.addEventListener('input', () => { scalePct = parseInt(scaleEl.value, 10) || 100; rebuildScaled(); }); rateEl.addEventListener('input', () => { state.rate = Math.max(1, parseInt(rateEl.value, 10) || 15); rateInfo.textContent = `Débit: ${state.rate} px/s / Rate: ${state.rate} px/s`; if (state.running) setRateTimer(); }); // init displays blockEl.dispatchEvent(new Event('input')); scaleEl.dispatchEvent(new Event('input')); rateEl.dispatchEvent(new Event('input')); console.log('[AutoPixel OWOP] ready ✅ (watermarks 500/5000, v1.4.3)'); })();