Compare commits
15 Commits
a85b65fca8
...
8659500408
| Author | SHA1 | Date | |
|---|---|---|---|
| 8659500408 | |||
| 5688b1725f | |||
| 2eb7f7ed11 | |||
| f3a6f60222 | |||
| 55d051ff37 | |||
| 9f94a17eda | |||
| 711eb7ac47 | |||
| 90ffd8948b | |||
| 8a0f99f573 | |||
| 55e2871531 | |||
| 8088044ec8 | |||
| 6167eeadb0 | |||
| 9c1ba17537 | |||
| 85a446f67f | |||
| 48b7138c1c |
@@ -14,6 +14,7 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi
|
||||
|
||||
- `skipTutorial` affects only newly created accounts, so you may wish to change it before logging in for the first time.
|
||||
- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
|
||||
- `bindAddress`, `httpPort`, `httpsPort` are related to how SpaceNinjaServer is reached on the network. Under Docker, these options are unchangable; modify your `docker-compose.yml`, instead.
|
||||
- `ircExecutable` can be provided with a relative path to an EXE which will be ran as a child process of SpaceNinjaServer.
|
||||
- `ircAddress`, `hubAddress`, and `nrsAddress` can be provided if these secondary servers are on a different machine.
|
||||
- `worldState.eidolonOverride` can be set to `day` or `night` to lock the time to day/fass and night/vome on Plains of Eidolon/Cambion Drift.
|
||||
|
||||
@@ -68,6 +68,14 @@
|
||||
"circuitGameModes": null,
|
||||
"darvoStockMultiplier": 1
|
||||
},
|
||||
"tunables": {
|
||||
"useLoginToken": false,
|
||||
"prohibitSkipMissionStartTimer": false,
|
||||
"prohibitFovOverride": false,
|
||||
"prohibitFreecam": false,
|
||||
"prohibitTeleport": false,
|
||||
"prohibitScripts": false
|
||||
},
|
||||
"dev": {
|
||||
"keepVendorsExpired": false
|
||||
}
|
||||
|
||||
@@ -7,13 +7,16 @@ services:
|
||||
- ./docker-data/static-data:/app/static/data
|
||||
- ./docker-data/logs:/app/logs
|
||||
ports:
|
||||
# The lefthand value determines the port you actually connect to. Within the container, SpaceNinjaServer will always use 80 and 443 (righthand values).
|
||||
- 80:80
|
||||
- 443:443
|
||||
|
||||
# Normally, the image is fetched from Docker Hub, but you can use the local Dockerfile by removing "image" above and adding this:
|
||||
#build: .
|
||||
# Works best when using `docker-compose up --force-recreate --build`.
|
||||
# Works best when using `docker compose up --force-recreate --build`.
|
||||
|
||||
environment:
|
||||
- CHOKIDAR_USEPOLLING=true
|
||||
depends_on:
|
||||
- mongodb
|
||||
mongodb:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
set -e
|
||||
|
||||
if [ ! -f conf/config.json ]; then
|
||||
jq --arg value "mongodb://mongodb:27017/openWF" '.mongodbUrl = $value' /app/config-vanilla.json > /app/conf/config.json
|
||||
jq --arg value "mongodb://mongodb:27017/openWF" '.mongodbUrl = $value | del(.bindAddress) | del(.httpPort) | del(.httpsPort)' /app/config-vanilla.json > /app/conf/config.json
|
||||
fi
|
||||
|
||||
exec npm run raw -- --configPath conf/config.json
|
||||
exec npm run raw -- --configPath conf/config.json --docker
|
||||
|
||||
24
src/controllers/api/checkPendingRecipesController.ts
Normal file
24
src/controllers/api/checkPendingRecipesController.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
|
||||
import type { RequestHandler } from "express";
|
||||
import { getInventory } from "../../services/inventoryService.ts";
|
||||
|
||||
export const checkPendingRecipesController: RequestHandler = async (req, res) => {
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
const inventory = await getInventory(accountId, "PendingRecipes");
|
||||
const now = Date.now();
|
||||
const resp: ICheckPendingRecipesResponse = {
|
||||
PendingRecipes: inventory.PendingRecipes.map(recipe => ({
|
||||
ItemType: recipe.ItemType,
|
||||
SecondsRemaining: Math.max(0, Math.floor((recipe.CompletionDate.getTime() - now) / 1000))
|
||||
}))
|
||||
};
|
||||
|
||||
res.send(resp);
|
||||
};
|
||||
|
||||
interface ICheckPendingRecipesResponse {
|
||||
PendingRecipes: {
|
||||
ItemType: string;
|
||||
SecondsRemaining: number;
|
||||
}[];
|
||||
}
|
||||
@@ -22,14 +22,15 @@ import {
|
||||
import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
|
||||
import type { IPendingRecipeDatabase } from "../../types/inventoryTypes/inventoryTypes.ts";
|
||||
import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";
|
||||
import { fromOid, toOid2 } from "../../helpers/inventoryHelpers.ts";
|
||||
import { fromOid, toOid2, version_compare } from "../../helpers/inventoryHelpers.ts";
|
||||
import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
|
||||
import type { IRecipe } from "warframe-public-export-plus";
|
||||
import type { IEquipmentClient } from "../../types/equipmentTypes.ts";
|
||||
import { EquipmentFeatures, Status } from "../../types/equipmentTypes.ts";
|
||||
|
||||
interface IClaimCompletedRecipeRequest {
|
||||
RecipeIds: IOidWithLegacySupport[];
|
||||
RecipeIds?: IOidWithLegacySupport[]; // U24.4 and beyond
|
||||
recipeNames?: string[]; // Builds before U24.4 down to U22.20
|
||||
}
|
||||
|
||||
interface IClaimCompletedRecipeResponse {
|
||||
@@ -38,22 +39,60 @@ interface IClaimCompletedRecipeResponse {
|
||||
}
|
||||
|
||||
export const claimCompletedRecipeController: RequestHandler = async (req, res) => {
|
||||
const claimCompletedRecipeRequest = getJSONfromString<IClaimCompletedRecipeRequest>(String(req.body));
|
||||
const account = await getAccountForRequest(req);
|
||||
const inventory = await getInventory(account._id.toString());
|
||||
const resp: IClaimCompletedRecipeResponse = {
|
||||
InventoryChanges: {}
|
||||
};
|
||||
for (const recipeId of claimCompletedRecipeRequest.RecipeIds) {
|
||||
const pendingRecipe = inventory.PendingRecipes.id(fromOid(recipeId));
|
||||
if (!pendingRecipe) {
|
||||
throw new Error(`no pending recipe found with id ${fromOid(recipeId)}`);
|
||||
}
|
||||
if (!req.query.recipeName) {
|
||||
const claimCompletedRecipeRequest = getJSONfromString<IClaimCompletedRecipeRequest>(String(req.body));
|
||||
const recipes = claimCompletedRecipeRequest.recipeNames ?? claimCompletedRecipeRequest.RecipeIds;
|
||||
if (recipes) {
|
||||
for (const recipeId of recipes) {
|
||||
let pendingRecipe;
|
||||
if (typeof recipeId === "string") {
|
||||
pendingRecipe = inventory.PendingRecipes.find(r => r.ItemType == recipeId);
|
||||
if (!pendingRecipe) {
|
||||
throw new Error(`no pending recipe found of type ${recipeId}`);
|
||||
}
|
||||
} else {
|
||||
pendingRecipe = inventory.PendingRecipes.id(fromOid(recipeId));
|
||||
if (!pendingRecipe) {
|
||||
throw new Error(`no pending recipe found with id ${fromOid(recipeId)}`);
|
||||
}
|
||||
}
|
||||
|
||||
//check recipe is indeed ready to be completed
|
||||
// if (pendingRecipe.CompletionDate > new Date()) {
|
||||
// throw new Error(`recipe ${pendingRecipe._id} is not ready to be completed`);
|
||||
// }
|
||||
//check recipe is indeed ready to be completed
|
||||
// if (pendingRecipe.CompletionDate > new Date()) {
|
||||
// throw new Error(`recipe ${pendingRecipe._id} is not ready to be completed`);
|
||||
// }
|
||||
|
||||
inventory.PendingRecipes.pull(pendingRecipe._id);
|
||||
|
||||
const recipe = getRecipe(pendingRecipe.ItemType);
|
||||
if (!recipe) {
|
||||
throw new Error(`no completed item found for recipe ${pendingRecipe._id.toString()}`);
|
||||
}
|
||||
|
||||
if (req.query.cancel) {
|
||||
const inventoryChanges: IInventoryChanges = {};
|
||||
await refundRecipeIngredients(inventory, inventoryChanges, recipe, pendingRecipe);
|
||||
await inventory.save();
|
||||
res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
|
||||
return;
|
||||
}
|
||||
|
||||
await claimCompletedRecipe(account, inventory, recipe, pendingRecipe, resp, req.query.rush);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`recipe list from request was undefined?`);
|
||||
}
|
||||
} else {
|
||||
const recipeName = String(req.query.recipeName); // U8
|
||||
const pendingRecipe = inventory.PendingRecipes.find(r => r.ItemType == recipeName);
|
||||
if (!pendingRecipe) {
|
||||
throw new Error(`no pending recipe found with ItemType ${recipeName}`);
|
||||
}
|
||||
|
||||
inventory.PendingRecipes.pull(pendingRecipe._id);
|
||||
|
||||
@@ -61,16 +100,14 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
|
||||
if (!recipe) {
|
||||
throw new Error(`no completed item found for recipe ${pendingRecipe._id.toString()}`);
|
||||
}
|
||||
|
||||
if (req.query.cancel) {
|
||||
const inventoryChanges: IInventoryChanges = {};
|
||||
await refundRecipeIngredients(inventory, inventoryChanges, recipe, pendingRecipe);
|
||||
await inventory.save();
|
||||
res.json(inventoryChanges); // Not a bug: In the specific case of cancelling a recipe, InventoryChanges are expected to be the root.
|
||||
return;
|
||||
}
|
||||
|
||||
await claimCompletedRecipe(account, inventory, recipe, pendingRecipe, resp, req.query.rush);
|
||||
await claimCompletedRecipe(
|
||||
account,
|
||||
inventory,
|
||||
recipe,
|
||||
pendingRecipe,
|
||||
resp,
|
||||
req.path.includes("instantCompleteRecipe.php") || req.query.rush
|
||||
);
|
||||
}
|
||||
await inventory.save();
|
||||
res.json(resp);
|
||||
@@ -121,13 +158,20 @@ const claimCompletedRecipe = async (
|
||||
}
|
||||
|
||||
if (rush) {
|
||||
let cost = recipe.skipBuildTimePrice;
|
||||
const end = Math.trunc(pendingRecipe.CompletionDate.getTime() / 1000);
|
||||
const start = end - recipe.buildTime;
|
||||
const secondsElapsed = Math.trunc(Date.now() / 1000) - start;
|
||||
const progress = secondsElapsed / recipe.buildTime;
|
||||
logger.debug(`rushing recipe at ${Math.trunc(progress * 100)}% completion`);
|
||||
const cost =
|
||||
progress > 0.5 ? Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5))) : recipe.skipBuildTimePrice;
|
||||
// U18 introduced rush cost scaling, don't use it for older versions.
|
||||
if (account.BuildLabel && version_compare(account.BuildLabel, "2015.12.03.00.00") >= 0) {
|
||||
// Haven't found the real build label for U18.0.0 yet, but this works
|
||||
cost =
|
||||
progress > 0.5
|
||||
? Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5)))
|
||||
: recipe.skipBuildTimePrice;
|
||||
}
|
||||
combineInventoryChanges(resp.InventoryChanges, updateCurrency(inventory, cost, true));
|
||||
}
|
||||
|
||||
|
||||
@@ -25,11 +25,24 @@ export const sellController: RequestHandler = async (req, res) => {
|
||||
//console.log(JSON.stringify(payload, null, 2));
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
const requiredFields = new Set<keyof TInventoryDatabaseDocument>();
|
||||
if (payload.SellCurrency == "SC_RegularCredits") {
|
||||
let sellCurrency = "SC_RegularCredits";
|
||||
if (payload.SellCurrency) {
|
||||
sellCurrency = payload.SellCurrency;
|
||||
} else {
|
||||
if (payload.SellForFusionPoints || payload.SellForPrimeBucks) {
|
||||
if (payload.SellForFusionPoints) {
|
||||
sellCurrency = "SC_FusionPoints";
|
||||
}
|
||||
if (payload.SellForPrimeBucks) {
|
||||
sellCurrency = "SC_PrimeBucks";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sellCurrency == "SC_RegularCredits") {
|
||||
requiredFields.add("RegularCredits");
|
||||
} else if (payload.SellCurrency == "SC_FusionPoints") {
|
||||
} else if (sellCurrency == "SC_FusionPoints") {
|
||||
requiredFields.add("FusionPoints");
|
||||
} else if (payload.SellCurrency == "SC_CrewShipFusionPoints") {
|
||||
} else if (sellCurrency == "SC_CrewShipFusionPoints") {
|
||||
requiredFields.add("CrewShipFusionPoints");
|
||||
} else {
|
||||
requiredFields.add("MiscItems");
|
||||
@@ -83,27 +96,27 @@ export const sellController: RequestHandler = async (req, res) => {
|
||||
const inventory = await getInventory(accountId, Array.from(requiredFields).join(" "));
|
||||
|
||||
// Give currency
|
||||
if (payload.SellCurrency == "SC_RegularCredits") {
|
||||
if (sellCurrency == "SC_RegularCredits") {
|
||||
inventory.RegularCredits += payload.SellPrice;
|
||||
} else if (payload.SellCurrency == "SC_FusionPoints") {
|
||||
} else if (sellCurrency == "SC_FusionPoints") {
|
||||
addFusionPoints(inventory, payload.SellPrice);
|
||||
} else if (payload.SellCurrency == "SC_CrewShipFusionPoints") {
|
||||
} else if (sellCurrency == "SC_CrewShipFusionPoints") {
|
||||
addCrewShipFusionPoints(inventory, payload.SellPrice);
|
||||
} else if (payload.SellCurrency == "SC_PrimeBucks") {
|
||||
} else if (sellCurrency == "SC_PrimeBucks") {
|
||||
addMiscItems(inventory, [
|
||||
{
|
||||
ItemType: "/Lotus/Types/Items/MiscItems/PrimeBucks",
|
||||
ItemCount: payload.SellPrice
|
||||
}
|
||||
]);
|
||||
} else if (payload.SellCurrency == "SC_DistillPoints") {
|
||||
} else if (sellCurrency == "SC_DistillPoints") {
|
||||
addMiscItems(inventory, [
|
||||
{
|
||||
ItemType: "/Lotus/Types/Items/MiscItems/DistillPoints",
|
||||
ItemCount: payload.SellPrice
|
||||
}
|
||||
]);
|
||||
} else if (payload.SellCurrency == "SC_Resources") {
|
||||
} else if (sellCurrency == "SC_Resources") {
|
||||
// Will add appropriate MiscItems from CrewShipWeapons or CrewShipWeaponSkins
|
||||
} else {
|
||||
throw new Error("Unknown SellCurrency: " + payload.SellCurrency);
|
||||
@@ -218,7 +231,7 @@ export const sellController: RequestHandler = async (req, res) => {
|
||||
} else {
|
||||
const index = inventory.CrewShipWeapons.findIndex(x => x._id.equals(sellItem.String));
|
||||
if (index != -1) {
|
||||
if (payload.SellCurrency == "SC_Resources") {
|
||||
if (sellCurrency == "SC_Resources") {
|
||||
refundPartialBuildCosts(inventory, inventory.CrewShipWeapons[index].ItemType, inventoryChanges);
|
||||
}
|
||||
inventory.CrewShipWeapons.splice(index, 1);
|
||||
@@ -241,7 +254,7 @@ export const sellController: RequestHandler = async (req, res) => {
|
||||
} else {
|
||||
const index = inventory.CrewShipWeaponSkins.findIndex(x => x._id.equals(sellItem.String));
|
||||
if (index != -1) {
|
||||
if (payload.SellCurrency == "SC_Resources") {
|
||||
if (sellCurrency == "SC_Resources") {
|
||||
refundPartialBuildCosts(
|
||||
inventory,
|
||||
inventory.CrewShipWeaponSkins[index].ItemType,
|
||||
@@ -346,7 +359,7 @@ interface ISellRequest {
|
||||
WeaponSkins?: ISellItem[]; // SNS specific field
|
||||
};
|
||||
SellPrice: number;
|
||||
SellCurrency:
|
||||
SellCurrency?:
|
||||
| "SC_RegularCredits"
|
||||
| "SC_PrimeBucks"
|
||||
| "SC_FusionPoints"
|
||||
@@ -355,6 +368,9 @@ interface ISellRequest {
|
||||
| "SC_Resources"
|
||||
| "somethingelsewemightnotknowabout";
|
||||
buildLabel: string;
|
||||
// These are used in old builds (undetermined where it changed) instead of SellCurrency
|
||||
SellForPrimeBucks?: boolean;
|
||||
SellForFusionPoints?: boolean;
|
||||
}
|
||||
|
||||
interface ISellItem {
|
||||
|
||||
@@ -24,7 +24,8 @@ export const startRecipeController: RequestHandler = async (req, res) => {
|
||||
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
|
||||
const recipeName = startRecipeRequest.RecipeName;
|
||||
let recipeName = startRecipeRequest.RecipeName;
|
||||
if (req.query.recipeName) recipeName = String(req.query.recipeName); // U8
|
||||
const recipe = getRecipe(recipeName);
|
||||
|
||||
if (!recipe) {
|
||||
@@ -68,7 +69,9 @@ export const startRecipeController: RequestHandler = async (req, res) => {
|
||||
freeUpSlot(inventory, InventorySlot.WEAPONS);
|
||||
}
|
||||
} else {
|
||||
await addItem(inventory, recipe.ingredients[i].ItemType, recipe.ingredients[i].ItemCount * -1);
|
||||
const itemType = recipe.ingredients[i].ItemType;
|
||||
const itemCount = recipe.ingredients[i].ItemCount;
|
||||
await addItem(inventory, itemType, itemCount * -1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export const completeAllMissionsController: RequestHandler = async (req, res) =>
|
||||
await handleStoreItemAcquisition(reward.StoreItem, inventory, reward.ItemCount, undefined, true);
|
||||
}
|
||||
addString(inventory.NodeIntrosCompleted, "TeshinHardModeUnlocked");
|
||||
addString(inventory.NodeIntrosCompleted, "CetusInvasionNodeIntro");
|
||||
addString(inventory.NodeIntrosCompleted, "CetusSyndicate_IntroJob");
|
||||
let syndicate = inventory.Affiliations.find(x => x.Tag == "CetusSyndicate");
|
||||
if (!syndicate) {
|
||||
|
||||
22
src/controllers/custom/setUmbraEchoesController.ts
Normal file
22
src/controllers/custom/setUmbraEchoesController.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
|
||||
import { getInventory } from "../../services/inventoryService.ts";
|
||||
import type { RequestHandler } from "express";
|
||||
import { broadcastInventoryUpdate } from "../../services/wsService.ts";
|
||||
|
||||
export const setUmbraEchoesController: RequestHandler = async (req, res) => {
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
const request = req.body as ISetUmbraEchoesRequest;
|
||||
const inventory = await getInventory(accountId, "Suits");
|
||||
const suit = inventory.Suits.id(request.oid);
|
||||
if (suit) {
|
||||
suit.UmbraDate = request.UmbraDate ? new Date(request.UmbraDate) : undefined;
|
||||
await inventory.save();
|
||||
broadcastInventoryUpdate(req);
|
||||
}
|
||||
res.end();
|
||||
};
|
||||
|
||||
interface ISetUmbraEchoesRequest {
|
||||
oid: string;
|
||||
UmbraDate: number;
|
||||
}
|
||||
@@ -3,10 +3,11 @@ import allScans from "../../../static/fixed_responses/allScans.json" with { type
|
||||
import { ExportEnemies } from "warframe-public-export-plus";
|
||||
import { getAccountIdForRequest } from "../../services/loginService.ts";
|
||||
import { getStats } from "../../services/statsService.ts";
|
||||
import { getInventory } from "../../services/inventoryService.ts";
|
||||
|
||||
export const unlockAllScansController: RequestHandler = async (req, res) => {
|
||||
const accountId = await getAccountIdForRequest(req);
|
||||
const stats = await getStats(accountId);
|
||||
const [stats, inventory] = await Promise.all([getStats(accountId), getInventory(accountId, "ChallengeProgress")]);
|
||||
|
||||
const scanTypes = new Set<string>(allScans);
|
||||
for (const type of Object.keys(ExportEnemies.avatars)) {
|
||||
@@ -18,6 +19,17 @@ export const unlockAllScansController: RequestHandler = async (req, res) => {
|
||||
stats.Scans.push({ type, scans: 9999 });
|
||||
}
|
||||
|
||||
await stats.save();
|
||||
const jsCodex = inventory.ChallengeProgress.find(x => x.Name === "JSCodexScan");
|
||||
|
||||
if (jsCodex) {
|
||||
jsCodex.Progress = 1;
|
||||
} else {
|
||||
inventory.ChallengeProgress.push({
|
||||
Name: "JSCodexScan",
|
||||
Progress: 1
|
||||
});
|
||||
}
|
||||
|
||||
await Promise.all([stats.save(), inventory.save()]);
|
||||
res.end();
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getLeaderboard } from "../../services/leaderboardService.ts";
|
||||
import type { RequestHandler } from "express";
|
||||
|
||||
export const leaderboardController: RequestHandler = async (req, res) => {
|
||||
export const leaderboardPostController: RequestHandler = async (req, res) => {
|
||||
const payload = JSON.parse(String(req.body)) as ILeaderboardRequest;
|
||||
res.json({
|
||||
results: await getLeaderboard(
|
||||
@@ -15,6 +15,33 @@ export const leaderboardController: RequestHandler = async (req, res) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const leaderboardGetController: RequestHandler = async (req, res) => {
|
||||
const payload: ILeaderboardRequest = {
|
||||
field: "archived." + String(req.query.field),
|
||||
before: Number(req.query.before),
|
||||
after: Number(req.query.after),
|
||||
pivotId: req.query.pivotAccountId ? String(req.query.pivotAccountId) : undefined,
|
||||
guildId: undefined,
|
||||
guildTier: undefined
|
||||
};
|
||||
res.json({
|
||||
players: (
|
||||
await getLeaderboard(
|
||||
payload.field,
|
||||
payload.before,
|
||||
payload.after,
|
||||
payload.pivotId,
|
||||
payload.guildId,
|
||||
payload.guildTier
|
||||
)
|
||||
).map(entry => ({
|
||||
DisplayName: entry.n,
|
||||
score: entry.s,
|
||||
rank: entry.r
|
||||
}))
|
||||
});
|
||||
};
|
||||
|
||||
interface ILeaderboardRequest {
|
||||
field: string;
|
||||
before: number;
|
||||
|
||||
@@ -2,6 +2,7 @@ interface IArguments {
|
||||
configPath?: string;
|
||||
dev?: boolean;
|
||||
secret?: string;
|
||||
docker?: boolean;
|
||||
}
|
||||
|
||||
export const args: IArguments = {};
|
||||
@@ -19,5 +20,9 @@ for (let i = 2; i < process.argv.length; ) {
|
||||
case "--secret":
|
||||
args.secret = process.argv[i++];
|
||||
break;
|
||||
|
||||
case "--docker":
|
||||
args.docker = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { cancelGuildAdvertisementController } from "../controllers/api/cancelGui
|
||||
import { changeDojoRootController } from "../controllers/api/changeDojoRootController.ts";
|
||||
import { changeGuildRankController } from "../controllers/api/changeGuildRankController.ts";
|
||||
import { checkDailyMissionBonusController } from "../controllers/api/checkDailyMissionBonusController.ts";
|
||||
import { checkPendingRecipesController } from "../controllers/api/checkPendingRecipesController.ts";
|
||||
import { claimCompletedRecipeController } from "../controllers/api/claimCompletedRecipeController.ts";
|
||||
import { claimJunctionChallengeRewardController } from "../controllers/api/claimJunctionChallengeRewardController.ts";
|
||||
import { claimLibraryDailyTaskRewardController } from "../controllers/api/claimLibraryDailyTaskRewardController.ts";
|
||||
@@ -184,6 +185,7 @@ apiRouter.get("/cancelGuildAdvertisement.php", cancelGuildAdvertisementControlle
|
||||
apiRouter.get("/changeDojoRoot.php", changeDojoRootController);
|
||||
apiRouter.get("/changeGuildRank.php", changeGuildRankController);
|
||||
apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController);
|
||||
apiRouter.get("/checkPendingRecipes.php", checkPendingRecipesController); // U8
|
||||
apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController);
|
||||
apiRouter.get("/completeCalendarEvent.php", completeCalendarEventController);
|
||||
apiRouter.get("/confirmAllianceInvitation.php", confirmAllianceInvitationController);
|
||||
@@ -305,6 +307,7 @@ apiRouter.post("/guildTech.php", guildTechController);
|
||||
apiRouter.post("/hostSession.php", hostSessionController);
|
||||
apiRouter.post("/hubBlessing.php", hubBlessingController);
|
||||
apiRouter.post("/infestedFoundry.php", infestedFoundryController);
|
||||
apiRouter.post("/instantCompleteRecipe.php", claimCompletedRecipeController); // U8
|
||||
apiRouter.post("/inventorySlots.php", inventorySlotsController);
|
||||
apiRouter.post("/joinSession.php", joinSessionController);
|
||||
apiRouter.post("/login.php", loginController);
|
||||
|
||||
@@ -46,6 +46,7 @@ import { updateFingerprintController } from "../controllers/custom/updateFingerp
|
||||
import { unlockLevelCapController } from "../controllers/custom/unlockLevelCapController.ts";
|
||||
import { changeModularPartsController } from "../controllers/custom/changeModularPartsController.ts";
|
||||
import { setInvigorationController } from "../controllers/custom/setInvigorationController.ts";
|
||||
import { setUmbraEchoesController } from "../controllers/custom/setUmbraEchoesController.ts";
|
||||
import { setAccountCheatController } from "../controllers/custom/setAccountCheatController.ts";
|
||||
import { setGuildCheatController } from "../controllers/custom/setGuildCheatController.ts";
|
||||
|
||||
@@ -97,6 +98,7 @@ customRouter.post("/updateFingerprint", updateFingerprintController);
|
||||
customRouter.post("/unlockLevelCap", unlockLevelCapController);
|
||||
customRouter.post("/changeModularParts", changeModularPartsController);
|
||||
customRouter.post("/setInvigoration", setInvigorationController);
|
||||
customRouter.post("/setUmbraEchoes", setUmbraEchoesController);
|
||||
customRouter.post("/setAccountCheat", setAccountCheatController);
|
||||
customRouter.post("/setGuildCheat", setGuildCheatController);
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import express from "express";
|
||||
import { viewController } from "../controllers/stats/viewController.ts";
|
||||
import { uploadController } from "../controllers/stats/uploadController.ts";
|
||||
import { leaderboardController } from "../controllers/stats/leaderboardController.ts";
|
||||
import { leaderboardPostController, leaderboardGetController } from "../controllers/stats/leaderboardController.ts";
|
||||
|
||||
const statsRouter = express.Router();
|
||||
|
||||
statsRouter.get("/view.php", viewController);
|
||||
statsRouter.get("/profileStats.php", viewController);
|
||||
statsRouter.get("/leaderboard.php", leaderboardGetController);
|
||||
statsRouter.post("/upload.php", uploadController);
|
||||
statsRouter.post("/leaderboardWeekly.php", leaderboardController);
|
||||
statsRouter.post("/leaderboardArchived.php", leaderboardController);
|
||||
statsRouter.post("/leaderboardWeekly.php", leaderboardPostController);
|
||||
statsRouter.post("/leaderboardArchived.php", leaderboardPostController);
|
||||
|
||||
export { statsRouter };
|
||||
|
||||
@@ -160,6 +160,11 @@ export const configRemovedOptionsKeys = [
|
||||
"relicRewardItemCountMultiplier",
|
||||
"nightwaveStandingMultiplier"
|
||||
];
|
||||
if (args.docker) {
|
||||
configRemovedOptionsKeys.push("bindAddress");
|
||||
configRemovedOptionsKeys.push("httpPort");
|
||||
configRemovedOptionsKeys.push("httpsPort");
|
||||
}
|
||||
|
||||
export const configPath = path.join(repoDir, args.configPath ?? "config.json");
|
||||
|
||||
|
||||
@@ -808,6 +808,7 @@ export const addItem = async (
|
||||
typeName.substring(1).split("/")[3] == "KubrowPet"
|
||||
) {
|
||||
if (
|
||||
typeName != "/Lotus/Types/Game/KubrowPet/Eggs/KubrowEgg" &&
|
||||
typeName != "/Lotus/Types/Game/KubrowPet/Eggs/KubrowPetEggItem" &&
|
||||
typeName != "/Lotus/Types/Game/KubrowPet/BlankTraitPrint" &&
|
||||
typeName != "/Lotus/Types/Game/KubrowPet/ImprintedTraitPrint"
|
||||
@@ -1848,8 +1849,8 @@ export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray:
|
||||
|
||||
if (MiscItems[itemIndex].ItemCount == 0) {
|
||||
MiscItems.splice(itemIndex, 1);
|
||||
} else if (MiscItems[itemIndex].ItemCount <= 0) {
|
||||
logger.warn(`inventory.MiscItems has a negative count for ${ItemType}`);
|
||||
} else if (MiscItems[itemIndex].ItemCount < 0) {
|
||||
throw new Error(`inventory.MiscItems has a negative count for ${ItemType} after subtracting ${ItemCount}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1870,8 +1871,10 @@ const applyArrayChanges = (
|
||||
arr[itemIndex].ItemCount += change.ItemCount;
|
||||
if (arr[itemIndex].ItemCount == 0) {
|
||||
arr.splice(itemIndex, 1);
|
||||
} else if (arr[itemIndex].ItemCount <= 0) {
|
||||
logger.warn(`inventory.${key} has a negative count for ${change.ItemType}`);
|
||||
} else if (arr[itemIndex].ItemCount < 0) {
|
||||
throw new Error(
|
||||
`inventory.${key} has a negative count for ${change.ItemType} after subtracting ${change.ItemCount}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1917,8 +1920,10 @@ export const addMods = (inventory: TInventoryDatabaseDocument, itemsArray: IRawU
|
||||
RawUpgrades[itemIndex].ItemCount += ItemCount;
|
||||
if (RawUpgrades[itemIndex].ItemCount == 0) {
|
||||
RawUpgrades.splice(itemIndex, 1);
|
||||
} else if (RawUpgrades[itemIndex].ItemCount <= 0) {
|
||||
logger.warn(`inventory.RawUpgrades has a negative count for ${ItemType}`);
|
||||
} else if (RawUpgrades[itemIndex].ItemCount < 0) {
|
||||
throw new Error(
|
||||
`inventory.RawUpgrades has a negative count for ${ItemType} after subtracting ${ItemCount}`
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1933,7 +1938,9 @@ export const addFusionTreasures = (inventory: TInventoryDatabaseDocument, itemsA
|
||||
if (FusionTreasures[itemIndex].ItemCount == 0) {
|
||||
FusionTreasures.splice(itemIndex, 1);
|
||||
} else if (FusionTreasures[itemIndex].ItemCount <= 0) {
|
||||
logger.warn(`inventory.FusionTreasures has a negative count for ${ItemType}`);
|
||||
throw new Error(
|
||||
`inventory.FusionTreasures has a negative count for ${ItemType} after subtracting ${ItemCount}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
FusionTreasures.push({ ItemCount, ItemType, Sockets });
|
||||
|
||||
@@ -3819,6 +3819,12 @@ export const getNightwaveSyndicateTag = (buildLabel: string | undefined): string
|
||||
if (version_compare(buildLabel, "2025.02.05.11.19") >= 0) {
|
||||
return "RadioLegionIntermission12Syndicate";
|
||||
}
|
||||
if (version_compare(buildLabel, "2024.08.21.20.02") >= 0) {
|
||||
return "RadioLegionIntermission11Syndicate";
|
||||
}
|
||||
if (version_compare(buildLabel, "2024.04.29.11.14") >= 0) {
|
||||
return "RadioLegionIntermission10Syndicate";
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
|
||||
@@ -822,6 +822,22 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="umbraEchoes-card" class="card mb-3 d-none">
|
||||
<h5 class="card-header" data-loc="detailedView_umbraEchoesLabel"></h5>
|
||||
<div class="card-body">
|
||||
<p data-loc="detailedView_umbraEchoesDescription"></p>
|
||||
<form onsubmit="submitUmbraEchoes(event)">
|
||||
<div class="mb-3">
|
||||
<label for="umbraEchoes-expiry" class="form-label" data-loc="detailedView_umbraEchoesExpiryLabel"></label>
|
||||
<input type="datetime-local" class="form-control" max="2038-01-19T03:14" id="umbraEchoes-expiry" onblur="this.value=new Date(this.value)>new Date(this.max)?new Date(this.max).toISOString().slice(0,16):this.value"/>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary" data-loc="general_setButton"></button>
|
||||
<button type="button" class="btn btn-danger" onclick="setUmbraEchoes()" data-loc="code_remove"></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modularParts-card" class="card mb-3 d-none">
|
||||
<h5 class="card-header" data-loc="detailedView_modularPartsLabel"></h5>
|
||||
<div class="card-body">
|
||||
|
||||
@@ -1665,6 +1665,12 @@ function updateInventory() {
|
||||
formatDatetime("%Y-%m-%d %H:%M", Number(item.UpgradesExpiry?.$date.$numberLong)) || "";
|
||||
}
|
||||
|
||||
if (item.ItemType != "/Lotus/Powersuits/Excalibur/ExcaliburUmbra") {
|
||||
document.getElementById("umbraEchoes-card").classList.remove("d-none");
|
||||
document.getElementById("umbraEchoes-expiry").value =
|
||||
formatDatetime("%Y-%m-%d %H:%M", Number(item.UmbraDate?.$date.$numberLong)) || "";
|
||||
}
|
||||
|
||||
{
|
||||
document.getElementById("loadout-card").classList.remove("d-none");
|
||||
const maxModConfigNum = Math.min(2 + (item.ModSlotPurchases ?? 0), 5);
|
||||
@@ -3532,6 +3538,7 @@ single.getRoute("#detailedView-route").on("beforeload", function () {
|
||||
document.getElementById("loadout-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("umbraEchoes-card").classList.add("d-none");
|
||||
document.getElementById("modularParts-card").classList.add("d-none");
|
||||
document.getElementById("modularParts-form").innerHTML = "";
|
||||
document.getElementById("valenceBonus-card").classList.add("d-none");
|
||||
@@ -4270,6 +4277,25 @@ function setInvigoration(data) {
|
||||
});
|
||||
}
|
||||
|
||||
function submitUmbraEchoes(event) {
|
||||
event.preventDefault();
|
||||
const expiry = document.getElementById("umbraEchoes-expiry").value;
|
||||
setUmbraEchoes({
|
||||
UmbraDate: expiry ? new Date(expiry).getTime() : Date.now() + 1 * 24 * 60 * 60 * 1000
|
||||
});
|
||||
}
|
||||
|
||||
function setUmbraEchoes(data) {
|
||||
const oid = new URLSearchParams(window.location.search).get("itemId");
|
||||
$.post({
|
||||
url: "/custom/setUmbraEchoes?" + window.authz,
|
||||
contentType: "application/json",
|
||||
data: JSON.stringify({ oid, ...data })
|
||||
}).done(function () {
|
||||
updateInventory();
|
||||
});
|
||||
}
|
||||
|
||||
function handleAbilityOverride(event, configIndex) {
|
||||
event.preventDefault();
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
@@ -165,6 +165,7 @@ dict = {
|
||||
detailedView_invigorationLabel: `Kräftigung`,
|
||||
detailedView_loadoutLabel: `Loadouts`,
|
||||
detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`,
|
||||
detailedView_umbraEchoesLabel: `[UNTRANSLATED] Echoes Of Umbra`,
|
||||
|
||||
invigorations_offensive_AbilityStrength: `+200% Fähigkeitsstärke`,
|
||||
invigorations_offensive_AbilityRange: `+100% Fähigkeitsreichweite`,
|
||||
@@ -195,6 +196,9 @@ dict = {
|
||||
abilityOverride_label: `Fähigkeitsüberschreibung`,
|
||||
abilityOverride_onSlot: `auf Slot`,
|
||||
|
||||
detailedView_umbraEchoesDescription: `Wird ein Warframe mit dieser Flüssigkeit injiziert, kann er selbstständig an der Seite des Operators kämpfen.`,
|
||||
detailedView_umbraEchoesExpiryLabel: `[UNTRANSLATED] Echo Expiry (optional)`,
|
||||
|
||||
mods_addRiven: `Riven hinzufügen`,
|
||||
mods_fingerprint: `Fingerabdruck`,
|
||||
mods_fingerprintHelp: `Benötigst du Hilfe mit dem Fingerabdruck?`,
|
||||
|
||||
@@ -164,6 +164,7 @@ dict = {
|
||||
detailedView_invigorationLabel: `Invigoration`,
|
||||
detailedView_loadoutLabel: `Loadouts`,
|
||||
detailedView_equipmentFeaturesLabel: `Equipment Features`,
|
||||
detailedView_umbraEchoesLabel: `Echoes Of Umbra`,
|
||||
|
||||
invigorations_offensive_AbilityStrength: `+200% Ability Strength`,
|
||||
invigorations_offensive_AbilityRange: `+100% Ability Range`,
|
||||
@@ -194,6 +195,9 @@ dict = {
|
||||
abilityOverride_label: `Ability Override`,
|
||||
abilityOverride_onSlot: `on slot`,
|
||||
|
||||
detailedView_umbraEchoesDescription: `Injecting this fluid into a Warframe will imbue it with the ability to fight autonomously alongside the Operator.`,
|
||||
detailedView_umbraEchoesExpiryLabel: `Echo Expiry (optional)`,
|
||||
|
||||
mods_addRiven: `Add Riven`,
|
||||
mods_fingerprint: `Fingerprint`,
|
||||
mods_fingerprintHelp: `Need help with the fingerprint?`,
|
||||
|
||||
@@ -165,6 +165,7 @@ dict = {
|
||||
detailedView_invigorationLabel: `Fortalecimiento`,
|
||||
detailedView_loadoutLabel: `Equipamientos`,
|
||||
detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`,
|
||||
detailedView_umbraEchoesLabel: `[UNTRANSLATED] Echoes Of Umbra`,
|
||||
|
||||
invigorations_offensive_AbilityStrength: `+200% Fuerza de Habilidad`,
|
||||
invigorations_offensive_AbilityRange: `+100% Alcance de Habilidad`,
|
||||
@@ -195,6 +196,9 @@ dict = {
|
||||
abilityOverride_label: `Intercambio de Habilidad`,
|
||||
abilityOverride_onSlot: `en el espacio`,
|
||||
|
||||
detailedView_umbraEchoesDescription: `Inyectar este fluido en un warframe lo imbuirá con la capacidad para luchar autónomamente junto a su operador.`,
|
||||
detailedView_umbraEchoesExpiryLabel: `[UNTRANSLATED] Echo Expiry (optional)`,
|
||||
|
||||
mods_addRiven: `Agregar Agrietado`,
|
||||
mods_fingerprint: `Huella digital`,
|
||||
mods_fingerprintHelp: `¿Necesitas ayuda con la huella digital?`,
|
||||
|
||||
@@ -165,6 +165,7 @@ dict = {
|
||||
detailedView_invigorationLabel: `Dynamisation`,
|
||||
detailedView_loadoutLabel: `Équipements`,
|
||||
detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`,
|
||||
detailedView_umbraEchoesLabel: `[UNTRANSLATED] Echoes Of Umbra`,
|
||||
|
||||
invigorations_offensive_AbilityStrength: `+200% de puissance de pouvoir`,
|
||||
invigorations_offensive_AbilityRange: `+100% de portée de pouvoir`,
|
||||
@@ -195,6 +196,9 @@ dict = {
|
||||
abilityOverride_label: `Remplacement de pouvoir`,
|
||||
abilityOverride_onSlot: `Sur l'emplacement`,
|
||||
|
||||
detailedView_umbraEchoesDescription: `L'injection de ce fluide dans une Warframe lui donnera la possibilité de se battre de manière autonome aux côtés de l'Opérateur.`,
|
||||
detailedView_umbraEchoesExpiryLabel: `[UNTRANSLATED] Echo Expiry (optional)`,
|
||||
|
||||
mods_addRiven: `Ajouter un riven`,
|
||||
mods_fingerprint: `Empreinte`,
|
||||
mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`,
|
||||
|
||||
@@ -165,6 +165,7 @@ dict = {
|
||||
detailedView_invigorationLabel: `Воодушевление`,
|
||||
detailedView_loadoutLabel: `Конфигурации`,
|
||||
detailedView_equipmentFeaturesLabel: `Модификаторы снаряжения`,
|
||||
detailedView_umbraEchoesLabel: `Эхо Умбры`,
|
||||
|
||||
invigorations_offensive_AbilityStrength: `+200% к силе способностей.`,
|
||||
invigorations_offensive_AbilityRange: `+100% к зоне поражения способностей.`,
|
||||
@@ -195,6 +196,9 @@ dict = {
|
||||
abilityOverride_label: `Переопределение способности`,
|
||||
abilityOverride_onSlot: `в ячейке`,
|
||||
|
||||
detailedView_umbraEchoesDescription: `Введение этой жидкости в варфрейм позволит ему автономно сражаться бок о бок с оператором.`,
|
||||
detailedView_umbraEchoesExpiryLabel: `Срок действия Эха (необязательно)`,
|
||||
|
||||
mods_addRiven: `Добавить мод Разлома`,
|
||||
mods_fingerprint: `Отпечаток`,
|
||||
mods_fingerprintHelp: `Нужна помощь с отпечатком?`,
|
||||
|
||||
@@ -165,6 +165,7 @@ dict = {
|
||||
detailedView_invigorationLabel: `Зміцнення`,
|
||||
detailedView_loadoutLabel: `Конфігурації`,
|
||||
detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`,
|
||||
detailedView_umbraEchoesLabel: `[UNTRANSLATED] Echoes Of Umbra`,
|
||||
|
||||
invigorations_offensive_AbilityStrength: `+200% до потужності здібностей.`,
|
||||
invigorations_offensive_AbilityRange: `+100% до досяжності здібностей.`,
|
||||
@@ -195,6 +196,9 @@ dict = {
|
||||
abilityOverride_label: `Перевизначення здібностей`,
|
||||
abilityOverride_onSlot: `у комірці`,
|
||||
|
||||
detailedView_umbraEchoesDescription: `Рідина, яка після введення дозволяє ворфрейму використовувати єдність і битися самостійно пліч-о-пліч з оператором.`,
|
||||
detailedView_umbraEchoesExpiryLabel: `[UNTRANSLATED] Echo Expiry (optional)`,
|
||||
|
||||
mods_addRiven: `Добавити модифікатор Розколу`,
|
||||
mods_fingerprint: `Відбиток`,
|
||||
mods_fingerprintHelp: `Потрібна допомога з відбитком?`,
|
||||
|
||||
@@ -165,6 +165,7 @@ dict = {
|
||||
detailedView_invigorationLabel: `活化`,
|
||||
detailedView_loadoutLabel: `配置`,
|
||||
detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`,
|
||||
detailedView_umbraEchoesLabel: `[UNTRANSLATED] Echoes Of Umbra`,
|
||||
|
||||
invigorations_offensive_AbilityStrength: `+200%技能强度`,
|
||||
invigorations_offensive_AbilityRange: `+100%技能范围`,
|
||||
@@ -195,6 +196,9 @@ dict = {
|
||||
abilityOverride_label: `技能替换`,
|
||||
abilityOverride_onSlot: `槽位`,
|
||||
|
||||
detailedView_umbraEchoesDescription: `将这种液体注入战甲内,使其具有与指挥官并肩作战的能力。`,
|
||||
detailedView_umbraEchoesExpiryLabel: `[UNTRANSLATED] Echo Expiry (optional)`,
|
||||
|
||||
mods_addRiven: `添加裂罅MOD`,
|
||||
mods_fingerprint: `印记`,
|
||||
mods_fingerprintHelp: `需要印记相关的帮助?`,
|
||||
|
||||
Reference in New Issue
Block a user