From 734d8216304af37e537918dac1dfa57daff5206b Mon Sep 17 00:00:00 2001 From: Dev Date: Sat, 27 Jan 2024 18:12:13 +0000 Subject: [PATCH] Improve mod item filtering code --- .../generators/BotEquipmentModGenerator.ts | 208 +++++++++--------- project/src/generators/BotWeaponGenerator.ts | 31 +-- project/src/helpers/BotGeneratorHelper.ts | 20 +- .../bots/IChooseRandomCompatibleModResult.ts | 8 + 4 files changed, 143 insertions(+), 124 deletions(-) create mode 100644 project/src/models/spt/bots/IChooseRandomCompatibleModResult.ts diff --git a/project/src/generators/BotEquipmentModGenerator.ts b/project/src/generators/BotEquipmentModGenerator.ts index 380a9ce0..7801da91 100644 --- a/project/src/generators/BotEquipmentModGenerator.ts +++ b/project/src/generators/BotEquipmentModGenerator.ts @@ -14,6 +14,7 @@ import { ITemplateItem, Slot } from "@spt-aki/models/eft/common/tables/ITemplate import { BaseClasses } from "@spt-aki/models/enums/BaseClasses"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { ModSpawn } from "@spt-aki/models/enums/ModSpawn"; +import { IChooseRandomCompatibleModResult } from "@spt-aki/models/spt/bots/IChooseRandomCompatibleModResult"; import { EquipmentFilterDetails, IBotConfig } from "@spt-aki/models/spt/config/IBotConfig"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { ConfigServer } from "@spt-aki/servers/ConfigServer"; @@ -735,9 +736,6 @@ export class BotEquipmentModGenerator modSpawnResult: ModSpawn ): [boolean, ITemplateItem] { - /** Chosen mod tpl */ - let modTpl: string; - let found = false; /** Slot mod will fill */ const parentSlot = parentTemplate._props.Slots.find((i) => i._name === modSlot); const weaponTemplate = this.itemHelper.getItem(weapon[0]._tpl)[1]; @@ -745,93 +743,64 @@ export class BotEquipmentModGenerator // It's ammo, use predefined ammo parameter if (this.getAmmoContainers().includes(modSlot) && modSlot !== "mod_magazine") { - modTpl = ammoTpl; + return this.itemHelper.getItem(ammoTpl) } - else + + // Ensure there's a pool of mods to pick from + let modPool = this.getModPoolForSlot(itemModPool, modSpawnResult, parentTemplate, weaponTemplate, modSlot, botEquipBlacklist, isRandomisableSlot); + if (!(modPool || parentSlot._required)) { - // Ensure there's a pool of mods to pick from - let modPool = this.getModPoolForSlot(itemModPool, modSpawnResult, parentTemplate, weaponTemplate, modSlot, botEquipBlacklist, isRandomisableSlot); - if (!(modPool || parentSlot._required)) + // Nothing in mod pool + item not required + this.logger.debug( + `Mod pool for slot: ${modSlot} on item: ${parentTemplate._name} was empty, skipping mod`, + ); + return null; + } + + // Filter out non-whitelisted scopes, use full modpool if filtered pool would have no elements + if (modSlot.includes("mod_scope") && botWeaponSightWhitelist) + { + // scope pool has more than one scope + if (modPool.length > 1) { - // Nothing in mod pool + item not required - this.logger.debug( - `Mod pool for slot: ${modSlot} on item: ${parentTemplate._name} was empty, skipping mod`, + modPool = this.filterSightsByWeaponType( + weapon[0], + modPool, + botWeaponSightWhitelist, ); - return null; } + } + + // Pick random mod that's compatible + const chosenModResult = this.chooseRandomCompatibleModTpl(modPool, parentSlot, modSpawnResult, weapon, modSlot); - // Filter out non-whitelisted scopes, use full modpool if filtered pool would have no elements - if (modSlot.includes("mod_scope") && botWeaponSightWhitelist) - { - // scope pool has more than one scope - if (modPool.length > 1) - { - modPool = this.filterSightsByWeaponType( - weapon[0], - modPool, - botWeaponSightWhitelist, - ); - } - } - - // Pick random mod and check it's compatible - const exhaustableModPool = new ExhaustableArray(modPool, this.randomUtil, this.jsonUtil); - let modCompatibilityResult: { incompatible: boolean; reason: string; } = { - incompatible: false, - reason: "", - }; - const modParentFilterList = parentSlot._props.filters[0].Filter; - while (exhaustableModPool.hasValues()) - { - modTpl = exhaustableModPool.getRandomValue(); - if (modSpawnResult === ModSpawn.DEFAULT_MOD && modPool.length === 1) - { - // default mod wanted and only one choice - found = true; + if (chosenModResult.slotBlocked) + { + // Don't bother trying to fit mod, slot is completely blocked + return null; + } - break; - } - - modCompatibilityResult = this.botGeneratorHelper.isItemIncompatibleWithCurrentItems( - weapon, - modTpl, - modSlot, - ); - const isOnModParentFilterList = modParentFilterList.includes(modTpl); - if (!isOnModParentFilterList) - { - continue; - } - if (!modCompatibilityResult.incompatible && !this.weaponModComboIsIncompatible(weapon, modTpl)) - { - found = true; - - break; - } - } - - // Log mod chosen was incompatible - if (modCompatibilityResult.incompatible && parentSlot._required) - { - this.logger.debug(modCompatibilityResult.reason); - this.logger.debug(`Weapon: ${weapon.map(x => `${x._tpl} ${x.slotId ?? ""}`).join(",")}`) - } + // Log if mod chosen was incompatible + if (chosenModResult.incompatible && parentSlot._required) + { + this.logger.debug(chosenModResult.reason); + this.logger.debug(`Weapon: ${weapon.map(x => `${x._tpl} ${x.slotId ?? ""}`).join(",")}`) } // Get random mod to attach from items db for required slots if none found above - if (!found && parentSlot !== undefined && parentSlot._required) + if (!chosenModResult.found && parentSlot !== undefined && parentSlot._required) { - modTpl = this.getRandomModTplFromItemDb(modTpl, parentSlot, modSlot, weapon); - found = !!modTpl; + chosenModResult.chosenTpl = this.getRandomModTplFromItemDb("", parentSlot, modSlot, weapon); + chosenModResult.found = true; } // Compatible item not found + not required - if (!found && parentSlot !== undefined && !parentSlot._required) + if (!chosenModResult.found && parentSlot !== undefined && !parentSlot._required) { return null; } - if (!found && parentSlot !== undefined) + if (!chosenModResult.found && parentSlot !== undefined) { if (parentSlot._required) { @@ -845,7 +814,65 @@ export class BotEquipmentModGenerator return null; } - return this.itemHelper.getItem(modTpl); + return this.itemHelper.getItem(chosenModResult.chosenTpl); + } + + protected chooseRandomCompatibleModTpl( + modPool: string[], + parentSlot: Slot, + modSpawnResult: ModSpawn, + weapon: Item[], + modSlotname: string): IChooseRandomCompatibleModResult + { + let chosenTpl: string; + const exhaustableModPool = new ExhaustableArray(modPool, this.randomUtil, this.jsonUtil); + let chosenModResult: IChooseRandomCompatibleModResult = { + incompatible: true, + found: false, + reason: "unknown", + }; + const modParentFilterList = parentSlot._props.filters[0].Filter; + + while (exhaustableModPool.hasValues()) + { + chosenTpl = exhaustableModPool.getRandomValue(); + if (modSpawnResult === ModSpawn.DEFAULT_MOD && modPool.length === 1) + { + // Default mod wanted and only one choice in pool + chosenModResult.found = true; + chosenModResult.chosenTpl = chosenTpl; + + break; + } + + const isOnModParentFilterList = modParentFilterList.includes(chosenTpl); + if (!isOnModParentFilterList) + { + continue; + } + + chosenModResult = this.botGeneratorHelper.isItemIncompatibleWithCurrentItems( + weapon, + chosenTpl, + modSlotname, + ); + + if (chosenModResult.slotBlocked) + { + break; + } + + // Some mod combos will never work, make sure its not happened + if (!chosenModResult.incompatible && !this.weaponModComboIsIncompatible(weapon, chosenTpl)) + { + chosenModResult.found = true; + chosenModResult.chosenTpl = chosenTpl; + + break; + } + } + + return chosenModResult; } /** @@ -982,26 +1009,28 @@ export class BotEquipmentModGenerator /** * Log errors if mod is not compatible with slot * @param modToAdd template of mod to check - * @param itemSlot slot the item will be placed in + * @param slotAddedToTemplate slot the item will be placed in * @param modSlot slot the mod will fill - * @param parentTemplate template of the mods parent item + * @param parentTemplate template of the mods being added * @param botRole * @returns true if valid */ protected isModValidForSlot( modToAdd: [boolean, ITemplateItem], - itemSlot: Slot, + slotAddedToTemplate: Slot, modSlot: string, parentTemplate: ITemplateItem, botRole: string ): boolean { + const modBeingAddedTemplate = modToAdd[1]; + // Mod lacks template item - if (!modToAdd[1]) + if (!modBeingAddedTemplate) { this.logger.error( this.localisationService.getText("bot-no_item_template_found_when_adding_mod", { - modId: modToAdd[1]._id, + modId: modBeingAddedTemplate._id, modSlot: modSlot, }), ); @@ -1014,13 +1043,14 @@ export class BotEquipmentModGenerator if (!modToAdd[0]) { // Slot must be filled, show warning - if (itemSlot._required) + if (slotAddedToTemplate._required) { this.logger.warning( this.localisationService.getText("bot-unable_to_add_mod_item_invalid", { - itemName: modToAdd[1]._name, + itemName: modBeingAddedTemplate._name, modSlot: modSlot, parentItemName: parentTemplate._name, + botRole: botRole }), ); } @@ -1028,24 +1058,6 @@ export class BotEquipmentModGenerator return false; } - // If mod id doesn't exist in slots filter list and mod id doesn't have any of the slots filters as a base class, mod isn't valid for the slot - if ( - !(itemSlot._props.filters[0].Filter.includes(modToAdd[1]._id) - || this.itemHelper.isOfBaseclasses(modToAdd[1]._id, itemSlot._props.filters[0].Filter)) - ) - { - this.logger.warning( - this.localisationService.getText("bot-mod_not_in_slot_filter_list", { - modId: modToAdd[1]._id, - modSlot: modSlot, - parentName: parentTemplate._name, - botRole: botRole - }), - ); - - return false; - } - return true; } diff --git a/project/src/generators/BotWeaponGenerator.ts b/project/src/generators/BotWeaponGenerator.ts index 427b98d3..3ff7eb03 100644 --- a/project/src/generators/BotWeaponGenerator.ts +++ b/project/src/generators/BotWeaponGenerator.ts @@ -357,31 +357,31 @@ export class BotWeaponGenerator { for (const mod of weaponItemArray) { - const modDbTemplate = this.itemHelper.getItem(mod._tpl)[1]; - if (!modDbTemplate._props.Slots?.length) + const modTemplate = this.itemHelper.getItem(mod._tpl)[1]; + if (!modTemplate._props.Slots?.length) { continue; } // Iterate over slots in db item, if required, check tpl in that slot matches the filter list - for (const modSlot of modDbTemplate._props.Slots) + for (const modSlotTemplate of modTemplate._props.Slots) { // Ignore optional mods - if (!modSlot._required) + if (!modSlotTemplate._required) { continue; } - const allowedTpls = modSlot._props.filters[0].Filter; - const slotName = modSlot._name; + const allowedTplsOnSlot = modSlotTemplate._props.filters[0].Filter; + const slotName = modSlotTemplate._name; - const weaponSlotItem = weaponItemArray.find((x) => x.parentId === mod._id && x.slotId === slotName); + const weaponSlotItem = weaponItemArray.find((weaponItem) => weaponItem.parentId === mod._id && weaponItem.slotId === slotName); if (!weaponSlotItem) { this.logger.warning( this.localisationService.getText("bot-weapons_required_slot_missing_item", { - modSlot: modSlot._name, - modName: modDbTemplate._name, + modSlot: modSlotTemplate._name, + modName: modTemplate._name, slotId: mod.slotId, botRole: botRole, }), @@ -389,19 +389,6 @@ export class BotWeaponGenerator return false; } - - if (!allowedTpls.includes(weaponSlotItem._tpl)) - { - this.logger.warning( - this.localisationService.getText("bot-weapon_contains_invalid_item", { - modSlot: modSlot._name, - modName: modDbTemplate._name, - weaponTpl: weaponSlotItem._tpl, - }), - ); - - return false; - } } } diff --git a/project/src/helpers/BotGeneratorHelper.ts b/project/src/helpers/BotGeneratorHelper.ts index 4882f93b..972233aa 100644 --- a/project/src/helpers/BotGeneratorHelper.ts +++ b/project/src/helpers/BotGeneratorHelper.ts @@ -9,6 +9,7 @@ import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem"; import { IGetRaidConfigurationRequestData } from "@spt-aki/models/eft/match/IGetRaidConfigurationRequestData"; import { BaseClasses } from "@spt-aki/models/enums/BaseClasses"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; +import { IChooseRandomCompatibleModResult } from "@spt-aki/models/spt/bots/IChooseRandomCompatibleModResult"; import { EquipmentFilters, IBotConfig, IRandomisedResourceValues } from "@spt-aki/models/spt/config/IBotConfig"; import { IPmcConfig } from "@spt-aki/models/spt/config/IPmcConfig"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; @@ -286,12 +287,12 @@ export class BotGeneratorHelper itemsEquipped: Item[], tplToCheck: string, equipmentSlot: string, - ): { incompatible: boolean; reason: string; slotBlocked?: boolean; } + ): IChooseRandomCompatibleModResult { // Skip slots that have no incompatibilities if (["Scabbard", "Backpack", "SecureContainer", "Holster", "ArmBand"].includes(equipmentSlot)) { - return { incompatible: false, reason: "" }; + return { incompatible: false, found: false, reason: "" }; } // TODO: Can probably be optimized to cache itemTemplates as items are added to inventory @@ -308,7 +309,7 @@ export class BotGeneratorHelper }), ); - return { incompatible: true, reason: `item: ${tplToCheck} does not exist in the database` }; + return { incompatible: true, found: false, reason: `item: ${tplToCheck} does not exist in the database` }; } if (!itemToEquip._props) @@ -321,7 +322,11 @@ export class BotGeneratorHelper }), ); - return { incompatible: true, reason: `item: ${tplToCheck} does not have a _props field` }; + return { + incompatible: true, + found: false, + reason: `item: ${tplToCheck} does not have a _props field` + }; } // Does an equipped item have a property that blocks the desired item - check for prop "BlocksX" .e.g BlocksEarpiece / BlocksFaceCover @@ -331,6 +336,7 @@ export class BotGeneratorHelper // this.logger.warning(`1 incompatibility found between - ${itemToEquip[1]._name} and ${blockingItem._name} - ${equipmentSlot}`); return { incompatible: true, + found: false, reason: `${tplToCheck} ${itemToEquip._name} in slot: ${equipmentSlot} blocked by: ${blockingItem._id} ${blockingItem._name}`, slotBlocked: true, @@ -344,6 +350,7 @@ export class BotGeneratorHelper // this.logger.warning(`2 incompatibility found between - ${itemToEquip[1]._name} and ${blockingItem._props.Name} - ${equipmentSlot}`); return { incompatible: true, + found: false, reason: `${tplToCheck} ${itemToEquip._name} in slot: ${equipmentSlot} blocked by: ${blockingItem._id} ${blockingItem._name}`, slotBlocked: true, @@ -358,6 +365,7 @@ export class BotGeneratorHelper { return { incompatible: true, + found: false, reason: `${tplToCheck} ${itemToEquip._name} is blocked by: ${existingHeadwear._tpl} in slot: ${existingHeadwear.slotId}`, slotBlocked: true, @@ -373,6 +381,7 @@ export class BotGeneratorHelper { return { incompatible: true, + found: false, reason: `${tplToCheck} ${itemToEquip._name} is blocked by: ${existingFaceCover._tpl} in slot: ${existingFaceCover.slotId}`, slotBlocked: true, @@ -388,6 +397,7 @@ export class BotGeneratorHelper { return { incompatible: true, + found: false, reason: `${tplToCheck} ${itemToEquip._name} is blocked by: ${existingEarpiece._tpl} in slot: ${existingEarpiece.slotId}`, slotBlocked: true, @@ -403,6 +413,7 @@ export class BotGeneratorHelper { return { incompatible: true, + found: false, reason: `${tplToCheck} ${itemToEquip._name} is blocked by: ${existingArmorVest._tpl} in slot: ${existingArmorVest.slotId}`, slotBlocked: true, @@ -417,6 +428,7 @@ export class BotGeneratorHelper // this.logger.warning(`3 incompatibility found between - ${itemToEquip[1]._name} and ${blockingInventoryItem._tpl} - ${equipmentSlot}`) return { incompatible: true, + found: false, reason: `${tplToCheck} blocks existing item ${blockingInventoryItem._tpl} in slot ${blockingInventoryItem.slotId}`, }; diff --git a/project/src/models/spt/bots/IChooseRandomCompatibleModResult.ts b/project/src/models/spt/bots/IChooseRandomCompatibleModResult.ts new file mode 100644 index 00000000..16c6b815 --- /dev/null +++ b/project/src/models/spt/bots/IChooseRandomCompatibleModResult.ts @@ -0,0 +1,8 @@ +export interface IChooseRandomCompatibleModResult +{ + incompatible: boolean; + found?: boolean; + chosenTpl?: string; + reason: string; + slotBlocked?: boolean; +} \ No newline at end of file