diff --git a/project/src/controllers/InventoryController.ts b/project/src/controllers/InventoryController.ts index 0db762ddb..df025fe75 100644 --- a/project/src/controllers/InventoryController.ts +++ b/project/src/controllers/InventoryController.ts @@ -928,24 +928,17 @@ export class InventoryController { } public setFavoriteItem(pmcData: IPmcData, request: ISetFavoriteItems, sessionId: string): void { - if (!pmcData.Inventory.favoriteItems) { - pmcData.Inventory.favoriteItems = []; - } + // The client sends the full list of favorite items, so clear the current favorites + pmcData.Inventory.favoriteItems = []; for (const itemId of request.items) { - // If id already exists in array, we're removing it - const indexOfItemAlreadyFavorited = pmcData.Inventory.favoriteItems.findIndex((x) => x._id === itemId); - if (indexOfItemAlreadyFavorited > -1) { - pmcData.Inventory.favoriteItems.splice(indexOfItemAlreadyFavorited, 1); - } else { - const item = pmcData.Inventory.items.find((i) => i._id === itemId); - - if (item === undefined) { - continue; - } - - pmcData.Inventory.favoriteItems.push(item); + // Leaving this in as validation that the item exists in the profile + const item = pmcData.Inventory.items.find((i) => i._id === itemId); + if (item === undefined) { + continue; } + + pmcData.Inventory.favoriteItems.push(itemId); } } diff --git a/project/src/controllers/ProfileController.ts b/project/src/controllers/ProfileController.ts index 1c7d78e98..4fa9b0332 100644 --- a/project/src/controllers/ProfileController.ts +++ b/project/src/controllers/ProfileController.ts @@ -401,6 +401,9 @@ export class ProfileController { return response; } + /** + * Handle client/profile/view + */ public getOtherProfile(sessionId: string, request: IGetOtherProfileRequest): IGetOtherProfileResponse { const player = this.profileHelper.getFullProfile(sessionId); const playerPmc = player.characters.pmc; @@ -431,7 +434,7 @@ export class ProfileController { Items: playerPmc.Inventory.items, }, achievements: playerPmc.Achievements, - favoriteItems: playerPmc.Inventory.favoriteItems ?? [], + favoriteItems: this.profileHelper.getOtherProfileFavorites(playerPmc), pmcStats: { eft: { totalInGameTime: playerPmc.Stats.Eft.TotalInGameTime, diff --git a/project/src/helpers/ProfileHelper.ts b/project/src/helpers/ProfileHelper.ts index 9783e7d8e..c835a2926 100644 --- a/project/src/helpers/ProfileHelper.ts +++ b/project/src/helpers/ProfileHelper.ts @@ -533,4 +533,29 @@ export class ProfileHelper { public getQuestItemsInProfile(profile: IPmcData): IItem[] { return profile.Inventory.items.filter((item) => item.parentId === profile.Inventory.questRaidItems); } + + /** + * Return a favorites array in the format expected by the getOtherProfile call + * @param profile + * @returns An array of IItem objects representing the favorited data + */ + public getOtherProfileFavorites(profile: IPmcData): IItem[] { + let fullFavorites = []; + + for (const itemId of profile.Inventory.favoriteItems ?? []) + { + // When viewing another users profile, the client expects a full item with children, so get that + const itemAndChildren = this.itemHelper.findAndReturnChildrenAsItems(profile.Inventory.items, itemId); + if (itemAndChildren && itemAndChildren.length > 0) + { + // To get the client to actually see the items, we set the main item's parent to null, so it's treated as a root item + const clonedItems = this.cloner.clone(itemAndChildren); + clonedItems[0].parentId = null; + + fullFavorites = fullFavorites.concat(clonedItems); + } + } + + return fullFavorites; + } } diff --git a/project/src/models/eft/common/tables/IBotBase.ts b/project/src/models/eft/common/tables/IBotBase.ts index 7078b2af8..4ccb57ccf 100644 --- a/project/src/models/eft/common/tables/IBotBase.ts +++ b/project/src/models/eft/common/tables/IBotBase.ts @@ -169,7 +169,7 @@ export interface IInventory { /** Key is hideout area enum numeric as string e.g. "24", value is area _id */ hideoutAreaStashes: Record; fastPanel: Record; - favoriteItems: IItem[]; + favoriteItems: string[]; } export interface IBaseJsonSkills { diff --git a/project/src/services/BotNameService.ts b/project/src/services/BotNameService.ts index e79c458e1..a3af1b62e 100644 --- a/project/src/services/BotNameService.ts +++ b/project/src/services/BotNameService.ts @@ -61,7 +61,7 @@ export class BotNameService { const isPlayerScav = botGenerationDetails.isPlayerScav; const simulateScavName = isPlayerScav ? false : this.shouldSimulatePlayerScavName(botRole); const showTypeInNickname = this.botConfig.showTypeInNickname && !isPlayerScav; - const roleShouldBeUnique = uniqueRoles.includes(botRole.toLowerCase()); + const roleShouldBeUnique = uniqueRoles?.includes(botRole.toLowerCase()); let isUnique = true; let attempts = 0; diff --git a/project/src/services/ProfileFixerService.ts b/project/src/services/ProfileFixerService.ts index 8cf2d2cfe..5dc7e4d68 100644 --- a/project/src/services/ProfileFixerService.ts +++ b/project/src/services/ProfileFixerService.ts @@ -64,6 +64,7 @@ export class ProfileFixerService { this.removeDanglingTaskConditionCounters(pmcProfile); this.removeOrphanedQuests(pmcProfile); this.verifyQuestProductionUnlocks(pmcProfile); + this.fixFavorites(pmcProfile); if (pmcProfile.Hideout) { this.addHideoutEliteSlots(pmcProfile); @@ -341,6 +342,23 @@ export class ProfileFixerService { } } + /** + * Initial release of SPT 3.10 used an incorrect favorites structure, reformat + * the structure to the correct MongoID array structure + * @param pmcProfile + */ + protected fixFavorites(pmcProfile: IPmcData): void { + const favoritesAsAny = pmcProfile.Inventory?.favoriteItems as any; + if (favoritesAsAny) + { + const correctedFavorites = favoritesAsAny.map((favorite) => { + return favorite._id ?? favorite; + }); + + pmcProfile.Inventory.favoriteItems = correctedFavorites ?? []; + } + } + /** * If the profile has elite Hideout Managment skill, add the additional slots from globals * NOTE: This seems redundant, but we will leave it here just incase. diff --git a/project/tests/generators/BotGenerator.test.ts b/project/tests/generators/BotGenerator.test.ts index 26f43809b..75442be5c 100644 --- a/project/tests/generators/BotGenerator.test.ts +++ b/project/tests/generators/BotGenerator.test.ts @@ -1,14 +1,14 @@ import "reflect-metadata"; -import { BotGenerator } from "@spt/generators/BotGenerator"; import { IPmcData } from "@spt/models/eft/common/IPmcData"; +import { BotNameService } from "@spt/services/BotNameService"; import { container } from "tsyringe"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; describe("BotGenerator", () => { - let botGenerator: any; + let botNameService: any; beforeEach(() => { - botGenerator = container.resolve("BotGenerator"); + botNameService = container.resolve("BotNameService"); }); afterEach(() => { @@ -49,48 +49,48 @@ describe("BotGenerator", () => { // }); // }); - describe("generateBotNickname", () => { + describe("generateUniqueBotNickname", () => { it("should choose random firstname for non player scav assault bot", () => { const botJsonTemplate = { firstName: ["one", "two"], lastName: [] }; - const botGenerationDetails = { isPlayerScav: false, isPmc: true, allPmcsHaveSameNameAsPlayer: false }; + const botGenerationDetails = { isPlayerScav: false, isPmc: false, allPmcsHaveSameNameAsPlayer: false }; const botRole = "assault"; - botGenerator.botConfig.chanceAssaultScavHasPlayerScavName = 0; + botNameService.botConfig.chanceAssaultScavHasPlayerScavName = 0; const mockPlayerProfile = { Info: { Nickname: "Player Nickname", Level: 1 } }; - vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(mockPlayerProfile); + vi.spyOn(botNameService.profileHelper, "getPmcProfile").mockReturnValue(mockPlayerProfile); - const result = botGenerator.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole); + const result = botNameService.generateUniqueBotNickname(botJsonTemplate, botGenerationDetails, botRole); expect(result).toMatch(/(one|two)/); }); it("should choose random lastname for non player scav assault bot", () => { const botJsonTemplate = { firstName: [], lastName: [["one", "two"]] }; - const botGenerationDetails = { isPlayerScav: false, isPmc: true, allPmcsHaveSameNameAsPlayer: false }; + const botGenerationDetails = { isPlayerScav: false, isPmc: false, allPmcsHaveSameNameAsPlayer: false }; const botRole = "assault"; - botGenerator.botConfig.chanceAssaultScavHasPlayerScavName = 0; + botNameService.botConfig.chanceAssaultScavHasPlayerScavName = 0; const mockPlayerProfile = { Info: { Nickname: "Player Nickname", Level: 1 } }; - vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(mockPlayerProfile); + vi.spyOn(botNameService.profileHelper, "getPmcProfile").mockReturnValue(mockPlayerProfile); - const result = botGenerator.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole); + const result = botNameService.generateUniqueBotNickname(botJsonTemplate, botGenerationDetails, botRole); expect(result).toMatch(/(one|two)/); }); it("should choose random firstname and lastname for non player scav assault bot", () => { const botJsonTemplate = { firstName: ["first-one", "first-two"], lastName: [["last-one", "last-two"]] }; - const botGenerationDetails = { isPlayerScav: false, isPmc: true, allPmcsHaveSameNameAsPlayer: false }; + const botGenerationDetails = { isPlayerScav: false, isPmc: false, allPmcsHaveSameNameAsPlayer: false }; const botRole = "assault"; - botGenerator.botConfig.chanceAssaultScavHasPlayerScavName = 0; + botNameService.botConfig.chanceAssaultScavHasPlayerScavName = 0; const mockPlayerProfile = { Info: { Nickname: "Player Nickname", Level: 1 } }; - vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(mockPlayerProfile); + vi.spyOn(botNameService.profileHelper, "getPmcProfile").mockReturnValue(mockPlayerProfile); - const result = botGenerator.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole); + const result = botNameService.generateUniqueBotNickname(botJsonTemplate, botGenerationDetails, botRole); expect(result).toMatch(/first-(one|two) last-(one|two)/); }); @@ -100,12 +100,12 @@ describe("BotGenerator", () => { const botGenerationDetails = { isPlayerScav: true, isPmc: false, allPmcsHaveSameNameAsPlayer: false }; const botRole = "assault"; - botGenerator.botConfig.chanceAssaultScavHasPlayerScavName = 0; + botNameService.botConfig.chanceAssaultScavHasPlayerScavName = 0; const mockPlayerProfile = { Info: { Nickname: "Player Nickname", Level: 1 } }; - vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(mockPlayerProfile); + vi.spyOn(botNameService.profileHelper, "getPmcProfile").mockReturnValue(mockPlayerProfile); - const result = botGenerator.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole); + const result = botNameService.generateUniqueBotNickname(botJsonTemplate, botGenerationDetails, botRole); expect(result).toMatch(/(one|two)/); }); @@ -115,12 +115,12 @@ describe("BotGenerator", () => { const botGenerationDetails = { isPlayerScav: true, isPmc: false, allPmcsHaveSameNameAsPlayer: false }; const botRole = "assault"; - botGenerator.botConfig.chanceAssaultScavHasPlayerScavName = 0; + botNameService.botConfig.chanceAssaultScavHasPlayerScavName = 0; const mockPlayerProfile = { Info: { Nickname: "Player Nickname", Level: 1 } }; - vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(mockPlayerProfile); + vi.spyOn(botNameService.profileHelper, "getPmcProfile").mockReturnValue(mockPlayerProfile); - const result = botGenerator.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole); + const result = botNameService.generateUniqueBotNickname(botJsonTemplate, botGenerationDetails, botRole); expect(result).toMatch(/(one|two)/); }); @@ -130,28 +130,28 @@ describe("BotGenerator", () => { const botGenerationDetails = { isPlayerScav: true, isPmc: false, allPmcsHaveSameNameAsPlayer: false }; const botRole = "assault"; - botGenerator.botConfig.chanceAssaultScavHasPlayerScavName = 0; + botNameService.botConfig.chanceAssaultScavHasPlayerScavName = 0; const mockPlayerProfile = { Info: { Nickname: "Player Nickname", Level: 1 } }; - vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(mockPlayerProfile); + vi.spyOn(botNameService.profileHelper, "getPmcProfile").mockReturnValue(mockPlayerProfile); - const result = botGenerator.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole); + const result = botNameService.generateUniqueBotNickname(botJsonTemplate, botGenerationDetails, botRole); expect(result).toMatch(/first-(one|two) last-(one|two)/); }); it("should append bot type to end of name when showTypeInNickname option is enabled ", () => { const botJsonTemplate = { firstName: ["firstname"], lastName: ["lastname"] }; - const botGenerationDetails = { isPlayerScav: false, isPmc: true, allPmcsHaveSameNameAsPlayer: false }; + const botGenerationDetails = { isPlayerScav: false, isPmc: false, allPmcsHaveSameNameAsPlayer: false }; const botRole = "assault"; - botGenerator.botConfig.chanceAssaultScavHasPlayerScavName = 0; - botGenerator.botConfig.showTypeInNickname = true; + botNameService.botConfig.chanceAssaultScavHasPlayerScavName = 0; + botNameService.botConfig.showTypeInNickname = true; const mockPlayerProfile = { Info: { Nickname: "Player Nickname", Level: 1 } }; - vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(mockPlayerProfile); + vi.spyOn(botNameService.profileHelper, "getPmcProfile").mockReturnValue(mockPlayerProfile); - const result = botGenerator.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole); + const result = botNameService.generateUniqueBotNickname(botJsonTemplate, botGenerationDetails, botRole); expect(result).toBe("firstname lastname assault"); }); @@ -161,17 +161,18 @@ describe("BotGenerator", () => { const botGenerationDetails = { isPlayerScav: false, isPmc: true, allPmcsHaveSameNameAsPlayer: true }; const botRole = "assault"; - botGenerator.botConfig.showTypeInNickname = false; - botGenerator.pmcConfig.addPrefixToSameNamePMCAsPlayerChance = 100; + botNameService.botConfig.showTypeInNickname = false; + botNameService.pmcConfig.addPrefixToSameNamePMCAsPlayerChance = 100; const mockPlayerProfile = { Info: { Nickname: "player", Level: 1 } }; - vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(mockPlayerProfile); + vi.spyOn(botNameService.profileHelper, "getPmcProfile").mockReturnValue(mockPlayerProfile); + vi.spyOn(botNameService.botHelper, "getPmcNicknameOfMaxLength").mockReturnValue("player"); const getRandomTextThatMatchesPartialKeySpy = vi - .spyOn((botGenerator as any).localisationService, "getRandomTextThatMatchesPartialKey") + .spyOn((botNameService as any).localisationService, "getRandomTextThatMatchesPartialKey") .mockReturnValue("test"); - const result = botGenerator.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole); + const result = botNameService.generateUniqueBotNickname(botJsonTemplate, botGenerationDetails, botRole); expect(getRandomTextThatMatchesPartialKeySpy).toHaveBeenCalled(); expect(result).toBe("test player"); @@ -179,17 +180,17 @@ describe("BotGenerator", () => { it("should generate PMC name in brackets behind scav name when chanceAssaultScavHasPlayerScavName is enabled", () => { const botJsonTemplate = { firstName: ["scav"], lastName: [] }; - const botGenerationDetails = { isPlayerScav: false, isPmc: true, allPmcsHaveSameNameAsPlayer: false }; + const botGenerationDetails = { isPlayerScav: false, isPmc: false, allPmcsHaveSameNameAsPlayer: false }; const botRole = "assault"; - botGenerator.botConfig.chanceAssaultScavHasPlayerScavName = 100; - botGenerator.databaseService.getBots().types.usec.firstName = ["player"]; - botGenerator.databaseService.getBots().types.bear.firstName = []; + botNameService.botConfig.chanceAssaultScavHasPlayerScavName = 100; + botNameService.databaseService.getBots().types.usec.firstName = ["player"]; + botNameService.databaseService.getBots().types.bear.firstName = []; const mockPlayerProfile = { Info: { Nickname: "Player", Level: 1 } }; - vi.spyOn(botGenerator.profileHelper, "getPmcProfile").mockReturnValue(mockPlayerProfile); + vi.spyOn(botNameService.profileHelper, "getPmcProfile").mockReturnValue(mockPlayerProfile); - const result = botGenerator.generateBotNickname(botJsonTemplate, botGenerationDetails, botRole); + const result = botNameService.generateUniqueBotNickname(botJsonTemplate, botGenerationDetails, botRole); expect(result).toBe("scav (player)"); }); }); diff --git a/project/tests/helpers/BotDifficultyHelper.test.ts b/project/tests/helpers/BotDifficultyHelper.test.ts index 459d369ba..cd1709708 100644 --- a/project/tests/helpers/BotDifficultyHelper.test.ts +++ b/project/tests/helpers/BotDifficultyHelper.test.ts @@ -42,7 +42,7 @@ describe("BotHelper", () => { }); const warningLogSpy = vi.spyOn(botDifficultyHelper.logger, "warning"); - const result = botDifficultyHelper.getBotDifficultySettings("INVALID_TYPE", "normal"); + const result = botDifficultyHelper.getBotDifficultySettings("INVALID_TYPE", "normal", { types: {} }); expect(result).toBe("test"); expect(warningLogSpy).toHaveBeenCalledTimes(1); });