// ==UserScript==
// @name OutcomeDB
// @namespace de.sabbasofa.outcomedb
// @version 2.0.8
// @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));
};
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 turnstyle 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");
bundle.additionalData = getAdditionalData(data.DB.crimesByType, bundle.typeID, resource);
if(!bundle.skillBefore || !bundle.skillAfter) {
console.error("[OutcomeDB] Could not find skill data, skipping...");
return;
}
console.log("[OutcomeDB] Ready to send bundle.", JSON.stringify(bundle));
sendBundleToAPI(bundle);
currentCrimesByTypeData = data.DB.crimesByType;
} catch (e) {
localStorage.setItem("outcomedb_last_error", JSON.stringify({error: {message: e.message, stack: e.stack}, body: body}));
console.error("[OutcomeDB] Error parsing data:", body, e);
if(e instanceof SyntaxError) return;
alert("OutcomeDB error attempt_parse. Please check console and report this to Lazerpent [2112641] or Hemicopter [2780600].");
}
}
function handleCrimesList(body, resource) {
console.log("[OutcomeDB] Updating crimes data.");
//Most likely cloudflare turnstyle 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) {
localStorage.setItem("outcomedb_last_error", JSON.stringify({error: {message: e.message, stack: e.stack}, body: body}));
console.error("[OutcomeDB] Error parsing data:", body, e);
if(e instanceof SyntaxError) return;
alert("OutcomeDB error list_parse. Please check console and report this to Lazerpent [2112641] or Hemicopter [2780600].");
}
}
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) {
alert("OutcomeDB error post_invalid. Please check console and report this to Lazerpent [2112641] or Hemicopter [2780600]: " + response.responseText);
}
},
onerror: function (e) {
localStorage.setItem("outcomedb_last_error", JSON.stringify({error: {message: e.message, stack: e.stack}}));
console.error("[OutcomeDB] Error sending bundle to API:", e);
alert("OutcomeDB error post_error. Please check console and report this to Lazerpent [2112641] or Hemicopter [2780600]: " + e);
}
});
}
function getStat(name) {
let allStatisticButtons = Array.from(win.document.querySelectorAll('button[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;
}
}
console.error(`[OutcomeDB] Could not find 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 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 target.subID.includes(subID);});
let afterTargetState = attemptData.targets.find((target) => {return 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;
}
function containsHtml(text) {
return text.includes("!DOCTYPE") || text.includes("!doctype") || text.includes("<html") || text.includes("<head") || text.includes("<body");
}
})();