Production-grade Unity SDKManager callback interceptor - force rewards
// ==UserScript==
// @name ZeroAd: Unity WebGL
// @namespace https://greasyfork.org/users/zeroadv4
// @version 4.0.0
// @description Production-grade Unity SDKManager callback interceptor - force rewards
// @author ZeroAd Team
// @match *://*/*
// @grant none
// @run-at document-start
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// ═══════════════════════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════════════════════
const CONFIG = {
DEBUG: true,
SCAN_INTERVAL_MS: 2000,
MAX_SCANS: 30 // 60 seconds total
};
const METRICS = {
patchedInstances: 0,
interceptedCalls: 0,
forcedRewards: 0
};
// ═══════════════════════════════════════════════════════════════
// UNITY SDK METHOD TARGETS
// ═══════════════════════════════════════════════════════════════
const SDK_METHODS = [
'OnVideoAdEnded',
'OnVideoAdError',
'OnVideoAdSkipped',
'OnRewardedAdComplete',
'OnRewardedAdFailed',
'OnAdComplete',
'OnAdError',
'OnAdSkipped',
'AdComplete',
'AdFailed',
'AdSkipped',
'RewardGranted',
'RewardFailed'
];
const SDK_OBJECTS = [
'SDKManager',
'AdsManager',
'AdManager',
'RewardManager',
'VideoAdManager'
];
// ═══════════════════════════════════════════════════════════════
// UTILITY LAYER
// ═══════════════════════════════════════════════════════════════
const log = (...args) => CONFIG.DEBUG && console.log('[ZeroAd:Unity]', ...args);
const warn = (...args) => CONFIG.DEBUG && console.warn('[ZeroAd:Unity]', ...args);
const error = (...args) => console.error('[ZeroAd:Unity]', ...args);
// ═══════════════════════════════════════════════════════════════
// HOOK LAYER
// ═══════════════════════════════════════════════════════════════
/**
* Patch Unity instance SendMessage to force reward callbacks
*/
function patchUnityInstance(instance) {
if (!instance || instance.__zaPatched || typeof instance.SendMessage !== 'function') {
return false;
}
try {
const originalSendMessage = instance.SendMessage;
instance.SendMessage = function(objectName, methodName, param) {
METRICS.interceptedCalls++;
// Check if this is an SDK callback
const isSDKObject = SDK_OBJECTS.includes(objectName);
const isSDKMethod = SDK_METHODS.includes(methodName);
if (isSDKObject && isSDKMethod) {
// Check if parameter indicates failure
const isFalse = param === 'false' ||
param === false ||
param === '0' ||
param === 0;
if (isFalse) {
log(`${objectName}.${methodName}("${param}") → forcing "true"`);
param = 'true';
METRICS.forcedRewards++;
} else {
log(`${objectName}.${methodName}("${param}") → pass through`);
}
}
// Call original with potentially modified parameter
return originalSendMessage.call(this, objectName, methodName, param);
};
instance.__zaPatched = true;
METRICS.patchedInstances++;
log('✓ Unity instance patched');
return true;
} catch (e) {
error('Failed to patch Unity instance:', e);
return false;
}
}
/**
* Scan for Unity instances across multiple possible locations
*/
function scanAndPatchInstances() {
let foundAny = false;
// Check window.unityInstance (most common)
if (window.unityInstance && !window.unityInstance.__zaPatched) {
if (patchUnityInstance(window.unityInstance)) {
foundAny = true;
}
}
// Check window.gameInstance (alternative naming)
if (window.gameInstance && !window.gameInstance.__zaPatched) {
if (patchUnityInstance(window.gameInstance)) {
foundAny = true;
}
}
// Check window.unity (another variant)
if (window.unity && typeof window.unity.SendMessage === 'function' && !window.unity.__zaPatched) {
if (patchUnityInstance(window.unity)) {
foundAny = true;
}
}
// Check for instances in UnityLoader namespace
if (window.UnityLoader?.instances) {
Object.values(window.UnityLoader.instances).forEach(instance => {
if (instance && !instance.__zaPatched) {
if (patchUnityInstance(instance)) {
foundAny = true;
}
}
});
}
return foundAny;
}
// ═══════════════════════════════════════════════════════════════
// PROPERTY TRAPS
// ═══════════════════════════════════════════════════════════════
/**
* Install property trap for window.unityInstance
*/
function installUnityInstanceTrap() {
let _unityInstance = window.unityInstance;
Object.defineProperty(window, 'unityInstance', {
configurable: true,
enumerable: true,
get() {
return _unityInstance;
},
set(newValue) {
if (newValue && !newValue.__zaPatched) {
log('unityInstance assigned, patching...');
patchUnityInstance(newValue);
}
_unityInstance = newValue;
}
});
log('✓ unityInstance trap installed');
}
/**
* Install property trap for window.gameInstance
*/
function installGameInstanceTrap() {
let _gameInstance = window.gameInstance;
Object.defineProperty(window, 'gameInstance', {
configurable: true,
enumerable: true,
get() {
return _gameInstance;
},
set(newValue) {
if (newValue && !newValue.__zaPatched) {
log('gameInstance assigned, patching...');
patchUnityInstance(newValue);
}
_gameInstance = newValue;
}
});
log('✓ gameInstance trap installed');
}
// ═══════════════════════════════════════════════════════════════
// ORCHESTRATION LAYER
// ═══════════════════════════════════════════════════════════════
let scanCount = 0;
let scanInterval = null;
/**
* Periodic scan for Unity instances
*/
function startPeriodicScan() {
scanInterval = setInterval(() => {
scanCount++;
const foundNew = scanAndPatchInstances();
if (foundNew) {
log(`Found and patched instance(s) on scan ${scanCount}`);
}
// Stop scanning after max attempts
if (scanCount >= CONFIG.MAX_SCANS) {
log(`Stopping periodic scan after ${scanCount} attempts`);
clearInterval(scanInterval);
}
}, CONFIG.SCAN_INTERVAL_MS);
log('✓ Periodic scan started');
}
/**
* Setup mutation observer to detect Unity loader scripts
*/
function setupMutationObserver() {
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.tagName === 'SCRIPT') {
const src = node.src || '';
if (src.includes('unity') ||
src.includes('Build') ||
src.includes('loader.js')) {
log('Unity script detected:', src.substring(0, 80));
node.addEventListener('load', () => {
log('Unity script loaded, scanning for instances');
setTimeout(scanAndPatchInstances, 100);
});
}
}
// Check for canvas elements (Unity render target)
if (node.tagName === 'CANVAS') {
log('Canvas element added, scanning for Unity instances');
setTimeout(scanAndPatchInstances, 100);
}
}
}
});
if (document.documentElement) {
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
}
log('✓ MutationObserver active');
}
// ═══════════════════════════════════════════════════════════════
// INITIALIZATION
// ═══════════════════════════════════════════════════════════════
function initialize() {
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
log('ZeroAd: Unity WebGL Reward Fix v4.0.0');
log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
// Install property traps
installUnityInstanceTrap();
installGameInstanceTrap();
// Immediate scan
scanAndPatchInstances();
// Start periodic scanning
startPeriodicScan();
// Setup mutation observer
setupMutationObserver();
// Expose metrics
window._zaMetrics = window._zaMetrics || {};
window._zaMetrics.unity = METRICS;
// Status report
setTimeout(() => {
log('━━━ Status Report ━━━');
log('Metrics:', METRICS);
log('━━━━━━━━━━━━━━━━━━━━━');
}, 10000);
}
// Run immediately
initialize();
// Defensive: re-run on DOM ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', scanAndPatchInstances);
}
})();