Improve mod item filtering code

This commit is contained in:
Dev 2024-01-27 18:12:13 +00:00
parent 8ca0c5d82b
commit 734d821630
4 changed files with 143 additions and 124 deletions

View File

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

View File

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

View File

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

View File

@ -0,0 +1,8 @@
export interface IChooseRandomCompatibleModResult
{
incompatible: boolean;
found?: boolean;
chosenTpl?: string;
reason: string;
slotBlocked?: boolean;
}