// ==UserScript== // @name Steam 家庭库统计 // @name:en Steam Family Library Stats // @namespace https://github.com/BYWled/steam-family-library-stats // @version 1.0.0 // @description 适配Steam全新React虚拟列表与CSS-Module混淆架构,精准提取家庭共享游戏归属与严谨计分 // @description:en Precisely extract game ownership within a Steam Family and score every member fairly. Fully adapted to Steam's brand-new React virtual list and CSS-Module obfuscated architecture. // @author BYWled // @match *://store.steampowered.com/account/familymanagement* // @grant none // @license GPL-3.0 // @icon https://store.steampowered.com/favicon.ico // @homepageURL https://github.com/BYWled/steam-family-library-stats // @supportURL https://github.com/BYWled/steam-family-library-stats/issues // @source https://github.com/BYWled/steam-family-library-stats/raw/refs/heads/main/steam-family-library-stats.user.js // ==/UserScript== // ------------------------------------------------------------------ // Copyright (C) 2026 BYWled // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, version 3 of the License. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // ------------------------------------------------------------------ (function () { 'use strict'; // ========================================== // 1. 全局数据中心 // ========================================== const state = { users: new Set(["我"]), // Map: appId/key => { name, appId, category, owners: Set, copies: number } games: new Map(), scannedKeys: new Set(), // 依据 appId/唯一名称去重,避免虚拟列表复用DOM导致的漏扫 isScanning: false, statusMessage: "点击下方按钮开始全自动扫描", stats: { totalIncluded: 0, totalExcluded: 0, totalFree: 0 } }; // ========================================== // 2. 严谨计分系统 // ========================================== // 权重公式: 1人独有=3分,6人共有=1分 -> 线性权重: weight = 3.4 - 0.4 * ownerCount function calculateWeight(ownerCount) { if (ownerCount <= 0) return 0; const w = 3.4 - 0.4 * ownerCount; return w < 1 ? 1 : Math.round(w * 100) / 100; } function recalculateStats() { state.stats.totalIncluded = 0; state.stats.totalExcluded = 0; state.stats.totalFree = 0; const userStats = new Map(); for (const u of state.users) { userStats.set(u, { count: 0, exclusiveCount: 0, score: 0 }); } for (const [, gameData] of state.games) { if (gameData.category === '已排除的游戏') { state.stats.totalExcluded++; } else if (gameData.category === '免费游戏') { state.stats.totalFree++; } else if (gameData.category === '已包含的游戏') { state.stats.totalIncluded++; const count = gameData.owners.size; const weight = calculateWeight(count); for (const u of gameData.owners) { if (!userStats.has(u)) { userStats.set(u, { count: 0, exclusiveCount: 0, score: 0 }); state.users.add(u); } const s = userStats.get(u); s.count++; if (count === 1) s.exclusiveCount++; s.score += weight; } } } return userStats; } // ========================================== // 3. 文本与所有权解析引擎 // ========================================== function cleanGameName(raw) { if (!raw) return ''; return raw .replace(/[\u200B-\u200D\uFEFF]/g, '') // 清理不可见零宽字符 .replace(/^[《\s]+|[》\s]+$/g, '') // 清理书名号与首尾空白 .trim(); } function cleanMemberName(str) { if (!str) return ''; let cleaned = str .replace(/[\u200B-\u200D\uFEFF]/g, '') .replace(/[*:\uff1a]/g, '') // 移除星号及冒号 .replace(/^来自/, '') .replace(/的库$/, '') .trim(); if (!cleaned || cleaned.length > 30 || cleaned.includes('篇用户评测') || cleaned.includes('评测结果')) { return ''; } return cleaned; } // 基于 5 种实际 DOM 结构的归属解析引擎 function parseOwnersFromTooltip(tooltip) { const owners = new Set(); if (!tooltip) { owners.add('我'); return owners; } const ownerBox = tooltip.querySelector('[class*="HSL873"]') || Array.from(tooltip.querySelectorAll('div')).find(d => (d.innerText || '').includes('来自')); // 情况 3:我独有(HSL873 存在但内容为空,或根本没有归属信息) if (!ownerBox || !ownerBox.innerText || ownerBox.innerText.trim() === '') { owners.add('我'); state.users.add('我'); return owners; } const boxText = ownerBox.innerText.replace(/[\u200B-\u200D\uFEFF]/g, '').trim(); // 情况 4 & 5:包含“以下成员”列表(严格仅提取