Inventory_Stack_Helper

Steam 物品堆叠工具

// ==UserScript==
// @name:zh-CN         Steam 库存物品堆叠工具
// @name               Inventory_Stack_Helper
// @namespace          https://blog.chrxw.com
// @supportURL         https://blog.chrxw.com/scripts.html
// @contributionURL    https://afdian.com/@chr233
// @version            2.5
// @description        Steam 物品堆叠工具
// @description:zh-CN  Steam 物品堆叠工具
// @author             Chr_
// @match              https://steamcommunity.com/profiles/*/inventory*
// @match              https://steamcommunity.com/id/*/inventory*
// @license            AGPL-3.0
// @icon               https://blog.chrxw.com/favicon.ico
// @grant              GM_addStyle
// ==/UserScript==

// 初始化
(() => {
    "use strict";

    let GappId = 0;
    let GcontextId = 2;

    const delay = 300;
    const amount = 5000;

    let token = document.querySelector("#application_config")?.getAttribute("data-loyalty_webapi_token");
    if (token) {
        token = token.replace(/"/g, "");
    }
    else {
        ShowAlertDialog("提示", "读取Token失败, 可能需要重新登录");
        return;
    }

    const GObjs = addPanel();
    loadSetting();
    doFitInventory();

    //==================================================================================================

    function genBtn(text, title, onclick) {
        let btn = document.createElement("button");
        btn.textContent = text;
        btn.title = title;
        btn.className = "ish_button";
        btn.addEventListener("click", onclick);
        return btn;
    }
    function genSpan(text) {
        let span = document.createElement("span");
        span.textContent = text;
        return span;
    }
    function genNumber(value, placeholder, title) {
        const t = document.createElement("input");
        t.className = "ish_inputbox";
        t.placeholder = placeholder;
        t.title = title;
        t.type = "number";
        t.value = value;
        return t;
    }

    function addPanel() {
        const btnArea = document.querySelector("div.inventory_links");

        const container = document.createElement("div");
        container.className = "ish_container";
        btnArea.insertBefore(container, btnArea.firstChild);

        const hiddenContainer = document.createElement("div");
        hiddenContainer.style.display = "none";
        container.appendChild(hiddenContainer);


        const btnStack = genBtn("堆叠", "堆叠库存中的物品", doStack);
        const btnUnstack = genBtn("反堆叠", "取消堆叠库存中的物品", doUnstack);

        const iptStackMax = genNumber("0", "堆叠上限", "物品堆叠上限, 留空或者0表示不设置堆叠上限");

        const btnHelp = genBtn("❓", "查看帮助", doHelp);
        const spStatus = genSpan("");

        hiddenContainer.appendChild(genSpan("库存"));

        container.appendChild(btnStack);
        container.appendChild(btnUnstack);
        container.appendChild(btnHelp);
        container.appendChild(iptStackMax);
        container.appendChild(spStatus);

        document.querySelectorAll('div.games_list_tabs>a').forEach(tab => {
            tab.addEventListener("click", doFitInventory);
        });

        document.querySelector("#responsive_inventory_select")?.addEventListener("change", doFitInventory);

        return { btnStack, btnUnstack, iptStackMax, btnHelp, spStatus };
    }

    function doHelp() {
        const { script: { version } } = GM_info;

        ShowAlertDialog("帮助",
            [
                "<p>【堆叠】: 将指定库存中的同类物品堆叠到一起</p>",
                "<p>【反堆叠】: 将指定库存中的已堆叠物品拆分成单个物品</p>",
                `<p>【<a href="https://keylol.com/t954659-1-1" target="_blank">发布帖</a>】 【<a href="https://blog.chrxw.com/scripts.html" target="_blank">脚本反馈</a>】</p>`,
                `<p>【Developed by <a href="https://steamcommunity.com/id/Chr_" target="_blank">Chr_</a>】 【当前版本 ${version}】</p>`,
            ].join("<br>")

        );
    }

    function doFitInventory() {
        const { appid, contextid } = g_ActiveInventory;

        GappId = appid ?? "0";
        GcontextId = contextid ?? "2";

        if (GappId == 753) {
            GcontextId = "6";
        }
    }

    function doStack() {
        const { btnStack, btnUnstack, iptStackMax, btnHelp, spStatus } = GObjs;

        if (GappId !== GappId || GcontextId !== GcontextId) {
            ShowAlertDialog("提示", "库存状态无效");
            return;
        }

        let stackMax = 0;
        if (iptStackMax.value) {
            stackMax = parseInt(iptStackMax.value);
            if (stackMax !== stackMax) {
                ShowAlertDialog("提示", "请检查 堆叠上限 是否填写正确");
                return;
            }
        }

        saveSetting();

        spStatus.textContent = "堆叠中 [正在加载库存]";
        btnStack.style.display = "none";
        btnUnstack.style.display = "none";
        btnHelp.style.display = "none";
        iptStackMax.style.display = "none";

        loadInventory(GappId, GcontextId, amount)
            .then(async (inv) => {
                if (!inv) {
                    ShowAlertDialog("提示", "库存读取失败, 请检查 AppId 和 ContextId 是否填写正确");
                    return;
                }

                const { assets } = inv;
                if (assets) {
                    const itemGroup = {};

                    for (let item of assets) {
                        const { classid } = item;

                        // 只处理宝珠和宝珠袋
                        if (GappId === 753) {
                            continue;
                        }

                        if (!itemGroup[classid]) {
                            itemGroup[classid] = [];
                        }
                        item.amount = parseInt(item.amount);
                        itemGroup[classid].push(item);
                    }

                    let totalReq = 0;
                    const todoList = [];
                    for (let classId in itemGroup) {
                        const items = itemGroup[classId];
                        if (stackMax === 0) {
                            if (items.length > 1) {
                                todoList.push(items);
                                totalReq += items.length - 1;
                            }
                        } else {
                            const stacks = [];
                            while (items.length > 0) {
                                const item = items.pop();
                                if (item.amount > stackMax) {
                                    continue;
                                }

                                let added = false;
                                for (let stack of stacks) {
                                    if (stack.amount + item.amount <= stackMax) {
                                        stack.list.push(item);
                                        stack.amount += item.amount;
                                        added = true;
                                        break;
                                    }
                                }

                                if (!added) {
                                    stacks.push({ list: [item,], amount: item.amount });
                                }
                            }

                            for (let stack of stacks) {
                                if (stack.list.length >= 1) {
                                    todoList.push(stack.list);
                                    totalReq += stack.list.length - 1;
                                }
                            }
                        }
                    }

                    if (totalReq > 0) {
                        const totalType = todoList.length;
                        spStatus.textContent = `堆叠中 [种类 0/${totalType} 请求 0/${totalReq} 0.00%]`;

                        let type = 1;
                        let req = 1;
                        for (let items of todoList) {
                            for (let i = 1; i < items.length; i++) {
                                await stackItem(GappId, items[i].assetid, items[0].assetid, items[i].amount);
                                await asyncDelay(delay);
                                const percent = (100 * req / totalReq).toFixed(2);
                                spStatus.textContent = `堆叠中 [种类 ${type}/${totalType} 请求 ${req++}/${totalReq} ${percent}%]`;
                            }
                            type++;
                        }
                    }

                    ShowAlertDialog("提示", totalReq > 0 ? "堆叠操作完成" : "无可堆叠物品");
                } else {
                    ShowAlertDialog("提示", "读取库存失败, 请稍后重试");
                }
            })
            .catch((err) => {
                ShowAlertDialog("提示", "库存读取出错, 错误信息\r\n" + err);
                console.error(err);
            })
            .finally(() => {
                spStatus.textContent = "";
                btnStack.style.display = null;
                btnUnstack.style.display = null;
                btnHelp.style.display = null;
                iptStackMax.style.display = null;
                g_ActiveInventory.m_owner.ReloadInventory(appId, contextId);
            });
    }

    function doUnstack() {
        const { btnStack, btnUnstack, iptStackMax, btnHelp, spStatus } = GObjs;

        if (GappId !== GappId || GcontextId !== GcontextId) {
            ShowAlertDialog("提示", "请检查 AppId 和 ContextId 是否填写正确");
            return;
        }

        saveSetting();

        spStatus.textContent = "反堆叠中 [正在加载库存]";
        btnStack.style.display = "none";
        btnUnstack.style.display = "none";
        btnHelp.style.display = "none";
        iptStackMax.style.display = "none";

        loadInventory(GappId, GcontextId, amount)
            .then(async (inv) => {
                if (!inv) {
                    ShowAlertDialog("提示", "库存读取失败, 请检查 AppId 和 ContextId 是否填写正确");
                    return;
                }

                const { assets } = inv;
                if (assets) {
                    const itemGroup = [];
                    let totalReq = 0;
                    for (let item of assets) {
                        const { amount } = item;

                        if (GappId === 753) {
                            continue;
                        }

                        if (amount > 1) {
                            item.amount = amount;
                            itemGroup.push(item);
                            totalReq += amount - 1;
                        }
                    }

                    if (totalReq > 0) {
                        const totalType = itemGroup.length;

                        spStatus.textContent = `反堆叠中 [种类 0/${totalType} 请求 0/${totalReq} 0.00%]`;

                        let type = 1;
                        let req = 1;

                        for (let item of itemGroup) {
                            for (let i = 1; i < item.amount; i++) {
                                await unStackItem(GappId, item.assetid, 1);
                                await asyncDelay(delay);
                                const percent = (100 * req / totalReq).toFixed(2);
                                spStatus.textContent = `反堆叠中 [种类 ${type}/${totalType} 请求 ${req++}/${totalReq} ${percent}%]`;
                            }
                            type++;
                        }
                    }

                    ShowAlertDialog("提示", totalReq > 0 ? "反堆叠操作完成" : "无可反堆叠物品");
                } else {
                    ShowAlertDialog("提示", "库存读取失败, 请检查 AppId 和 ContextId 是否填写正确");
                }
            })
            .catch((err) => {
                ShowAlertDialog("提示", "库存读取出错, 错误信息\r\n" + err);
                console.error(err);
            })
            .finally(() => {
                spStatus.textContent = "";
                btnStack.style.display = null;
                btnUnstack.style.display = null;
                btnHelp.style.display = null;
                iptStackMax.style.display = null;
                g_ActiveInventory.m_owner.ReloadInventory(appId, contextId);
            });
    }

    function loadSetting() {
        const { iptStackMax } = GObjs;
        iptStackMax.value = localStorage.getItem("ish_limit") ?? "0";
    }

    function saveSetting() {
        const { iptStackMax } = GObjs;
        localStorage.setItem("ish_limit", iptStackMax.value);
    }

    //==================================================================================================

    // 延时
    function asyncDelay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    // 读取库存
    function loadInventory(appId, contextId, count) {
        return new Promise((resolve, reject) => {
            fetch(`https://steamcommunity.com/inventory/${g_steamID}/${appId}/${contextId}?l=${g_strLanguage}&count=${count}`)
                .then(async (response) => {
                    response.json().then(json => {
                        if (json) {
                            resolve(json);
                        } else {
                            fetch(`https://steamcommunity.com/inventory/${g_steamID}/${appId}/${contextId}?l=${g_strLanguage}&count=${count}`)
                                .then(async (response) => {
                                    response.json().then(json => {
                                        if (json) {
                                            resolve(json);
                                        } else {
                                            fetch(`https://steamcommunity.com/inventory/${g_steamID}/${appId}/${contextId}?l=${g_strLanguage}&count=${count}`)
                                                .then(async (response) => {
                                                    response.json().then(json => {
                                                        if (json) {
                                                            resolve(json);
                                                        } else {

                                                        }
                                                    });
                                                })
                                                .catch((err) => {
                                                    console.error(err);
                                                    reject(`读取库存失败 ${err}`);
                                                });
                                        }
                                    });
                                })
                                .catch((err) => {
                                    console.error(err);
                                    reject(`读取库存失败 ${err}`);
                                });
                        }
                    });
                })
                .catch((err) => {
                    console.error(err);
                    reject(`读取库存失败 ${err}`);
                });
        });
    }

    // 堆叠物品
    function stackItem(appId, fromAssetId, destAssetId, quantity) {
        return new Promise((resolve, reject) => {
            fetch(
                `https://api.steampowered.com/IInventoryService/CombineItemStacks/v1/`,
                {
                    method: "POST",
                    body: `access_token=${token}&appid=${appId}&fromitemid=${fromAssetId}&destitemid=${destAssetId}&quantity=${quantity}&steamid=${g_steamID}`,
                    headers: {
                        "content-type":
                            "application/x-www-form-urlencoded; charset=UTF-8",
                    },
                }
            )
                .then((response) => {
                    response.json().then(json => {
                        const { success } = json;
                        resolve(success);
                    });
                })
                .catch((err) => {
                    console.error(err);
                    reject(`堆叠物品失败 ${err}`);
                });
        });
    }

    // 取消堆叠物品
    function unStackItem(appId, itemAssetId, quantity) {
        return new Promise((resolve, reject) => {
            fetch(
                `https://api.steampowered.com/IInventoryService/SplitItemStack/v1/`,
                {
                    method: "POST",
                    body: `access_token=${token}&appid=${appId}&itemid=${itemAssetId}&quantity=${quantity}&steamid=${g_steamID}`,
                    headers: {
                        "content-type":
                            "application/x-www-form-urlencoded; charset=UTF-8",
                    },
                }
            )
                .then((response) => {
                    response.json().then(json => {
                        const { success } = json;
                        resolve(success);
                    });
                })
                .catch((err) => {
                    console.error(err);
                    reject(`取消堆叠物品失败 ${err}`);
                });
        });
    }
})();

GM_addStyle(`
div.ish_container {
  display: inline;
}

div.ish_container > * {
  margin-right: 5px;
}

input.ish_inputbox {
  width: 70px;
  padding: 5px;
}

input.ish_inputbox:nth-of-type(3),
input.ish_inputbox:nth-of-type(4){
  width: 50px;
}
`);