XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESpaceNinjaServer

A simple server for a small space ninja game

公开
关注 0 Fork 0 Star 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
AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
提交于

代码差异

17 个文件 +276 -22
Modified package-lock.json +4 -4
@@ -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",
Modified package.json +1 -1
@@ -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",
Modified src/controllers/api/inventoryController.ts +1 -1
@@ -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();
Modified src/controllers/api/sellController.ts +9 -0
@@ -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:
Modified src/controllers/custom/getItemListsController.ts +15 -5
@@ -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)) {
Modified src/controllers/dynamic/getProfileViewingDataController.ts +3 -10
@@ -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]);
Added src/helpers/skinLookupTable.ts +8 -0
@@ -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 }
Modified src/services/inventoryService.ts +95 -1
@@ -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 => {
Modified static/webui/index.html +20 -0
@@ -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>
Modified static/webui/script.js +78 -0
@@ -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
Modified static/webui/translations/de.js +6 -0
@@ -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`,
Modified static/webui/translations/en.js +6 -0
@@ -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`,
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