Handle changes to airdrop endpoint

Reworked airdrop loot generation to incorporate 'radar' drops

Added `forced loot` system to LootGenerator service, can now explicitly define what items it contains

Fixed airdrop system sending incorrect icon to client
This commit is contained in:
Dev 2024-09-27 14:30:35 +01:00
parent 3bbe167450
commit 80762d6a00
8 changed files with 117 additions and 13 deletions

View File

@ -3,10 +3,12 @@
"mixed": 5, "mixed": 5,
"weaponArmor": 2, "weaponArmor": 2,
"foodMedical": 1, "foodMedical": 1,
"barter": 1 "barter": 1,
"radar": 0
}, },
"loot": { "loot": {
"mixed": { "mixed": {
"icon": "Common",
"weaponPresetCount": { "weaponPresetCount": {
"min": 3, "min": 3,
"max": 5 "max": 5
@ -108,6 +110,7 @@
"allowBossItems": false "allowBossItems": false
}, },
"weaponArmor": { "weaponArmor": {
"icon": "Weapon",
"weaponPresetCount": { "weaponPresetCount": {
"min": 6, "min": 6,
"max": 8 "max": 8
@ -180,6 +183,7 @@
"allowBossItems": false "allowBossItems": false
}, },
"foodMedical": { "foodMedical": {
"icon": "Medical",
"weaponPresetCount": { "weaponPresetCount": {
"min": 0, "min": 0,
"max": 0 "max": 0
@ -260,6 +264,7 @@
"allowBossItems": false "allowBossItems": false
}, },
"barter": { "barter": {
"icon": "Supply",
"weaponPresetCount": { "weaponPresetCount": {
"min": 0, "min": 0,
"max": 0 "max": 0
@ -339,6 +344,39 @@
}, },
"armorLevelWhitelist": [0], "armorLevelWhitelist": [0],
"allowBossItems": false "allowBossItems": false
} },
} "radar": {
"icon": "Supply",
"weaponPresetCount": {
"min": 0,
"max": 0
},
"armorPresetCount": {
"min": 0,
"max": 0
},
"itemCount": {
"min": 0,
"max": 0
},
"weaponCrateCount": {
"min": 0,
"max": 0
},
"itemBlacklist": [],
"itemTypeWhitelist": [],
"itemLimits": {},
"itemStackLimits": {},
"armorLevelWhitelist": [],
"allowBossItems": false,
"useForcedLoot": true,
"forcedLoot": {
"66d9f7256916142b3b02276e": {"min": 2, "max": 4 }
}
}
},
"customAirdropMapping": {
"66da1b49099cf6adcc07a36b": "radar",
"66da1b546916142b3b022777": "radar"
}
} }

View File

@ -2,6 +2,7 @@ import { LocationController } from "@spt/controllers/LocationController";
import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData"; import { IEmptyRequestData } from "@spt/models/eft/common/IEmptyRequestData";
import { ILocationsGenerateAllResponse } from "@spt/models/eft/common/ILocationsSourceDestinationBase"; import { ILocationsGenerateAllResponse } from "@spt/models/eft/common/ILocationsSourceDestinationBase";
import { IGetBodyResponseData } from "@spt/models/eft/httpResponse/IGetBodyResponseData"; import { IGetBodyResponseData } from "@spt/models/eft/httpResponse/IGetBodyResponseData";
import { IGetAirdropLootRequest } from "@spt/models/eft/location/IGetAirdropLootRequest";
import { IGetAirdropLootResponse } from "@spt/models/eft/location/IGetAirdropLootResponse"; import { IGetAirdropLootResponse } from "@spt/models/eft/location/IGetAirdropLootResponse";
import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil"; import { HttpResponseUtil } from "@spt/utils/HttpResponseUtil";
import { inject, injectable } from "tsyringe"; import { inject, injectable } from "tsyringe";
@ -25,9 +26,9 @@ export class LocationCallbacks {
/** Handle client/airdrop/loot */ /** Handle client/airdrop/loot */
public getAirdropLoot( public getAirdropLoot(
url: string, url: string,
info: IEmptyRequestData, info: IGetAirdropLootRequest,
sessionID: string, sessionID: string,
): IGetBodyResponseData<IGetAirdropLootResponse> { ): IGetBodyResponseData<IGetAirdropLootResponse> {
return this.httpResponse.getBody(this.locationController.getAirdropLoot()); return this.httpResponse.getBody(this.locationController.getAirdropLoot(info));
} }
} }

View File

@ -1,4 +1,5 @@
import { ILocationsGenerateAllResponse } from "@spt/models/eft/common/ILocationsSourceDestinationBase"; import { ILocationsGenerateAllResponse } from "@spt/models/eft/common/ILocationsSourceDestinationBase";
import { IGetAirdropLootRequest } from "@spt/models/eft/location/IGetAirdropLootRequest";
import { IGetAirdropLootResponse } from "@spt/models/eft/location/IGetAirdropLootResponse"; import { IGetAirdropLootResponse } from "@spt/models/eft/location/IGetAirdropLootResponse";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes"; import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { ILocationConfig } from "@spt/models/spt/config/ILocationConfig"; import { ILocationConfig } from "@spt/models/spt/config/ILocationConfig";
@ -50,7 +51,11 @@ export class LocationController {
} }
/** Handle client/airdrop/loot */ /** Handle client/airdrop/loot */
public getAirdropLoot(): IGetAirdropLootResponse { public getAirdropLoot(request: IGetAirdropLootRequest): IGetAirdropLootResponse {
if (request.containerId) {
return this.airdropService.generateCustomAirdropLoot(request);
}
return this.airdropService.generateAirdropLoot(); return this.airdropService.generateAirdropLoot();
} }
} }

