diff --git a/components/candle/challengeService.ts b/components/candle/challengeService.ts index 7d6051dc..86e4e4f9 100644 --- a/components/candle/challengeService.ts +++ b/components/candle/challengeService.ts @@ -233,6 +233,10 @@ export abstract class ChallengeRegistry { return this.challenges[gameVersion].get(challengeId) } + getChallengeIds(gameVersion: GameVersion): string[] { + return Array.from(this.challenges[gameVersion].keys()) + } + removeChallenge(challengeId: string, gameVersion: GameVersion): boolean { const challenge = this.challenges[gameVersion].get(challengeId) if (!challenge) return false diff --git a/components/candle/progressionService.ts b/components/candle/progressionService.ts index e75d7006..c613c839 100644 --- a/components/candle/progressionService.ts +++ b/components/candle/progressionService.ts @@ -240,22 +240,14 @@ export class ProgressionService { // Update the SubLocation data const profileData = userProfile.Extensions.progression.PlayerProfileXP - let foundSubLocation = profileData.Sublocations.find( - (e) => e.Location === parentLocationId, - ) - - if (!foundSubLocation) { - foundSubLocation = { - Location: parentLocationId, - Xp: 0, - ActionXp: 0, - } - - profileData.Sublocations.push(foundSubLocation) + profileData.Sublocations[contract.Metadata.Location] ??= { + Xp: 0, + ActionXp: 0, } - foundSubLocation.Xp += masteryXp - foundSubLocation.ActionXp += actionXp + profileData.Sublocations[contract.Metadata.Location].Xp += masteryXp + profileData.Sublocations[contract.Metadata.Location].ActionXp += + actionXp return true } diff --git a/components/configSwizzleManager.ts b/components/configSwizzleManager.ts index 6ca868eb..68ba0bf5 100644 --- a/components/configSwizzleManager.ts +++ b/components/configSwizzleManager.ts @@ -108,7 +108,7 @@ import MultiplayerPresets from "../static/MultiplayerPresets.json" import LobbySlimTemplate from "../static/LobbySlimTemplate.json" import MasteryDataForLocationTemplate from "../static/MasteryDataForLocationTemplate.json" import LegacyMasteryLocationTemplate from "../static/LegacyMasteryLocationTemplate.json" -import DefaultCpdConfig from "../static/DefaultCpdConfig.json" +import DefaultCpdConfigs from "../static/DefaultCpdConfigs.json" import EvergreenGameChangerProperties from "../static/EvergreenGameChangerProperties.json" import AreaMap from "../static/AreaMap.json" import ArcadePageTemplate from "../static/ArcadePageTemplate.json" @@ -217,7 +217,7 @@ const configs = { LobbySlimTemplate, MasteryDataForLocationTemplate, LegacyMasteryLocationTemplate, - DefaultCpdConfig, + DefaultCpdConfigs, EvergreenGameChangerProperties, AreaMap, ArcadePageTemplate, diff --git a/components/evergreen.ts b/components/evergreen.ts index 8134efd2..f11340a7 100644 --- a/components/evergreen.ts +++ b/components/evergreen.ts @@ -22,6 +22,10 @@ import { ContractProgressionData } from "./types/types" import { getFlag } from "./flags" import { EVERGREEN_LEVEL_INFO } from "./utils" +type DefaultCpdConfigs = { + [cpdId: string]: ContractProgressionData +} + export function setCpd( data: ContractProgressionData, uID: string, @@ -42,15 +46,22 @@ export function getCpd(uID: string, cpdID: string): ContractProgressionData { if (!Object.keys(userData.Extensions.CPD).includes(cpdID)) { const defaultCPD = getConfig( - "DefaultCpdConfig", + "DefaultCpdConfigs", false, - ) as ContractProgressionData + ) as DefaultCpdConfigs - setCpd(defaultCPD, uID, cpdID) + setCpd( + Object.keys(defaultCPD).includes(cpdID) ? defaultCPD[cpdID] : {}, + uID, + cpdID, + ) } // NOTE: Override the EvergreenLevel with the latest Mastery Level - if (getFlag("gameplayUnlockAllFreelancerMasteries")) { + if ( + getFlag("gameplayUnlockAllFreelancerMasteries") && + cpdID === "f8ec92c2-4fa2-471e-ae08-545480c746ee" + ) { userData.Extensions.CPD[cpdID]["EvergreenLevel"] = EVERGREEN_LEVEL_INFO.length } diff --git a/components/menuData.ts b/components/menuData.ts index 00624a82..fa0f9f9c 100644 --- a/components/menuData.ts +++ b/components/menuData.ts @@ -95,6 +95,19 @@ import { getPlayerProfileData } from "./menus/playerProfile" const menuDataRouter = Router() +// We make this lookup table to quickly get it, there's no other quick way for it. +export const SNIPER_UNLOCK_TO_LOCATION: Record = { + FIREARMS_SC_HERO_SNIPER_HM: "LOCATION_PARENT_AUSTRIA", + FIREARMS_SC_HERO_SNIPER_KNIGHT: "LOCATION_PARENT_AUSTRIA", + FIREARMS_SC_HERO_SNIPER_STONE: "LOCATION_PARENT_AUSTRIA", + FIREARMS_SC_SEAGULL_HM: "LOCATION_PARENT_SALTY", + FIREARMS_SC_SEAGULL_KNIGHT: "LOCATION_PARENT_SALTY", + FIREARMS_SC_SEAGULL_STONE: "LOCATION_PARENT_SALTY", + FIREARMS_SC_FALCON_HM: "LOCATION_PARENT_CAGED", + FIREARMS_SC_FALCON_KNIGHT: "LOCATION_PARENT_CAGED", + FIREARMS_SC_FALCON_STONE: "LOCATION_PARENT_CAGED", +} + // /profiles/page/ menuDataRouter.get( @@ -1400,25 +1413,12 @@ menuDataRouter.get( "/GetMasteryCompletionDataForUnlockable", // @ts-expect-error Has jwt props. (req: RequestWithJwt, res) => { - // We make this lookup table to quickly get it, there's no other quick way for it. - const unlockToLoc: Record = { - FIREARMS_SC_HERO_SNIPER_HM: "LOCATION_PARENT_AUSTRIA", - FIREARMS_SC_HERO_SNIPER_KNIGHT: "LOCATION_PARENT_AUSTRIA", - FIREARMS_SC_HERO_SNIPER_STONE: "LOCATION_PARENT_AUSTRIA", - FIREARMS_SC_SEAGULL_HM: "LOCATION_PARENT_SALTY", - FIREARMS_SC_SEAGULL_KNIGHT: "LOCATION_PARENT_SALTY", - FIREARMS_SC_SEAGULL_STONE: "LOCATION_PARENT_SALTY", - FIREARMS_SC_FALCON_HM: "LOCATION_PARENT_CAGED", - FIREARMS_SC_FALCON_KNIGHT: "LOCATION_PARENT_CAGED", - FIREARMS_SC_FALCON_STONE: "LOCATION_PARENT_CAGED", - } - res.json({ template: null, data: { CompletionData: controller.masteryService.getLocationCompletion( - unlockToLoc[req.query.unlockableId], - unlockToLoc[req.query.unlockableId], + SNIPER_UNLOCK_TO_LOCATION[req.query.unlockableId], + SNIPER_UNLOCK_TO_LOCATION[req.query.unlockableId], req.gameVersion, req.jwt.unique_name, "sniper", diff --git a/components/menus/playerProfile.ts b/components/menus/playerProfile.ts index dfe0ea5b..d3073ef8 100644 --- a/components/menus/playerProfile.ts +++ b/components/menus/playerProfile.ts @@ -30,6 +30,13 @@ import { getDestinationCompletion } from "./destinations" import { getUserData } from "../databaseHandler" import { isSniperLocation } from "../utils" +type XpData = { + [location: string]: { + Xp: number + ActionXp: number + } +} + export function getPlayerProfileData( gameVersion: GameVersion, userId: string, @@ -47,7 +54,27 @@ export function getPlayerProfileData( playerProfilePage.SubLocationData = [] + const userProfile = getUserData(userId, gameVersion) + const subLocationMap = + userProfile.Extensions.progression.PlayerProfileXP.Sublocations + const xpData: XpData = {} + for (const subLocationKey in locationData.children) { + const subLocation = locationData.children[subLocationKey] + const parentLocation = + locationData.parents[subLocation.Properties.ParentLocation || ""] + + // We find all sublocations and add their XP for the season data + const subLocationData = subLocationMap[subLocationKey] + + xpData[parentLocation.Id] ??= { + Xp: 0, + ActionXp: 0, + } + + xpData[parentLocation.Id].Xp += subLocationData?.Xp ?? 0 + xpData[parentLocation.Id].ActionXp += subLocationData?.ActionXp ?? 0 + // Ewww... if ( subLocationKey === "LOCATION_ICA_FACILITY_ARRIVAL" || @@ -56,10 +83,6 @@ export function getPlayerProfileData( continue } - const subLocation = locationData.children[subLocationKey] - const parentLocation = - locationData.parents[subLocation.Properties.ParentLocation || ""] - const completionData = generateCompletionData( subLocation.Id, userId, @@ -109,24 +132,17 @@ export function getPlayerProfileData( }) } - const userProfile = getUserData(userId, gameVersion) playerProfilePage.PlayerProfileXp.Total = userProfile.Extensions.progression.PlayerProfileXP.Total playerProfilePage.PlayerProfileXp.Level = userProfile.Extensions.progression.PlayerProfileXP.ProfileLevel - const subLocationMap = new Map( - userProfile.Extensions.progression.PlayerProfileXP.Sublocations.map( - (obj) => [obj.Location, obj], - ), - ) - for (const season of playerProfilePage.PlayerProfileXp.Seasons) { for (const location of season.Locations) { - const subLocationData = subLocationMap.get(location.LocationId) + const locationData = xpData[location.LocationId] - location.Xp = subLocationData?.Xp || 0 - location.ActionXp = subLocationData?.ActionXp || 0 + location.Xp = locationData?.Xp || 0 + location.ActionXp = locationData?.ActionXp || 0 if ( location.LocationProgression && diff --git a/components/types/types.ts b/components/types/types.ts index 69e785a1..1e02f49e 100644 --- a/components/types/types.ts +++ b/components/types/types.ts @@ -511,10 +511,11 @@ export type UserProfile = { */ Total: number Sublocations: { - Location: string - Xp: number - ActionXp: number - }[] + [location: string]: { + Xp: number + ActionXp: number + } + } } /** * If the mastery location has subpackages and not drops, it will @@ -560,6 +561,7 @@ export type UserProfile = { [opportunityId: RepositoryId]: boolean } CPD: CPDStore + LastOfficialSync: Date | string | null } ETag: string | null Gamertag: string @@ -1572,3 +1574,9 @@ export type SMFLastDeploy = { peacockPlugins?: string[] } } + +export type OfficialSublocation = { + Location: string + Xp: number + ActionXp: number +} diff --git a/components/utils.ts b/components/utils.ts index a9474464..88b7ebc3 100644 --- a/components/utils.ts +++ b/components/utils.ts @@ -21,6 +21,7 @@ import type { NextFunction, Response } from "express" import type { GameVersion, MissionManifestObjective, + OfficialSublocation, PeacockLocationsData, RepositoryId, RequestWithJwt, @@ -66,7 +67,7 @@ export const contractCreationTutorialId = "d7e2607c-6916-48e2-9588-976c7d8998bb" * * See docs/USER_PROFILES.md for more. */ -export const LATEST_PROFILE_VERSION = 1 +export const LATEST_PROFILE_VERSION = 2 export async function checkForUpdates(): Promise { if (getFlag("updateChecking") === false) { @@ -249,7 +250,6 @@ export function clampValue(value: number, min: number, max: number) { * * @param profile The user profile to update * @param gameVersion The game version - * @returns The updated user profile. */ function updateUserProfile( profile: UserProfile, @@ -266,6 +266,29 @@ function updateUserProfile( case LATEST_PROFILE_VERSION: // This profile updated to the latest version, we're done. return + case 1: { + /* ////// VERSION 2 ////// */ + + const sublocations = profile.Extensions.progression.PlayerProfileXP + .Sublocations as unknown as OfficialSublocation[] + + profile.Extensions.progression.PlayerProfileXP.Sublocations = + Object.fromEntries( + sublocations.map((value) => [ + value.Location, + { + Xp: value.Xp, + ActionXp: value.ActionXp, + }, + ]), + ) + + profile.Extensions.LastOfficialSync = null + + profile.Version = 2 + + return updateUserProfile(profile, gameVersion) + } default: { // Check that the profile version is indeed undefined. If it isn't, // we've forgotten to add a version to the switch. @@ -726,3 +749,26 @@ export function isSuit(repoId: string): boolean { ? suitsToTypeMap[repoId] !== "disguise" : false } + +type SublocationMap = { + [parentId: string]: string[] +} + +export function getSublocations(gameVersion: GameVersion): SublocationMap { + const sublocations: SublocationMap = {} + const locations = getVersionedConfig( + "LocationsData", + gameVersion, + false, + ) + + for (const child of Object.values(locations.children)) { + if (!child.Properties.ParentLocation) continue + + sublocations[child.Properties.ParentLocation] ??= [] + + sublocations[child.Properties.ParentLocation].push(child.Id) + } + + return sublocations +} diff --git a/components/webFeatures.ts b/components/webFeatures.ts index 2556b99a..6a222a42 100644 --- a/components/webFeatures.ts +++ b/components/webFeatures.ts @@ -19,12 +19,43 @@ import { NextFunction, Request, Response, Router } from "express" import { getConfig } from "./configSwizzleManager" import { readdir, readFile } from "fs/promises" -import { GameVersion, UserProfile } from "./types/types" +import { + ChallengeProgressionData, + GameVersion, + HitsCategoryCategory, + OfficialSublocation, + ProgressionData, + UserProfile, +} from "./types/types" import { join } from "path" -import { uuidRegex, versions } from "./utils" +import { + getRemoteService, + getSublocations, + isSniperLocation, + levelForXp, + uuidRegex, + versions, +} from "./utils" import { getUserData, loadUserData, writeUserData } from "./databaseHandler" import { controller } from "./controller" import { log, LogLevel } from "./loggingInterop" +import { OfficialServerAuth, userAuths } from "./officialServerAuth" +import { AxiosError } from "axios" +import { SNIPER_UNLOCK_TO_LOCATION } from "./menuData" + +type OfficialProfileResponse = UserProfile & { + Extensions: { + progression: { + Unlockables: { + [unlockableId: string]: ProgressionData + } + } + } +} + +type SubPackageData = { + [id: string]: ProgressionData +} const webFeaturesRouter = Router() @@ -108,9 +139,21 @@ webFeaturesRouter.get("/local-users", async (req: CommonRequest, res) => { (name) => name !== "lop.json", ) - const result = [] + /** + * Sync this type with `webui/src/utils`! + */ + type BasicUser = Readonly<{ + id: string + name: string + platform: string + lastOfficialSync: string | null + }> + + const result: BasicUser[] = [] for (const file of files) { + if (file === "lop.json") continue + const read = JSON.parse( (await readFile(join(dir, file))).toString(), ) as UserProfile @@ -119,6 +162,8 @@ webFeaturesRouter.get("/local-users", async (req: CommonRequest, res) => { id: read.Id, name: read.Gamertag, platform: read.EpicId ? "Epic" : "Steam", + lastOfficialSync: + read.Extensions.LastOfficialSync?.toString() || null, }) } @@ -217,4 +262,339 @@ webFeaturesRouter.get( }, ) +type EscalationData = { + PeacockEscalations: { + [escalationId: string]: number + } + PeacockCompletedEscalations: string[] +} + +type OfficialHitsCategory = { + data: HitsCategoryCategory +} + +async function getHitsCategory( + auth: OfficialServerAuth, + remoteService: string, + category: string, + page: number, +): Promise<[results: EscalationData, hasMore: boolean]> { + const data: EscalationData = { + PeacockEscalations: {}, + PeacockCompletedEscalations: [], + } + + const hits = await auth._useService( + `https://${remoteService}.hitman.io/profiles/page/HitsCategory?page=${page}&type=${category}&mode=dataonly`, + true, + ) + + for (const hit of hits.data.data.Data.Hits) { + data.PeacockEscalations[hit.Id] = + hit.UserCentricContract.Data.EscalationCompletedLevels! + 1 + + if (hit.UserCentricContract.Data.EscalationCompleted) + data.PeacockCompletedEscalations.push(hit.Id) + } + + return [data, hits.data.data.Data.HasMore] +} + +async function getAllHitsCategory( + auth: OfficialServerAuth, + remoteService: string, + category: string, +): Promise { + const data: EscalationData = { + PeacockEscalations: {}, + PeacockCompletedEscalations: [], + } + + let page = 0 + let hasMore = true + + while (hasMore) { + const [results, more] = await getHitsCategory( + auth, + remoteService, + category, + page, + ) + + data.PeacockEscalations = { + ...data.PeacockEscalations, + ...results.PeacockEscalations, + } + + data.PeacockCompletedEscalations = [ + ...data.PeacockCompletedEscalations, + ...results.PeacockCompletedEscalations, + ] + + page++ + hasMore = more + } + + return data +} + +webFeaturesRouter.post( + "/sync-progress", + commonValidationMiddleware, + async (req: CommonRequest, res) => { + const remoteService = getRemoteService(req.query.gv) + const auth = userAuths.get(req.query.user) + + if (!auth) { + formErrorMessage( + res, + "Failed to get official authentication data. Please connect to Peacock first.", + ) + return + } + + const userdata = getUserData(req.query.user, req.query.gv) + + try { + // Challenge Progression + const challengeProgression = await auth._useService< + ChallengeProgressionData[] + >( + `https://${remoteService}.hitman.io/authentication/api/userchannel/ChallengesService/GetProgression`, + false, + { + profileid: req.query.user, + challengeids: controller.challengeService.getChallengeIds( + req.query.gv, + ), + }, + ) + + userdata.Extensions.ChallengeProgression = Object.fromEntries( + challengeProgression.data.map((data) => { + return [ + data.ChallengeId, + { + Ticked: data.Completed, + Completed: data.Completed, + CurrentState: + (data.State["CurrentState"] as string) ?? + "Start", + State: data.State, + }, + ] + }), + ) + + // Profile Progression + const exts = await auth._useService( + `https://${remoteService}.hitman.io/authentication/api/userchannel/ProfileService/GetProfile`, + false, + { + id: req.query.user, + extensions: [ + "achievements", + "friends", + "gameclient", + "gamepersistentdata", + "opportunityprogression", + "progression", + "defaultloadout", + ], + }, + ) + + if (req.query.gv !== "h1") { + const sublocations = exts.data.Extensions.progression + .PlayerProfileXP + .Sublocations as unknown as OfficialSublocation[] + + userdata.Extensions.progression.PlayerProfileXP = { + ...userdata.Extensions.progression.PlayerProfileXP, + Total: exts.data.Extensions.progression.PlayerProfileXP + .Total, + ProfileLevel: levelForXp( + exts.data.Extensions.progression.PlayerProfileXP.Total, + ), + Sublocations: Object.fromEntries( + sublocations.map((value) => [ + value.Location, + { + Xp: value.Xp, + ActionXp: value.ActionXp, + }, + ]), + ), + } + + userdata.Extensions.opportunityprogression = Object.fromEntries( + Object.keys( + exts.data.Extensions.opportunityprogression, + ).map((value) => [value, true]), + ) + + for (const [unlockId, data] of Object.entries( + exts.data.Extensions.progression.Unlockables, + )) { + const unlockableId = unlockId.toUpperCase() + + if (!(unlockableId in SNIPER_UNLOCK_TO_LOCATION)) continue + ;( + userdata.Extensions.progression.Locations[ + SNIPER_UNLOCK_TO_LOCATION[unlockableId] + ] as SubPackageData + )[unlockableId] = { + Xp: data.Xp, + Level: data.Level, + PreviouslySeenXp: data.PreviouslySeenXp, + } + } + } + + userdata.Extensions.gamepersistentdata = + exts.data.Extensions.gamepersistentdata + + const sublocations = getSublocations(req.query.gv) + userdata.Extensions.defaultloadout ??= {} + + if (exts.data.Extensions.defaultloadout) { + for (const [parent, loadout] of Object.entries( + exts.data.Extensions.defaultloadout, + )) { + for (const child of sublocations[parent]) { + userdata.Extensions.defaultloadout[child] = loadout + } + } + } + + userdata.Extensions.achievements = exts.data.Extensions.achievements + + for (const [locId, data] of Object.entries( + exts.data.Extensions.progression.Locations, + )) { + const location = ( + locId.startsWith("location_parent") + ? locId + : locId.replace("location_", "location_parent_") + ).toUpperCase() + + if (isSniperLocation(location)) continue + + if (req.query.gv === "h1") { + const parent = location.endsWith("PRO1") + ? location.substring(0, location.length - 5) + : location + + const packageId: string = location.endsWith("PRO1") + ? "pro1" + : "normal" + + ;( + userdata.Extensions.progression.Locations[ + parent + ] as SubPackageData + )[packageId] = { + Xp: data.Xp as number, + Level: data.Level as number, + PreviouslySeenXp: data.Xp as number, + } + } else { + userdata.Extensions.progression.Locations[location] = { + Xp: data.Xp as number, + Level: data.Level as number, + PreviouslySeenXp: data.PreviouslySeenXp as number, + } + } + } + + // Escalation & Arcade Progression + const escalations = await getAllHitsCategory( + auth, + remoteService!, + "ContractAttack", + ) + + const arcade = + req.query.gv === "h3" + ? await getAllHitsCategory(auth, remoteService!, "Arcade") + : { + PeacockEscalations: {}, + PeacockCompletedEscalations: [], + } + + userdata.Extensions.PeacockEscalations = { + ...userdata.Extensions.PeacockEscalations, + ...escalations.PeacockEscalations, + ...arcade.PeacockEscalations, + } + + userdata.Extensions.PeacockCompletedEscalations = [ + ...userdata.Extensions.PeacockCompletedEscalations, + ...escalations.PeacockCompletedEscalations, + ...arcade.PeacockCompletedEscalations, + ] + + for (const id of userdata.Extensions.PeacockCompletedEscalations) { + userdata.Extensions.PeacockPlayedContracts[id] = { + LastPlayedAt: new Date().getTime(), + Completed: true, + IsEscalation: true, + } + } + + // Freelancer Progression + // TODO: Try and see if there is a less intensive way to do this + // GetForPlay2 is quite intensive on IOI's side as it starts a session + if (req.query.gv === "h3") { + await auth._useService( + `https://${remoteService}.hitman.io/authentication/api/configuration/Init?configName=pc-prod&lockedContentDisabled=false&isFreePrologueUser=false&isIntroPackUser=false&isFullExperienceUser=true`, + true, + ) + + const freelancerSession = await auth._useService<{ + ContractProgressionData: Record< + string, + string | number | boolean + > + }>( + `https://${remoteService}.hitman.io/authentication/api/userchannel/ContractsService/GetForPlay2`, + false, + { + id: "f8ec92c2-4fa2-471e-ae08-545480c746ee", + locationId: "", + extraGameChangerIds: [], + difficultyLevel: 0, + }, + ) + + userdata.Extensions.CPD[ + "f8ec92c2-4fa2-471e-ae08-545480c746ee" + ] = freelancerSession.data.ContractProgressionData + } + + userdata.Extensions.LastOfficialSync = new Date().toISOString() + + writeUserData(req.query.user, req.query.gv) + } catch (error) { + if (error instanceof AxiosError) { + formErrorMessage( + res, + `Failed to sync official data: got ${error.response?.status} ${error.response?.statusText}.`, + ) + return + } else { + formErrorMessage( + res, + `Failed to sync official data: got ${JSON.stringify(error)}.`, + ) + return + } + } + + res.json({ + success: true, + }) + }, +) + export { webFeaturesRouter } diff --git a/docs/USER_PROFILES.md b/docs/USER_PROFILES.md index c17c6058..37a1738c 100644 --- a/docs/USER_PROFILES.md +++ b/docs/USER_PROFILES.md @@ -23,3 +23,10 @@ Version 1 introduced the profile versioning system. The changes are: - Sniper locations now have their unlockables contained inside the location. - Unused properties were removed from locations in `u.Extensions.progression.Locations`. - `u.Extensions.progression.Unlockables` has been removed. + +## Version 2 + +Version 2 introduced support for syncing official progress to Peacock. The changes are: + +- `u.Extensions.LastOfficialSync` has been added which records a date and time of the last official sync. +- `u.Extensions.progression.PlayerProfileXp.Sublocations` has been changed to an object. diff --git a/static/DefaultCpdConfig.json b/static/DefaultCpdConfig.json deleted file mode 100644 index dcf42af5..00000000 --- a/static/DefaultCpdConfig.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "EvergreenLevel": 1.0 -} diff --git a/static/DefaultCpdConfigs.json b/static/DefaultCpdConfigs.json new file mode 100644 index 00000000..18b3c6d3 --- /dev/null +++ b/static/DefaultCpdConfigs.json @@ -0,0 +1,5 @@ +{ + "f8ec92c2-4fa2-471e-ae08-545480c746ee": { + "EvergreenLevel": 1.0 + } +} diff --git a/static/LegacyUserDefault.json b/static/LegacyUserDefault.json index a17cf098..57dae07b 100644 --- a/static/LegacyUserDefault.json +++ b/static/LegacyUserDefault.json @@ -53,78 +53,7 @@ }, "PlayerProfileXP": { "Total": 0, - "Sublocations": [ - { - "Location": "LOCATION_ICA_FACILITY", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_PARIS", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_COASTALTOWN", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_COASTALTOWN_MOVIESET", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_COASTALTOWN_NIGHT", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_COASTALTOWN_EBOLA", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_MARRAKECH", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_MARRAKECH_NIGHT", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_BANGKOK", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_BANGKOK_ZIKA", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_COLORADO", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_COLORADO_RABIES", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_HOKKAIDO", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_HOKKAIDO_FLU", - "Xp": 0, - "ActionXp": 0 - } - ], + "Sublocations": {}, "PreviouslySeenTotal": 0, "ProfileLevel": 0, "PreviouslySeenStaging": null @@ -162,7 +91,8 @@ }, "opportunityprogression": {}, "inventory": [], - "friends": [] + "friends": [], + "LastOfficialSync": null }, "ETag": null, "Gamertag": null, @@ -173,5 +103,5 @@ "XboxLiveId": null, "PSNAccountId": null, "PSNOnlineId": null, - "Version": 1 + "Version": 2 } diff --git a/static/UserDefault.json b/static/UserDefault.json index 2947236a..a5000c2f 100644 --- a/static/UserDefault.json +++ b/static/UserDefault.json @@ -184,168 +184,7 @@ }, "PlayerProfileXP": { "Total": 0, - "Sublocations": [ - { - "Location": "LOCATION_ICA_FACILITY", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_PARIS", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_COASTALTOWN", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_COASTALTOWN_MOVIESET", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_COASTALTOWN_NIGHT", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_COASTALTOWN_EBOLA", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_MARRAKECH", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_MARRAKECH_NIGHT", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_BANGKOK", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_BANGKOK_ZIKA", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_COLORADO", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_COLORADO_RABIES", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_HOKKAIDO", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_HOKKAIDO_FLU", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_NEWZEALAND", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_MIAMI", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_MIAMI_COTTONMOUTH", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_COLOMBIA", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_MUMBAI", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_NORTHAMERICA", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_NORTHSEA", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_GREEDY_RACCOON", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_OPULENT_STINGRAY", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_AUSTRIA", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_SALTY_SEAGULL", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_CAGED_FALCON", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_GOLDEN_GECKO", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_ANCESTRAL_BULLDOG", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_EDGY_FOX", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_WET_RAT", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_ELEGANT_LLAMA", - "Xp": 0, - "ActionXp": 0 - }, - { - "Location": "LOCATION_TRAPPED_WOLVERINE", - "Xp": 0, - "ActionXp": 0 - } - ], + "Sublocations": {}, "PreviouslySeenTotal": 0, "ProfileLevel": 0, "PreviouslySeenStaging": null @@ -384,7 +223,8 @@ }, "opportunityprogression": {}, "friends": [], - "CPD": {} + "CPD": {}, + "LastOfficialSync": null }, "ETag": null, "Gamertag": null, @@ -395,5 +235,5 @@ "XboxLiveId": null, "PSNAccountId": null, "PSNOnlineId": null, - "Version": 1 + "Version": 2 } diff --git a/webui/package.json b/webui/package.json index ab9bc835..c785448c 100644 --- a/webui/package.json +++ b/webui/package.json @@ -4,6 +4,7 @@ "private": true, "type": "module", "dependencies": { + "@radix-ui/react-alert-dialog": "^1.0.5", "axios": "^1.6.8", "clsx": "^2.1.0", "immer": "^10.0.4", diff --git a/webui/src/App.css b/webui/src/App.css index cb418c99..963a31b8 100644 --- a/webui/src/App.css +++ b/webui/src/App.css @@ -114,3 +114,33 @@ body { .pagination-nav__item:first-child .pagination-nav__label::before { content: "" !important; } + +/* RADIX DIALOG */ +.AlertDialogOverlay { + position: fixed; +} + +.AlertDialogContent { + background: white; + border: solid 2px black; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 90vw; + max-width: 640px; + max-height: 85vh; + padding: 25px; +} + +.AlertDialogTitle { + margin: 0 0 1rem; + font-size: 20px; + font-weight: 500; +} + +.AlertDialogDescription { + margin-bottom: 20px; + font-size: 16px; + line-height: 1.5; +} diff --git a/webui/src/App.tsx b/webui/src/App.tsx index 5e348a6c..c0cde6bc 100644 --- a/webui/src/App.tsx +++ b/webui/src/App.tsx @@ -21,6 +21,7 @@ import "infima/dist/css/default/default.css" import "./App.css" import { createBrowserRouter, RouterProvider } from "react-router-dom" import { DevToolsPage } from "./pages/DevToolsPage" +import { TransferPage } from "./pages/TransferPage" import { LoadoutPage } from "./pages/LoadoutPage" import { RootPage } from "./pages/RootPage" import { Home } from "./pages/Home" @@ -41,6 +42,10 @@ const router = createBrowserRouter([ element: , } : null!, + { + path: "ui/transfer", + element: , + }, { path: "ui/loadouts", element: , diff --git a/webui/src/LoadoutPreview.tsx b/webui/src/LoadoutPreview.tsx index 2be5e461..00b65854 100644 --- a/webui/src/LoadoutPreview.tsx +++ b/webui/src/LoadoutPreview.tsx @@ -26,7 +26,7 @@ export interface LoadoutPreviewProps { function adjustCase(input: string): string { return input .split(" ") - .map((i) => `${i[0].toUpperCase()}${i.substr(1).toLowerCase()}`) + .map((i) => `${i[0].toUpperCase()}${i.substring(1).toLowerCase()}`) .join(" ") } diff --git a/webui/src/LoadoutsForGameVersion.tsx b/webui/src/LoadoutsForGameVersion.tsx index 257d2a49..73e796c9 100644 --- a/webui/src/LoadoutsForGameVersion.tsx +++ b/webui/src/LoadoutsForGameVersion.tsx @@ -52,7 +52,6 @@ export function LoadoutsForGameVersion({ } // note: putting these 2 consts together make intellij and prettier conflict each other - // @ts-expect-error fake news const theGameVer = data[`h${gameVersion}`] as LoadoutsGameVersion const loadoutsForVersion: Loadout[] = theGameVer.loadouts @@ -62,7 +61,6 @@ export function LoadoutsForGameVersion({