1
mirror of https://github.com/thepeacockproject/Peacock synced 2024-11-16 11:03:30 +01:00

feat: official -> peacock progression transfer (#426)

* 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

View File

@ -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

View File

@ -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,
profileData.Sublocations[contract.Metadata.Location] ??= {
Xp: 0,
ActionXp: 0,
}
profileData.Sublocations.push(foundSubLocation)
}
foundSubLocation.Xp += masteryXp
foundSubLocation.ActionXp += actionXp
profileData.Sublocations[contract.Metadata.Location].Xp += masteryXp
profileData.Sublocations[contract.Metadata.Location].ActionXp +=
actionXp
return true
}

View File

@ -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,

View File

@ -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
}

View File

@ -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<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/
menuDataRouter.get(
@ -1400,25 +1413,12 @@ menuDataRouter.get(
"/GetMasteryCompletionDataForUnlockable",
// @ts-expect-error Has jwt props.
(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({
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",

View File

@ -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 &&

View File

@ -511,10 +511,11 @@ export type UserProfile = {
*/
Total: number
Sublocations: {
Location: string
[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
}

View File

@ -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<void> {
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<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
}

View File

@ -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<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 }

View File

@ -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.

View File

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

View File

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

View File

@ -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
}

View File

@ -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
}

View File

@ -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",

View File

@ -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;
}

View File

@ -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: <DevToolsPage />,
}
: null!,
{
path: "ui/transfer",
element: <TransferPage />,
},
{
path: "ui/loadouts",
element: <LoadoutPage />,

View File

@ -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(" ")
}

View File

@ -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({
<nav className="pagination-nav">
{loadoutsForVersion.map((loadout) => {
const isActive =
// @ts-expect-error also fake news
data[`h${gameVersion}`].selected === loadout.id
return (

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

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

View File

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

View File

@ -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>
</>
)
}

View File

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

449
yarn.lock
View File

@ -44,6 +44,15 @@ __metadata:
languageName: node
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":
version: 1.6.0
resolution: "@colors/colors@npm:1.6.0"
@ -570,6 +579,7 @@ __metadata:
version: 0.0.0-use.local
resolution: "@peacockproject/web-ui@workspace:webui"
dependencies:
"@radix-ui/react-alert-dialog": "npm:^1.0.5"
"@types/react": "npm:^18.2.74"
"@types/react-dom": "npm:^18.2.23"
axios: "npm:^1.6.8"
@ -606,6 +616,319 @@ __metadata:
languageName: node
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":
version: 3.0.0
resolution: "@rdil/parallel-prettier@npm:3.0.0"
@ -1328,6 +1651,15 @@ __metadata:
languageName: node
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":
version: 1.0.2
resolution: "array-find-index@npm:1.0.2"
@ -1858,6 +2190,13 @@ __metadata:
languageName: node
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":
version: 29.6.3
resolution: "diff-sequences@npm:29.6.3"
@ -2603,6 +2942,13 @@ __metadata:
languageName: node
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":
version: 8.0.1
resolution: "get-stream@npm:8.0.1"
@ -2907,6 +3253,15 @@ __metadata:
languageName: node
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":
version: 2.0.0
resolution: "ip@npm:2.0.0"
@ -3349,7 +3704,7 @@ __metadata:
languageName: node
linkType: hard
"loose-envify@npm:^1.1.0":
"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0":
version: 1.4.0
resolution: "loose-envify@npm:1.4.0"
dependencies:
@ -4235,6 +4590,41 @@ __metadata:
languageName: node
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":
version: 6.22.3
resolution: "react-router-dom@npm:6.22.3"
@ -4259,6 +4649,23 @@ __metadata:
languageName: node
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":
version: 18.2.0
resolution: "react@npm:18.2.0"
@ -4294,6 +4701,13 @@ __metadata:
languageName: node
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":
version: 1.1.0
resolution: "remove-trailing-separator@npm:1.1.0"
@ -5068,7 +5482,7 @@ __metadata:
languageName: node
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
resolution: "tslib@npm:2.6.2"
checksum: 10/bd26c22d36736513980091a1e356378e8b662ded04204453d353a7f34a4c21ed0afc59b5f90719d4ba756e581a162ecbf93118dc9c6be5acf70aa309188166ca
@ -5200,6 +5614,37 @@ __metadata:
languageName: node
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":
version: 1.2.0
resolution: "use-sync-external-store@npm:1.2.0"