merge upstream

This commit is contained in:
Animan8000 2025-05-01 04:13:37 -07:00
commit dd0d60c22f
14 changed files with 152 additions and 100 deletions

View File

@ -11,9 +11,10 @@ import {
import { getAccountForRequest, getAccountFromSuffixedName, getSuffixedName } from "@/src/services/loginService";
import { addItems, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger";
import { ExportFlavour, ExportGear } from "warframe-public-export-plus";
import { ExportFlavour } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService";
import { IOid } from "@/src/types/commonTypes";
export const inboxController: RequestHandler = async (req, res) => {
const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query;
@ -28,10 +29,10 @@ export const inboxController: RequestHandler = async (req, res) => {
return;
}
await deleteMessageRead(deleteId as string);
await deleteMessageRead(parseOid(deleteId as string));
res.status(200).end();
} else if (messageId) {
const message = await getMessage(messageId as string);
const message = await getMessage(parseOid(messageId as string));
message.r = true;
await message.save();
@ -50,7 +51,7 @@ export const inboxController: RequestHandler = async (req, res) => {
inventory,
attachmentItems.map(attItem => ({
ItemType: isStoreItem(attItem) ? fromStoreItem(attItem) : attItem,
ItemCount: attItem in ExportGear ? (ExportGear[attItem].purchaseQuantity ?? 1) : 1
ItemCount: 1
})),
inventoryChanges
);
@ -100,7 +101,7 @@ export const inboxController: RequestHandler = async (req, res) => {
await createNewEventMessages(req);
const messages = await Inbox.find({ ownerId: accountId }).sort({ date: 1 });
const latestClientMessage = messages.find(m => m._id.toString() === latestClientMessageId);
const latestClientMessage = messages.find(m => m._id.toString() === parseOid(latestClientMessageId as string));
if (!latestClientMessage) {
logger.debug(`this should only happen after DeleteAllRead `);
@ -123,3 +124,11 @@ export const inboxController: RequestHandler = async (req, res) => {
res.json({ Inbox: inbox });
}
};
// 33.6.0 has query arguments like lastMessage={"$oid":"68112baebf192e786d1502bb"} instead of lastMessage=68112baebf192e786d1502bb
const parseOid = (oid: string): string => {
if (oid[0] == "{") {
return (JSON.parse(oid) as IOid).$oid;
}
return oid;
};

View File

@ -106,9 +106,16 @@ export const inventoryController: RequestHandler = async (request, response) =>
const currentDuviriMood = Math.trunc(Date.now() / 7200000);
if (lastSyncDuviriMood != currentDuviriMood) {
logger.debug(`refreshing duviri seed`);
if (!inventory.DuviriInfo) {
inventory.DuviriInfo = {
Seed: generateRewardSeed(),
NumCompletions: 0
};
} else {
inventory.DuviriInfo.Seed = generateRewardSeed();
}
}
}
inventory.LastInventorySync = new Types.ObjectId();
await inventory.save();

View File

@ -7,6 +7,7 @@ import { Account } from "@/src/models/loginModel";
import { createAccount, isCorrectPassword, isNameTaken } from "@/src/services/loginService";
import { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "@/src/types/loginTypes";
import { logger } from "@/src/utils/logger";
import { version_compare } from "@/src/services/worldStateService";
export const loginController: RequestHandler = async (request, response) => {
const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object
@ -94,12 +95,11 @@ export const loginController: RequestHandler = async (request, response) => {
};
const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, buildLabel: string): ILoginResponse => {
return {
const resp: ILoginResponse = {
id: account.id,
DisplayName: account.DisplayName,
CountryCode: account.CountryCode,
ClientType: account.ClientType,
CrossPlatformAllowed: account.CrossPlatformAllowed,
ForceLogoutVersion: account.ForceLogoutVersion,
AmazonAuthToken: account.AmazonAuthToken,
AmazonRefreshToken: account.AmazonRefreshToken,
@ -108,11 +108,17 @@ const createLoginResponse = (myAddress: string, account: IDatabaseAccountJson, b
Nonce: account.Nonce,
Groups: [],
IRC: config.myIrcAddresses ?? [myAddress],
platformCDNs: [`https://${myAddress}/`],
HUB: `https://${myAddress}/api/`,
NRS: config.NRS,
DTLS: 99,
BuildLabel: buildLabel,
MatchmakingBuildId: buildConfig.matchmakingBuildId
BuildLabel: buildLabel
};
if (version_compare(buildLabel, "2022.09.06.19.24") >= 0) {
resp.CrossPlatformAllowed = account.CrossPlatformAllowed;
resp.HUB = `https://${myAddress}/api/`;
resp.MatchmakingBuildId = buildConfig.matchmakingBuildId;
if (version_compare(buildLabel, "2023.04.25.23.40") >= 0) {
resp.platformCDNs = [`https://${myAddress}/`];
}
}
return resp;
};

View File

@ -1,4 +1,4 @@
import { addGearExpByCategory, getInventory } from "@/src/services/inventoryService";
import { applyClientEquipmentUpdates, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
@ -20,7 +20,7 @@ export const addXpController: RequestHandler = async (req, res) => {
}
}
}
addGearExpByCategory(inventory, gear, category as TEquipmentKey);
applyClientEquipmentUpdates(inventory, gear, category as TEquipmentKey);
}
await inventory.save();
res.end();

View File

@ -10,6 +10,10 @@ export const toMongoDate = (date: Date): IMongoDate => {
return { $date: { $numberLong: date.getTime().toString() } };
};
export const fromMongoDate = (date: IMongoDate): Date => {
return new Date(parseInt(date.$date.$numberLong));
};
export const kubrowWeights: Record<TRarity, number> = {
COMMON: 6,
UNCOMMON: 4,

View File

@ -391,8 +391,8 @@ MailboxSchema.set("toJSON", {
const DuviriInfoSchema = new Schema<IDuviriInfo>(
{
Seed: BigInt,
NumCompletions: { type: Number, default: 0 }
Seed: { type: BigInt, required: true },
NumCompletions: { type: Number, required: true }
},
{
_id: false,
@ -1688,9 +1688,9 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//Like BossAladV,BossCaptainVor come for you on missions % chance
DeathMarks: { type: [String], default: [] },
//Zanuka
Harvestable: Boolean,
Harvestable: { type: Boolean, default: true },
//Grustag three
DeathSquadable: Boolean,
DeathSquadable: { type: Boolean, default: true },
EndlessXP: { type: [endlessXpProgressSchema], default: undefined },

View File

@ -69,6 +69,7 @@ import {
import { createShip } from "./shipService";
import {
catbrowDetails,
fromMongoDate,
kubrowDetails,
kubrowFurPatternsWeights,
kubrowWeights,
@ -486,6 +487,10 @@ export const addItem = async (
};
}
if (typeName in ExportGear) {
// Multipling by purchase quantity for gear because:
// - The Saya's Vigil scanner message has it as a non-counted attachment.
// - Blueprints for Ancient Protector Specter, Shield Osprey Specter, etc. have num=1 despite giving their purchaseQuantity.
quantity *= ExportGear[typeName].purchaseQuantity ?? 1;
const consumablesChanges = [
{
ItemType: typeName,
@ -1471,21 +1476,20 @@ export const addEmailItem = async (
return inventoryChanges;
};
//TODO: wrong id is not erroring
export const addGearExpByCategory = (
export const applyClientEquipmentUpdates = (
inventory: TInventoryDatabaseDocument,
gearArray: IEquipmentClient[],
categoryName: TEquipmentKey
): void => {
const category = inventory[categoryName];
gearArray.forEach(({ ItemId, XP }) => {
if (!XP) {
return;
gearArray.forEach(({ ItemId, XP, InfestationDate }) => {
const item = category.id(ItemId.$oid);
if (!item) {
throw new Error(`No item with id ${ItemId.$oid} in ${categoryName}`);
}
const item = category.id(ItemId.$oid);
if (item) {
if (XP) {
item.XP ??= 0;
item.XP += XP;
@ -1500,6 +1504,10 @@ export const addGearExpByCategory = (
});
}
}
if (InfestationDate) {
item.InfestationDate = fromMongoDate(InfestationDate);
}
});
};

View File

@ -21,7 +21,6 @@ import {
addFocusXpIncreases,
addFusionPoints,
addFusionTreasures,
addGearExpByCategory,
addItem,
addLevelKeys,
addLoreFragmentScans,
@ -32,6 +31,7 @@ import {
addShipDecorations,
addSkin,
addStanding,
applyClientEquipmentUpdates,
combineInventoryChanges,
generateRewardSeed,
getCalendarProgress,
@ -143,38 +143,6 @@ export const addMissionInventoryUpdates = async (
]);
}
}
// Somewhat heuristically detect G3 capture:
// - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1365
// - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1694
// - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1724
if (
inventoryUpdates.MissionFailed &&
inventoryUpdates.MissionStatus == "GS_FAILURE" &&
inventoryUpdates.ObjectiveReached &&
!inventoryUpdates.LockedWeaponGroup &&
!inventory.LockedWeaponGroup &&
!inventoryUpdates.LevelKeyName
) {
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "NORMAL"))!;
const config = loadout.NORMAL.id(inventory.CurrentLoadOutIds[0].$oid)!;
const SuitId = new Types.ObjectId(config.s!.ItemId.$oid);
inventory.BrandedSuits ??= [];
if (!inventory.BrandedSuits.find(x => x.equals(SuitId))) {
inventory.BrandedSuits.push(SuitId);
await createMessage(inventory.accountOwnerId, [
{
sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
msg: "/Lotus/Language/G1Quests/BrandedMessage",
sub: "/Lotus/Language/G1Quests/BrandedTitle",
att: ["/Lotus/Types/Recipes/Components/BrandRemovalBlueprint"],
highPriority: true // TOVERIFY: I cannot find any content of this within the last 10 years so I can only assume that highPriority is set (it certainly would make sense), but I just don't know for sure that it is so on live.
}
]);
}
}
}
if (inventoryUpdates.RewardInfo) {
if (inventoryUpdates.RewardInfo.periodicMissionTag) {
@ -537,6 +505,23 @@ export const addMissionInventoryUpdates = async (
}
break;
}
case "BrandedSuits": {
inventory.BrandedSuits ??= [];
if (!inventory.BrandedSuits.find(x => x.equals(value.$oid))) {
inventory.BrandedSuits.push(new Types.ObjectId(value.$oid));
await createMessage(inventory.accountOwnerId, [
{
sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
msg: "/Lotus/Language/G1Quests/BrandedMessage",
sub: "/Lotus/Language/G1Quests/BrandedTitle",
att: ["/Lotus/Types/Recipes/Components/BrandRemovalBlueprint"],
highPriority: true // TOVERIFY: I cannot find any content of this within the last 10 years so I can only assume that highPriority is set (it certainly would make sense), but I just don't know for sure that it is so on live.
}
]);
}
break;
}
case "LockedWeaponGroup": {
inventory.LockedWeaponGroup = {
s: new Types.ObjectId(value.s.$oid),
@ -545,12 +530,17 @@ export const addMissionInventoryUpdates = async (
m: value.m ? new Types.ObjectId(value.m.$oid) : undefined,
sn: value.sn ? new Types.ObjectId(value.sn.$oid) : undefined
};
inventory.Harvestable = false;
break;
}
case "UnlockWeapons": {
inventory.LockedWeaponGroup = undefined;
break;
}
case "IncHarvester": {
inventory.Harvestable = true;
break;
}
case "CurrentLoadOutIds": {
if (value.LoadOuts) {
const loadout = await Loadout.findOne({ loadoutOwnerId: inventory.accountOwnerId });
@ -611,7 +601,7 @@ export const addMissionInventoryUpdates = async (
case "duviriCaveOffers": {
// Duviri cave offers (generated with the duviri seed) change after completing one of its game modes (not when aborting).
if (inventoryUpdates.MissionStatus != "GS_QUIT") {
inventory.DuviriInfo.Seed = generateRewardSeed();
inventory.DuviriInfo!.Seed = generateRewardSeed();
}
break;
}
@ -670,9 +660,8 @@ export const addMissionInventoryUpdates = async (
}
break;
default:
// Equipment XP updates
if (equipmentKeys.includes(key as TEquipmentKey)) {
addGearExpByCategory(inventory, value as IEquipmentClient[], key as TEquipmentKey);
applyClientEquipmentUpdates(inventory, value as IEquipmentClient[], key as TEquipmentKey);
}
break;
// if (

View File

@ -6,7 +6,7 @@ export interface IRngResult {
probability: number;
}
export const getRandomElement = <T>(arr: T[]): T | undefined => {
export const getRandomElement = <T>(arr: readonly T[]): T | undefined => {
return arr[Math.floor(Math.random() * arr.length)];
};
@ -113,7 +113,7 @@ export class CRng {
return min;
}
randomElement<T>(arr: T[]): T | undefined {
randomElement<T>(arr: readonly T[]): T | undefined {
return arr[Math.floor(this.random() * arr.length)];
}
@ -145,7 +145,7 @@ export class SRng {
return min;
}
randomElement<T>(arr: T[]): T | undefined {
randomElement<T>(arr: readonly T[]): T | undefined {
return arr[this.randomInt(0, arr.length - 1)];
}

View File

@ -33,9 +33,11 @@ const sortieBosses = [
"SORTIE_BOSS_LEPHANTIS",
"SORTIE_BOSS_INFALAD",
"SORTIE_BOSS_CORRUPTED_VOR"
];
] as const;
const sortieBossToFaction: Record<string, string> = {
type TSortieBoss = (typeof sortieBosses)[number];
const sortieBossToFaction: Record<TSortieBoss, string> = {
SORTIE_BOSS_HYENA: "FC_CORPUS",
SORTIE_BOSS_KELA: "FC_GRINEER",
SORTIE_BOSS_VOR: "FC_GRINEER",
@ -74,21 +76,22 @@ const sortieFactionToSpecialMissionTileset: Record<string, string> = {
FC_INFESTATION: "CorpusShipTileset"
};
const sortieBossNode: Record<string, string> = {
SORTIE_BOSS_HYENA: "SolNode127",
SORTIE_BOSS_KELA: "SolNode193",
SORTIE_BOSS_VOR: "SolNode108",
SORTIE_BOSS_RUK: "SolNode32",
SORTIE_BOSS_HEK: "SolNode24",
SORTIE_BOSS_KRIL: "SolNode99",
SORTIE_BOSS_TYL: "SolNode105",
SORTIE_BOSS_JACKAL: "SolNode104",
const sortieBossNode: Record<Exclude<TSortieBoss, "SORTIE_BOSS_CORRUPTED_VOR">, string> = {
SORTIE_BOSS_ALAD: "SolNode53",
SORTIE_BOSS_AMBULAS: "SolNode51",
SORTIE_BOSS_NEF: "SettlementNode20",
SORTIE_BOSS_RAPTOR: "SolNode210",
SORTIE_BOSS_HEK: "SolNode24",
SORTIE_BOSS_HYENA: "SolNode127",
SORTIE_BOSS_INFALAD: "SolNode166",
SORTIE_BOSS_JACKAL: "SolNode104",
SORTIE_BOSS_KELA: "SolNode193",
SORTIE_BOSS_KRIL: "SolNode99",
SORTIE_BOSS_LEPHANTIS: "SolNode712",
SORTIE_BOSS_INFALAD: "SolNode705"
SORTIE_BOSS_NEF: "SettlementNode20",
SORTIE_BOSS_PHORID: "SolNode171",
SORTIE_BOSS_RAPTOR: "SolNode210",
SORTIE_BOSS_RUK: "SolNode32",
SORTIE_BOSS_TYL: "SolNode105",
SORTIE_BOSS_VOR: "SolNode108"
};
const eidolonJobs = [
@ -270,6 +273,7 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => {
key in sortieTilesets
) {
if (
value.missionIndex != 0 && // Assassination will be decided independently
value.missionIndex != 5 && // Sorties do not have capture missions
!availableMissionIndexes.includes(value.missionIndex)
) {
@ -310,20 +314,10 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => {
sortieFactionToSpecialMissionTileset[sortieBossToFaction[boss]]
);
if (i == 2 && rng.randomInt(0, 2) == 2) {
if (i == 2 && boss != "SORTIE_BOSS_CORRUPTED_VOR" && rng.randomInt(0, 2) == 2) {
const filteredModifiers = modifiers.filter(mod => mod !== "SORTIE_MODIFIER_MELEE_ONLY");
const modifierType = rng.randomElement(filteredModifiers)!;
if (boss == "SORTIE_BOSS_PHORID") {
selectedNodes.push({
missionType: "MT_ASSASSINATION",
modifierType,
node,
tileset: sortieTilesets[node as keyof typeof sortieTilesets]
});
nodes.splice(randomIndex, 1);
continue;
} else if (sortieBossNode[boss]) {
selectedNodes.push({
missionType: "MT_ASSASSINATION",
modifierType,
@ -332,7 +326,6 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => {
});
continue;
}
}
const missionType = eMissionType[missionIndex].tag;
@ -724,6 +717,11 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
SyndicateMissions: [...staticWorldState.SyndicateMissions]
};
// Omit void fissures for versions prior to Whispers in the Walls to avoid errors with the unknown deimos nodes having void fissures.
if (buildLabel && version_compare(buildLabel, "2023.11.06.13.39") <= 0) {
worldState.ActiveMissions = [];
}
if (config.worldState?.starDays) {
worldState.Goals.push({
_id: { $oid: "67a4dcce2a198564d62e1647" },
@ -1227,3 +1225,20 @@ export const isArchwingMission = (node: IRegion): boolean => {
}
return false;
};
export const version_compare = (a: string, b: string): number => {
const a_digits = a
.split("/")[0]
.split(".")
.map(x => parseInt(x));
const b_digits = b
.split("/")[0]
.split(".")
.map(x => parseInt(x));
for (let i = 0; i != a_digits.length; ++i) {
if (a_digits[i] != b_digits[i]) {
return a_digits[i] > b_digits[i] ? 1 : -1;
}
}
return 0;
};

View File

@ -202,7 +202,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
OperatorLoadOuts: IOperatorConfigClient[];
KahlLoadOuts: IOperatorConfigClient[];
DuviriInfo: IDuviriInfo;
DuviriInfo?: IDuviriInfo;
Mailbox?: IMailboxClient;
SubscribedToEmails: number;
Created: IMongoDate;

View File

@ -4,7 +4,7 @@ export interface IAccountAndLoginResponseCommons {
DisplayName: string;
CountryCode: string;
ClientType: string;
CrossPlatformAllowed: boolean;
CrossPlatformAllowed?: boolean;
ForceLogoutVersion: number;
AmazonAuthToken?: string;
AmazonRefreshToken?: string;
@ -46,7 +46,7 @@ export interface ILoginResponse extends IAccountAndLoginResponseCommons {
id: string;
Groups: IGroup[];
BuildLabel: string;
MatchmakingBuildId: string;
MatchmakingBuildId?: string;
platformCDNs?: string[];
NRS?: string[];
DTLS: number;

View File

@ -130,6 +130,7 @@ export type IMissionInventoryUpdateRequest = {
}[];
KubrowPetEggs?: IKubrowPetEggClient[];
DiscoveredMarkers?: IDiscoveredMarker[];
BrandedSuits?: IOid; // sent when captured by g3
LockedWeaponGroup?: ILockedWeaponGroupClient; // sent when captured by zanuka
UnlockWeapons?: boolean; // sent when recovered weapons from zanuka capture
IncHarvester?: boolean; // sent when recovered weapons from zanuka capture

View File

@ -10,6 +10,7 @@ export interface IWorldState {
LiteSorties: ILiteSortie[];
SyndicateMissions: ISyndicateMissionInfo[];
GlobalUpgrades: IGlobalUpgrade[];
ActiveMissions: IFissure[];
NodeOverrides: INodeOverride[];
EndlessXpChoices: IEndlessXpChoice[];
SeasonInfo: {
@ -71,6 +72,18 @@ export interface IGlobalUpgrade {
LocalizeDescTag: string;
}
export interface IFissure {
_id: IOid;
Region: number;
Seed: number;
Activation: IMongoDate;
Expiry: IMongoDate;
Node: string;
MissionType: string;
Modifier: string;
Hard?: boolean;
}
export interface INodeOverride {
_id: IOid;
Activation?: IMongoDate;