- // ==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;
- }
- `);