From 2264ae176bef5ae01e555e20a56ef39ccabcc561 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Mon, 3 Nov 2025 23:56:31 -0800 Subject: [PATCH 01/17] fix: handle fake jobs without syndicateMissionId (#2982) Closes #2970 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2982 Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com> Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/services/missionInventoryUpdateService.ts | 131 +++++++++--------- 1 file changed, 69 insertions(+), 62 deletions(-) diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index aafaec0a..884813d5 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -1522,7 +1522,38 @@ export const addMissionRewards = async ( syndicateEntry = Goals.find(m => m._id.$oid === syndicateMissionId); if (syndicateEntry) syndicateEntry.Tag = syndicateEntry.JobAffiliationTag!; } - if (syndicateEntry && syndicateEntry.Jobs && !jobType.startsWith("/Lotus/Types/Gameplay/NokkoColony/Jobs")) { + const specialCase = [ + { + endings: ["Heists/HeistProfitTakerBountyOne"], + stage: 2, + amount: 1000, + tag: "SolarisSyndicate" + }, + { + endings: [ + "Heists/HeistProfitTakerBountyTwo", + "Heists/HeistProfitTakerBountyThree", + "Heists/HeistProfitTakerBountyFour", + "Heists/HeistExploiterBountyOne" + ], + amount: 1000, + tag: "SolarisSyndicate" + }, + { endings: ["Hunts/AllTeralystsHunt"], stage: 2, amount: 5000, tag: "CetusSyndicate" }, + { + endings: ["Hunts/TeralystHunt"], + amount: 1000, + tag: "CetusSyndicate" + }, + { endings: ["Jobs/NewbieJob"], amount: 200, tag: "CetusSyndicate" } + ]; + const match = specialCase.find(rule => rule.endings.some(e => jobType.endsWith(e))); + if (match) { + const specialCaseReward = match.stage === undefined || rewardInfo.JobStage === match.stage ? match : null; + if (specialCaseReward) { + addStanding(inventory, match.tag, Math.floor(match.amount / (rewardInfo.Q ? 0.8 : 1)), AffiliationMods); + } + } else if (syndicateEntry && syndicateEntry.Jobs) { let currentJob = syndicateEntry.Jobs[rewardInfo.JobTier!]; if ( [ @@ -1580,42 +1611,15 @@ export const addMissionRewards = async ( ); logger.warning(`currentJob`, { currentJob: currentJob }); } - } else { - const specialCase = [ - { endings: ["Heists/HeistProfitTakerBountyOne"], stage: 2, amount: 1000 }, - { endings: ["Hunts/AllTeralystsHunt"], stage: 2, amount: 5000 }, - { - endings: [ - "Hunts/TeralystHunt", - "Heists/HeistProfitTakerBountyTwo", - "Heists/HeistProfitTakerBountyThree", - "Heists/HeistProfitTakerBountyFour", - "Heists/HeistExploiterBountyOne" - ], - amount: 1000 - } - ]; - const specialCaseReward = specialCase.find( - rule => - rule.endings.some(e => jobType.endsWith(e)) && - (rule.stage === undefined || rewardInfo.JobStage === rule.stage) + } else if (!jobType.startsWith("/Lotus/Types/Gameplay/NokkoColony/Jobs")) { + addStanding( + inventory, + syndicateEntry.Tag, + Math.floor(currentJob.xpAmounts[rewardInfo.JobStage] / (rewardInfo.Q ? 0.8 : 1)), + AffiliationMods ); - - if (specialCaseReward) { - addStanding(inventory, syndicateEntry.Tag, specialCaseReward.amount, AffiliationMods); - } else { - addStanding( - inventory, - syndicateEntry.Tag, - Math.floor(currentJob.xpAmounts[rewardInfo.JobStage] / (rewardInfo.Q ? 0.8 : 1)), - AffiliationMods - ); - } } } - if (jobType == "/Lotus/Types/Gameplay/Eidolon/Jobs/NewbieJob") { - addStanding(inventory, "CetusSyndicate", Math.floor(200 / (rewardInfo.Q ? 0.8 : 1)), AffiliationMods); - } } if (rewardInfo.challengeMissionId) { @@ -1969,7 +1973,8 @@ function getRandomMissionDrops( if (syndicateEntry) syndicateEntry.Tag = syndicateEntry.JobAffiliationTag!; } if (syndicateEntry && syndicateEntry.Jobs) { - let job = syndicateEntry.Jobs[RewardInfo.JobTier!]; + let job; + if (RewardInfo.JobTier && RewardInfo.JobTier > 0) job = syndicateEntry.Jobs[RewardInfo.JobTier]; if (syndicateEntry.Tag === "EntratiSyndicate") { if ( @@ -2073,38 +2078,40 @@ function getRandomMissionDrops( ) { job = syndicateEntry.Jobs.find(j => j.jobType === jobType)!; } - rewardManifests = [job.rewards]; - if (job.xpAmounts.length > 1) { - const curentStage = RewardInfo.JobStage! + 1; - const totalStage = job.xpAmounts.length; - let tableIndex = 1; // Stage 2, Stage 3 of 4, and Stage 3 of 5 + if (job) { + rewardManifests = [job.rewards]; + if (job.xpAmounts.length > 1) { + const curentStage = RewardInfo.JobStage! + 1; + const totalStage = job.xpAmounts.length; + let tableIndex = 1; // Stage 2, Stage 3 of 4, and Stage 3 of 5 - if (curentStage == 1) { - tableIndex = 0; - } else if (curentStage == totalStage) { - tableIndex = 3; - } else if (totalStage == 5 && curentStage == 4) { - tableIndex = 2; - } - if (jobType.startsWith("/Lotus/Types/Gameplay/NokkoColony/Jobs/NokkoJob")) { - if (RewardInfo.JobStage === job.xpAmounts.length - 1) { - rotations = [0, 1, 2]; + if (curentStage == 1) { + tableIndex = 0; + } else if (curentStage == totalStage) { + tableIndex = 3; + } else if (totalStage == 5 && curentStage == 4) { + tableIndex = 2; + } + if (jobType.startsWith("/Lotus/Types/Gameplay/NokkoColony/Jobs/NokkoJob")) { + if (RewardInfo.JobStage === job.xpAmounts.length - 1) { + rotations = [0, 1, 2]; + } else { + rewardManifests = []; + } } else { - rewardManifests = []; + rotations = [tableIndex]; } } else { - rotations = [tableIndex]; + rotations = [0]; + } + if ( + RewardInfo.Q && + (RewardInfo.JobStage === job.xpAmounts.length - 1 || jobType.endsWith("VaultBounty")) && + !jobType.startsWith("/Lotus/Types/Gameplay/NokkoColony/Jobs/NokkoJob") && + !isEndlessJob + ) { + rotations.push(ExportRewards[job.rewards].length - 1); } - } else { - rotations = [0]; - } - if ( - RewardInfo.Q && - (RewardInfo.JobStage === job.xpAmounts.length - 1 || jobType.endsWith("VaultBounty")) && - !jobType.startsWith("/Lotus/Types/Gameplay/NokkoColony/Jobs/NokkoJob") && - !isEndlessJob - ) { - rotations.push(ExportRewards[job.rewards].length - 1); } } } From ebda91f517346fcd717320d5586eb187bf25e92b Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 5 Nov 2025 01:10:27 -0800 Subject: [PATCH 02/17] chore: keep conquests in sync between pre- & post-U40 clients (#2985) Reviewed-on: https://www.onlyg.it/OpenWF/SpaceNinjaServer/pulls/2985 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/conquestService.ts | 17 +++++++++++++++++ src/services/worldStateService.ts | 24 +++++++++++++++++------- src/types/worldStateTypes.ts | 4 ++-- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/services/conquestService.ts b/src/services/conquestService.ts index f950b20a..16f20041 100644 --- a/src/services/conquestService.ts +++ b/src/services/conquestService.ts @@ -423,3 +423,20 @@ export const getConquest = ( RandomSeed: rng.randomInt(0, 1_000_000) }; }; + +export const getMissionTypeForLegacyOverride = (missionType: TMissionType, conquestType: TConquestType): string => { + if (missionType == "MT_ENDLESS_CAPTURE") { + return "EndlessCapture"; + } + let str = missionType.substring(3, 4).toUpperCase() + missionType.substring(4).toLowerCase(); + if (str == "Extermination") { + str = "Exterminate"; + } + if (str == "Artifact") { + str = "Disruption"; + } + if (str == "Defense" && conquestType == "CT_LAB") { + str = "DualDefense"; + } + return str; +}; diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 9ba454fb..fb6cbea5 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -41,7 +41,7 @@ import type { import { toMongoDate, toOid, version_compare } from "../helpers/inventoryHelpers.ts"; import { logger } from "../utils/logger.ts"; import { DailyDeal, Fissure } from "../models/worldStateModel.ts"; -import { getConquest } from "./conquestService.ts"; +import { getConquest, getMissionTypeForLegacyOverride } from "./conquestService.ts"; const sortieBosses = [ "SORTIE_BOSS_HYENA", @@ -3562,13 +3562,11 @@ export const getWorldState = (buildLabel?: string): IWorldState => { worldState.KnownCalendarSeasons.push(getCalendarSeason(week + 1)); } + const season = (["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"] as const)[week % 4]; + const labConquest = getConquest("CT_LAB", week, null); + const hexConquest = getConquest("CT_HEX", week, season); if (!buildLabel || version_compare(buildLabel, "2025.10.14.16.10") >= 0) { - worldState.Conquests = []; - { - const season = (["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"] as const)[week % 4]; - worldState.Conquests.push(getConquest("CT_LAB", week, null)); - worldState.Conquests.push(getConquest("CT_HEX", week, season)); - } + worldState.Conquests = [labConquest, hexConquest]; if (isBeforeNextExpectedWorldStateRefresh(timeMs, weekEnd)) { const season = (["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"] as const)[(week + 1) % 4]; worldState.Conquests.push(getConquest("CT_LAB", week, null)); @@ -3621,6 +3619,18 @@ export const getWorldState = (buildLabel?: string): IWorldState => { e: cheeseEnd, n: cheeseNext }, + lqo: { + mt: labConquest.Missions.map(x => getMissionTypeForLegacyOverride(x.missionType, "CT_LAB")), + mv: labConquest.Missions.map(x => x.difficulties[1].deviation), + c: labConquest.Missions.map(x => x.difficulties[1].risks), + fv: labConquest.Variables + }, + hqo: { + mt: hexConquest.Missions.map(x => getMissionTypeForLegacyOverride(x.missionType, "CT_HEX")), + mv: hexConquest.Missions.map(x => x.difficulties[1].deviation), + c: hexConquest.Missions.map(x => x.difficulties[1].risks), + fv: hexConquest.Variables + }, sfn: [550, 553, 554, 555][halfHour % 4] }; if (Array.isArray(config.worldState?.circuitGameModes)) { diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index 799e261d..c323fa0f 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -485,9 +485,9 @@ interface IFbst { // < 40.0.0 interface IConquestOverride { - mt?: string[]; // mission types but "Exterminate" instead of "MT_EXTERMINATION", etc. and "DualDefense" instead of "Defense" for hex conquest + mt?: string[]; mv?: string[]; - mf?: number[]; // hex conquest only + mf?: number[]; // hex conquest only. unknown purpose. c?: [string, string][]; fv?: string[]; } From 4be7a9e6c603ee14634fda115d457f4ad9bbbace Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Wed, 5 Nov 2025 01:10:39 -0800 Subject: [PATCH 03/17] feat: sync worldstate on config change (#2986) Reviewed-on: https://www.onlyg.it/OpenWF/SpaceNinjaServer/pulls/2986 Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com> Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/controllers/custom/configController.ts | 5 ++++- src/services/wsService.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/controllers/custom/configController.ts b/src/controllers/custom/configController.ts index 70b61174..1b0eeecb 100644 --- a/src/controllers/custom/configController.ts +++ b/src/controllers/custom/configController.ts @@ -2,7 +2,7 @@ import type { RequestHandler } from "express"; import { config, syncConfigWithDatabase } from "../../services/configService.ts"; import { getAccountForRequest, isAdministrator } from "../../services/loginService.ts"; import { saveConfig } from "../../services/configWriterService.ts"; -import { sendWsBroadcastEx } from "../../services/wsService.ts"; +import { sendWsBroadcastEx, sendWsBroadcast } from "../../services/wsService.ts"; export const getConfigController: RequestHandler = async (req, res) => { const account = await getAccountForRequest(req); @@ -21,11 +21,14 @@ export const getConfigController: RequestHandler = async (req, res) => { export const setConfigController: RequestHandler = async (req, res) => { const account = await getAccountForRequest(req); if (isAdministrator(account)) { + let isWorldStateUpdate = false; for (const [id, value] of Object.entries(req.body as Record)) { + if (id.startsWith("worldState")) isWorldStateUpdate = true; const [obj, idx] = configIdToIndexable(id); obj[idx] = value; } sendWsBroadcastEx({ config_reloaded: true }, undefined, parseInt(String(req.query.wsid))); + if (isWorldStateUpdate) sendWsBroadcast({ sync_world_state: true }); syncConfigWithDatabase(); await saveConfig(); res.end(); diff --git a/src/services/wsService.ts b/src/services/wsService.ts index 6850e1a2..9538deb9 100644 --- a/src/services/wsService.ts +++ b/src/services/wsService.ts @@ -92,6 +92,7 @@ interface IWsMsgToClient { // to game/bootstrapper (https://openwf.io/bootstrapper-manual) sync_inventory?: boolean; + sync_world_state?: boolean; tunables?: ITunables; } From 50e9cafc0985e2982b4a1178c3b1ea03ae20118b Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 5 Nov 2025 01:56:52 -0800 Subject: [PATCH 04/17] chore: fix unable to login in U11 (#2988) Reviewed-on: https://www.onlyg.it/OpenWF/SpaceNinjaServer/pulls/2988 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/loginController.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index 9c30f843..ce9ac07a 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -119,7 +119,8 @@ const createLoginResponse = ( resp.CountryCode = account.CountryCode; } else { // U8 - resp.NatHash = "0"; + resp.NatHash = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; resp.SteamId = "0"; } if (version_compare(buildLabel, "2015.02.13.10.41") >= 0) { From be064fd249d9792f7d3756f94deda7b5c84a498a Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 5 Nov 2025 05:36:38 -0800 Subject: [PATCH 05/17] fix: properly provide hex conquest for pre-U40 clients (#2992) The missing mf field did end up causing script errors + exterminate was not provided in the proper format Reviewed-on: https://www.onlyg.it/OpenWF/SpaceNinjaServer/pulls/2992 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/conquestService.ts | 31 ++++++++++++++++++++++++++++--- src/services/worldStateService.ts | 3 ++- src/types/worldStateTypes.ts | 2 +- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/services/conquestService.ts b/src/services/conquestService.ts index 16f20041..0a29341c 100644 --- a/src/services/conquestService.ts +++ b/src/services/conquestService.ts @@ -429,9 +429,6 @@ export const getMissionTypeForLegacyOverride = (missionType: TMissionType, conqu return "EndlessCapture"; } let str = missionType.substring(3, 4).toUpperCase() + missionType.substring(4).toLowerCase(); - if (str == "Extermination") { - str = "Exterminate"; - } if (str == "Artifact") { str = "Disruption"; } @@ -440,3 +437,31 @@ export const getMissionTypeForLegacyOverride = (missionType: TMissionType, conqu } return str; }; + +export const factionToInt = (faction: TFaction | "FC_TENNO"): number => { + switch (faction) { + case "FC_GRINEER": + return 0; + case "FC_CORPUS": + return 1; + case "FC_INFESTATION": + return 2; + case "FC_OROKIN": + return 3; + case "FC_RED_VEIL": + return 4; + case "FC_SENTIENT": + return 5; + case "FC_NARMER": + return 6; + case "FC_MITW": + return 7; + case "FC_SCALDRA": + return 8; + case "FC_TECHROT": + return 9; + case "FC_DUVIRI": + return 10; + } + throw new Error(`unexpected faction ${faction}`); +}; diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index fb6cbea5..c8c1181f 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -41,7 +41,7 @@ import type { import { toMongoDate, toOid, version_compare } from "../helpers/inventoryHelpers.ts"; import { logger } from "../utils/logger.ts"; import { DailyDeal, Fissure } from "../models/worldStateModel.ts"; -import { getConquest, getMissionTypeForLegacyOverride } from "./conquestService.ts"; +import { factionToInt, getConquest, getMissionTypeForLegacyOverride } from "./conquestService.ts"; const sortieBosses = [ "SORTIE_BOSS_HYENA", @@ -3628,6 +3628,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { hqo: { mt: hexConquest.Missions.map(x => getMissionTypeForLegacyOverride(x.missionType, "CT_HEX")), mv: hexConquest.Missions.map(x => x.difficulties[1].deviation), + mf: hexConquest.Missions.map(x => factionToInt(x.faction)), c: hexConquest.Missions.map(x => x.difficulties[1].risks), fv: hexConquest.Variables }, diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index c323fa0f..4c6aef44 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -487,7 +487,7 @@ interface IFbst { interface IConquestOverride { mt?: string[]; mv?: string[]; - mf?: number[]; // hex conquest only. unknown purpose. + mf?: number[]; c?: [string, string][]; fv?: string[]; } From 65138b64fc20e99d6ec3db2664febf51fc8c318d Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 5 Nov 2025 07:58:25 -0800 Subject: [PATCH 06/17] chore(webui): inform the user of JSON parsing errors during import (#3001) Closes #2996 Reviewed-on: https://www.onlyg.it/OpenWF/SpaceNinjaServer/pulls/3001 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- static/webui/script.js | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/static/webui/script.js b/static/webui/script.js index 200bc040..8c88a69a 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -3552,16 +3552,21 @@ function doPopArchonCrystalUpgrade(type) { function doImport() { revalidateAuthz().then(() => { - $.post({ - url: "/custom/import?" + window.authz, - contentType: "application/json", - data: JSON.stringify({ - inventory: JSON.parse($("#import-inventory").val()) - }) - }).then(function () { - toast(loc("code_succImport")); - updateInventory(); - }); + try { + $.post({ + url: "/custom/import?" + window.authz, + contentType: "application/json", + data: JSON.stringify({ + inventory: JSON.parse($("#import-inventory").val()) + }) + }).then(function () { + toast(loc("code_succImport")); + updateInventory(); + }); + } catch (e) { + toast(e); + console.error(e); + } }); } From b8d0924f0f563aeabd496777c21864b538c5d057 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 5 Nov 2025 08:01:31 -0800 Subject: [PATCH 07/17] chore: fix unable to login in U13 (#2989) Reviewed-on: https://www.onlyg.it/OpenWF/SpaceNinjaServer/pulls/2989 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/loginController.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index ce9ac07a..8efcadaa 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -114,11 +114,11 @@ const createLoginResponse = ( Nonce: account.Nonce, BuildLabel: buildLabel }; - if (version_compare(buildLabel, "2014.10.24.08.24") >= 0) { - // U15 and up + if (version_compare(buildLabel, "2014.04.10.17.47") >= 0) { + // U13 and up resp.CountryCode = account.CountryCode; } else { - // U8 + // U12 and down resp.NatHash = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; resp.SteamId = "0"; From 986201697fd9c6a975261b4db03cd0b19fb3d7e4 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Thu, 6 Nov 2025 00:16:58 -0800 Subject: [PATCH 08/17] feat(webui): equipment features in detailed view (#2987) Closes #2927 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2987 Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com> Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/controllers/api/gildWeaponController.ts | 35 ++++----- src/controllers/api/inventoryController.ts | 1 + .../custom/equipmentFeaturesController.ts | 34 +++++++++ src/routes/custom.ts | 2 + static/webui/index.html | 4 + static/webui/script.js | 74 +++++++++++++++---- static/webui/translations/de.js | 8 ++ static/webui/translations/en.js | 8 ++ static/webui/translations/es.js | 8 ++ static/webui/translations/fr.js | 8 ++ static/webui/translations/ru.js | 8 ++ static/webui/translations/uk.js | 8 ++ static/webui/translations/zh.js | 8 ++ 13 files changed, 172 insertions(+), 34 deletions(-) create mode 100644 src/controllers/custom/equipmentFeaturesController.ts diff --git a/src/controllers/api/gildWeaponController.ts b/src/controllers/api/gildWeaponController.ts index dea62765..b9133426 100644 --- a/src/controllers/api/gildWeaponController.ts +++ b/src/controllers/api/gildWeaponController.ts @@ -34,10 +34,9 @@ export const gildWeaponController: RequestHandler = async (req, res) => { const weapon = inventory[data.Category][weaponIndex]; weapon.Features ??= 0; weapon.Features |= EquipmentFeatures.GILDED; - if (data.Recipe != "webui") { - weapon.ItemName = data.ItemName; - weapon.XP = 0; - } + weapon.ItemName = data.ItemName; + weapon.XP = 0; + if (data.Category != "OperatorAmps" && data.PolarizeSlot && data.PolarizeValue) { weapon.Polarity = [ { @@ -52,22 +51,20 @@ export const gildWeaponController: RequestHandler = async (req, res) => { const affiliationMods = []; - if (data.Recipe != "webui") { - const recipe = ExportRecipes[data.Recipe]; - inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({ - ItemType: ingredient.ItemType, - ItemCount: ingredient.ItemCount * -1 - })); - addMiscItems(inventory, inventoryChanges.MiscItems); + const recipe = ExportRecipes[data.Recipe]; + inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({ + ItemType: ingredient.ItemType, + ItemCount: ingredient.ItemCount * -1 + })); + addMiscItems(inventory, inventoryChanges.MiscItems); - if (recipe.syndicateStandingChange) { - const affiliation = inventory.Affiliations.find(x => x.Tag == recipe.syndicateStandingChange!.tag)!; - affiliation.Standing += recipe.syndicateStandingChange.value; - affiliationMods.push({ - Tag: recipe.syndicateStandingChange.tag, - Standing: recipe.syndicateStandingChange.value - }); - } + if (recipe.syndicateStandingChange) { + const affiliation = inventory.Affiliations.find(x => x.Tag == recipe.syndicateStandingChange!.tag)!; + affiliation.Standing += recipe.syndicateStandingChange.value; + affiliationMods.push({ + Tag: recipe.syndicateStandingChange.tag, + Standing: recipe.syndicateStandingChange.value + }); } await inventory.save(); diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 5d107510..b832c41b 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -410,6 +410,7 @@ export const getInventoryResponse = async ( for (const equipment of inventoryResponse[key]) { equipment.Features ??= 0; equipment.Features |= EquipmentFeatures.ARCANE_SLOT; + equipment.Features |= EquipmentFeatures.SECOND_ARCANE_SLOT; } } } diff --git a/src/controllers/custom/equipmentFeaturesController.ts b/src/controllers/custom/equipmentFeaturesController.ts new file mode 100644 index 00000000..68d0e60f --- /dev/null +++ b/src/controllers/custom/equipmentFeaturesController.ts @@ -0,0 +1,34 @@ +import type { RequestHandler } from "express"; +import { getAccountIdForRequest } from "../../services/loginService.ts"; +import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts"; +import { getInventory } from "../../services/inventoryService.ts"; +import { EquipmentFeatures } from "../../types/equipmentTypes.ts"; +import { sendWsBroadcastTo } from "../../services/wsService.ts"; + +export const equipmentFeaturesController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const category = req.query.Category as TEquipmentKey; + const inventory = await getInventory( + accountId, + `${category} unlockDoubleCapacityPotatoesEverywhere unlockExilusEverywhere unlockArcanesEverywhere` + ); + const bit = Number(req.query.bit) as EquipmentFeatures; + if ( + (inventory.unlockDoubleCapacityPotatoesEverywhere && bit === EquipmentFeatures.DOUBLE_CAPACITY) || + (inventory.unlockExilusEverywhere && bit === EquipmentFeatures.UTILITY_SLOT) || + (inventory.unlockArcanesEverywhere && + (bit === EquipmentFeatures.ARCANE_SLOT || bit === EquipmentFeatures.SECOND_ARCANE_SLOT)) + ) { + res.status(400).end(); + } + const item = inventory[category].id(req.query.ItemId as string); + if (item) { + item.Features ??= 0; + item.Features ^= bit; + await inventory.save(); + sendWsBroadcastTo(accountId, { sync_inventory: true }); + res.status(200).end(); + } else { + res.status(400).end(); + } +}; diff --git a/src/routes/custom.ts b/src/routes/custom.ts index 05747f5a..1a6fca1f 100644 --- a/src/routes/custom.ts +++ b/src/routes/custom.ts @@ -1,6 +1,7 @@ import express from "express"; import { tunablesController } from "../controllers/custom/tunablesController.ts"; +import { equipmentFeaturesController } from "../controllers/custom/equipmentFeaturesController.ts"; import { getItemListsController } from "../controllers/custom/getItemListsController.ts"; import { pushArchonCrystalUpgradeController } from "../controllers/custom/pushArchonCrystalUpgradeController.ts"; import { popArchonCrystalUpgradeController } from "../controllers/custom/popArchonCrystalUpgradeController.ts"; @@ -53,6 +54,7 @@ import { getConfigController, setConfigController } from "../controllers/custom/ const customRouter = express.Router(); customRouter.get("/tunables.json", tunablesController); +customRouter.get("/equipmentFeatures", equipmentFeaturesController); customRouter.get("/getItemLists", getItemListsController); customRouter.get("/pushArchonCrystalUpgrade", pushArchonCrystalUpgradeController); customRouter.get("/popArchonCrystalUpgrade", popArchonCrystalUpgradeController); diff --git a/static/webui/index.html b/static/webui/index.html index e9c3bcf3..75fa3d52 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -840,6 +840,10 @@ +
+
+
+

diff --git a/static/webui/script.js b/static/webui/script.js index 8c88a69a..422881c5 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -912,12 +912,7 @@ function updateInventory() { td.appendChild(a); } - if ( - ["Suits", "LongGuns", "Pistols", "Melee", "SpaceGuns", "SpaceMelee"].includes( - category - ) || - modularWeapons.includes(item.ItemType) - ) { + { const a = document.createElement("a"); a.href = "/webui/detailedView?productCategory=" + category + "&itemId=" + item.ItemId.$oid; @@ -930,7 +925,7 @@ function updateInventory() { a.href = "#"; a.onclick = function (event) { event.preventDefault(); - gildEquipment(category, item.ItemId.$oid); + equipmentFeatures(category, item.ItemId.$oid, 8); }; a.title = loc("code_gild"); a.innerHTML = ``; @@ -1560,6 +1555,57 @@ function updateInventory() { $("#detailedView-title").text(itemName); } + { + document.getElementById("equipmentFeatures-card").classList.remove("d-none"); + const buttonsCard = document.getElementById("equipmentFeaturesButtons-card"); + buttonsCard.innerHTML = ""; + item.Features ??= 0; + const bits = []; + if (category != "OperatorAmps") bits.push(1); + if (["Suits", "LongGuns", "Pistols", "Melee"].includes(category)) bits.push(2); + if (modularWeapons.includes(item.ItemType)) bits.push(8); + if (["LongGuns", "Pistols", "Melee", "SpaceGuns", "OperatorAmps"].includes(category)) + bits.push(32); + if (category == "SpaceGuns") bits.push(4, 64); + if ( + ["LongGuns", "Pistols", "Melee", "SpaceGuns", "SpaceMelee"].includes(category) && + item.UpgradeFingerprint + ) + bits.push(1024); + for (const bit of bits.sort((a, b) => a - b)) { + const wrapper = document.createElement("div"); + wrapper.classList = "form-check"; + + const input = document.createElement("input"); + input.classList = "form-check-input"; + input.type = "checkbox"; + input.id = `detailedView-feature-${bit}`; + input.checked = item.Features & bit; + + const label = document.createElement("label"); + label.classList = "form-check-label"; + label.htmlFor = input.id; + label.innerHTML = loc(`code_feature_${bit}`); + label.setAttribute("data-loc", `code_feature_${bit}`); + + input.onchange = function (event) { + event.preventDefault(); + equipmentFeatures(category, oid, bit); + }; + if ( + (data.unlockDoubleCapacityPotatoesEverywhere && bit === 1) || + (data.unlockExilusEverywhere && bit === 2) || + (data.unlockArcanesEverywhere && (bit === 32 || bit === 64)) + ) { + input.disabled = true; + } + + wrapper.appendChild(input); + wrapper.appendChild(label); + buttonsCard.appendChild(wrapper); + } + } + if (category == "Suits") { document.getElementById("archonShards-card").classList.remove("d-none"); @@ -2864,15 +2910,11 @@ function disposeOfItems(category, type, count) { }); } -function gildEquipment(category, oid) { +function equipmentFeatures(category, oid, bit) { revalidateAuthz().then(() => { - $.post({ - url: "/api/gildWeapon.php?" + window.authz + "&ItemId=" + oid + "&Category=" + category, - contentType: "application/octet-stream", - data: JSON.stringify({ - Recipe: "webui" - }) - }).done(function () { + $.get( + "/custom/equipmentFeatures?" + window.authz + "&ItemId=" + oid + "&Category=" + category + "&bit=" + bit + ).done(function () { updateInventory(); }); }); @@ -3478,6 +3520,8 @@ single.getRoute("#detailedView-route").on("beforeload", function () { document.getElementById("modularParts-card").classList.add("d-none"); document.getElementById("modularParts-form").innerHTML = ""; document.getElementById("valenceBonus-card").classList.add("d-none"); + document.getElementById("equipmentFeatures-card").classList.add("d-none"); + document.getElementById("equipmentFeaturesButtons-card").innerHTML = ""; if (window.didInitialInventoryUpdate) { updateInventory(); } diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index e47aa556..3bef4a97 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -81,6 +81,13 @@ dict = { code_operatorFaceName: `Operator-Gesicht: |INDEX|`, code_succChange: `Erfolgreich geändert.`, code_requiredInvigorationUpgrade: `Du musst sowohl ein Offensiv- als auch ein Support-Upgrade auswählen.`, + code_feature_1: `[UNTRANSLATED] Orokin Reactor`, + code_feature_2: `[UNTRANSLATED] Exilus Adapter`, + code_feature_4: `[UNTRANSLATED] Gravimag`, + code_feature_8: `[UNTRANSLATED] Gild`, + code_feature_32: `[UNTRANSLATED] Arcane Slot`, + code_feature_64: `[UNTRANSLATED] Second Arcane Slot`, + code_feature_1024: `[UNTRANSLATED] Valence Override`, 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_passwordLabel: `Passwort`, @@ -155,6 +162,7 @@ dict = { detailedView_modularPartsLabel: `Modulare Teile ändern`, detailedView_invigorationLabel: `Kräftigung`, detailedView_loadoutLabel: `Loadouts`, + detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`, invigorations_offensive_AbilityStrength: `+200% Fähigkeitsstärke`, invigorations_offensive_AbilityRange: `+100% Fähigkeitsreichweite`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index c9398726..75d3cd0c 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -80,6 +80,13 @@ dict = { code_operatorFaceName: `Operator Visage |INDEX|`, code_succChange: `Successfully changed.`, code_requiredInvigorationUpgrade: `You must select both an offensive & utility upgrade.`, + code_feature_1: `Orokin Reactor`, + code_feature_2: `Exilus Adapter`, + code_feature_4: `Gravimag`, + code_feature_8: `Gild`, + code_feature_32: `Arcane Slot`, + code_feature_64: `Second Arcane Slot`, + code_feature_1024: `Valence Override`, login_description: `Login using your OpenWF account credentials (same as in-game when connecting to this server).`, login_emailLabel: `Email address`, login_passwordLabel: `Password`, @@ -154,6 +161,7 @@ dict = { detailedView_modularPartsLabel: `Change Modular Parts`, detailedView_invigorationLabel: `Invigoration`, detailedView_loadoutLabel: `Loadouts`, + detailedView_equipmentFeaturesLabel: `Equipment Features`, invigorations_offensive_AbilityStrength: `+200% Ability Strength`, invigorations_offensive_AbilityRange: `+100% Ability Range`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 93e576a0..5de24c77 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -81,6 +81,13 @@ dict = { code_operatorFaceName: `Rostro del operador |INDEX|`, code_succChange: `Cambiado correctamente`, code_requiredInvigorationUpgrade: `Debes seleccionar una mejora ofensiva y una mejora de utilidad.`, + code_feature_1: `[UNTRANSLATED] Orokin Reactor`, + code_feature_2: `[UNTRANSLATED] Exilus Adapter`, + code_feature_4: `[UNTRANSLATED] Gravimag`, + code_feature_8: `[UNTRANSLATED] Gild`, + code_feature_32: `[UNTRANSLATED] Arcane Slot`, + code_feature_64: `[UNTRANSLATED] Second Arcane Slot`, + code_feature_1024: `[UNTRANSLATED] Valence Override`, 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_passwordLabel: `Contraseña`, @@ -155,6 +162,7 @@ dict = { detailedView_modularPartsLabel: `Cambiar partes modulares`, detailedView_invigorationLabel: `Fortalecimiento`, detailedView_loadoutLabel: `Equipamientos`, + detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`, invigorations_offensive_AbilityStrength: `+200% Fuerza de Habilidad`, invigorations_offensive_AbilityRange: `+100% Alcance de Habilidad`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 9bc24f9c..5246d35d 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -81,6 +81,13 @@ dict = { code_operatorFaceName: `Visage de l'Opérateur |INDEX|`, code_succChange: `Changement effectué.`, code_requiredInvigorationUpgrade: `Invigoration offensive et défensive requises.`, + code_feature_1: `[UNTRANSLATED] Orokin Reactor`, + code_feature_2: `[UNTRANSLATED] Exilus Adapter`, + code_feature_4: `[UNTRANSLATED] Gravimag`, + code_feature_8: `[UNTRANSLATED] Gild`, + code_feature_32: `[UNTRANSLATED] Arcane Slot`, + code_feature_64: `[UNTRANSLATED] Second Arcane Slot`, + code_feature_1024: `[UNTRANSLATED] Valence Override`, login_description: `Connexion avec les informations de connexion OpenWF.`, login_emailLabel: `Email`, login_passwordLabel: `Mot de passe`, @@ -155,6 +162,7 @@ dict = { detailedView_modularPartsLabel: `Changer l'équipement modulaire`, detailedView_invigorationLabel: `Dynamisation`, detailedView_loadoutLabel: `Équipements`, + detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`, invigorations_offensive_AbilityStrength: `+200% de puissance de pouvoir`, invigorations_offensive_AbilityRange: `+100% de portée de pouvoir`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 4b3ad9a3..d9062c26 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -81,6 +81,13 @@ dict = { code_operatorFaceName: `Внешность оператора: |INDEX|`, code_succChange: `Успешно изменено.`, code_requiredInvigorationUpgrade: `Вы должны выбрать как атакующее, так и вспомогательное улучшение.`, + code_feature_1: `Реактор Орокин`, + code_feature_2: `Адаптер Эксилус`, + code_feature_4: `Гравимаг`, + code_feature_8: `Улучшение`, + code_feature_32: `Слот Мистификатора`, + code_feature_64: `Второй слот Мистификатора`, + code_feature_1024: `Переопределение валентности`, login_description: `Войдите, используя учетные данные OpenWF (те же, что и в игре при подключении к этому серверу).`, login_emailLabel: `Адрес электронной почты`, login_passwordLabel: `Пароль`, @@ -155,6 +162,7 @@ dict = { detailedView_modularPartsLabel: `Изменить модульные части`, detailedView_invigorationLabel: `Воодушевление`, detailedView_loadoutLabel: `Конфигурации`, + detailedView_equipmentFeaturesLabel: `Модификаторы снаряжения`, invigorations_offensive_AbilityStrength: `+200% к силе способностей.`, invigorations_offensive_AbilityRange: `+100% к зоне поражения способностей.`, diff --git a/static/webui/translations/uk.js b/static/webui/translations/uk.js index cdef5c70..422a9e36 100644 --- a/static/webui/translations/uk.js +++ b/static/webui/translations/uk.js @@ -81,6 +81,13 @@ dict = { code_operatorFaceName: `Зовнішність оператора: |INDEX|`, code_succChange: `Успішно змінено.`, code_requiredInvigorationUpgrade: `Ви повинні вибрати як атакуюче, так і допоміжне вдосконалення.`, + code_feature_1: `[UNTRANSLATED] Orokin Reactor`, + code_feature_2: `[UNTRANSLATED] Exilus Adapter`, + code_feature_4: `[UNTRANSLATED] Gravimag`, + code_feature_8: `[UNTRANSLATED] Gild`, + code_feature_32: `[UNTRANSLATED] Arcane Slot`, + code_feature_64: `[UNTRANSLATED] Second Arcane Slot`, + code_feature_1024: `[UNTRANSLATED] Valence Override`, login_description: `Увійдіть, використовуючи облікові дані OpenWF (ті ж, що й у грі при підключенні до цього серверу).`, login_emailLabel: `Адреса електронної пошти`, login_passwordLabel: `Пароль`, @@ -155,6 +162,7 @@ dict = { detailedView_modularPartsLabel: `Змінити модульні частини`, detailedView_invigorationLabel: `Зміцнення`, detailedView_loadoutLabel: `Конфігурації`, + detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`, invigorations_offensive_AbilityStrength: `+200% до потужності здібностей.`, invigorations_offensive_AbilityRange: `+100% до досяжності здібностей.`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 58d8d467..b07c44a5 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -81,6 +81,13 @@ dict = { code_operatorFaceName: `指挥官面部 |INDEX|`, code_succChange: `更改成功`, code_requiredInvigorationUpgrade: `[UNTRANSLATED] You must select both an offensive & utility upgrade.`, + code_feature_1: `[UNTRANSLATED] Orokin Reactor`, + code_feature_2: `[UNTRANSLATED] Exilus Adapter`, + code_feature_4: `[UNTRANSLATED] Gravimag`, + code_feature_8: `[UNTRANSLATED] Gild`, + code_feature_32: `[UNTRANSLATED] Arcane Slot`, + code_feature_64: `[UNTRANSLATED] Second Arcane Slot`, + code_feature_1024: `[UNTRANSLATED] Valence Override`, login_description: `使用您的 OpenWF 账户凭证登录(与游戏内连接本服务器时使用的昵称相同)`, login_emailLabel: `电子邮箱`, login_passwordLabel: `密码`, @@ -155,6 +162,7 @@ dict = { detailedView_modularPartsLabel: `更换部件`, detailedView_invigorationLabel: `活化`, detailedView_loadoutLabel: `配置`, + detailedView_equipmentFeaturesLabel: `[UNTRANSLATED] Equipment Features`, invigorations_offensive_AbilityStrength: `+200%技能强度`, invigorations_offensive_AbilityRange: `+100%技能范围`, From 4ab5794f61c5e6ac3fccd184103c58f88445d7f1 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 6 Nov 2025 00:17:10 -0800 Subject: [PATCH 09/17] fix: acquisition of armory cache from daily tribute (#2990) Closes #2991 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2990 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/purchaseService.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/purchaseService.ts b/src/services/purchaseService.ts index 69277c54..c889af0c 100644 --- a/src/services/purchaseService.ts +++ b/src/services/purchaseService.ts @@ -491,6 +491,7 @@ export const handleStoreItemAcquisition = async ( const slotPurchaseNameToSlotName: Record = { SuitSlotItem: { name: "SuitBin", purchaseQuantity: 1 }, TwoSentinelSlotItem: { name: "SentinelBin", purchaseQuantity: 2 }, + WeaponSlotItem: { name: "WeaponBin", purchaseQuantity: 1 }, TwoWeaponSlotItem: { name: "WeaponBin", purchaseQuantity: 2 }, SpaceSuitSlotItem: { name: "SpaceSuitBin", purchaseQuantity: 1 }, TwoSpaceWeaponSlotItem: { name: "SpaceWeaponBin", purchaseQuantity: 2 }, @@ -514,7 +515,9 @@ const handleSlotPurchase = ( ): IPurchaseResponse => { logger.debug(`slot name ${slotPurchaseNameFull}`); const slotPurchaseName = slotPurchaseNameFull.substring(slotPurchaseNameFull.lastIndexOf("/") + 1); - if (!(slotPurchaseName in slotPurchaseNameToSlotName)) throw new Error(`invalid slot name ${slotPurchaseName}`); + if (!(slotPurchaseName in slotPurchaseNameToSlotName)) { + throw new Error(`invalid slot purchase name ${slotPurchaseName}`); + } logger.debug(`slot purchase name ${slotPurchaseName}`); const slotName = slotPurchaseNameToSlotName[slotPurchaseName].name; From 8ceab34f224567aacffa611aa3db846ce24cd5f5 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Thu, 6 Nov 2025 00:17:21 -0800 Subject: [PATCH 10/17] feat: get endpoint for giveStartingGear (#2997) Closes #2995 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2997 Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com> Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- .../api/giveStartingGearController.ts | 20 ++++++++++++++++++- src/routes/api.ts | 5 +++-- src/services/inventoryService.ts | 7 ++++--- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/controllers/api/giveStartingGearController.ts b/src/controllers/api/giveStartingGearController.ts index cd964ef1..895ffec3 100644 --- a/src/controllers/api/giveStartingGearController.ts +++ b/src/controllers/api/giveStartingGearController.ts @@ -4,7 +4,7 @@ import { getAccountIdForRequest } from "../../services/loginService.ts"; import type { TPartialStartingGear } from "../../types/inventoryTypes/inventoryTypes.ts"; import type { RequestHandler } from "express"; -export const giveStartingGearController: RequestHandler = async (req, res) => { +export const giveStartingGearPostController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const startingGear = getJSONfromString(String(req.body)); const inventory = await getInventory(accountId); @@ -14,3 +14,21 @@ export const giveStartingGearController: RequestHandler = async (req, res) => { res.send(inventoryChanges); }; + +export const giveStartingGearGetController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const inventory = await getInventory(accountId); + + const inventoryChanges = await addStartingGear(inventory, { + Suits: [ + { + ItemType: String(req.query.warframeName), + ItemId: { $oid: "0" }, + Configs: [] + } + ] + }); + await inventory.save(); + + res.send(inventoryChanges); // Not sure if this is even needed +}; diff --git a/src/routes/api.ts b/src/routes/api.ts index 6184c8f0..684a7285 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -80,7 +80,7 @@ import { giveKeyChainTriggeredItemsController } from "../controllers/api/giveKey import { giveKeyChainTriggeredMessageController } from "../controllers/api/giveKeyChainTriggeredMessageController.ts"; import { giveQuestKeyRewardController } from "../controllers/api/giveQuestKeyRewardController.ts"; import { giveShipDecoAndLoreFragmentController } from "../controllers/api/giveShipDecoAndLoreFragmentController.ts"; -import { giveStartingGearController } from "../controllers/api/giveStartingGearController.ts"; +import { giveStartingGearGetController, giveStartingGearPostController } from "../controllers/api/giveStartingGearController.ts"; import { guildTechController } from "../controllers/api/guildTechController.ts"; import { hostSessionController } from "../controllers/api/hostSessionController.ts"; import { hubBlessingController } from "../controllers/api/hubBlessingController.ts"; @@ -208,6 +208,7 @@ apiRouter.get("/getNewRewardSeed.php", getNewRewardSeedController); apiRouter.get("/getPastWeeklyChallenges.php", getPastWeeklyChallengesController) apiRouter.get("/getShip.php", getShipController); apiRouter.get("/getShipDecos.php", (_req, res) => { res.end(); }); // needed to log in on U22.8 +apiRouter.get("/giveStartingGear.php", giveStartingGearGetController); apiRouter.get("/getVendorInfo.php", getVendorInfoController); apiRouter.get("/hub", hubController); apiRouter.get("/hubInstances", hubInstancesController); @@ -297,7 +298,7 @@ apiRouter.post("/giveKeyChainTriggeredItems.php", giveKeyChainTriggeredItemsCont apiRouter.post("/giveKeyChainTriggeredMessage.php", giveKeyChainTriggeredMessageController); apiRouter.post("/giveQuestKeyReward.php", giveQuestKeyRewardController); apiRouter.post("/giveShipDecoAndLoreFragment.php", giveShipDecoAndLoreFragmentController); -apiRouter.post("/giveStartingGear.php", giveStartingGearController); +apiRouter.post("/giveStartingGear.php", giveStartingGearPostController); apiRouter.post("/guildTech.php", guildTechController); apiRouter.post("/hostSession.php", hostSessionController); apiRouter.post("/hubBlessing.php", hubBlessingController); diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index b8cbcc16..d01b42ff 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -137,18 +137,19 @@ export const createInventory = async ( export const addStartingGear = async ( inventory: TInventoryDatabaseDocument, - startingGear?: TPartialStartingGear + startingGear?: Partial ): Promise => { if (inventory.ReceivedStartingGear) { throw new Error(`account has already received starting gear`); } inventory.ReceivedStartingGear = true; - const { LongGuns, Pistols, Suits, Melee } = startingGear || { + const { LongGuns, Pistols, Suits, Melee } = { LongGuns: [{ ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }], Pistols: [{ ItemType: "/Lotus/Weapons/Tenno/Pistol/Pistol" }], Suits: [{ ItemType: "/Lotus/Powersuits/Excalibur/Excalibur" }], - Melee: [{ ItemType: "/Lotus/Weapons/Tenno/Melee/LongSword/LongSword" }] + Melee: [{ ItemType: "/Lotus/Weapons/Tenno/Melee/LongSword/LongSword" }], + ...startingGear }; //TODO: properly merge weapon bin changes it is currently static here From 9f432d39195b2ecce641c6eeaca60882ae39252b Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Thu, 6 Nov 2025 00:17:30 -0800 Subject: [PATCH 11/17] feat: get endpoint trainingResult (#3000) Closes #2994 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/3000 Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com> Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- .../api/trainingResultController.ts | 33 +++++++++++++------ src/routes/api.ts | 5 +-- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/controllers/api/trainingResultController.ts b/src/controllers/api/trainingResultController.ts index 8c1d55bd..dafa689a 100644 --- a/src/controllers/api/trainingResultController.ts +++ b/src/controllers/api/trainingResultController.ts @@ -17,14 +17,13 @@ interface ITrainingResultsResponse { InventoryChanges: IInventoryChanges; } -const trainingResultController: RequestHandler = async (req, res): Promise => { - const accountId = await getAccountIdForRequest(req); - - const trainingResults = getJSONfromString(String(req.body)); - +const handleTrainingProgress = async ( + accountId: string, + numLevelsGained: number +): Promise => { const inventory = await getInventory(accountId, "TrainingDate PlayerLevel TradesRemaining noMasteryRankUpCooldown"); - if (trainingResults.numLevelsGained == 1) { + if (numLevelsGained === 1) { let time = Date.now(); if (!inventory.noMasteryRankUpCooldown) { time += unixTimesInMs.hour * 23; @@ -67,13 +66,27 @@ const trainingResultController: RequestHandler = async (req, res): Promise const changedinventory = await inventory.save(); - res.json({ + return { NewTrainingDate: { $date: { $numberLong: changedinventory.TrainingDate.getTime().toString() } }, - NewLevel: trainingResults.numLevelsGained == 1 ? changedinventory.PlayerLevel : inventory.PlayerLevel, + NewLevel: numLevelsGained == 1 ? changedinventory.PlayerLevel : inventory.PlayerLevel, InventoryChanges: {} - } satisfies ITrainingResultsResponse); + }; }; -export { trainingResultController }; +export const trainingResultPostController: RequestHandler = async (req, res): Promise => { + const accountId = await getAccountIdForRequest(req); + const { numLevelsGained } = getJSONfromString(String(req.body)); + + const response = await handleTrainingProgress(accountId, numLevelsGained); + res.json(response satisfies ITrainingResultsResponse); +}; + +export const trainingResultGetController: RequestHandler = async (req, res): Promise => { + const accountId = await getAccountIdForRequest(req); + const numLevelsGained = Number(req.query.numLevelsGained ?? 0); + + const response = await handleTrainingProgress(accountId, numLevelsGained); + res.json(response satisfies ITrainingResultsResponse); +}; diff --git a/src/routes/api.ts b/src/routes/api.ts index 684a7285..5e84ee24 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -158,7 +158,7 @@ import { syndicateSacrificeController } from "../controllers/api/syndicateSacrif import { syndicateStandingBonusController } from "../controllers/api/syndicateStandingBonusController.ts"; import { tauntHistoryController } from "../controllers/api/tauntHistoryController.ts"; import { tradingController } from "../controllers/api/tradingController.ts"; -import { trainingResultController } from "../controllers/api/trainingResultController.ts"; +import { trainingResultGetController, trainingResultPostController } from "../controllers/api/trainingResultController.ts"; import { umbraController } from "../controllers/api/umbraController.ts"; import { unlockShipFeatureController } from "../controllers/api/unlockShipFeatureController.ts"; import { updateAlignmentController } from "../controllers/api/updateAlignmentController.ts"; @@ -236,6 +236,7 @@ apiRouter.get("/startLibraryDailyTask.php", startLibraryDailyTaskController); apiRouter.get("/startLibraryPersonalTarget.php", startLibraryPersonalTargetController); apiRouter.get("/surveys.php", surveysController); apiRouter.get("/trading.php", tradingController); +apiRouter.get("/trainingResult.php", trainingResultGetController); apiRouter.get("/updateSession.php", updateSessionGetController); apiRouter.get("/upgradeOperator.php", upgradeOperatorController); apiRouter.get("/worldState.php", worldStateController); // U8 @@ -357,7 +358,7 @@ apiRouter.post("/stepSequencers.php", stepSequencersController); apiRouter.post("/syndicateSacrifice.php", syndicateSacrificeController); apiRouter.post("/syndicateStandingBonus.php", syndicateStandingBonusController); apiRouter.post("/tauntHistory.php", tauntHistoryController); -apiRouter.post("/trainingResult.php", trainingResultController); +apiRouter.post("/trainingResult.php", trainingResultPostController); apiRouter.post("/umbra.php", umbraController); apiRouter.post("/unlockShipFeature.php", unlockShipFeatureController); apiRouter.post("/updateAlignment.php", updateAlignmentController); From 71785fffd7f7889264603d60bdbc963c405d81b8 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 6 Nov 2025 00:17:39 -0800 Subject: [PATCH 12/17] chore(webui): inform user when import did nothing (#3002) Closes #2998 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/3002 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/custom/importController.ts | 12 ++++-- src/services/importService.ts | 46 +++++++++++++++++++++- static/webui/script.js | 4 +- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/controllers/custom/importController.ts b/src/controllers/custom/importController.ts index 1dda3189..47a850f6 100644 --- a/src/controllers/custom/importController.ts +++ b/src/controllers/custom/importController.ts @@ -12,11 +12,16 @@ export const importController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const request = req.body as IImportRequest; + let anyKnownKey = false; + const inventory = await getInventory(accountId); - importInventory(inventory, request.inventory); - await inventory.save(); + if (importInventory(inventory, request.inventory)) { + anyKnownKey = true; + await inventory.save(); + } if ("LoadOutPresets" in request.inventory && request.inventory.LoadOutPresets) { + anyKnownKey = true; const loadout = await getLoadout(accountId); importLoadOutPresets(loadout, request.inventory.LoadOutPresets); await loadout.save(); @@ -27,12 +32,13 @@ export const importController: RequestHandler = async (req, res) => { "Apartment" in request.inventory || "TailorShop" in request.inventory ) { + anyKnownKey = true; const personalRooms = await getPersonalRooms(accountId); importPersonalRooms(personalRooms, request.inventory); await personalRooms.save(); } - res.end(); + res.json(anyKnownKey); broadcastInventoryUpdate(req); }; diff --git a/src/services/importService.ts b/src/services/importService.ts index c949a953..98ba73dd 100644 --- a/src/services/importService.ts +++ b/src/services/importService.ts @@ -254,17 +254,21 @@ const convertItemConfig = (client: T): T => { }; }; -export const importInventory = (db: TInventoryDatabaseDocument, client: Partial): void => { +export const importInventory = (db: TInventoryDatabaseDocument, client: Partial): boolean => { + let anyKnownKey = false; for (const key of equipmentKeys) { if (client[key] !== undefined) { + anyKnownKey = true; replaceArray(db[key], client[key].map(convertEquipment)); } } if (client.WeaponSkins !== undefined) { + anyKnownKey = true; replaceArray(db.WeaponSkins, client.WeaponSkins.map(convertWeaponSkin)); } for (const key of ["Upgrades", "CrewShipSalvagedWeaponSkins", "CrewShipWeaponSkins"] as const) { if (client[key] !== undefined) { + anyKnownKey = true; replaceArray(db[key], client[key].map(convertUpgrade)); } } @@ -280,6 +284,7 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< "CrewShipRawSalvage" ] as const) { if (client[key] !== undefined) { + anyKnownKey = true; db[key].splice(0, db[key].length); client[key].forEach(x => { db[key].push({ @@ -291,11 +296,13 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< } for (const key of ["AdultOperatorLoadOuts", "OperatorLoadOuts", "KahlLoadOuts"] as const) { if (client[key] !== undefined) { + anyKnownKey = true; replaceArray(db[key], client[key].map(convertOperatorConfig)); } } for (const key of slotNames) { if (client[key] !== undefined) { + anyKnownKey = true; replaceSlots(db[key], client[key]); } } @@ -312,6 +319,7 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< "Counselor" ] as const) { if (client[key] !== undefined) { + anyKnownKey = true; db[key] = client[key]; } } @@ -338,6 +346,7 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< "EchoesHexConquestCacheScoreMission" ] as const) { if (client[key] !== undefined) { + anyKnownKey = true; db[key] = client[key]; } } @@ -353,6 +362,7 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< "ActiveAvatarImageType" ] as const) { if (client[key] !== undefined) { + anyKnownKey = true; db[key] = client[key]; } } @@ -369,6 +379,7 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< "EchoesHexConquestActiveStickers" ] as const) { if (client[key] !== undefined) { + anyKnownKey = true; db[key] = client[key]; } } @@ -382,103 +393,133 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< "EntratiVaultCountResetDate" ] as const) { if (client[key] !== undefined) { + anyKnownKey = true; db[key] = fromMongoDate(client[key]); } } // IRewardAtten[] for (const key of ["SortieRewardAttenuation", "SpecialItemRewardAttenuation"] as const) { if (client[key] !== undefined) { + anyKnownKey = true; db[key] = client[key]; } } if (client.XPInfo !== undefined) { + anyKnownKey = true; db.XPInfo = client.XPInfo; } if (client.CurrentLoadOutIds !== undefined) { + anyKnownKey = true; db.CurrentLoadOutIds = client.CurrentLoadOutIds; } if (client.Affiliations !== undefined) { + anyKnownKey = true; db.Affiliations = client.Affiliations; } if (client.FusionTreasures !== undefined) { + anyKnownKey = true; db.FusionTreasures = client.FusionTreasures; } if (client.FocusUpgrades !== undefined) { + anyKnownKey = true; db.FocusUpgrades = client.FocusUpgrades; } if (client.EvolutionProgress !== undefined) { + anyKnownKey = true; db.EvolutionProgress = client.EvolutionProgress; } if (client.InfestedFoundry !== undefined) { + anyKnownKey = true; db.InfestedFoundry = convertInfestedFoundry(client.InfestedFoundry); } if (client.DialogueHistory !== undefined) { + anyKnownKey = true; db.DialogueHistory = convertDialogueHistory(client.DialogueHistory); } if (client.CustomMarkers !== undefined) { + anyKnownKey = true; db.CustomMarkers = client.CustomMarkers; } if (client.ChallengeProgress !== undefined) { + anyKnownKey = true; db.ChallengeProgress = client.ChallengeProgress; } if (client.QuestKeys !== undefined) { + anyKnownKey = true; replaceArray(db.QuestKeys, client.QuestKeys.map(convertQuestKey)); } if (client.LastRegionPlayed !== undefined) { + anyKnownKey = true; db.LastRegionPlayed = client.LastRegionPlayed; } if (client.PendingRecipes !== undefined) { + anyKnownKey = true; replaceArray(db.PendingRecipes, client.PendingRecipes.map(convertPendingRecipe)); } if (client.TauntHistory !== undefined) { + anyKnownKey = true; db.TauntHistory = client.TauntHistory; } if (client.LoreFragmentScans !== undefined) { + anyKnownKey = true; db.LoreFragmentScans = client.LoreFragmentScans; } for (const key of ["PendingSpectreLoadouts", "SpectreLoadouts"] as const) { if (client[key] !== undefined) { + anyKnownKey = true; db[key] = client[key]; } } if (client.FocusXP !== undefined) { + anyKnownKey = true; db.FocusXP = client.FocusXP; } for (const key of ["Alignment", "AlignmentReplay"] as const) { if (client[key] !== undefined) { + anyKnownKey = true; db[key] = client[key]; } } if (client.StepSequencers !== undefined) { + anyKnownKey = true; db.StepSequencers = client.StepSequencers; } if (client.CompletedJobChains !== undefined) { + anyKnownKey = true; db.CompletedJobChains = client.CompletedJobChains; } if (client.Nemesis !== undefined) { + anyKnownKey = true; db.Nemesis = convertNemesis(client.Nemesis); } if (client.PlayerSkills !== undefined) { + anyKnownKey = true; db.PlayerSkills = client.PlayerSkills; } if (client.LotusCustomization !== undefined) { + anyKnownKey = true; db.LotusCustomization = convertItemConfig(client.LotusCustomization); } if (client.CollectibleSeries !== undefined) { + anyKnownKey = true; db.CollectibleSeries = client.CollectibleSeries; } for (const key of ["LibraryAvailableDailyTaskInfo", "LibraryActiveDailyTaskInfo"] as const) { if (client[key] !== undefined) { + anyKnownKey = true; db[key] = client[key]; } } if (client.SongChallenges !== undefined) { + anyKnownKey = true; db.SongChallenges = client.SongChallenges; } if (client.Missions !== undefined) { + anyKnownKey = true; db.Missions = client.Missions; } if (client.FlavourItems !== undefined) { + anyKnownKey = true; db.FlavourItems.splice(0, db.FlavourItems.length); client.FlavourItems.forEach(x => { db.FlavourItems.push({ @@ -487,11 +528,14 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial< }); } if (client.Accolades !== undefined) { + anyKnownKey = true; db.Accolades = client.Accolades; } if (client.Boosters !== undefined) { + anyKnownKey = true; replaceArray(db.Boosters, client.Boosters); } + return anyKnownKey; }; export const importLoadOutConfig = (client: ILoadoutConfigClient): ILoadoutConfigDatabase => { diff --git a/static/webui/script.js b/static/webui/script.js index 422881c5..10a35d83 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -3603,8 +3603,8 @@ function doImport() { data: JSON.stringify({ inventory: JSON.parse($("#import-inventory").val()) }) - }).then(function () { - toast(loc("code_succImport")); + }).then(function (anyKnownKey) { + toast(loc(anyKnownKey ? "code_succImport" : "code_nothingToDo")); updateInventory(); }); } catch (e) { From e58655e3f47e2644d4d510da5a1f66e6ad2e79e3 Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Thu, 6 Nov 2025 00:17:52 -0800 Subject: [PATCH 13/17] feat: profileStats endpoint for U8 (#3003) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/3003 Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com> Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/controllers/api/getFriendsController.ts | 11 ++++++----- src/controllers/stats/viewController.ts | 3 +-- src/routes/stats.ts | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/controllers/api/getFriendsController.ts b/src/controllers/api/getFriendsController.ts index 797b44b9..ab3fe0a5 100644 --- a/src/controllers/api/getFriendsController.ts +++ b/src/controllers/api/getFriendsController.ts @@ -1,13 +1,14 @@ -import { toOid } from "../../helpers/inventoryHelpers.ts"; +import { toOid2 } from "../../helpers/inventoryHelpers.ts"; import { Friendship } from "../../models/friendModel.ts"; import { addAccountDataToFriendInfo, addInventoryDataToFriendInfo } from "../../services/friendService.ts"; -import { getAccountIdForRequest } from "../../services/loginService.ts"; +import { getAccountForRequest } from "../../services/loginService.ts"; import type { IFriendInfo } from "../../types/friendTypes.ts"; import type { Request, RequestHandler, Response } from "express"; // POST with {} instead of GET as of 38.5.0 export const getFriendsController: RequestHandler = async (req: Request, res: Response) => { - const accountId = await getAccountIdForRequest(req); + const account = await getAccountForRequest(req); + const accountId = account._id.toString(); const response: IGetFriendsResponse = { Current: [], IncomingFriendRequests: [], @@ -20,14 +21,14 @@ export const getFriendsController: RequestHandler = async (req: Request, res: Re for (const externalFriendship of externalFriendships) { if (!internalFriendships.find(x => x.friend.equals(externalFriendship.owner))) { response.IncomingFriendRequests.push({ - _id: toOid(externalFriendship.owner), + _id: toOid2(externalFriendship.owner, account.BuildLabel), Note: externalFriendship.Note }); } } for (const internalFriendship of internalFriendships) { const friendInfo: IFriendInfo = { - _id: toOid(internalFriendship.friend) + _id: toOid2(internalFriendship.friend, account.BuildLabel) }; if (externalFriendships.find(x => x.owner.equals(internalFriendship.friend))) { response.Current.push(friendInfo); diff --git a/src/controllers/stats/viewController.ts b/src/controllers/stats/viewController.ts index 861dacdc..72911bfc 100644 --- a/src/controllers/stats/viewController.ts +++ b/src/controllers/stats/viewController.ts @@ -1,11 +1,10 @@ import type { RequestHandler } from "express"; -import { getAccountIdForRequest } from "../../services/loginService.ts"; import { getInventory } from "../../services/inventoryService.ts"; import { getStats } from "../../services/statsService.ts"; import type { IStatsClient } from "../../types/statTypes.ts"; const viewController: RequestHandler = async (req, res) => { - const accountId = await getAccountIdForRequest(req); + const accountId = String(req.query.id ?? req.query.lookupId); const inventory = await getInventory(accountId, "XPInfo"); const playerStats = await getStats(accountId); diff --git a/src/routes/stats.ts b/src/routes/stats.ts index 22d54d28..8f3dfe5b 100644 --- a/src/routes/stats.ts +++ b/src/routes/stats.ts @@ -6,6 +6,7 @@ import { leaderboardController } from "../controllers/stats/leaderboardControlle const statsRouter = express.Router(); statsRouter.get("/view.php", viewController); +statsRouter.get("/profileStats.php", viewController); statsRouter.post("/upload.php", uploadController); statsRouter.post("/leaderboardWeekly.php", leaderboardController); statsRouter.post("/leaderboardArchived.php", leaderboardController); From e1add8bf18bcbfd4d523870db288f82fc124791f Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Fri, 7 Nov 2025 00:52:15 -0800 Subject: [PATCH 14/17] fix(webui): ignore BuildLabel (#3005) WebUI is designed to work with the modern inventory format. It simply doesn't work with the legacy oid. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/3005 Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com> Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> --- src/controllers/api/inventoryController.ts | 6 ++- static/webui/script.js | 62 +++++++++++----------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index b832c41b..e68ae983 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -271,7 +271,11 @@ export const inventoryController: RequestHandler = async (request, response) => await inventory.save(); response.json( - await getInventoryResponse(inventory, "xpBasedLevelCapDisabled" in request.query, account.BuildLabel) + await getInventoryResponse( + inventory, + "xpBasedLevelCapDisabled" in request.query, + "ignoreBuildLabel" in request.query ? undefined : account.BuildLabel + ) ); }; diff --git a/static/webui/script.js b/static/webui/script.js index 10a35d83..1eda8345 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -734,7 +734,7 @@ const accountCheats = document.querySelectorAll("#account-cheats input[id]"); // Assumes that caller revalidates authz function updateInventory() { - const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1"); + const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1&ignoreBuildLabel=1"); req.done(data => { window.itemListPromise.then(itemMap => { window.didInitialInventoryUpdate = true; @@ -2719,7 +2719,7 @@ function addMissingEvolutionProgress() { function maxRankAllEvolutions() { revalidateAuthz().then(() => { - const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1"); + const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1&ignoreBuildLabel=1"); req.done(data => { const requests = []; @@ -2743,7 +2743,7 @@ function maxRankAllEvolutions() { function maxRankAllEquipment(categories) { revalidateAuthz().then(() => { - const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1"); + const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1&ignoreBuildLabel=1"); req.done(data => { window.itemListPromise.then(itemMap => { const batchData = {}; @@ -3077,7 +3077,7 @@ function doAcquireRiven() { ]) }).done(function () { // Get riven's assigned id - $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1").done(data => { + $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1&ignoreBuildLabel=1").done(data => { for (const rawUpgrade of data.RawUpgrades) { if (rawUpgrade.ItemType === uniqueName) { // Add fingerprint to riven @@ -3301,33 +3301,35 @@ single.getRoute("/webui/cheats").on("beforeload", function () { function doUnlockAllFocusSchools() { revalidateAuthz().then(() => { - $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1").done(async data => { - const missingFocusUpgrades = { - "/Lotus/Upgrades/Focus/Attack/AttackFocusAbility": true, - "/Lotus/Upgrades/Focus/Tactic/TacticFocusAbility": true, - "/Lotus/Upgrades/Focus/Ward/WardFocusAbility": true, - "/Lotus/Upgrades/Focus/Defense/DefenseFocusAbility": true, - "/Lotus/Upgrades/Focus/Power/PowerFocusAbility": true - }; - if (data.FocusUpgrades) { - for (const focusUpgrade of data.FocusUpgrades) { - if (focusUpgrade.ItemType in missingFocusUpgrades) { - delete missingFocusUpgrades[focusUpgrade.ItemType]; + $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1&ignoreBuildLabel=1").done( + async data => { + const missingFocusUpgrades = { + "/Lotus/Upgrades/Focus/Attack/AttackFocusAbility": true, + "/Lotus/Upgrades/Focus/Tactic/TacticFocusAbility": true, + "/Lotus/Upgrades/Focus/Ward/WardFocusAbility": true, + "/Lotus/Upgrades/Focus/Defense/DefenseFocusAbility": true, + "/Lotus/Upgrades/Focus/Power/PowerFocusAbility": true + }; + if (data.FocusUpgrades) { + for (const focusUpgrade of data.FocusUpgrades) { + if (focusUpgrade.ItemType in missingFocusUpgrades) { + delete missingFocusUpgrades[focusUpgrade.ItemType]; + } + } + } + for (const upgradeType of Object.keys(missingFocusUpgrades)) { + await unlockFocusSchool(upgradeType); + } + if (Object.keys(missingFocusUpgrades).length == 0) { + toast(loc("code_focusAllUnlocked")); + } else { + toast(loc("code_focusUnlocked").split("|COUNT|").join(Object.keys(missingFocusUpgrades).length)); + if (ws_is_open) { + window.ws.send(JSON.stringify({ sync_inventory: true })); } } } - for (const upgradeType of Object.keys(missingFocusUpgrades)) { - await unlockFocusSchool(upgradeType); - } - if (Object.keys(missingFocusUpgrades).length == 0) { - toast(loc("code_focusAllUnlocked")); - } else { - toast(loc("code_focusUnlocked").split("|COUNT|").join(Object.keys(missingFocusUpgrades).length)); - if (ws_is_open) { - window.ws.send(JSON.stringify({ sync_inventory: true })); - } - } - }); + ); }); } @@ -3445,7 +3447,7 @@ function doAddAllMods() { modsAll.delete("/Lotus/Upgrades/Mods/Fusers/LegendaryModFuser"); revalidateAuthz().then(() => { - const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1"); + const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1&ignoreBuildLabel=1"); req.done(data => { for (const modOwned of data.RawUpgrades) { if ((modOwned.ItemCount ?? 1) > 0) { @@ -3477,7 +3479,7 @@ function doAddAllMods() { function doRemoveUnrankedMods() { revalidateAuthz().then(() => { - const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1"); + const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1&ignoreBuildLabel=1"); req.done(inventory => { window.itemListPromise.then(itemMap => { $.post({ From 5c397a180cd30ea55752c324e6609ff1319f2035 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Fri, 7 Nov 2025 00:52:23 -0800 Subject: [PATCH 15/17] fix: worldState being ignored by U18.18 - U20.4 (#3007) Closes #3006. A Bootstrapper code update will also be needed. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/3007 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/worldStateService.ts | 9 +++++++++ src/types/worldStateTypes.ts | 1 + 2 files changed, 10 insertions(+) diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index c8c1181f..decf7019 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -3639,6 +3639,15 @@ export const getWorldState = (buildLabel?: string): IWorldState => { } worldState.Tmp = JSON.stringify(tmp); + // This must be the last field in these versions. + if ( + buildLabel && + version_compare(buildLabel, "2016.08.19.17.12") >= 0 && + version_compare(buildLabel, "2017.05.05.15.41") <= 0 + ) { + worldState.WorldSeed = "4763605"; + } + return worldState; }; diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index 4c6aef44..10386dd6 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -2,6 +2,7 @@ import type { IMissionReward, TFaction, TMissionType } from "warframe-public-exp import type { IMongoDate, IOid } from "./commonTypes.ts"; export interface IWorldState { + WorldSeed?: string; Version: number; // for goals BuildLabel: string; Time: number; From f50f4a924b53b6099e477dbe3fc58da8234cb773 Mon Sep 17 00:00:00 2001 From: VoltPrime Date: Fri, 7 Nov 2025 00:52:32 -0800 Subject: [PATCH 16/17] fix: give MK1-Braton for old tutorial (#3008) The old tutorial is supposed to give the MK1-Braton, but giveStartingGear was defaulting to the regular Braton. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/3008 Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com> Co-authored-by: VoltPrime Co-committed-by: VoltPrime --- src/controllers/api/giveStartingGearController.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/controllers/api/giveStartingGearController.ts b/src/controllers/api/giveStartingGearController.ts index 895ffec3..6dcba34b 100644 --- a/src/controllers/api/giveStartingGearController.ts +++ b/src/controllers/api/giveStartingGearController.ts @@ -26,6 +26,13 @@ export const giveStartingGearGetController: RequestHandler = async (req, res) => ItemId: { $oid: "0" }, Configs: [] } + ], + LongGuns: [ + { + ItemType: "/Lotus/Weapons/Tenno/Rifle/StartingRifle", + ItemId: { $oid: "0" }, + Configs: [] + } ] }); await inventory.save(); From 5c1cf3bfb136c90c2251c4116bc4da42e58983a2 Mon Sep 17 00:00:00 2001 From: VoltPrime Date: Fri, 7 Nov 2025 23:36:06 -0800 Subject: [PATCH 17/17] feat: get endpoint for addFriendImage (#3009) Used by U17 and below. Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/3009 Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com> Co-authored-by: VoltPrime Co-committed-by: VoltPrime --- src/controllers/api/addFriendImageController.ts | 17 ++++++++++++++++- src/routes/api.ts | 5 +++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/controllers/api/addFriendImageController.ts b/src/controllers/api/addFriendImageController.ts index d3702c12..6d867ee8 100644 --- a/src/controllers/api/addFriendImageController.ts +++ b/src/controllers/api/addFriendImageController.ts @@ -3,7 +3,22 @@ import { getJSONfromString } from "../../helpers/stringHelpers.ts"; import { getAccountIdForRequest } from "../../services/loginService.ts"; import { Inventory } from "../../models/inventoryModels/inventoryModel.ts"; -export const addFriendImageController: RequestHandler = async (req, res) => { +export const addFriendImageGetController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + + await Inventory.updateOne( + { + accountOwnerId: accountId + }, + { + ActiveAvatarImageType: String(req.query.avatarImageType) + } + ); + + res.json({}); +}; + +export const addFriendImagePostController: RequestHandler = async (req, res) => { const accountId = await getAccountIdForRequest(req); const json = getJSONfromString(String(req.body)); diff --git a/src/routes/api.ts b/src/routes/api.ts index 5e84ee24..ee1f18fa 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -4,7 +4,7 @@ import { abortDojoComponentController } from "../controllers/api/abortDojoCompon import { abortDojoComponentDestructionController } from "../controllers/api/abortDojoComponentDestructionController.ts"; import { activateRandomModController } from "../controllers/api/activateRandomModController.ts"; import { addFriendController } from "../controllers/api/addFriendController.ts"; -import { addFriendImageController } from "../controllers/api/addFriendImageController.ts"; +import { addFriendImageGetController, addFriendImagePostController } from "../controllers/api/addFriendImageController.ts"; import { addIgnoredUserController } from "../controllers/api/addIgnoredUserController.ts"; import { addPendingFriendController } from "../controllers/api/addPendingFriendController.ts"; import { addToAllianceController } from "../controllers/api/addToAllianceController.ts"; @@ -178,6 +178,7 @@ const apiRouter = express.Router(); // get apiRouter.get("/abandonLibraryDailyTask.php", abandonLibraryDailyTaskController); apiRouter.get("/abortDojoComponentDestruction.php", abortDojoComponentDestructionController); +apiRouter.get("/addFriendImage.php", addFriendImageGetController) // U17 and below apiRouter.get("/apartment.php", apartmentController); apiRouter.get("/cancelGuildAdvertisement.php", cancelGuildAdvertisementController); apiRouter.get("/changeDojoRoot.php", changeDojoRootController); @@ -245,7 +246,7 @@ apiRouter.get("/worldState.php", worldStateController); // U8 apiRouter.post("/abortDojoComponent.php", abortDojoComponentController); apiRouter.post("/activateRandomMod.php", activateRandomModController); apiRouter.post("/addFriend.php", addFriendController); -apiRouter.post("/addFriendImage.php", addFriendImageController); +apiRouter.post("/addFriendImage.php", addFriendImagePostController); apiRouter.post("/addIgnoredUser.php", addIgnoredUserController); apiRouter.post("/addPendingFriend.php", addPendingFriendController); apiRouter.post("/addToAlliance.php", addToAllianceController);