Torn Rigs Layout Switcher

Adds "Save current rig layout" & "Empty rig layout" quick actions to a Cracking crime.

目前为 2023-12-29 提交的版本。查看 最新版本

// ==UserScript==
// @name         Torn Rigs Layout Switcher
// @namespace    https://github.com/SOLiNARY
// @version      0.3
// @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 = {};
    let requestsQueue = [];

    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);

                const spamButton = document.createElement('button');
                spamButton.id = 'silmaril-rigs-layout-switcher-spam-button';
                spamButton.type = 'button';
                spamButton.className = 'torn-btn non-deletable disabled';
                spamButton.textContent = 'No layout chosen';
                spamButton.addEventListener('click', async event => {
                    if (event.target.classList.contains('disabled')){
                        return;
                    }
                    const urlToProcess = requestsQueue.shift();
                    if (requestsQueue.length == 0) {
                        event.target.classList.add('disabled');
                        event.target.textContent = 'All requests completed!';
                    } else {
                        event.target.textContent = `Spam click ${requestsQueue.length} more times!`;
                    }
                    await sendSetLayoutRequest(urlToProcess);
                });

                if (!panelAdded){
                    mutation.querySelector('div.currentCrime___KNKYQ').append(getRigChoices(), getLayoutActions(mutation), buttonContainer, spamButton);
                    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("[TornRigsLayoutSwitcher] 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("[TornRigsLayoutSwitcher] User cancelled the layout naming input.");
            }
        });

        const exportAllLayouts = document.createElement('button');
        exportAllLayouts.type = 'button';
        exportAllLayouts.className = 'torn-btn';
        exportAllLayouts.textContent = 'Export all layouts';
        exportAllLayouts.addEventListener('click', () => {
            // console.log('export all layouts');
            rigLayouts = localStorage.getItem("silmaril-rigs-layout-switcher-layouts") ?? "";
            rigLayoutsArray = rigLayouts.split(',');
            let allLayoutsToExport = {};

            rigLayoutsArray.forEach((layoutName) => {
                if (layoutName == ''){
                    return;
                }
                try {
                    const layoutInfo = localStorage.getItem(`silmaril-rigs-layout-switcher-layout-${layoutName}`);
                    // console.log(`layout ${layoutName}`, layoutInfo);
                    allLayoutsToExport[layoutName] = JSON.parse(layoutInfo);
                    // console.log('allLayoutsToExport step', allLayoutsToExport);
                } catch (e) {
                    return;
                }
            });

            // console.log('allLayoutsToExport final', allLayoutsToExport);
            const allLayoutsToExportRaw = JSON.stringify(allLayoutsToExport);
            // console.log('allLayoutsToExport stringify', allLayoutsToExportRaw);

            prompt('Copy the text below & share it with friends!', allLayoutsToExportRaw);
        });

        const exportLayout = document.createElement('button');
        exportLayout.type = 'button';
        exportLayout.className = 'torn-btn';
        exportLayout.textContent = 'Export layout';
        exportLayout.addEventListener('click', () => {
            // console.log('export layout');
            let userInput = prompt("Please enter a name of a layout to export" ?? '').toLowerCase();
            if (userInput !== null && userInput.length > 0) {
                let layoutToExport = {};
                try {
                    const layoutInfo = localStorage.getItem(`silmaril-rigs-layout-switcher-layout-${userInput}`);
                    if (layoutInfo == null) {
                        console.log('[TornRigsLayoutSwitcher] No layout with this name!', userInput);
                        return;
                    }
                    // console.log(`layout ${userInput}`, layoutInfo);
                    layoutToExport[userInput] = JSON.parse(layoutInfo);
                } catch (e) {
                    return;
                }

                // console.log('layoutToExport final', layoutToExport);
                const layoutToExportRaw = JSON.stringify(layoutToExport);
                // console.log('layoutToExport stringify', layoutToExportRaw);

                prompt('Copy the text below & share it with friends!', layoutToExportRaw);
            } else {
                console.error("[TornRigsLayoutSwitcher] User cancelled the layout naming input.");
            }
        });

        const importLayouts = document.createElement('button');
        importLayouts.type = 'button';
        importLayouts.className = 'torn-btn';
        importLayouts.textContent = 'Import layouts';
        importLayouts.addEventListener('click', () => {
            // console.log('save current layout');
            let userInput = prompt("Please copy-paste the string which has been shared with you" ?? '').toLowerCase();
            if (userInput !== null && userInput.length > 0) {
                try {
                    const allLayoutsToImport = JSON.parse(userInput);
                    // console.log('allLayoutsToImport', allLayoutsToImport);
                    const allLayoutNamesToImport = Object.keys(allLayoutsToImport);
                    rigLayouts = localStorage.getItem("silmaril-rigs-layout-switcher-layouts") ?? "";
                    rigLayoutsArray = rigLayouts.split(',');

                    allLayoutNamesToImport.forEach((layoutName) => {
                        if (layoutName == ''){
                            return;
                        }
                        // console.log('layoutName', layoutName);
                        // console.log('layout', allLayoutsToImport[layoutName]);
                        try {
                            if (!rigLayoutsArray.includes(layoutName)) {
                                rigLayoutsArray.push(layoutName);
                            }
                            localStorage.setItem(`silmaril-rigs-layout-switcher-layout-${layoutName}`, JSON.stringify(allLayoutsToImport[layoutName]));
                        } catch (e) {
                            return;
                        }
                    });

                    rigLayouts = rigLayoutsArray.join(',');
                    localStorage.setItem("silmaril-rigs-layout-switcher-layouts", rigLayouts);
                }
                catch (e) {
                    return;
                }

                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("[TornRigsLayoutSwitcher] User cancelled the layout naming input.");
            }
        });

        actionsContainer.append(saveCurrentLayout, deleteLayout, exportAllLayouts, exportLayout, importLayouts);
        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 saveSetLayoutRequestsToQueue(action, currentRig, layoutItems, root);
    }

    function saveSetLayoutRequestsToQueue(action, rig, items, root){
        // console.log('items', items);
        requestsQueue = [];
        const urlWithRfcv = setLayoutUrl.replace("{rfcv}", rfcv);
        items.forEach((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);
            requestsQueue.push(url);
        });
        let spamButton = document.getElementById('silmaril-rigs-layout-switcher-spam-button');
        spamButton.textContent = `Spam click ${requestsQueue.length} more times!`;
        spamButton.classList.remove('disabled');
    }

    async function sendSetLayoutRequest(url){
        await fetch(url, {
            method: 'GET',
        })
            .then(response => {
            if (response.ok) {
                // console.log('component change request OK');
            } else {
                console.error("[TornRigsLayoutSwitcher] Set Layout request failed:", response);
            }
        })
            .catch(error => {
            console.error("[TornRigsLayoutSwitcher] Error setting Layout:", error);
        });
    }

    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;
        }
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址