Merge pull request #1 from JustEngineerLGTM/main

New paint prediction system
This commit is contained in:
SKProCH
2025-10-13 13:41:03 +03:00
committed by GitHub
3 changed files with 223 additions and 160 deletions
-23
View File
@@ -13,29 +13,6 @@ chrome.runtime.onInstalled.addListener(async () => {
console.log('📦 Installation resource test:', resources);
});
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'executeScript') {
// Get tabId from sender or requests
const tabId = request.tabId || sender.tab?.id;
if (!tabId) {
sendResponse({ success: false, error: 'Could not determine target tab' });
return;
}
// Use IIFE for async handlings
(async () => {
try {
await executeLocalScript(request.scriptName, tabId);
sendResponse({ success: true });
} catch (error) {
sendResponse({ success: false, error: error.message });
}
})();
return true; // Important: indicates async response
}
});
async function executeLocalScript(scriptName, tabId) {
try {
+220 -135
View File
@@ -10,36 +10,6 @@
// ==/UserScript==
localStorage.removeItem("lp");
// Fallback translation function for when utils manager isn't loaded
function getText(key, params) {
// Try to get translation from loadedTranslations
try {
if (window.loadedTranslations && window.loadedTranslations[key]) {
let text = window.loadedTranslations[key];
if (params) {
Object.keys(params).forEach(paramKey => {
text = text.replace(new RegExp(`{{${paramKey}}}`, 'g'), params[paramKey]);
});
}
return text;
}
// Try with state.language if available
if (window.state && window.state.language && window.loadedTranslations && window.loadedTranslations[window.state.language] && window.loadedTranslations[window.state.language][key]) {
let text = window.loadedTranslations[window.state.language][key];
if (params) {
Object.keys(params).forEach(paramKey => {
text = text.replace(new RegExp(`{{${paramKey}}}`, 'g'), params[paramKey]);
});
}
return text;
}
} catch (error) {
console.warn('Error in getText fallback:', error);
}
return key; // Fallback to key if no translation found
}
; (async () => {
// Prevent multiple instances of this script from running
@@ -952,6 +922,94 @@ function getText(key, params) {
// Create global account manager instance
const accountManager = new AccountManager();
// Local Charge Model to minimize API calls and drive account switching
const ChargeModel = (() => {
class Model {
constructor() {
this.map = new Map(); // token -> {charges,max,lastTickAt,lastSyncAt}
this.tickIntervalMs = 30_000; // +1 charge per 30s
this.timer = null;
this.startedAt = Date.now();
}
seedFromAccounts(accounts) {
const now = Date.now();
(accounts || []).forEach(acc => {
if (!acc || !acc.token) return;
const existing = this.map.get(acc.token) || {};
const charges = Number.isFinite(acc.Charges) ? Math.floor(acc.Charges) : (existing.charges || 0);
const max = Number.isFinite(acc.Max) ? Math.floor(acc.Max) : (existing.max || 1);
this.map.set(acc.token, {
charges: Math.max(0, Math.min(charges, max)),
max: Math.max(1, max),
lastTickAt: existing.lastTickAt || now,
lastSyncAt: existing.lastSyncAt || now,
});
});
}
ensureToken(token) {
if (!token) return null;
if (!this.map.has(token)) {
this.map.set(token, { charges: 0, max: Math.max(1, state.maxCharges || 1), lastTickAt: Date.now(), lastSyncAt: 0 });
}
return this.map.get(token);
}
get(token) { return this.ensureToken(token); }
getForCurrent() { return this.get(accountManager.getCurrentAccount()?.token); }
setFromServer(token, charges, max) {
const node = this.ensureToken(token);
if (!node) return;
node.charges = Math.max(0, Math.min(Math.floor(charges || 0), Math.max(1, Math.floor(max || node.max || 1))));
node.max = Math.max(1, Math.floor(max || node.max || 1));
node.lastSyncAt = Date.now();
}
decrement(token, amount) {
const node = this.ensureToken(token);
if (!node) return 0;
const n = Math.max(0, Math.floor(amount || 0));
node.charges = Math.max(0, node.charges - n);
return node.charges;
}
incrementTickAll() {
const now = Date.now();
accountManager.getAllAccounts().forEach(acc => {
if (!acc?.token) return;
const node = this.ensureToken(acc.token);
if (!node) return;
// catch-up ticks if tab was inactive
const elapsed = now - (node.lastTickAt || now);
const ticks = Math.floor(elapsed / this.tickIntervalMs);
if (ticks > 0) {
node.charges = Math.min(node.max, node.charges + ticks);
node.lastTickAt = (node.lastTickAt || now) + ticks * this.tickIntervalMs;
}
});
// Mirror values into AccountManager and UI state
this.syncToAccountManager();
}
start() {
if (this.timer) return;
this.timer = setInterval(() => this.incrementTickAll(), this.tickIntervalMs);
}
stop() { if (this.timer) { clearInterval(this.timer); this.timer = null; } }
predictTimeToReach(token, target) {
const node = this.get(token);
if (!node) return Infinity;
if (node.charges >= target) return 0;
return (target - node.charges) * this.tickIntervalMs;
}
syncToAccountManager() {
const list = accountManager.getAllAccounts();
list.forEach(acc => {
const node = this.map.get(acc.token);
if (!node) return;
accountManager.updateAccountData(acc.token, { Charges: node.charges, Max: node.max });
});
renderAccountsList();
}
}
return new Model();
})();
// GLOBAL STATE
const state = {
running: false,
@@ -1017,6 +1075,10 @@ function getText(key, params) {
notificationIntervalMinutes: CONFIG.NOTIFICATIONS.REPEAT_MINUTES,
_lastChargesNotifyAt: 0,
_lastChargesBelow: true,
// Switch debouncing state
lastSwitchAt: 0,
paintedSinceSwitch: 0,
minMsBetweenSwitches: 3000,
// Smart save tracking
_lastSavePixelCount: 0,
_lastSaveTime: 0,
@@ -4348,7 +4410,7 @@ function getText(key, params) {
const timeText = Utils.msToTimeText(remainingMs);
if (currentChargesEl) {
currentChargesEl.innerHTML = `${state.displayCharges} / ${state.maxCharges}`;
currentChargesEl.innerHTML = `${state.displayCharges} / ${max}`;
}
if (
@@ -4546,7 +4608,7 @@ function getText(key, params) {
totalMaxCharges = accounts.reduce((sum, acc) => {
if (currentAccount && acc.token === currentAccount.token) {
// Use real-time max charges for current account
return sum + Math.floor(state.maxCharges || acc.Max || 0);
return sum + Math.floor((state.fullChargeData?.max ?? state.maxCharges ?? acc.Max ?? 0));
} else {
// Use stored data for other accounts
return sum + Math.floor(acc.Max || 0);
@@ -4571,7 +4633,7 @@ function getText(key, params) {
<i class="fas fa-bolt"></i> ${Utils.t('charges')}
</div>
<div class="wplace-stat-value" id="wplace-stat-charges-value">
${state.displayCharges} / ${state.maxCharges}
${state.displayCharges} / ${state.fullChargeData?.max ?? state.maxCharges}
</div>
</div>
<div class="wplace-stat-item">
@@ -8503,9 +8565,20 @@ function getText(key, params) {
state.lastPaintedPosition = { x: lastPixel.localX, y: lastPixel.localY };
}
// Track painted pixels since last account switch to avoid flip-flop
state.paintedSinceSwitch = (state.paintedSinceSwitch || 0) + actuallyPaintedCount;
// IMPORTANT: Decrement charges locally to match Acc-Switch.js behavior
state.displayCharges = Math.max(0, state.displayCharges - batchSize);
state.preciseCurrentCharges = Math.max(0, state.preciseCurrentCharges - batchSize);
// Also update the global local charge model per account with bonus logic
try {
const tok = accountManager.getCurrentAccount()?.token;
const after = ChargeModel.decrement(tok, batchSize);
state.displayCharges = Math.floor(after);
state.preciseCurrentCharges = after;
if (tok) accountManager.updateAccountData(tok, { Charges: state.displayCharges });
} catch {}
state.fullChargeData = {
...state.fullChargeData,
@@ -8671,13 +8744,23 @@ function getText(key, params) {
// IMPORTANT: Check charges once at start, then paint until depleted
console.log('🔋 Checking initial charges for painting session');
const initialChargeCheck = await WPlaceService.getCharges();
state.displayCharges = Math.floor(initialChargeCheck.charges);
state.preciseCurrentCharges = initialChargeCheck.charges;
state.cooldown = initialChargeCheck.cooldown;
// Use local charge model to avoid extra API calls
const currentToken = accountManager.getCurrentAccount()?.token;
let node = ChargeModel.get(currentToken);
// One-time sync for current account if never synced (safety)
if (node && !node.lastSyncAt) {
try {
const sync = await WPlaceService.getCharges();
ChargeModel.setFromServer(currentToken, sync.charges, sync.max);
node = ChargeModel.get(currentToken);
} catch {}
}
state.displayCharges = Math.floor(node?.charges || 0);
state.preciseCurrentCharges = node?.charges || 0;
state.cooldown = CONFIG.COOLDOWN_DEFAULT;
await updateStats();
if (state.displayCharges <= 0) {
console.log('⚡ No charges available, skipping painting session');
console.log('⚡ No charges available (local), skipping painting session');
return 'charges_depleted';
}
@@ -9030,55 +9113,38 @@ function getText(key, params) {
// const maxChargeChecks = 10; // REMOVED: No limit on API calls during cooldown
while (!state.stopFlag) {
chargeCheckCount++;
const threshold = Math.max(1, state.cooldownChargeThreshold || 1);
const accounts = accountManager.getAllAccounts();
let anyReady = false;
let bestMs = Infinity;
let currentCharges = ChargeModel.getForCurrent()?.charges || 0;
const { charges, cooldown } = await WPlaceService.getCharges();
state.displayCharges = Math.floor(charges);
state.preciseCurrentCharges = charges;
state.cooldown = cooldown;
for (const acc of accounts) {
const node = ChargeModel.get(acc.token);
if (!node) continue;
if (node.charges >= threshold) {
anyReady = true;
break;
}
const ms = ChargeModel.predictTimeToReach(acc.token, threshold);
if (ms < bestMs) bestMs = ms;
}
if (state.displayCharges >= state.cooldownChargeThreshold) {
console.log(`✅ Cooldown target reached: ${state.displayCharges}/${state.cooldownChargeThreshold}`);
if (anyReady) {
console.log(`✅ Cooldown target reached locally (≥${threshold})`);
NotificationManager.maybeNotifyChargesReached(true);
await updateStats();
return 'target_reached';
}
const waitMs = Number.isFinite(bestMs) ? Math.max(1000, Math.min(bestMs, 10000)) : 10000;
updateUI('noChargesThreshold', 'warning', {
time: Utils.msToTimeText(state.cooldown),
threshold: state.cooldownChargeThreshold,
current: state.displayCharges,
time: Utils.msToTimeText(waitMs),
threshold,
current: currentCharges,
});
await updateStats();
// Smart delay calculation to reduce API calls
const chargesNeeded = state.cooldownChargeThreshold - state.displayCharges;
const estimatedWaitTime = chargesNeeded * state.cooldown;
// Use longer delays during cooldown to prevent rate limiting
let delayTime;
if (chargeCheckCount < 3) {
// First few checks - shorter delay
delayTime = Math.max(3000, state.cooldown); // 3 seconds minimum
} else if (estimatedWaitTime > 60000) {
// Long wait expected - check every 15 seconds
delayTime = 10000;
} else if (estimatedWaitTime > 30000) {
// Medium wait - check every 15 seconds
delayTime = 15000;
} else {
// Close to target - check every 5 seconds
delayTime = 5000;
}
console.log(`⏱️ Cooldown check ${chargeCheckCount}: ${state.displayCharges}/${state.cooldownChargeThreshold} charges, waiting 10s before next check`);
await Utils.sleep(10000);
// REMOVED: No limit on charge checks - bot will wait infinitely until charges are available
// if (chargeCheckCount >= maxChargeChecks) {
// console.warn('⚠️ Max charge checks reached during cooldown, continuing anyway');
// break;
// }
await Utils.sleep(waitMs);
}
return 'stopped';
@@ -9847,9 +9913,10 @@ function getText(key, params) {
console.log('✅ Found wasm Module...');
return url.split('/').pop();
}
} catch { }
} catch (e) { /* ignore individual fetch errors */ }
}
console.error(`❌ Could not find Pawtect chunk: `, error);
console.error('❌ Could not find Pawtect chunk among preloaded modules');
return null;
}
async function purchase(type) {
@@ -9993,12 +10060,6 @@ function getText(key, params) {
});
}
async function fetchAccount() {
const { ID, Charges, Max, Droplets } = await WPlaceService.fetchCheck();
console.log("User's ID :", ID);
console.log("User's Charges :", Charges, "/", Max);
console.log("User's Droplets :", Droplets);
}
async function fetchAllAccountDetails() {
if (state.isFetchingAllAccounts) {
@@ -10109,6 +10170,7 @@ function getText(key, params) {
}
console.log(`🎯 [FETCH] Completed fetching fresh data for all accounts`);
try { ChargeModel.seedFromAccounts(accountManager.getAllAccounts()); } catch {}
}
// Render the accounts list with fresh data
@@ -10131,34 +10193,18 @@ function getText(key, params) {
// Function to update current account charges in the account list
async function updateCurrentAccountInList() {
if (accountManager.getAccountCount() === 0) return;
try {
// Always trust backend /me and then map by ID to avoid mixing data between tokens
const currentAccountData = await WPlaceService.getCharges();
state.displayCharges = Math.floor(currentAccountData.charges);
state.preciseCurrentCharges = currentAccountData.charges;
const current = accountManager.getCurrentAccount();
const node = ChargeModel.get(current?.token);
if (!current || !node) return;
state.displayCharges = Math.floor(node.charges || 0);
state.preciseCurrentCharges = node.charges || 0;
await updateStats();
const accounts = accountManager.getAllAccounts();
const idx = accounts.findIndex(acc => acc.ID === currentAccountData.id);
const targetToken = idx !== -1 ? accounts[idx].token : accountManager.getCurrentAccount()?.token;
if (targetToken) {
accountManager.updateAccountData(targetToken, {
Charges: Math.floor(state.displayCharges || 0),
Max: Math.floor(currentAccountData.max || state.maxCharges || 0),
Droplets: Math.floor(currentAccountData.droplets)
});
// Keep manager index in sync with reality when possible
if (idx !== -1 && typeof accountManager.setCurrentIndex === 'function') {
accountManager.setCurrentIndex(idx);
state.accountIndex = idx;
}
// Re-render the account list to show updated charges
renderAccountsList();
}
accountManager.updateAccountData(current.token, {
Charges: Math.floor(node.charges || 0),
Max: Math.floor(node.max || current.Max || 0)
});
renderAccountsList();
} catch (e) {
console.warn('⚠️ updateCurrentAccountInList failed:', e);
}
@@ -10321,6 +10367,18 @@ function getText(key, params) {
async function switchToNextAccount(accounts) {
console.log(`🔄 [SWITCH] Starting account switch`);
// Debounce rapid consecutive switches when no painting happened
try {
const now = Date.now();
const last = state.lastSwitchAt || 0;
const minGap = state.minMsBetweenSwitches || 3000;
const painted = state.paintedSinceSwitch || 0;
if (now - last < minGap && painted === 0) {
console.log(`⏳ [SWITCH] Debounced rapid switch (Δ${now - last}ms < ${minGap}ms and paintedSinceSwitch=${painted}).`);
return false;
}
} catch {}
// Validate we have accounts
if (accountManager.getAccountCount() === 0) {
console.error('❌ No accounts available for switching');
@@ -10401,6 +10459,10 @@ function getText(key, params) {
// Update the account status and UI after successful switch
await updateCurrentAccountInList();
// Record switch time and reset painted counter to prevent rapid bouncing
state.lastSwitchAt = Date.now();
state.paintedSinceSwitch = 0;
return true;
} catch (error) {
console.error('❌ [SWITCH] Account switch failed:', error);
@@ -10411,6 +10473,19 @@ function getText(key, params) {
// SIMPLIFIED helper function for specific account switching
async function switchToSpecificAccount(token, accountName) {
console.log(`🔄 [SPECIFIC SWITCH] Attempting to switch to account: ${accountName}`);
// Debounce rapid consecutive switches when no painting happened
try {
const now = Date.now();
const last = state.lastSwitchAt || 0;
const minGap = state.minMsBetweenSwitches || 3000;
const painted = state.paintedSinceSwitch || 0;
if (now - last < minGap && painted === 0) {
console.log(`⏳ [SPECIFIC SWITCH] Debounced rapid switch (Δ${now - last}ms < ${minGap}ms and paintedSinceSwitch=${painted}).`);
return false;
}
} catch {}
if (!token) {
console.error('❌ [SPECIFIC SWITCH] Missing token');
return false;
@@ -10471,6 +10546,7 @@ function getText(key, params) {
// Fetch fresh stats for UI/state
const { charges, cooldown, droplets, max } = await WPlaceService.getCharges();
try { ChargeModel.setFromServer(token, charges, max); } catch {}
state.displayCharges = Math.floor(charges);
state.preciseCurrentCharges = charges;
state.cooldown = cooldown;
@@ -10530,42 +10606,43 @@ function getText(key, params) {
try {
const total = accountManager.getAccountCount();
if (total <= 1) return false;
const threshold = Math.max(1, minRequired || 1);
const startIdx = accountManager.currentIndex;
let best = { cooldown: Infinity, token: null, name: null, idx: -1 };
let candidate = null; // {token,name,idx}
let bestWait = Infinity; // ms to reach threshold
for (let step = 1; step <= total - 1; step++) {
const idx = (startIdx + step) % total;
const acc = accountManager.getAccountByIndex(idx);
if (!acc || !acc.token) continue;
const node = ChargeModel.get(acc.token);
const localCharges = Math.floor(node?.charges || 0);
console.log(`🔄 [SEARCH] Switching temporarily to ${acc.displayName} (${idx + 1}/${total}) to check charges...`);
const ok = await switchToSpecificAccount(acc.token, acc.displayName);
if (!ok) {
console.warn(`⚠️ [SEARCH] Failed to switch to ${acc.displayName}, trying next...`);
continue;
}
const currentAccountData = await WPlaceService.getCharges();
const charges = Math.floor(currentAccountData?.charges || 0);
const cooldown = Math.max(0, Number(currentAccountData?.cooldown || 0));
if (charges >= minRequired) {
console.log(`✅ [SEARCH] Found account with sufficient charges: ${acc.displayName} (⚡${charges})`);
return true;
}
console.log(`⏳ [SEARCH] ${acc.displayName} has no charges (⚡${charges}), cooldown ${cooldown}ms`);
if (cooldown < best.cooldown) {
best = { cooldown, token: acc.token, name: acc.displayName, idx };
console.log(`🔍 [SEARCH] Checking locally ${acc.displayName}: ⚡${localCharges}/${node?.max ?? 0}`);
if (localCharges >= threshold) {
candidate = { token: acc.token, name: acc.displayName, idx };
break;
} else {
const eta = ChargeModel.predictTimeToReach(acc.token, threshold);
if (eta < bestWait) {
bestWait = eta;
candidate = { token: acc.token, name: acc.displayName, idx };
}
}
}
// None had enough charges: switch to the one with the soonest recharge and return false
if (best.token && best.idx !== accountManager.currentIndex) {
console.log(`🎯 [SEARCH] Switching to account with soonest recharge: ${best.name} (~${Utils.msToTimeText(best.cooldown)})`);
await switchToSpecificAccount(best.token, best.name);
if (candidate && ChargeModel.get(candidate.token)?.charges >= threshold) {
console.log(`✅ [SEARCH] Local model found eligible account: ${candidate.name}`);
const ok = await switchToSpecificAccount(candidate.token, candidate.name);
return !!ok;
}
// None eligible yet do not switch now. Caller may enter cooldown.
if (candidate) {
console.log(`🕒 [SEARCH] No accounts meet threshold. Best candidate: ${candidate.name} in ~${Utils.msToTimeText(bestWait)}`);
} else {
console.log('🎯 [SEARCH] Staying on current account for cooldown.');
console.log('🕒 [SEARCH] No candidate accounts available.');
}
return false;
} catch (e) {
@@ -10583,6 +10660,14 @@ function getText(key, params) {
console.log('🔄 Initial account load from cache...');
try {
await accountManager.loadAccounts();
// Seed and start local charge model regardless of count
try {
state.chargeModel = ChargeModel;
ChargeModel.seedFromAccounts(accountManager.getAllAccounts());
ChargeModel.start();
console.log('⚡ Local ChargeModel started (tick +1 per 30s for all accounts)');
} catch (e) { console.warn('ChargeModel init failed:', e); }
if (accountManager.getAccountCount() > 0) {
console.log(`✅ Loaded ${accountManager.getAccountCount()} cached accounts`);
renderAccountsList();
+3 -2
View File
@@ -595,9 +595,10 @@
console.log('✅ Found wasm Module...');
return url.split('/').pop();
}
} catch { }
} catch (e) { /* ignore individual fetch errors */ }
}
console.error(`❌ Could not find Pawtect chunk with string: ${str}`);
console.error('❌ Could not find Pawtect chunk among preloaded modules');
return null;
}
// Audio notification system