Greasy Fork is available in English.
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.
当前为
// ==UserScript==
// @name OWOP AutoPixel – [draggable + FR/EN]
// @namespace owop-autopixel
// @version 2.0
// @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
// ==/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 = `
<h3 id="apxTitle">Image to Pixel By Agents_K</h3>
<div class="row">
<label class="wide">Image locale / Local image
<input type="file" id="apxFile" accept="image/*">
</label>
<label>X (monde) / X (world)
<input type="number" id="apxX" step="1" value="0">
</label>
<label>Y (monde) / Y (world)
<input type="number" id="apxY" step="1" value="0">
</label>
<button id="apxSetMouse" class="wide" title="Prendre X/Y sous la souris / Take X/Y from mouse">Prendre X/Y depuis la souris / Take X/Y from mouse</button>
</div>
<div class="row">
<label class="wide">Échelle de l'image (%) / Image scale (%)
<input type="range" id="apxScale" min="10" max="400" step="10" value="100">
<span class="small" id="apxScaleInfo">Échelle: 100% • Taille posée: — / Scale: 100% • Placed size: —</span>
</label>
<label class="wide">Taille du bloc (px) / Block size (px)
<input type="range" id="apxBlock" min="1" max="15" step="2" value="3">
<span class="small" id="apxBlockInfo">Bloc: 3×3 (9 px) / Block: 3×3 (9 px)</span>
</label>
<label class="wide">Débit (pixels / seconde) / Rate (pixels per second)
<input type="range" id="apxRate" min="1" max="60" step="1" value="15">
<span class="small" id="apxRateInfo">Débit: 15 px/s / Rate: 15 px/s</span>
</label>
</div>
<div class="row">
<button id="apxStart" style="grid-column:1/2">Démarrer / Start</button>
<button id="apxStop" style="grid-column:2/2">Arrêter / Stop</button>
<button id="apxReset" class="wide">Réinitialiser progression / Reset progress</button>
</div>
<div id="apxStatus"><span id="apxLed"></span><span id="apxMsg">En attente d'une image… / Waiting for an image…</span></div>
`;
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<by; dy++) {
for (let dx=0; dx<bx; dx++) {
const ix = scanX + dx, iy = scanY + dy;
if (ix>=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)');
})();