perfectionist-plugins 2.4.7 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -52
- package/auto-accept.js +296 -294
- package/auto-claimer.js +1 -37
- package/auto-read.js +107 -42
- package/ban-blocker.js +483 -677
- package/better-client.js +362 -793
- package/dodge-helper.js +375 -673
- package/hide-unowned-skins.js +63 -686
- package/package.json +40 -40
- package/perfectionist-utils.js +540 -301
- package/unlock-profile-skins.js +174 -620
package/unlock-profile-skins.js
CHANGED
|
@@ -1,121 +1,87 @@
|
|
|
1
|
-
export const version = "
|
|
1
|
+
export const version = "2.0.0";
|
|
2
2
|
|
|
3
3
|
const PLUGIN_ID = "unlock-profile-skins";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
4
|
+
|
|
5
|
+
// The "Set profile background" window is the rcp-fe-lol-skins-picker client plugin, and nothing else in the
|
|
6
|
+
// client uses it. Its data binding is private to it, so unlocking the data there changes this window only:
|
|
7
|
+
// Collection, champ select, the store and loot keep seeing the real inventory.
|
|
8
|
+
const PICKER_PLUGIN = "rcp-fe-lol-skins-picker";
|
|
9
|
+
const CHAMPIONS_PATH = /^\/lol-champions\/v1\/inventories\/\d+\/champions$/;
|
|
10
|
+
const MINIMAL_PATH = /^\/lol-champions\/v1\/inventories\/\d+\/champions-minimal$/;
|
|
11
|
+
const AUGMENTS_PATH = /^\/lol-inventory\/v2\/inventory\/SKIN_AUGMENT$/i;
|
|
12
|
+
const GROUPING_SELECTOR = ".flyout-component lol-uikit-framed-dropdown.control-panel-grouping-options";
|
|
13
|
+
const TRANSLATIONS_URI = "/fe/lol-skins-picker/trans.json";
|
|
14
|
+
const TIER_KEY = "control_panel_grouping_by_tier";
|
|
15
|
+
|
|
11
16
|
const FAKE_PURCHASE_DATE = 1577836800000;
|
|
12
17
|
const FAKE_RENTAL_END = 4102444800000;
|
|
13
|
-
const
|
|
14
|
-
const GROUPING_OPTIONS = "lol-uikit-dropdown-option";
|
|
15
|
-
const HARVEST_WAIT_MS = 2000;
|
|
16
|
-
const INVENTORY_TEXT_CACHE_LIMIT = 2;
|
|
18
|
+
const SHADOW_CHAMPION_ID = 60000;
|
|
17
19
|
|
|
18
|
-
let filterActive = true;
|
|
19
20
|
let api = null;
|
|
20
|
-
let
|
|
21
|
-
let
|
|
22
|
-
let
|
|
23
|
-
let
|
|
21
|
+
let active = false;
|
|
22
|
+
let handles = [];
|
|
23
|
+
let unwatchGrouping = null;
|
|
24
|
+
let pickerApi = null;
|
|
25
|
+
let pickerSelectSkin = null;
|
|
26
|
+
let tierLabel = null;
|
|
24
27
|
const augmentIds = new Set();
|
|
25
|
-
const pendingTargetLoads = new Set();
|
|
26
28
|
const variantIdRemap = new Map();
|
|
27
|
-
const inventoryTextCache = new Map();
|
|
28
|
-
|
|
29
|
-
function readDefaultEnabled() {
|
|
30
|
-
try {
|
|
31
|
-
const saved = JSON.parse(localStorage.getItem(STATE_KEY) || "{}");
|
|
32
|
-
return saved.enabled?.[PLUGIN_ID] !== false;
|
|
33
|
-
} catch {
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
29
|
|
|
38
30
|
function logError(scope, error) {
|
|
39
|
-
console.error(`[
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function isPickerOpen() {
|
|
43
|
-
return document.querySelector(PICKER_BUTTON_SELECTOR) !== null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function cleanPath(url) {
|
|
47
|
-
let path = String(url ?? "").split("?")[0];
|
|
48
|
-
const origin = /^[a-z][a-z0-9+.-]*:\/\/[^/]+/i.exec(path);
|
|
49
|
-
if (origin) path = path.slice(origin[0].length) || "/";
|
|
50
|
-
return path;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function pathIdOf(url) {
|
|
54
|
-
const match = INVENTORY_PATH.exec(cleanPath(url));
|
|
55
|
-
return match ? Number(match[1]) : null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function isTargetUrl(url) {
|
|
59
|
-
const id = pathIdOf(url);
|
|
60
|
-
return id !== null && summonerId !== null && id === summonerId;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function isAugmentUrl(url) {
|
|
64
|
-
return AUGMENT_INVENTORY_PATH.test(cleanPath(url));
|
|
31
|
+
console.error(`[Perfectionism] Unlock Profile Skins: ${scope}`, error);
|
|
65
32
|
}
|
|
66
33
|
|
|
67
|
-
function
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
ownership.owned = true;
|
|
73
|
-
changed = true;
|
|
74
|
-
}
|
|
75
|
-
if (needsFakeDate) {
|
|
76
|
-
const rental = ownership.rental;
|
|
77
|
-
if (rental && typeof rental === "object") {
|
|
78
|
-
if (rental.purchaseDate !== FAKE_PURCHASE_DATE) {
|
|
79
|
-
rental.purchaseDate = FAKE_PURCHASE_DATE;
|
|
80
|
-
changed = true;
|
|
81
|
-
}
|
|
82
|
-
if (rental.endDate !== undefined && rental.endDate !== FAKE_RENTAL_END) {
|
|
83
|
-
rental.endDate = FAKE_RENTAL_END;
|
|
84
|
-
changed = true;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (ownership.purchaseDate !== undefined && ownership.purchaseDate !== FAKE_PURCHASE_DATE) {
|
|
88
|
-
ownership.purchaseDate = FAKE_PURCHASE_DATE;
|
|
89
|
-
changed = true;
|
|
90
|
-
}
|
|
34
|
+
function cloneData(value) {
|
|
35
|
+
try {
|
|
36
|
+
return structuredClone(value);
|
|
37
|
+
} catch {
|
|
38
|
+
return JSON.parse(JSON.stringify(value));
|
|
91
39
|
}
|
|
92
|
-
return changed;
|
|
93
40
|
}
|
|
94
41
|
|
|
95
|
-
function
|
|
42
|
+
function ownedRecord() {
|
|
96
43
|
return {
|
|
97
44
|
owned: true,
|
|
98
45
|
loyaltyReward: false,
|
|
99
46
|
xboxGPReward: false,
|
|
100
|
-
rental: { rented: false, purchaseDate:
|
|
47
|
+
rental: { rented: false, purchaseDate: FAKE_PURCHASE_DATE, endDate: 0, winCountRemaining: 0 },
|
|
101
48
|
};
|
|
102
49
|
}
|
|
103
50
|
|
|
104
|
-
function
|
|
105
|
-
if (!
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
51
|
+
function unlockOwnership(ownership) {
|
|
52
|
+
if (!ownership || typeof ownership !== "object") return;
|
|
53
|
+
const wasOwned = ownership.owned === true && ownership.rental?.rented !== true;
|
|
54
|
+
ownership.owned = true;
|
|
55
|
+
if (wasOwned) return;
|
|
56
|
+
const rental = ownership.rental;
|
|
57
|
+
if (rental && typeof rental === "object") {
|
|
58
|
+
rental.rented = false;
|
|
59
|
+
rental.purchaseDate = FAKE_PURCHASE_DATE;
|
|
60
|
+
if (rental.endDate !== undefined) rental.endDate = FAKE_RENTAL_END;
|
|
111
61
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
62
|
+
if (ownership.purchaseDate !== undefined) ownership.purchaseDate = FAKE_PURCHASE_DATE;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function unlockItem(item) {
|
|
66
|
+
if (!item || typeof item !== "object") return;
|
|
67
|
+
if (item.ownership && typeof item.ownership === "object") unlockOwnership(item.ownership);
|
|
68
|
+
else if (typeof item.id === "number" || typeof item.contentId === "string") item.ownership = ownedRecord();
|
|
69
|
+
if (item.owned === false) item.owned = true;
|
|
70
|
+
if (item.unlocked === false) item.unlocked = true;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function unlockAugments(holder) {
|
|
74
|
+
const list = holder?.skinAugments?.augments;
|
|
75
|
+
if (!Array.isArray(list)) return;
|
|
76
|
+
for (const aug of list) {
|
|
77
|
+
if (!aug || typeof aug !== "object") continue;
|
|
78
|
+
if (typeof aug.contentId === "string") augmentIds.add(aug.contentId);
|
|
79
|
+
if (aug.ownership && typeof aug.ownership === "object") unlockOwnership(aug.ownership);
|
|
80
|
+
else aug.ownership = ownedRecord();
|
|
115
81
|
}
|
|
116
82
|
}
|
|
117
83
|
|
|
118
|
-
function
|
|
84
|
+
function variantLabel(overlays) {
|
|
119
85
|
for (const overlay of overlays || []) {
|
|
120
86
|
const match = /splash_tile_([a-z0-9]+)\.png$/i.exec(String(overlay?.tileLCOverlayPath || ""));
|
|
121
87
|
if (match) return match[1].charAt(0).toUpperCase() + match[1].slice(1);
|
|
@@ -123,598 +89,186 @@ function variantLabelFromOverlays(overlays) {
|
|
|
123
89
|
return "Variant";
|
|
124
90
|
}
|
|
125
91
|
|
|
92
|
+
// Signature/event skins keep their colour variants as augments with tile overlays on one tier. Split each
|
|
93
|
+
// overlay variant into its own tile so every variant can be picked; the fake ids are mapped back to the real
|
|
94
|
+
// tier id when the profile saves the choice (see wrapSelectSkin).
|
|
126
95
|
function splitVariantTiers(skin) {
|
|
127
|
-
const tiers = skin
|
|
96
|
+
const tiers = skin?.questSkinInfo?.tiers;
|
|
97
|
+
if (!Array.isArray(tiers)) return;
|
|
128
98
|
const parent = typeof skin.id === "number" ? skin.id : null;
|
|
129
99
|
const taken = new Set(tiers.map((tier) => tier?.id));
|
|
130
100
|
const out = [];
|
|
131
|
-
let changed = false;
|
|
132
101
|
let sibling = 0;
|
|
133
102
|
for (const tier of tiers) {
|
|
134
103
|
out.push(tier);
|
|
135
104
|
const augments = tier?.skinAugments?.augments;
|
|
136
105
|
if (!Array.isArray(augments) || augments.length < 2) continue;
|
|
137
|
-
const withOverlays = augments.filter((aug) => Array.isArray(aug?.overlays) && aug.overlays.some((o) => o
|
|
138
|
-
const plain = augments.filter((aug) => withOverlays.
|
|
106
|
+
const withOverlays = augments.filter((aug) => Array.isArray(aug?.overlays) && aug.overlays.some((o) => o?.tileLCOverlayPath));
|
|
107
|
+
const plain = augments.filter((aug) => !withOverlays.includes(aug));
|
|
139
108
|
if (withOverlays.length === 0 || plain.length === 0) continue;
|
|
140
109
|
tier.skinAugments = { ...tier.skinAugments, augments: plain };
|
|
141
110
|
for (const aug of withOverlays) {
|
|
142
|
-
let id = parent !== null ? parent + 1000000 * sibling : -(tier.id ?? 0) * 10 - sibling;
|
|
111
|
+
let id = parent !== null ? parent + 1000000 * (sibling + 1) : -((tier.id ?? 0) * 10) - sibling - 1;
|
|
143
112
|
while (taken.has(id)) id += parent !== null ? 1000000 : -1;
|
|
144
113
|
taken.add(id);
|
|
145
|
-
sibling
|
|
114
|
+
sibling += 1;
|
|
146
115
|
variantIdRemap.set(id, tier.id);
|
|
147
116
|
out.push({
|
|
148
117
|
...tier,
|
|
149
118
|
id,
|
|
150
|
-
name: `${tier.name ?? ""} ${
|
|
119
|
+
name: `${tier.name ?? ""} ${variantLabel(aug.overlays)}`.trim(),
|
|
151
120
|
skinAugments: { ...tier.skinAugments, augments: [aug] },
|
|
152
121
|
});
|
|
153
|
-
changed = true;
|
|
154
122
|
}
|
|
155
123
|
}
|
|
156
|
-
|
|
157
|
-
return changed;
|
|
124
|
+
skin.questSkinInfo.tiers = out;
|
|
158
125
|
}
|
|
159
126
|
|
|
160
|
-
function
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
for (const
|
|
164
|
-
|
|
127
|
+
function unlockSkin(skin) {
|
|
128
|
+
unlockItem(skin);
|
|
129
|
+
unlockAugments(skin);
|
|
130
|
+
for (const chroma of Array.isArray(skin?.chromas) ? skin.chromas : []) {
|
|
131
|
+
unlockItem(chroma);
|
|
132
|
+
unlockAugments(chroma);
|
|
133
|
+
}
|
|
134
|
+
const tiers = skin?.questSkinInfo?.tiers;
|
|
135
|
+
if (Array.isArray(tiers)) {
|
|
136
|
+
for (const tier of tiers) {
|
|
137
|
+
unlockItem(tier);
|
|
138
|
+
unlockAugments(tier);
|
|
139
|
+
}
|
|
140
|
+
splitVariantTiers(skin);
|
|
165
141
|
}
|
|
166
|
-
return out;
|
|
167
142
|
}
|
|
168
143
|
|
|
169
|
-
function
|
|
170
|
-
if (!
|
|
171
|
-
|
|
172
|
-
|
|
144
|
+
function unlockChampions(data) {
|
|
145
|
+
if (!Array.isArray(data)) return data;
|
|
146
|
+
const before = augmentIds.size;
|
|
147
|
+
const out = cloneData(data).filter((champion) => !(typeof champion?.id === "number" && champion.id >= SHADOW_CHAMPION_ID));
|
|
148
|
+
for (const champion of out) {
|
|
149
|
+
if (!champion || typeof champion !== "object") continue;
|
|
150
|
+
// The picker copies the champion's ownership onto its base skin and every tier, so an unowned
|
|
151
|
+
// champion would hide all of its skins. Unlock the champion too (only inside this window).
|
|
152
|
+
unlockItem(champion);
|
|
153
|
+
for (const skin of Array.isArray(champion.skins) ? champion.skins : []) unlockSkin(skin);
|
|
154
|
+
}
|
|
155
|
+
if (augmentIds.size !== before) queueMicrotask(() => augmentHandle()?.reapply());
|
|
156
|
+
return out;
|
|
173
157
|
}
|
|
174
158
|
|
|
175
|
-
function
|
|
176
|
-
if (!
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
if (!Array.isArray(node) && node.questSkinInfo && Array.isArray(node.questSkinInfo.tiers)) {
|
|
187
|
-
changed = splitVariantTiers(node) || changed;
|
|
188
|
-
}
|
|
189
|
-
const augList = node.skinAugments?.augments;
|
|
190
|
-
if (Array.isArray(augList)) {
|
|
191
|
-
for (const aug of augList) {
|
|
192
|
-
if (!aug || typeof aug !== "object") continue;
|
|
193
|
-
if (typeof aug.contentId === "string") augmentIds.add(aug.contentId);
|
|
194
|
-
if (aug.ownership && typeof aug.ownership === "object") {
|
|
195
|
-
changed = unlockOwnership(aug.ownership) || changed;
|
|
196
|
-
} else {
|
|
197
|
-
aug.ownership = synthesizedOwnership();
|
|
198
|
-
changed = true;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
const championNode = Array.isArray(node.skins);
|
|
203
|
-
if (!championNode) {
|
|
204
|
-
if (node.ownership && typeof node.ownership === "object") {
|
|
205
|
-
changed = unlockOwnership(node.ownership) || changed;
|
|
206
|
-
} else if (typeof node.contentId === "string" && Array.isArray(node.overlays)) {
|
|
207
|
-
node.ownership = synthesizedOwnership();
|
|
208
|
-
changed = true;
|
|
209
|
-
}
|
|
210
|
-
if (node.owned === false) {
|
|
211
|
-
node.owned = true;
|
|
212
|
-
changed = true;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
const values = Array.isArray(node)
|
|
216
|
-
? node
|
|
217
|
-
: Object.keys(node)
|
|
218
|
-
.filter((key) => key !== "ownership")
|
|
219
|
-
.map((key) => node[key]);
|
|
220
|
-
for (const value of values) {
|
|
221
|
-
if (value && typeof value === "object") changed = unlockDeep(value) || changed;
|
|
222
|
-
}
|
|
223
|
-
return changed;
|
|
159
|
+
function unlockMinimal(data) {
|
|
160
|
+
if (!Array.isArray(data)) return data;
|
|
161
|
+
return cloneData(data)
|
|
162
|
+
.filter((champion) => !(typeof champion?.id === "number" && champion.id >= SHADOW_CHAMPION_ID))
|
|
163
|
+
.map((champion) => {
|
|
164
|
+
unlockItem(champion);
|
|
165
|
+
return champion;
|
|
166
|
+
});
|
|
224
167
|
}
|
|
225
168
|
|
|
226
|
-
function unlockAugmentInventory(
|
|
227
|
-
if (!Array.isArray(
|
|
228
|
-
|
|
169
|
+
function unlockAugmentInventory(data) {
|
|
170
|
+
if (!Array.isArray(data)) return data;
|
|
171
|
+
const out = cloneData(data);
|
|
229
172
|
const present = new Set();
|
|
230
|
-
for (const item of
|
|
173
|
+
for (const item of out) {
|
|
231
174
|
if (!item || typeof item !== "object") continue;
|
|
232
175
|
if (typeof item.uuid === "string") present.add(item.uuid);
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
changed = true;
|
|
236
|
-
}
|
|
237
|
-
if (item.ownershipType !== undefined && item.ownershipType !== "OWNED") {
|
|
238
|
-
item.ownershipType = "OWNED";
|
|
239
|
-
changed = true;
|
|
240
|
-
}
|
|
176
|
+
item.owned = true;
|
|
177
|
+
if (item.ownershipType !== undefined) item.ownershipType = "OWNED";
|
|
241
178
|
}
|
|
242
179
|
for (const id of augmentIds) {
|
|
243
180
|
if (present.has(id)) continue;
|
|
244
|
-
|
|
245
|
-
uuid: id,
|
|
246
|
-
inventoryType: "SKIN_AUGMENT",
|
|
247
|
-
ownershipType: "OWNED",
|
|
248
|
-
owned: true,
|
|
249
|
-
quantity: 1,
|
|
250
|
-
purchaseDate: FAKE_PURCHASE_DATE,
|
|
251
|
-
});
|
|
252
|
-
changed = true;
|
|
253
|
-
}
|
|
254
|
-
return changed;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
function ensureAugmentIds() {
|
|
258
|
-
if (augmentIds.size > 0) return Promise.resolve();
|
|
259
|
-
if (harvestPromise) return harvestPromise;
|
|
260
|
-
harvestPromise = (async () => {
|
|
261
|
-
const waits = [...pendingTargetLoads];
|
|
262
|
-
if (waits.length > 0) {
|
|
263
|
-
await Promise.race([
|
|
264
|
-
Promise.allSettled(waits),
|
|
265
|
-
new Promise((resolve) => setTimeout(resolve, HARVEST_WAIT_MS)),
|
|
266
|
-
]);
|
|
267
|
-
}
|
|
268
|
-
if (augmentIds.size > 0) return;
|
|
269
|
-
try {
|
|
270
|
-
const get = rawFetch || ((...args) => window.fetch(...args));
|
|
271
|
-
let id = summonerId;
|
|
272
|
-
if (id === null) {
|
|
273
|
-
const response = await get(CURRENT_SUMMONER_URI);
|
|
274
|
-
const summoner = await response.json().catch(() => null);
|
|
275
|
-
id = typeof summoner?.summonerId === "number" ? summoner.summonerId : null;
|
|
276
|
-
if (id !== null) summonerId = id;
|
|
277
|
-
}
|
|
278
|
-
if (id === null) return;
|
|
279
|
-
const response = await get(`/lol-champions/v1/inventories/${id}/champions`);
|
|
280
|
-
const json = await response.json().catch(() => null);
|
|
281
|
-
if (json) harvestFromPayload(json);
|
|
282
|
-
} catch {}
|
|
283
|
-
})().finally(() => {
|
|
284
|
-
harvestPromise = null;
|
|
285
|
-
});
|
|
286
|
-
return harvestPromise;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
let tierObserver = null;
|
|
290
|
-
let tierAppliedForOpen = false;
|
|
291
|
-
let tierDropdown = null;
|
|
292
|
-
let tierArmListener = null;
|
|
293
|
-
|
|
294
|
-
function findGroupingDropdown() {
|
|
295
|
-
for (const dd of document.querySelectorAll("lol-uikit-framed-dropdown")) {
|
|
296
|
-
const labels = [...dd.querySelectorAll(GROUPING_OPTIONS)].map((o) => o.textContent.trim().toLowerCase());
|
|
297
|
-
if (labels.includes("my collection") && labels.includes(TIER_LABEL)) return dd;
|
|
298
|
-
}
|
|
299
|
-
return null;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function applyTierGrouping() {
|
|
303
|
-
if (tierDropdown && !tierDropdown.isConnected) {
|
|
304
|
-
tierDropdown = null;
|
|
305
|
-
tierAppliedForOpen = false;
|
|
181
|
+
out.push({ uuid: id, inventoryType: "SKIN_AUGMENT", ownershipType: "OWNED", owned: true, quantity: 1, purchaseDate: FAKE_PURCHASE_DATE });
|
|
306
182
|
}
|
|
307
|
-
|
|
308
|
-
if (!isPickerOpen()) return;
|
|
309
|
-
const dd = tierDropdown ?? findGroupingDropdown();
|
|
310
|
-
if (!dd) {
|
|
311
|
-
tierAppliedForOpen = false;
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
tierDropdown = dd;
|
|
315
|
-
const options = [...dd.querySelectorAll(GROUPING_OPTIONS)];
|
|
316
|
-
const tier = options.find((o) => o.textContent.trim().toLowerCase() === TIER_LABEL);
|
|
317
|
-
if (!tier) return;
|
|
318
|
-
const current = options.find((o) => o.hasAttribute("selected"));
|
|
319
|
-
if (current !== tier) tier.click();
|
|
320
|
-
tierAppliedForOpen = true;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function startTierGrouping() {
|
|
324
|
-
if (tierObserver) return;
|
|
325
|
-
tierObserver = new MutationObserver(() => {
|
|
326
|
-
if (!isPickerOpen()) {
|
|
327
|
-
stopTierGrouping();
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
try {
|
|
331
|
-
applyTierGrouping();
|
|
332
|
-
} catch (error) {
|
|
333
|
-
logError("tier grouping failed.", error);
|
|
334
|
-
}
|
|
335
|
-
});
|
|
336
|
-
tierObserver.observe(document.documentElement ?? document, { childList: true, subtree: true });
|
|
337
|
-
applyTierGrouping();
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function stopTierGrouping() {
|
|
341
|
-
tierObserver?.disconnect();
|
|
342
|
-
tierObserver = null;
|
|
343
|
-
tierAppliedForOpen = false;
|
|
344
|
-
tierDropdown = null;
|
|
183
|
+
return out;
|
|
345
184
|
}
|
|
346
185
|
|
|
347
|
-
function
|
|
348
|
-
|
|
349
|
-
ensureSummonerId();
|
|
186
|
+
function augmentHandle() {
|
|
187
|
+
return handles[2] ?? null;
|
|
350
188
|
}
|
|
351
189
|
|
|
352
|
-
function
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const deadline = Date.now() + timeout;
|
|
356
|
-
return new Promise((resolve) => {
|
|
357
|
-
const step = () => {
|
|
358
|
-
if (summonerId !== null) return resolve(true);
|
|
359
|
-
if (Date.now() >= deadline) return resolve(false);
|
|
360
|
-
setTimeout(step, 40);
|
|
361
|
-
};
|
|
362
|
-
setTimeout(step, 0);
|
|
363
|
-
});
|
|
190
|
+
function remapId(id) {
|
|
191
|
+
const numeric = Number(id);
|
|
192
|
+
return variantIdRemap.has(numeric) ? variantIdRemap.get(numeric) : id;
|
|
364
193
|
}
|
|
365
194
|
|
|
366
|
-
function
|
|
367
|
-
if (summonerId !== null || summonerPending) return;
|
|
368
|
-
summonerPending = true;
|
|
369
|
-
let request;
|
|
370
|
-
try {
|
|
371
|
-
const get = api?.lcu?.get;
|
|
372
|
-
request = get
|
|
373
|
-
? get(CURRENT_SUMMONER_URI, { ttl: SUMMONER_TTL })
|
|
374
|
-
: (rawFetch || window.fetch)(CURRENT_SUMMONER_URI).then((response) => response.json());
|
|
375
|
-
} catch {
|
|
376
|
-
summonerPending = false;
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
Promise.resolve(request)
|
|
380
|
-
.then((summoner) => {
|
|
381
|
-
summonerId = typeof summoner?.summonerId === "number" ? summoner.summonerId : null;
|
|
382
|
-
})
|
|
383
|
-
.catch(() => {})
|
|
384
|
-
.finally(() => {
|
|
385
|
-
summonerPending = false;
|
|
386
|
-
});
|
|
387
|
-
}
|
|
388
|
-
function rewriteInventoryText(raw) {
|
|
389
|
-
if (typeof raw !== "string") return raw;
|
|
390
|
-
if (inventoryTextCache.has(raw)) {
|
|
391
|
-
const cached = inventoryTextCache.get(raw);
|
|
392
|
-
inventoryTextCache.delete(raw);
|
|
393
|
-
inventoryTextCache.set(raw, cached);
|
|
394
|
-
return cached;
|
|
395
|
-
}
|
|
396
|
-
let rewritten = raw;
|
|
195
|
+
async function wrapSelectSkin() {
|
|
397
196
|
try {
|
|
398
|
-
const
|
|
399
|
-
if (
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
197
|
+
const picker = await api.whenRcpReady(PICKER_PLUGIN, 60000);
|
|
198
|
+
if (!active || !picker || typeof picker.selectSkin !== "function" || pickerApi) return;
|
|
199
|
+
const original = picker.selectSkin;
|
|
200
|
+
pickerApi = picker;
|
|
201
|
+
pickerSelectSkin = original;
|
|
202
|
+
picker.selectSkin = function (onSelected, onDefault, onReady) {
|
|
203
|
+
const select =
|
|
204
|
+
typeof onSelected === "function"
|
|
205
|
+
? function (id, ...rest) {
|
|
206
|
+
return onSelected.call(this, active ? remapId(id) : id, ...rest);
|
|
207
|
+
}
|
|
208
|
+
: onSelected;
|
|
209
|
+
return original.call(this, select, onDefault, onReady);
|
|
210
|
+
};
|
|
211
|
+
} catch (error) {
|
|
212
|
+
logError("could not hook the skin picker.", error);
|
|
404
213
|
}
|
|
405
|
-
return rewritten;
|
|
406
214
|
}
|
|
407
215
|
|
|
408
|
-
function
|
|
409
|
-
if (
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
if (!targetFrame && !augmentFrame) return null;
|
|
413
|
-
let frame;
|
|
414
|
-
try {
|
|
415
|
-
frame = JSON.parse(text);
|
|
416
|
-
} catch {
|
|
417
|
-
return null;
|
|
216
|
+
function unwrapSelectSkin() {
|
|
217
|
+
if (pickerApi && pickerSelectSkin) {
|
|
218
|
+
if (Object.prototype.hasOwnProperty.call(pickerApi, "selectSkin")) delete pickerApi.selectSkin;
|
|
219
|
+
if (pickerApi.selectSkin !== pickerSelectSkin && typeof pickerSelectSkin === "function") pickerApi.selectSkin = pickerSelectSkin;
|
|
418
220
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (!payload || typeof payload !== "object" || typeof payload.uri !== "string") return null;
|
|
422
|
-
let changed;
|
|
423
|
-
if (isTargetUrl(payload.uri)) {
|
|
424
|
-
changed = unlockDeep(payload.data);
|
|
425
|
-
} else if (isAugmentUrl(payload.uri)) {
|
|
426
|
-
changed = unlockAugmentInventory(payload.data);
|
|
427
|
-
} else {
|
|
428
|
-
return null;
|
|
429
|
-
}
|
|
430
|
-
if (!changed) return null;
|
|
431
|
-
return JSON.stringify(frame);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function wrapMessageListener(listener) {
|
|
435
|
-
if (typeof listener !== "function" || listener.__perfProfileWrap) return listener;
|
|
436
|
-
const wrapped = function (event) {
|
|
437
|
-
if (event && typeof event.data === "string") {
|
|
438
|
-
const rewritten = rewriteWebSocketFrame(event.data);
|
|
439
|
-
if (rewritten !== null) {
|
|
440
|
-
const fresh = new MessageEvent("message", {
|
|
441
|
-
data: rewritten,
|
|
442
|
-
origin: event.origin,
|
|
443
|
-
lastEventId: event.lastEventId,
|
|
444
|
-
ports: event.ports ? [...event.ports] : [],
|
|
445
|
-
source: event.source ?? null,
|
|
446
|
-
});
|
|
447
|
-
return listener.call(this, fresh);
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
return listener.call(this, event);
|
|
451
|
-
};
|
|
452
|
-
wrapped.__perfProfileWrap = true;
|
|
453
|
-
wrapped.__perfProfileOriginal = listener;
|
|
454
|
-
return wrapped;
|
|
221
|
+
pickerApi = null;
|
|
222
|
+
pickerSelectSkin = null;
|
|
455
223
|
}
|
|
456
224
|
|
|
457
|
-
function
|
|
458
|
-
|
|
459
|
-
if (!proto || proto.addEventListener.__perfProfileFilter) return;
|
|
460
|
-
|
|
461
|
-
const origAdd = proto.addEventListener;
|
|
462
|
-
const origRemove = proto.removeEventListener;
|
|
463
|
-
const origDispatch = proto.dispatchEvent;
|
|
464
|
-
|
|
465
|
-
proto.addEventListener = function (type, listener, options) {
|
|
466
|
-
return origAdd.call(this, type, type === "message" ? wrapMessageListener(listener) : listener, options);
|
|
467
|
-
};
|
|
468
|
-
proto.removeEventListener = function (type, listener, options) {
|
|
469
|
-
return origRemove.call(this, type, (listener && listener.__perfProfileOriginal) || listener, options);
|
|
470
|
-
};
|
|
471
|
-
proto.dispatchEvent = function (event) {
|
|
472
|
-
if (event && event.type === "message" && typeof event.data === "string") {
|
|
473
|
-
const rewritten = rewriteWebSocketFrame(event.data);
|
|
474
|
-
if (rewritten !== null) {
|
|
475
|
-
return origDispatch.call(
|
|
476
|
-
this,
|
|
477
|
-
new MessageEvent("message", {
|
|
478
|
-
data: rewritten,
|
|
479
|
-
origin: event.origin,
|
|
480
|
-
lastEventId: event.lastEventId,
|
|
481
|
-
ports: event.ports ? [...event.ports] : [],
|
|
482
|
-
source: event.source ?? null,
|
|
483
|
-
}),
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
return origDispatch.call(this, event);
|
|
488
|
-
};
|
|
489
|
-
|
|
490
|
-
const descriptor = Object.getOwnPropertyDescriptor(proto, "onmessage");
|
|
491
|
-
if (descriptor && descriptor.set && descriptor.get) {
|
|
492
|
-
Object.defineProperty(proto, "onmessage", {
|
|
493
|
-
configurable: true,
|
|
494
|
-
enumerable: descriptor.enumerable,
|
|
495
|
-
get: descriptor.get,
|
|
496
|
-
set(value) {
|
|
497
|
-
descriptor.set.call(this, typeof value === "function" ? wrapMessageListener(value) : value);
|
|
498
|
-
},
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
proto.addEventListener.__perfProfileFilter = true;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function installFetchFilter() {
|
|
506
|
-
const original = window.fetch;
|
|
507
|
-
if (typeof original !== "function" || original.__perfProfileFilter) return;
|
|
508
|
-
rawFetch = (...args) => original.call(window, ...args);
|
|
509
|
-
|
|
510
|
-
const wrapped = function (input, init) {
|
|
511
|
-
const url = typeof input === "string" ? input : (input && input.url) || "";
|
|
512
|
-
const inventory = pathIdOf(url) !== null;
|
|
513
|
-
const augmentRequest = isAugmentUrl(url);
|
|
514
|
-
let options = init;
|
|
515
|
-
if (filterActive && options && typeof options.body === "string") {
|
|
516
|
-
const mapped = remapVariantIds(options.body);
|
|
517
|
-
if (mapped !== options.body) options = { ...options, body: mapped };
|
|
518
|
-
}
|
|
519
|
-
const promise = original.call(this, input, options);
|
|
520
|
-
if (!filterActive || (!inventory && !augmentRequest)) return promise;
|
|
521
|
-
if (inventory) maybeRefreshSummoner();
|
|
522
|
-
|
|
523
|
-
return whenSummonerKnown(SUMMONER_WAIT_MS).then(() => {
|
|
524
|
-
const target = isTargetUrl(url);
|
|
525
|
-
if (!augmentRequest && !target) return promise;
|
|
526
|
-
if (target && augmentIds.size === 0) {
|
|
527
|
-
const latch = (async () => {
|
|
528
|
-
try {
|
|
529
|
-
const response = await promise;
|
|
530
|
-
if (response && response.ok && augmentIds.size === 0) {
|
|
531
|
-
const json = await response.clone().json().catch(() => null);
|
|
532
|
-
if (json) harvestFromPayload(json);
|
|
533
|
-
}
|
|
534
|
-
} catch {}
|
|
535
|
-
pendingTargetLoads.delete(latch);
|
|
536
|
-
})();
|
|
537
|
-
pendingTargetLoads.add(latch);
|
|
538
|
-
}
|
|
539
|
-
const ready = augmentRequest && augmentIds.size === 0 ? ensureAugmentIds() : Promise.resolve();
|
|
540
|
-
return ready
|
|
541
|
-
.then(() => promise)
|
|
542
|
-
.then((response) => {
|
|
543
|
-
if (!response || !response.ok) return response;
|
|
544
|
-
if (augmentRequest) {
|
|
545
|
-
return response
|
|
546
|
-
.clone()
|
|
547
|
-
.json()
|
|
548
|
-
.catch(() => null)
|
|
549
|
-
.then((json) => {
|
|
550
|
-
if (!json || !unlockAugmentInventory(json)) return response;
|
|
551
|
-
return new Response(JSON.stringify(json), {
|
|
552
|
-
status: response.status,
|
|
553
|
-
statusText: response.statusText,
|
|
554
|
-
headers: response.headers,
|
|
555
|
-
});
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
return response
|
|
559
|
-
.clone()
|
|
560
|
-
.text()
|
|
561
|
-
.catch(() => null)
|
|
562
|
-
.then((text) => {
|
|
563
|
-
if (text === null) return response;
|
|
564
|
-
const rewritten = rewriteInventoryText(text);
|
|
565
|
-
if (rewritten === text) return response;
|
|
566
|
-
return new Response(rewritten, {
|
|
567
|
-
status: response.status,
|
|
568
|
-
statusText: response.statusText,
|
|
569
|
-
headers: response.headers,
|
|
570
|
-
});
|
|
571
|
-
});
|
|
572
|
-
});
|
|
573
|
-
});
|
|
574
|
-
};
|
|
575
|
-
wrapped.__perfProfileFilter = true;
|
|
576
|
-
window.fetch = wrapped;
|
|
577
|
-
}
|
|
578
|
-
function installXhrFilter() {
|
|
579
|
-
const proto = window.XMLHttpRequest && window.XMLHttpRequest.prototype;
|
|
580
|
-
if (!proto || proto.open.__perfProfileFilter) return;
|
|
581
|
-
|
|
582
|
-
const origOpen = proto.open;
|
|
583
|
-
const origSend = proto.send;
|
|
584
|
-
const textDescriptor = Object.getOwnPropertyDescriptor(proto, "responseText");
|
|
585
|
-
const responseDescriptor = Object.getOwnPropertyDescriptor(proto, "response");
|
|
586
|
-
|
|
587
|
-
proto.open = function (method, url, ...rest) {
|
|
588
|
-
this.__perfProfileUrl = typeof url === "string" ? url : "";
|
|
589
|
-
return origOpen.call(this, method, url, ...rest);
|
|
590
|
-
};
|
|
591
|
-
proto.open.__perfProfileFilter = true;
|
|
592
|
-
|
|
593
|
-
proto.send = function (...args) {
|
|
594
|
-
const url = this.__perfProfileUrl || "";
|
|
595
|
-
const target = isTargetUrl(url);
|
|
596
|
-
const augment = !target && isAugmentUrl(url);
|
|
597
|
-
if (filterActive && pathIdOf(url) !== null) maybeRefreshSummoner();
|
|
598
|
-
if (filterActive && typeof args[0] === "string") {
|
|
599
|
-
const mapped = remapVariantIds(args[0]);
|
|
600
|
-
if (mapped !== args[0]) args = [mapped, ...args.slice(1)];
|
|
601
|
-
}
|
|
602
|
-
const xhr = this;
|
|
603
|
-
if (target && filterActive && augmentIds.size === 0 && textDescriptor && textDescriptor.get) {
|
|
604
|
-
const latch = new Promise((resolve) => {
|
|
605
|
-
const finish = () => {
|
|
606
|
-
if (augmentIds.size === 0) {
|
|
607
|
-
try {
|
|
608
|
-
harvestFromPayload(JSON.parse(textDescriptor.get.call(xhr)));
|
|
609
|
-
} catch {}
|
|
610
|
-
}
|
|
611
|
-
pendingTargetLoads.delete(latch);
|
|
612
|
-
resolve();
|
|
613
|
-
};
|
|
614
|
-
xhr.addEventListener("load", finish, { once: true });
|
|
615
|
-
xhr.addEventListener("error", finish, { once: true });
|
|
616
|
-
xhr.addEventListener("abort", finish, { once: true });
|
|
617
|
-
});
|
|
618
|
-
pendingTargetLoads.add(latch);
|
|
619
|
-
}
|
|
620
|
-
if (textDescriptor && textDescriptor.get && (pathIdOf(url) !== null || augment)) {
|
|
621
|
-
const cache = { raw: null, gate: null, filtered: null };
|
|
622
|
-
const gateOpen = () => filterActive && (augment || isTargetUrl(url));
|
|
623
|
-
const rewriteBody = (json) => (augment ? unlockAugmentInventory(json) : unlockDeep(json));
|
|
624
|
-
const filteredText = () => {
|
|
625
|
-
const gate = gateOpen();
|
|
626
|
-
if (cache.filtered !== null && cache.gate === gate) return cache.filtered;
|
|
627
|
-
const raw = textDescriptor.get.call(xhr);
|
|
628
|
-
cache.gate = gate;
|
|
629
|
-
cache.filtered = raw;
|
|
630
|
-
if (gate) {
|
|
631
|
-
if (augment) {
|
|
632
|
-
try {
|
|
633
|
-
const json = JSON.parse(raw);
|
|
634
|
-
if (rewriteBody(json)) cache.filtered = JSON.stringify(json);
|
|
635
|
-
} catch {}
|
|
636
|
-
} else {
|
|
637
|
-
cache.filtered = rewriteInventoryText(raw);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
return cache.filtered;
|
|
641
|
-
};
|
|
642
|
-
Object.defineProperty(xhr, "responseText", { configurable: true, get: filteredText });
|
|
643
|
-
if (responseDescriptor && responseDescriptor.get) {
|
|
644
|
-
Object.defineProperty(xhr, "response", {
|
|
645
|
-
configurable: true,
|
|
646
|
-
get() {
|
|
647
|
-
const type = xhr.responseType;
|
|
648
|
-
if (type === "" || type === "text") return filteredText();
|
|
649
|
-
const value = responseDescriptor.get.call(xhr);
|
|
650
|
-
if (type === "json" && gateOpen() && value && typeof value === "object") rewriteBody(value);
|
|
651
|
-
return value;
|
|
652
|
-
},
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
if (augment && filterActive && augmentIds.size === 0) {
|
|
657
|
-
const sendArgs = args;
|
|
658
|
-
ensureAugmentIds().finally(() => {
|
|
659
|
-
try {
|
|
660
|
-
origSend.apply(xhr, sendArgs);
|
|
661
|
-
} catch {}
|
|
662
|
-
});
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
return origSend.apply(this, args);
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
function installSourceFilter() {
|
|
225
|
+
async function loadTierLabel() {
|
|
226
|
+
if (tierLabel !== null) return tierLabel;
|
|
670
227
|
try {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
} catch (error) {
|
|
677
|
-
logError("could not install the source filter.", error);
|
|
228
|
+
const response = await fetch(TRANSLATIONS_URI);
|
|
229
|
+
const strings = response.ok ? await response.json() : null;
|
|
230
|
+
tierLabel = String(strings?.[TIER_KEY] ?? "").trim().toLowerCase();
|
|
231
|
+
} catch {
|
|
232
|
+
tierLabel = "";
|
|
678
233
|
}
|
|
234
|
+
return tierLabel;
|
|
679
235
|
}
|
|
680
236
|
|
|
681
|
-
function
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
}
|
|
237
|
+
async function groupByTier(dropdown) {
|
|
238
|
+
const label = await loadTierLabel();
|
|
239
|
+
if (!active || !dropdown.isConnected) return;
|
|
240
|
+
const options = [...dropdown.querySelectorAll("lol-uikit-dropdown-option")];
|
|
241
|
+
if (options.length === 0) return;
|
|
242
|
+
const tier = (label && options.find((option) => option.textContent.trim().toLowerCase() === label)) ?? options[options.length - 1];
|
|
243
|
+
if (tier && !tier.hasAttribute("selected")) tier.click();
|
|
689
244
|
}
|
|
690
245
|
|
|
691
246
|
function onEnable(utils) {
|
|
692
247
|
api = utils;
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
248
|
+
active = true;
|
|
249
|
+
const transform = (match, fn) => utils.transformBinding({ identifier: PICKER_PLUGIN, match: (path) => match.test(path), transform: fn });
|
|
250
|
+
handles = [
|
|
251
|
+
transform(CHAMPIONS_PATH, unlockChampions),
|
|
252
|
+
transform(MINIMAL_PATH, unlockMinimal),
|
|
253
|
+
transform(AUGMENTS_PATH, unlockAugmentInventory),
|
|
254
|
+
];
|
|
255
|
+
unwatchGrouping = utils.watch(GROUPING_SELECTOR, (dropdown) => {
|
|
256
|
+
groupByTier(dropdown).catch((error) => logError("tier grouping failed.", error));
|
|
257
|
+
});
|
|
258
|
+
wrapSelectSkin();
|
|
704
259
|
}
|
|
705
260
|
|
|
706
261
|
function onDisable() {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
262
|
+
active = false;
|
|
263
|
+
for (const handle of handles) handle.dispose();
|
|
264
|
+
handles = [];
|
|
265
|
+
unwatchGrouping?.();
|
|
266
|
+
unwatchGrouping = null;
|
|
267
|
+
unwrapSelectSkin();
|
|
712
268
|
api = null;
|
|
713
269
|
}
|
|
714
270
|
|
|
715
271
|
export function init() {
|
|
716
|
-
installSourceFilter();
|
|
717
|
-
|
|
718
272
|
const registerWith = (utils) => {
|
|
719
273
|
utils.registerPlugin({
|
|
720
274
|
id: PLUGIN_ID,
|
|
@@ -729,6 +283,6 @@ export function init() {
|
|
|
729
283
|
if (window.PerfectionistUtils) {
|
|
730
284
|
registerWith(window.PerfectionistUtils);
|
|
731
285
|
} else {
|
|
732
|
-
addEventListener("perfectionist:utils-ready", (event) => registerWith(event.detail), { once: true });
|
|
286
|
+
addEventListener("perfectionist:utils-ready", (event) => registerWith(event.detail ?? window.PerfectionistUtils), { once: true });
|
|
733
287
|
}
|
|
734
|
-
}
|
|
288
|
+
}
|