forked from OpenWF/SpaceNinjaServer
		
	Compare commits
	
		
			24 Commits
		
	
	
		
			64b43fcccf
			...
			509f7f0d9b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 509f7f0d9b | |||
| aada031a80 | |||
| a2a441ecb0 | |||
| c0a0463a68 | |||
| 2307a40833 | |||
| 304af514e2 | |||
| ddf3cd49b5 | |||
| 41e3f0136f | |||
| c0ca9d9398 | |||
| 0f6b55beed | |||
| f8550e9afe | |||
| b53c4d9125 | |||
| 922b65cfab | |||
| 2f642df20a | |||
| 62314e89c7 | |||
| 56aa3e3331 | |||
| c3f486488f | |||
| 49c353d895 | |||
| 90ab560620 | |||
| b0e80fcfa8 | |||
| 2c62fb3c3c | |||
| 5b215733aa | |||
| 39866b9a2b | |||
| fad1ee9314 | 
@ -21,7 +21,7 @@
 | 
				
			|||||||
        "@typescript-eslint/no-unsafe-argument": "error",
 | 
					        "@typescript-eslint/no-unsafe-argument": "error",
 | 
				
			||||||
        "@typescript-eslint/no-unsafe-call": "error",
 | 
					        "@typescript-eslint/no-unsafe-call": "error",
 | 
				
			||||||
        "@typescript-eslint/no-unsafe-assignment": "error",
 | 
					        "@typescript-eslint/no-unsafe-assignment": "error",
 | 
				
			||||||
        "@typescript-eslint/no-explicit-any": "error",
 | 
					        "@typescript-eslint/no-explicit-any": "off",
 | 
				
			||||||
        "no-loss-of-precision": "error",
 | 
					        "no-loss-of-precision": "error",
 | 
				
			||||||
        "@typescript-eslint/no-unnecessary-condition": "error",
 | 
					        "@typescript-eslint/no-unnecessary-condition": "error",
 | 
				
			||||||
        "@typescript-eslint/no-base-to-string": "off",
 | 
					        "@typescript-eslint/no-base-to-string": "off",
 | 
				
			||||||
 | 
				
			|||||||
@ -2,3 +2,4 @@ src/routes/api.ts
 | 
				
			|||||||
static/webui/libs/
 | 
					static/webui/libs/
 | 
				
			||||||
*.html
 | 
					*.html
 | 
				
			||||||
*.md
 | 
					*.md
 | 
				
			||||||
 | 
					config.json.example
 | 
				
			||||||
 | 
				
			|||||||
@ -7,5 +7,6 @@ WORKDIR /app
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
RUN npm i --omit=dev
 | 
					RUN npm i --omit=dev
 | 
				
			||||||