View File

@ -153,6 +153,32 @@ export class LootGenerator {
return result; return result;
} }
public createForcedLoot(airdropConfig: LootRequest) {
const result: IItem[] = [];
const forcedItems = Object.keys(airdropConfig.forcedLoot);
for (const tpl of forcedItems) {
const details = airdropConfig.forcedLoot[tpl];
const randomisedItemCount = this.randomUtil.getInt(details.min, details.max);
// Add forced loot item to result
const newLootItem: IItem = {
_id: this.hashUtil.generate(),
_tpl: tpl,
upd: {
StackObjectsCount: randomisedItemCount,
SpawnedInSession: true,
},
};
const splitResults = this.itemHelper.splitStack(newLootItem);
result.push(...splitResults);
}
return result;
}
protected getItemRewardPool( protected getItemRewardPool(
itemTplBlacklist: string[], itemTplBlacklist: string[],
itemTypeWhitelist: string[], itemTypeWhitelist: string[],

View File

@ -3,4 +3,5 @@ export enum AirdropTypeEnum {
SUPPLY = "barter", SUPPLY = "barter",
FOOD_MEDICAL = "foodMedical", FOOD_MEDICAL = "foodMedical",
WEAPON_ARMOR = "weaponArmor", WEAPON_ARMOR = "weaponArmor",
RADAR = "radar",
} }

View File

@ -7,6 +7,7 @@ export interface IAirdropConfig extends IBaseConfig {
airdropTypeWeightings: Record<AirdropTypeEnum, number>; airdropTypeWeightings: Record<AirdropTypeEnum, number>;
/** What rewards will the loot crate contain, keyed by drop type e.g. mixed/weaponArmor/foodMedical/barter */ /** What rewards will the loot crate contain, keyed by drop type e.g. mixed/weaponArmor/foodMedical/barter */
loot: Record<string, IAirdropLoot>; loot: Record<string, IAirdropLoot>;
customAirdropMapping: Record<string, AirdropTypeEnum>;
} }
/** Chance map will have an airdrop occur out of 100 - locations not included count as 0% */ /** Chance map will have an airdrop occur out of 100 - locations not included count as 0% */
@ -23,6 +24,7 @@ export interface IAirdropChancePercent {
/** Loot inside crate */ /** Loot inside crate */
export interface IAirdropLoot { export interface IAirdropLoot {
icon: AirdropTypeEnum;
/** Min/max of weapons inside crate */ /** Min/max of weapons inside crate */
weaponPresetCount?: MinMax; weaponPresetCount?: MinMax;
/** Min/max of armors (head/chest/rig) inside crate */ /** Min/max of armors (head/chest/rig) inside crate */
@ -43,4 +45,6 @@ export interface IAirdropLoot {
armorLevelWhitelist?: number[]; armorLevelWhitelist?: number[];
/** Should boss items be added to airdrop crate */ /** Should boss items be added to airdrop crate */
allowBossItems: boolean; allowBossItems: boolean;
useForcedLoot?: boolean;
forcedLoot?: Record<string, MinMax>;
} }

View File

@ -1,4 +1,5 @@
import { MinMax } from "@spt/models/common/MinMax"; import { MinMax } from "@spt/models/common/MinMax";
import { AirdropTypeEnum } from "@spt/models/enums/AirdropType";
export interface LootRequest { export interface LootRequest {
weaponPresetCount: MinMax; weaponPresetCount: MinMax;
@ -13,4 +14,10 @@ export interface LootRequest {
armorLevelWhitelist: number[]; armorLevelWhitelist: number[];
allowBossItems: boolean; allowBossItems: boolean;
useRewarditemBlacklist?: boolean; useRewarditemBlacklist?: boolean;
useForcedLoot?: boolean;
forcedLoot?: Record<string, MinMax>;
}
export interface IAirdropLootRequest extends LootRequest {
icon?: AirdropTypeEnum;
} }

View File

@ -2,12 +2,13 @@ import { LootGenerator } from "@spt/generators/LootGenerator";
import { ItemHelper } from "@spt/helpers/ItemHelper"; import { ItemHelper } from "@spt/helpers/ItemHelper";
import { WeightedRandomHelper } from "@spt/helpers/WeightedRandomHelper"; import { WeightedRandomHelper } from "@spt/helpers/WeightedRandomHelper";
import { IItem } from "@spt/models/eft/common/tables/IItem"; import { IItem } from "@spt/models/eft/common/tables/IItem";
import { IGetAirdropLootRequest } from "@spt/models/eft/location/IGetAirdropLootRequest";
import { IGetAirdropLootResponse } from "@spt/models/eft/location/IGetAirdropLootResponse"; import { IGetAirdropLootResponse } from "@spt/models/eft/location/IGetAirdropLootResponse";
import { AirdropTypeEnum } from "@spt/models/enums/AirdropType"; import { AirdropTypeEnum } from "@spt/models/enums/AirdropType";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes"; import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { ItemTpl } from "@spt/models/enums/ItemTpl"; import { ItemTpl } from "@spt/models/enums/ItemTpl";
import { IAirdropConfig, IAirdropLoot } from "@spt/models/spt/config/IAirdropConfig"; import { IAirdropConfig, IAirdropLoot } from "@spt/models/spt/config/IAirdropConfig";
import { LootRequest } from "@spt/models/spt/services/LootRequest"; import { IAirdropLootRequest, LootRequest } from "@spt/models/spt/services/LootRequest";
import { ILogger } from "@spt/models/spt/utils/ILogger"; import { ILogger } from "@spt/models/spt/utils/ILogger";
import { ConfigServer } from "@spt/servers/ConfigServer"; import { ConfigServer } from "@spt/servers/ConfigServer";
import { DatabaseService } from "@spt/services/DatabaseService"; import { DatabaseService } from "@spt/services/DatabaseService";
@ -36,21 +37,36 @@ export class AirdropService {
this.airdropConfig = this.configServer.getConfig(ConfigTypes.AIRDROP); this.airdropConfig = this.configServer.getConfig(ConfigTypes.AIRDROP);
} }
public generateCustomAirdropLoot(request: IGetAirdropLootRequest): IGetAirdropLootResponse {
const customAirdropInformation = this.airdropConfig.customAirdropMapping[request.containerId];
if (!customAirdropInformation) {
this.logger.warning(
`Unable to find data for custom airdrop ${request.containerId}, returning random airdrop instead`,
);
return this.generateAirdropLoot();
}
return this.generateAirdropLoot(customAirdropInformation);
}
/** /**
* Handle client/location/getAirdropLoot * Handle client/location/getAirdropLoot
* Get loot for an airdrop container * Get loot for an airdrop container
* Generates it randomly based on config/airdrop.json values * Generates it randomly based on config/airdrop.json values
* @returns Array of LootItem objects * @returns Array of LootItem objects
*/ */
public generateAirdropLoot(): IGetAirdropLootResponse { public generateAirdropLoot(forcedAirdropType = null): IGetAirdropLootResponse {
const airdropType = this.chooseAirdropType(); const airdropType = forcedAirdropType ? forcedAirdropType : this.chooseAirdropType();
this.logger.debug(`Chose ${airdropType} for airdrop loot`); this.logger.debug(`Chose ${airdropType} for airdrop loot`);
// Common/weapon/etc // Common/weapon/etc
const airdropConfig = this.getAirdropLootConfigByType(airdropType); const airdropConfig = this.getAirdropLootConfigByType(airdropType);
// generate loot to put into airdrop crate // generate loot to put into airdrop crate
const crateLoot = this.lootGenerator.createRandomLoot(airdropConfig); const crateLoot = airdropConfig.useForcedLoot
? this.lootGenerator.createForcedLoot(airdropConfig)
: this.lootGenerator.createRandomLoot(airdropConfig);
// Create airdrop crate and add to result in first spot // Create airdrop crate and add to result in first spot
const airdropCrateItem = this.getAirdropCrateItem(airdropType); const airdropCrateItem = this.getAirdropCrateItem(airdropType);
@ -58,7 +74,7 @@ export class AirdropService {
// Add crate to front of array // Add crate to front of array
crateLoot.unshift(airdropCrateItem); crateLoot.unshift(airdropCrateItem);
// Reparent loot items to create we added above // Reparent loot items to crate we added above
for (const item of crateLoot) { for (const item of crateLoot) {
if (item._id === airdropCrateItem._id) { if (item._id === airdropCrateItem._id) {
// Crate itself, don't alter // Crate itself, don't alter
@ -72,7 +88,7 @@ export class AirdropService {
} }
} }
return { icon: airdropType, container: crateLoot }; return { icon: airdropConfig.icon, container: crateLoot };
} }
/** /**
@ -103,6 +119,9 @@ export class AirdropService {
case AirdropTypeEnum.COMMON: case AirdropTypeEnum.COMMON:
airdropContainer._tpl = ItemTpl.LOOTCONTAINER_AIRDROP_COMMON_SUPPLY_CRATE; airdropContainer._tpl = ItemTpl.LOOTCONTAINER_AIRDROP_COMMON_SUPPLY_CRATE;
break; break;
case AirdropTypeEnum.RADAR:
airdropContainer._tpl = ItemTpl.LOOTCONTAINER_AIRDROP_TECHNICAL_SUPPLY_CRATE_EVENT_1;
break;
default: default:
airdropContainer._tpl = ItemTpl.LOOTCONTAINER_AIRDROP_COMMON_SUPPLY_CRATE; airdropContainer._tpl = ItemTpl.LOOTCONTAINER_AIRDROP_COMMON_SUPPLY_CRATE;
break; break;
@ -126,7 +145,7 @@ export class AirdropService {
* @param airdropType Type of airdrop to get settings for * @param airdropType Type of airdrop to get settings for
* @returns LootRequest * @returns LootRequest
*/ */
protected getAirdropLootConfigByType(airdropType: AirdropTypeEnum): LootRequest { protected getAirdropLootConfigByType(airdropType: AirdropTypeEnum): IAirdropLootRequest {
let lootSettingsByType: IAirdropLoot = this.airdropConfig.loot[airdropType]; let lootSettingsByType: IAirdropLoot = this.airdropConfig.loot[airdropType];
if (!lootSettingsByType) { if (!lootSettingsByType) {
this.logger.error( this.logger.error(
@ -136,6 +155,7 @@ export class AirdropService {
} }
return { return {
icon: lootSettingsByType.icon,
weaponPresetCount: lootSettingsByType.weaponPresetCount, weaponPresetCount: lootSettingsByType.weaponPresetCount,
armorPresetCount: lootSettingsByType.armorPresetCount, armorPresetCount: lootSettingsByType.armorPresetCount,
itemCount: lootSettingsByType.itemCount, itemCount: lootSettingsByType.itemCount,
@ -150,6 +170,8 @@ export class AirdropService {
itemStackLimits: lootSettingsByType.itemStackLimits, itemStackLimits: lootSettingsByType.itemStackLimits,
armorLevelWhitelist: lootSettingsByType.armorLevelWhitelist, armorLevelWhitelist: lootSettingsByType.armorLevelWhitelist,
allowBossItems: lootSettingsByType.allowBossItems, allowBossItems: lootSettingsByType.allowBossItems,
useForcedLoot: lootSettingsByType.useForcedLoot,
forcedLoot: lootSettingsByType.forcedLoot,
}; };
} }
} }