// ==UserScript==
// @name Torn Rigs Layout Switcher
// @namespace https://github.com/SOLiNARY
// @version 0.1.1
// @description Adds "Save current rig layout" & "Empty rig layout" quick actions to a Cracking crime.
// @author Ramin Quluzade, Silmaril [2665762]
// @license MIT License
// @match https://www.torn.com/loader.php?sid=crimes*
// @match https://www.torn.com/loader.php?sid=crimes#/cracking
// @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant none
// @run-at document-start
// ==/UserScript==
(async function() {
'use strict';
const rfcvArg = "rfcv=";
const isTampermonkeyEnabled = typeof unsafeWindow !== 'undefined';
let rfcv = localStorage.getItem("silmaril-rigs-layout-switcher-rfcv") ?? null;
const crimesReadUrl = '/loader.php?sid=crimesData&step=crimesList&typeID=10&';
const crimesWriteUrl = '/loader.php?sid=crimesData&step=prepare&typeID=10&crimeID=204&value1=';
const emptyLayout = `[ {"x": 0, "y": 0, "item": null}, {"x": 1, "y": 0, "item": null}, {"x": 2, "y": 0, "item": null}, {"x": 3, "y": 0, "item": null}, {"x": 4, "y": 0, "item": null}, {"x": 0, "y": 1, "item": null}, {"x": 1, "y": 1, "item": null}, {"x": 2, "y": 1, "item": null}, {"x": 3, "y": 1, "item": null}, {"x": 4, "y": 1, "item": null}, {"x": 0, "y": 2, "item": null}, {"x": 1, "y": 2, "item": null}, {"x": 2, "y": 2, "item": null}, {"x": 3, "y": 2, "item": null}, {"x": 4, "y": 2, "item": null}, {"x": 0, "y": 3, "item": null}, {"x": 1, "y": 3, "item": null}, {"x": 2, "y": 3, "item": null}, {"x": 3, "y": 3, "item": null}, {"x": 4, "y": 3, "item": null}, {"x": 0, "y": 4, "item": null}, {"x": 1, "y": 4, "item": null}, {"x": 2, "y": 4, "item": null}, {"x": 3, "y": 4, "item": null}, {"x": 4, "y": 4, "item": null}]`;
let rfcvUpdatedThisSession = false;
let mutationFound = false;
let panelAdded = false;
let rigLayouts = localStorage.getItem("silmaril-rigs-layout-switcher-layouts") ?? "";
let rigLayoutsArray = rigLayouts.split(',');
// console.log('rigLayouts', rigLayouts, rigLayoutsArray);
let currentRig = 0;
let rigsInfo = {};
const { fetch: originalFetch } = isTampermonkeyEnabled ? unsafeWindow : window;
const customFetch = async (...args) => {
let [resource, config] = args;
let response = await originalFetch(resource, config);
let fetchUrl = response.url;
if (fetchUrl.indexOf(crimesReadUrl) >= 0 || fetchUrl.indexOf(crimesWriteUrl) >= 0) {
try {
const jsonData = await response.clone().json();
const rig = fetchUrl.indexOf(crimesReadUrl) >= 0 ? jsonData.DB.crimesByType.rig : jsonData.DB.additionalInfo.prepareInfo.rig;
rig.chassis.forEach((rigData, rigId) => {
// console.log('rigData', rigData);
// console.log('rigId', rigId);
let items = [];
rigData.components.forEach((componentData) => {
if (componentData.ID == 0) {
return;
}
let componentInfo = new ComponentInfo(componentData.coords[0].x, componentData.coords[0].y, componentData.ID);
if (componentData.coords[1] != null) {
componentInfo.x2 = componentData.coords[1].x;
componentInfo.y2 = componentData.coords[1].y;
}
items.push(componentInfo);
});
rigsInfo[rigId] = items;
});
// console.log('rigsInfo', rigsInfo);
} catch (error) {
console.log('[TornRigsLayoutSwitcher] No targets, skipping the script init', error);
}
}
if (rfcvUpdatedThisSession) {
return response;
}
if (!rfcvUpdatedThisSession){
let rfcvIdx = fetchUrl.indexOf(rfcvArg);
if (rfcvIdx >= 0){
rfcv = fetchUrl.substr(rfcvIdx + rfcvArg.length);
localStorage.setItem("silmaril-loadout-switcher-rfcv", rfcv);
document.querySelectorAll("div.silmaril-torn-rigs-layout-switcher-container button").forEach((button) => button.classList.remove("disabled"));
rfcvUpdatedThisSession = true;
}
}
return response;
};
if (isTampermonkeyEnabled){
unsafeWindow.fetch = customFetch;
} else {
window.fetch = customFetch;
}
const styles = `
div.silmaril-torn-rigs-layout-switcher-container {
display: inline-flex;
align-items: center;
margin-left: 5px;
}
.wave-animation {
position: relative;
overflow: hidden;
}
.wave {
pointer-events: none;
position: absolute;
width: 100%;
height: 33px;
background-color: transparent;
opacity: 0;
transform: translateX(-100%);
animation: waveAnimation 3s cubic-bezier(0, 0, 0, 1);
}
@keyframes waveAnimation {
0% {
opacity: 1;
transform: translateX(-100%);
}
100% {
opacity: 0;
transform: translateX(100%);
}
}
`;
if (isTampermonkeyEnabled){
GM_addStyle(styles);
} else {
let style = document.createElement("style");
style.type = "text/css";
style.innerHTML = styles;
while (document.head == null){
await sleep(50);
}
document.head.appendChild(style);
}
const setLayoutUrl = "/loader.php?sid=crimesData&step=prepare&typeID=10&crimeID=204&value1={layout}&rfcv={rfcv}";
const layoutTemplate = `{"step":"{step}","chassisID":{rig},"ID":{component},"coords":[{shortComponent}{longComponent}]}`;
const coordinatesTemplate = `{"x":{xCoordinate},"y":{yCoordinate}}`;
const add = 'add';
const remove = 'remove';
const componentIds = {
"eCPU": 1,
"CPU": 2,
"HPCPU": 3,
"Fan": 4,
"Water Block": 5,
"Heat Sink": 6,
"PSU": 7,
"None": 0
};
const observerTarget = document.querySelector("html");
const observerConfig = { attributes: false, childList: true, characterData: false, subtree: true };
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutationItem) {
if (mutationFound || panelAdded){
observer.disconnect();
return;
}
let mutation = mutationItem.target;
if (mutation.classList == "crime-root cracking-root") {
// console.log('MATCHED RIG', mutation.querySelector('div.rig___aY5rF'));
const rigDiv = mutation.querySelector('div.rig___aY5rF');
if (rigDiv == null)
{
// console.log('no rig found');
return;
}
// console.log('Rig Found!');
mutationFound = true;
observer.disconnect();
const buttonContainer = document.createElement('div');
buttonContainer.className = 'silmaril-rigs-layout-switcher-templates';
const waveDiv = document.createElement('div');
waveDiv.className = 'wave';
buttonContainer.appendChild(waveDiv);
addLayoutsAndSettingButtons(buttonContainer);
if (!panelAdded){
mutation.querySelector('div.currentCrime___KNKYQ').append(getRigChoices(), getLayoutActions(mutation), buttonContainer);
panelAdded = true;
}
}
});
});
observer.observe(observerTarget, observerConfig);
function addLayoutsAndSettingButtons(root){
const empty = document.createElement('button');
empty.type = 'button';
empty.className = 'torn-btn non-deletable';
empty.textContent = 'Empty';
empty.setAttribute('data-action', 'remove');
empty.addEventListener('click', () => {handleLoadoutClick(root)});
root.appendChild(empty);
addLayoutButtons(root);
}
function getRigChoices(){
const rigChoiceWrapper = document.createElement('ul');
rigChoiceWrapper.role = 'tablist';
rigChoiceWrapper.className = 'torn-tabs tabs-dark ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all';
const rigChoice1 = document.createElement('li');
rigChoice1.id = 'silmaril-rigs-layout-switcher-choice-1';
rigChoice1.role = 'tab';
rigChoice1.ariaSelected = 'true';
rigChoice1.setAttribute('data-disable', '0');
rigChoice1.className = 'ui-state-default ui-corner-top ui-tabs-active ui-state-active';
rigChoice1.addEventListener('click', event => {
// console.log('event', event);
currentRig = 0;
event.target.parentNode.classList.add('ui-tabs-active');
event.target.parentNode.classList.add('ui-state-active');
let rig2 = document.getElementById('silmaril-rigs-layout-switcher-choice-2');
rig2.classList.remove('ui-tabs-active');
rig2.classList.remove('ui-state-active');
let rig3 = document.getElementById('silmaril-rigs-layout-switcher-choice-3');
rig3.classList.remove('ui-tabs-active');
rig3.classList.remove('ui-state-active');
// console.log('currentRig', currentRig);
});
const rigChoiceLink1 = document.createElement('a');
rigChoiceLink1.innerText = 'Chassis #1';
rigChoice1.append(rigChoiceLink1);
const rigChoice2 = document.createElement('li');
rigChoice2.id = 'silmaril-rigs-layout-switcher-choice-2';
rigChoice2.role = 'tab';
rigChoice2.ariaSelected = 'false';
rigChoice2.setAttribute('data-disable', '0');
rigChoice2.className = 'ui-state-default ui-corner-top';
rigChoice2.addEventListener('click', event => {
// console.log('event', event);
currentRig = 1;
event.target.parentNode.classList.add('ui-tabs-active');
event.target.parentNode.classList.add('ui-state-active');
let rig1 = document.getElementById('silmaril-rigs-layout-switcher-choice-1');
rig1.classList.remove('ui-tabs-active');
rig1.classList.remove('ui-state-active');
let rig3 = document.getElementById('silmaril-rigs-layout-switcher-choice-3');
rig3.classList.remove('ui-tabs-active');
rig3.classList.remove('ui-state-active');
// console.log('currentRig', currentRig);
});
const rigChoiceLink2 = document.createElement('a');
rigChoiceLink2.innerText = 'Chassis #2';
rigChoice2.append(rigChoiceLink2);
const rigChoice3 = document.createElement('li');
rigChoice3.id = 'silmaril-rigs-layout-switcher-choice-3';
rigChoice3.role = 'tab';
rigChoice3.ariaSelected = 'false';
rigChoice3.setAttribute('data-disable', '0');
rigChoice3.className = 'ui-state-default ui-corner-top';
rigChoice3.addEventListener('click', event => {
// console.log('event', event);
currentRig = 2;
event.target.parentNode.classList.add('ui-tabs-active');
event.target.parentNode.classList.add('ui-state-active');
let rig1 = document.getElementById('silmaril-rigs-layout-switcher-choice-1');
rig1.classList.remove('ui-tabs-active');
rig1.classList.remove('ui-state-active');
let rig2 = document.getElementById('silmaril-rigs-layout-switcher-choice-2');
rig2.classList.remove('ui-tabs-active');
rig2.classList.remove('ui-state-active');
// console.log('currentRig', currentRig);
});
const rigChoiceLink3 = document.createElement('a');
rigChoiceLink3.innerText = 'Chassis #3';
rigChoice3.append(rigChoiceLink3);
rigChoiceWrapper.append(rigChoice1, rigChoice2, rigChoice3);
// console.log('rigChoiceWrapper', rigChoiceWrapper);
return rigChoiceWrapper;
}
function getLayoutActions(root){
const actionsContainer = document.createElement('div');
actionsContainer.className = 'silmaril-torn-rigs-layout-switcher-container';
const saveCurrentLayout = document.createElement('button');
saveCurrentLayout.type = 'button';
saveCurrentLayout.className = 'torn-btn';
saveCurrentLayout.textContent = 'Save current layout';
saveCurrentLayout.addEventListener('click', () => {
// console.log('save current layout');
let userInput = prompt("Please enter a unique name for a layout (DO NOT USE COMMAS ',')" ?? '').toLowerCase();
if (userInput !== null && userInput.length > 0) {
rigLayouts = localStorage.getItem("silmaril-rigs-layout-switcher-layouts") ?? "";
rigLayoutsArray = rigLayouts.split(',');
if (!rigLayoutsArray.includes(userInput)) {
rigLayoutsArray.push(userInput);
rigLayouts = rigLayoutsArray.join(',');
localStorage.setItem("silmaril-rigs-layout-switcher-layouts", rigLayouts);
}
localStorage.setItem(`silmaril-rigs-layout-switcher-layout-${userInput}`, JSON.stringify(rigsInfo[currentRig]));
root.querySelectorAll("div.silmaril-rigs-layout-switcher-templates > button:not(.non-deletable)").forEach((item) => item.remove());
addLayoutButtons(root.querySelector('div.currentCrime___KNKYQ > div.silmaril-rigs-layout-switcher-templates'));
} else {
console.error("[TornBazaarFiller] User cancelled the layout naming input.");
}
});
const deleteLayout = document.createElement('button');
deleteLayout.type = 'button';
deleteLayout.className = 'torn-btn';
deleteLayout.textContent = 'Delete layout';
deleteLayout.addEventListener('click', () => {
// console.log('delete layout');
let userInput = prompt("Please enter a name of a layout to delete" ?? '').toLowerCase();
if (userInput !== null && userInput.length > 0) {
rigLayouts = localStorage.getItem("silmaril-rigs-layout-switcher-layouts") ?? "";
rigLayoutsArray = rigLayouts.split(',');
rigLayoutsArray = rigLayoutsArray.filter(item => item !== userInput);
rigLayouts = rigLayoutsArray.join(',');
localStorage.setItem("silmaril-rigs-layout-switcher-layouts", rigLayouts);
localStorage.removeItem(`silmaril-rigs-layout-switcher-layout-${userInput}`);
root.querySelectorAll("div.silmaril-rigs-layout-switcher-templates > button:not(.non-deletable)").forEach((item) => item.remove());
addLayoutButtons(root.querySelector('div.currentCrime___KNKYQ > div.silmaril-rigs-layout-switcher-templates'));
} else {
console.error("[TornBazaarFiller] User cancelled the layout naming input.");
}
});
actionsContainer.append(saveCurrentLayout, deleteLayout);
return actionsContainer;
}
async function addLayoutButtons(root){
rigLayoutsArray.forEach((layout) => {
if (layout == ''){
return;
}
const button = document.createElement('button');
button.type = 'button';
button.className = rfcv === null ? 'torn-btn disabled' : 'torn-btn';
button.textContent = layout;
button.setAttribute('data-action', 'add');
button.addEventListener('click', () => {handleLoadoutClick(root, layout)});
root.appendChild(button);
})
}
async function handleLoadoutClick(root, layoutName = null){
let layout = layoutName == null ? emptyLayout : localStorage.getItem(`silmaril-rigs-layout-switcher-layout-${layoutName}`);
let action = event.target.getAttribute('data-action');
if (event.target.classList.contains('disabled')){
return;
}
const layoutItems = JSON.parse(layout);
// console.log('layout', layoutItems);
await sendSetLayoutRequests(action, currentRig, layoutItems, root);
}
async function sendSetLayoutRequests(action, rig, items, root){
// console.log('items', items);
const urlWithRfcv = setLayoutUrl.replace("{rfcv}", rfcv);
let wave = root.querySelector("div.wave");
items.forEach(async (item) => {
const coordinates = coordinatesTemplate.replace("{xCoordinate}", item.x).replace("{yCoordinate}", item.y);
let layoutUrl2 = layoutTemplate.replace("{step}", action).replace("{rig}", rig).replace("{component}", item.item).replace("{shortComponent}", coordinates);
let layoutUrl;
if (item.x2 != null) {
const coordinatesLong = coordinatesTemplate.replace("{xCoordinate}", item.x2).replace("{yCoordinate}", item.y2);
layoutUrl = layoutUrl2.replace("{longComponent}", `,${coordinatesLong}`);
} else {
layoutUrl = layoutUrl2.replace("{longComponent}", "");
}
const url = urlWithRfcv.replace("{layout}", layoutUrl);
await fetch(url, {
method: 'GET',
})
.then(response => {
if (response.ok) {
wave.style.backgroundColor = "green";
} else {
console.error("[TornRigsLayoutSwitcher] Set Loadout request failed:", response);
wave.style.backgroundColor = "red";
wave.style.animationDuration = "5s";
}
})
.catch(error => {
console.error("[TornRigsLayoutSwitcher] Error setting loadout:", error);
wave.style.backgroundColor = "red";
wave.style.animationDuration = "5s";
});
await sleep(40);
});
wave.style.animation = 'none';
wave.offsetHeight;
wave.style.animation = null;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
class ComponentInfo {
constructor(x, y, item, x2 = null, y2 = null) {
this.x = x;
this.y = y;
this.item = item;
this.x2 = x2;
this.y2 = y2;
}
}
})();