Refactored airdrop loot code

This commit is contained in:
Dev 2024-07-06 13:39:56 +01:00
parent 4cf1c10c51
commit 1f76ce5d10
8 changed files with 193 additions and 138 deletions

View File

@ -2,55 +2,42 @@ import { inject, injectable } from "tsyringe";
import { ApplicationContext } from "@spt/context/ApplicationContext";
import { ContextVariableType } from "@spt/context/ContextVariableType";
import { LocationGenerator } from "@spt/generators/LocationGenerator";
import { LootGenerator } from "@spt/generators/LootGenerator";
import { WeightedRandomHelper } from "@spt/helpers/WeightedRandomHelper";
import { ILocationBase } from "@spt/models/eft/common/ILocationBase";
import { ILocationsGenerateAllResponse } from "@spt/models/eft/common/ILocationsSourceDestinationBase";
import { ILooseLoot, SpawnpointTemplate } from "@spt/models/eft/common/ILooseLoot";
import { IGetAirdropLootResponse } from "@spt/models/eft/location/IGetAirdropLootResponse";
import { IGetLocationRequestData } from "@spt/models/eft/location/IGetLocationRequestData";
import { AirdropTypeEnum } from "@spt/models/enums/AirdropType";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { AirdropLoot, IAirdropConfig } from "@spt/models/spt/config/IAirdropConfig";
import { ILocationConfig } from "@spt/models/spt/config/ILocationConfig";
import { IRaidChanges } from "@spt/models/spt/location/IRaidChanges";
import { ILocations } from "@spt/models/spt/server/ILocations";
import { LootRequest } from "@spt/models/spt/services/LootRequest";
import { ILogger } from "@spt/models/spt/utils/ILogger";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { AirdropService } from "@spt/services/AirdropService";
import { DatabaseService } from "@spt/services/DatabaseService";
import { ItemFilterService } from "@spt/services/ItemFilterService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { RaidTimeAdjustmentService } from "@spt/services/RaidTimeAdjustmentService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HashUtil } from "@spt/utils/HashUtil";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { TimeUtil } from "@spt/utils/TimeUtil";
@injectable()
export class LocationController
{
protected airdropConfig: IAirdropConfig;
protected locationConfig: ILocationConfig;
constructor(
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper,
@inject("PrimaryLogger") protected logger: ILogger,
@inject("LocationGenerator") protected locationGenerator: LocationGenerator,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("RaidTimeAdjustmentService") protected raidTimeAdjustmentService: RaidTimeAdjustmentService,
@inject("ItemFilterService") protected itemFilterService: ItemFilterService,
@inject("LootGenerator") protected lootGenerator: LootGenerator,
@inject("DatabaseService") protected databaseService: DatabaseService,
@inject("AirdropService") protected airdropService: AirdropService,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("ApplicationContext") protected applicationContext: ApplicationContext,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{
this.airdropConfig = this.configServer.getConfig(ConfigTypes.AIRDROP);
this.locationConfig = this.configServer.getConfig(ConfigTypes.LOCATION);
}
@ -165,65 +152,8 @@ export class LocationController
return { locations: locations, paths: locationsFromDb.base.paths };
}
/**
* Handle client/location/getAirdropLoot
* Get loot for an airdrop container
* Generates it randomly based on config/airdrop.json values
* @returns Array of LootItem objects
*/
public getAirdropLoot(): IGetAirdropLootResponse
{
const airdropType = this.chooseAirdropType();
this.logger.debug(`Chose ${airdropType} for airdrop loot`);
const airdropConfig = this.getAirdropLootConfigByType(airdropType);
return { icon: airdropType, container: this.lootGenerator.createRandomLoot(airdropConfig) };
}
/**
* Randomly pick a type of airdrop loot using weighted values from config
* @returns airdrop type value
*/
protected chooseAirdropType(): AirdropTypeEnum
{
const possibleAirdropTypes = this.airdropConfig.airdropTypeWeightings;
return this.weightedRandomHelper.getWeightedValue(possibleAirdropTypes);
}
/**
* Get the configuration for a specific type of airdrop
* @param airdropType Type of airdrop to get settings for
* @returns LootRequest
*/
protected getAirdropLootConfigByType(airdropType: AirdropTypeEnum): LootRequest
{
let lootSettingsByType: AirdropLoot = this.airdropConfig.loot[airdropType];
if (!lootSettingsByType)
{
this.logger.error(
this.localisationService.getText("location-unable_to_find_airdrop_drop_config_of_type", airdropType),
);
lootSettingsByType = this.airdropConfig.loot[AirdropTypeEnum.COMMON];
}
return {
airdropLoot: airdropType,
weaponPresetCount: lootSettingsByType.weaponPresetCount,
armorPresetCount: lootSettingsByType.armorPresetCount,
itemCount: lootSettingsByType.itemCount,
weaponCrateCount: lootSettingsByType.weaponCrateCount,
itemBlacklist: [
...lootSettingsByType.itemBlacklist,
...this.itemFilterService.getItemRewardBlacklist(),
...this.itemFilterService.getBossItems(),
],
itemTypeWhitelist: lootSettingsByType.itemTypeWhitelist,
itemLimits: lootSettingsByType.itemLimits,
itemStackLimits: lootSettingsByType.itemStackLimits,
armorLevelWhitelist: lootSettingsByType.armorLevelWhitelist,
allowBossItems: lootSettingsByType.allowBossItems,
};
return this.airdropService.generateAirdropLoot();
}
}

