ZeroAd: Loader

Hybrid detection loader with console log reading & multi-platform support

2026/08/22のページです。最新版はこちら

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         ZeroAd: Loader
// @namespace    https://greasyfork.org/en/users/1483567-vujohn123
// @version      5.0.0
// @description  Hybrid detection loader with console log reading & multi-platform support
// @author       ZeroAd Team
// @match        *://*/*
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    if (window.__zaLoaderLoaded) return;
    window.__zaLoaderLoaded = true;

    const CONFIG = {
        DEBUG: true,
        RETRY_DELAY: 100,
        MAX_RETRIES: 5,
        DETECTION_INTERVAL: 200,    // ms giữa các lần quét
        MAX_DETECTION_ATTEMPTS: 30, // ~6 giây
    };
    const PREFIX = '[ZeroAd:Loader]';
    const log = {
        info: (...args) => console.log(PREFIX, 'ℹ️', ...args),
        debug: (...args) => CONFIG.DEBUG && console.log(PREFIX, '🐛', ...args),
        warn: (...args) => console.warn(PREFIX, '⚠️', ...args),
        error: (...args) => console.error(PREFIX, '❌', ...args),
        success: (...args) => console.log(PREFIX, '✅', ...args),
    };

    // ============================================================
    //  PLATFORM MAP
    // ============================================================
    const SCRIPT_MAP = {
        'poki': 579407,
        'crazygames': 579404,
        'adinplay': 579403,
        'gamevui': 579405,
        'unity': 583938,
        'gamedistribution': 583939,
        'playgama': 592446,
        'yandex': 592456,
    };
    const FALLBACK_PLATFORM = 'adinplay';

    const loaded = new Set();
    const pending = new Set();
    let detectionCount = 0;

    // ============================================================
    //  LOAD SCRIPT VỚI RETRY
    // ============================================================
    function loadScriptWithRetry(platformKey, retries = CONFIG.MAX_RETRIES, delay = CONFIG.RETRY_DELAY) {
        const id = SCRIPT_MAP[platformKey];
        if (!id || loaded.has(platformKey) || pending.has(platformKey)) return;
        pending.add(platformKey);
        const url = `https://greasyfork.org/scripts/${id}/code/${id}.user.js`;
        const script = document.createElement('script');
        const tryLoad = (attempt) => {
            script.onload = () => {
                loaded.add(platformKey);
                pending.delete(platformKey);
                log.success(`Loaded ${platformKey}`);
            };
            script.onerror = () => {
                if (attempt < retries) {
                    const nextDelay = delay * Math.pow(2, attempt);
                    log.warn(`Retry ${platformKey} in ${nextDelay}ms (attempt ${attempt+1}/${retries})`);
                    setTimeout(() => tryLoad(attempt + 1), nextDelay);
                } else {
                    pending.delete(platformKey);
                    log.error(`Failed to load ${platformKey} after ${retries} attempts`);
                }
            };
            script.src = url;
            (document.head || document.documentElement).appendChild(script);
        };
        tryLoad(0);
    }

    // ============================================================
    //  DETECTION ENGINE – Hybrid
    // ============================================================
    function detectPlatforms() {
        const detected = new Set();
        const host = window.location.hostname.toLowerCase();
        const doc = document;

        // ----- 1. Hostname-based -----
        if (host.includes('poki.com')) detected.add('poki');
        if (host.includes('crazygames.com')) detected.add('crazygames');
        if (host.includes('gamevui.vn')) detected.add('gamevui');
        if (host.includes('gamedistribution.com')) detected.add('gamedistribution');
        if (host.includes('playgama.com')) detected.add('playgama');
        if (host.includes('yandex') || host.includes('games.yandex')) detected.add('yandex');

        // ----- 2. Global objects / classes -----
        if (window.PokiSDK) detected.add('poki');
        if (window.CrazyGames || window.CrazygamesAds) detected.add('crazygames');
        if (window.GVAdBreak) detected.add('gamevui');
        if (window.GDSdk || window.gdApi) detected.add('gamedistribution');
        if (window.PlayGama || window.playgama || window.pg || window.PG) detected.add('playgama');
        if (window.ysdk || window.YaGames) detected.add('yandex');

        // ----- 3. Unity detection (by object or canvas) -----
        if (window.unityInstance || window.gameInstance || window.UnityLoader) {
            detected.add('unity');
        } else {
            // Tìm canvas có class unity-canvas hoặc WebGL context
            const canvases = doc.querySelectorAll('canvas');
            for (const canvas of canvases) {
                if (canvas.classList.contains('unity-canvas')) {
                    detected.add('unity');
                    break;
                }
                try {
                    const ctx = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
                    if (ctx) {
                        // Nếu canvas có WebGL context và không phải là canvas của quảng cáo
                        detected.add('unity');
                        break;
                    }
                } catch(e) {}
            }
        }

        // ----- 4. Script src scanning -----
        const scripts = doc.querySelectorAll('script[src]');
        for (const script of scripts) {
            const src = script.src.toLowerCase();
            if (src.includes('poki.com/sdk')) detected.add('poki');
            if (src.includes('sdk.crazygames.com')) detected.add('crazygames');
            if (src.includes('gamevui.vn')) detected.add('gamevui');
            if (src.includes('gamedistribution.com')) detected.add('gamedistribution');
            if (src.includes('playgama.com')) detected.add('playgama');
            if (src.includes('yandex') || src.includes('games.yandex')) detected.add('yandex');
            if (src.includes('unity')) detected.add('unity');
        }

        // ----- 5. Meta tags / window properties -----
        const metaPlatform = doc.querySelector('meta[name="platform"]');
        if (metaPlatform) {
            const p = metaPlatform.getAttribute('content');
            if (p && SCRIPT_MAP[p]) detected.add(p);
        }
        if (window.gamePlatform && SCRIPT_MAP[window.gamePlatform]) {
            detected.add(window.gamePlatform);
        }

        // ----- 6. Console log reader (đã được tích hợp bên dưới) -----
        // Console logs sẽ được xử lý qua sự kiện, sẽ gọi detectPlatforms() lại nếu cần

        // Nếu không có gì -> fallback
        if (detected.size === 0) {
            detected.add(FALLBACK_PLATFORM);
        }

        return detected;
    }

    // ============================================================
    //  CONSOLE LOG READER – Lắng nghe logs để phát hiện platform
    // ============================================================
    let consoleLogDetected = new Set();
    function setupConsoleReader() {
        // Lưu trữ các hàm console gốc
        const originalLog = console.log;
        const originalWarn = console.warn;
        const originalError = console.error;

        const checkLog = (args) => {
            if (!args || args.length === 0) return;
            const msg = args.join(' ').toLowerCase();
            let extra = new Set();

            // Các pattern đặc trưng
            if (msg.includes('poki') || msg.includes('poki sdk')) extra.add('poki');
            if (msg.includes('crazygames') || msg.includes('crazygames sdk')) extra.add('crazygames');
            if (msg.includes('gamevui')) extra.add('gamevui');
            if (msg.includes('gamedistribution')) extra.add('gamedistribution');
            if (msg.includes('playgama')) extra.add('playgama');
            if (msg.includes('yandex') || msg.includes('ysdk') || msg.includes('yagames')) extra.add('yandex');
            if (msg.includes('unity') || msg.includes('unity webgl')) extra.add('unity');
            if (msg.includes('adinplay')) extra.add('adinplay');

            // Nếu có phát hiện mới, trigger re-detection
            if (extra.size > 0) {
                for (const p of extra) {
                    if (!consoleLogDetected.has(p)) {
                        consoleLogDetected.add(p);
                        log.debug(`Console log detected: ${p}`);
                        // Kích hoạt quét lại ngay
                        scheduleDetection();
                    }
                }
            }
        };

        console.log = function(...args) {
            originalLog.apply(console, args);
            checkLog(args);
        };
        console.warn = function(...args) {
            originalWarn.apply(console, args);
            checkLog(args);
        };
        console.error = function(...args) {
            originalError.apply(console, args);
            checkLog(args);
        };

        // Cũng có thể dùng sự kiện nếu có (một số trình duyệt hỗ trợ)
        // Không cần vì đã override hàm
        log.debug('Console reader installed');
    }

    // ============================================================
    //  DEBOUNCED DETECTION
    // ============================================================
    let detectionTimer = null;
    function scheduleDetection() {
        if (detectionTimer) {
            clearTimeout(detectionTimer);
        }
        detectionTimer = setTimeout(() => {
            detectionTimer = null;
            runDetection();
        }, 100);
    }

    function runDetection() {
        const platforms = detectPlatforms();
        log.debug('Detection result:', [...platforms]);
        for (const key of platforms) {
            if (!loaded.has(key) && !pending.has(key)) {
                loadScriptWithRetry(key);
            }
        }
        detectionCount++;
        // Nếu đã quét đủ số lần và vẫn không thấy platform mới -> dừng polling
        if (detectionCount >= CONFIG.MAX_DETECTION_ATTEMPTS) {
            log.debug('Max detection attempts reached, stopping periodic checks');
            if (window.__zaDetectionInterval) {
                clearInterval(window.__zaDetectionInterval);
                window.__zaDetectionInterval = null;
            }
        }
    }

    // ============================================================
    //  MUTATION OBSERVER
    // ============================================================
    function setupObserver() {
        const observer = new MutationObserver(() => {
            scheduleDetection();
        });
        observer.observe(document.documentElement, { childList: true, subtree: true, attributes: false });
        log.debug('MutationObserver installed');
    }

    // ============================================================
    //  GLOBAL TRAPS
    // ============================================================
    function installTraps() {
        const sdkNames = [
            'PokiSDK', 'CrazyGames', 'CrazygamesAds', 'GVAdBreak',
            'GDSdk', 'gdApi', 'unityInstance', 'gameInstance', 'UnityLoader',
            'PlayGama', 'playgama', 'pg', 'PG',
            'ysdk', 'YaGames'
        ];
        sdkNames.forEach(name => {
            let _value = window[name];
            Object.defineProperty(window, name, {
                configurable: true,
                enumerable: true,
                get() { return _value; },
                set(newVal) {
                    _value = newVal;
                    log.debug(`Trap: window.${name} assigned`);
                    scheduleDetection();
                }
            });
        });
        log.debug('Global traps installed');
    }

    // ============================================================
    //  PERIODIC SCAN
    // ============================================================
    function startPeriodicScan() {
        if (window.__zaDetectionInterval) return;
        window.__zaDetectionInterval = setInterval(() => {
            // Chỉ quét nếu chưa đạt max attempts
            if (detectionCount < CONFIG.MAX_DETECTION_ATTEMPTS) {
                runDetection();
            } else {
                // Nếu đã quét đủ mà không có gì mới, có thể dừng
                // Nhưng vẫn để cho các trap và observer kích hoạt lại nếu cần
            }
        }, CONFIG.DETECTION_INTERVAL);
        log.debug(`Periodic scan started (every ${CONFIG.DETECTION_INTERVAL}ms)`);
    }

    // ============================================================
    //  INIT
    // ============================================================
    function init() {
        installTraps();
        setupConsoleReader();
        setupObserver();
        startPeriodicScan();

        // Quét ngay lập tức
        setTimeout(() => runDetection(), 10);

        // Khi DOMContentLoaded, quét lại để bắt các script tải muộn
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => {
                log.debug('DOMContentLoaded – re-detecting');
                runDetection();
            });
        } else {
            // Nếu đã sẵn sàng, quét lại sau 500ms
            setTimeout(runDetection, 500);
        }

        log.info('Hybrid Loader initialized');
    }

    // Chờ head tồn tại
    if (document.head) {
        init();
    } else {
        const headObserver = new MutationObserver(() => {
            if (document.head) {
                headObserver.disconnect();
                init();
            }
        });
        headObserver.observe(document.documentElement, { childList: true, subtree: true });
    }

})();