feat: getProfileViewingData for players #1258

Merged
Sainan merged 1 commits from profile-viewing-data into main 2025-03-21 05:19:54 -07:00
9 changed files with 193 additions and 7 deletions

View File

@ -32,6 +32,7 @@ app.use(requestLogger);
app.use("/api", apiRouter); app.use("/api", apiRouter);
app.use("/", cacheRouter); app.use("/", cacheRouter);
app.use("/custom", customRouter); app.use("/custom", customRouter);
app.use("/dynamic", dynamicController);
app.use("/:id/dynamic", dynamicController); app.use("/:id/dynamic", dynamicController);
app.use("/pay", payRouter); app.use("/pay", payRouter);
app.use("/stats", statsRouter); app.use("/stats", statsRouter);

View File

@ -297,7 +297,7 @@ const resourceGetParent = (resourceName: string): string | undefined => {
}; };
// This is FNV1a-32 except operating under modulus 2^31 because JavaScript is stinky and likes producing negative integers out of nowhere. // This is FNV1a-32 except operating under modulus 2^31 because JavaScript is stinky and likes producing negative integers out of nowhere.
const catBreadHash = (name: string): number => { export const catBreadHash = (name: string): number => {
let hash = 2166136261; let hash = 2166136261;
for (let i = 0; i != name.length; ++i) { for (let i = 0; i != name.length; ++i) {
hash = (hash ^ name.charCodeAt(i)) & 0x7fffffff; hash = (hash ^ name.charCodeAt(i)) & 0x7fffffff;

View File

@ -0,0 +1,183 @@
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { Guild } from "@/src/models/guildModel";
import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
import { Account } from "@/src/models/loginModel";
import { Stats, TStatsDatabaseDocument } from "@/src/models/statsModel";
import { allDailyAffiliationKeys } from "@/src/services/inventoryService";
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import {
IAffiliation,
IAlignment,
IChallengeProgress,
IDailyAffiliations,
ILoadoutConfigClient,
IMission,
IPlayerSkills,
ITypeXPItem
} from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
import { catBreadHash } from "../api/inventoryController";
import { ExportCustoms } from "warframe-public-export-plus";
export const getProfileViewingDataController: RequestHandler = async (req, res) => {
if (!req.query.playerId) {
res.status(400).end();
return;
}
const account = await Account.findOne({ _id: req.query.playerId as string }, "DisplayName");
if (!account) {
res.status(400).send("No account or guild ID specified");
return;
}
const inventory = (await Inventory.findOne({ accountOwnerId: account._id }))!;
const loadout = (await Loadout.findOne({ _id: inventory.LoadOutPresets }, "NORMAL"))!;
const result: IPlayerProfileViewingDataResult = {
AccountId: toOid(account._id),
DisplayName: account.DisplayName,
PlayerLevel: inventory.PlayerLevel,
LoadOutInventory: {
WeaponSkins: [],
XPInfo: inventory.XPInfo
},
PlayerSkills: inventory.PlayerSkills,
ChallengeProgress: inventory.ChallengeProgress,
DeathMarks: inventory.DeathMarks,
Harvestable: inventory.Harvestable,
DeathSquadable: inventory.DeathSquadable,
Created: toMongoDate(inventory.Created),
MigratedToConsole: false,
Missions: inventory.Missions,
Affiliations: inventory.Affiliations,
DailyFocus: inventory.DailyFocus,
Wishlist: inventory.Wishlist,
Alignment: inventory.Alignment
};
if (inventory.CurrentLoadOutIds.length) {
result.LoadOutPreset = loadout.NORMAL.id(inventory.CurrentLoadOutIds[0].$oid)!.toJSON<ILoadoutConfigClient>();
result.LoadOutPreset.ItemId = undefined;
const skins = new Set<string>();
if (result.LoadOutPreset.s) {
result.LoadOutInventory.Suits = [
inventory.Suits.id(result.LoadOutPreset.s.ItemId.$oid)!.toJSON<IEquipmentClient>()
];
resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Suits[0]);
}
if (result.LoadOutPreset.p) {
result.LoadOutInventory.Pistols = [
inventory.Pistols.id(result.LoadOutPreset.p.ItemId.$oid)!.toJSON<IEquipmentClient>()
];
resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Pistols[0]);
}
if (result.LoadOutPreset.l) {
result.LoadOutInventory.LongGuns = [
inventory.LongGuns.id(result.LoadOutPreset.l.ItemId.$oid)!.toJSON<IEquipmentClient>()
];
resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.LongGuns[0]);
}
if (result.LoadOutPreset.m) {
result.LoadOutInventory.Melee = [
inventory.Melee.id(result.LoadOutPreset.m.ItemId.$oid)!.toJSON<IEquipmentClient>()
];
resolveAndCollectSkins(inventory, skins, result.LoadOutInventory.Melee[0]);
}
for (const skin of skins) {
result.LoadOutInventory.WeaponSkins.push({ ItemType: skin });
}
}
if (inventory.GuildId) {
const guild = (await Guild.findOne({ _id: inventory.GuildId }, "Name Tier XP Class"))!;
result.GuildId = toOid(inventory.GuildId);
result.GuildName = guild.Name;
result.GuildTier = guild.Tier;
result.GuildXp = guild.XP;
result.GuildClass = guild.Class;
result.GuildEmblem = false;
}
for (const key of allDailyAffiliationKeys) {
result[key] = inventory[key];
}
const stats = (await Stats.findOne({ accountOwnerId: account._id }))!.toJSON<Partial<TStatsDatabaseDocument>>();
delete stats._id;
delete stats.__v;
delete stats.accountOwnerId;
res.json({
Results: [result],
TechProjects: [],
XpComponents: [],
//XpCacheExpiryDate, some IMongoDate in the future, no clue what it's for
Stats: stats
});
};
interface IPlayerProfileViewingDataResult extends Partial<IDailyAffiliations> {
AccountId: IOid;
DisplayName: string;
PlayerLevel: number;
LoadOutPreset?: Omit<ILoadoutConfigClient, "ItemId"> & { ItemId?: IOid };
LoadOutInventory: {
WeaponSkins: { ItemType: string }[];
Suits?: IEquipmentClient[];
Pistols?: IEquipmentClient[];
LongGuns?: IEquipmentClient[];
Melee?: IEquipmentClient[];
XPInfo: ITypeXPItem[];
};
GuildId?: IOid;
GuildName?: string;
GuildTier?: number;
GuildXp?: number;
GuildClass?: number;
GuildEmblem?: boolean;
PlayerSkills: IPlayerSkills;
ChallengeProgress: IChallengeProgress[];
DeathMarks: string[];
Harvestable: boolean;
DeathSquadable: boolean;
Created: IMongoDate;
MigratedToConsole: boolean;
Missions: IMission[];
Affiliations: IAffiliation[];
DailyFocus: number;
Wishlist: string[];
Alignment?: IAlignment;
}
let skinLookupTable: Record<number, string> | undefined;
const resolveAndCollectSkins = (
inventory: TInventoryDatabaseDocument,
skins: Set<string>,
item: IEquipmentClient
): void => {
for (const config of item.Configs) {
if (config.Skins) {
for (let i = 0; i != config.Skins.length; ++i) {
// Resolve oids to type names
if (config.Skins[i].length == 24) {
if (config.Skins[i].substring(0, 16) == "ca70ca70ca70ca70") {
if (!skinLookupTable) {
skinLookupTable = {};
for (const key of Object.keys(ExportCustoms)) {
skinLookupTable[catBreadHash(key)] = key;
}
}
config.Skins[i] = skinLookupTable[parseInt(config.Skins[i].substring(16), 16)];
} else {
const skinItem = inventory.WeaponSkins.id(config.Skins[i]);
config.Skins[i] = skinItem ? skinItem.ItemType : "";
}
}
// Collect type names
if (config.Skins[i]) {
skins.add(config.Skins[i]);
}
}
}
}
};

View File

@ -153,6 +153,7 @@ const guildSchema = new Schema<IGuildDatabase>(
LongMOTD: { type: longMOTDSchema, default: undefined }, LongMOTD: { type: longMOTDSchema, default: undefined },
Ranks: { type: [guildRankSchema], default: defaultRanks }, Ranks: { type: [guildRankSchema], default: defaultRanks },
TradeTax: { type: Number, default: 0 }, TradeTax: { type: Number, default: 0 },
Tier: { type: Number, default: 1 },
DojoComponents: { type: [dojoComponentSchema], default: [] }, DojoComponents: { type: [dojoComponentSchema], default: [] },
DojoCapacity: { type: Number, default: 100 }, DojoCapacity: { type: Number, default: 100 },
DojoEnergy: { type: Number, default: 5 }, DojoEnergy: { type: Number, default: 5 },

View File

@ -468,7 +468,6 @@ const seasonChallengeHistorySchema = new Schema<ISeasonChallenge>(
//TODO: check whether this is complete //TODO: check whether this is complete
const playerSkillsSchema = new Schema<IPlayerSkills>( const playerSkillsSchema = new Schema<IPlayerSkills>(
{ {
LPP_NONE: { type: Number, default: 0 },
LPP_SPACE: { type: Number, default: 0 }, LPP_SPACE: { type: Number, default: 0 },
LPS_PILOTING: { type: Number, default: 0 }, LPS_PILOTING: { type: Number, default: 0 },
LPS_GUNNERY: { type: Number, default: 0 }, LPS_GUNNERY: { type: Number, default: 0 },

View File

@ -1,10 +1,12 @@
import { aggregateSessionsController } from "@/src/controllers/dynamic/aggregateSessionsController";
import { worldStateController } from "@/src/controllers/dynamic/worldStateController";
import express from "express"; import express from "express";
import { aggregateSessionsController } from "@/src/controllers/dynamic/aggregateSessionsController";
import { getProfileViewingDataController } from "@/src/controllers/dynamic/getProfileViewingDataController";
import { worldStateController } from "@/src/controllers/dynamic/worldStateController";
const dynamicController = express.Router(); const dynamicController = express.Router();
dynamicController.get("/worldState.php", worldStateController);
dynamicController.get("/aggregateSessions.php", aggregateSessionsController); dynamicController.get("/aggregateSessions.php", aggregateSessionsController);
dynamicController.get("/getProfileViewingData.php", getProfileViewingDataController);
dynamicController.get("/worldState.php", worldStateController);
export { dynamicController }; export { dynamicController };

View File

@ -91,7 +91,7 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s
Members: members, Members: members,
Ranks: guild.Ranks, Ranks: guild.Ranks,
TradeTax: guild.TradeTax, TradeTax: guild.TradeTax,
Tier: 1, Tier: guild.Tier,
Vault: getGuildVault(guild), Vault: getGuildVault(guild),
ActiveDojoColorResearch: guild.ActiveDojoColorResearch, ActiveDojoColorResearch: guild.ActiveDojoColorResearch,
Class: guild.Class, Class: guild.Class,

View File

@ -27,6 +27,7 @@ export interface IGuildDatabase {
LongMOTD?: ILongMOTD; LongMOTD?: ILongMOTD;
Ranks: IGuildRank[]; Ranks: IGuildRank[];
TradeTax: number; TradeTax: number;
Tier: number;
DojoComponents: IDojoComponentDatabase[]; DojoComponents: IDojoComponentDatabase[];
DojoCapacity: number; DojoCapacity: number;

View File

@ -937,7 +937,6 @@ export interface IPersonalTechProject {
} }
export interface IPlayerSkills { export interface IPlayerSkills {
LPP_NONE: number;
LPP_SPACE: number; LPP_SPACE: number;
LPS_PILOTING: number; LPS_PILOTING: number;
LPS_GUNNERY: number; LPS_GUNNERY: number;