1
mirror of https://github.com/thepeacockproject/Peacock synced 2025-03-27 11:12:44 +01:00

feat: official -> peacock progression transfer ()

* Support multiple CPD configs

* Initial work on progression transfer - challenges

* Add transfer of profile data

* Change how ActionXp is stored

We make the UserDefault contain an empty object, saves having to add all the locations. They're only added when XP is actually gained in that sublocation, this mimics official behaviour.

* Fix player profile data

* Sync escalation and arcade progress

* Sync freelancer CPD data

* Fix linting

* Don't override previous challenge progression

* Bump default profile version

* Overriding challenge progression is inevitable

* Remove pointless cast

* Only get arcade and freelancer data in H3

* Remove debug print

* Support 2016 progression transfer

* Transfer default loadouts

* Don't clone LocationsData for getSublocations

Co-authored-by: Reece Dunham <me@rdil.rocks>
This commit is contained in:
Anthony Fuller 2024-04-20 01:45:44 +01:00 committed by GitHub
parent 6db75c3772
commit beb610c340
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1333 additions and 314 deletions

@ -233,6 +233,10 @@ export abstract class ChallengeRegistry {
return this.challenges[gameVersion].get(challengeId) return this.challenges[gameVersion].get(challengeId)
} }
getChallengeIds(gameVersion: GameVersion): string[] {
return Array.from(this.challenges[gameVersion].keys())
}
removeChallenge(challengeId: string, gameVersion: GameVersion): boolean { removeChallenge(challengeId: string, gameVersion: GameVersion): boolean {
const challenge = this.challenges[gameVersion].get(challengeId) const challenge = this.challenges[gameVersion].get(challengeId)
if (!challenge) return false if (!challenge) return false

@ -240,22 +240,14 @@ export class ProgressionService {
// Update the SubLocation data // Update the SubLocation data
const profileData = userProfile.Extensions.progression.PlayerProfileXP const profileData = userProfile.Extensions.progression.PlayerProfileXP
let foundSubLocation = profileData.Sublocations.find( profileData.Sublocations[contract.Metadata.Location] ??= {
(e) => e.Location === parentLocationId,
)
if (!foundSubLocation) {
foundSubLocation = {
Location: parentLocationId,
Xp: 0, Xp: 0,
ActionXp: 0, ActionXp: 0,
} }
profileData.Sublocations.push(foundSubLocation) profileData.Sublocations[contract.Metadata.Location].Xp += masteryXp
} profileData.Sublocations[contract.Metadata.Location].ActionXp +=
actionXp
foundSubLocation.Xp += masteryXp
foundSubLocation.ActionXp += actionXp
return true return true
} }

@ -108,7 +108,7 @@ import MultiplayerPresets from "../static/MultiplayerPresets.json"
import LobbySlimTemplate from "../static/LobbySlimTemplate.json" import LobbySlimTemplate from "../static/LobbySlimTemplate.json"
import MasteryDataForLocationTemplate from "../static/MasteryDataForLocationTemplate.json" import MasteryDataForLocationTemplate from "../static/MasteryDataForLocationTemplate.json"
import LegacyMasteryLocationTemplate from "../static/LegacyMasteryLocationTemplate.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 EvergreenGameChangerProperties from "../static/EvergreenGameChangerProperties.json"
import AreaMap from "../static/AreaMap.json" import AreaMap from "../static/AreaMap.json"
import ArcadePageTemplate from "../static/ArcadePageTemplate.json" import ArcadePageTemplate from "../static/ArcadePageTemplate.json"
@ -217,7 +217,7 @@ const configs = {
LobbySlimTemplate, LobbySlimTemplate,
MasteryDataForLocationTemplate, MasteryDataForLocationTemplate,
LegacyMasteryLocationTemplate, LegacyMasteryLocationTemplate,
DefaultCpdConfig, DefaultCpdConfigs,
EvergreenGameChangerProperties, EvergreenGameChangerProperties,
AreaMap, AreaMap,
ArcadePageTemplate, ArcadePageTemplate,

@ -22,6 +22,10 @@ import { ContractProgressionData } from "./types/types"
import { getFlag } from "./flags" import { getFlag } from "./flags"
import { EVERGREEN_LEVEL_INFO } from "./utils" import { EVERGREEN_LEVEL_INFO } from "./utils"
type DefaultCpdConfigs = {
[cpdId: string]: ContractProgressionData
}
export function setCpd( export function setCpd(
data: ContractProgressionData, data: ContractProgressionData,
uID: string, uID: string,
@ -42,15 +46,22 @@ export function getCpd(uID: string, cpdID: string): ContractProgressionData {
if (!Object.keys(userData.Extensions.CPD).includes(cpdID)) { if (!Object.keys(userData.Extensions.CPD).includes(cpdID)) {
const defaultCPD = getConfig( const defaultCPD = getConfig(
"DefaultCpdConfig", "DefaultCpdConfigs",
false, 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 // 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"] = userData.Extensions.CPD[cpdID]["EvergreenLevel"] =
EVERGREEN_LEVEL_INFO.length EVERGREEN_LEVEL_INFO.length
} }

@ -95,6 +95,19 @@ import { getPlayerProfileData } from "./menus/playerProfile"
const menuDataRouter = Router() 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<string, string> = {
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/ // /profiles/page/
menuDataRouter.get( menuDataRouter.get(
@ -1400,25 +1413,12 @@ menuDataRouter.get(
"/GetMasteryCompletionDataForUnlockable", "/GetMasteryCompletionDataForUnlockable",
// @ts-expect-error Has jwt props. // @ts-expect-error Has jwt props.
(req: RequestWithJwt<GetMasteryCompletionDataForUnlockableQuery>, res) => { (req: RequestWithJwt<GetMasteryCompletionDataForUnlockableQuery>, res) => {
// We make this lookup table to quickly get it, there's no other quick way for it.
const unlockToLoc: Record<string, string> = {
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({ res.json({
template: null, template: null,
data: { data: {
CompletionData: controller.masteryService.getLocationCompletion( CompletionData: controller.masteryService.getLocationCompletion(
unlockToLoc[req.query.unlockableId], SNIPER_UNLOCK_TO_LOCATION[req.query.unlockableId],
unlockToLoc[req.query.unlockableId], SNIPER_UNLOCK_TO_LOCATION[req.query.unlockableId],
req.gameVersion, req.gameVersion,
req.jwt.unique_name, req.jwt.unique_name,
"sniper", "sniper",

@ -30,6 +30,13 @@ import { getDestinationCompletion } from "./destinations"
import { getUserData } from "../databaseHandler" import { getUserData } from "../databaseHandler"
import { isSniperLocation } from "../utils" import { isSniperLocation } from "../utils"
type XpData = {
[location: string]: {
Xp: number
ActionXp: number
}
}
export function getPlayerProfileData( export function getPlayerProfileData(
gameVersion: GameVersion, gameVersion: GameVersion,
userId: string, userId: string,
@ -47,7 +54,27 @@ export function getPlayerProfileData(
playerProfilePage.SubLocationData = [] playerProfilePage.SubLocationData = []
const userProfile = getUserData(userId, gameVersion)
const subLocationMap =
userProfile.Extensions.progression.PlayerProfileXP.Sublocations
const xpData: XpData = {}
for (const subLocationKey in locationData.children) { 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... // Ewww...
if ( if (
subLocationKey === "LOCATION_ICA_FACILITY_ARRIVAL" || subLocationKey === "LOCATION_ICA_FACILITY_ARRIVAL" ||
@ -56,10 +83,6 @@ export function getPlayerProfileData(
continue continue
} }
const subLocation = locationData.children[subLocationKey]
const parentLocation =
locationData.parents[subLocation.Properties.ParentLocation || ""]
const completionData = generateCompletionData( const completionData = generateCompletionData(
subLocation.Id, subLocation.Id,
userId, userId,
@ -109,24 +132,17 @@ export function getPlayerProfileData(
}) })
} }
const userProfile = getUserData(userId, gameVersion)
playerProfilePage.PlayerProfileXp.Total = playerProfilePage.PlayerProfileXp.Total =
userProfile.Extensions.progression.PlayerProfileXP.Total userProfile.Extensions.progression.PlayerProfileXP.Total
playerProfilePage.PlayerProfileXp.Level = playerProfilePage.PlayerProfileXp.Level =
userProfile.Extensions.progression.PlayerProfileXP.ProfileLevel 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 season of playerProfilePage.PlayerProfileXp.Seasons) {
for (const location of season.Locations) { for (const location of season.Locations) {
const subLocationData = subLocationMap.get(location.LocationId) const locationData = xpData[location.LocationId]
location.Xp = subLocationData?.Xp || 0 location.Xp = locationData?.Xp || 0
location.ActionXp = subLocationData?.ActionXp || 0 location.ActionXp = locationData?.ActionXp || 0
if ( if (
location.LocationProgression && location.LocationProgression &&

@ -511,10 +511,11 @@ export type UserProfile = {
*/ */
Total: number Total: number
Sublocations: { Sublocations: {
Location: string [location: string]: {
Xp: number Xp: number
ActionXp: number ActionXp: number
}[] }
}
} }
/** /**
* If the mastery location has subpackages and not drops, it will * If the mastery location has subpackages and not drops, it will
@ -560,6 +561,7 @@ export type UserProfile = {
[opportunityId: RepositoryId]: boolean [opportunityId: RepositoryId]: boolean
} }
CPD: CPDStore CPD: CPDStore
LastOfficialSync: Date | string | null
} }
ETag: string | null ETag: string | null
Gamertag: string Gamertag: string
@ -1572,3 +1574,9 @@ export type SMFLastDeploy = {
peacockPlugins?: string[] peacockPlugins?: string[]
} }
} }
export type OfficialSublocation = {
Location: string
Xp: number
ActionXp: number
}

@ -21,6 +21,7 @@ import type { NextFunction, Response } from "express"
import type { import type {
GameVersion, GameVersion,
MissionManifestObjective, MissionManifestObjective,
OfficialSublocation,
PeacockLocationsData, PeacockLocationsData,
RepositoryId, RepositoryId,
RequestWithJwt, RequestWithJwt,
@ -66,7 +67,7 @@ export const contractCreationTutorialId = "d7e2607c-6916-48e2-9588-976c7d8998bb"
* *
* See docs/USER_PROFILES.md for more. * See docs/USER_PROFILES.md for more.
*/ */
export const LATEST_PROFILE_VERSION = 1 export const LATEST_PROFILE_VERSION = 2
export async function checkForUpdates(): Promise<void> { export async function checkForUpdates(): Promise<void> {
if (getFlag("updateChecking") === false) { 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 profile The user profile to update
* @param gameVersion The game version * @param gameVersion The game version
* @returns The updated user profile.
*/ */
function updateUserProfile( function updateUserProfile(
profile: UserProfile, profile: UserProfile,
@ -266,6 +266,29 @@ function updateUserProfile(
case LATEST_PROFILE_VERSION: case LATEST_PROFILE_VERSION:
// This profile updated to the latest version, we're done. // This profile updated to the latest version, we're done.
return 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: { default: {
// Check that the profile version is indeed undefined. If it isn't, // Check that the profile version is indeed undefined. If it isn't,
// we've forgotten to add a version to the switch. // we've forgotten to add a version to the switch.
@ -726,3 +749,26 @@ export function isSuit(repoId: string): boolean {
? suitsToTypeMap[repoId] !== "disguise" ? suitsToTypeMap[repoId] !== "disguise"
: false : false
} }
type SublocationMap = {
[parentId: string]: string[]
}
export function getSublocations(gameVersion: GameVersion): SublocationMap {
const sublocations: SublocationMap = {}
const locations = getVersionedConfig<PeacockLocationsData>(
"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
}

@ -19,12 +19,43 @@
import { NextFunction, Request, Response, Router } from "express" import { NextFunction, Request, Response, Router } from "express"
import { getConfig } from "./configSwizzleManager" import { getConfig } from "./configSwizzleManager"
import { readdir, readFile } from "fs/promises" 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 { join } from "path"
import { uuidRegex, versions } from "./utils" import {
getRemoteService,
getSublocations,
isSniperLocation,
levelForXp,
uuidRegex,
versions,
} from "./utils"
import { getUserData, loadUserData, writeUserData } from "./databaseHandler" import { getUserData, loadUserData, writeUserData } from "./databaseHandler"
import { controller } from "./controller" import { controller } from "./controller"
import { log, LogLevel } from "./loggingInterop" 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() const webFeaturesRouter = Router()
@ -108,9 +139,21 @@ webFeaturesRouter.get("/local-users", async (req: CommonRequest, res) => {
(name) => name !== "lop.json", (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) { for (const file of files) {
if (file === "lop.json") continue
const read = JSON.parse( const read = JSON.parse(
(await readFile(join(dir, file))).toString(), (await readFile(join(dir, file))).toString(),
) as UserProfile ) as UserProfile
@ -119,6 +162,8 @@ webFeaturesRouter.get("/local-users", async (req: CommonRequest, res) => {
id: read.Id, id: read.Id,
name: read.Gamertag, name: read.Gamertag,
platform: read.EpicId ? "Epic" : "Steam", 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<OfficialHitsCategory>(
`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<EscalationData> {
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<OfficialProfileResponse>(
`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 } export { webFeaturesRouter }

@ -23,3 +23,10 @@ Version 1 introduced the profile versioning system. The changes are:
- Sniper locations now have their unlockables contained inside the location. - Sniper locations now have their unlockables contained inside the location.
- Unused properties were removed from locations in `u.Extensions.progression.Locations`. - Unused properties were removed from locations in `u.Extensions.progression.Locations`.
- `u.Extensions.progression.Unlockables` has been removed. - `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.

@ -1,3 +0,0 @@
{
"EvergreenLevel": 1.0
}

@ -0,0 +1,5 @@
{
"f8ec92c2-4fa2-471e-ae08-545480c746ee": {
"EvergreenLevel": 1.0
}
}

@ -53,78 +53,7 @@
}, },
"PlayerProfileXP": { "PlayerProfileXP": {
"Total": 0, "Total": 0,
"Sublocations": [ "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
}
],
"PreviouslySeenTotal": 0, "PreviouslySeenTotal": 0,
"ProfileLevel": 0, "ProfileLevel": 0,
"PreviouslySeenStaging": null "PreviouslySeenStaging": null
@ -162,7 +91,8 @@
}, },
"opportunityprogression": {}, "opportunityprogression": {},
"inventory": [], "inventory": [],
"friends": [] "friends": [],
"LastOfficialSync": null
}, },
"ETag": null, "ETag": null,
"Gamertag": null, "Gamertag": null,
@ -173,5 +103,5 @@
"XboxLiveId": null, "XboxLiveId": null,
"PSNAccountId": null, "PSNAccountId": null,
"PSNOnlineId": null, "PSNOnlineId": null,
"Version": 1 "Version": 2
} }

@ -184,168 +184,7 @@
}, },
"PlayerProfileXP": { "PlayerProfileXP": {
"Total": 0, "Total": 0,
"Sublocations": [ "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
}
],
"PreviouslySeenTotal": 0, "PreviouslySeenTotal": 0,
"ProfileLevel": 0, "ProfileLevel": 0,
"PreviouslySeenStaging": null "PreviouslySeenStaging": null
@ -384,7 +223,8 @@
}, },
"opportunityprogression": {}, "opportunityprogression": {},
"friends": [], "friends": [],
"CPD": {} "CPD": {},
"LastOfficialSync": null
}, },
"ETag": null, "ETag": null,
"Gamertag": null, "Gamertag": null,
@ -395,5 +235,5 @@
"XboxLiveId": null, "XboxLiveId": null,
"PSNAccountId": null, "PSNAccountId": null,
"PSNOnlineId": null, "PSNOnlineId": null,
"Version": 1 "Version": 2
} }

@ -4,6 +4,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@radix-ui/react-alert-dialog": "^1.0.5",
"axios": "^1.6.8", "axios": "^1.6.8",
"clsx": "^2.1.0", "clsx": "^2.1.0",
"immer": "^10.0.4", "immer": "^10.0.4",

@ -114,3 +114,33 @@ body {
.pagination-nav__item:first-child .pagination-nav__label::before { .pagination-nav__item:first-child .pagination-nav__label::before {
content: "" !important; 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;
}

@ -21,6 +21,7 @@ import "infima/dist/css/default/default.css"
import "./App.css" import "./App.css"
import { createBrowserRouter, RouterProvider } from "react-router-dom" import { createBrowserRouter, RouterProvider } from "react-router-dom"
import { DevToolsPage } from "./pages/DevToolsPage" import { DevToolsPage } from "./pages/DevToolsPage"
import { TransferPage } from "./pages/TransferPage"
import { LoadoutPage } from "./pages/LoadoutPage" import { LoadoutPage } from "./pages/LoadoutPage"
import { RootPage } from "./pages/RootPage" import { RootPage } from "./pages/RootPage"
import { Home } from "./pages/Home" import { Home } from "./pages/Home"
@ -41,6 +42,10 @@ const router = createBrowserRouter([
element: <DevToolsPage />, element: <DevToolsPage />,
} }
: null!, : null!,
{
path: "ui/transfer",
element: <TransferPage />,
},
{ {
path: "ui/loadouts", path: "ui/loadouts",
element: <LoadoutPage />, element: <LoadoutPage />,

@ -26,7 +26,7 @@ export interface LoadoutPreviewProps {
function adjustCase(input: string): string { function adjustCase(input: string): string {
return input return input
.split(" ") .split(" ")
.map((i) => `${i[0].toUpperCase()}${i.substr(1).toLowerCase()}`) .map((i) => `${i[0].toUpperCase()}${i.substring(1).toLowerCase()}`)
.join(" ") .join(" ")
} }

@ -52,7 +52,6 @@ export function LoadoutsForGameVersion({
} }
// note: putting these 2 consts together make intellij and prettier conflict each other // 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 theGameVer = data[`h${gameVersion}`] as LoadoutsGameVersion
const loadoutsForVersion: Loadout[] = theGameVer.loadouts const loadoutsForVersion: Loadout[] = theGameVer.loadouts
@ -62,7 +61,6 @@ export function LoadoutsForGameVersion({
<nav className="pagination-nav"> <nav className="pagination-nav">
{loadoutsForVersion.map((loadout) => { {loadoutsForVersion.map((loadout) => {
const isActive = const isActive =
// @ts-expect-error also fake news
data[`h${gameVersion}`].selected === loadout.id data[`h${gameVersion}`].selected === loadout.id
return ( return (

@ -0,0 +1,90 @@
/*
* The Peacock Project - a HITMAN server replacement.
* Copyright (C) 2021-2024 The Peacock Project Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from "react"
import * as AlertDialog from "@radix-ui/react-alert-dialog"
type TransferAlertProps = {
trigger: React.ReactElement
doSync: () => void
}
function Speech() {
return (
<>
This will <b>delete existing progress on this Peacock profile</b>.
<br />
Your progress will be replaced with whatever you have completed on
the official servers.
<br />
<br />
<b>THIS CANNOT BE UNDONE. Are you SURE you want to continue?</b>
<br />
<br />
<b>
You may want to make a copy of your <code>userdata</code> folder
first.
</b>
</>
)
}
export function TransferAlert(props: TransferAlertProps) {
return (
<AlertDialog.Root>
<AlertDialog.Trigger asChild>{props.trigger}</AlertDialog.Trigger>
<AlertDialog.Portal>
<AlertDialog.Overlay className="AlertDialogOverlay" />
<AlertDialog.Content className="AlertDialogContent">
<AlertDialog.Title className="AlertDialogTitle">
<b>Danger!</b> Are you sure you want to continue?
</AlertDialog.Title>
<AlertDialog.Description className="AlertDialogDescription">
<Speech />
</AlertDialog.Description>
<div
style={{
display: "flex",
gap: 25,
justifyContent: "flex-end",
}}
>
<AlertDialog.Cancel asChild>
<button className="button button--primary">
Cancel
</button>
</AlertDialog.Cancel>
<AlertDialog.Action asChild>
<button
className="button button--danger"
onClick={props.doSync}
>
Yes, sync now
</button>
</AlertDialog.Action>
</div>
</AlertDialog.Content>
</AlertDialog.Portal>
</AlertDialog.Root>
)
}

@ -0,0 +1,114 @@
/*
* The Peacock Project - a HITMAN server replacement.
* Copyright (C) 2021-2024 The Peacock Project Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import * as React from "react"
import { axiosClient, BasicUser } from "./utils"
import { TransferAlert } from "./TransferAlert"
type TransferDetailsProps = {
user: BasicUser
gv: "h1" | "h2" | "h3"
}
function InProgressAlert() {
return (
<div className="alert alert--primary" role="alert">
Performing the transfer, this may take a bit...
</div>
)
}
function TransferSuccessfulAlert() {
return (
<div className="alert alert--success" role="alert">
Transfer completed successfully!
</div>
)
}
function TransferErrorAlert({ error }: { error: string }) {
return (
<div className="alert alert--danger" role="alert">
Failed to perform transfer. {error}
</div>
)
}
export function TransferDetails({ user, gv }: TransferDetailsProps) {
const [fetching, setFetching] = React.useState<boolean>(false)
const [error, setError] = React.useState<string | null>(null)
const [lastSucceeded, setLastSucceeded] = React.useState<boolean | null>(
null,
)
const doSync = React.useCallback(async () => {
try {
const resp = await axiosClient.post(
"/_wf/sync-progress",
{},
{
params: {
gv,
user: user.id,
},
},
)
if (resp.data.success) {
setFetching(false)
setLastSucceeded(true)
setError(null)
} else {
setFetching(false)
setLastSucceeded(false)
setError(resp.data.error)
}
} catch (e) {
console.error(e)
setFetching(false)
setError("Failed to perform internal request to begin transfer.")
setLastSucceeded(false)
}
}, [gv, user.id])
return (
<div className={"margin-top"}>
<p>
Selected profile: {user.name} ({user.platform})
</p>
<p>Last official server sync: {user.lastOfficialSync || "never"}</p>
{fetching ? <InProgressAlert /> : null}
{lastSucceeded === true ? <TransferSuccessfulAlert /> : null}
{lastSucceeded === false && error ? (
<TransferErrorAlert error={error} />
) : null}
<TransferAlert
trigger={
<button className={"button button--success"}>
Sync Now
</button>
}
doSync={doSync}
/>
</div>
)
}

@ -17,10 +17,10 @@
*/ */
import * as React from "react" import * as React from "react"
import { IUser } from "../utils" import { BasicUser } from "../utils"
export interface SelectUserProps { export interface SelectUserProps {
users: IUser[] users: BasicUser[]
setUser: React.Dispatch<React.SetStateAction<string | undefined>> setUser: React.Dispatch<React.SetStateAction<string | undefined>>
} }

@ -19,7 +19,7 @@
import * as React from "react" import * as React from "react"
import { Hero } from "../components/Hero" import { Hero } from "../components/Hero"
import useSWR from "swr" import useSWR from "swr"
import { baseURL, fetcher, IUser } from "../utils" import { baseURL, BasicUser, fetcher } from "../utils"
import { SelectUser } from "../components/SelectUser" import { SelectUser } from "../components/SelectUser"
import { GameVersionTabs } from "../components/GameVersionTabs" import { GameVersionTabs } from "../components/GameVersionTabs"
import { EscalationLevelPicker } from "../EscalationLevelPicker" import { EscalationLevelPicker } from "../EscalationLevelPicker"
@ -60,7 +60,7 @@ export function EscalationLevelPage() {
const isReadyToSelectUser = Boolean( const isReadyToSelectUser = Boolean(
user === undefined && user === undefined &&
userData && userData &&
(userData as { error: string } & IUser[])?.error !== "bad gv", (userData as { error: string } & BasicUser[])?.error !== "bad gv",
) )
function getStatus(): string { function getStatus(): string {
@ -93,7 +93,10 @@ export function EscalationLevelPage() {
/> />
)} )}
{isReadyToSelectUser && ( {isReadyToSelectUser && (
<SelectUser users={userData as IUser[]} setUser={setUser} /> <SelectUser
users={userData as BasicUser[]}
setUser={setUser}
/>
)} )}
{Boolean(codenameData) && {Boolean(codenameData) &&
gameVersion !== 0 && gameVersion !== 0 &&

@ -0,0 +1,96 @@
/*
* The Peacock Project - a HITMAN server replacement.
* Copyright (C) 2021-2024 The Peacock Project Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { Hero } from "../components/Hero"
import * as React from "react"
import useSWR from "swr"
import { baseURL, BasicUser, fetcher } from "../utils"
import { GameVersionTabs } from "../components/GameVersionTabs"
import { SelectUser } from "../components/SelectUser"
import { TransferDetails } from "../TransferDetails"
export function TransferPage() {
const [user, setUser] = React.useState<string | undefined>(undefined)
const [gameVersion, setGameVersion] = React.useState<number>(0)
const { data: userData, error: userFetchError } = useSWR(
`${baseURL}/_wf/local-users?gv=h${gameVersion}`,
fetcher,
)
if (userFetchError) {
console.error(userFetchError)
}
const isReadyToSelectUser = Boolean(
gameVersion !== 0 &&
user === undefined &&
userData &&
(userData as { error: string } & BasicUser[])?.error !== "bad gv",
)
function getStatus(): string {
if (gameVersion === 0) {
return "Select your game version."
}
if (isReadyToSelectUser) {
return "Select target user profile."
}
return ""
}
return (
<>
<header>
<Hero
title="Official Server Transfer Tool"
subtext={getStatus()}
/>
</header>
<main className="container">
{gameVersion === 0 && (
<GameVersionTabs
gameVersion={gameVersion}
setGameVersion={setGameVersion}
/>
)}
{isReadyToSelectUser && (
<SelectUser
users={userData as BasicUser[]}
setUser={setUser}
/>
)}
{gameVersion !== 0 &&
!isReadyToSelectUser &&
user &&
userData && (
<TransferDetails
gv={`h${gameVersion as 1 | 2 | 3}`}
user={
(userData as BasicUser[]).find(
(u) => u.id === user,
)!
}
/>
)}
</main>
</>
)
}

@ -55,11 +55,12 @@ export interface Loadout {
} }
} }
export interface IUser { export type BasicUser = Readonly<{
readonly id: string id: string
readonly name: string name: string
readonly platform: string platform: string
} lastOfficialSync: string | null
}>
export interface LoadoutsGameVersion { export interface LoadoutsGameVersion {
selected: string selected: string

449
yarn.lock

@ -44,6 +44,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/runtime@npm:^7.13.10":
version: 7.24.4
resolution: "@babel/runtime@npm:7.24.4"
dependencies:
regenerator-runtime: "npm:^0.14.0"
checksum: 10/8ec8ce2c145bc7e31dd39ab66df124f357f65c11489aefacb30f431bae913b9aaa66aa5efe5321ea2bf8878af3fcee338c87e7599519a952e3a6f83aa1b03308
languageName: node
linkType: hard
"@colors/colors@npm:1.6.0, @colors/colors@npm:^1.6.0": "@colors/colors@npm:1.6.0, @colors/colors@npm:^1.6.0":
version: 1.6.0 version: 1.6.0
resolution: "@colors/colors@npm:1.6.0" resolution: "@colors/colors@npm:1.6.0"
@ -570,6 +579,7 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "@peacockproject/web-ui@workspace:webui" resolution: "@peacockproject/web-ui@workspace:webui"
dependencies: dependencies:
"@radix-ui/react-alert-dialog": "npm:^1.0.5"
"@types/react": "npm:^18.2.74" "@types/react": "npm:^18.2.74"
"@types/react-dom": "npm:^18.2.23" "@types/react-dom": "npm:^18.2.23"
axios: "npm:^1.6.8" axios: "npm:^1.6.8"
@ -606,6 +616,319 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@radix-ui/primitive@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/primitive@npm:1.0.1"
dependencies:
"@babel/runtime": "npm:^7.13.10"
checksum: 10/2b93e161d3fdabe9a64919def7fa3ceaecf2848341e9211520c401181c9eaebb8451c630b066fad2256e5c639c95edc41de0ba59c40eff37e799918d019822d1
languageName: node
linkType: hard
"@radix-ui/react-alert-dialog@npm:^1.0.5":
version: 1.0.5
resolution: "@radix-ui/react-alert-dialog@npm:1.0.5"
dependencies:
"@babel/runtime": "npm:^7.13.10"
"@radix-ui/primitive": "npm:1.0.1"
"@radix-ui/react-compose-refs": "npm:1.0.1"
"@radix-ui/react-context": "npm:1.0.1"
"@radix-ui/react-dialog": "npm:1.0.5"
"@radix-ui/react-primitive": "npm:1.0.3"
"@radix-ui/react-slot": "npm:1.0.2"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 10/966eeef94056caa4105278686018bc8ec5ec10584191d9e878944b1785ae88a355cbff0b754af4c3878bd97af8d954583b7a2e4819b928ccf6839cd2962e8a09
languageName: node
linkType: hard
"@radix-ui/react-compose-refs@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-compose-refs@npm:1.0.1"
dependencies:
"@babel/runtime": "npm:^7.13.10"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/2b9a613b6db5bff8865588b6bf4065f73021b3d16c0a90b2d4c23deceeb63612f1f15de188227ebdc5f88222cab031be617a9dd025874c0487b303be3e5cc2a8
languageName: node
linkType: hard
"@radix-ui/react-context@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-context@npm:1.0.1"
dependencies:
"@babel/runtime": "npm:^7.13.10"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/a02187a3bae3a0f1be5fab5ad19c1ef06ceff1028d957e4d9994f0186f594a9c3d93ee34bacb86d1fa8eb274493362944398e1c17054d12cb3b75384f9ae564b
languageName: node
linkType: hard
"@radix-ui/react-dialog@npm:1.0.5":
version: 1.0.5
resolution: "@radix-ui/react-dialog@npm:1.0.5"
dependencies:
"@babel/runtime": "npm:^7.13.10"
"@radix-ui/primitive": "npm:1.0.1"
"@radix-ui/react-compose-refs": "npm:1.0.1"
"@radix-ui/react-context": "npm:1.0.1"
"@radix-ui/react-dismissable-layer": "npm:1.0.5"
"@radix-ui/react-focus-guards": "npm:1.0.1"
"@radix-ui/react-focus-scope": "npm:1.0.4"
"@radix-ui/react-id": "npm:1.0.1"
"@radix-ui/react-portal": "npm:1.0.4"
"@radix-ui/react-presence": "npm:1.0.1"
"@radix-ui/react-primitive": "npm:1.0.3"
"@radix-ui/react-slot": "npm:1.0.2"
"@radix-ui/react-use-controllable-state": "npm:1.0.1"
aria-hidden: "npm:^1.1.1"
react-remove-scroll: "npm:2.5.5"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 10/adbd7301586db712616a0f8dd54a25e7544853cbf61b5d6e279215d479f57ac35157847ee424d54a7e707969a926ca0a7c28934400c9ac224bd0c7cc19229aca
languageName: node
linkType: hard
"@radix-ui/react-dismissable-layer@npm:1.0.5":
version: 1.0.5
resolution: "@radix-ui/react-dismissable-layer@npm:1.0.5"
dependencies:
"@babel/runtime": "npm:^7.13.10"
"@radix-ui/primitive": "npm:1.0.1"
"@radix-ui/react-compose-refs": "npm:1.0.1"
"@radix-ui/react-primitive": "npm:1.0.3"
"@radix-ui/react-use-callback-ref": "npm:1.0.1"
"@radix-ui/react-use-escape-keydown": "npm:1.0.3"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 10/f1626d69bb50ec226032bb7d8c5abaaf7359c2d7660309b0ed3daaedd91f30717573aac1a1cb82d589b7f915cf464b95a12da0a3b91b6acfefb6fbbc62b992de
languageName: node
linkType: hard
"@radix-ui/react-focus-guards@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-focus-guards@npm:1.0.1"
dependencies:
"@babel/runtime": "npm:^7.13.10"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/1f8ca8f83b884b3612788d0742f3f054e327856d90a39841a47897dbed95e114ee512362ae314177de226d05310047cabbf66b686ae86ad1b65b6b295be24ef7
languageName: node
linkType: hard
"@radix-ui/react-focus-scope@npm:1.0.4":
version: 1.0.4
resolution: "@radix-ui/react-focus-scope@npm:1.0.4"
dependencies:
"@babel/runtime": "npm:^7.13.10"
"@radix-ui/react-compose-refs": "npm:1.0.1"
"@radix-ui/react-primitive": "npm:1.0.3"
"@radix-ui/react-use-callback-ref": "npm:1.0.1"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 10/3590e74c6b682737c7ac4bf8db41b3df7b09a0320f3836c619e487df9915451e5dafade9923a74383a7366c59e9436f5fff4301d70c0d15928e0e16b36e58bc9
languageName: node
linkType: hard
"@radix-ui/react-id@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-id@npm:1.0.1"
dependencies:
"@babel/runtime": "npm:^7.13.10"
"@radix-ui/react-use-layout-effect": "npm:1.0.1"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/446a453d799cc790dd2a1583ff8328da88271bff64530b5a17c102fa7fb35eece3cf8985359d416f65e330cd81aa7b8fe984ea125fc4f4eaf4b3801d698e49fe
languageName: node
linkType: hard
"@radix-ui/react-portal@npm:1.0.4":
version: 1.0.4
resolution: "@radix-ui/react-portal@npm:1.0.4"
dependencies:
"@babel/runtime": "npm:^7.13.10"
"@radix-ui/react-primitive": "npm:1.0.3"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 10/c4cf35e2f26a89703189d0eef3ceeeb706ae0832e98e558730a5e929ca7c72c7cb510413a24eca94c7732f8d659a1e81942bec7b90540cb73ce9e4885d040b64
languageName: node
linkType: hard
"@radix-ui/react-presence@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-presence@npm:1.0.1"
dependencies:
"@babel/runtime": "npm:^7.13.10"
"@radix-ui/react-compose-refs": "npm:1.0.1"
"@radix-ui/react-use-layout-effect": "npm:1.0.1"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 10/406f0b5a54ea4e7881e15bddc3863234bb14bf3abd4a6e56ea57c6df6f9265a9ad5cfa158e3a98614f0dcbbb7c5f537e1f7158346e57cc3f29b522d62cf28823
languageName: node
linkType: hard
"@radix-ui/react-primitive@npm:1.0.3":
version: 1.0.3
resolution: "@radix-ui/react-primitive@npm:1.0.3"
dependencies:
"@babel/runtime": "npm:^7.13.10"
"@radix-ui/react-slot": "npm:1.0.2"
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 10/bedb934ac07c710dc5550a7bfc7065d47e099d958cde1d37e4b1947ae5451f1b7e6f8ff5965e242578bf2c619065e6038c3a3aa779e5eafa7da3e3dbc685799f
languageName: node
linkType: hard
"@radix-ui/react-slot@npm:1.0.2":
version: 1.0.2
resolution: "@radix-ui/react-slot@npm:1.0.2"
dependencies:
"@babel/runtime": "npm:^7.13.10"
"@radix-ui/react-compose-refs": "npm:1.0.1"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/734866561e991438fbcf22af06e56b272ed6ee8f7b536489ee3bf2f736f8b53bf6bc14ebde94834aa0aceda854d018a0ce20bb171defffbaed1f566006cbb887
languageName: node
linkType: hard
"@radix-ui/react-use-callback-ref@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-use-callback-ref@npm:1.0.1"
dependencies:
"@babel/runtime": "npm:^7.13.10"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/b9fd39911c3644bbda14a84e4fca080682bef84212b8d8931fcaa2d2814465de242c4cfd8d7afb3020646bead9c5e539d478cea0a7031bee8a8a3bb164f3bc4c
languageName: node
linkType: hard
"@radix-ui/react-use-controllable-state@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-use-controllable-state@npm:1.0.1"
dependencies:
"@babel/runtime": "npm:^7.13.10"
"@radix-ui/react-use-callback-ref": "npm:1.0.1"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/dee2be1937d293c3a492cb6d279fc11495a8f19dc595cdbfe24b434e917302f9ac91db24e8cc5af9a065f3f209c3423115b5442e65a5be9fd1e9091338972be9
languageName: node
linkType: hard
"@radix-ui/react-use-escape-keydown@npm:1.0.3":
version: 1.0.3
resolution: "@radix-ui/react-use-escape-keydown@npm:1.0.3"
dependencies:
"@babel/runtime": "npm:^7.13.10"
"@radix-ui/react-use-callback-ref": "npm:1.0.1"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/c6ed0d9ce780f67f924980eb305af1f6cce2a8acbaf043a58abe0aa3cc551d9aa76ccee14531df89bbee302ead7ecc7fce330886f82d4672c5eda52f357ef9b8
languageName: node
linkType: hard
"@radix-ui/react-use-layout-effect@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-use-layout-effect@npm:1.0.1"
dependencies:
"@babel/runtime": "npm:^7.13.10"
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/bed9c7e8de243a5ec3b93bb6a5860950b0dba359b6680c84d57c7a655e123dec9b5891c5dfe81ab970652e7779fe2ad102a23177c7896dde95f7340817d47ae5
languageName: node
linkType: hard
"@rdil/parallel-prettier@npm:^3.0.0": "@rdil/parallel-prettier@npm:^3.0.0":
version: 3.0.0 version: 3.0.0
resolution: "@rdil/parallel-prettier@npm:3.0.0" resolution: "@rdil/parallel-prettier@npm:3.0.0"
@ -1328,6 +1651,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"aria-hidden@npm:^1.1.1":
version: 1.2.4
resolution: "aria-hidden@npm:1.2.4"
dependencies:
tslib: "npm:^2.0.0"
checksum: 10/df4bc15423aaaba3729a7d40abcbf6d3fffa5b8fd5eb33d3ac8b7da0110c47552fca60d97f2e1edfbb68a27cae1da499f1c3896966efb3e26aac4e3b57e3cc8b
languageName: node
linkType: hard
"array-find-index@npm:^1.0.2": "array-find-index@npm:^1.0.2":
version: 1.0.2 version: 1.0.2
resolution: "array-find-index@npm:1.0.2" resolution: "array-find-index@npm:1.0.2"
@ -1858,6 +2190,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"detect-node-es@npm:^1.1.0":
version: 1.1.0
resolution: "detect-node-es@npm:1.1.0"
checksum: 10/e46307d7264644975b71c104b9f028ed1d3d34b83a15b8a22373640ce5ea630e5640b1078b8ea15f202b54641da71e4aa7597093bd4b91f113db520a26a37449
languageName: node
linkType: hard
"diff-sequences@npm:^29.6.3": "diff-sequences@npm:^29.6.3":
version: 29.6.3 version: 29.6.3
resolution: "diff-sequences@npm:29.6.3" resolution: "diff-sequences@npm:29.6.3"
@ -2603,6 +2942,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"get-nonce@npm:^1.0.0":
version: 1.0.1
resolution: "get-nonce@npm:1.0.1"
checksum: 10/ad5104871d114a694ecc506a2d406e2331beccb961fe1e110dc25556b38bcdbf399a823a8a375976cd8889668156a9561e12ebe3fa6a4c6ba169c8466c2ff868
languageName: node
linkType: hard
"get-stream@npm:^8.0.1": "get-stream@npm:^8.0.1":
version: 8.0.1 version: 8.0.1
resolution: "get-stream@npm:8.0.1" resolution: "get-stream@npm:8.0.1"
@ -2907,6 +3253,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"invariant@npm:^2.2.4":
version: 2.2.4
resolution: "invariant@npm:2.2.4"
dependencies:
loose-envify: "npm:^1.0.0"
checksum: 10/cc3182d793aad82a8d1f0af697b462939cb46066ec48bbf1707c150ad5fad6406137e91a262022c269702e01621f35ef60269f6c0d7fd178487959809acdfb14
languageName: node
linkType: hard
"ip@npm:^2.0.0": "ip@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "ip@npm:2.0.0" resolution: "ip@npm:2.0.0"
@ -3349,7 +3704,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"loose-envify@npm:^1.1.0": "loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0":
version: 1.4.0 version: 1.4.0
resolution: "loose-envify@npm:1.4.0" resolution: "loose-envify@npm:1.4.0"
dependencies: dependencies:
@ -4235,6 +4590,41 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-remove-scroll-bar@npm:^2.3.3":
version: 2.3.6
resolution: "react-remove-scroll-bar@npm:2.3.6"
dependencies:
react-style-singleton: "npm:^2.2.1"
tslib: "npm:^2.0.0"
peerDependencies:
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/5ab8eda61d5b10825447d11e9c824486c929351a471457c22452caa19b6898e18c3af6a46c3fa68010c713baed1eb9956106d068b4a1058bdcf97a1a9bbed734
languageName: node
linkType: hard
"react-remove-scroll@npm:2.5.5":
version: 2.5.5
resolution: "react-remove-scroll@npm:2.5.5"
dependencies:
react-remove-scroll-bar: "npm:^2.3.3"
react-style-singleton: "npm:^2.2.1"
tslib: "npm:^2.1.0"
use-callback-ref: "npm:^1.3.0"
use-sidecar: "npm:^1.1.2"
peerDependencies:
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/f0646ac384ce3852d1f41e30a9f9e251b11cf3b430d1d114c937c8fa7f90a895c06378d0d6b6ff0b2d00cbccf15e845921944fd6074ae67a0fb347a718106d88
languageName: node
linkType: hard
"react-router-dom@npm:^6.22.3": "react-router-dom@npm:^6.22.3":
version: 6.22.3 version: 6.22.3
resolution: "react-router-dom@npm:6.22.3" resolution: "react-router-dom@npm:6.22.3"
@ -4259,6 +4649,23 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"react-style-singleton@npm:^2.2.1":
version: 2.2.1
resolution: "react-style-singleton@npm:2.2.1"
dependencies:
get-nonce: "npm:^1.0.0"
invariant: "npm:^2.2.4"
tslib: "npm:^2.0.0"
peerDependencies:
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/80c58fd6aac3594e351e2e7b048d8a5b09508adb21031a38b3c40911fe58295572eddc640d4b20a7be364842c8ed1120fe30097e22ea055316b375b88d4ff02a
languageName: node
linkType: hard
"react@npm:^18.2.0": "react@npm:^18.2.0":
version: 18.2.0 version: 18.2.0
resolution: "react@npm:18.2.0" resolution: "react@npm:18.2.0"
@ -4294,6 +4701,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"regenerator-runtime@npm:^0.14.0":
version: 0.14.1
resolution: "regenerator-runtime@npm:0.14.1"
checksum: 10/5db3161abb311eef8c45bcf6565f4f378f785900ed3945acf740a9888c792f75b98ecb77f0775f3bf95502ff423529d23e94f41d80c8256e8fa05ed4b07cf471
languageName: node
linkType: hard
"remove-trailing-separator@npm:^1.1.0": "remove-trailing-separator@npm:^1.1.0":
version: 1.1.0 version: 1.1.0
resolution: "remove-trailing-separator@npm:1.1.0" resolution: "remove-trailing-separator@npm:1.1.0"
@ -5068,7 +5482,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.6.2": "tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.6.2":
version: 2.6.2 version: 2.6.2
resolution: "tslib@npm:2.6.2" resolution: "tslib@npm:2.6.2"
checksum: 10/bd26c22d36736513980091a1e356378e8b662ded04204453d353a7f34a4c21ed0afc59b5f90719d4ba756e581a162ecbf93118dc9c6be5acf70aa309188166ca checksum: 10/bd26c22d36736513980091a1e356378e8b662ded04204453d353a7f34a4c21ed0afc59b5f90719d4ba756e581a162ecbf93118dc9c6be5acf70aa309188166ca
@ -5200,6 +5614,37 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"use-callback-ref@npm:^1.3.0":
version: 1.3.2
resolution: "use-callback-ref@npm:1.3.2"
dependencies:
tslib: "npm:^2.0.0"
peerDependencies:
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/3be76eae71b52ab233b4fde974eddeff72e67e6723100a0c0297df4b0d60daabedfa706ffb314d0a52645f2c1235e50fdbd53d99f374eb5df68c74d412e98a9b
languageName: node
linkType: hard
"use-sidecar@npm:^1.1.2":
version: 1.1.2
resolution: "use-sidecar@npm:1.1.2"
dependencies:
detect-node-es: "npm:^1.1.0"
tslib: "npm:^2.0.0"
peerDependencies:
"@types/react": ^16.9.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 10/ec99e31aefeb880f6dc4d02cb19a01d123364954f857811470ece32872f70d6c3eadbe4d073770706a9b7db6136f2a9fbf1bb803e07fbb21e936a47479281690
languageName: node
linkType: hard
"use-sync-external-store@npm:^1.2.0": "use-sync-external-store@npm:^1.2.0":
version: 1.2.0 version: 1.2.0
resolution: "use-sync-external-store@npm:1.2.0" resolution: "use-sync-external-store@npm:1.2.0"