OutcomeDB

Captures crime outcome, skill gain and target data for analysis

目前為 2025-03-16 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         OutcomeDB
// @namespace    de.sabbasofa.outcomedb
// @version      2.1.0
// @description  Captures crime outcome, skill gain and target data for analysis
// @author       Hemicopter [2780600], Lazerpent [2112641]
// @match        https://www.torn.com/loader.php?sid=crimes*
// @match        https://torn.com/loader.php?sid=crimes*
// @grant        GM_xmlhttpRequest
// @connect      api.lzpt.io

// ==/UserScript==

(function () {
    'use strict';

    const isTampermonkeyEnabled = typeof unsafeWindow !== 'undefined';
    const win = isTampermonkeyEnabled ? unsafeWindow : window;
    const {fetch: originalFetch} = win;
    let currentCrimesByTypeData;
    let serverTime = Math.floor(Date.now() / 1000);

    win.fetch = async (...args) => {
        let [resource, config] = args;
        return originalFetch(resource, config).then(response => detectCrimeDataRequest(resource, response));
    };
    registerCopyError();
    console.log("[OutcomeDB] Watching for crime.");

    function detectCrimeDataRequest(resource, response) {
        if(!(resource.includes("sid=crimesData"))) return response;
        if (resource.includes("step=attempt")) response.clone().text().then(body => handleCrimeAttempt(body, resource));
        if (resource.includes("step=crimesList")) response.clone().text().then(body => handleCrimesList(body, resource));
        return response;
    }

    function handleCrimeAttempt(body, resource) {
        console.log("[OutcomeDB] Found crime attempt.");
        console.log("[OutcomeDB] url:", resource);

        //Most likely cloudflare turnstile or server error
        if(containsHtml(body)) {
            console.error("[OutcomeDB] Unexpected HTML data, skipping...");
            return;
        }

        try {
            let data = JSON.parse(body);
            if (data.error) {
                console.log("[OutcomeDB] Failed crime attempt: " + data.error);
                console.log(JSON.stringify(data));
                return;
            }
            if (!(data.DB && data.DB.outcome)) return;
            if(data.DB.outcome.result === "error") {
                console.log("[OutcomeDB] Failed crime attempt.");
                console.log(JSON.stringify(data));
                return;
            }
            console.log("[OutcomeDB] Found outcome.");
            console.log("[OutcomeDB] Preparing bundle.");
            serverTime = data.DB.time;
            let bundle = {};
            bundle.outcome = data.DB.outcome;
            bundle.typeID = resource.split("typeID=")[1].split("&")[0];
            bundle.crimeID = resource.split("crimeID=")[1].split("&")[0];

            bundle.skillBefore = getStat("Skill");
            bundle.skillAfter = data.DB.currentUserStatistics[0].value;
            bundle.progressionBonus = getStat("Progression bonus");

            if(!bundle.skillBefore || !bundle.skillAfter || !bundle.progressionBonus) return;

            bundle.additionalData = getAdditionalData(data.DB.crimesByType, bundle.typeID, resource);
            console.log("[OutcomeDB] Ready to send bundle.", JSON.stringify(bundle));
            sendBundleToAPI(bundle);
            currentCrimesByTypeData = data.DB.crimesByType;

        } catch (e) {
            if(e instanceof SyntaxError) return;
            handleError("crime_attempt_parse", JSON.stringify({error: {message: e.message, stack: e.stack}, body: body}));
        }
    }

    function handleCrimesList(body, resource) {
        console.log("[OutcomeDB] Updating crimes data.");

        //Most likely cloudflare turnstile or server error
        if(containsHtml(body)) {
            console.error("[OutcomeDB] Unexpected HTML data, skipping...");
            return;
        }

        try {
            let data = JSON.parse(body);
            if (data.error) {
                console.log("[OutcomeDB] Failed crimesList: " + data.error);
                console.log(JSON.stringify(data));
                return;
            }
            if (!(data.DB && data.DB.crimesByType)) return;
            currentCrimesByTypeData = data.DB.crimesByType;
            serverTime = data.DB.time;

        } catch (e) {
            if(e instanceof SyntaxError) return;
            handleError("crime_list_parse", JSON.stringify({error: {message: e.message, stack: e.stack}, body: body}));
        }
    }

    function sendBundleToAPI(bundle) {
        GM_xmlhttpRequest({
            method: "POST",
            url: "https://api.lzpt.io/outcomedb",
            headers: {"Content-Type": "application/json"},
            data: JSON.stringify(bundle),
            onload: function (response) {
                if(!response) return; // because pda doesn't know how to network i guess
                if(containsHtml(response.responseText)) {
                    console.error("[OutcomeDB] lzpt rate limit hit, skipping...");
                    return;
                }
                console.log("[OutcomeDB] Bundle successfully sent to API:", response.responseText);

                const json = JSON.parse(response.responseText);
                if (json.error) {
                    handleError("post_invalid", JSON.stringify({error: {responseText: response.responseText}}));
                }
            },
            onerror: function (e) {
                handleError("api_post_error", JSON.stringify({error: {message: e.statusText, status: e.status, full: e}}));
            }
        });
    }

    function getStat(name) {
        let allStatisticButtons = Array.from(win.document.querySelectorAll('li[class^="statistic___"]'));

        let statButton = allStatisticButtons.find(button => {
            return Array.from(button.querySelectorAll('span')).some(span => span.textContent.trim() === name);
        });

        if (statButton) {
            let valueSpan = statButton.querySelector('span[class^="value___"]');
            if (valueSpan) {
                console.log(`[OutcomeDB] Found stat (${name}): '${valueSpan.textContent}'`);
                return valueSpan.textContent;
            }
        }
        handleError("stat_missing", JSON.stringify({stat: name}));
    }

    function getAdditionalData(attemptData, typeID, resource) {
        try {
            if(typeID === "12") return extractScammingData(attemptData, resource);
            return null;

        } catch(error) {
            console.error("[OutcomeDB] Additional data failed, skipping:", error);
            return null;
        }
    }

    function handleError(name, data) {
        localStorage.setItem("outcomedb_last_error", JSON.stringify({type: name, data: data, timestamp: (Math.floor(Date.now() / 1000))}));
        console.error("[OutcomeDB] " + name + ":", data);
        alert("OutcomeDB error " + name + ". Please check console and report this to Hemicopter [2780600]");
    }

    function copyLastError() {
        let heading = win.document.querySelectorAll('h4[class^="heading___"]')[0];
        let content = localStorage.getItem("outcomedb_last_error");
        if (!content) return;
        navigator.clipboard.writeText(content);
        setTimeout(resetHeading, 1000, heading, heading.innerHTML);
        heading.innerHTML = "Copied successfully.";
    }

    function resetHeading(heading, text) {
        heading.innerHTML = text;
    }

    function registerCopyError() {
        let heading = win.document.querySelectorAll('h4[class^="heading___"]')[0];
        heading.addEventListener("click", copyLastError);
    }

    function containsHtml(text) {
        return text.includes("!DOCTYPE") || text.includes("!doctype") || text.includes("<html") || text.includes("<head") || text.includes("<body");
    }

    function extractScammingData(attemptData, resource) {

        //However Laz did that
        if(!currentCrimesByTypeData) return null;

        //Is it linked to a target?
        if(!(resource.includes("value1"))) return null;
        let subID = resource.split("value1=")[1].split("&")[0];

        console.log("[OutcomeDB] Extracting additional scamming data.");

        //Get target states
        let beforeTargetState = currentCrimesByTypeData.targets.find((target) => {return String(target.subID).includes(subID);});
        let afterTargetState = attemptData.targets.find((target) => {return String(target.subID).includes(subID);});

        let additionalData = {};

        //target information
        additionalData.gender = beforeTargetState.gender? beforeTargetState.gender : afterTargetState.gender;
        additionalData.target = beforeTargetState.target? beforeTargetState.target : afterTargetState.target;

        //action information
        if(!beforeTargetState.bar) additionalData.action = "read";
        else if(!afterTargetState) additionalData.action = "capitalize";
        else additionalData.action = afterTargetState.lastAction;

        const transformBar = (bar) => {
            if (!bar) return null;
            const stateMapping = {
                "neutral": "n",
                "fail": "f",
                "low": "l",
                "medium": "m",
                "high": "h",
                "sensitivity": "s",
                "temptation": "t",
                "hesitation": "w",
                "concern": "c"
            };
            return bar.map(state => stateMapping[state] || '?').join('');
        };

        if (beforeTargetState) {
            additionalData.targetBefore = {
                "multiplierUsed": beforeTargetState.multiplierUsed,
                "pip": beforeTargetState.pip,
                "turns": beforeTargetState.turns,
                "bar": transformBar(beforeTargetState.bar)
            };
        }

        if (afterTargetState) {
            additionalData.targetAfter = {
                "multiplierUsed": afterTargetState.multiplierUsed,
                "pip": afterTargetState.pip,
                "turns": afterTargetState.turns,
                "bar": transformBar(afterTargetState.bar)
            };
            additionalData.cooldown = afterTargetState.cooldown ? Math.floor(afterTargetState.cooldown - serverTime) : null;
        }

        //targetAfter is missing on capitalize
        if(!additionalData.targetAfter) additionalData.targetAfter = {};

        console.log("[OutcomeDB] Additional data gathered.");
        return additionalData;
    }

})();