RUN npm run build
 | 
					RUN npm run build
 | 
				
			||||||
 | 
					RUN date '+%d %B %Y' > BUILD_DATE
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
 | 
					ENTRYPOINT ["/app/docker-entrypoint.sh"]
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										652
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										652
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -14,6 +14,7 @@
 | 
				
			|||||||
    "dev": "node scripts/dev.js",
 | 
					    "dev": "node scripts/dev.js",
 | 
				
			||||||
    "dev:bun": "bun scripts/dev.js",
 | 
					    "dev:bun": "bun scripts/dev.js",
 | 
				
			||||||
    "verify": "tsgo --noEmit",
 | 
					    "verify": "tsgo --noEmit",
 | 
				
			||||||
 | 
					    "verify:tsc": "tsc --noEmit",
 | 
				
			||||||
    "bun-run": "bun src/index.ts",
 | 
					    "bun-run": "bun src/index.ts",
 | 
				
			||||||
    "lint": "eslint --ext .ts .",
 | 
					    "lint": "eslint --ext .ts .",
 | 
				
			||||||
    "lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .",
 | 
					    "lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .",
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										22
									
								
								src/controllers/api/apartmentController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/controllers/api/apartmentController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
				
			||||||
 | 
					import { getPersonalRooms } from "@/src/services/personalRoomsService";
 | 
				
			||||||
 | 
					import { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const apartmentController: RequestHandler = async (req, res) => {
 | 
				
			||||||
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
 | 
					    const personalRooms = await getPersonalRooms(accountId, "Apartment");
 | 
				
			||||||
 | 
					    const response: IApartmentResponse = {};
 | 
				
			||||||
 | 
					    if (req.query.backdrop !== undefined) {
 | 
				
			||||||
 | 
					        response.NewBackdropItem = personalRooms.Apartment.VideoWallBackdrop = req.query.backdrop as string;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (req.query.soundscape !== undefined) {
 | 
				
			||||||
 | 
					        response.NewSoundscapeItem = personalRooms.Apartment.Soundscape = req.query.soundscape as string;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    await personalRooms.save();
 | 
				
			||||||
 | 
					    res.json(response);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IApartmentResponse {
 | 
				
			||||||
 | 
					    NewBackdropItem?: string;
 | 
				
			||||||
 | 
					    NewSoundscapeItem?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -88,7 +88,6 @@ export const crewShipFusionController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    superiorItem.UpgradeFingerprint = JSON.stringify(fingerprint);
 | 
					    superiorItem.UpgradeFingerprint = JSON.stringify(fingerprint);
 | 
				
			||||||
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
					 | 
				
			||||||
    inventoryChanges[category] = [superiorItem.toJSON() as any];
 | 
					    inventoryChanges[category] = [superiorItem.toJSON() as any];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await inventory.save();
 | 
					    await inventory.save();
 | 
				
			||||||
 | 
				
			|||||||
@ -3,11 +3,13 @@ import {
 | 
				
			|||||||
    getGuildForRequestEx,
 | 
					    getGuildForRequestEx,
 | 
				
			||||||
    hasAccessToDojo,
 | 
					    hasAccessToDojo,
 | 
				
			||||||
    hasGuildPermission,
 | 
					    hasGuildPermission,
 | 
				
			||||||
 | 
					    refundDojoDeco,
 | 
				
			||||||
    removeDojoDeco
 | 
					    removeDojoDeco
 | 
				
			||||||
} from "@/src/services/guildService";
 | 
					} from "@/src/services/guildService";
 | 
				
			||||||
import { getInventory } from "@/src/services/inventoryService";
 | 
					import { getInventory } from "@/src/services/inventoryService";
 | 
				
			||||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
					import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
				
			||||||
import { GuildPermission } from "@/src/types/guildTypes";
 | 
					import { GuildPermission } from "@/src/types/guildTypes";
 | 
				
			||||||
 | 
					import { logger } from "@/src/utils/logger";
 | 
				
			||||||
import { RequestHandler } from "express";
 | 
					import { RequestHandler } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const destroyDojoDecoController: RequestHandler = async (req, res) => {
 | 
					export const destroyDojoDecoController: RequestHandler = async (req, res) => {
 | 
				
			||||||
@ -18,9 +20,20 @@ export const destroyDojoDecoController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
        res.json({ DojoRequestStatus: -1 });
 | 
					        res.json({ DojoRequestStatus: -1 });
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest;
 | 
					    const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest | IClearObstacleCourseRequest;
 | 
				
			||||||
 | 
					    if ("DecoType" in request) {
 | 
				
			||||||
    removeDojoDeco(guild, request.ComponentId, request.DecoId);
 | 
					        removeDojoDeco(guild, request.ComponentId, request.DecoId);
 | 
				
			||||||
 | 
					    } else if (request.Act == "cObst") {
 | 
				
			||||||
 | 
					        const component = guild.DojoComponents.id(request.ComponentId)!;
 | 
				
			||||||
 | 
					        if (component.Decos) {
 | 
				
			||||||
 | 
					            for (const deco of component.Decos) {
 | 
				
			||||||
 | 
					                refundDojoDeco(guild, component, deco);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            component.Decos.splice(0, component.Decos.length);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        logger.error(`unhandled destroyDojoDeco request`, request);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await guild.save();
 | 
					    await guild.save();
 | 
				
			||||||
    res.json(await getDojoClient(guild, 0, request.ComponentId));
 | 
					    res.json(await getDojoClient(guild, 0, request.ComponentId));
 | 
				
			||||||
@ -31,3 +44,8 @@ interface IDestroyDojoDecoRequest {
 | 
				
			|||||||
    ComponentId: string;
 | 
					    ComponentId: string;
 | 
				
			||||||
    DecoId: string;
 | 
					    DecoId: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IClearObstacleCourseRequest {
 | 
				
			||||||
 | 
					    ComponentId: string;
 | 
				
			||||||
 | 
					    Act: "cObst" | "maybesomethingelsewedontknowabout";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -130,7 +130,7 @@ const createLoginResponse = (
 | 
				
			|||||||
        resp.Groups = [];
 | 
					        resp.Groups = [];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (version_compare(buildLabel, "2021.04.13.19.58") >= 0) {
 | 
					    if (version_compare(buildLabel, "2021.04.13.19.58") >= 0) {
 | 
				
			||||||
        resp.DTLS = 99;
 | 
					        resp.DTLS = 0; // bit 0 enables DTLS. if enabled, additional bits can be set, e.g. bit 2 to enable logging. on live, the value is 99.
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (version_compare(buildLabel, "2022.04.29.12.53") >= 0) {
 | 
					    if (version_compare(buildLabel, "2022.04.29.12.53") >= 0) {
 | 
				
			||||||
        resp.ClientType = account.ClientType;
 | 
					        resp.ClientType = account.ClientType;
 | 
				
			||||||
 | 
				
			|||||||
@ -9,13 +9,14 @@ import {
 | 
				
			|||||||
    freeUpSlot,
 | 
					    freeUpSlot,
 | 
				
			||||||
    combineInventoryChanges,
 | 
					    combineInventoryChanges,
 | 
				
			||||||
    addCrewShipRawSalvage,
 | 
					    addCrewShipRawSalvage,
 | 
				
			||||||
    addFusionPoints
 | 
					    addFusionPoints,
 | 
				
			||||||
 | 
					    addCrewShipFusionPoints
 | 
				
			||||||
} from "@/src/services/inventoryService";
 | 
					} from "@/src/services/inventoryService";
 | 
				
			||||||
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
					import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
				
			||||||
import { ExportDojoRecipes } from "warframe-public-export-plus";
 | 
					import { ExportDojoRecipes } from "warframe-public-export-plus";
 | 
				
			||||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
					import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
				
			||||||
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
 | 
					import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
 | 
				
			||||||
import { sendWsBroadcastTo } from "@/src/services/wsService";
 | 
					import { sendWsBroadcastEx } from "@/src/services/wsService";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const sellController: RequestHandler = async (req, res) => {
 | 
					export const sellController: RequestHandler = async (req, res) => {
 | 
				
			||||||
    const payload = JSON.parse(String(req.body)) as ISellRequest;
 | 
					    const payload = JSON.parse(String(req.body)) as ISellRequest;
 | 
				
			||||||
@ -26,6 +27,8 @@ export const sellController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
        requiredFields.add("RegularCredits");
 | 
					        requiredFields.add("RegularCredits");
 | 
				
			||||||
    } else if (payload.SellCurrency == "SC_FusionPoints") {
 | 
					    } else if (payload.SellCurrency == "SC_FusionPoints") {
 | 
				
			||||||
        requiredFields.add("FusionPoints");
 | 
					        requiredFields.add("FusionPoints");
 | 
				
			||||||
 | 
					    } else if (payload.SellCurrency == "SC_CrewShipFusionPoints") {
 | 
				
			||||||
 | 
					        requiredFields.add("CrewShipFusionPoints");
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        requiredFields.add("MiscItems");
 | 
					        requiredFields.add("MiscItems");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -79,6 +82,8 @@ export const sellController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
        inventory.RegularCredits += payload.SellPrice;
 | 
					        inventory.RegularCredits += payload.SellPrice;
 | 
				
			||||||
    } else if (payload.SellCurrency == "SC_FusionPoints") {
 | 
					    } else if (payload.SellCurrency == "SC_FusionPoints") {
 | 
				
			||||||
        addFusionPoints(inventory, payload.SellPrice);
 | 
					        addFusionPoints(inventory, payload.SellPrice);
 | 
				
			||||||
 | 
					    } else if (payload.SellCurrency == "SC_CrewShipFusionPoints") {
 | 
				
			||||||
 | 
					        addCrewShipFusionPoints(inventory, payload.SellPrice);
 | 
				
			||||||
    } else if (payload.SellCurrency == "SC_PrimeBucks") {
 | 
					    } else if (payload.SellCurrency == "SC_PrimeBucks") {
 | 
				
			||||||
        addMiscItems(inventory, [
 | 
					        addMiscItems(inventory, [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@ -295,7 +300,7 @@ export const sellController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
    res.json({
 | 
					    res.json({
 | 
				
			||||||
        inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges"
 | 
					        inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges"
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    sendWsBroadcastTo(accountId, { update_inventory: true });
 | 
					    sendWsBroadcastEx({ update_inventory: true }, accountId, parseInt(String(req.query.wsid)));
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface ISellRequest {
 | 
					interface ISellRequest {
 | 
				
			||||||
@ -330,7 +335,8 @@ interface ISellRequest {
 | 
				
			|||||||
        | "SC_FusionPoints"
 | 
					        | "SC_FusionPoints"
 | 
				
			||||||
        | "SC_DistillPoints"
 | 
					        | "SC_DistillPoints"
 | 
				
			||||||
        | "SC_CrewShipFusionPoints"
 | 
					        | "SC_CrewShipFusionPoints"
 | 
				
			||||||
        | "SC_Resources";
 | 
					        | "SC_Resources"
 | 
				
			||||||
 | 
					        | "somethingelsewemightnotknowabout";
 | 
				
			||||||
    buildLabel: string;
 | 
					    buildLabel: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,20 +1,17 @@
 | 
				
			|||||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
					import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
				
			||||||
import { IShipDecorationsRequest } from "@/src/types/personalRoomsTypes";
 | 
					import { IShipDecorationsRequest, IResetShipDecorationsRequest } from "@/src/types/personalRoomsTypes";
 | 
				
			||||||
import { logger } from "@/src/utils/logger";
 | 
					 | 
				
			||||||
import { RequestHandler } from "express";
 | 
					import { RequestHandler } from "express";
 | 
				
			||||||
import { handleSetShipDecorations } from "@/src/services/shipCustomizationsService";
 | 
					import { handleResetShipDecorations, handleSetShipDecorations } from "@/src/services/shipCustomizationsService";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const shipDecorationsController: RequestHandler = async (req, res) => {
 | 
					export const shipDecorationsController: RequestHandler = async (req, res) => {
 | 
				
			||||||
    const accountId = await getAccountIdForRequest(req);
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
    const shipDecorationsRequest = JSON.parse(req.body as string) as IShipDecorationsRequest;
 | 
					    if (req.query.reset == "1") {
 | 
				
			||||||
 | 
					        const request = JSON.parse(req.body as string) as IResetShipDecorationsRequest;
 | 
				
			||||||
    try {
 | 
					        const response = await handleResetShipDecorations(accountId, request);
 | 
				
			||||||
 | 
					        res.send(response);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        const shipDecorationsRequest = JSON.parse(req.body as string) as IShipDecorationsRequest;
 | 
				
			||||||
        const placedDecoration = await handleSetShipDecorations(accountId, shipDecorationsRequest);
 | 
					        const placedDecoration = await handleSetShipDecorations(accountId, shipDecorationsRequest);
 | 
				
			||||||
        res.send(placedDecoration);
 | 
					        res.send(placedDecoration);
 | 
				
			||||||
    } catch (error: unknown) {
 | 
					 | 
				
			||||||
        if (error instanceof Error) {
 | 
					 | 
				
			||||||
            logger.error(`error in shipDecorationsController: ${error.message}`);
 | 
					 | 
				
			||||||
            res.status(400).json({ error: error.message });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,8 @@
 | 
				
			|||||||
import { RequestHandler } from "express";
 | 
					import { RequestHandler } from "express";
 | 
				
			||||||
import { config } from "@/src/services/configService";
 | 
					import { config, syncConfigWithDatabase } from "@/src/services/configService";
 | 
				
			||||||
import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
 | 
					import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
 | 
				
			||||||
import { saveConfig } from "@/src/services/configWriterService";
 | 
					import { saveConfig } from "@/src/services/configWriterService";
 | 
				
			||||||
import { sendWsBroadcastExcept } from "@/src/services/wsService";
 | 
					import { sendWsBroadcastEx } from "@/src/services/wsService";
 | 
				
			||||||
import { syncConfigWithDatabase } from "@/src/services/configWatcherService";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getConfigController: RequestHandler = async (req, res) => {
 | 
					export const getConfigController: RequestHandler = async (req, res) => {
 | 
				
			||||||
    const account = await getAccountForRequest(req);
 | 
					    const account = await getAccountForRequest(req);
 | 
				
			||||||
@ -26,7 +25,7 @@ export const setConfigController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
            const [obj, idx] = configIdToIndexable(id);
 | 
					            const [obj, idx] = configIdToIndexable(id);
 | 
				
			||||||
            obj[idx] = value;
 | 
					            obj[idx] = value;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        sendWsBroadcastExcept(parseInt(String(req.query.wsid)), { config_reloaded: true });
 | 
					        sendWsBroadcastEx({ config_reloaded: true }, undefined, parseInt(String(req.query.wsid)));
 | 
				
			||||||
        syncConfigWithDatabase();
 | 
					        syncConfigWithDatabase();
 | 
				
			||||||
        await saveConfig();
 | 
					        await saveConfig();
 | 
				
			||||||
        res.end();
 | 
					        res.end();
 | 
				
			||||||
 | 
				
			|||||||
@ -11,6 +11,7 @@ import { GuildMember } from "@/src/models/guildModel";
 | 
				
			|||||||
import { Leaderboard } from "@/src/models/leaderboardModel";
 | 
					import { Leaderboard } from "@/src/models/leaderboardModel";
 | 
				
			||||||
import { deleteGuild } from "@/src/services/guildService";
 | 
					import { deleteGuild } from "@/src/services/guildService";
 | 
				
			||||||
import { Friendship } from "@/src/models/friendModel";
 | 
					import { Friendship } from "@/src/models/friendModel";
 | 
				
			||||||
 | 
					import { sendWsBroadcastTo } from "@/src/services/wsService";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const deleteAccountController: RequestHandler = async (req, res) => {
 | 
					export const deleteAccountController: RequestHandler = async (req, res) => {
 | 
				
			||||||
    const accountId = await getAccountIdForRequest(req);
 | 
					    const accountId = await getAccountIdForRequest(req);
 | 
				
			||||||
@ -36,5 +37,8 @@ export const deleteAccountController: RequestHandler = async (req, res) => {
 | 
				
			|||||||
        Ship.deleteMany({ ShipOwnerId: accountId }),
 | 
					        Ship.deleteMany({ ShipOwnerId: accountId }),
 | 
				
			||||||
        Stats.deleteOne({ accountOwnerId: accountId })
 | 
					        Stats.deleteOne({ accountOwnerId: accountId })
 | 
				
			||||||
    ]);
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    sendWsBroadcastTo(accountId, { logged_out: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.end();
 | 
					    res.end();
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -141,7 +141,7 @@ export const getProfileViewingDataGetController: RequestHandler = async (req, re
 | 
				
			|||||||
                                }
 | 
					                                }
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        } else {
 | 
					                        } else {
 | 
				
			||||||
                            // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
 | 
					                            // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
 | 
				
			||||||
                            combinedStats[arrayName].push(entry as any);
 | 
					                            combinedStats[arrayName].push(entry as any);
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										16
									
								
								src/index.ts
									
									
									
									
									
								
							
							
						
						
									
										16
									
								
								src/index.ts
									
									
									
									
									
								
							@ -1,5 +1,5 @@
 | 
				
			|||||||
// First, init config.
 | 
					// First, init config.
 | 
				
			||||||
import { config, configPath, loadConfig } from "@/src/services/configService";
 | 
					import { config, configPath, loadConfig, syncConfigWithDatabase } from "@/src/services/configService";
 | 
				
			||||||
import fs from "fs";
 | 
					import fs from "fs";
 | 
				
			||||||
try {
 | 
					try {
 | 
				
			||||||
    loadConfig();
 | 
					    loadConfig();
 | 
				
			||||||
@ -18,17 +18,23 @@ logger.info("Starting up...");
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Proceed with normal startup: bring up config watcher service, validate config, connect to MongoDB, and finally start listening for HTTP.
 | 
					// Proceed with normal startup: bring up config watcher service, validate config, connect to MongoDB, and finally start listening for HTTP.
 | 
				
			||||||
import mongoose from "mongoose";
 | 
					import mongoose from "mongoose";
 | 
				
			||||||
 | 
					import path from "path";
 | 
				
			||||||
import { JSONStringify } from "json-with-bigint";
 | 
					import { JSONStringify } from "json-with-bigint";
 | 
				
			||||||
import { startWebServer } from "@/src/services/webService";
 | 
					import { startWebServer } from "@/src/services/webService";
 | 
				
			||||||
 | 
					import { validateConfig } from "@/src/services/configWatcherService";
 | 
				
			||||||
import { syncConfigWithDatabase, validateConfig } from "@/src/services/configWatcherService";
 | 
					 | 
				
			||||||
import { updateWorldStateCollections } from "@/src/services/worldStateService";
 | 
					import { updateWorldStateCollections } from "@/src/services/worldStateService";
 | 
				
			||||||
 | 
					import { repoDir } from "@/src/helpers/pathHelper";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Patch JSON.stringify to work flawlessly with Bigints.
 | 
					JSON.stringify = JSONStringify; // Patch JSON.stringify to work flawlessly with Bigints.
 | 
				
			||||||
JSON.stringify = JSONStringify;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
validateConfig();
 | 
					validateConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fs.readFile(path.join(repoDir, "BUILD_DATE"), "utf-8", (err, data) => {
 | 
				
			||||||
 | 
					    if (!err) {
 | 
				
			||||||
 | 
					        logger.info(`Docker image was built on ${data.trim()}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mongoose
 | 
					mongoose
 | 
				
			||||||
    .connect(config.mongodbUrl)
 | 
					    .connect(config.mongodbUrl)
 | 
				
			||||||
    .then(() => {
 | 
					    .then(() => {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,16 +1,11 @@
 | 
				
			|||||||
import { NextFunction, Request, Response } from "express";
 | 
					import { NextFunction, Request, Response } from "express";
 | 
				
			||||||
import { logger } from "@/src/utils/logger";
 | 
					import { logError } from "@/src/utils/logger";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const errorHandler = (err: Error, req: Request, res: Response, _next: NextFunction): void => {
 | 
					export const errorHandler = (err: Error, req: Request, res: Response, _next: NextFunction): void => {
 | 
				
			||||||
    if (err.message == "Invalid accountId-nonce pair") {
 | 
					    if (err.message == "Invalid accountId-nonce pair") {
 | 
				
			||||||
        res.status(400).send("Log-in expired");
 | 
					        res.status(400).send("Log-in expired");
 | 
				
			||||||
    } else if (err.stack) {
 | 
					 | 
				
			||||||
        const stackArr = err.stack.split("\n");
 | 
					 | 
				
			||||||
        stackArr[0] += ` while processing ${req.path} request`;
 | 
					 | 
				
			||||||
        logger.error(stackArr.join("\n"));
 | 
					 | 
				
			||||||
        res.status(500).end();
 | 
					 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        logger.error(`uncaught error while processing ${req.path} request: ${err.message}`);
 | 
					        logError(err, `processing ${req.path} request`);
 | 
				
			||||||
        res.status(500).end();
 | 
					        res.status(500).end();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -150,7 +150,7 @@ messageSchema.virtual("messageId").get(function (this: IMessageDatabase) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
messageSchema.set("toJSON", {
 | 
					messageSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_document, returnedObject) {
 | 
					    transform(_document, returnedObject: Record<string, any>) {
 | 
				
			||||||
        const messageDatabase = returnedObject as IMessageDatabase;
 | 
					        const messageDatabase = returnedObject as IMessageDatabase;
 | 
				
			||||||
        const messageClient = returnedObject as IMessageClient;
 | 
					        const messageClient = returnedObject as IMessageClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -121,7 +121,7 @@ import {
 | 
				
			|||||||
export const typeCountSchema = new Schema<ITypeCount>({ ItemType: String, ItemCount: Number }, { _id: false });
 | 
					export const typeCountSchema = new Schema<ITypeCount>({ ItemType: String, ItemCount: Number }, { _id: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
typeCountSchema.set("toJSON", {
 | 
					typeCountSchema.set("toJSON", {
 | 
				
			||||||
    transform(_doc, obj) {
 | 
					    transform(_doc, obj: Record<string, any>) {
 | 
				
			||||||
        if (obj.ItemCount > 2147483647) {
 | 
					        if (obj.ItemCount > 2147483647) {
 | 
				
			||||||
            obj.ItemCount = 2147483647;
 | 
					            obj.ItemCount = 2147483647;
 | 
				
			||||||
        } else if (obj.ItemCount < -2147483648) {
 | 
					        } else if (obj.ItemCount < -2147483648) {
 | 
				
			||||||
@ -189,7 +189,7 @@ operatorConfigSchema.virtual("ItemId").get(function () {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
operatorConfigSchema.set("toJSON", {
 | 
					operatorConfigSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_document, returnedObject) {
 | 
					    transform(_document, returnedObject: Record<string, any>) {
 | 
				
			||||||
        delete returnedObject._id;
 | 
					        delete returnedObject._id;
 | 
				
			||||||
        delete returnedObject.__v;
 | 
					        delete returnedObject.__v;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -226,7 +226,7 @@ const ItemConfigSchema = new Schema<IItemConfig>(
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ItemConfigSchema.set("toJSON", {
 | 
					ItemConfigSchema.set("toJSON", {
 | 
				
			||||||
    transform(_document, returnedObject) {
 | 
					    transform(_document, returnedObject: Record<string, any>) {
 | 
				
			||||||
        delete returnedObject.__v;
 | 
					        delete returnedObject.__v;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@ -261,7 +261,7 @@ RawUpgrades.virtual("LastAdded").get(function () {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
RawUpgrades.set("toJSON", {
 | 
					RawUpgrades.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_document, returnedObject) {
 | 
					    transform(_document, returnedObject: Record<string, any>) {
 | 
				
			||||||
        delete returnedObject._id;
 | 
					        delete returnedObject._id;
 | 
				
			||||||
        delete returnedObject.__v;
 | 
					        delete returnedObject.__v;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -282,7 +282,7 @@ upgradeSchema.virtual("ItemId").get(function () {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
upgradeSchema.set("toJSON", {
 | 
					upgradeSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_document, returnedObject) {
 | 
					    transform(_document, returnedObject: Record<string, any>) {
 | 
				
			||||||
        delete returnedObject._id;
 | 
					        delete returnedObject._id;
 | 
				
			||||||
        delete returnedObject.__v;
 | 
					        delete returnedObject.__v;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -325,7 +325,7 @@ const crewMemberSchema = new Schema<ICrewMemberDatabase>(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
crewMemberSchema.set("toJSON", {
 | 
					crewMemberSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_doc, obj) {
 | 
					    transform(_doc, obj: Record<string, any>) {
 | 
				
			||||||
        const db = obj as ICrewMemberDatabase;
 | 
					        const db = obj as ICrewMemberDatabase;
 | 
				
			||||||
        const client = obj as ICrewMemberClient;
 | 
					        const client = obj as ICrewMemberClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -353,7 +353,7 @@ const FlavourItemSchema = new Schema(
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
FlavourItemSchema.set("toJSON", {
 | 
					FlavourItemSchema.set("toJSON", {
 | 
				
			||||||
    transform(_document, returnedObject) {
 | 
					    transform(_document, returnedObject: Record<string, any>) {
 | 
				
			||||||
        delete returnedObject._id;
 | 
					        delete returnedObject._id;
 | 
				
			||||||
        delete returnedObject.__v;
 | 
					        delete returnedObject.__v;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -367,7 +367,7 @@ FlavourItemSchema.set("toJSON", {
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
MailboxSchema.set("toJSON", {
 | 
					MailboxSchema.set("toJSON", {
 | 
				
			||||||
    transform(_document, returnedObject) {
 | 
					    transform(_document, returnedObject: Record<string, any>) {
 | 
				
			||||||
        const mailboxDatabase = returnedObject as HydratedDocument<IMailboxDatabase, { __v?: number }>;
 | 
					        const mailboxDatabase = returnedObject as HydratedDocument<IMailboxDatabase, { __v?: number }>;
 | 
				
			||||||
        delete mailboxDatabase.__v;
 | 
					        delete mailboxDatabase.__v;
 | 
				
			||||||
        (returnedObject as IMailboxClient).LastInboxId = toOid(mailboxDatabase.LastInboxId);
 | 
					        (returnedObject as IMailboxClient).LastInboxId = toOid(mailboxDatabase.LastInboxId);
 | 
				
			||||||
@ -386,7 +386,7 @@ const DuviriInfoSchema = new Schema<IDuviriInfo>(
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
DuviriInfoSchema.set("toJSON", {
 | 
					DuviriInfoSchema.set("toJSON", {
 | 
				
			||||||
    transform(_document, returnedObject) {
 | 
					    transform(_document, returnedObject: Record<string, any>) {
 | 
				
			||||||
        delete returnedObject.__v;
 | 
					        delete returnedObject.__v;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@ -416,7 +416,7 @@ const droneSchema = new Schema<IDroneDatabase>(
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
droneSchema.set("toJSON", {
 | 
					droneSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_document, obj) {
 | 
					    transform(_document, obj: Record<string, any>) {
 | 
				
			||||||
        const client = obj as IDroneClient;
 | 
					        const client = obj as IDroneClient;
 | 
				
			||||||
        const db = obj as IDroneDatabase;
 | 
					        const db = obj as IDroneDatabase;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -457,7 +457,7 @@ const personalGoalProgressSchema = new Schema<IPersonalGoalProgressDatabase>(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
personalGoalProgressSchema.set("toJSON", {
 | 
					personalGoalProgressSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_doc, obj) {
 | 
					    transform(_doc, obj: Record<string, any>) {
 | 
				
			||||||
        const db = obj as IPersonalGoalProgressDatabase;
 | 
					        const db = obj as IPersonalGoalProgressDatabase;
 | 
				
			||||||
        const client = obj as IPersonalGoalProgressClient;
 | 
					        const client = obj as IPersonalGoalProgressClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -502,7 +502,7 @@ StepSequencersSchema.virtual("ItemId").get(function () {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
StepSequencersSchema.set("toJSON", {
 | 
					StepSequencersSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_document, returnedObject) {
 | 
					    transform(_document, returnedObject: Record<string, any>) {
 | 
				
			||||||
        delete returnedObject._id;
 | 
					        delete returnedObject._id;
 | 
				
			||||||
        delete returnedObject.__v;
 | 
					        delete returnedObject.__v;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -516,7 +516,7 @@ const kubrowPetEggSchema = new Schema<IKubrowPetEggDatabase>(
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
kubrowPetEggSchema.set("toJSON", {
 | 
					kubrowPetEggSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_document, obj) {
 | 
					    transform(_document, obj: Record<string, any>) {
 | 
				
			||||||
        const client = obj as IKubrowPetEggClient;
 | 
					        const client = obj as IKubrowPetEggClient;
 | 
				
			||||||
        const db = obj as IKubrowPetEggDatabase;
 | 
					        const db = obj as IKubrowPetEggDatabase;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -586,7 +586,7 @@ personalTechProjectSchema.virtual("ItemId").get(function () {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
personalTechProjectSchema.set("toJSON", {
 | 
					personalTechProjectSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_doc, ret, _options) {
 | 
					    transform(_doc, ret: Record<string, any>) {
 | 
				
			||||||
        delete ret._id;
 | 
					        delete ret._id;
 | 
				
			||||||
        delete ret.__v;
 | 
					        delete ret.__v;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -687,7 +687,7 @@ const questKeysSchema = new Schema<IQuestKeyDatabase>(
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
questKeysSchema.set("toJSON", {
 | 
					questKeysSchema.set("toJSON", {
 | 
				
			||||||
    transform(_doc, ret, _options) {
 | 
					    transform(_doc, ret: Record<string, any>) {
 | 
				
			||||||
        const questKeysDatabase = ret as IQuestKeyDatabase;
 | 
					        const questKeysDatabase = ret as IQuestKeyDatabase;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (questKeysDatabase.CompletionDate) {
 | 
					        if (questKeysDatabase.CompletionDate) {
 | 
				
			||||||
@ -709,7 +709,7 @@ const invasionProgressSchema = new Schema<IInvasionProgressDatabase>(
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
invasionProgressSchema.set("toJSON", {
 | 
					invasionProgressSchema.set("toJSON", {
 | 
				
			||||||
    transform(_doc, obj) {
 | 
					    transform(_doc, obj: Record<string, any>) {
 | 
				
			||||||
        const db = obj as IInvasionProgressDatabase;
 | 
					        const db = obj as IInvasionProgressDatabase;
 | 
				
			||||||
        const client = obj as IInvasionProgressClient;
 | 
					        const client = obj as IInvasionProgressClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -748,7 +748,7 @@ weaponSkinsSchema.virtual("ItemId").get(function () {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
weaponSkinsSchema.set("toJSON", {
 | 
					weaponSkinsSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_doc, ret, _options) {
 | 
					    transform(_doc, ret: Record<string, any>) {
 | 
				
			||||||
        delete ret._id;
 | 
					        delete ret._id;
 | 
				
			||||||
        delete ret.__v;
 | 
					        delete ret.__v;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -772,7 +772,7 @@ const periodicMissionCompletionsSchema = new Schema<IPeriodicMissionCompletionDa
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
periodicMissionCompletionsSchema.set("toJSON", {
 | 
					periodicMissionCompletionsSchema.set("toJSON", {
 | 
				
			||||||
    transform(_doc, ret, _options) {
 | 
					    transform(_doc, ret: Record<string, any>) {
 | 
				
			||||||
        const periodicMissionCompletionDatabase = ret as IPeriodicMissionCompletionDatabase;
 | 
					        const periodicMissionCompletionDatabase = ret as IPeriodicMissionCompletionDatabase;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        (periodicMissionCompletionDatabase as unknown as IPeriodicMissionCompletionResponse).date = toMongoDate(
 | 
					        (periodicMissionCompletionDatabase as unknown as IPeriodicMissionCompletionResponse).date = toMongoDate(
 | 
				
			||||||
@ -849,7 +849,7 @@ const endlessXpProgressSchema = new Schema<IEndlessXpProgressDatabase>(
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
endlessXpProgressSchema.set("toJSON", {
 | 
					endlessXpProgressSchema.set("toJSON", {
 | 
				
			||||||
    transform(_doc, ret) {
 | 
					    transform(_doc, ret: Record<string, any>) {
 | 
				
			||||||
        const db = ret as IEndlessXpProgressDatabase;
 | 
					        const db = ret as IEndlessXpProgressDatabase;
 | 
				
			||||||
        const client = ret as IEndlessXpProgressClient;
 | 
					        const client = ret as IEndlessXpProgressClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -898,7 +898,7 @@ const crewShipMemberSchema = new Schema<ICrewShipMemberDatabase>(
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
crewShipMemberSchema.set("toJSON", {
 | 
					crewShipMemberSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_doc, obj) {
 | 
					    transform(_doc, obj: Record<string, any>) {
 | 
				
			||||||
        const db = obj as ICrewShipMemberDatabase;
 | 
					        const db = obj as ICrewShipMemberDatabase;
 | 
				
			||||||
        const client = obj as ICrewShipMemberClient;
 | 
					        const client = obj as ICrewShipMemberClient;
 | 
				
			||||||
        if (db.ItemId) {
 | 
					        if (db.ItemId) {
 | 
				
			||||||
@ -951,7 +951,7 @@ const dialogueSchema = new Schema<IDialogueDatabase>(
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
dialogueSchema.set("toJSON", {
 | 
					dialogueSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_doc, ret) {
 | 
					    transform(_doc, ret: Record<string, any>) {
 | 
				
			||||||
        const db = ret as IDialogueDatabase;
 | 
					        const db = ret as IDialogueDatabase;
 | 
				
			||||||
        const client = ret as IDialogueClient;
 | 
					        const client = ret as IDialogueClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -997,7 +997,7 @@ const kubrowPetPrintSchema = new Schema<IKubrowPetPrintDatabase>({
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
kubrowPetPrintSchema.set("toJSON", {
 | 
					kubrowPetPrintSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_doc, obj) {
 | 
					    transform(_doc, obj: Record<string, any>) {
 | 
				
			||||||
        const db = obj as IKubrowPetPrintDatabase;
 | 
					        const db = obj as IKubrowPetPrintDatabase;
 | 
				
			||||||
        const client = obj as IKubrowPetPrintClient;
 | 
					        const client = obj as IKubrowPetPrintClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1025,7 +1025,7 @@ const detailsSchema = new Schema<IKubrowPetDetailsDatabase>(
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
detailsSchema.set("toJSON", {
 | 
					detailsSchema.set("toJSON", {
 | 
				
			||||||
    transform(_doc, returnedObject) {
 | 
					    transform(_doc, returnedObject: Record<string, any>) {
 | 
				
			||||||
        delete returnedObject.__v;
 | 
					        delete returnedObject.__v;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const db = returnedObject as IKubrowPetDetailsDatabase;
 | 
					        const db = returnedObject as IKubrowPetDetailsDatabase;
 | 
				
			||||||
@ -1081,7 +1081,7 @@ EquipmentSchema.virtual("ItemId").get(function () {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
EquipmentSchema.set("toJSON", {
 | 
					EquipmentSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_document, returnedObject) {
 | 
					    transform(_document, returnedObject: Record<string, any>) {
 | 
				
			||||||
        delete returnedObject._id;
 | 
					        delete returnedObject._id;
 | 
				
			||||||
        delete returnedObject.__v;
 | 
					        delete returnedObject.__v;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1132,7 +1132,7 @@ pendingRecipeSchema.virtual("ItemId").get(function () {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
pendingRecipeSchema.set("toJSON", {
 | 
					pendingRecipeSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_document, returnedObject) {
 | 
					    transform(_document, returnedObject: Record<string, any>) {
 | 
				
			||||||
        delete returnedObject._id;
 | 
					        delete returnedObject._id;
 | 
				
			||||||
        delete returnedObject.__v;
 | 
					        delete returnedObject.__v;
 | 
				
			||||||
        delete returnedObject.LongGuns;
 | 
					        delete returnedObject.LongGuns;
 | 
				
			||||||
@ -1170,7 +1170,7 @@ const infestedFoundrySchema = new Schema<IInfestedFoundryDatabase>(
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
infestedFoundrySchema.set("toJSON", {
 | 
					infestedFoundrySchema.set("toJSON", {
 | 
				
			||||||
    transform(_doc, ret, _options) {
 | 
					    transform(_doc, ret: Record<string, any>) {
 | 
				
			||||||
        if (ret.AbilityOverrideUnlockCooldown) {
 | 
					        if (ret.AbilityOverrideUnlockCooldown) {
 | 
				
			||||||
            // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
 | 
					            // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
 | 
				
			||||||
            ret.AbilityOverrideUnlockCooldown = toMongoDate(ret.AbilityOverrideUnlockCooldown);
 | 
					            ret.AbilityOverrideUnlockCooldown = toMongoDate(ret.AbilityOverrideUnlockCooldown);
 | 
				
			||||||
@ -1243,7 +1243,7 @@ const vendorPurchaseHistoryEntrySchema = new Schema<IVendorPurchaseHistoryEntryD
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
vendorPurchaseHistoryEntrySchema.set("toJSON", {
 | 
					vendorPurchaseHistoryEntrySchema.set("toJSON", {
 | 
				
			||||||
    transform(_doc, obj) {
 | 
					    transform(_doc, obj: Record<string, any>) {
 | 
				
			||||||
        const db = obj as IVendorPurchaseHistoryEntryDatabase;
 | 
					        const db = obj as IVendorPurchaseHistoryEntryDatabase;
 | 
				
			||||||
        const client = obj as IVendorPurchaseHistoryEntryClient;
 | 
					        const client = obj as IVendorPurchaseHistoryEntryClient;
 | 
				
			||||||
        client.Expiry = toMongoDate(db.Expiry);
 | 
					        client.Expiry = toMongoDate(db.Expiry);
 | 
				
			||||||
@ -1286,7 +1286,7 @@ const pendingCouponSchema = new Schema<IPendingCouponDatabase>(
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pendingCouponSchema.set("toJSON", {
 | 
					pendingCouponSchema.set("toJSON", {
 | 
				
			||||||
    transform(_doc, ret, _options) {
 | 
					    transform(_doc, ret: Record<string, any>) {
 | 
				
			||||||
        (ret as IPendingCouponClient).Expiry = toMongoDate((ret as IPendingCouponDatabase).Expiry);
 | 
					        (ret as IPendingCouponClient).Expiry = toMongoDate((ret as IPendingCouponDatabase).Expiry);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@ -1353,7 +1353,7 @@ const nemesisSchema = new Schema<INemesisDatabase>(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
nemesisSchema.set("toJSON", {
 | 
					nemesisSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_doc, obj) {
 | 
					    transform(_doc, obj: Record<string, any>) {
 | 
				
			||||||
        const db = obj as INemesisDatabase;
 | 
					        const db = obj as INemesisDatabase;
 | 
				
			||||||
        const client = obj as INemesisClient;
 | 
					        const client = obj as INemesisClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1383,7 +1383,7 @@ const lastSortieRewardSchema = new Schema<ILastSortieRewardDatabase>(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
lastSortieRewardSchema.set("toJSON", {
 | 
					lastSortieRewardSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_doc, obj) {
 | 
					    transform(_doc, obj: Record<string, any>) {
 | 
				
			||||||
        const db = obj as ILastSortieRewardDatabase;
 | 
					        const db = obj as ILastSortieRewardDatabase;
 | 
				
			||||||
        const client = obj as ILastSortieRewardClient;
 | 
					        const client = obj as ILastSortieRewardClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1437,6 +1437,8 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
 | 
				
			|||||||
        PremiumCreditsFree: { type: Number, default: 0 },
 | 
					        PremiumCreditsFree: { type: Number, default: 0 },
 | 
				
			||||||
        //Endo
 | 
					        //Endo
 | 
				
			||||||
        FusionPoints: { type: Number, default: 0 },
 | 
					        FusionPoints: { type: Number, default: 0 },
 | 
				
			||||||
 | 
					        //Dirac
 | 
				
			||||||
 | 
					        CrewShipFusionPoints: { type: Number, default: 0 },
 | 
				
			||||||
        //Regal Aya
 | 
					        //Regal Aya
 | 
				
			||||||
        PrimeTokens: { type: Number, default: 0 },
 | 
					        PrimeTokens: { type: Number, default: 0 },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1790,7 +1792,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
inventorySchema.set("toJSON", {
 | 
					inventorySchema.set("toJSON", {
 | 
				
			||||||
    transform(_document, returnedObject) {
 | 
					    transform(_document, returnedObject: Record<string, any>) {
 | 
				
			||||||
        delete returnedObject._id;
 | 
					        delete returnedObject._id;
 | 
				
			||||||
        delete returnedObject.__v;
 | 
					        delete returnedObject.__v;
 | 
				
			||||||
        delete returnedObject.accountOwnerId;
 | 
					        delete returnedObject.accountOwnerId;
 | 
				
			||||||
 | 
				
			|||||||
@ -49,7 +49,7 @@ loadoutConfigSchema.virtual("ItemId").get(function () {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
loadoutConfigSchema.set("toJSON", {
 | 
					loadoutConfigSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_doc, ret, _options) {
 | 
					    transform(_doc, ret: Record<string, any>) {
 | 
				
			||||||
        delete ret._id;
 | 
					        delete ret._id;
 | 
				
			||||||
        delete ret.__v;
 | 
					        delete ret.__v;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -71,7 +71,7 @@ export const loadoutSchema = new Schema<ILoadoutDatabase, loadoutModelType>({
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
loadoutSchema.set("toJSON", {
 | 
					loadoutSchema.set("toJSON", {
 | 
				
			||||||
    transform(_doc, ret, _options) {
 | 
					    transform(_doc, ret: Record<string, any>) {
 | 
				
			||||||
        delete ret._id;
 | 
					        delete ret._id;
 | 
				
			||||||
        delete ret.__v;
 | 
					        delete ret.__v;
 | 
				
			||||||
        delete ret.loadoutOwnerId;
 | 
					        delete ret.loadoutOwnerId;
 | 
				
			||||||
 | 
				
			|||||||
@ -32,7 +32,7 @@ const databaseAccountSchema = new Schema<IDatabaseAccountJson>(
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
databaseAccountSchema.set("toJSON", {
 | 
					databaseAccountSchema.set("toJSON", {
 | 
				
			||||||
    transform(_document, returnedObject) {
 | 
					    transform(_document, returnedObject: Record<string, any>) {
 | 
				
			||||||
        delete returnedObject._id;
 | 
					        delete returnedObject._id;
 | 
				
			||||||
        delete returnedObject.__v;
 | 
					        delete returnedObject.__v;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
				
			|||||||
@ -55,7 +55,7 @@ placedDecosSchema.virtual("id").get(function (this: IPlacedDecosDatabase) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
placedDecosSchema.set("toJSON", {
 | 
					placedDecosSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_document, returnedObject) {
 | 
					    transform(_document, returnedObject: Record<string, any>) {
 | 
				
			||||||
        delete returnedObject._id;
 | 
					        delete returnedObject._id;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@ -78,7 +78,7 @@ const favouriteLoadoutSchema = new Schema<IFavouriteLoadoutDatabase>(
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
favouriteLoadoutSchema.set("toJSON", {
 | 
					favouriteLoadoutSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_document, returnedObject) {
 | 
					    transform(_document, returnedObject: Record<string, any>) {
 | 
				
			||||||
        // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
 | 
					        // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
 | 
				
			||||||
        returnedObject.LoadoutId = toOid(returnedObject.LoadoutId);
 | 
					        returnedObject.LoadoutId = toOid(returnedObject.LoadoutId);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -95,7 +95,7 @@ const plantSchema = new Schema<IPlantDatabase>(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
plantSchema.set("toJSON", {
 | 
					plantSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_doc, obj) {
 | 
					    transform(_doc, obj: Record<string, any>) {
 | 
				
			||||||
        const client = obj as IPlantClient;
 | 
					        const client = obj as IPlantClient;
 | 
				
			||||||
        const db = obj as IPlantDatabase;
 | 
					        const db = obj as IPlantDatabase;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -122,7 +122,9 @@ const apartmentSchema = new Schema<IApartmentDatabase>(
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        Rooms: [roomSchema],
 | 
					        Rooms: [roomSchema],
 | 
				
			||||||
        FavouriteLoadouts: [favouriteLoadoutSchema],
 | 
					        FavouriteLoadouts: [favouriteLoadoutSchema],
 | 
				
			||||||
        Gardening: gardeningSchema
 | 
					        Gardening: gardeningSchema,
 | 
				
			||||||
 | 
					        VideoWallBackdrop: String,
 | 
				
			||||||
 | 
					        Soundscape: String
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    { _id: false }
 | 
					    { _id: false }
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
@ -156,7 +158,7 @@ const orbiterSchema = new Schema<IOrbiterDatabase>(
 | 
				
			|||||||
);
 | 
					);
 | 
				
			||||||
orbiterSchema.set("toJSON", {
 | 
					orbiterSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_doc, obj) {
 | 
					    transform(_doc, obj: Record<string, any>) {
 | 
				
			||||||
        const db = obj as IOrbiterDatabase;
 | 
					        const db = obj as IOrbiterDatabase;
 | 
				
			||||||
        const client = obj as IOrbiterClient;
 | 
					        const client = obj as IOrbiterClient;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -22,7 +22,7 @@ shipSchema.virtual("ItemId").get(function () {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
shipSchema.set("toJSON", {
 | 
					shipSchema.set("toJSON", {
 | 
				
			||||||
    virtuals: true,
 | 
					    virtuals: true,
 | 
				
			||||||
    transform(_document, returnedObject) {
 | 
					    transform(_document, returnedObject: Record<string, any>) {
 | 
				
			||||||
        const shipResponse = returnedObject as IShipInventory;
 | 
					        const shipResponse = returnedObject as IShipInventory;
 | 
				
			||||||
        const shipDatabase = returnedObject as IShipDatabase;
 | 
					        const shipDatabase = returnedObject as IShipDatabase;
 | 
				
			||||||
        delete returnedObject._id;
 | 
					        delete returnedObject._id;
 | 
				
			||||||
 | 
				
			|||||||
@ -101,7 +101,7 @@ const statsSchema = new Schema<IStatsDatabase>({
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
statsSchema.set("toJSON", {
 | 
					statsSchema.set("toJSON", {
 | 
				
			||||||
    transform(_document, returnedObject) {
 | 
					    transform(_document, returnedObject: Record<string, any>) {
 | 
				
			||||||
        delete returnedObject._id;
 | 
					        delete returnedObject._id;
 | 
				
			||||||
        delete returnedObject.__v;
 | 
					        delete returnedObject.__v;
 | 
				
			||||||
        delete returnedObject.accountOwnerId;
 | 
					        delete returnedObject.accountOwnerId;
 | 
				
			||||||
 | 
				
			|||||||
@ -10,6 +10,7 @@ import { addPendingFriendController } from "@/src/controllers/api/addPendingFrie
 | 
				
			|||||||
import { addToAllianceController } from "@/src/controllers/api/addToAllianceController";
 | 
					import { addToAllianceController } from "@/src/controllers/api/addToAllianceController";
 | 
				
			||||||
import { addToGuildController } from "@/src/controllers/api/addToGuildController";
 | 
					import { addToGuildController } from "@/src/controllers/api/addToGuildController";
 | 
				
			||||||
import { adoptPetController } from "@/src/controllers/api/adoptPetController";
 | 
					import { adoptPetController } from "@/src/controllers/api/adoptPetController";
 | 
				
			||||||
 | 
					import { apartmentController } from "@/src/controllers/api/apartmentController";
 | 
				
			||||||
import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController";
 | 
					import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController";
 | 
				
			||||||
import { archonFusionController } from "@/src/controllers/api/archonFusionController";
 | 
					import { archonFusionController } from "@/src/controllers/api/archonFusionController";
 | 
				
			||||||
import { artifactsController } from "@/src/controllers/api/artifactsController";
 | 
					import { artifactsController } from "@/src/controllers/api/artifactsController";
 | 
				
			||||||
@ -168,6 +169,7 @@ const apiRouter = express.Router();
 | 
				
			|||||||
// get
 | 
					// get
 | 
				
			||||||
apiRouter.get("/abandonLibraryDailyTask.php", abandonLibraryDailyTaskController);
 | 
					apiRouter.get("/abandonLibraryDailyTask.php", abandonLibraryDailyTaskController);
 | 
				
			||||||
apiRouter.get("/abortDojoComponentDestruction.php", abortDojoComponentDestructionController);
 | 
					apiRouter.get("/abortDojoComponentDestruction.php", abortDojoComponentDestructionController);
 | 
				
			||||||
 | 
					apiRouter.get("/apartment.php", apartmentController);
 | 
				
			||||||
apiRouter.get("/cancelGuildAdvertisement.php", cancelGuildAdvertisementController);
 | 
					apiRouter.get("/cancelGuildAdvertisement.php", cancelGuildAdvertisementController);
 | 
				
			||||||
apiRouter.get("/changeDojoRoot.php", changeDojoRootController);
 | 
					apiRouter.get("/changeDojoRoot.php", changeDojoRootController);
 | 
				
			||||||
apiRouter.get("/changeGuildRank.php", changeGuildRankController);
 | 
					apiRouter.get("/changeGuildRank.php", changeGuildRankController);
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@ import fs from "fs";
 | 
				
			|||||||
import path from "path";
 | 
					import path from "path";
 | 
				
			||||||
import { repoDir } from "@/src/helpers/pathHelper";
 | 
					import { repoDir } from "@/src/helpers/pathHelper";
 | 
				
			||||||
import { args } from "@/src/helpers/commandLineArguments";
 | 
					import { args } from "@/src/helpers/commandLineArguments";
 | 
				
			||||||
 | 
					import { Inbox } from "@/src/models/inboxModel";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IConfig {
 | 
					export interface IConfig {
 | 
				
			||||||
    mongodbUrl: string;
 | 
					    mongodbUrl: string;
 | 
				
			||||||
@ -113,9 +114,26 @@ export const loadConfig = (): void => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // Set all values to undefined now so if the new config.json omits some fields that were previously present, it's correct in-memory.
 | 
					    // Set all values to undefined now so if the new config.json omits some fields that were previously present, it's correct in-memory.
 | 
				
			||||||
    for (const key of Object.keys(config)) {
 | 
					    for (const key of Object.keys(config)) {
 | 
				
			||||||
        // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
 | 
					        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
 | 
				
			||||||
        (config as any)[key] = undefined;
 | 
					        (config as any)[key] = undefined;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Object.assign(config, newConfig);
 | 
					    Object.assign(config, newConfig);
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const syncConfigWithDatabase = (): void => {
 | 
				
			||||||
 | 
					    // Event messages are deleted after endDate. Since we don't use beginDate/endDate and instead have config toggles, we need to delete the messages once those bools are false.
 | 
				
			||||||
 | 
					    // Also, for some reason, I can't just do `Inbox.deleteMany(...)`; - it needs this whole circus.
 | 
				
			||||||
 | 
					    if (!config.worldState?.creditBoost) {
 | 
				
			||||||
 | 
					        void Inbox.deleteMany({ globaUpgradeId: "5b23106f283a555109666672" }).then(() => {});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!config.worldState?.affinityBoost) {
 | 
				
			||||||
 | 
					        void Inbox.deleteMany({ globaUpgradeId: "5b23106f283a555109666673" }).then(() => {});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!config.worldState?.resourceBoost) {
 | 
				
			||||||
 | 
					        void Inbox.deleteMany({ globaUpgradeId: "5b23106f283a555109666674" }).then(() => {});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!config.worldState?.galleonOfGhouls) {
 | 
				
			||||||
 | 
					        void Inbox.deleteMany({ goalTag: "GalleonRobbery" }).then(() => {});
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -1,10 +1,9 @@
 | 
				
			|||||||
import chokidar from "chokidar";
 | 
					import chokidar from "chokidar";
 | 
				
			||||||
import { logger } from "@/src/utils/logger";
 | 
					import { logger } from "@/src/utils/logger";
 | 
				
			||||||
import { config, configPath, loadConfig } from "@/src/services/configService";
 | 
					import { config, configPath, loadConfig, syncConfigWithDatabase } from "@/src/services/configService";
 | 
				
			||||||
import { saveConfig, shouldReloadConfig } from "@/src/services/configWriterService";
 | 
					import { saveConfig, shouldReloadConfig } from "@/src/services/configWriterService";
 | 
				
			||||||
import { getWebPorts, startWebServer, stopWebServer } from "@/src/services/webService";
 | 
					import { getWebPorts, startWebServer, stopWebServer } from "@/src/services/webService";
 | 
				
			||||||
import { sendWsBroadcast } from "@/src/services/wsService";
 | 
					import { sendWsBroadcast } from "@/src/services/wsService";
 | 
				
			||||||
import { Inbox } from "@/src/models/inboxModel";
 | 
					 | 
				
			||||||
import varzia from "@/static/fixed_responses/worldState/varzia.json";
 | 
					import varzia from "@/static/fixed_responses/worldState/varzia.json";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
chokidar.watch(configPath).on("change", () => {
 | 
					chokidar.watch(configPath).on("change", () => {
 | 
				
			||||||
@ -68,20 +67,3 @@ export const validateConfig = (): void => {
 | 
				
			|||||||
        void saveConfig();
 | 
					        void saveConfig();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					 | 
				
			||||||
export const syncConfigWithDatabase = (): void => {
 | 
					 | 
				
			||||||
    // Event messages are deleted after endDate. Since we don't use beginDate/endDate and instead have config toggles, we need to delete the messages once those bools are false.
 | 
					 | 
				
			||||||
    // Also, for some reason, I can't just do `Inbox.deleteMany(...)`; - it needs this whole circus.
 | 
					 | 
				
			||||||
    if (!config.worldState?.creditBoost) {
 | 
					 | 
				
			||||||
        void Inbox.deleteMany({ globaUpgradeId: "5b23106f283a555109666672" }).then(() => {});
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (!config.worldState?.affinityBoost) {
 | 
					 | 
				
			||||||
        void Inbox.deleteMany({ globaUpgradeId: "5b23106f283a555109666673" }).then(() => {});
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (!config.worldState?.resourceBoost) {
 | 
					 | 
				
			||||||
        void Inbox.deleteMany({ globaUpgradeId: "5b23106f283a555109666674" }).then(() => {});
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (!config.worldState?.galleonOfGhouls) {
 | 
					 | 
				
			||||||
        void Inbox.deleteMany({ goalTag: "GalleonRobbery" }).then(() => {});
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -13,6 +13,7 @@ import {
 | 
				
			|||||||
    IDojoComponentDatabase,
 | 
					    IDojoComponentDatabase,
 | 
				
			||||||
    IDojoContributable,
 | 
					    IDojoContributable,
 | 
				
			||||||
    IDojoDecoClient,
 | 
					    IDojoDecoClient,
 | 
				
			||||||
 | 
					    IDojoDecoDatabase,
 | 
				
			||||||
    IGuildClient,
 | 
					    IGuildClient,
 | 
				
			||||||
    IGuildMemberClient,
 | 
					    IGuildMemberClient,
 | 
				
			||||||
    IGuildMemberDatabase,
 | 
					    IGuildMemberDatabase,
 | 
				
			||||||
@ -309,7 +310,7 @@ export const removeDojoRoom = async (
 | 
				
			|||||||
        guild.DojoEnergy -= meta.energy;
 | 
					        guild.DojoEnergy -= meta.energy;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    moveResourcesToVault(guild, component);
 | 
					    moveResourcesToVault(guild, component);
 | 
				
			||||||
    component.Decos?.forEach(deco => moveResourcesToVault(guild, deco));
 | 
					    component.Decos?.forEach(deco => refundDojoDeco(guild, component, deco));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (guild.RoomChanges) {
 | 
					    if (guild.RoomChanges) {
 | 
				
			||||||
        const index = guild.RoomChanges.findIndex(x => x.componentId.equals(component._id));
 | 
					        const index = guild.RoomChanges.findIndex(x => x.componentId.equals(component._id));
 | 
				
			||||||
@ -344,6 +345,14 @@ export const removeDojoDeco = (
 | 
				
			|||||||
        component.Decos!.findIndex(x => x._id.equals(decoId)),
 | 
					        component.Decos!.findIndex(x => x._id.equals(decoId)),
 | 
				
			||||||
        1
 | 
					        1
 | 
				
			||||||
    )[0];
 | 
					    )[0];
 | 
				
			||||||
 | 
					    refundDojoDeco(guild, component, deco);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const refundDojoDeco = (
 | 
				
			||||||
 | 
					    guild: TGuildDatabaseDocument,
 | 
				
			||||||
 | 
					    component: IDojoComponentDatabase,
 | 
				
			||||||
 | 
					    deco: IDojoDecoDatabase
 | 
				
			||||||
 | 
					): void => {
 | 
				
			||||||
    const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type);
 | 
					    const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type);
 | 
				
			||||||
    if (meta) {
 | 
					    if (meta) {
 | 
				
			||||||
        if (meta.capacityCost) {
 | 
					        if (meta.capacityCost) {
 | 
				
			||||||
@ -369,7 +378,7 @@ export const removeDojoDeco = (
 | 
				
			|||||||
            ]);
 | 
					            ]);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    moveResourcesToVault(guild, deco);
 | 
					    moveResourcesToVault(guild, deco); // Refund resources spent on construction
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const moveResourcesToVault = (guild: TGuildDatabaseDocument, component: IDojoContributable): void => {
 | 
					const moveResourcesToVault = (guild: TGuildDatabaseDocument, component: IDojoContributable): void => {
 | 
				
			||||||
 | 
				
			|||||||
@ -1241,6 +1241,15 @@ export const addFusionPoints = (inventory: TInventoryDatabaseDocument, add: numb
 | 
				
			|||||||
    return add;
 | 
					    return add;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const addCrewShipFusionPoints = (inventory: TInventoryDatabaseDocument, add: number): number => {
 | 
				
			||||||
 | 
					    if (inventory.CrewShipFusionPoints + add > 2147483647) {
 | 
				
			||||||
 | 
					        logger.warn(`capping CrewShipFusionPoints balance at 2147483647`);
 | 
				
			||||||
 | 
					        add = 2147483647 - inventory.CrewShipFusionPoints;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    inventory.CrewShipFusionPoints += add;
 | 
				
			||||||
 | 
					    return add;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const standingLimitBinToInventoryKey: Record<
 | 
					const standingLimitBinToInventoryKey: Record<
 | 
				
			||||||
    Exclude<TStandingLimitBin, "STANDING_LIMIT_BIN_NONE">,
 | 
					    Exclude<TStandingLimitBin, "STANDING_LIMIT_BIN_NONE">,
 | 
				
			||||||
    keyof IDailyAffiliations
 | 
					    keyof IDailyAffiliations
 | 
				
			||||||
 | 
				
			|||||||
@ -32,7 +32,7 @@ export const getUsernameFromEmail = async (email: string): Promise<string> => {
 | 
				
			|||||||
            name = nameFromEmail + suffix;
 | 
					            name = nameFromEmail + suffix;
 | 
				
			||||||
        } while (await isNameTaken(name));
 | 
					        } while (await isNameTaken(name));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return nameFromEmail;
 | 
					    return name;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createAccount = async (accountData: IDatabaseAccountRequiredFields): Promise<IDatabaseAccountJson> => {
 | 
					export const createAccount = async (accountData: IDatabaseAccountRequiredFields): Promise<IDatabaseAccountJson> => {
 | 
				
			||||||
 | 
				
			|||||||
@ -1283,9 +1283,7 @@ export const addMissionRewards = async (
 | 
				
			|||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                let medallionAmount = Math.floor(
 | 
					                let medallionAmount = Math.floor(currentJob.xpAmounts[rewardInfo.JobStage] / (rewardInfo.Q ? 0.8 : 1));
 | 
				
			||||||
                    Math.min(rewardInfo.JobStage, currentJob.xpAmounts.length - 1) / (rewardInfo.Q ? 0.8 : 1)
 | 
					 | 
				
			||||||
                );
 | 
					 | 
				
			||||||
                if (
 | 
					                if (
 | 
				
			||||||
                    ["DeimosEndlessAreaDefenseBounty", "DeimosEndlessExcavateBounty", "DeimosEndlessPurifyBounty"].some(
 | 
					                    ["DeimosEndlessAreaDefenseBounty", "DeimosEndlessExcavateBounty", "DeimosEndlessPurifyBounty"].some(
 | 
				
			||||||
                        ending => jobType.endsWith(ending)
 | 
					                        ending => jobType.endsWith(ending)
 | 
				
			||||||
@ -1637,7 +1635,10 @@ function getRandomMissionDrops(
 | 
				
			|||||||
            rewardManifests = [
 | 
					            rewardManifests = [
 | 
				
			||||||
                "/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriMurmurFinalSteelChestRewards"
 | 
					                "/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriMurmurFinalSteelChestRewards"
 | 
				
			||||||
            ];
 | 
					            ];
 | 
				
			||||||
        } else if (RewardInfo.T == 70) {
 | 
					        } else if (
 | 
				
			||||||
 | 
					            RewardInfo.T == 70 ||
 | 
				
			||||||
 | 
					            RewardInfo.T == 6 // https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2526
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
            // Orowyrm chest, gives 10 Pathos Clamps, or 15 on Steel Path.
 | 
					            // Orowyrm chest, gives 10 Pathos Clamps, or 15 on Steel Path.
 | 
				
			||||||
            drops.push({
 | 
					            drops.push({
 | 
				
			||||||
                StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem",
 | 
					                StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem",
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,8 @@
 | 
				
			|||||||
import { getPersonalRooms } from "@/src/services/personalRoomsService";
 | 
					import { getPersonalRooms } from "@/src/services/personalRoomsService";
 | 
				
			||||||
import { getShip } from "@/src/services/shipService";
 | 
					import { getShip } from "@/src/services/shipService";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
 | 
					    IResetShipDecorationsRequest,
 | 
				
			||||||
 | 
					    IResetShipDecorationsResponse,
 | 
				
			||||||
    ISetPlacedDecoInfoRequest,
 | 
					    ISetPlacedDecoInfoRequest,
 | 
				
			||||||
    ISetShipCustomizationsRequest,
 | 
					    ISetShipCustomizationsRequest,
 | 
				
			||||||
    IShipDecorationsRequest,
 | 
					    IShipDecorationsRequest,
 | 
				
			||||||
@ -52,12 +54,7 @@ export const handleSetShipDecorations = async (
 | 
				
			|||||||
): Promise<IShipDecorationsResponse> => {
 | 
					): Promise<IShipDecorationsResponse> => {
 | 
				
			||||||
    const personalRooms = await getPersonalRooms(accountId);
 | 
					    const personalRooms = await getPersonalRooms(accountId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const rooms =
 | 
					    const rooms = getRoomsForBootLocation(personalRooms, placedDecoration);
 | 
				
			||||||
        placedDecoration.BootLocation == "SHOP"
 | 
					 | 
				
			||||||
            ? personalRooms.TailorShop.Rooms
 | 
					 | 
				
			||||||
            : placedDecoration.IsApartment
 | 
					 | 
				
			||||||
              ? personalRooms.Apartment.Rooms
 | 
					 | 
				
			||||||
              : personalRooms.Ship.Rooms;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const roomToPlaceIn = rooms.find(room => room.Name === placedDecoration.Room);
 | 
					    const roomToPlaceIn = rooms.find(room => room.Name === placedDecoration.Room);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -159,7 +156,6 @@ export const handleSetShipDecorations = async (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if (!config.unlockAllShipDecorations) {
 | 
					    if (!config.unlockAllShipDecorations) {
 | 
				
			||||||
        const inventory = await getInventory(accountId);
 | 
					        const inventory = await getInventory(accountId);
 | 
				
			||||||
        const itemType = Object.entries(ExportResources).find(arr => arr[1].deco == placedDecoration.Type)![0];
 | 
					 | 
				
			||||||
        if (placedDecoration.Sockets !== undefined) {
 | 
					        if (placedDecoration.Sockets !== undefined) {
 | 
				
			||||||
            addFusionTreasures(inventory, [{ ItemType: itemType, Sockets: placedDecoration.Sockets, ItemCount: -1 }]);
 | 
					            addFusionTreasures(inventory, [{ ItemType: itemType, Sockets: placedDecoration.Sockets, ItemCount: -1 }]);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
@ -192,17 +188,62 @@ export const handleSetShipDecorations = async (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const getRoomsForBootLocation = (
 | 
					const getRoomsForBootLocation = (
 | 
				
			||||||
    personalRooms: TPersonalRoomsDatabaseDocument,
 | 
					    personalRooms: TPersonalRoomsDatabaseDocument,
 | 
				
			||||||
    bootLocation: TBootLocation | undefined
 | 
					    request: { BootLocation?: TBootLocation; IsApartment?: boolean }
 | 
				
			||||||
): RoomsType[] => {
 | 
					): RoomsType[] => {
 | 
				
			||||||
    if (bootLocation == "SHOP") {
 | 
					    if (request.BootLocation == "SHOP") {
 | 
				
			||||||
        return personalRooms.TailorShop.Rooms;
 | 
					        return personalRooms.TailorShop.Rooms;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (bootLocation == "APARTMENT") {
 | 
					    if (request.BootLocation == "APARTMENT" || request.IsApartment) {
 | 
				
			||||||
        return personalRooms.Apartment.Rooms;
 | 
					        return personalRooms.Apartment.Rooms;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return personalRooms.Ship.Rooms;
 | 
					    return personalRooms.Ship.Rooms;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const handleResetShipDecorations = async (
 | 
				
			||||||
 | 
					    accountId: string,
 | 
				
			||||||
 | 
					    request: IResetShipDecorationsRequest
 | 
				
			||||||
 | 
					): Promise<IResetShipDecorationsResponse> => {
 | 
				
			||||||
 | 
					    const [personalRooms, inventory] = await Promise.all([getPersonalRooms(accountId), getInventory(accountId)]);
 | 
				
			||||||
 | 
					    const room = getRoomsForBootLocation(personalRooms, request).find(room => room.Name === request.Room);
 | 
				
			||||||
 | 
					    if (!room) {
 | 
				
			||||||
 | 
					        throw new Error(`unknown room: ${request.Room}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const deco of room.PlacedDecos) {
 | 
				
			||||||
 | 
					        const entry = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type);
 | 
				
			||||||
 | 
					        if (!entry) {
 | 
				
			||||||
 | 
					            throw new Error(`unknown deco type: ${deco.Type}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const [itemType, meta] = entry;
 | 
				
			||||||
 | 
					        if (meta.capacityCost === undefined) {
 | 
				
			||||||
 | 
					            throw new Error(`unknown deco type: ${deco.Type}`);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // refund item
 | 
				
			||||||
 | 
					        if (!config.unlockAllShipDecorations) {
 | 
				
			||||||
 | 
					            if (deco.Sockets !== undefined) {
 | 
				
			||||||
 | 
					                addFusionTreasures(inventory, [{ ItemType: itemType, Sockets: deco.Sockets, ItemCount: 1 }]);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                addShipDecorations(inventory, [{ ItemType: itemType, ItemCount: 1 }]);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // refund capacity
 | 
				
			||||||
 | 
					        room.MaxCapacity += meta.capacityCost;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // empty room
 | 
				
			||||||
 | 
					    room.PlacedDecos.splice(0, room.PlacedDecos.length);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await Promise.all([personalRooms.save(), inventory.save()]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        ResetRoom: request.Room,
 | 
				
			||||||
 | 
					        ClaimedDecos: [], // Not sure what this is for; the client already implies that the decos were returned to inventory.
 | 
				
			||||||
 | 
					        NewCapacity: room.MaxCapacity
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const handleSetPlacedDecoInfo = async (accountId: string, req: ISetPlacedDecoInfoRequest): Promise<void> => {
 | 
					export const handleSetPlacedDecoInfo = async (accountId: string, req: ISetPlacedDecoInfoRequest): Promise<void> => {
 | 
				
			||||||
    if (req.GuildId && req.ComponentId) {
 | 
					    if (req.GuildId && req.ComponentId) {
 | 
				
			||||||
        const guild = (await Guild.findById(req.GuildId))!;
 | 
					        const guild = (await Guild.findById(req.GuildId))!;
 | 
				
			||||||
@ -217,7 +258,7 @@ export const handleSetPlacedDecoInfo = async (accountId: string, req: ISetPlaced
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const personalRooms = await getPersonalRooms(accountId);
 | 
					    const personalRooms = await getPersonalRooms(accountId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const room = getRoomsForBootLocation(personalRooms, req.BootLocation).find(room => room.Name === req.Room);
 | 
					    const room = getRoomsForBootLocation(personalRooms, req).find(room => room.Name === req.Room);
 | 
				
			||||||
    if (!room) {
 | 
					    if (!room) {
 | 
				
			||||||
        throw new Error(`unknown room: ${req.Room}`);
 | 
					        throw new Error(`unknown room: ${req.Room}`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -5,6 +5,7 @@ import { Account } from "@/src/models/loginModel";
 | 
				
			|||||||
import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } from "@/src/services/loginService";
 | 
					import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } from "@/src/services/loginService";
 | 
				
			||||||
import { IDatabaseAccountJson } from "@/src/types/loginTypes";
 | 
					import { IDatabaseAccountJson } from "@/src/types/loginTypes";
 | 
				
			||||||
import { HydratedDocument } from "mongoose";
 | 
					import { HydratedDocument } from "mongoose";
 | 
				
			||||||
 | 
					import { logError } from "@/src/utils/logger";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let wsServer: ws.Server | undefined;
 | 
					let wsServer: ws.Server | undefined;
 | 
				
			||||||
let wssServer: ws.Server | undefined;
 | 
					let wssServer: ws.Server | undefined;
 | 
				
			||||||
@ -43,7 +44,7 @@ export const stopWsServers = (promises: Promise<void>[]): void => {
 | 
				
			|||||||
let lastWsid: number = 0;
 | 
					let lastWsid: number = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface IWsCustomData extends ws {
 | 
					interface IWsCustomData extends ws {
 | 
				
			||||||
    id?: number;
 | 
					    id: number;
 | 
				
			||||||
    accountId?: string;
 | 
					    accountId?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -88,63 +89,67 @@ const wsOnConnect = (ws: ws, req: http.IncomingMessage): void => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
					    // eslint-disable-next-line @typescript-eslint/no-misused-promises
 | 
				
			||||||
    ws.on("message", async msg => {
 | 
					    ws.on("message", async msg => {
 | 
				
			||||||
        const data = JSON.parse(String(msg)) as IWsMsgFromClient;
 | 
					        try {
 | 
				
			||||||
        if (data.auth) {
 | 
					            const data = JSON.parse(String(msg)) as IWsMsgFromClient;
 | 
				
			||||||
            let account: IDatabaseAccountJson | null = await Account.findOne({ email: data.auth.email });
 | 
					            if (data.auth) {
 | 
				
			||||||
            if (account) {
 | 
					                let account: IDatabaseAccountJson | null = await Account.findOne({ email: data.auth.email });
 | 
				
			||||||
                if (isCorrectPassword(data.auth.password, account.password)) {
 | 
					                if (account) {
 | 
				
			||||||
                    if (!account.Nonce) {
 | 
					                    if (isCorrectPassword(data.auth.password, account.password)) {
 | 
				
			||||||
                        account.ClientType = "webui";
 | 
					                        if (!account.Nonce) {
 | 
				
			||||||
                        account.Nonce = createNonce();
 | 
					                            account.ClientType = "webui";
 | 
				
			||||||
                        await (account as HydratedDocument<IDatabaseAccountJson>).save();
 | 
					                            account.Nonce = createNonce();
 | 
				
			||||||
 | 
					                            await (account as HydratedDocument<IDatabaseAccountJson>).save();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    } else {
 | 
				
			||||||
 | 
					                        account = null;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					                } else if (data.auth.isRegister) {
 | 
				
			||||||
 | 
					                    const name = await getUsernameFromEmail(data.auth.email);
 | 
				
			||||||
 | 
					                    account = await createAccount({
 | 
				
			||||||
 | 
					                        email: data.auth.email,
 | 
				
			||||||
 | 
					                        password: data.auth.password,
 | 
				
			||||||
 | 
					                        ClientType: "webui",
 | 
				
			||||||
 | 
					                        LastLogin: new Date(),
 | 
				
			||||||
 | 
					                        DisplayName: name,
 | 
				
			||||||
 | 
					                        Nonce: createNonce()
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (account) {
 | 
				
			||||||
 | 
					                    (ws as IWsCustomData).accountId = account.id;
 | 
				
			||||||
 | 
					                    ws.send(
 | 
				
			||||||
 | 
					                        JSON.stringify({
 | 
				
			||||||
 | 
					                            auth_succ: {
 | 
				
			||||||
 | 
					                                id: account.id,
 | 
				
			||||||
 | 
					                                DisplayName: account.DisplayName,
 | 
				
			||||||
 | 
					                                Nonce: account.Nonce
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        } satisfies IWsMsgToClient)
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    account = null;
 | 
					                    ws.send(
 | 
				
			||||||
 | 
					                        JSON.stringify({
 | 
				
			||||||
 | 
					                            auth_fail: {
 | 
				
			||||||
 | 
					                                isRegister: data.auth.isRegister
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        } satisfies IWsMsgToClient)
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            } else if (data.auth.isRegister) {
 | 
					 | 
				
			||||||
                const name = await getUsernameFromEmail(data.auth.email);
 | 
					 | 
				
			||||||
                account = await createAccount({
 | 
					 | 
				
			||||||
                    email: data.auth.email,
 | 
					 | 
				
			||||||
                    password: data.auth.password,
 | 
					 | 
				
			||||||
                    ClientType: "webui",
 | 
					 | 
				
			||||||
                    LastLogin: new Date(),
 | 
					 | 
				
			||||||
                    DisplayName: name,
 | 
					 | 
				
			||||||
                    Nonce: createNonce()
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            if (account) {
 | 
					            if (data.logout) {
 | 
				
			||||||
                (ws as IWsCustomData).accountId = account.id;
 | 
					                const accountId = (ws as IWsCustomData).accountId;
 | 
				
			||||||
                ws.send(
 | 
					                (ws as IWsCustomData).accountId = undefined;
 | 
				
			||||||
                    JSON.stringify({
 | 
					                await Account.updateOne(
 | 
				
			||||||
                        auth_succ: {
 | 
					                    {
 | 
				
			||||||
                            id: account.id,
 | 
					                        _id: accountId,
 | 
				
			||||||
                            DisplayName: account.DisplayName,
 | 
					                        ClientType: "webui"
 | 
				
			||||||
                            Nonce: account.Nonce
 | 
					                    },
 | 
				
			||||||
                        }
 | 
					                    {
 | 
				
			||||||
                    } satisfies IWsMsgToClient)
 | 
					                        Nonce: 0
 | 
				
			||||||
                );
 | 
					                    }
 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                ws.send(
 | 
					 | 
				
			||||||
                    JSON.stringify({
 | 
					 | 
				
			||||||
                        auth_fail: {
 | 
					 | 
				
			||||||
                            isRegister: data.auth.isRegister
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    } satisfies IWsMsgToClient)
 | 
					 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        } catch (e) {
 | 
				
			||||||
        if (data.logout) {
 | 
					            logError(e as Error, `processing websocket message`);
 | 
				
			||||||
            const accountId = (ws as IWsCustomData).accountId;
 | 
					 | 
				
			||||||
            (ws as IWsCustomData).accountId = undefined;
 | 
					 | 
				
			||||||
            await Account.updateOne(
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    _id: accountId,
 | 
					 | 
				
			||||||
                    ClientType: "webui"
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    Nonce: 0
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@ -181,18 +186,24 @@ export const sendWsBroadcastTo = (accountId: string, data: IWsMsgToClient): void
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const sendWsBroadcastExcept = (wsid: number | undefined, data: IWsMsgToClient): void => {
 | 
					export const sendWsBroadcastEx = (data: IWsMsgToClient, accountId?: string, excludeWsid?: number): void => {
 | 
				
			||||||
    const msg = JSON.stringify(data);
 | 
					    const msg = JSON.stringify(data);
 | 
				
			||||||
    if (wsServer) {
 | 
					    if (wsServer) {
 | 
				
			||||||
        for (const client of wsServer.clients) {
 | 
					        for (const client of wsServer.clients) {
 | 
				
			||||||
            if ((client as IWsCustomData).id != wsid) {
 | 
					            if (
 | 
				
			||||||
 | 
					                (!accountId || (client as IWsCustomData).accountId == accountId) &&
 | 
				
			||||||
 | 
					                (client as IWsCustomData).id != excludeWsid
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
                client.send(msg);
 | 
					                client.send(msg);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (wssServer) {
 | 
					    if (wssServer) {
 | 
				
			||||||
        for (const client of wssServer.clients) {
 | 
					        for (const client of wssServer.clients) {
 | 
				
			||||||
            if ((client as IWsCustomData).id != wsid) {
 | 
					            if (
 | 
				
			||||||
 | 
					                (!accountId || (client as IWsCustomData).accountId == accountId) &&
 | 
				
			||||||
 | 
					                (client as IWsCustomData).id != excludeWsid
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
                client.send(msg);
 | 
					                client.send(msg);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,3 @@
 | 
				
			|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
 | 
					 | 
				
			||||||
import { Types } from "mongoose";
 | 
					import { Types } from "mongoose";
 | 
				
			||||||
import { IOid, IMongoDate, IOidWithLegacySupport, ITypeCount } from "@/src/types/commonTypes";
 | 
					import { IOid, IMongoDate, IOidWithLegacySupport, ITypeCount } from "@/src/types/commonTypes";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@ -216,6 +215,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
 | 
				
			|||||||
    PremiumCredits: number;
 | 
					    PremiumCredits: number;
 | 
				
			||||||
    PremiumCreditsFree: number;
 | 
					    PremiumCreditsFree: number;
 | 
				
			||||||
    FusionPoints: number;
 | 
					    FusionPoints: number;
 | 
				
			||||||
 | 
					    CrewShipFusionPoints: number; //Dirac (pre-rework Railjack)
 | 
				
			||||||
    PrimeTokens: number;
 | 
					    PrimeTokens: number;
 | 
				
			||||||
    SuitBin: ISlots;
 | 
					    SuitBin: ISlots;
 | 
				
			||||||
    WeaponBin: ISlots;
 | 
					    WeaponBin: ISlots;
 | 
				
			||||||
 | 
				
			|||||||
@ -91,12 +91,16 @@ export interface IApartmentClient {
 | 
				
			|||||||
    Gardening: IGardeningClient;
 | 
					    Gardening: IGardeningClient;
 | 
				
			||||||
    Rooms: IRoom[];
 | 
					    Rooms: IRoom[];
 | 
				
			||||||
    FavouriteLoadouts: IFavouriteLoadout[];
 | 
					    FavouriteLoadouts: IFavouriteLoadout[];
 | 
				
			||||||
 | 
					    VideoWallBackdrop?: string;
 | 
				
			||||||
 | 
					    Soundscape?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IApartmentDatabase {
 | 
					export interface IApartmentDatabase {
 | 
				
			||||||
    Gardening: IGardeningDatabase;
 | 
					    Gardening: IGardeningDatabase;
 | 
				
			||||||
    Rooms: IRoom[];
 | 
					    Rooms: IRoom[];
 | 
				
			||||||
    FavouriteLoadouts: IFavouriteLoadoutDatabase[];
 | 
					    FavouriteLoadouts: IFavouriteLoadoutDatabase[];
 | 
				
			||||||
 | 
					    VideoWallBackdrop?: string;
 | 
				
			||||||
 | 
					    Soundscape?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IPlacedDecosDatabase {
 | 
					export interface IPlacedDecosDatabase {
 | 
				
			||||||
@ -150,6 +154,17 @@ export interface IShipDecorationsResponse {
 | 
				
			|||||||
    NewRoom?: string;
 | 
					    NewRoom?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IResetShipDecorationsRequest {
 | 
				
			||||||
 | 
					    Room: string;
 | 
				
			||||||
 | 
					    BootLocation?: TBootLocation;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IResetShipDecorationsResponse {
 | 
				
			||||||
 | 
					    ResetRoom: string;
 | 
				
			||||||
 | 
					    ClaimedDecos: [];
 | 
				
			||||||
 | 
					    NewCapacity: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ISetPlacedDecoInfoRequest {
 | 
					export interface ISetPlacedDecoInfoRequest {
 | 
				
			||||||
    DecoType: string;
 | 
					    DecoType: string;
 | 
				
			||||||
    DecoId: string;
 | 
					    DecoId: string;
 | 
				
			||||||
 | 
				
			|||||||
@ -108,3 +108,13 @@ errorLog.on("new", filename => logger.info(`Using error log file: ${filename}`))
 | 
				
			|||||||
combinedLog.on("new", filename => logger.info(`Using combined log file: ${filename}`));
 | 
					combinedLog.on("new", filename => logger.info(`Using combined log file: ${filename}`));
 | 
				
			||||||
errorLog.on("rotate", filename => logger.info(`Rotated error log file: ${filename}`));
 | 
					errorLog.on("rotate", filename => logger.info(`Rotated error log file: ${filename}`));
 | 
				
			||||||
combinedLog.on("rotate", filename => logger.info(`Rotated combined log file: ${filename}`));
 | 
					combinedLog.on("rotate", filename => logger.info(`Rotated combined log file: ${filename}`));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const logError = (err: Error, context: string): void => {
 | 
				
			||||||
 | 
					    if (err.stack) {
 | 
				
			||||||
 | 
					        const stackArr = err.stack.split("\n");
 | 
				
			||||||
 | 
					        stackArr[0] += ` while ${context}`;
 | 
				
			||||||
 | 
					        logger.error(stackArr.join("\n"));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        logger.error(`uncaught error while ${context}: ${err.message}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -436,22 +436,22 @@
 | 
				
			|||||||
                    <h5 class="card-header" data-loc="general_bulkActions"></h5>
 | 
					                    <h5 class="card-header" data-loc="general_bulkActions"></h5>
 | 
				
			||||||
                    <div class="card-body">
 | 
					                    <div class="card-body">
 | 
				
			||||||
                        <div class="mb-2 d-flex flex-wrap gap-2">
 | 
					                        <div class="mb-2 d-flex flex-wrap gap-2">
 | 
				
			||||||
                            <button class="btn btn-primary" onclick="addMissingEquipment(['Suits']);" data-loc="inventory_bulkAddSuits"></button>
 | 
					                            <button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['Suits']);" data-loc="inventory_bulkAddSuits"></button>
 | 
				
			||||||
                            <button class="btn btn-primary" onclick="addMissingEquipment(['Melee', 'LongGuns', 'Pistols']);" data-loc="inventory_bulkAddWeapons"></button>
 | 
					                            <button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['Melee', 'LongGuns', 'Pistols']);" data-loc="inventory_bulkAddWeapons"></button>
 | 
				
			||||||
                            <button class="btn btn-primary" onclick="addMissingEquipment(['SpaceSuits']);" data-loc="inventory_bulkAddSpaceSuits"></button>
 | 
					                            <button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['SpaceSuits']);" data-loc="inventory_bulkAddSpaceSuits"></button>
 | 
				
			||||||
                            <button class="btn btn-primary" onclick="addMissingEquipment(['SpaceGuns', 'SpaceMelee']);" data-loc="inventory_bulkAddSpaceWeapons"></button>
 | 
					                            <button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['SpaceGuns', 'SpaceMelee']);" data-loc="inventory_bulkAddSpaceWeapons"></button>
 | 
				
			||||||
                            <button class="btn btn-primary" onclick="addMissingEquipment(['Sentinels']);" data-loc="inventory_bulkAddSentinels"></button>
 | 
					                            <button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['Sentinels']);" data-loc="inventory_bulkAddSentinels"></button>
 | 
				
			||||||
                            <button class="btn btn-primary" onclick="addMissingEquipment(['SentinelWeapons']);" data-loc="inventory_bulkAddSentinelWeapons"></button>
 | 
					                            <button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['SentinelWeapons']);" data-loc="inventory_bulkAddSentinelWeapons"></button>
 | 
				
			||||||
                            <button class="btn btn-primary" onclick="addMissingEvolutionProgress();" data-loc="inventory_bulkAddEvolutionProgress"></button>
 | 
					                            <button class="btn btn-primary" onclick="debounce(addMissingEvolutionProgress);" data-loc="inventory_bulkAddEvolutionProgress"></button>
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
                        <div class="mb-2 d-flex flex-wrap gap-2">
 | 
					                        <div class="mb-2 d-flex flex-wrap gap-2">
 | 
				
			||||||
                            <button class="btn btn-success" onclick="maxRankAllEquipment(['Suits']);" data-loc="inventory_bulkRankUpSuits"></button>
 | 
					                            <button class="btn btn-success" onclick="debounce(maxRankAllEquipment, ['Suits']);" data-loc="inventory_bulkRankUpSuits"></button>
 | 
				
			||||||
                            <button class="btn btn-success" onclick="maxRankAllEquipment(['Melee', 'LongGuns', 'Pistols']);" data-loc="inventory_bulkRankUpWeapons"></button>
 | 
					                            <button class="btn btn-success" onclick="debounce(maxRankAllEquipment, ['Melee', 'LongGuns', 'Pistols']);" data-loc="inventory_bulkRankUpWeapons"></button>
 | 
				
			||||||
                            <button class="btn btn-success" onclick="maxRankAllEquipment(['SpaceSuits']);" data-loc="inventory_bulkRankUpSpaceSuits"></button>
 | 
					                            <button class="btn btn-success" onclick="debounce(maxRankAllEquipment, ['SpaceSuits']);" data-loc="inventory_bulkRankUpSpaceSuits"></button>
 | 
				
			||||||
                            <button class="btn btn-success" onclick="maxRankAllEquipment(['SpaceGuns', 'SpaceMelee']);" data-loc="inventory_bulkRankUpSpaceWeapons"></button>
 | 
					                            <button class="btn btn-success" onclick="debounce(maxRankAllEquipment, ['SpaceGuns', 'SpaceMelee']);" data-loc="inventory_bulkRankUpSpaceWeapons"></button>
 | 
				
			||||||
                            <button class="btn btn-success" onclick="maxRankAllEquipment(['Sentinels']);" data-loc="inventory_bulkRankUpSentinels"></button>
 | 
					                            <button class="btn btn-success" onclick="debounce(maxRankAllEquipment, ['Sentinels']);" data-loc="inventory_bulkRankUpSentinels"></button>
 | 
				
			||||||
                            <button class="btn btn-success" onclick="maxRankAllEquipment(['SentinelWeapons']);" data-loc="inventory_bulkRankUpSentinelWeapons"></button>
 | 
					                            <button class="btn btn-success" onclick="debounce(maxRankAllEquipment, ['SentinelWeapons']);" data-loc="inventory_bulkRankUpSentinelWeapons"></button>
 | 
				
			||||||
                            <button class="btn btn-success" onclick="maxRankAllEvolutions();" data-loc="inventory_bulkRankUpEvolutionProgress"></button>
 | 
					                            <button class="btn btn-success" onclick="debounce(maxRankAllEvolutions);" data-loc="inventory_bulkRankUpEvolutionProgress"></button>
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
 | 
				
			|||||||
@ -118,9 +118,16 @@ function doLogin() {
 | 
				
			|||||||
    window.registerSubmit = false;
 | 
					    window.registerSubmit = false;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function revalidateAuthz() {
 | 
					function revalidateAuthz() {
 | 
				
			||||||
    await getWebSocket();
 | 
					    return new Promise(resolve => {
 | 
				
			||||||
    // We have a websocket connection, so authz should be good.
 | 
					        let interval;
 | 
				
			||||||
 | 
					        interval = setInterval(() => {
 | 
				
			||||||
 | 
					            if (ws_is_open && !auth_pending) {
 | 
				
			||||||
 | 
					                clearInterval(interval);
 | 
				
			||||||
 | 
					                resolve();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }, 10);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function logout() {
 | 
					function logout() {
 | 
				
			||||||
@ -865,15 +872,11 @@ function updateInventory() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            const datalistEvolutionProgress = document.querySelectorAll("#datalist-EvolutionProgress option");
 | 
					            const datalistEvolutionProgress = document.querySelectorAll("#datalist-EvolutionProgress option");
 | 
				
			||||||
            const formEvolutionProgress = document.querySelector('form[onsubmit*="doAcquireEvolution()"]');
 | 
					            const formEvolutionProgress = document.querySelector('form[onsubmit*="doAcquireEvolution()"]');
 | 
				
			||||||
            const giveAllQEvolutionProgress = document.querySelector(
 | 
					 | 
				
			||||||
                'button[onclick*="addMissingEvolutionProgress()"]'
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (datalistEvolutionProgress.length === 0) {
 | 
					            if (datalistEvolutionProgress.length === 0) {
 | 
				
			||||||
                formEvolutionProgress.classList.add("disabled");
 | 
					                formEvolutionProgress.classList.add("disabled");
 | 
				
			||||||
                formEvolutionProgress.querySelector("input").disabled = true;
 | 
					                formEvolutionProgress.querySelector("input").disabled = true;
 | 
				
			||||||
                formEvolutionProgress.querySelector("button").disabled = true;
 | 
					                formEvolutionProgress.querySelector("button").disabled = true;
 | 
				
			||||||
                giveAllQEvolutionProgress.disabled = true;
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (data.CrewShipHarnesses?.length) {
 | 
					            if (data.CrewShipHarnesses?.length) {
 | 
				
			||||||
@ -1534,14 +1537,17 @@ $(document).on("input", "input[list]", function () {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function dispatchAddItemsRequestsBatch(requests) {
 | 
					function dispatchAddItemsRequestsBatch(requests) {
 | 
				
			||||||
    revalidateAuthz().then(() => {
 | 
					    return new Promise(resolve => {
 | 
				
			||||||
        const req = $.post({
 | 
					        revalidateAuthz().then(() => {
 | 
				
			||||||
            url: "/custom/addItems?" + window.authz,
 | 
					            const req = $.post({
 | 
				
			||||||
            contentType: "application/json",
 | 
					                url: "/custom/addItems?" + window.authz,
 | 
				
			||||||
            data: JSON.stringify(requests)
 | 
					                contentType: "application/json",
 | 
				
			||||||
        });
 | 
					                data: JSON.stringify(requests)
 | 
				
			||||||
        req.done(() => {
 | 
					            });
 | 
				
			||||||
            updateInventory();
 | 
					            req.done(() => {
 | 
				
			||||||
 | 
					                updateInventory();
 | 
				
			||||||
 | 
					                resolve();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -1562,7 +1568,7 @@ function addMissingEquipment(categories) {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    if (requests.length != 0 && window.confirm(loc("code_addItemsConfirm").split("|COUNT|").join(requests.length))) {
 | 
					    if (requests.length != 0 && window.confirm(loc("code_addItemsConfirm").split("|COUNT|").join(requests.length))) {
 | 
				
			||||||
        dispatchAddItemsRequestsBatch(requests);
 | 
					        return dispatchAddItemsRequestsBatch(requests);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1578,7 +1584,7 @@ function addMissingEvolutionProgress() {
 | 
				
			|||||||
        requests.push({ ItemType: uniqueName, Rank: permanentEvolutionWeapons.has(uniqueName) ? 0 : 1 });
 | 
					        requests.push({ ItemType: uniqueName, Rank: permanentEvolutionWeapons.has(uniqueName) ? 0 : 1 });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    if (requests.length != 0 && window.confirm(loc("code_addItemsConfirm").split("|COUNT|").join(requests.length))) {
 | 
					    if (requests.length != 0 && window.confirm(loc("code_addItemsConfirm").split("|COUNT|").join(requests.length))) {
 | 
				
			||||||
        setEvolutionProgress(requests);
 | 
					        return setEvolutionProgress(requests);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1746,7 +1752,7 @@ function disposeOfGear(category, oid) {
 | 
				
			|||||||
        ];
 | 
					        ];
 | 
				
			||||||
        revalidateAuthz().then(() => {
 | 
					        revalidateAuthz().then(() => {
 | 
				
			||||||
            $.post({
 | 
					            $.post({
 | 
				
			||||||
                url: "/api/sell.php?" + window.authz,
 | 
					                url: "/api/sell.php?" + window.authz + "&wsid=" + wsid,
 | 
				
			||||||
                contentType: "text/plain",
 | 
					                contentType: "text/plain",
 | 
				
			||||||
                data: JSON.stringify(data)
 | 
					                data: JSON.stringify(data)
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
@ -1768,7 +1774,7 @@ function disposeOfItems(category, type, count) {
 | 
				
			|||||||
    ];
 | 
					    ];
 | 
				
			||||||
    revalidateAuthz().then(() => {
 | 
					    revalidateAuthz().then(() => {
 | 
				
			||||||
        $.post({
 | 
					        $.post({
 | 
				
			||||||
            url: "/api/sell.php?" + window.authz,
 | 
					            url: "/api/sell.php?" + window.authz + "&wsid=" + wsid,
 | 
				
			||||||
            contentType: "text/plain",
 | 
					            contentType: "text/plain",
 | 
				
			||||||
            data: JSON.stringify(data)
 | 
					            data: JSON.stringify(data)
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
@ -1805,14 +1811,17 @@ function maturePet(oid, revert) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setEvolutionProgress(requests) {
 | 
					function setEvolutionProgress(requests) {
 | 
				
			||||||
    revalidateAuthz().then(() => {
 | 
					    return new Promise(resolve => {
 | 
				
			||||||
        const req = $.post({
 | 
					        revalidateAuthz().then(() => {
 | 
				
			||||||
            url: "/custom/setEvolutionProgress?" + window.authz,
 | 
					            const req = $.post({
 | 
				
			||||||
            contentType: "application/json",
 | 
					                url: "/custom/setEvolutionProgress?" + window.authz,
 | 
				
			||||||
            data: JSON.stringify(requests)
 | 
					                contentType: "application/json",
 | 
				
			||||||
        });
 | 
					                data: JSON.stringify(requests)
 | 
				
			||||||
        req.done(() => {
 | 
					            });
 | 
				
			||||||
            updateInventory();
 | 
					            req.done(() => {
 | 
				
			||||||
 | 
					                updateInventory();
 | 
				
			||||||
 | 
					                resolve();
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -2080,6 +2089,10 @@ single.getRoute("/webui/cheats").on("beforeload", function () {
 | 
				
			|||||||
                })
 | 
					                })
 | 
				
			||||||
                .fail(res => {
 | 
					                .fail(res => {
 | 
				
			||||||
                    if (res.responseText == "Log-in expired") {
 | 
					                    if (res.responseText == "Log-in expired") {
 | 
				
			||||||
 | 
					                        if (ws_is_open && !auth_pending) {
 | 
				
			||||||
 | 
					                            console.warn("Credentials invalidated but the server didn't let us know");
 | 
				
			||||||
 | 
					                            sendAuth();
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                        revalidateAuthz().then(() => {
 | 
					                        revalidateAuthz().then(() => {
 | 
				
			||||||
                            if (single.getCurrentPath() == "/webui/cheats") {
 | 
					                            if (single.getCurrentPath() == "/webui/cheats") {
 | 
				
			||||||
                                single.loadRoute("/webui/cheats");
 | 
					                                single.loadRoute("/webui/cheats");
 | 
				
			||||||
@ -2202,7 +2215,7 @@ function doRemoveUnrankedMods() {
 | 
				
			|||||||
        req.done(inventory => {
 | 
					        req.done(inventory => {
 | 
				
			||||||
            window.itemListPromise.then(itemMap => {
 | 
					            window.itemListPromise.then(itemMap => {
 | 
				
			||||||
                $.post({
 | 
					                $.post({
 | 
				
			||||||
                    url: "/api/sell.php?" + window.authz,
 | 
					                    url: "/api/sell.php?" + window.authz + "&wsid=" + wsid,
 | 
				
			||||||
                    contentType: "text/plain",
 | 
					                    contentType: "text/plain",
 | 
				
			||||||
                    data: JSON.stringify({
 | 
					                    data: JSON.stringify({
 | 
				
			||||||
                        SellCurrency: "SC_RegularCredits",
 | 
					                        SellCurrency: "SC_RegularCredits",
 | 
				
			||||||
@ -2236,6 +2249,7 @@ single.getRoute("#detailedView-route").on("beforeload", function () {
 | 
				
			|||||||
    document.getElementById("detailedView-title").textContent = "";
 | 
					    document.getElementById("detailedView-title").textContent = "";
 | 
				
			||||||
    document.querySelector("#detailedView-route .text-body-secondary").textContent = "";
 | 
					    document.querySelector("#detailedView-route .text-body-secondary").textContent = "";
 | 
				
			||||||
    document.getElementById("archonShards-card").classList.add("d-none");
 | 
					    document.getElementById("archonShards-card").classList.add("d-none");
 | 
				
			||||||
 | 
					    document.getElementById("edit-suit-invigorations-card").classList.add("d-none");
 | 
				
			||||||
    document.getElementById("modularParts-card").classList.add("d-none");
 | 
					    document.getElementById("modularParts-card").classList.add("d-none");
 | 
				
			||||||
    document.getElementById("modularParts-form").innerHTML = "";
 | 
					    document.getElementById("modularParts-form").innerHTML = "";
 | 
				
			||||||
    document.getElementById("valenceBonus-card").classList.add("d-none");
 | 
					    document.getElementById("valenceBonus-card").classList.add("d-none");
 | 
				
			||||||
@ -2499,9 +2513,17 @@ function formatDatetime(fmt, date) {
 | 
				
			|||||||
const calls_in_flight = new Set();
 | 
					const calls_in_flight = new Set();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function debounce(func, ...args) {
 | 
					async function debounce(func, ...args) {
 | 
				
			||||||
    calls_in_flight.add(func);
 | 
					    if (!func.name) {
 | 
				
			||||||
    await func(...args);
 | 
					        throw new Error(`cannot debounce anonymous functions`);
 | 
				
			||||||
    calls_in_flight.delete(func);
 | 
					    }
 | 
				
			||||||
 | 
					    const callid = JSON.stringify({ func: func.name, args });
 | 
				
			||||||
 | 
					    if (!calls_in_flight.has(callid)) {
 | 
				
			||||||
 | 
					        calls_in_flight.add(callid);
 | 
				
			||||||
 | 
					        await func(...args);
 | 
				
			||||||
 | 
					        calls_in_flight.delete(callid);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        console.log("debouncing", callid);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function doMaxPlexus() {
 | 
					async function doMaxPlexus() {
 | 
				
			||||||
 | 
				
			|||||||
@ -63,7 +63,7 @@ dict = {
 | 
				
			|||||||
    code_mature: `Für den Kampf auswachsen lassen`,
 | 
					    code_mature: `Für den Kampf auswachsen lassen`,
 | 
				
			||||||
    code_unmature: `Genetisches Altern zurücksetzen`,
 | 
					    code_unmature: `Genetisches Altern zurücksetzen`,
 | 
				
			||||||
    code_succChange: `Erfolgreich geändert.`,
 | 
					    code_succChange: `Erfolgreich geändert.`,
 | 
				
			||||||
    code_requiredInvigorationUpgrade: `[UNTRANSLATED] You must select both an offensive & defensive upgrade.`,
 | 
					    code_requiredInvigorationUpgrade: `Du musst sowohl ein offensives & defensives Upgrade auswählen.`,
 | 
				
			||||||
    login_description: `Melde dich mit deinem OpenWF-Account an (denselben Angaben wie im Spiel, wenn du dich mit diesem Server verbindest).`,
 | 
					    login_description: `Melde dich mit deinem OpenWF-Account an (denselben Angaben wie im Spiel, wenn du dich mit diesem Server verbindest).`,
 | 
				
			||||||
    login_emailLabel: `E-Mail-Adresse`,
 | 
					    login_emailLabel: `E-Mail-Adresse`,
 | 
				
			||||||
    login_passwordLabel: `Passwort`,
 | 
					    login_passwordLabel: `Passwort`,
 | 
				
			||||||
@ -127,33 +127,33 @@ dict = {
 | 
				
			|||||||
    detailedView_valenceBonusLabel: `Valenz-Bonus`,
 | 
					    detailedView_valenceBonusLabel: `Valenz-Bonus`,
 | 
				
			||||||
    detailedView_valenceBonusDescription: `Du kannst den Valenz-Bonus deiner Waffe festlegen oder entfernen.`,
 | 
					    detailedView_valenceBonusDescription: `Du kannst den Valenz-Bonus deiner Waffe festlegen oder entfernen.`,
 | 
				
			||||||
    detailedView_modularPartsLabel: `Modulare Teile ändern`,
 | 
					    detailedView_modularPartsLabel: `Modulare Teile ändern`,
 | 
				
			||||||
    detailedView_suitInvigorationLabel: `[UNTRANSLATED] Warframe Invigoration`,
 | 
					    detailedView_suitInvigorationLabel: `Warframe-Kräftigung`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    invigorations_offensive_AbilityStrength: `[UNTRANSLATED] +200% Ability Strength`,
 | 
					    invigorations_offensive_AbilityStrength: `+200% Fähigkeitsstärke`,
 | 
				
			||||||
    invigorations_offensive_AbilityRange: `[UNTRANSLATED] +100% Ability Range`,
 | 
					    invigorations_offensive_AbilityRange: `+100% Fähigkeitsreichweite`,
 | 
				
			||||||
    invigorations_offensive_AbilityDuration: `[UNTRANSLATED] +100% Ability Duration`,
 | 
					    invigorations_offensive_AbilityDuration: `+100% Fähigkeitsdauer`,
 | 
				
			||||||
    invigorations_offensive_MeleeDamage: `[UNTRANSLATED] +250% Melee Damage`,
 | 
					    invigorations_offensive_MeleeDamage: `+250% Nahkampfschaden`,
 | 
				
			||||||
    invigorations_offensive_PrimaryDamage: `[UNTRANSLATED] +250% Primary Damage`,
 | 
					    invigorations_offensive_PrimaryDamage: `+250% Primärwaffen Schaden`,
 | 
				
			||||||
    invigorations_offensive_SecondaryDamage: `[UNTRANSLATED] +250% Secondary Damage`,
 | 
					    invigorations_offensive_SecondaryDamage: `+250% Sekundärwaffen Schaden`,
 | 
				
			||||||
    invigorations_offensive_PrimaryCritChance: `[UNTRANSLATED] +200% Primary Critical Chance`,
 | 
					    invigorations_offensive_PrimaryCritChance: `+200% Primärwaffen Krit. Chance`,
 | 
				
			||||||
    invigorations_offensive_SecondaryCritChance: `[UNTRANSLATED] +200% Secondary Critical Chance`,
 | 
					    invigorations_offensive_SecondaryCritChance: `+200% Sekundärwaffen Krit. Chance`,
 | 
				
			||||||
    invigorations_offensive_MeleeCritChance: `[UNTRANSLATED] +200% Melee Critical Chance`,
 | 
					    invigorations_offensive_MeleeCritChance: `+200% Nahkampfwaffen Krit. Chance`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    invigorations_utility_AbilityEfficiency: `[UNTRANSLATED] +75% Ability Efficiency`,
 | 
					    invigorations_utility_AbilityEfficiency: `+75% Fähigkeitseffizienz`,
 | 
				
			||||||
    invigorations_utility_SprintSpeed: `[UNTRANSLATED] +75% Sprint Speed`,
 | 
					    invigorations_utility_SprintSpeed: `+75% Sprintgeschwindigkeit`,
 | 
				
			||||||
    invigorations_utility_ParkourVelocity: `[UNTRANSLATED] +75% Parkour Velocity`,
 | 
					    invigorations_utility_ParkourVelocity: `+75% Parkourgeschwindigkeit`,
 | 
				
			||||||
    invigorations_utility_HealthMax: `[UNTRANSLATED] +1000 Health`,
 | 
					    invigorations_utility_HealthMax: `+1000 Gesundheit`,
 | 
				
			||||||
    invigorations_utility_EnergyMax: `[UNTRANSLATED] +200% Energy Max`,
 | 
					    invigorations_utility_EnergyMax: `+200% Max. Energie`,
 | 
				
			||||||
    invigorations_utility_StatusImmune: `[UNTRANSLATED] Immune to Status Effects`,
 | 
					    invigorations_utility_StatusImmune: `Immun gegen Statuseffekte`,
 | 
				
			||||||
    invigorations_utility_ReloadSpeed: `[UNTRANSLATED] +75% Reload Speed`,
 | 
					    invigorations_utility_ReloadSpeed: `+75% Nachladegeschwindigkeit`,
 | 
				
			||||||
    invigorations_utility_HealthRegen: `[UNTRANSLATED] +25 Health Regen/s`,
 | 
					    invigorations_utility_HealthRegen: `+25 Gesundheitsregeneration pro Sekunde`,
 | 
				
			||||||
    invigorations_utility_ArmorMax: `[UNTRANSLATED] +1000 Armor`,
 | 
					    invigorations_utility_ArmorMax: `+1000 Rüstung`,
 | 
				
			||||||
    invigorations_utility_Jumps: `[UNTRANSLATED] +5 Jump Resets`,
 | 
					    invigorations_utility_Jumps: `+5 Sprung-Zurücksetzungen`,
 | 
				
			||||||
    invigorations_utility_EnergyRegen: `[UNTRANSLATED] +2 Energy Regen/s`,
 | 
					    invigorations_utility_EnergyRegen: `+2 Energieregeneration pro Sekunde`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    invigorations_offensiveLabel: `[UNTRANSLATED] Offensive Upgrade`,
 | 
					    invigorations_offensiveLabel: `Offensives Upgrade`,
 | 
				
			||||||
    invigorations_defensiveLabel: `[UNTRANSLATED] Defensive Upgrade`,
 | 
					    invigorations_defensiveLabel: `Defensives Upgrade`,
 | 
				
			||||||
    invigorations_expiryLabel: `[UNTRANSLATED] Upgrades Expiry (optional)`,
 | 
					    invigorations_expiryLabel: `Upgrades Ablaufdatum (optional)`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    mods_addRiven: `Riven hinzufügen`,
 | 
					    mods_addRiven: `Riven hinzufügen`,
 | 
				
			||||||
    mods_fingerprint: `Fingerabdruck`,
 | 
					    mods_fingerprint: `Fingerabdruck`,
 | 
				
			||||||
@ -306,7 +306,7 @@ dict = {
 | 
				
			|||||||
    upgrade_WarframeGlobeEffectHealth: `+|VAL|% Wirksamkeit bei Gesundheitskugeln`,
 | 
					    upgrade_WarframeGlobeEffectHealth: `+|VAL|% Wirksamkeit bei Gesundheitskugeln`,
 | 
				
			||||||
    upgrade_WarframeHealthMax: `+|VAL| Gesundheit`,
 | 
					    upgrade_WarframeHealthMax: `+|VAL| Gesundheit`,
 | 
				
			||||||
    upgrade_WarframeHPBoostFromImpact: `+|VAL1| Gesundheit beim Töten eines Gegners mit Explosionsschaden (Max. |VAL2| Gesundheit)`,
 | 
					    upgrade_WarframeHPBoostFromImpact: `+|VAL1| Gesundheit beim Töten eines Gegners mit Explosionsschaden (Max. |VAL2| Gesundheit)`,
 | 
				
			||||||
    upgrade_WarframeParkourVelocity: `+|VAL|% Parkour-Geschwindigkeit`,
 | 
					    upgrade_WarframeParkourVelocity: `+|VAL|% Parkourgeschwindigkeit`,
 | 
				
			||||||
    upgrade_WarframeRadiationDamageBoost: `+|VAL|% Fähigkeitsschaden auf Gegner, die von Strahlungs-Status betroffen sind`,
 | 
					    upgrade_WarframeRadiationDamageBoost: `+|VAL|% Fähigkeitsschaden auf Gegner, die von Strahlungs-Status betroffen sind`,
 | 
				
			||||||
    upgrade_WarframeHealthRegen: `+|VAL| Gesundheitsregeneration pro Sekunde`,
 | 
					    upgrade_WarframeHealthRegen: `+|VAL| Gesundheitsregeneration pro Sekunde`,
 | 
				
			||||||
    upgrade_WarframeShieldMax: `+|VAL| Schildkapazität`,
 | 
					    upgrade_WarframeShieldMax: `+|VAL| Schildkapazität`,
 | 
				
			||||||
@ -327,15 +327,15 @@ dict = {
 | 
				
			|||||||
    upgrade_OnFailHackReset: `+50% Chance, das Hacken bei Fehlschlag zu wiederholen`,
 | 
					    upgrade_OnFailHackReset: `+50% Chance, das Hacken bei Fehlschlag zu wiederholen`,
 | 
				
			||||||
    upgrade_DamageReductionOnHack: `+75% Schadensreduktion beim Hacken`,
 | 
					    upgrade_DamageReductionOnHack: `+75% Schadensreduktion beim Hacken`,
 | 
				
			||||||
    upgrade_OnExecutionReviveCompanion: `Gnadenstoß-Kills verkürzen die Erholungszeit des Begleiters um 15s`,
 | 
					    upgrade_OnExecutionReviveCompanion: `Gnadenstoß-Kills verkürzen die Erholungszeit des Begleiters um 15s`,
 | 
				
			||||||
    upgrade_OnExecutionParkourSpeed: `+60% Parkour-Geschwindigkeit für 15s nach Gnadenstoß`,
 | 
					    upgrade_OnExecutionParkourSpeed: `+60% Parkourgeschwindigkeit für 15s nach Gnadenstoß`,
 | 
				
			||||||
    upgrade_AvatarTimeLimitIncrease: `+8s extra Zeit beim Hacken`,
 | 
					    upgrade_AvatarTimeLimitIncrease: `+8s extra Zeit beim Hacken`,
 | 
				
			||||||
    upgrade_ElectrifyOnHack: `Setze beim Hacken Gegner innerhalb von 20m unter Strom`,
 | 
					    upgrade_ElectrifyOnHack: `Setze beim Hacken Gegner innerhalb von 20m unter Strom`,
 | 
				
			||||||
    upgrade_OnExecutionTerrify: `+50% Chance bei Gnadenstoß, dass Feinde innerhalb von 15m vor Furcht für 8s kauern`,
 | 
					    upgrade_OnExecutionTerrify: `+50% Chance bei Gnadenstoß, dass Feinde innerhalb von 15m vor Furcht für 8s kauern`,
 | 
				
			||||||
    upgrade_OnHackLockers: `Schließe nach dem Hacken 5 Spinde innerhalb von 20m auf`,
 | 
					    upgrade_OnHackLockers: `Schließe nach dem Hacken 5 Spinde innerhalb von 20m auf`,
 | 
				
			||||||
    upgrade_OnExecutionBlind: `Blende bei einem Gnadenstoß Gegner innerhalb von 18m`,
 | 
					    upgrade_OnExecutionBlind: `Blende bei einem Gnadenstoß Gegner innerhalb von 18m`,
 | 
				
			||||||
    upgrade_OnExecutionDrainPower: `Nächste Fähigkeit erhält +50% Fähigkeitsstärke nach Gnadenstoß`,
 | 
					    upgrade_OnExecutionDrainPower: `Nächste Fähigkeit erhält +50% Fähigkeitsstärke nach Gnadenstoß`,
 | 
				
			||||||
    upgrade_OnHackSprintSpeed: `+75% Sprint-Geschwindigkeit für 15s nach dem Hacken`,
 | 
					    upgrade_OnHackSprintSpeed: `+75% Sprintgeschwindigkeit für 15s nach dem Hacken`,
 | 
				
			||||||
    upgrade_SwiftExecute: `+50% Gnadenstoß-Geschwindigkeit`,
 | 
					    upgrade_SwiftExecute: `+50% Gnadenstoßgeschwindigkeit`,
 | 
				
			||||||
    upgrade_OnHackInvis: `+15s Unsichtbarkeit nach dem Hacken`,
 | 
					    upgrade_OnHackInvis: `+15s Unsichtbarkeit nach dem Hacken`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    damageType_Electricity: `Elektrizität`,
 | 
					    damageType_Electricity: `Elektrizität`,
 | 
				
			||||||
 | 
				
			|||||||
@ -63,7 +63,7 @@ dict = {
 | 
				
			|||||||
    code_mature: `Listo para el combate`,
 | 
					    code_mature: `Listo para el combate`,
 | 
				
			||||||
    code_unmature: `Regresar el envejecimiento genético`,
 | 
					    code_unmature: `Regresar el envejecimiento genético`,
 | 
				
			||||||
    code_succChange: `Cambiado correctamente`,
 | 
					    code_succChange: `Cambiado correctamente`,
 | 
				
			||||||
    code_requiredInvigorationUpgrade: `[UNTRANSLATED] You must select both an offensive & defensive upgrade.`,
 | 
					    code_requiredInvigorationUpgrade: `Debes seleccionar una mejora ofensiva y una defensiva.`,
 | 
				
			||||||
    login_description: `Inicia sesión con las credenciales de tu cuenta OpenWF (las mismas que usas en el juego al conectarte a este servidor).`,
 | 
					    login_description: `Inicia sesión con las credenciales de tu cuenta OpenWF (las mismas que usas en el juego al conectarte a este servidor).`,
 | 
				
			||||||
    login_emailLabel: `Dirección de correo electrónico`,
 | 
					    login_emailLabel: `Dirección de correo electrónico`,
 | 
				
			||||||
    login_passwordLabel: `Contraseña`,
 | 
					    login_passwordLabel: `Contraseña`,
 | 
				
			||||||
@ -127,33 +127,33 @@ dict = {
 | 
				
			|||||||
    detailedView_valenceBonusLabel: `Bônus de Valência`,
 | 
					    detailedView_valenceBonusLabel: `Bônus de Valência`,
 | 
				
			||||||
    detailedView_valenceBonusDescription: `Puedes establecer o quitar el bono de valencia de tu arma.`,
 | 
					    detailedView_valenceBonusDescription: `Puedes establecer o quitar el bono de valencia de tu arma.`,
 | 
				
			||||||
    detailedView_modularPartsLabel: `Cambiar partes modulares`,
 | 
					    detailedView_modularPartsLabel: `Cambiar partes modulares`,
 | 
				
			||||||
    detailedView_suitInvigorationLabel: `[UNTRANSLATED] Warframe Invigoration`,
 | 
					    detailedView_suitInvigorationLabel: `Vigorización de Warframe`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    invigorations_offensive_AbilityStrength: `[UNTRANSLATED] +200% Ability Strength`,
 | 
					    invigorations_offensive_AbilityStrength: `+200% Fuerza de Habilidad`,
 | 
				
			||||||
    invigorations_offensive_AbilityRange: `[UNTRANSLATED] +100% Ability Range`,
 | 
					    invigorations_offensive_AbilityRange: `+100% Alcance de Habilidad`,
 | 
				
			||||||
    invigorations_offensive_AbilityDuration: `[UNTRANSLATED] +100% Ability Duration`,
 | 
					    invigorations_offensive_AbilityDuration: `+100% Duración de Habilidad`,
 | 
				
			||||||
    invigorations_offensive_MeleeDamage: `[UNTRANSLATED] +250% Melee Damage`,
 | 
					    invigorations_offensive_MeleeDamage: `+250% Daño Cuerpo a Cuerpo`,
 | 
				
			||||||
    invigorations_offensive_PrimaryDamage: `[UNTRANSLATED] +250% Primary Damage`,
 | 
					    invigorations_offensive_PrimaryDamage: `+250% Daño de Arma Principal`,
 | 
				
			||||||
    invigorations_offensive_SecondaryDamage: `[UNTRANSLATED] +250% Secondary Damage`,
 | 
					    invigorations_offensive_SecondaryDamage: `+250% Daño de Arma Secundaria`,
 | 
				
			||||||
    invigorations_offensive_PrimaryCritChance: `[UNTRANSLATED] +200% Primary Critical Chance`,
 | 
					    invigorations_offensive_PrimaryCritChance: `+200% Probabilidad Crítica de Arma Principal`,
 | 
				
			||||||
    invigorations_offensive_SecondaryCritChance: `[UNTRANSLATED] +200% Secondary Critical Chance`,
 | 
					    invigorations_offensive_SecondaryCritChance: `+200% Probabilidad Crítica de Arma Secundaria`,
 | 
				
			||||||
    invigorations_offensive_MeleeCritChance: `[UNTRANSLATED] +200% Melee Critical Chance`,
 | 
					    invigorations_offensive_MeleeCritChance: `+200% Probabilidad Crítica Cuerpo a Cuerpo`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    invigorations_utility_AbilityEfficiency: `[UNTRANSLATED] +75% Ability Efficiency`,
 | 
					    invigorations_utility_AbilityEfficiency: `+75% Eficiencia de Habilidad`,
 | 
				
			||||||
    invigorations_utility_SprintSpeed: `[UNTRANSLATED] +75% Sprint Speed`,
 | 
					    invigorations_utility_SprintSpeed: `+75% Velocidad de Sprint`,
 | 
				
			||||||
    invigorations_utility_ParkourVelocity: `[UNTRANSLATED] +75% Parkour Velocity`,
 | 
					    invigorations_utility_ParkourVelocity: `+75% Velocidad de Parkour`,
 | 
				
			||||||
    invigorations_utility_HealthMax: `[UNTRANSLATED] +1000 Health`,
 | 
					    invigorations_utility_HealthMax: `+1000 Salud Máx.`,
 | 
				
			||||||
    invigorations_utility_EnergyMax: `[UNTRANSLATED] +200% Energy Max`,
 | 
					    invigorations_utility_EnergyMax: `+200% Energía Máx.`,
 | 
				
			||||||
    invigorations_utility_StatusImmune: `[UNTRANSLATED] Immune to Status Effects`,
 | 
					    invigorations_utility_StatusImmune: `Inmune a Efectos de Estado`,
 | 
				
			||||||
    invigorations_utility_ReloadSpeed: `[UNTRANSLATED] +75% Reload Speed`,
 | 
					    invigorations_utility_ReloadSpeed: `+75% Velocidad de Recarga`,
 | 
				
			||||||
    invigorations_utility_HealthRegen: `[UNTRANSLATED] +25 Health Regen/s`,
 | 
					    invigorations_utility_HealthRegen: `+25 Regeneración de Salud/s`,
 | 
				
			||||||
    invigorations_utility_ArmorMax: `[UNTRANSLATED] +1000 Armor`,
 | 
					    invigorations_utility_ArmorMax: `+1000 Armadura Máx.`,
 | 
				
			||||||
    invigorations_utility_Jumps: `[UNTRANSLATED] +5 Jump Resets`,
 | 
					    invigorations_utility_Jumps: `+5 Restablecimientos de Salto`,
 | 
				
			||||||
    invigorations_utility_EnergyRegen: `[UNTRANSLATED] +2 Energy Regen/s`,
 | 
					    invigorations_utility_EnergyRegen: `+2 Regeneración de Energía/s`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    invigorations_offensiveLabel: `[UNTRANSLATED] Offensive Upgrade`,
 | 
					    invigorations_offensiveLabel: `Mejora Ofensiva`,
 | 
				
			||||||
    invigorations_defensiveLabel: `[UNTRANSLATED] Defensive Upgrade`,
 | 
					    invigorations_defensiveLabel: `Mejora Defensiva`,
 | 
				
			||||||
    invigorations_expiryLabel: `[UNTRANSLATED] Upgrades Expiry (optional)`,
 | 
					    invigorations_expiryLabel: `Caducidad de Mejoras (opcional)`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    mods_addRiven: `Agregar Agrietado`,
 | 
					    mods_addRiven: `Agregar Agrietado`,
 | 
				
			||||||
    mods_fingerprint: `Huella digital`,
 | 
					    mods_fingerprint: `Huella digital`,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,11 @@
 | 
				
			|||||||
// French translation by Vitruvio
 | 
					// French translation by Vitruvio
 | 
				
			||||||
dict = {
 | 
					dict = {
 | 
				
			||||||
    general_inventoryUpdateNote: `[UNTRANSLATED] Note: To see changes in-game, you need to resync your inventory, e.g. using the bootstrapper's /sync command, visiting a dojo/relay, or relogging.`,
 | 
					    general_inventoryUpdateNote: `Note : Pour voir les changements en jeu, l'inventaire doit être actualisé. Cela se fait en tapant /sync dans le tchat, en visitant un dojo/relais ou en se reconnectant.`,
 | 
				
			||||||
    general_addButton: `Ajouter`,
 | 
					    general_addButton: `Ajouter`,
 | 
				
			||||||
    general_setButton: `[UNTRANSLATED] Set`,
 | 
					    general_setButton: `Définir`,
 | 
				
			||||||
    general_none: `Aucun`,
 | 
					    general_none: `Aucun`,
 | 
				
			||||||
    general_bulkActions: `Action groupée`,
 | 
					    general_bulkActions: `Action groupée`,
 | 
				
			||||||
    general_loading: `[UNTRANSLATED] Loading...`,
 | 
					    general_loading: `Chargement...`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    code_loginFail: `Connexion échouée. Vérifiez le mot de passe.`,
 | 
					    code_loginFail: `Connexion échouée. Vérifiez le mot de passe.`,
 | 
				
			||||||
    code_regFail: `Enregistrement impossible. Compte existant?`,
 | 
					    code_regFail: `Enregistrement impossible. Compte existant?`,
 | 
				
			||||||
@ -46,8 +46,8 @@ dict = {
 | 
				
			|||||||
    code_focusUnlocked: `|COUNT| écoles de Focus déverrouillées ! Synchronisation de l'inventaire nécessaire.`,
 | 
					    code_focusUnlocked: `|COUNT| écoles de Focus déverrouillées ! Synchronisation de l'inventaire nécessaire.`,
 | 
				
			||||||
    code_addModsConfirm: `Ajouter |COUNT| mods à l'inventaire ?`,
 | 
					    code_addModsConfirm: `Ajouter |COUNT| mods à l'inventaire ?`,
 | 
				
			||||||
    code_succImport: `Importé.`,
 | 
					    code_succImport: `Importé.`,
 | 
				
			||||||
    code_succRelog: `[UNTRANSLATED] Done. Please note that you'll need to relog to see a difference in-game.`,
 | 
					    code_succRelog: `Succès. Un redémarrage du jeu est nécessaire.`,
 | 
				
			||||||
    code_nothingToDo: `[UNTRANSLATED] Done. There was nothing to do.`,
 | 
					    code_nothingToDo: `Succès.`,
 | 
				
			||||||
    code_gild: `Polir`,
 | 
					    code_gild: `Polir`,
 | 
				
			||||||
    code_moa: `Moa`,
 | 
					    code_moa: `Moa`,
 | 
				
			||||||
    code_zanuka: `Molosse`,
 | 
					    code_zanuka: `Molosse`,
 | 
				
			||||||
@ -62,8 +62,8 @@ dict = {
 | 
				
			|||||||
    code_pigment: `Pigment`,
 | 
					    code_pigment: `Pigment`,
 | 
				
			||||||
    code_mature: `Maturer pour le combat`,
 | 
					    code_mature: `Maturer pour le combat`,
 | 
				
			||||||
    code_unmature: `Régrésser l'âge génétique`,
 | 
					    code_unmature: `Régrésser l'âge génétique`,
 | 
				
			||||||
    code_succChange: `[UNTRANSLATED] Successfully changed.`,
 | 
					    code_succChange: `Changement effectué.`,
 | 
				
			||||||
    code_requiredInvigorationUpgrade: `[UNTRANSLATED] You must select both an offensive & defensive upgrade.`,
 | 
					    code_requiredInvigorationUpgrade: `Augmentation offensive et défensive requises.`,
 | 
				
			||||||
    login_description: `Connexion avec les informations de connexion OpenWF.`,
 | 
					    login_description: `Connexion avec les informations de connexion OpenWF.`,
 | 
				
			||||||
    login_emailLabel: `Email`,
 | 
					    login_emailLabel: `Email`,
 | 
				
			||||||
    login_passwordLabel: `Mot de passe`,
 | 
					    login_passwordLabel: `Mot de passe`,
 | 
				
			||||||
@ -125,42 +125,42 @@ dict = {
 | 
				
			|||||||
    detailedView_archonShardsDescription: `Slots illimités pour appliquer plusieurs améliorations`,
 | 
					    detailedView_archonShardsDescription: `Slots illimités pour appliquer plusieurs améliorations`,
 | 
				
			||||||
    detailedView_archonShardsDescription2: `Un délai sera présent entre l'application des éclats et le chargement en jeu.`,
 | 
					    detailedView_archonShardsDescription2: `Un délai sera présent entre l'application des éclats et le chargement en jeu.`,
 | 
				
			||||||
    detailedView_valenceBonusLabel: `Bonus de Valence`,
 | 
					    detailedView_valenceBonusLabel: `Bonus de Valence`,
 | 
				
			||||||
    detailedView_valenceBonusDescription: `[UNTRANSLATED] You can set or remove the Valence Bonus from your weapon.`,
 | 
					    detailedView_valenceBonusDescription: `Définir le Bonus Valence de l'arme.`,
 | 
				
			||||||
    detailedView_modularPartsLabel: `[UNTRANSLATED] Change Modular Parts`,
 | 
					    detailedView_modularPartsLabel: `Changer l'équipement modulaire`,
 | 
				
			||||||
    detailedView_suitInvigorationLabel: `[UNTRANSLATED] Warframe Invigoration`,
 | 
					    detailedView_suitInvigorationLabel: `Invigoration de Warframe`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    invigorations_offensive_AbilityStrength: `[UNTRANSLATED] +200% Ability Strength`,
 | 
					    invigorations_offensive_AbilityStrength: `+200% de puissance de pouvoir`,
 | 
				
			||||||
    invigorations_offensive_AbilityRange: `[UNTRANSLATED] +100% Ability Range`,
 | 
					    invigorations_offensive_AbilityRange: `+100% de portée de pouvoir`,
 | 
				
			||||||
    invigorations_offensive_AbilityDuration: `[UNTRANSLATED] +100% Ability Duration`,
 | 
					    invigorations_offensive_AbilityDuration: `+100% de durée de pouvoir`,
 | 
				
			||||||
    invigorations_offensive_MeleeDamage: `[UNTRANSLATED] +250% Melee Damage`,
 | 
					    invigorations_offensive_MeleeDamage: `+250% de dégâts de mêlée`,
 | 
				
			||||||
    invigorations_offensive_PrimaryDamage: `[UNTRANSLATED] +250% Primary Damage`,
 | 
					    invigorations_offensive_PrimaryDamage: `+250% de dégâts d'arme primaire`,
 | 
				
			||||||
    invigorations_offensive_SecondaryDamage: `[UNTRANSLATED] +250% Secondary Damage`,
 | 
					    invigorations_offensive_SecondaryDamage: `+250% de dégâts d'arme secondaire`,
 | 
				
			||||||
    invigorations_offensive_PrimaryCritChance: `[UNTRANSLATED] +200% Primary Critical Chance`,
 | 
					    invigorations_offensive_PrimaryCritChance: `+200% de chances critique sur arme primaire`,
 | 
				
			||||||
    invigorations_offensive_SecondaryCritChance: `[UNTRANSLATED] +200% Secondary Critical Chance`,
 | 
					    invigorations_offensive_SecondaryCritChance: `+200% de chances critique sur arme secondaire`,
 | 
				
			||||||
    invigorations_offensive_MeleeCritChance: `[UNTRANSLATED] +200% Melee Critical Chance`,
 | 
					    invigorations_offensive_MeleeCritChance: `+200% de chances critique en mêlée`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    invigorations_utility_AbilityEfficiency: `[UNTRANSLATED] +75% Ability Efficiency`,
 | 
					    invigorations_utility_AbilityEfficiency: `+75% d'efficacité de pouvoir`,
 | 
				
			||||||
    invigorations_utility_SprintSpeed: `[UNTRANSLATED] +75% Sprint Speed`,
 | 
					    invigorations_utility_SprintSpeed: `+75% de vitesse de course`,
 | 
				
			||||||
    invigorations_utility_ParkourVelocity: `[UNTRANSLATED] +75% Parkour Velocity`,
 | 
					    invigorations_utility_ParkourVelocity: `+75% de vélocité de parkour`,
 | 
				
			||||||
    invigorations_utility_HealthMax: `[UNTRANSLATED] +1000 Health`,
 | 
					    invigorations_utility_HealthMax: `+1000 de vie`,
 | 
				
			||||||
    invigorations_utility_EnergyMax: `[UNTRANSLATED] +200% Energy Max`,
 | 
					    invigorations_utility_EnergyMax: `+200% d'énergie max`,
 | 
				
			||||||
    invigorations_utility_StatusImmune: `[UNTRANSLATED] Immune to Status Effects`,
 | 
					    invigorations_utility_StatusImmune: `Immunisé contre les effets de statut`,
 | 
				
			||||||
    invigorations_utility_ReloadSpeed: `[UNTRANSLATED] +75% Reload Speed`,
 | 
					    invigorations_utility_ReloadSpeed: `+75% de vitesse de rechargement`,
 | 
				
			||||||
    invigorations_utility_HealthRegen: `[UNTRANSLATED] +25 Health Regen/s`,
 | 
					    invigorations_utility_HealthRegen: `+25 de vie régénérés/s`,
 | 
				
			||||||
    invigorations_utility_ArmorMax: `[UNTRANSLATED] +1000 Armor`,
 | 
					    invigorations_utility_ArmorMax: `+1000 d'armure`,
 | 
				
			||||||
    invigorations_utility_Jumps: `[UNTRANSLATED] +5 Jump Resets`,
 | 
					    invigorations_utility_Jumps: `+5 réinitialisations de saut`,
 | 
				
			||||||
    invigorations_utility_EnergyRegen: `[UNTRANSLATED] +2 Energy Regen/s`,
 | 
					    invigorations_utility_EnergyRegen: `+2 d'énergie régénérés/s`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    invigorations_offensiveLabel: `[UNTRANSLATED] Offensive Upgrade`,
 | 
					    invigorations_offensiveLabel: `Amélioration offensive`,
 | 
				
			||||||
    invigorations_defensiveLabel: `[UNTRANSLATED] Defensive Upgrade`,
 | 
					    invigorations_defensiveLabel: `Amélioration défensive`,
 | 
				
			||||||
    invigorations_expiryLabel: `[UNTRANSLATED] Upgrades Expiry (optional)`,
 | 
					    invigorations_expiryLabel: `Expiration de l'invigoration (optionnel)`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    mods_addRiven: `Ajouter un riven`,
 | 
					    mods_addRiven: `Ajouter un riven`,
 | 
				
			||||||
    mods_fingerprint: `Empreinte`,
 | 
					    mods_fingerprint: `Empreinte`,
 | 
				
			||||||
    mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`,
 | 
					    mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`,
 | 
				
			||||||
    mods_rivens: `Rivens`,
 | 
					    mods_rivens: `Rivens`,
 | 
				
			||||||
    mods_mods: `Mods`,
 | 
					    mods_mods: `Mods`,
 | 
				
			||||||
    mods_addMax: `[UNTRANSLATED] Add Maxed`,
 | 
					    mods_addMax: `Ajouter les mods niveau max`,
 | 
				
			||||||
    mods_addMissingUnrankedMods: `Ajouter les mods sans rang manquants`,
 | 
					    mods_addMissingUnrankedMods: `Ajouter les mods sans rang manquants`,
 | 
				
			||||||
    mods_removeUnranked: `Retirer les mods sans rang`,
 | 
					    mods_removeUnranked: `Retirer les mods sans rang`,
 | 
				
			||||||
    mods_addMissingMaxRankMods: `Ajouter les mods niveau max manquants`,
 | 
					    mods_addMissingMaxRankMods: `Ajouter les mods niveau max manquants`,
 | 
				
			||||||
@ -170,7 +170,7 @@ dict = {
 | 
				
			|||||||
    cheats_skipAllDialogue: `Passer les dialogues`,
 | 
					    cheats_skipAllDialogue: `Passer les dialogues`,
 | 
				
			||||||
    cheats_unlockAllScans: `Débloquer tous les scans`,
 | 
					    cheats_unlockAllScans: `Débloquer tous les scans`,
 | 
				
			||||||
    cheats_unlockAllMissions: `Débloquer toutes les missions`,
 | 
					    cheats_unlockAllMissions: `Débloquer toutes les missions`,
 | 
				
			||||||
    cheats_unlockAllMissions_ok: `[UNTRANSLATED] Success. Please note that you'll need to enter a dojo/relay or relog for the client to refresh the star chart.`,
 | 
					    cheats_unlockAllMissions_ok: `Succès. Une actualisation de l'inventaire est nécessaire.`,
 | 
				
			||||||
    cheats_infiniteCredits: `Crédits infinis`,
 | 
					    cheats_infiniteCredits: `Crédits infinis`,
 | 
				
			||||||
    cheats_infinitePlatinum: `Platinum infini`,
 | 
					    cheats_infinitePlatinum: `Platinum infini`,
 | 
				
			||||||
    cheats_infiniteEndo: `Endo infini`,
 | 
					    cheats_infiniteEndo: `Endo infini`,
 | 
				
			||||||
@ -201,8 +201,8 @@ dict = {
 | 
				
			|||||||
    cheats_noDeathMarks: `Aucune marque d'assassin`,
 | 
					    cheats_noDeathMarks: `Aucune marque d'assassin`,
 | 
				
			||||||
    cheats_noKimCooldowns: `Aucun cooldown sur le KIM`,
 | 
					    cheats_noKimCooldowns: `Aucun cooldown sur le KIM`,
 | 
				
			||||||
    cheats_fullyStockedVendors: `Les vendeurs ont un stock à 100%`,
 | 
					    cheats_fullyStockedVendors: `Les vendeurs ont un stock à 100%`,
 | 
				
			||||||
    cheats_baroAlwaysAvailable: `[UNTRANSLATED] Baro Always Available`,
 | 
					    cheats_baroAlwaysAvailable: `Baro toujours présent`,
 | 
				
			||||||
    cheats_baroFullyStocked: `[UNTRANSLATED] Baro Fully Stocked`,
 | 
					    cheats_baroFullyStocked: `Stock de Baro au max`,
 | 
				
			||||||
    cheats_syndicateMissionsRepeatable: `Mission syndicat répétables`,
 | 
					    cheats_syndicateMissionsRepeatable: `Mission syndicat répétables`,
 | 
				
			||||||
    cheats_unlockAllProfitTakerStages: `Débloquer toutes les étapes du Preneur de Profit`,
 | 
					    cheats_unlockAllProfitTakerStages: `Débloquer toutes les étapes du Preneur de Profit`,
 | 
				
			||||||
    cheats_instantFinishRivenChallenge: `Débloquer le challenge Riven instantanément`,
 | 
					    cheats_instantFinishRivenChallenge: `Débloquer le challenge Riven instantanément`,
 | 
				
			||||||
@ -216,75 +216,75 @@ dict = {
 | 
				
			|||||||
    cheats_noDojoResearchTime: `Aucun temps de recherche (Dojo)`,
 | 
					    cheats_noDojoResearchTime: `Aucun temps de recherche (Dojo)`,
 | 
				
			||||||
    cheats_fastClanAscension: `Ascension de clan rapide`,
 | 
					    cheats_fastClanAscension: `Ascension de clan rapide`,
 | 
				
			||||||
    cheats_missionsCanGiveAllRelics: `Les missions donnent toutes les reliques`,
 | 
					    cheats_missionsCanGiveAllRelics: `Les missions donnent toutes les reliques`,
 | 
				
			||||||
    cheats_exceptionalRelicsAlwaysGiveBronzeReward: `[UNTRANSLATED] Exceptional Relics Always Give Bronze Reward`,
 | 
					    cheats_exceptionalRelicsAlwaysGiveBronzeReward: `Les reliques exceptionnelles donnent toujours une récompense en bronze`,
 | 
				
			||||||
    cheats_flawlessRelicsAlwaysGiveSilverReward: `[UNTRANSLATED] Flawless Relics Always Give Silver Reward`,
 | 
					    cheats_flawlessRelicsAlwaysGiveSilverReward: `Les reliques parfaites donnent toujours une récompense en argent`,
 | 
				
			||||||
    cheats_radiantRelicsAlwaysGiveGoldReward: `[UNTRANSLATED] Radiant Relics Always Give Gold Reward`,
 | 
					    cheats_radiantRelicsAlwaysGiveGoldReward: `Les reliques éclatantes donnent toujours une récompense en or`,
 | 
				
			||||||
    cheats_unlockAllSimarisResearchEntries: `Débloquer toute les recherches chez Simaris`,
 | 
					    cheats_unlockAllSimarisResearchEntries: `Débloquer toute les recherches chez Simaris`,
 | 
				
			||||||
    cheats_disableDailyTribute: `[UNTRANSLATED] Disable Daily Tribute`,
 | 
					    cheats_disableDailyTribute: `Désactiver la récompense quotidienne de connexion`,
 | 
				
			||||||
    cheats_spoofMasteryRank: `Rang de maîtrise personnalisé (-1 pour désactiver)`,
 | 
					    cheats_spoofMasteryRank: `Rang de maîtrise personnalisé (-1 pour désactiver)`,
 | 
				
			||||||
    cheats_relicRewardItemCountMultiplier: `[UNTRANSLATED] Relic Reward Item Count Multiplier`,
 | 
					    cheats_relicRewardItemCountMultiplier: `Multiplicateur de récompenses de relique`,
 | 
				
			||||||
    cheats_nightwaveStandingMultiplier: `Multiplicateur de réputation d'Ondes Nocturnes`,
 | 
					    cheats_nightwaveStandingMultiplier: `Multiplicateur de réputation d'Ondes Nocturnes`,
 | 
				
			||||||
    cheats_save: `Sauvegarder`,
 | 
					    cheats_save: `Sauvegarder`,
 | 
				
			||||||
    cheats_account: `Compte`,
 | 
					    cheats_account: `Compte`,
 | 
				
			||||||
    cheats_unlockAllFocusSchools: `Débloquer toutes les écoles de focus`,
 | 
					    cheats_unlockAllFocusSchools: `Débloquer toutes les écoles de focus`,
 | 
				
			||||||
    cheats_helminthUnlockAll: `Helminth niveau max`,
 | 
					    cheats_helminthUnlockAll: `Helminth niveau max`,
 | 
				
			||||||
    cheats_addMissingSubsumedAbilities: `[UNTRANSLATED] Add Missing Subsumed Abilities`,
 | 
					    cheats_addMissingSubsumedAbilities: `Ajouter les capacités subsumées manquantes`,
 | 
				
			||||||
    cheats_intrinsicsUnlockAll: `Inhérences niveau max`,
 | 
					    cheats_intrinsicsUnlockAll: `Inhérences niveau max`,
 | 
				
			||||||
    cheats_changeSupportedSyndicate: `Allégeance`,
 | 
					    cheats_changeSupportedSyndicate: `Allégeance`,
 | 
				
			||||||
    cheats_changeButton: `Changer`,
 | 
					    cheats_changeButton: `Changer`,
 | 
				
			||||||
    cheats_markAllAsRead: `[UNTRANSLATED] Mark Inbox As Read`,
 | 
					    cheats_markAllAsRead: `Marquer la boîte de réception comme lue`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    worldState: `[UNTRANSLATED] World State`,
 | 
					    worldState: `Carte Solaire`,
 | 
				
			||||||
    worldState_creditBoost: `[UNTRANSLATED] Credit Boost`,
 | 
					    worldState_creditBoost: `Booster de Crédit`,
 | 
				
			||||||
    worldState_affinityBoost: `[UNTRANSLATED] Affinity Boost`,
 | 
					    worldState_affinityBoost: `Booster d'Affinité`,
 | 
				
			||||||
    worldState_resourceBoost: `[UNTRANSLATED] Resource Boost`,
 | 
					    worldState_resourceBoost: `Booster de Ressource`,
 | 
				
			||||||
    worldState_starDays: `[UNTRANSLATED] Star Days`,
 | 
					    worldState_starDays: `Jours Stellaires`,
 | 
				
			||||||
    worldState_galleonOfGhouls: `[UNTRANSLATED] Galleon of Ghouls`,
 | 
					    worldState_galleonOfGhouls: `Galion des Goules`,
 | 
				
			||||||
    disabled: `[UNTRANSLATED] Disabled`,
 | 
					    disabled: `Désactivé`,
 | 
				
			||||||
    worldState_we1: `[UNTRANSLATED] Weekend 1`,
 | 
					    worldState_we1: `Weekend 1`,
 | 
				
			||||||
    worldState_we2: `[UNTRANSLATED] Weekend 2`,
 | 
					    worldState_we2: `Weekend 2`,
 | 
				
			||||||
    worldState_we3: `[UNTRANSLATED] Weekend 3`,
 | 
					    worldState_we3: `Weekend 3`,
 | 
				
			||||||
    worldState_eidolonOverride: `[UNTRANSLATED] Eidolon Override`,
 | 
					    worldState_eidolonOverride: `Météo Plaines d'Eidolon`,
 | 
				
			||||||
    worldState_day: `[UNTRANSLATED] Day`,
 | 
					    worldState_day: `Jour`,
 | 
				
			||||||
    worldState_night: `[UNTRANSLATED] Night`,
 | 
					    worldState_night: `Nuit`,
 | 
				
			||||||
    worldState_vallisOverride: `[UNTRANSLATED] Orb Vallis Override`,
 | 
					    worldState_vallisOverride: `Météo Vallée Orbis`,
 | 
				
			||||||
    worldState_warm: `[UNTRANSLATED] Warm`,
 | 
					    worldState_warm: `Chaud`,
 | 
				
			||||||
    worldState_cold: `[UNTRANSLATED] Cold`,
 | 
					    worldState_cold: `Froid`,
 | 
				
			||||||
    worldState_duviriOverride: `[UNTRANSLATED] Duviri Override`,
 | 
					    worldState_duviriOverride: `Spirale Duviri`,
 | 
				
			||||||
    worldState_joy: `[UNTRANSLATED] Joy`,
 | 
					    worldState_joy: `Joie`,
 | 
				
			||||||
    worldState_anger: `[UNTRANSLATED] Anger`,
 | 
					    worldState_anger: `Colère`,
 | 
				
			||||||
    worldState_envy: `[UNTRANSLATED] Envy`,
 | 
					    worldState_envy: `Envie `,
 | 
				
			||||||
    worldState_sorrow: `[UNTRANSLATED] Sorrow`,
 | 
					    worldState_sorrow: `hagrin`,
 | 
				
			||||||
    worldState_fear: `[UNTRANSLATED] Fear`,
 | 
					    worldState_fear: `Peur`,
 | 
				
			||||||
    worldState_nightwaveOverride: `[UNTRANSLATED] Nightwave Override`,
 | 
					    worldState_nightwaveOverride: `Saison d'Ondes Nocturnes`,
 | 
				
			||||||
    worldState_RadioLegionIntermission13Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 9`,
 | 
					    worldState_RadioLegionIntermission13Syndicate: `Mix de Nora Vol. 9`,
 | 
				
			||||||
    worldState_RadioLegionIntermission12Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 8`,
 | 
					    worldState_RadioLegionIntermission12Syndicate: `Mix de Nora Vol. 8`,
 | 
				
			||||||
    worldState_RadioLegionIntermission11Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 7`,
 | 
					    worldState_RadioLegionIntermission11Syndicate: `Mix de Nora Vol. 7`,
 | 
				
			||||||
    worldState_RadioLegionIntermission10Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 6`,
 | 
					    worldState_RadioLegionIntermission10Syndicate: `Mix de Nora Vol. 6`,
 | 
				
			||||||
    worldState_RadioLegionIntermission9Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 5`,
 | 
					    worldState_RadioLegionIntermission9Syndicate: `Mix de Nora Vol. 5`,
 | 
				
			||||||
    worldState_RadioLegionIntermission8Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 4`,
 | 
					    worldState_RadioLegionIntermission8Syndicate: `Mix de Nora Vol. 4`,
 | 
				
			||||||
    worldState_RadioLegionIntermission7Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 3`,
 | 
					    worldState_RadioLegionIntermission7Syndicate: `Mix de Nora Vol. 3`,
 | 
				
			||||||
    worldState_RadioLegionIntermission6Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 2`,
 | 
					    worldState_RadioLegionIntermission6Syndicate: `Mix de Nora Vol. 2`,
 | 
				
			||||||
    worldState_RadioLegionIntermission5Syndicate: `[UNTRANSLATED] Nora's Mix Vol. 1`,
 | 
					    worldState_RadioLegionIntermission5Syndicate: `Mix de Nora Vol. 1`,
 | 
				
			||||||
    worldState_RadioLegionIntermission4Syndicate: `[UNTRANSLATED] Nora's Choice`,
 | 
					    worldState_RadioLegionIntermission4Syndicate: `La Sélection de Nora`,
 | 
				
			||||||
    worldState_RadioLegionIntermission3Syndicate: `[UNTRANSLATED] Intermission III`,
 | 
					    worldState_RadioLegionIntermission3Syndicate: `Intermission III`,
 | 
				
			||||||
    worldState_RadioLegion3Syndicate: `[UNTRANSLATED] Glassmaker`,
 | 
					    worldState_RadioLegion3Syndicate: `Les Mystères du Verre`,
 | 
				
			||||||
    worldState_RadioLegionIntermission2Syndicate: `[UNTRANSLATED] Intermission II`,
 | 
					    worldState_RadioLegionIntermission2Syndicate: `Intermission II`,
 | 
				
			||||||
    worldState_RadioLegion2Syndicate: `[UNTRANSLATED] The Emissary`,
 | 
					    worldState_RadioLegion2Syndicate: `L'Émissaire`,
 | 
				
			||||||
    worldState_RadioLegionIntermissionSyndicate: `[UNTRANSLATED] Intermission I`,
 | 
					    worldState_RadioLegionIntermissionSyndicate: `Intermission I`,
 | 
				
			||||||
    worldState_RadioLegionSyndicate: `[UNTRANSLATED] The Wolf of Saturn Six`,
 | 
					    worldState_RadioLegionSyndicate: `Le Loup de Saturne Six`,
 | 
				
			||||||
    worldState_fissures: `[UNTRANSLATED] Fissures`,
 | 
					    worldState_fissures: `Fissures`,
 | 
				
			||||||
    normal: `[UNTRANSLATED] Normal`,
 | 
					    normal: `Normal`,
 | 
				
			||||||
    worldState_allAtOnceNormal: `[UNTRANSLATED] All At Once, Normal`,
 | 
					    worldState_allAtOnceNormal: `Toutes, Normal`,
 | 
				
			||||||
    worldState_allAtOnceSteelPath: `[UNTRANSLATED] All At Once, Steel Path`,
 | 
					    worldState_allAtOnceSteelPath: `Toutes, Route de l'Acier`,
 | 
				
			||||||
    worldState_theCircuitOverride: `[UNTRANSLATED] The Circuit Override`,
 | 
					    worldState_theCircuitOverride: `Remplacement du Circuit`,
 | 
				
			||||||
    worldState_darvoStockMultiplier: `[UNTRANSLATED] Darvo Stock Multiplier`,
 | 
					    worldState_darvoStockMultiplier: `Multiplicateur du stock de Darvo`,
 | 
				
			||||||
    worldState_varziaFullyStocked: `[UNTRANSLATED] Varzia Fully Stocked`,
 | 
					    worldState_varziaFullyStocked: `Stock de Varzia au max`,
 | 
				
			||||||
    worldState_varziaOverride: `[UNTRANSLATED] Varzia Rotation Override`,
 | 
					    worldState_varziaOverride: `Rotation de Varzia`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    import_importNote: `Import manuel. Toutes les modifcations supportées par l'inventaire <b>écraseront celles présentes dans la base de données</b>.`,
 | 
					    import_importNote: `Import manuel. Toutes les modifcations supportées par l'inventaire <b>écraseront celles présentes dans la base de données</b>.`,
 | 
				
			||||||
    import_submit: `Soumettre`,
 | 
					    import_submit: `Soumettre`,
 | 
				
			||||||
    import_samples: `Echantillons :`,
 | 
					    import_samples: `Échantillons :`,
 | 
				
			||||||
    import_samples_maxFocus: `Toutes les écoles de focus au rang max`,
 | 
					    import_samples_maxFocus: `Toutes les écoles de focus au rang max`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    upgrade_Equilibrium: `Ramasser de la santé donne +|VAL|% d'énergie supplémentaire. Ramasser de l'énergie donne +|VAL|% de santé supplémentaire.`,
 | 
					    upgrade_Equilibrium: `Ramasser de la santé donne +|VAL|% d'énergie supplémentaire. Ramasser de l'énergie donne +|VAL|% de santé supplémentaire.`,
 | 
				
			||||||
@ -305,7 +305,7 @@ dict = {
 | 
				
			|||||||
    upgrade_WarframeGlobeEffectEnergy: `+|VAL|% d'efficacité d'orbe d'énergie`,
 | 
					    upgrade_WarframeGlobeEffectEnergy: `+|VAL|% d'efficacité d'orbe d'énergie`,
 | 
				
			||||||
    upgrade_WarframeGlobeEffectHealth: `+|VAL|% d'efficacité d'orbe de santé`,
 | 
					    upgrade_WarframeGlobeEffectHealth: `+|VAL|% d'efficacité d'orbe de santé`,
 | 
				
			||||||
    upgrade_WarframeHealthMax: `+|VAL| de santé`,
 | 
					    upgrade_WarframeHealthMax: `+|VAL| de santé`,
 | 
				
			||||||
    upgrade_WarframeHPBoostFromImpact: `[UNTRANSLATED] +|VAL1| Health on kill with Blast Damage (Max |VAL2| Health)`,
 | 
					    upgrade_WarframeHPBoostFromImpact: `+|VAL1| de vie sur élimination avec des dégâts d'explostion (Max |VAL2| Health)`,
 | 
				
			||||||
    upgrade_WarframeParkourVelocity: `+|VAL|% de vélocité de parkour`,
 | 
					    upgrade_WarframeParkourVelocity: `+|VAL|% de vélocité de parkour`,
 | 
				
			||||||
    upgrade_WarframeRadiationDamageBoost: `+|VAL|% de dégâts de pouvoir sur les ennemis affectés par du statut radiation`,
 | 
					    upgrade_WarframeRadiationDamageBoost: `+|VAL|% de dégâts de pouvoir sur les ennemis affectés par du statut radiation`,
 | 
				
			||||||
    upgrade_WarframeHealthRegen: `+|VAL| régénération de santé/s`,
 | 
					    upgrade_WarframeHealthRegen: `+|VAL| régénération de santé/s`,
 | 
				
			||||||
@ -324,7 +324,7 @@ dict = {
 | 
				
			|||||||
    upgrade_OnExecutionAmmo: `100% de rechargement des armes primaires et secondaires sur une une miséricorde`,
 | 
					    upgrade_OnExecutionAmmo: `100% de rechargement des armes primaires et secondaires sur une une miséricorde`,
 | 
				
			||||||
    upgrade_OnExecutionHealthDrop: `100% de chance de drop une orbe de santé sur une miséricorde`,
 | 
					    upgrade_OnExecutionHealthDrop: `100% de chance de drop une orbe de santé sur une miséricorde`,
 | 
				
			||||||
    upgrade_OnExecutionEnergyDrop: `50% de chance de drop une orbe d'énergie sur une miséricorde`,
 | 
					    upgrade_OnExecutionEnergyDrop: `50% de chance de drop une orbe d'énergie sur une miséricorde`,
 | 
				
			||||||
    upgrade_OnFailHackReset: `[UNTRANSLATED] +50% Hacking Retry Chance`,
 | 
					    upgrade_OnFailHackReset: `+50% de chance de refaire un piratage`,
 | 
				
			||||||
    upgrade_DamageReductionOnHack: `75% de réduction de dégâts pendant un piratage`,
 | 
					    upgrade_DamageReductionOnHack: `75% de réduction de dégâts pendant un piratage`,
 | 
				
			||||||
    upgrade_OnExecutionReviveCompanion: `Les miséricordes réduisent le temps de récupération du compagnon de 15s`,
 | 
					    upgrade_OnExecutionReviveCompanion: `Les miséricordes réduisent le temps de récupération du compagnon de 15s`,
 | 
				
			||||||
    upgrade_OnExecutionParkourSpeed: `+60% de vitesse de parkour pendant 15s après une miséricorde`,
 | 
					    upgrade_OnExecutionParkourSpeed: `+60% de vitesse de parkour pendant 15s après une miséricorde`,
 | 
				
			||||||
@ -333,10 +333,10 @@ dict = {
 | 
				
			|||||||
    upgrade_OnExecutionTerrify: `Les ennemis dans un rayon de 15m ont 50% de chance de s'enfuir après une miséricorde`,
 | 
					    upgrade_OnExecutionTerrify: `Les ennemis dans un rayon de 15m ont 50% de chance de s'enfuir après une miséricorde`,
 | 
				
			||||||
    upgrade_OnHackLockers: `5 casiers s'ouvrent dans un rayon de 20m après un piratage`,
 | 
					    upgrade_OnHackLockers: `5 casiers s'ouvrent dans un rayon de 20m après un piratage`,
 | 
				
			||||||
    upgrade_OnExecutionBlind: `Les ennemis sont aveuglés dans un rayon de 18 après une miséricorde`,
 | 
					    upgrade_OnExecutionBlind: `Les ennemis sont aveuglés dans un rayon de 18 après une miséricorde`,
 | 
				
			||||||
    upgrade_OnExecutionDrainPower: `[UNTRANSLATED] Next ability cast gains +50% Ability Strength on Mercy`,
 | 
					    upgrade_OnExecutionDrainPower: `Le prochain pouvoir activé gagne +50% de puissance de pouvoir après une miséricorde`,
 | 
				
			||||||
    upgrade_OnHackSprintSpeed: `+75% de vitesse de course pendant 15s après un piratage`,
 | 
					    upgrade_OnHackSprintSpeed: `+75% de vitesse de course pendant 15s après un piratage`,
 | 
				
			||||||
    upgrade_SwiftExecute: `[UNTRANSLATED] +50% Mercy Kill Speed`,
 | 
					    upgrade_SwiftExecute: `+50% de vitesse de d'éxecution en miséricorde`,
 | 
				
			||||||
    upgrade_OnHackInvis: `[UNTRANSLATED] Invisible for 15 seconds after Hacking`,
 | 
					    upgrade_OnHackInvis: `Invisible pendant 15s après un piratage`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    damageType_Electricity: `Électrique`,
 | 
					    damageType_Electricity: `Électrique`,
 | 
				
			||||||
    damageType_Fire: `Feu`,
 | 
					    damageType_Fire: `Feu`,
 | 
				
			||||||
@ -346,8 +346,8 @@ dict = {
 | 
				
			|||||||
    damageType_Poison: `Poison`,
 | 
					    damageType_Poison: `Poison`,
 | 
				
			||||||
    damageType_Radiation: `Radiations`,
 | 
					    damageType_Radiation: `Radiations`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    theme_dark: `[UNTRANSLATED] Dark Theme`,
 | 
					    theme_dark: `Thème sombre`,
 | 
				
			||||||
    theme_light: `[UNTRANSLATED] Light Theme`,
 | 
					    theme_light: `Thème clair`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    prettier_sucks_ass: ``
 | 
					    prettier_sucks_ass: ``
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -63,7 +63,7 @@ dict = {
 | 
				
			|||||||
    code_mature: `成长并战备`,
 | 
					    code_mature: `成长并战备`,
 | 
				
			||||||
    code_unmature: `逆转衰老基因`,
 | 
					    code_unmature: `逆转衰老基因`,
 | 
				
			||||||
    code_succChange: `更改成功.`,
 | 
					    code_succChange: `更改成功.`,
 | 
				
			||||||
    code_requiredInvigorationUpgrade: `[UNTRANSLATED] You must select both an offensive & defensive upgrade.`,
 | 
					    code_requiredInvigorationUpgrade: `您必须同时选择一个进攻型和一个功能型活化属性.`,
 | 
				
			||||||
    login_description: `使用您的 OpenWF 账户凭证登录(与游戏内连接本服务器时使用的昵称相同).`,
 | 
					    login_description: `使用您的 OpenWF 账户凭证登录(与游戏内连接本服务器时使用的昵称相同).`,
 | 
				
			||||||
    login_emailLabel: `电子邮箱`,
 | 
					    login_emailLabel: `电子邮箱`,
 | 
				
			||||||
    login_passwordLabel: `密码`,
 | 
					    login_passwordLabel: `密码`,
 | 
				
			||||||
@ -127,29 +127,29 @@ dict = {
 | 
				
			|||||||
    detailedView_valenceBonusLabel: `效价加成`,
 | 
					    detailedView_valenceBonusLabel: `效价加成`,
 | 
				
			||||||
    detailedView_valenceBonusDescription: `您可以设置或移除武器上的效价加成.`,
 | 
					    detailedView_valenceBonusDescription: `您可以设置或移除武器上的效价加成.`,
 | 
				
			||||||
    detailedView_modularPartsLabel: `更换部件`,
 | 
					    detailedView_modularPartsLabel: `更换部件`,
 | 
				
			||||||
    detailedView_suitInvigorationLabel: `[UNTRANSLATED] Warframe Invigoration`,
 | 
					    detailedView_suitInvigorationLabel: `编辑战甲活化属性`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    invigorations_offensive_AbilityStrength: `[UNTRANSLATED] +200% Ability Strength`,
 | 
					    invigorations_offensive_AbilityStrength: `+200%技能强度`,
 | 
				
			||||||
    invigorations_offensive_AbilityRange: `[UNTRANSLATED] +100% Ability Range`,
 | 
					    invigorations_offensive_AbilityRange: `+100%技能范围`,
 | 
				
			||||||
    invigorations_offensive_AbilityDuration: `[UNTRANSLATED] +100% Ability Duration`,
 | 
					    invigorations_offensive_AbilityDuration: `+100%技能持续时间`,
 | 
				
			||||||
    invigorations_offensive_MeleeDamage: `[UNTRANSLATED] +250% Melee Damage`,
 | 
					    invigorations_offensive_MeleeDamage: `+250%近战武器伤害`,
 | 
				
			||||||
    invigorations_offensive_PrimaryDamage: `[UNTRANSLATED] +250% Primary Damage`,
 | 
					    invigorations_offensive_PrimaryDamage: `+250%主要武器伤害`,
 | 
				
			||||||
    invigorations_offensive_SecondaryDamage: `[UNTRANSLATED] +250% Secondary Damage`,
 | 
					    invigorations_offensive_SecondaryDamage: `+250%次要武器伤害`,
 | 
				
			||||||
    invigorations_offensive_PrimaryCritChance: `[UNTRANSLATED] +200% Primary Critical Chance`,
 | 
					    invigorations_offensive_PrimaryCritChance: `+200%主要武器暴击几率`,
 | 
				
			||||||
    invigorations_offensive_SecondaryCritChance: `[UNTRANSLATED] +200% Secondary Critical Chance`,
 | 
					    invigorations_offensive_SecondaryCritChance: `+200%次要武器暴击几率`,
 | 
				
			||||||
    invigorations_offensive_MeleeCritChance: `[UNTRANSLATED] +200% Melee Critical Chance`,
 | 
					    invigorations_offensive_MeleeCritChance: `+200%近战武器暴击几率`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    invigorations_utility_AbilityEfficiency: `[UNTRANSLATED] +75% Ability Efficiency`,
 | 
					    invigorations_utility_AbilityEfficiency: `+75%技能效率`,
 | 
				
			||||||
    invigorations_utility_SprintSpeed: `[UNTRANSLATED] +75% Sprint Speed`,
 | 
					    invigorations_utility_SprintSpeed: `+75%冲刺速度`,
 | 
				
			||||||
    invigorations_utility_ParkourVelocity: `[UNTRANSLATED] +75% Parkour Velocity`,
 | 
					    invigorations_utility_ParkourVelocity: `+75%跑酷速度`,
 | 
				
			||||||
    invigorations_utility_HealthMax: `[UNTRANSLATED] +1000 Health`,
 | 
					    invigorations_utility_HealthMax: `+1000生命值`,
 | 
				
			||||||
    invigorations_utility_EnergyMax: `[UNTRANSLATED] +200% Energy Max`,
 | 
					    invigorations_utility_EnergyMax: `+200%最大能量`,
 | 
				
			||||||
    invigorations_utility_StatusImmune: `[UNTRANSLATED] Immune to Status Effects`,
 | 
					    invigorations_utility_StatusImmune: `异常状态免疫`,
 | 
				
			||||||
    invigorations_utility_ReloadSpeed: `[UNTRANSLATED] +75% Reload Speed`,
 | 
					    invigorations_utility_ReloadSpeed: `+75%装填速度`,
 | 
				
			||||||
    invigorations_utility_HealthRegen: `[UNTRANSLATED] +25 Health Regen/s`,
 | 
					    invigorations_utility_HealthRegen: `+25/秒生命再生`,
 | 
				
			||||||
    invigorations_utility_ArmorMax: `[UNTRANSLATED] +1000 Armor`,
 | 
					    invigorations_utility_ArmorMax: `+1000护甲值`,
 | 
				
			||||||
    invigorations_utility_Jumps: `[UNTRANSLATED] +5 Jump Resets`,
 | 
					    invigorations_utility_Jumps: `+5跳跃次数`,
 | 
				
			||||||
    invigorations_utility_EnergyRegen: `[UNTRANSLATED] +2 Energy Regen/s`,
 | 
					    invigorations_utility_EnergyRegen: `+2/秒能量再生`,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    invigorations_offensiveLabel: `进攻型属性`,
 | 
					    invigorations_offensiveLabel: `进攻型属性`,
 | 
				
			||||||
    invigorations_defensiveLabel: `功能型属性`,
 | 
					    invigorations_defensiveLabel: `功能型属性`,
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user