Rain.GG Case Battle Verifier

Verify case battle fairness directly on the site.

// ==UserScript==
// @name         Rain.GG Case Battle Verifier
// @namespace    https://gge.gg
// @version      1.11
// @description  Verify case battle fairness directly on the site.
// @match        https://rain.gg/*
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/hmac-sha512.min.js
// @grant        none
// @icon         https://www.google.com/s2/favicons?sz=64&domain=rain.gg
// @license      WTFPL
// @author       twitter.com/thes0meguy
// ==/UserScript==
(function() {
    'use strict';

    function waitForModal() {
        const modal = document.getElementById('modal_window');
        if (modal && !modal.dataset.verifierInjected) {
            injectVerifier(modal);
            modal.dataset.verifierInjected = 'true';
        }
    }

    const modalCheckInterval = setInterval(waitForModal, 500);

    function injectVerifier(modal) {
        let serverSeed = getInputValueByLabelText(modal, 'Server seed');
        let eosBlockHash = getInputValueByLabelText(modal, 'EOS block hash');

        if (!serverSeed || !eosBlockHash) {
            console.warn('Could not automatically find Server seed or EOS block hash.');
            return;
            serverSeed = prompt('Please enter the Server seed:', '');
            eosBlockHash = prompt('Please enter the EOS block hash:', '');

            if (!serverSeed || !eosBlockHash) {
                alert('Error: Server seed and EOS block hash are required.');
                return;
            }
        }

        const roundsDiv = createInputDiv('Number of Rounds', `<input type="number" min="1" value="1" id="rounds-input" style="display: flex;
    flex-grow: 1;
    padding: 9px 12px;
    border: 0px;
    outline: none;
    min-height: 38px;
    background-color: transparent;
    color: inherit;
    font-family: var(--font-geogrotesque-wide);
    font-weight: 500;
    font-size: 14px;
    line-height: 1.42957;
    text-transform: none;
    caret-color: rgb(240, 242, 245) !important;">`);
        const playersDiv = createInputDiv('Number of Players', `
            <select id="players-select" style="display: flex;
    flex-grow: 1;
    padding: 9px 12px;
    border: 0px;
    outline: none;
    min-height: 38px;
    background-color: transparent;
    color: inherit;
    font-family: var(--font-geogrotesque-wide);
    font-weight: 500;
    font-size: 14px;
    line-height: 1.42957;
    text-transform: none;
    caret-color: rgb(240, 242, 245) !important;">
                <option value="2">2 players</option>
                <option value="3">3 players</option>
                <option value="4">4 players</option>
                <option value="6">6 players</option>
            </select>
        `);

        const buttonDiv = document.createElement('div');
        buttonDiv.style.textAlign = 'center';
        buttonDiv.style.marginTop = '15px';
        buttonDiv.innerHTML = `
            <button id="verify-btn" style="    font-size: 16px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    outline: none;
    border-radius: 8px;
    letter-spacing: 0.6px;
    background-color: rgb(246, 175, 22);
    color: rgb(1, 16, 30);
    min-height: 32px;
    padding: 7px 12px;
    width: 100%;
    font-family: var(--font-geogrotesque-wide);
    font-weight: 900;">VERIFY</button>
        `;

        const resultsDiv = document.createElement('div');
        resultsDiv.id = 'results';
        resultsDiv.style.marginTop = '20px';

        const insertPoint = modal.querySelector('div[class*="sc-482ebc6f-0"]');
        if (!insertPoint) {
            console.error('Could not find the insert point in the modal.');
            return;
        }

        insertPoint.appendChild(roundsDiv);
        insertPoint.appendChild(playersDiv);
        insertPoint.appendChild(buttonDiv);
        insertPoint.appendChild(resultsDiv);

        document.getElementById('verify-btn').addEventListener('click', function() {
            const rounds = parseInt(document.getElementById('rounds-input').value);
            const players = parseInt(document.getElementById('players-select').value);

            if (isNaN(rounds) || rounds < 1) {
                alert('Please enter a valid number of rounds.');
                return;
            }

            resultsDiv.innerHTML = '';

            let tableHtml = '<table style="width: 100%; border-collapse: collapse;">';
            tableHtml += '<thead style="text-align: inherit;  color: rgb(240, 242, 245);font-family: var(--font-geogrotesque-wide);font-weight: 500;font-size: 12px;line-height: 1.33433;text-transform: none;"><tr><th style="border: 0px solid #ccc; padding: 2px;">Round</th>';

            for (let playerNum = 0; playerNum < players; playerNum++) {
                tableHtml += `<th style="border: 0px solid #ccc; padding: 2px;">Slot ${playerNum}</th>`;
            }
            tableHtml += '</tr></thead><tbody style="text-align: center;  color: rgb(240, 242, 245);font-family: var(--font-geogrotesque-wide);font-weight: 500;font-size: 12px;line-height: 1.33433;text-transform: none;">';

            for (let roundNum = 1; roundNum <= rounds; roundNum++) {
                tableHtml += `<tr><td style="border: 0px solid #ccc; padding: 2px;">${roundNum}</td>`;
                for (let playerNum = 0; playerNum < players; playerNum++) {
                    const randomNumber = roll(serverSeed, eosBlockHash, roundNum, playerNum);
                    tableHtml += `<td style="border: 0px solid #ccc; padding: 2px;">${randomNumber}</td>`;
                }
                tableHtml += '</tr>';

                if (roundNum === rounds) {
                    tableHtml += `<tr><td style="border: 0px solid #ccc; padding: 2px;">Tie</td>`;
                    for (let playerNum = 0; playerNum < players; playerNum++) {
                        const tie = tieRoll(serverSeed, eosBlockHash, roundNum, playerNum);
                        tableHtml += `<td style="border: 0px solid #ccc; padding: 2px;">${tie}</td>`;
                    }
                    tableHtml += '</tr>';
                }
            }

            tableHtml += '</tbody></table>';

            resultsDiv.innerHTML = tableHtml;
        });
    }

    function getInputValueByLabelText(modal, labelText) {
        const spans = modal.querySelectorAll('span');
        for (let span of spans) {
            if (span.textContent.trim() === labelText) {
                let parentDiv = span.closest('div');
                let inputDiv = parentDiv.nextElementSibling;
                if (inputDiv) {
                    const input = inputDiv.querySelector('input');
                    if (input) {
                        return input.value;
                    }
                }
            }
        }
        return null;
    }

    function createInputDiv(labelText, inputHTML) {
        const containerDiv = document.createElement('div');
        containerDiv.style.marginTop = '10px';
        containerDiv.innerHTML = `
            <div style="margin-bottom: 5px;">
                <span style="text-align: inherit;color: rgb(240, 242, 245);font-family: var(--font-geogrotesque-wide);font-weight: 500;font-size: 12px;line-height: 1.33433;text-transform: none;">${labelText}</span>
            </div>
            <div style="display: flex; background-color: rgb(1, 16, 30); color: rgb(133, 150, 173); align-items: center; margin-top: 4px; border: 1px solid rgb(37, 57, 82); border-radius: 8px; overflow: hidden;">
                ${inputHTML}
            </div>
        `;
        return containerDiv;
    }


    function* getBytesChunks(serverSeed, clientSeed, nonce) {
        let step = 0;
        let counter = 0;

        while (true) {
            const hmac = CryptoJS.HmacSHA512(`${clientSeed}:${nonce}:${step}`, serverSeed);
            const hashValue = hmac.toString(CryptoJS.enc.Hex);

            while (counter < 16) {
                const hashBytes = [];

                for (let i = 0; i < 4; i++) {
                    const byteHex = hashValue.substring(i * 2 + counter * 8, i * 2 + 2 + counter * 8);
                    hashBytes.push(parseInt(byteHex, 16));
                }

                yield hashBytes;
                counter += 1;
            }

            counter = 0;
            step += 1;
        }
    }

    function getFloats(serverSeed, clientSeed, nonce, count) {
        const bytesGenerator = getBytesChunks(serverSeed, clientSeed, nonce);
        const floats = [];

        while (floats.length < count) {
            const bytes = bytesGenerator.next().value;
            const float = bytes.reduce((acc, value, i) => acc + value / Math.pow(256, i + 1), 0);
            floats.push(float);
        }

        return floats;
    }

    function roll(serverSeed, eosBlockHash, roundNum, playerNum) {
        const nonce = `${roundNum}:${playerNum}`;
        const float = getFloats(serverSeed, eosBlockHash, nonce, 1)[0];

        return Math.floor(float * 10000000 + 1);
    }

    function tieRoll(serverSeed, eosBlockHash, roundNum, playerNum) {
        const nonce = `${roundNum}:${playerNum}:tie`;
        const float = getFloats(serverSeed, eosBlockHash, nonce, 1)[0];

        return Math.floor(float * (9999 - 1000 + 1) + 1000);
    }

})();