1
mirror of https://github.com/thepeacockproject/Peacock synced 2024-11-29 09:15:11 +01:00

feat: Add API to simplify creation of global challenges (#459)

* feat: Add API to simplify creation of global challenges

* feat: Finalize global challenge API
style: Renamed `globalGroups` into `globalMergeGroups` to clarify function.
docs: Provided docs on new API
refactor: Moved classic, elusive and H1/H2 escalations into globalMergeGroups
feat: Added `challengeService.registerChallengeList` for easier challenge group registration
feat: Added `challengeService.hasGroup` method to check if group was already registered.
This commit is contained in:
Pavel Alexandrov 2024-06-03 19:40:15 +03:00 committed by GitHub
parent 4dbc0b39de
commit a306cc5643
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 145 additions and 66 deletions

View File

@ -12,6 +12,6 @@ Thanks to Moo for the name suggestion.
Challenge data reside in files in `contractdata` ending in `_CHALLENGES.json`. When building, the `packResources` function in `buildTasks.mjs` will take all such files and pack them into files in the `resources/challenges` folder.
Then, when the server starts, challenge data is loaded from the files in the `resources/challenges` folder into the memory, via the `_loadResources` function in `controller.ts`. It calls the `_handleChallengeResources` function that in turn calls `challengeService.registerGroup` and `challengeService.registerChallenge` to register the challenge group and the respective challenges.
Then, when the server starts, challenge data is loaded from the files in the `resources/challenges` folder into the memory, via the `_loadResources` function in `controller.ts`. It calls the `_handleChallengeResources` function that in turn calls `challengeService.registerGroup` and `challengeService.registerChallengeList` to register the challenge group and the respective challenges.
These two registration functions initialize two data structures, `groups: Map<string, Map<string, SavedChallengeGroup>>`, which maps `parentLocationId`s to `Map`s of `groupId`s to `SavedChallengeGroup` objects for this group of this parent location, and `groupContents: Map<string, Map<string, Set<string>>>`, which maps `parentLocationId`s to `Map`s of `groupId`s to `Set`s of challenge Ids in this group of this parent location. All subsequent operations on challenges write to or read from these two `Map`s.

View File

@ -87,6 +87,38 @@ export type ChallengePack = {
Icon: string
}
export type GlobalChallengeGroup = {
/**
* ID of a challenge group that will have location and global challenges merged.
*
* Not necessarily should match the group it's being merged with,
* but it's highly advised to match them.
*/
groupId: string
/**
* The global challenge group location ID from where the global challenges are merged.
*/
location: string
/**
* Which game versions are supported by this global challenge group.
*/
gameVersions: GameVersion[]
/**
* If set, this global challenge group will be visible in all locations,
* regardless if this group has challenges in that location or not.
*
* This option is useful when challenge group has to be active for all locations,
* but doesn't have any location-specific challenges.
* However it's advised to create location-specific challenges instead,
* as this option will apply the challenge group to all locations and missions,
* including Freelancer and Sniper modes.
*
* This can be alleviated by providing valid InclusionData filter to challenges,
* in order to exclude them from locations you do not wish them to be in.
*/
allLocations?: boolean
}
/**
* A base class providing challenge registration support.
*/
@ -186,16 +218,56 @@ export abstract class ChallengeRegistry {
],
])
/**
* The list of user-made challenge groups that span multiple locations
* and should merge their global type challenges merged with location challenges.
*
* @see GlobalChallengeGroup fields for more information.
*/
public globalMergeGroups: Map<string, GlobalChallengeGroup> = new Map([
[
"classic",
{
gameVersions: ["h1", "h2", "h3", "scpc"],
groupId: "classic",
location: "GLOBAL_CLASSIC_CHALLENGES",
},
],
[
"elusive",
{
gameVersions: ["h1", "h2", "h3", "scpc"],
groupId: "elusive",
location: "GLOBAL_ELUSIVES_CHALLENGES",
},
],
[
// H2 & H1 have the escalation challenges in "feats"
"feats",
{
gameVersions: ["h1", "h2", "scpc"],
groupId: "feats",
location: "GLOBAL_ESCALATION_CHALLENGES",
},
],
])
registerChallenge(
challenge: RegistryChallenge,
groupId: string,
location: string,
gameVersion: GameVersion,
): void {
this.registerChallengeList([challenge], groupId, location, gameVersion)
}
registerChallengeList(
challenges: RegistryChallenge[],
groupId: string,
location: string,
gameVersion: GameVersion,
): void {
const gameChallenges = this.groupContents[gameVersion]
challenge.inGroup = groupId
challenge.inLocation = location
this.challenges[gameVersion].set(challenge.Id, challenge)
if (!gameChallenges.has(location)) {
gameChallenges.set(location, new Map())
@ -208,9 +280,14 @@ export abstract class ChallengeRegistry {
}
const set = locationMap.get(groupId)!
set.add(challenge.Id)
this.checkHeuristics(challenge, gameVersion)
for (const challenge of challenges) {
challenge.inGroup = groupId
challenge.inLocation = location
this.challenges[gameVersion].set(challenge.Id, challenge)
set.add(challenge.Id)
this.checkHeuristics(challenge, gameVersion)
}
}
registerGroup(
@ -227,6 +304,23 @@ export abstract class ChallengeRegistry {
gameGroups.get(location)?.set(group.CategoryId, group)
}
/**
* Check if `groupId` is already registered for given `location` and `gameVersion`.
*
* @param groupId The group ID to check
* @param location The location group belongs to
* @param gameVersion The game version group works in
*/
hasGroup(
groupId: string,
location: string,
gameVersion: GameVersion,
): boolean {
return (
this.groups[gameVersion]?.get(location)?.get(groupId) !== undefined
)
}
getChallengeById(
challengeId: string,
gameVersion: GameVersion,
@ -290,18 +384,6 @@ export abstract class ChallengeRegistry {
const mainGroup = gameGroups.get(location)?.get(groupId)
if (groupId === "feats" && gameVersion !== "h3") {
if (!mainGroup) {
// emergency bailout - shouldn't happen in practice
return undefined
}
return mergeSavedChallengeGroups(
mainGroup,
gameGroups.get("GLOBAL_ESCALATION_CHALLENGES")?.get(groupId),
)
}
if (groupId?.includes("featured")) {
return gameGroups.get("GLOBAL_FEATURED_CHALLENGES")?.get(groupId)
}
@ -314,32 +396,24 @@ export abstract class ChallengeRegistry {
return gameGroups.get("GLOBAL_ESCALATION_CHALLENGES")?.get(groupId)
}
// Included by default. Filtered later.
if (groupId === "classic" && location !== "GLOBAL_CLASSIC_CHALLENGES") {
if (!mainGroup) {
// emergency bailout - shouldn't happen in practice
return undefined
}
// Global merge groups are included by default. Filtered later.
return mergeSavedChallengeGroups(
mainGroup,
gameGroups.get("GLOBAL_CLASSIC_CHALLENGES")?.get(groupId),
)
}
const globalGroup = this.globalMergeGroups.get(groupId)
if (
groupId === "elusive" &&
location !== "GLOBAL_ELUSIVES_CHALLENGES"
globalGroup &&
location !== globalGroup.location &&
globalGroup.gameVersions.includes(gameVersion)
) {
const globalGroupChallenges = gameGroups
.get(globalGroup.location)
?.get(globalGroup.groupId)
if (!mainGroup) {
// emergency bailout - shouldn't happen in practice
return undefined
return globalGroupChallenges
}
return mergeSavedChallengeGroups(
mainGroup,
gameGroups.get("GLOBAL_ELUSIVES_CHALLENGES")?.get(groupId),
)
return mergeSavedChallengeGroups(mainGroup, globalGroupChallenges)
}
return mainGroup
@ -363,15 +437,6 @@ export abstract class ChallengeRegistry {
): Set<string> | undefined {
const gameChalGC = this.groupContents[gameVersion]
if (groupId === "feats" && gameVersion !== "h3") {
return new Set([
...(gameChalGC.get(location)?.get(groupId) ?? []),
...(gameChalGC
.get("GLOBAL_ESCALATION_CHALLENGES")
?.get(groupId) ?? []),
])
}
if (groupId?.includes("featured")) {
return gameChalGC.get("GLOBAL_FEATURED_CHALLENGES")?.get(groupId)
}
@ -384,24 +449,20 @@ export abstract class ChallengeRegistry {
return gameChalGC.get("GLOBAL_ESCALATION_CHALLENGES")?.get(groupId)
}
// Included by default. Filtered later.
if (groupId === "classic" && location !== "GLOBAL_CLASSIC_CHALLENGES") {
return new Set([
...(gameChalGC.get(location)?.get(groupId) ?? []),
...(gameChalGC.get("GLOBAL_CLASSIC_CHALLENGES")?.get(groupId) ??
[]),
])
}
// Global merge groups are included by default. Filtered later.
const globalGroup = this.globalMergeGroups.get(groupId)
if (
groupId === "elusive" &&
location !== "GLOBAL_ELUSIVES_CHALLENGES"
globalGroup &&
globalGroup.location !== location &&
globalGroup.gameVersions.includes(gameVersion)
) {
return new Set([
...(gameChalGC.get(location)?.get(groupId) ?? []),
...(gameChalGC
.get("GLOBAL_ELUSIVES_CHALLENGES")
?.get(groupId) ?? []),
.get(globalGroup.location)
?.get(globalGroup.groupId) ?? []),
])
}
@ -705,6 +766,26 @@ export class ChallengeService extends ChallengeRegistry {
gameVersion,
)
}
// Handle merge gropus with `allLocations` flag.
// Should apply only if location has no challenges in that group,
// as those are merged in `getGroupContentByIdLoc` already.
for (const globalGroup of this.globalMergeGroups.values()) {
if (
globalGroup.allLocations &&
globalGroup.gameVersions.includes(gameVersion) &&
this.groups[gameVersion]
.get(location)
?.get(globalGroup.groupId) === undefined
) {
this.getGroupedChallengesByLoc(
filter,
globalGroup.location,
challenges,
gameVersion,
)
}
}
}
// remove empty groups

View File

@ -959,14 +959,12 @@ export class Controller {
version,
)
for (const challenge of group.Challenges) {
this.challengeService.registerChallenge(
challenge,
group.CategoryId,
data.meta.Location,
version,
)
}
this.challengeService.registerChallengeList(
group.Challenges,
group.CategoryId,
data.meta.Location,
version,
)
}
}
}