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 { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { ModSpawn } from "@spt-aki/models/enums/ModSpawn"; 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 { EquipmentFilterDetails, IBotConfig } from "@spt-aki/models/spt/config/IBotConfig";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { ConfigServer } from "@spt-aki/servers/ConfigServer"; import { ConfigServer } from "@spt-aki/servers/ConfigServer";
@ -735,9 +736,6 @@ export class BotEquipmentModGenerator
modSpawnResult: ModSpawn modSpawnResult: ModSpawn
): [boolean, ITemplateItem] ): [boolean, ITemplateItem]
{ {
/** Chosen mod tpl */
let modTpl: string;
let found = false;
/** Slot mod will fill */ /** Slot mod will fill */
const parentSlot = parentTemplate._props.Slots.find((i) => i._name === modSlot); const parentSlot = parentTemplate._props.Slots.find((i) => i._name === modSlot);
const weaponTemplate = this.itemHelper.getItem(weapon[0]._tpl)[1]; const weaponTemplate = this.itemHelper.getItem(weapon[0]._tpl)[1];
@ -745,93 +743,64 @@ export class BotEquipmentModGenerator
// It's ammo, use predefined ammo parameter // It's ammo, use predefined ammo parameter
if (this.getAmmoContainers().includes(modSlot) && modSlot !== "mod_magazine") 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 // Nothing in mod pool + item not required
let modPool = this.getModPoolForSlot(itemModPool, modSpawnResult, parentTemplate, weaponTemplate, modSlot, botEquipBlacklist, isRandomisableSlot); this.logger.debug(
if (!(modPool || parentSlot._required)) `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 modPool = this.filterSightsByWeaponType(
this.logger.debug( weapon[0],
`Mod pool for slot: ${modSlot} on item: ${parentTemplate._name} was empty, skipping mod`, 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 (chosenModResult.slotBlocked)
if (modSlot.includes("mod_scope") && botWeaponSightWhitelist) {
{ // Don't bother trying to fit mod, slot is completely blocked
// scope pool has more than one scope return null;
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;
break; // Log if mod chosen was incompatible
} if (chosenModResult.incompatible && parentSlot._required)
{
modCompatibilityResult = this.botGeneratorHelper.isItemIncompatibleWithCurrentItems( this.logger.debug(chosenModResult.reason);
weapon, this.logger.debug(`Weapon: ${weapon.map(x => `${x._tpl} ${x.slotId ?? ""}`).join(",")}`)
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(",")}`)
}
} }
// Get random mod to attach from items db for required slots if none found above // 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); chosenModResult.chosenTpl = this.getRandomModTplFromItemDb("", parentSlot, modSlot, weapon);
found = !!modTpl; chosenModResult.found = true;
} }
// Compatible item not found + not required // Compatible item not found + not required
if (!found && parentSlot !== undefined && !parentSlot._required) if (!chosenModResult.found && parentSlot !== undefined && !parentSlot._required)
{ {
return null; return null;
} }
if (!found && parentSlot !== undefined) if (!chosenModResult.found && parentSlot !== undefined)
{ {
if (parentSlot._required) if (parentSlot._required)
{ {
@ -845,7 +814,65 @@ export class BotEquipmentModGenerator
return null; 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 * Log errors if mod is not compatible with slot
* @param modToAdd template of mod to check * @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 modSlot slot the mod will fill
* @param parentTemplate template of the mods parent item * @param parentTemplate template of the mods being added
* @param botRole * @param botRole
* @returns true if valid * @returns true if valid
*/ */
protected isModValidForSlot( protected isModValidForSlot(
modToAdd: [boolean, ITemplateItem], modToAdd: [boolean, ITemplateItem],
itemSlot: Slot, slotAddedToTemplate: Slot,
modSlot: string, modSlot: string,
parentTemplate: ITemplateItem, parentTemplate: ITemplateItem,
botRole: string botRole: string
): boolean ): boolean
{ {
const modBeingAddedTemplate = modToAdd[1];
// Mod lacks template item // Mod lacks template item
if (!modToAdd[1]) if (!modBeingAddedTemplate)
{ {
this.logger.error( this.logger.error(
this.localisationService.getText("bot-no_item_template_found_when_adding_mod", { this.localisationService.getText("bot-no_item_template_found_when_adding_mod", {
modId: modToAdd[1]._id, modId: modBeingAddedTemplate._id,
modSlot: modSlot, modSlot: modSlot,
}), }),
); );
@ -1014,13 +1043,14 @@ export class BotEquipmentModGenerator
if (!modToAdd[0]) if (!modToAdd[0])
{ {
// Slot must be filled, show warning // Slot must be filled, show warning
if (itemSlot._required) if (slotAddedToTemplate._required)
{ {
this.logger.warning( this.logger.warning(
this.localisationService.getText("bot-unable_to_add_mod_item_invalid", { this.localisationService.getText("bot-unable_to_add_mod_item_invalid", {
itemName: modToAdd[1]._name, itemName: modBeingAddedTemplate._name,
modSlot: modSlot, modSlot: modSlot,
parentItemName: parentTemplate._name, parentItemName: parentTemplate._name,
botRole: botRole
}), }),
); );
} }
@ -1028,24 +1058,6 @@ export class BotEquipmentModGenerator
return false; 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; return true;
} }

View File

@ -357,31 +357,31 @@ export class BotWeaponGenerator
{ {
for (const mod of weaponItemArray) for (const mod of weaponItemArray)
{ {
const modDbTemplate = this.itemHelper.getItem(mod._tpl)[1]; const modTemplate = this.itemHelper.getItem(mod._tpl)[1];
if (!modDbTemplate._props.Slots?.length) if (!modTemplate._props.Slots?.length)
{ {
continue; continue;
} }
// Iterate over slots in db item, if required, check tpl in that slot matches the filter list // 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 // Ignore optional mods
if (!modSlot._required) if (!modSlotTemplate._required)
{ {
continue; continue;
} }
const allowedTpls = modSlot._props.filters[0].Filter; const allowedTplsOnSlot = modSlotTemplate._props.filters[0].Filter;
const slotName = modSlot._name; 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) if (!weaponSlotItem)
{ {
this.logger.warning( this.logger.warning(
this.localisationService.getText("bot-weapons_required_slot_missing_item", { this.localisationService.getText("bot-weapons_required_slot_missing_item", {
modSlot: modSlot._name, modSlot: modSlotTemplate._name,
modName: modDbTemplate._name, modName: modTemplate._name,
slotId: mod.slotId, slotId: mod.slotId,
botRole: botRole, botRole: botRole,
}), }),
@ -389,19 +389,6 @@ export class BotWeaponGenerator
return false; 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 { IGetRaidConfigurationRequestData } from "@spt-aki/models/eft/match/IGetRaidConfigurationRequestData";
import { BaseClasses } from "@spt-aki/models/enums/BaseClasses"; import { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; 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 { EquipmentFilters, IBotConfig, IRandomisedResourceValues } from "@spt-aki/models/spt/config/IBotConfig";
import { IPmcConfig } from "@spt-aki/models/spt/config/IPmcConfig"; import { IPmcConfig } from "@spt-aki/models/spt/config/IPmcConfig";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger"; import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
@ -286,12 +287,12 @@ export class BotGeneratorHelper
itemsEquipped: Item[], itemsEquipped: Item[],
tplToCheck: string, tplToCheck: string,
equipmentSlot: string, equipmentSlot: string,
): { incompatible: boolean; reason: string; slotBlocked?: boolean; } ): IChooseRandomCompatibleModResult
{ {
// Skip slots that have no incompatibilities // Skip slots that have no incompatibilities
if (["Scabbard", "Backpack", "SecureContainer", "Holster", "ArmBand"].includes(equipmentSlot)) 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 // 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) 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 // 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}`); // this.logger.warning(`1 incompatibility found between - ${itemToEquip[1]._name} and ${blockingItem._name} - ${equipmentSlot}`);
return { return {
incompatible: true, incompatible: true,
found: false,
reason: reason:
`${tplToCheck} ${itemToEquip._name} in slot: ${equipmentSlot} blocked by: ${blockingItem._id} ${blockingItem._name}`, `${tplToCheck} ${itemToEquip._name} in slot: ${equipmentSlot} blocked by: ${blockingItem._id} ${blockingItem._name}`,
slotBlocked: true, slotBlocked: true,
@ -344,6 +350,7 @@ export class BotGeneratorHelper
// this.logger.warning(`2 incompatibility found between - ${itemToEquip[1]._name} and ${blockingItem._props.Name} - ${equipmentSlot}`); // this.logger.warning(`2 incompatibility found between - ${itemToEquip[1]._name} and ${blockingItem._props.Name} - ${equipmentSlot}`);
return { return {
incompatible: true, incompatible: true,
found: false,
reason: reason:
`${tplToCheck} ${itemToEquip._name} in slot: ${equipmentSlot} blocked by: ${blockingItem._id} ${blockingItem._name}`, `${tplToCheck} ${itemToEquip._name} in slot: ${equipmentSlot} blocked by: ${blockingItem._id} ${blockingItem._name}`,
slotBlocked: true, slotBlocked: true,
@ -358,6 +365,7 @@ export class BotGeneratorHelper
{ {
return { return {
incompatible: true, incompatible: true,
found: false,
reason: reason:
`${tplToCheck} ${itemToEquip._name} is blocked by: ${existingHeadwear._tpl} in slot: ${existingHeadwear.slotId}`, `${tplToCheck} ${itemToEquip._name} is blocked by: ${existingHeadwear._tpl} in slot: ${existingHeadwear.slotId}`,
slotBlocked: true, slotBlocked: true,
@ -373,6 +381,7 @@ export class BotGeneratorHelper
{ {
return { return {
incompatible: true, incompatible: true,
found: false,
reason: reason:
`${tplToCheck} ${itemToEquip._name} is blocked by: ${existingFaceCover._tpl} in slot: ${existingFaceCover.slotId}`, `${tplToCheck} ${itemToEquip._name} is blocked by: ${existingFaceCover._tpl} in slot: ${existingFaceCover.slotId}`,
slotBlocked: true, slotBlocked: true,
@ -388,6 +397,7 @@ export class BotGeneratorHelper
{ {
return { return {
incompatible: true, incompatible: true,
found: false,
reason: reason:
`${tplToCheck} ${itemToEquip._name} is blocked by: ${existingEarpiece._tpl} in slot: ${existingEarpiece.slotId}`, `${tplToCheck} ${itemToEquip._name} is blocked by: ${existingEarpiece._tpl} in slot: ${existingEarpiece.slotId}`,
slotBlocked: true, slotBlocked: true,
@ -403,6 +413,7 @@ export class BotGeneratorHelper
{ {
return { return {
incompatible: true, incompatible: true,
found: false,
reason: reason:
`${tplToCheck} ${itemToEquip._name} is blocked by: ${existingArmorVest._tpl} in slot: ${existingArmorVest.slotId}`, `${tplToCheck} ${itemToEquip._name} is blocked by: ${existingArmorVest._tpl} in slot: ${existingArmorVest.slotId}`,
slotBlocked: true, slotBlocked: true,
@ -417,6 +428,7 @@ export class BotGeneratorHelper
// this.logger.warning(`3 incompatibility found between - ${itemToEquip[1]._name} and ${blockingInventoryItem._tpl} - ${equipmentSlot}`) // this.logger.warning(`3 incompatibility found between - ${itemToEquip[1]._name} and ${blockingInventoryItem._tpl} - ${equipmentSlot}`)
return { return {
incompatible: true, incompatible: true,
found: false,
reason: reason:
`${tplToCheck} blocks existing item ${blockingInventoryItem._tpl} in slot ${blockingInventoryItem.slotId}`, `${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;
}