using SPTarkov.DI.Annotations; using SPTarkov.Server.Core.Helpers; using SPTarkov.Server.Core.Models.Common; using SPTarkov.Server.Core.Models.Eft.Common.Tables; using SPTarkov.Server.Core.Models.Enums; using SPTarkov.Server.Core.Models.Spt.Bots; using SPTarkov.Server.Core.Models.Spt.Config; using SPTarkov.Server.Core.Models.Utils; using SPTarkov.Server.Core.Servers; using SPTarkov.Server.Core.Services; using SPTarkov.Server.Core.Utils; using SPTarkov.Server.Core.Utils.Cloners; using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel; namespace SPTarkov.Server.Core.Generators; [Injectable] public class BotLootGenerator( ISptLogger _logger, RandomUtil _randomUtil, ItemHelper _itemHelper, InventoryHelper _inventoryHelper, HandbookHelper _handbookHelper, BotGeneratorHelper _botGeneratorHelper, BotWeaponGenerator _botWeaponGenerator, WeightedRandomHelper _weightedRandomHelper, BotHelper _botHelper, BotLootCacheService _botLootCacheService, ServerLocalisationService _serverLocalisationService, ConfigServer _configServer, ICloner _cloner ) { protected readonly BotConfig _botConfig = _configServer.GetConfig(); protected readonly PmcConfig _pmcConfig = _configServer.GetConfig(); /// /// /// /// protected ItemSpawnLimitSettings GetItemSpawnLimitsForBot(string botRole) { var limits = GetItemSpawnLimitsForBotType(botRole); // Clone limits and set all values to 0 to use as a running total var limitsForBotDict = _cloner.Clone(limits); // Init current count of items we want to limit foreach (var limit in limitsForBotDict) { limitsForBotDict[limit.Key] = 0; } return new ItemSpawnLimitSettings { CurrentLimits = limitsForBotDict, GlobalLimits = GetItemSpawnLimitsForBotType(botRole), }; } /// /// Add loot to bots containers /// /// Session id /// Clone of Base JSON db file for the bot having its loot generated /// Will bot be a pmc /// Role of bot, e.g. assault /// Inventory to add loot to /// Level of bot public void GenerateLoot( string sessionId, BotType botJsonTemplate, bool isPmc, string botRole, BotBaseInventory botInventory, int botLevel ) { // Limits on item types to be added as loot var itemCounts = botJsonTemplate.BotGeneration?.Items; if ( itemCounts?.BackpackLoot.Weights is null || itemCounts.PocketLoot.Weights is null || itemCounts.VestLoot.Weights is null || itemCounts.SpecialItems.Weights is null || itemCounts.Healing.Weights is null || itemCounts.Drugs.Weights is null || itemCounts.Food.Weights is null || itemCounts.Drink.Weights is null || itemCounts.Currency.Weights is null || itemCounts.Stims.Weights is null || itemCounts.Grenades.Weights is null ) { _logger.Warning( _serverLocalisationService.GetText("bot-unable_to_generate_bot_loot", botRole) ); return; } var backpackLootCount = _weightedRandomHelper.GetWeightedValue( itemCounts.BackpackLoot.Weights ); var pocketLootCount = _weightedRandomHelper.GetWeightedValue(itemCounts.PocketLoot.Weights); var vestLootCount = _weightedRandomHelper.GetWeightedValue(itemCounts.VestLoot.Weights); var specialLootItemCount = _weightedRandomHelper.GetWeightedValue( itemCounts.SpecialItems.Weights ); var healingItemCount = _weightedRandomHelper.GetWeightedValue(itemCounts.Healing.Weights); var drugItemCount = _weightedRandomHelper.GetWeightedValue(itemCounts.Drugs.Weights); var foodItemCount = _weightedRandomHelper.GetWeightedValue(itemCounts.Food.Weights); var drinkItemCount = _weightedRandomHelper.GetWeightedValue(itemCounts.Drink.Weights); var currencyItemCount = _weightedRandomHelper.GetWeightedValue(itemCounts.Currency.Weights); var stimItemCount = _weightedRandomHelper.GetWeightedValue(itemCounts.Stims.Weights); var grenadeCount = _weightedRandomHelper.GetWeightedValue(itemCounts.Grenades.Weights); // If bot has been flagged as not having loot, set below counts to 0 if (_botConfig.DisableLootOnBotTypes.Contains(botRole.ToLowerInvariant())) { backpackLootCount = 0; pocketLootCount = 0; vestLootCount = 0; currencyItemCount = 0; } // Forced pmc healing loot into secure container if (isPmc && _pmcConfig.ForceHealingItemsIntoSecure) { AddForcedMedicalItemsToPmcSecure(botInventory, botRole); } var botItemLimits = GetItemSpawnLimitsForBot(botRole); var containersBotHasAvailable = GetAvailableContainersBotCanStoreItemsIn(botInventory); // This set is passed as a reference to fill up the containers that are already full, this alleviates // generation of the bots by avoiding checking the slots of containers we already know are full HashSet filledContainerIds = []; // Special items AddLootFromPool( _botLootCacheService.GetLootFromCache( botRole, isPmc, LootCacheType.Special, botJsonTemplate ), containersBotHasAvailable, specialLootItemCount, botInventory, botRole, botItemLimits, containersIdFull: filledContainerIds ); // Healing items / Meds AddLootFromPool( _botLootCacheService.GetLootFromCache( botRole, isPmc, LootCacheType.HealingItems, botJsonTemplate ), containersBotHasAvailable, healingItemCount, botInventory, botRole, null, 0, isPmc, filledContainerIds ); // Drugs AddLootFromPool( _botLootCacheService.GetLootFromCache( botRole, isPmc, LootCacheType.DrugItems, botJsonTemplate ), containersBotHasAvailable, drugItemCount, botInventory, botRole, null, 0, isPmc, filledContainerIds ); // Food AddLootFromPool( _botLootCacheService.GetLootFromCache( botRole, isPmc, LootCacheType.FoodItems, botJsonTemplate ), containersBotHasAvailable, foodItemCount, botInventory, botRole, null, 0, isPmc, filledContainerIds ); // Drink AddLootFromPool( _botLootCacheService.GetLootFromCache( botRole, isPmc, LootCacheType.DrinkItems, botJsonTemplate ), containersBotHasAvailable, drinkItemCount, botInventory, botRole, null, 0, isPmc, filledContainerIds ); // Currency AddLootFromPool( _botLootCacheService.GetLootFromCache( botRole, isPmc, LootCacheType.CurrencyItems, botJsonTemplate ), containersBotHasAvailable, currencyItemCount, botInventory, botRole, null, 0, isPmc, filledContainerIds ); // Stims AddLootFromPool( _botLootCacheService.GetLootFromCache( botRole, isPmc, LootCacheType.StimItems, botJsonTemplate ), containersBotHasAvailable, stimItemCount, botInventory, botRole, botItemLimits, 0, isPmc, filledContainerIds ); // Grenades AddLootFromPool( _botLootCacheService.GetLootFromCache( botRole, isPmc, LootCacheType.GrenadeItems, botJsonTemplate ), [EquipmentSlots.Pockets, EquipmentSlots.TacticalVest], // Can't use containersBotHasEquipped as we don't want grenades added to backpack grenadeCount, botInventory, botRole, null, 0, isPmc, filledContainerIds ); var itemPriceLimits = GetSingleItemLootPriceLimits(botLevel, isPmc); // Backpack - generate loot if they have one if (containersBotHasAvailable.Contains(EquipmentSlots.Backpack)) { // Add randomly generated weapon to PMC backpacks if (isPmc && _randomUtil.GetChance100(_pmcConfig.LooseWeaponInBackpackChancePercent)) { AddLooseWeaponsToInventorySlot( sessionId, botInventory, EquipmentSlots.Backpack, botJsonTemplate.BotInventory, botJsonTemplate.BotChances?.WeaponModsChances, botRole, isPmc, botLevel, filledContainerIds ); } var backpackLootRoubleTotal = GetBackpackRoubleTotalByLevel(botLevel, isPmc); AddLootFromPool( _botLootCacheService.GetLootFromCache( botRole, isPmc, LootCacheType.Backpack, botJsonTemplate, itemPriceLimits?.Backpack ), [EquipmentSlots.Backpack], backpackLootCount, botInventory, botRole, botItemLimits, backpackLootRoubleTotal ?? 0, isPmc, filledContainerIds ); } // TacticalVest - generate loot if they have one if (containersBotHasAvailable.Contains(EquipmentSlots.TacticalVest)) // Vest { AddLootFromPool( _botLootCacheService.GetLootFromCache( botRole, isPmc, LootCacheType.Vest, botJsonTemplate, itemPriceLimits?.Vest ), [EquipmentSlots.TacticalVest], vestLootCount, botInventory, botRole, botItemLimits, _pmcConfig.MaxVestLootTotalRub, isPmc, filledContainerIds ); } // Pockets AddLootFromPool( _botLootCacheService.GetLootFromCache( botRole, isPmc, LootCacheType.Pocket, botJsonTemplate, itemPriceLimits?.Pocket ), [EquipmentSlots.Pockets], pocketLootCount, botInventory, botRole, botItemLimits, _pmcConfig.MaxPocketLootTotalRub, isPmc, filledContainerIds ); // Secure // only add if not a pmc or is pmc and flag is true if (!isPmc || (isPmc && _pmcConfig.AddSecureContainerLootFromBotConfig)) { AddLootFromPool( _botLootCacheService.GetLootFromCache( botRole, isPmc, LootCacheType.Secure, botJsonTemplate ), [EquipmentSlots.SecuredContainer], 50, botInventory, botRole, null, -1, isPmc, filledContainerIds ); } } protected MinMaxLootItemValue? GetSingleItemLootPriceLimits(int botLevel, bool isPmc) { // TODO - extend to other bot types if (!isPmc) { return null; } var matchingValue = _pmcConfig?.LootItemLimitsRub?.FirstOrDefault(minMaxValue => botLevel >= minMaxValue.Min && botLevel <= minMaxValue.Max ); return matchingValue; } /// /// Gets the rouble cost total for loot in a bots backpack by the bots levl /// Will return 0 for non PMCs /// /// Bots level /// Is the bot a PMC /// int protected double? GetBackpackRoubleTotalByLevel(int botLevel, bool isPmc) { if (!isPmc) { return 0; } var matchingValue = _pmcConfig.MaxBackpackLootTotalRub.FirstOrDefault(minMaxValue => botLevel >= minMaxValue.Min && botLevel <= minMaxValue.Max ); return matchingValue?.Value; } /// /// Get an array of the containers a bot has on them (pockets/backpack/vest) /// /// Bot to check /// Array of available slots protected HashSet GetAvailableContainersBotCanStoreItemsIn( BotBaseInventory botInventory ) { HashSet result = [EquipmentSlots.Pockets]; if ( (botInventory.Items ?? []).Any(item => item.SlotId == nameof(EquipmentSlots.TacticalVest) ) ) { result.Add(EquipmentSlots.TacticalVest); } if ((botInventory.Items ?? []).Any(item => item.SlotId == nameof(EquipmentSlots.Backpack))) { result.Add(EquipmentSlots.Backpack); } return result; } /// /// Force healing items onto bot to ensure they can heal in-raid /// /// Inventory to add items to /// Role of bot (pmcBEAR/pmcUSEC) protected void AddForcedMedicalItemsToPmcSecure(BotBaseInventory botInventory, string botRole) { // surv12 AddLootFromPool( new Dictionary { { "5d02797c86f774203f38e30a", 1 } }, [EquipmentSlots.SecuredContainer], 1, botInventory, botRole, null, 0, true ); // AFAK AddLootFromPool( new Dictionary { { "60098ad7c2240c0fe85c570a", 1 } }, [EquipmentSlots.SecuredContainer], 10, botInventory, botRole, null, 0, true ); } /// /// Take random items from a pool and add to an inventory until totalItemCount or totalValueLimit or space limit is reached /// /// Pool of items to pick from with weight /// What equipment slot will the loot items be added to /// Max count of items to add /// Bot inventory loot will be added to /// Role of the bot loot is being generated for (assault/pmcbot) /// Item spawn limits the bot must adhere to /// /// Total value of loot allowed in roubles /// Is bot being generated for a pmc protected void AddLootFromPool( Dictionary pool, HashSet equipmentSlots, double totalItemCount, BotBaseInventory inventoryToAddItemsTo, string botRole, ItemSpawnLimitSettings? itemSpawnLimits, double totalValueLimitRub = 0, bool isPmc = false, HashSet? containersIdFull = null ) { // Loot pool has items var poolSize = pool.Count; if (poolSize <= 0) { return; } double currentTotalRub = 0; var fitItemIntoContainerAttempts = 0; for (var i = 0; i < totalItemCount; i++) { // Pool can become empty if item spawn limits keep removing items if (pool.Count == 0) { return; } var weightedItemTpl = _weightedRandomHelper.GetWeightedValue(pool); var (key, itemToAddTemplate) = _itemHelper.GetItem(weightedItemTpl); if (!key) { _logger.Warning( $"Unable to process item tpl: {weightedItemTpl} for slots: {equipmentSlots} on bot: {botRole}" ); continue; } if ( itemSpawnLimits is not null && ItemHasReachedSpawnLimit(itemToAddTemplate, botRole, itemSpawnLimits) ) { // Remove item from pool to prevent it being picked again pool.Remove(weightedItemTpl); i--; continue; } var newRootItemId = new MongoId(); List itemWithChildrenToAdd = [ new() { Id = newRootItemId, Template = itemToAddTemplate?.Id ?? string.Empty, Upd = _botGeneratorHelper.GenerateExtraPropertiesForItem( itemToAddTemplate, botRole ), }, ]; // Is Simple-Wallet / WZ wallet if (_botConfig.WalletLoot.WalletTplPool.Contains(weightedItemTpl)) { var addCurrencyToWallet = _randomUtil.GetChance100( _botConfig.WalletLoot.ChancePercent ); if (addCurrencyToWallet) { // Create the currency items we want to add to wallet var itemsToAdd = CreateWalletLoot(newRootItemId); // Get the container grid for the wallet var containerGrid = _inventoryHelper.GetContainerSlotMap(weightedItemTpl); // Check if all the chosen currency items fit into wallet var canAddToContainer = _inventoryHelper.CanPlaceItemsInContainer( _cloner.Clone(containerGrid), // MUST clone grid before passing in as function modifies grid itemsToAdd ); if (canAddToContainer) { // Add each currency to wallet foreach (var itemToAdd in itemsToAdd) { _inventoryHelper.PlaceItemInContainer( containerGrid, itemToAdd, itemWithChildrenToAdd[0].Id, "main" ); } itemWithChildrenToAdd.AddRange(itemsToAdd.SelectMany(x => x)); } } } // Some items (ammoBox/ammo) need extra changes AddRequiredChildItemsToParent(itemToAddTemplate, itemWithChildrenToAdd, isPmc, botRole); // Attempt to add item to container(s) var itemAddedResult = _botGeneratorHelper.AddItemWithChildrenToEquipmentSlot( equipmentSlots, newRootItemId, itemToAddTemplate.Id, itemWithChildrenToAdd, inventoryToAddItemsTo, containersIdFull ); // Handle when item cannot be added if (itemAddedResult != ItemAddedResult.SUCCESS) { if (itemAddedResult == ItemAddedResult.NO_CONTAINERS) { // Bot has no container to put item in, exit if (_logger.IsLogEnabled(LogLevel.Debug)) { _logger.Debug( $"Unable to add: {totalItemCount} items to bot as it lacks a container to include them" ); } break; } fitItemIntoContainerAttempts++; if (fitItemIntoContainerAttempts >= 4) { if (_logger.IsLogEnabled(LogLevel.Debug)) { _logger.Debug( $"Failed placing item: {itemToAddTemplate.Id} - {itemToAddTemplate.Name}: {i} of: {totalItemCount} items into: {botRole} " + $"containers: {string.Join(",", equipmentSlots)}. Tried: {fitItemIntoContainerAttempts} " + $"times, reason: {itemAddedResult}, skipping" ); } break; } // Try again, failed but still under attempt limit continue; } // Item added okay, reset counter for next item fitItemIntoContainerAttempts = 0; // Stop adding items to bots pool if rolling total is over total limit if (totalValueLimitRub > 0) { currentTotalRub += _handbookHelper.GetTemplatePrice(itemToAddTemplate.Id); if (currentTotalRub > totalValueLimitRub) { break; } } } } /// /// Adds loot to the specified Wallet /// /// Wallet to add loot to /// Generated list of currency stacks with the wallet as their parent public List> CreateWalletLoot(string walletId) { List> result = []; // Choose how many stacks of currency will be added to wallet var itemCount = _randomUtil.GetInt( _botConfig.WalletLoot.ItemCount.Min, _botConfig.WalletLoot.ItemCount.Max ); for (var index = 0; index < itemCount; index++) { // Choose the size of the currency stack - default is 5k, 10k, 15k, 20k, 25k var chosenStackCount = _weightedRandomHelper.GetWeightedValue( _botConfig.WalletLoot.StackSizeWeight ); List items = [ new() { Id = new MongoId(), Template = _weightedRandomHelper.GetWeightedValue( _botConfig.WalletLoot.CurrencyWeight ), ParentId = walletId, Upd = new Upd { StackObjectsCount = int.Parse(chosenStackCount) }, }, ]; result.Add(items); } return result; } /// /// Some items need child items to function, add them to the itemToAddChildrenTo array /// /// Db template of item to check /// Item to add children to /// Is the item being generated for a pmc (affects money/ammo stack sizes) /// role bot has that owns item public void AddRequiredChildItemsToParent( TemplateItem? itemToAddTemplate, List itemToAddChildrenTo, bool isPmc, string botRole ) { // Fill ammo box if (_itemHelper.IsOfBaseclass(itemToAddTemplate.Id, BaseClasses.AMMO_BOX)) { _itemHelper.AddCartridgesToAmmoBox(itemToAddChildrenTo, itemToAddTemplate); } // Make money a stack else if (_itemHelper.IsOfBaseclass(itemToAddTemplate.Id, BaseClasses.MONEY)) { RandomiseMoneyStackSize(botRole, itemToAddTemplate, itemToAddChildrenTo[0]); } // Make ammo a stack else if (_itemHelper.IsOfBaseclass(itemToAddTemplate.Id, BaseClasses.AMMO)) { RandomiseAmmoStackSize(isPmc, itemToAddTemplate, itemToAddChildrenTo[0]); } // Must add soft inserts/plates else if (_itemHelper.ItemRequiresSoftInserts(itemToAddTemplate.Id)) { _itemHelper.AddChildSlotItems(itemToAddChildrenTo, itemToAddTemplate); } } /// /// Add generated weapons to inventory as loot /// /// Session/Player id /// Inventory to add preset to /// Slot to place the preset in (backpack) /// Bots template, assault.json /// Chances for mods to spawn on weapon /// bots role .e.g. pmcBot /// are we generating for a pmc /// /// public void AddLooseWeaponsToInventorySlot( string sessionId, BotBaseInventory botInventory, EquipmentSlots equipmentSlot, BotTypeInventory? templateInventory, Dictionary? modChances, string botRole, bool isPmc, int botLevel, HashSet? containersIdFull ) { var chosenWeaponType = _randomUtil.GetArrayValue( [ EquipmentSlots.FirstPrimaryWeapon.ToString(), EquipmentSlots.FirstPrimaryWeapon.ToString(), EquipmentSlots.FirstPrimaryWeapon.ToString(), EquipmentSlots.Holster.ToString(), ] ); var randomisedWeaponCount = _randomUtil.GetInt( _pmcConfig.LooseWeaponInBackpackLootMinMax.Min, _pmcConfig.LooseWeaponInBackpackLootMinMax.Max ); if (randomisedWeaponCount <= 0) { return; } for (var i = 0; i < randomisedWeaponCount; i++) { var generatedWeapon = _botWeaponGenerator.GenerateRandomWeapon( sessionId, chosenWeaponType, templateInventory, botInventory.Equipment, modChances, botRole, isPmc, botLevel ); var weaponRootItem = generatedWeapon.Weapon?.FirstOrDefault(); if (weaponRootItem is null) { _logger.Error( $"Generated loose weapon: {chosenWeaponType} for: {botRole} level: {botLevel} was null, skipping" ); continue; } var result = _botGeneratorHelper.AddItemWithChildrenToEquipmentSlot( [equipmentSlot], weaponRootItem.Id, weaponRootItem.Template, generatedWeapon.Weapon, botInventory, containersIdFull ); if (result != ItemAddedResult.SUCCESS) { if (_logger.IsLogEnabled(LogLevel.Debug)) { _logger.Debug( $"Failed to add additional weapon: {weaponRootItem.Id} to bot backpack, reason: {result.ToString()}" ); } } } } /// /// Check if an item has reached its bot-specific spawn limit /// /// Item we check to see if its reached spawn limit /// Bot type /// /// true if item has reached spawn limit protected bool ItemHasReachedSpawnLimit( TemplateItem? itemTemplate, string botRole, ItemSpawnLimitSettings? itemSpawnLimits ) { // PMCs and scavs have different sections of bot config for spawn limits if (itemSpawnLimits is not null && itemSpawnLimits.GlobalLimits?.Count == 0) // No items found in spawn limit, drop out { return false; } // No spawn limits, skipping if (itemSpawnLimits is null) { return false; } var idToCheckFor = GetMatchingIdFromSpawnLimits(itemTemplate, itemSpawnLimits.GlobalLimits); if (idToCheckFor is null) // ParentId or tplid not found in spawnLimits, not a spawn limited item, skip { return false; } // Use tryAdd to see if it exists, and automatically add 1 if (!itemSpawnLimits.CurrentLimits.TryAdd(idToCheckFor, 1)) // if it does exist, come in here and increment // Increment item count with this bot type { itemSpawnLimits.CurrentLimits[idToCheckFor]++; } // Check if over limit var currentLimitCount = itemSpawnLimits.CurrentLimits[idToCheckFor]; if ( itemSpawnLimits.CurrentLimits[idToCheckFor] > itemSpawnLimits.GlobalLimits[idToCheckFor] ) { // Prevent edge-case of small loot pools + code trying to add limited item over and over infinitely if (currentLimitCount > currentLimitCount * 10) { if (_logger.IsLogEnabled(LogLevel.Debug)) { _logger.Debug( _serverLocalisationService.GetText( "bot-item_spawn_limit_reached_skipping_item", new { botRole, itemName = itemTemplate.Name, attempts = currentLimitCount, } ) ); } return false; } return true; } return false; } /// /// Randomise the stack size of a money object, uses different values for pmc or scavs /// /// Role bot has that has money stack /// item details from db /// Money item to randomise public void RandomiseMoneyStackSize(string botRole, TemplateItem itemTemplate, Item moneyItem) { // Get all currency weights for this bot type if (!_botConfig.CurrencyStackSize.TryGetValue(botRole, out var currencyWeights)) { currencyWeights = _botConfig.CurrencyStackSize["default"]; } var currencyWeight = currencyWeights[moneyItem.Template]; _itemHelper.AddUpdObjectToItem(moneyItem); moneyItem.Upd.StackObjectsCount = int.Parse( _weightedRandomHelper.GetWeightedValue(currencyWeight) ); } /// /// Randomise the size of an ammo stack /// /// Is ammo on a PMC bot /// item details from db /// Ammo item to randomise public void RandomiseAmmoStackSize(bool isPmc, TemplateItem itemTemplate, Item ammoItem) { var randomSize = _itemHelper.GetRandomisedAmmoStackSize(itemTemplate); _itemHelper.AddUpdObjectToItem(ammoItem); ammoItem.Upd.StackObjectsCount = randomSize; } /// /// Get spawn limits for a specific bot type from bot.json config /// If no limit found for a non pmc bot, fall back to defaults /// /// what role does the bot have /// Dictionary of tplIds and limit public Dictionary GetItemSpawnLimitsForBotType(string botRole) { if (_botHelper.IsBotPmc(botRole)) { return _botConfig.ItemSpawnLimits["pmc"]; } if (_botConfig.ItemSpawnLimits.ContainsKey(botRole.ToLowerInvariant())) { return _botConfig.ItemSpawnLimits[botRole.ToLowerInvariant()]; } _logger.Warning( _serverLocalisationService.GetText( "bot-unable_to_find_spawn_limits_fallback_to_defaults", botRole ) ); return new Dictionary(); } /// /// Get the parentId or tplId of item inside spawnLimits object if it exists /// /// item we want to look for in spawn limits /// Limits to check for item /// id as string, otherwise undefined public string? GetMatchingIdFromSpawnLimits( TemplateItem itemTemplate, Dictionary spawnLimits ) { if (spawnLimits.ContainsKey(itemTemplate.Id)) { return itemTemplate.Id; } // tplId not found in spawnLimits, check if parentId is if (spawnLimits.ContainsKey(itemTemplate.Parent)) { return itemTemplate.Parent; } // parentId and tplId not found return null; } }