View File

@ -87,7 +87,7 @@ export class ProfileController
if (!pmc?.Info?.Level)
{
return {
username: profile.info.username,
username: profile.info?.username ?? "",
nickname: "unknown",
side: "unknown",
currlvl: 0,

View File

@ -197,6 +197,7 @@ import { IWebSocketConnectionHandler } from "@spt/servers/ws/IWebSocketConnectio
import { DefaultSptWebSocketMessageHandler } from "@spt/servers/ws/message/DefaultSptWebSocketMessageHandler";
import { ISptWebSocketMessageHandler } from "@spt/servers/ws/message/ISptWebSocketMessageHandler";
import { SptWebSocketConnectionHandler } from "@spt/servers/ws/SptWebSocketConnectionHandler";
import { AirdropService } from "@spt/services/AirdropService";
import { BotEquipmentFilterService } from "@spt/services/BotEquipmentFilterService";
import { BotEquipmentModPoolService } from "@spt/services/BotEquipmentModPoolService";
import { BotGenerationCacheService } from "@spt/services/BotGenerationCacheService";
@ -795,6 +796,9 @@ export class Container
depContainer.register<ProfileActivityService>("ProfileActivityService", ProfileActivityService, {
lifecycle: Lifecycle.Singleton,
});
depContainer.register<AirdropService>("AirdropService", AirdropService, {
lifecycle: Lifecycle.Singleton,
});
}
private static registerServers(depContainer: DependencyContainer): void

View File

@ -8,7 +8,6 @@ import { Item } from "@spt/models/eft/common/tables/IItem";
import { ITemplateItem } from "@spt/models/eft/common/tables/ITemplateItem";
import { BaseClasses } from "@spt/models/enums/BaseClasses";
import { ISealedAirdropContainerSettings, RewardDetails } from "@spt/models/spt/config/IInventoryConfig";
import { LootItem } from "@spt/models/spt/services/LootItem";
import { LootRequest } from "@spt/models/spt/services/LootRequest";
import { ILogger } from "@spt/models/spt/utils/ILogger";
import { DatabaseService } from "@spt/services/DatabaseService";
@ -17,8 +16,6 @@ import { LocalisationService } from "@spt/services/LocalisationService";
import { RagfairLinkedItemService } from "@spt/services/RagfairLinkedItemService";
import { HashUtil } from "@spt/utils/HashUtil";
import { RandomUtil } from "@spt/utils/RandomUtil";
import { AirdropTypeEnum } from "@spt/models/enums/AirdropType";
import { ItemTpl } from "@spt/models/enums/ItemTpl";
type ItemLimit = { current: number, max: number };
@ -48,39 +45,6 @@ export class LootGenerator
public createRandomLoot(options: LootRequest): Item[]
{
const result: Item[] = [];
let airdropContainerParentID = "";
if (options.airdropLoot)
{
airdropContainerParentID = this.hashUtil.generate();
let airdropContainer = {
_id: airdropContainerParentID,
_tpl: "",
upd: {
SpawnedInSession: true,
StackObjectsCount: 1
}
}
switch (options.airdropLoot) {
case AirdropTypeEnum.MEDICAL:
airdropContainer._tpl = ItemTpl.LOOTCONTAINER_AIRDROP_MEDICAL_CRATE
break;
case AirdropTypeEnum.SUPPLY:
airdropContainer._tpl = ItemTpl.LOOTCONTAINER_AIRDROP_SUPPLY_CRATE
break;
case AirdropTypeEnum.WEAPON:
airdropContainer._tpl = ItemTpl.LOOTCONTAINER_AIRDROP_WEAPON_CRATE
break;
case AirdropTypeEnum.COMMON:
default:
airdropContainer._tpl = ItemTpl.LOOTCONTAINER_AIRDROP_COMMON_SUPPLY_CRATE
break;
}
result.push(airdropContainer);
}
const itemTypeCounts = this.initItemLimitCounter(options.itemLimits);
const itemsDb = this.databaseService.getItems();
@ -104,18 +68,18 @@ export class LootGenerator
}
// Handle sealed weapon containers
const desiredWeaponCrateCount = this.randomUtil.getInt(
const sealedWeaponCrateCount = this.randomUtil.getInt(
options.weaponCrateCount.min,
options.weaponCrateCount.max,
);
if (desiredWeaponCrateCount > 0)
if (sealedWeaponCrateCount > 0)
{
// Get list of all sealed containers from db
// Get list of all sealed containers from db - they're all the same, just for flavor
const sealedWeaponContainerPool = Object.values(itemsDb).filter((item) =>
item._name.includes("event_container_airdrop"),
);
for (let index = 0; index < desiredWeaponCrateCount; index++)
for (let index = 0; index < sealedWeaponCrateCount; index++)
{
// Choose one at random + add to results array
const chosenSealedContainer = this.randomUtil.getArrayValue(sealedWeaponContainerPool);
@ -124,8 +88,8 @@ export class LootGenerator
_tpl: chosenSealedContainer._id,
upd: {
StackObjectsCount: 1,
SpawnedInSession: true
}
SpawnedInSession: true,
},
});
}
}
@ -139,6 +103,7 @@ export class LootGenerator
&& options.itemTypeWhitelist.includes(item[1]._parent),
);
// Pool has items we could add as loot, proceed
if (items.length > 0)
{
const randomisedItemCount = this.randomUtil.getInt(options.itemCount.min, options.itemCount.max);
@ -221,19 +186,6 @@ export class LootGenerator
}
}
for (const item of result) {
if (item._id == airdropContainerParentID)
{
continue;
}
if (!item.parentId)
{
item.parentId = airdropContainerParentID;
item.slotId = "main"
}
}
return result;
}
@ -313,8 +265,8 @@ export class LootGenerator
_tpl: randomItem._id,
upd: {
StackObjectsCount: 1,
SpawnedInSession: true
}
SpawnedInSession: true,
},
};
// Special case - handle items that need a stackcount > 1
@ -421,8 +373,9 @@ export class LootGenerator
const presetAndMods: Item[] = this.itemHelper.replaceIDs(chosenPreset._items);
this.itemHelper.remapRootItemId(presetAndMods);
// Add chosen preset tpl to result array
presetAndMods.forEach(item => {
result.push(item)
presetAndMods.forEach((item) =>
{
result.push(item);
});
if (itemLimitCount)

View File

@ -3,5 +3,5 @@ export enum AirdropTypeEnum
COMMON = "common",
SUPPLY = "supply",
MEDICAL = "medical",
WEAPON = "weapon"
}
WEAPON = "weapon",
}

