返回提交历史
Modified
package-lock.json
+4
-4
Modified
package.json
+1
-1
Modified
src/controllers/api/inventoryController.ts
+1
-1
Modified
src/controllers/api/sellController.ts
+9
-0
Modified
src/controllers/custom/getItemListsController.ts
+15
-5
Modified
src/controllers/dynamic/getProfileViewingDataController.ts
+3
-10
Added
src/helpers/skinLookupTable.ts
+8
-0
Modified
src/services/inventoryService.ts
+95
-1
Modified
static/webui/index.html
+20
-0
Modified
static/webui/script.js
+78
-0
Modified
static/webui/translations/de.js
+6
-0
Modified
static/webui/translations/en.js
+6
-0
Modified
static/webui/translations/es.js
+6
-0
Modified
static/webui/translations/fr.js
+6
-0
Modified
static/webui/translations/ru.js
+6
-0
Modified
static/webui/translations/uk.js
+6
-0
Modified
static/webui/translations/zh.js
+6
-0
XFEstudio/XFESpaceNinjaServer
feat(webui): skins (#2816)
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2816 Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com> Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
02f09357
代码差异
17 个文件
+276
-22
@@ -18,7 +18,7 @@
18
18
"morgan": "^1.10.0",
19
19
"ncp": "^2.0.0",
20
20
"undici": "^7.10.0",
21
"warframe-public-export-plus": "^0.5.89",
21
"warframe-public-export-plus": "^0.5.90",
22
22
"warframe-riven-info": "^0.1.2",
23
23
"winston": "^3.17.0",
24
24
"winston-daily-rotate-file": "^5.0.0",
@@ -5534,9 +5534,9 @@
5534
5534
}
5535
5535
},
5536
5536
"node_modules/warframe-public-export-plus": {
5537
"version": "0.5.89",
5538
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.89.tgz",
5539
"integrity": "sha512-a6dM1MirzofSsuv3LlRQHFLSSIGKPVSN93dcXSDmA3njsWqOGjJJdWyXqcyxxYw8rEB8CNowSHst/MUmKvKlRg=="
5537
"version": "0.5.90",
5538
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.90.tgz",
5539
"integrity": "sha512-PzYFJ+qOZawPOVx9+hGlOosGdqSJMSRL6c1z3CCXMiY5SEHUs7p9JIMjkqnvg1g7vflVWnyG6E5/2gYkQs0d7w=="
5540
5540
},
5541
5541
"node_modules/warframe-riven-info": {
5542
5542
"version": "0.1.2",
@@ -36,7 +36,7 @@
36
36
"morgan": "^1.10.0",
37
37
"ncp": "^2.0.0",
38
38
"undici": "^7.10.0",
39
"warframe-public-export-plus": "^0.5.89",
39
"warframe-public-export-plus": "^0.5.90",
40
40
"warframe-riven-info": "^0.1.2",
41
41
"winston": "^3.17.0",
42
42
"winston-daily-rotate-file": "^5.0.0",
@@ -177,7 +177,7 @@ export const inventoryController: RequestHandler = async (request, response) =>
177
177
}
178
178
}
179
179
180
cleanupInventory(inventory);
180
await cleanupInventory(inventory);
181
181
182
182
inventory.NextRefill = new Date((today + 1) * 86400000); // tomorrow at 0 UTC
183
183
//await inventory.save();
@@ -77,6 +77,9 @@ export const sellController: RequestHandler = async (req, res) => {
77
77
requiredFields.add("CrewShipSalvagedWeaponSkins");
78
78
}
79
79
}
80
if (payload.Items.WeaponSkins) {
81
requiredFields.add("WeaponSkins");
82
}
80
83
const inventory = await getInventory(accountId, Array.from(requiredFields).join(" "));
81
84
82
85
// Give currency
@@ -302,6 +305,11 @@ export const sellController: RequestHandler = async (req, res) => {
302
305
addFusionTreasures(inventory, [parseFusionTreasure(sellItem.String, sellItem.Count * -1)]);
303
306
});
304
307
}
308
if (payload.Items.WeaponSkins) {
309
payload.Items.WeaponSkins.forEach(sellItem => {
310
inventory.WeaponSkins.pull({ _id: sellItem.String });
311
});
312
}
305
313
306
314
await inventory.save();
307
315
res.json({
@@ -335,6 +343,7 @@ interface ISellRequest {
335
343
CrewShipWeapons?: ISellItem[];
336
344
CrewShipWeaponSkins?: ISellItem[];
337
345
FusionTreasures?: ISellItem[];
346
WeaponSkins?: ISellItem[]; // SNS specific field
338
347
};
339
348
SellPrice: number;
340
349
SellCurrency:
@@ -66,6 +66,7 @@ interface ItemLists {
66
66
VaultDecoRecipes: ListedItem[];
67
67
FlavourItems: ListedItem[];
68
68
ShipDecorations: ListedItem[];
69
WeaponSkins: ListedItem[];
69
70
//circuitGameModes: ListedItem[];
70
71
}
71
72
@@ -107,7 +108,8 @@ const getItemListsController: RequestHandler = (req, response) => {
107
108
TechProjects: [],
108
109
VaultDecoRecipes: [],
109
110
FlavourItems: [],
110
ShipDecorations: []
111
ShipDecorations: [],
112
WeaponSkins: []
111
113
/*circuitGameModes: [
112
114
{
113
115
uniqueName: "Survival",
@@ -298,10 +300,18 @@ const getItemListsController: RequestHandler = (req, response) => {
298
300
});
299
301
}
300
302
for (const [uniqueName, item] of Object.entries(ExportCustoms)) {
301
res.miscitems.push({
302
uniqueName: uniqueName,
303
name: getString(item.name, lang)
304
});
303
if (
304
item.productCategory == "WeaponSkins" &&
305
!uniqueName.startsWith("/Lotus/Types/Game/Lotus") && // Base Items
306
!uniqueName.endsWith("ProjectileSkin") && // UnrealTournament ProjectileSkins
307
!uniqueName.endsWith("Coat") // Frost Prime stuff
308
) {
309
res.WeaponSkins.push({
310
uniqueName: uniqueName,
311
name: getString(item.name, lang),
312
alwaysAvailable: item.alwaysAvailable
313
});
314
}
305
315
}
306
316
307
317
for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) {
@@ -20,13 +20,14 @@ import type {
20
20
} from "../../types/inventoryTypes/inventoryTypes.ts";
21
21
import { LoadoutIndex } from "../../types/inventoryTypes/inventoryTypes.ts";
22
22
import type { RequestHandler } from "express";
23
import { catBreadHash, getJSONfromString } from "../../helpers/stringHelpers.ts";
24
import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus";
23
import { getJSONfromString } from "../../helpers/stringHelpers.ts";
24
import { ExportDojoRecipes } from "warframe-public-export-plus";
25
25
import type { IStatsClient } from "../../types/statTypes.ts";
26
26
import { toStoreItem } from "../../services/itemDataService.ts";
27
27
import type { FlattenMaps } from "mongoose";
28
28
import type { IEquipmentClient } from "../../types/equipmentTypes.ts";
29
29
import type { ILoadoutConfigClient } from "../../types/saveLoadoutTypes.ts";
30
import { skinLookupTable } from "../../helpers/skinLookupTable.ts";
30
31
31
32
const getProfileViewingDataByPlayerIdImpl = async (playerId: string): Promise<IProfileViewingData | undefined> => {
32
33
const account = await Account.findById(playerId, "DisplayName");
@@ -261,8 +262,6 @@ interface IXPComponentClient {
261
262
locTags?: Record<string, string>;
262
263
}
263
264
264
let skinLookupTable: Record<number, string> | undefined;
265
266
265
const resolveAndCollectSkins = (
267
266
inventory: TInventoryDatabaseDocument,
268
267
skins: Set<string>,
@@ -274,12 +273,6 @@ const resolveAndCollectSkins = (
274
273
// Resolve oids to type names
275
274
if (config.Skins[i].length == 24) {
276
275
if (config.Skins[i].substring(0, 16) == "ca70ca70ca70ca70") {
277
if (!skinLookupTable) {
278
skinLookupTable = {};
279
for (const key of Object.keys(ExportCustoms)) {
280
skinLookupTable[catBreadHash(key)] = key;
281
}
282
}
283
276
config.Skins[i] = skinLookupTable[parseInt(config.Skins[i].substring(16), 16)];
284
277
} else {
285
278
const skinItem = inventory.WeaponSkins.id(config.Skins[i]);
@@ -0,0 +1,8 @@
1
import { ExportCustoms } from "warframe-public-export-plus";
2
import { catBreadHash } from "./stringHelpers.ts";
3
4
export const skinLookupTable: Record<number, string> = {};
5
6
for (const key of Object.keys(ExportCustoms)) {
7
skinLookupTable[catBreadHash(key)] = key;
8
}
@@ -92,6 +92,7 @@ import type {
92
92
} from "../types/equipmentTypes.ts";
93
93
import { EquipmentFeatures, Status } from "../types/equipmentTypes.ts";
94
94
import type { ITypeCount } from "../types/commonTypes.ts";
95
import { skinLookupTable } from "../helpers/skinLookupTable.ts";
95
96
96
97
export const createInventory = async (
97
98
accountOwnerId: Types.ObjectId,
@@ -2260,7 +2261,7 @@ export const setupKahlSyndicate = (inventory: TInventoryDatabaseDocument): void
2260
2261
});
2261
2262
};
2262
2263
2263
export const cleanupInventory = (inventory: TInventoryDatabaseDocument): void => {
2264
export const cleanupInventory = async (inventory: TInventoryDatabaseDocument): Promise<void> => {
2264
2265
inventory.CurrentLoadOutIds = inventory.CurrentLoadOutIds.map(fromDbOid);
2265
2266
2266
2267
let index = inventory.MiscItems.findIndex(x => x.ItemType == "");
@@ -2314,6 +2315,99 @@ export const cleanupInventory = (inventory: TInventoryDatabaseDocument): void =>
2314
2315
logger.debug(`removed ModularParts from ${numFixed} non-modular items`);
2315
2316
}
2316
2317
}
2318
2319
{
2320
const weaponMap = new Map<string, string>();
2321
for (const skin of inventory.WeaponSkins) {
2322
weaponMap.set(skin.ItemType, skin._id.toString());
2323
}
2324
2325
const itemsToAdd = new Set<string>();
2326
2327
for (const key of equipmentKeys) {
2328
if (key in inventory) {
2329
for (const equipment of inventory[key]) {
2330
for (const config of equipment.Configs) {
2331
if (config.Skins) collectSkins(config.Skins, weaponMap, itemsToAdd);
2332
}
2333
}
2334
}
2335
}
2336
2337
for (const key of ["AdultOperatorLoadOuts", "OperatorLoadOuts", "KahlLoadOuts"] as const) {
2338
if (key in inventory) {
2339
for (const loadOut of inventory[key]) {
2340
if (loadOut.Skins) collectSkins(loadOut.Skins, weaponMap, itemsToAdd);
2341
}
2342
}
2343
}
2344
2345
if (inventory.LotusCustomization?.Skins)
2346
collectSkins(inventory.LotusCustomization.Skins, weaponMap, itemsToAdd);
2347
2348
if (itemsToAdd.size > 0) {
2349
logger.debug(`Adding ${itemsToAdd.size} items due to migration from unlockAllSkins cheat`);
2350
const inventoryChanges = await addItems(inventory, Array.from(itemsToAdd));
2351
2352
if (inventoryChanges.WeaponSkins) {
2353
for (const skin of inventoryChanges.WeaponSkins as IWeaponSkinClient[]) {
2354
weaponMap.set(skin.ItemType, skin.ItemId.toString());
2355
}
2356
}
2357
2358
for (const key of equipmentKeys) {
2359
if (key in inventory) {
2360
for (const equipment of inventory[key]) {
2361
for (const config of equipment.Configs) {
2362
if (config.Skins) replaceSkinIds(config.Skins, weaponMap);
2363
}
2364
}
2365
}
2366
}
2367
2368
for (const key of ["AdultOperatorLoadOuts", "OperatorLoadOuts", "KahlLoadOuts"] as const) {
2369
if (key in inventory) {
2370
for (const loadOut of inventory[key]) {
2371
if (loadOut.Skins) replaceSkinIds(loadOut.Skins, weaponMap);
2372
}
2373
}
2374
}
2375
2376
if (inventory.LotusCustomization?.Skins) replaceSkinIds(inventory.LotusCustomization.Skins, weaponMap);
2377
}
2378
}
2379
};
2380
2381
const collectSkins = (skins: string[], weaponMap: Map<string, string>, itemsToAdd: Set<string>): void => {
2382
for (const skinId of skins) {
2383
if (skinId.startsWith("ca70ca70ca70ca70")) {
2384
const typeName = skinLookupTable[parseInt(skinId.slice(16), 16)];
2385
if (!weaponMap.has(typeName)) {
2386
const { requirement } = ExportCustoms[typeName];
2387
if (typeof requirement == "string") {
2388
itemsToAdd.add(requirement);
2389
} else {
2390
itemsToAdd.add(typeName);
2391
}
2392
}
2393
}
2394
}
2395
};
2396
2397
const replaceSkinIds = (skins: string[], weaponMap: Map<string, string>): void => {
2398
for (let i = 0; i < skins.length; i++) {
2399
const skinId = skins[i];
2400
if (skinId.startsWith("ca70ca70ca70ca70")) {
2401
const typeName = skinLookupTable[parseInt(skinId.slice(16), 16)];
2402
const inventoryId = weaponMap.get(typeName);
2403
if (inventoryId) {
2404
skins[i] = inventoryId;
2405
} else if (typeName in ExportCustoms) {
2406
const { requirement } = ExportCustoms[typeName];
2407
skins[i] = typeof requirement == "string" ? typeName : "";
2408
}
2409
}
2410
}
2317
2411
};
2318
2412
2319
2413
export const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => {
@@ -526,6 +526,24 @@
526
526
</div>
527
527
</div>
528
528
</div>
529
<div class="row g-3 mb-3">
530
<div class="col-lg-6">
531
<div class="card" style="height: 400px;">
532
<h5 class="card-header" data-loc="inventory_weaponSkins"></h5>
533
<div class="card-body d-flex flex-column">
534
<form class="input-group mb-3" onsubmit="doAcquireEquipment('WeaponSkins');return false;">
535
<input class="form-control" id="acquire-type-WeaponSkins" list="datalist-WeaponSkins" />
536
<button class="btn btn-primary" type="submit" data-loc="general_addButton"></button>
537
</form>
538
<div class="overflow-auto">
539
<table class="table table-hover w-100">
540
<tbody id="WeaponSkins-list"></tbody>
541
</table>
542
</div>
543
</div>
544
</div>
545
</div>
546
</div>
529
547
<div class="card">
530
548
<h5 class="card-header" data-loc="general_bulkActions"></h5>
531
549
<div class="card-body">
@@ -538,6 +556,7 @@
538
556
<button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['SentinelWeapons']);" data-loc="inventory_bulkAddSentinelWeapons"></button>
539
557
<button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['FlavourItems']);" data-loc="inventory_bulkAddFlavourItems"></button>
540
558
<button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['ShipDecorations']);" data-loc="inventory_bulkAddShipDecorations"></button>
559
<button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['WeaponSkins']);" data-loc="inventory_bulkAddWeaponSkins"></button>
541
560
<button class="btn btn-primary" onclick="debounce(addMissingEvolutionProgress);" data-loc="inventory_bulkAddEvolutionProgress"></button>
542
561
</div>
543
562
<div class="mb-2 d-flex flex-wrap gap-2">
@@ -1528,6 +1547,7 @@
1528
1547
<datalist id="datalist-VaultDecoRecipes"></datalist>
1529
1548
<datalist id="datalist-FlavourItems"></datalist>
1530
1549
<datalist id="datalist-ShipDecorations"></datalist>
1550
<datalist id="datalist-WeaponSkins"></datalist>
1531
1551
<datalist id="datalist-circuitGameModes">
1532
1552
<option>Survival</option>
1533
1553
<option>VoidFlood</option>
@@ -599,6 +599,46 @@ function fetchItemList() {
599
599
}
600
600
itemMap[item.uniqueName] = { ...item, type };
601
601
});
602
} else if (type == "WeaponSkins") {
603
let beardNumber = 1;
604
let cutNumber = 13;
605
let adultHeadNumber = 1;
606
let headNumber = 1;
607
items.forEach(item => {
608
if (item.name == "") {
609
if (item.uniqueName.includes("/Beards/")) {
610
item.name = loc("code_drifterBeardName")
611
.split("|INDEX|")
612
.join(beardNumber.toString().padStart(3, "0"));
613
beardNumber++;
614
} else if (item.uniqueName.includes("/Hair/")) {
615
item.name = loc("code_cutName")
616
.split("|INDEX|")
617
.join(cutNumber.toString().padStart(3, "0"));
618
cutNumber++;
619
if (cutNumber == 19) cutNumber = 21;
620
} else if (item.uniqueName.includes("/Heads/Adult")) {
621
item.name = loc("code_drifterFaceName")
622
.split("|INDEX|")
623
.join(adultHeadNumber.toString().padStart(3, "0"));
624
adultHeadNumber++;
625
} else if (item.uniqueName.includes("/Heads/")) {
626
item.name = loc("code_operatorFaceName")
627
.split("|INDEX|")
628
.join(headNumber.toString().padStart(3, "0"));
629
headNumber++;
630
} else {
631
item.name = item.uniqueName;
632
}
633
}
634
if (!item.alwaysAvailable) {
635
const option = document.createElement("option");
636
option.setAttribute("data-key", item.uniqueName);
637
option.value = item.name;
638
document.getElementById("datalist-" + type).appendChild(option);
639
}
640
itemMap[item.uniqueName] = { ...item, type };
641
});
602
642
} else {
603
643
const nameToItems = {};
604
644
items.forEach(item => {
@@ -1103,6 +1143,44 @@ function updateInventory() {
1103
1143
document.getElementById("FlavourItems-list").appendChild(tr);
1104
1144
});
1105
1145
1146
document.getElementById("WeaponSkins-list").innerHTML = "";
1147
data.WeaponSkins.forEach(item => {
1148
if (item.ItemId.$oid.startsWith("ca70ca70ca70ca70")) return;
1149
const datalist = document.getElementById("datalist-WeaponSkins");
1150
const optionToRemove = datalist.querySelector(`option[data-key="${item.ItemType}"]`);
1151
if (optionToRemove) {
1152
datalist.removeChild(optionToRemove);
1153
}
1154
const tr = document.createElement("tr");
1155
{
1156
const td = document.createElement("td");
1157
const name = itemMap[item.ItemType]?.name?.trim();
1158
td.textContent = name || item.ItemType;
1159
tr.appendChild(td);
1160
}
1161
{
1162
const td = document.createElement("td");
1163
td.classList = "text-end text-nowrap";
1164
{
1165
const a = document.createElement("a");
1166
a.href = "#";
1167
a.onclick = function (event) {
1168
event.preventDefault();
1169
document.getElementById("WeaponSkins-list").removeChild(tr);
1170
reAddToItemList(itemMap, "WeaponSkins", item.ItemType);
1171
disposeOfGear("WeaponSkins", item.ItemId.$oid);
1172
};
1173
a.title = loc("code_remove");
1174
a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M135.2 17.7L128 32H32C14.3 32 0 46.3 0 64S14.3 96 32 96H416c17.7 0 32-14.3 32-32s-14.3-32-32-32H320l-7.2-14.3C307.4 6.8 296.3 0 284.2 0H163.8c-12.1 0-23.2 6.8-28.6 17.7zM416 128H32L53.2 467c1.6 25.3 22.6 45 47.9 45H346.9c25.3 0 46.3-19.7 47.9-45L416 128z"/></svg>`;
1175
td.appendChild(a);
1176
}
1177
1178
tr.appendChild(td);
1179
}
1180
1181
document.getElementById("WeaponSkins-list").appendChild(tr);
1182
});
1183
1106
1184
const datalistEvolutionProgress = document.querySelectorAll("#datalist-EvolutionProgress option");
1107
1185
const formEvolutionProgress = document.querySelector('form[onsubmit*="doAcquireEvolution()"]');
1108
1186
@@ -75,6 +75,10 @@ dict = {
75
75
code_funded: `[UNTRANSLATED] Funded`,
76
76
code_replays: `[UNTRANSLATED] Replays`,
77
77
code_stalker: `Stalker`,
78
code_cutName: `Frisur: |INDEX|`,
79
code_drifterBeardName: `Drifter-Bart: |INDEX|`,
80
code_drifterFaceName: `Drifter-Gesicht: |INDEX|`,
81
code_operatorFaceName: `Operator-Gesicht: |INDEX|`,
78
82
code_succChange: `Erfolgreich geändert.`,
79
83
code_requiredInvigorationUpgrade: `[UNTRANSLATED] You must select both an offensive & utility upgrade.`,
80
84
login_description: `Melde dich mit deinem OpenWF-Account an (denselben Angaben wie im Spiel, wenn du dich mit diesem Server verbindest).`,
@@ -112,6 +116,7 @@ dict = {
112
116
inventory_boosters: `Booster`,
113
117
inventory_flavourItems: `<abbr title="Animationssets, Glyphen, Farbpaletten usw.">Sammlerstücke</abbr>`,
114
118
inventory_shipDecorations: `Schiffsdekorationen`,
119
inventory_weaponSkins: `Skins`,
115
120
inventory_bulkAddSuits: `Fehlende Warframes hinzufügen`,
116
121
inventory_bulkAddWeapons: `Fehlende Waffen hinzufügen`,
117
122
inventory_bulkAddSpaceSuits: `Fehlende Archwings hinzufügen`,
@@ -121,6 +126,7 @@ dict = {
121
126
inventory_bulkAddFlavourItems: `[UNTRANSLATED] Add Missing Flavour Items`,
122
127
inventory_bulkAddShipDecorations: `[UNTRANSLATED] Add Missing Ship Decorations`,
123
128
inventory_bulkAddEvolutionProgress: `Fehlende Incarnon-Entwicklungsfortschritte hinzufügen`,
129
inventory_bulkAddWeaponSkins: `[UNTRANSLATED] Add Missing Skins`,
124
130
inventory_bulkRankUpSuits: `Alle Warframes auf Max. Rang`,
125
131
inventory_bulkRankUpWeapons: `Alle Waffen auf Max. Rang`,
126
132
inventory_bulkRankUpSpaceSuits: `Alle Archwings auf Max. Rang`,
@@ -74,6 +74,10 @@ dict = {
74
74
code_funded: `Funded`,
75
75
code_replays: `Replays`,
76
76
code_stalker: `Stalker`,
77
code_cutName: `Cut |INDEX|`,
78
code_drifterBeardName: `Drifter Beard |INDEX|`,
79
code_drifterFaceName: `Drifter Visage |INDEX|`,
80
code_operatorFaceName: `Operator Visage |INDEX|`,
77
81
code_succChange: `Successfully changed.`,
78
82
code_requiredInvigorationUpgrade: `You must select both an offensive & utility upgrade.`,
79
83
login_description: `Login using your OpenWF account credentials (same as in-game when connecting to this server).`,
@@ -111,6 +115,7 @@ dict = {
111
115
inventory_boosters: `Boosters`,
112
116
inventory_flavourItems: `<abbr title="Animation Sets, Glyphs, Palettes, etc.">Flavour Items</abbr>`,
113
117
inventory_shipDecorations: `Ship Decorations`,
118
inventory_weaponSkins: `Skins`,
114
119
inventory_bulkAddSuits: `Add Missing Warframes`,
115
120
inventory_bulkAddWeapons: `Add Missing Weapons`,
116
121
inventory_bulkAddSpaceSuits: `Add Missing Archwings`,
@@ -120,6 +125,7 @@ dict = {
120
125
inventory_bulkAddFlavourItems: `Add Missing Flavour Items`,
121
126
inventory_bulkAddShipDecorations: `Add Missing Ship Decorations`,
122
127
inventory_bulkAddEvolutionProgress: `Add Missing Incarnon Evolution Progress`,
128
inventory_bulkAddWeaponSkins: `Add Missing Skins`,
123
129
inventory_bulkRankUpSuits: `Max Rank All Warframes`,
124
130
inventory_bulkRankUpWeapons: `Max Rank All Weapons`,
125
131
inventory_bulkRankUpSpaceSuits: `Max Rank All Archwings`,