View File

@ -1,9 +1,7 @@
import { MinMax } from "@spt/models/common/MinMax";
import { AirdropTypeEnum } from "@spt/models/enums/AirdropType";
export interface LootRequest
{
airdropLoot?: AirdropTypeEnum
weaponPresetCount: MinMax
armorPresetCount: MinMax
itemCount: MinMax

View File

@ -3,6 +3,7 @@ import { LocationCallbacks } from "@spt/callbacks/LocationCallbacks";
import { RouteAction, StaticRouter } from "@spt/di/Router";
import { ILocationsGenerateAllResponse } from "@spt/models/eft/common/ILocationsSourceDestinationBase";
import { IGetBodyResponseData } from "@spt/models/eft/httpResponse/IGetBodyResponseData";
import { IGetAirdropLootResponse } from "@spt/models/eft/location/IGetAirdropLootResponse";
@injectable()
export class LocationStaticRouter extends StaticRouter
@ -24,7 +25,12 @@ export class LocationStaticRouter extends StaticRouter
),
new RouteAction(
"/client/airdrop/loot",
async (url: string, info: any, sessionID: string, _output: string): Promise<string> =>
async (
url: string,
info: any,
sessionID: string,
output: string,
): Promise<IGetBodyResponseData<IGetAirdropLootResponse>> =>
{
return this.locationCallbacks.getAirdropLoot(url, info, sessionID);
},

View File

@ -0,0 +1,164 @@
import { inject, injectable } from "tsyringe";
import { LootGenerator } from "@spt/generators/LootGenerator";
import { ItemHelper } from "@spt/helpers/ItemHelper";
import { WeightedRandomHelper } from "@spt/helpers/WeightedRandomHelper";
import { Item } from "@spt/models/eft/common/tables/IItem";
import { IGetAirdropLootResponse } from "@spt/models/eft/location/IGetAirdropLootResponse";
import { AirdropTypeEnum } from "@spt/models/enums/AirdropType";
import { ConfigTypes } from "@spt/models/enums/ConfigTypes";
import { ItemTpl } from "@spt/models/enums/ItemTpl";
import { AirdropLoot, IAirdropConfig } from "@spt/models/spt/config/IAirdropConfig";
import { LootRequest } from "@spt/models/spt/services/LootRequest";
import { ILogger } from "@spt/models/spt/utils/ILogger";
import { ConfigServer } from "@spt/servers/ConfigServer";
import { DatabaseService } from "@spt/services/DatabaseService";
import { ItemFilterService } from "@spt/services/ItemFilterService";
import { LocalisationService } from "@spt/services/LocalisationService";
import { ICloner } from "@spt/utils/cloners/ICloner";
import { HashUtil } from "@spt/utils/HashUtil";
@injectable()
export class AirdropService
{
protected airdropConfig: IAirdropConfig;
constructor(
@inject("PrimaryLogger") protected logger: ILogger,
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("ItemHelper") protected itemHelper: ItemHelper,
@inject("WeightedRandomHelper") protected weightedRandomHelper: WeightedRandomHelper,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ItemFilterService") protected itemFilterService: ItemFilterService,
@inject("LootGenerator") protected lootGenerator: LootGenerator,
@inject("DatabaseService") protected databaseService: DatabaseService,
@inject("ConfigServer") protected configServer: ConfigServer,
@inject("PrimaryCloner") protected cloner: ICloner,
)
{
this.airdropConfig = this.configServer.getConfig(ConfigTypes.AIRDROP);
}
/**
* Handle client/location/getAirdropLoot
* Get loot for an airdrop container
* Generates it randomly based on config/airdrop.json values
* @returns Array of LootItem objects
*/
public generateAirdropLoot(): IGetAirdropLootResponse
{
const airdropType = this.chooseAirdropType();
this.logger.debug(`Chose ${airdropType} for airdrop loot`);
// Common/weapon/etc
const airdropConfig = this.getAirdropLootConfigByType(airdropType);
// generate loot to put into airdrop crate
const crateLoot = this.lootGenerator.createRandomLoot(airdropConfig);
// Create airdrop crate and add to result in first spot
const airdropCrateItem = this.getAirdropCrateItem(airdropType);
// Add crate to front of array
crateLoot.unshift(airdropCrateItem);
// Reparent loot items to create we added above
for (const item of crateLoot)
{
if (item._id == airdropCrateItem._id)
{
// Crate itself, don't alter
continue;
}
// no parentId = root item, make item have create as parent
if (!item.parentId)
{
item.parentId = airdropCrateItem._id;
item.slotId = "main";
}
}
return { icon: airdropType, container: crateLoot };
}
/**
* Create a container create item based on passed in airdrop type
* @param airdropType What tpye of container: weapon/common etc
* @returns Item
*/
protected getAirdropCrateItem(airdropType: AirdropTypeEnum): Item
{
const airdropContainer = {
_id: this.hashUtil.generate(),
_tpl: "", // picked later
upd: {
SpawnedInSession: true,
StackObjectsCount: 1,
},
};
switch (airdropType)
{
case AirdropTypeEnum.MEDICAL:
airdropContainer._tpl = ItemTpl.LOOTCONTAINER_AIRDROP_MEDICAL_CRATE;
break;
case AirdropTypeEnum.SUPPLY:
airdropContainer._tpl = ItemTpl.LOOTCONTAINER_AIRDROP_SUPPLY_CRATE;
break;
case AirdropTypeEnum.WEAPON:
airdropContainer._tpl = ItemTpl.LOOTCONTAINER_AIRDROP_WEAPON_CRATE;
break;
case AirdropTypeEnum.COMMON:
default:
airdropContainer._tpl = ItemTpl.LOOTCONTAINER_AIRDROP_COMMON_SUPPLY_CRATE;
break;
}
return airdropContainer;
}
/**
* Randomly pick a type of airdrop loot using weighted values from config
* @returns airdrop type value
*/
protected chooseAirdropType(): AirdropTypeEnum
{
const possibleAirdropTypes = this.airdropConfig.airdropTypeWeightings;
return this.weightedRandomHelper.getWeightedValue(possibleAirdropTypes);
}
/**
* Get the configuration for a specific type of airdrop
* @param airdropType Type of airdrop to get settings for
* @returns LootRequest
*/
protected getAirdropLootConfigByType(airdropType: AirdropTypeEnum): LootRequest
{
let lootSettingsByType: AirdropLoot = this.airdropConfig.loot[airdropType];
if (!lootSettingsByType)
{
this.logger.error(
this.localisationService.getText("location-unable_to_find_airdrop_drop_config_of_type", airdropType),
);
lootSettingsByType = this.airdropConfig.loot[AirdropTypeEnum.COMMON];
}
return {
weaponPresetCount: lootSettingsByType.weaponPresetCount,
armorPresetCount: lootSettingsByType.armorPresetCount,
itemCount: lootSettingsByType.itemCount,
weaponCrateCount: lootSettingsByType.weaponCrateCount,
itemBlacklist: [
...lootSettingsByType.itemBlacklist,
...this.itemFilterService.getItemRewardBlacklist(),
...this.itemFilterService.getBossItems(),
],
itemTypeWhitelist: lootSettingsByType.itemTypeWhitelist,
itemLimits: lootSettingsByType.itemLimits,
itemStackLimits: lootSettingsByType.itemStackLimits,
armorLevelWhitelist: lootSettingsByType.armorLevelWhitelist,
allowBossItems: lootSettingsByType.allowBossItems,
};
}
}