diff --git a/Core/Generators/BotGenerator.cs b/Core/Generators/BotGenerator.cs index 9a863ad0..5ca0ae21 100644 --- a/Core/Generators/BotGenerator.cs +++ b/Core/Generators/BotGenerator.cs @@ -57,7 +57,7 @@ public class BotGenerator BotNameService botNameService, ConfigServer configServer, ICloner cloner - ) + ) { _logger = logger; _hashUtil = hashUtil; @@ -97,7 +97,8 @@ public class BotGenerator bot.Info.Settings.Role = role; bot.Info.Side = "Savage"; - var botGenDetails = new BotGenerationDetails{ + var botGenDetails = new BotGenerationDetails + { IsPmc = false, Side = "Savage", Role = role, @@ -156,7 +157,23 @@ public class BotGenerator /// constructed bot public BotBase PrepareAndGenerateBot(string sessionId, BotGenerationDetails botGenerationDetails) { - throw new NotImplementedException(); + var preparedBotBase = GetPreparedBotBase( + botGenerationDetails.EventRole ?? botGenerationDetails.Role, // Use eventRole if provided, + botGenerationDetails.Side, + botGenerationDetails.BotDifficulty + ); + + // Get raw json data for bot (Cloned) + var botRole = botGenerationDetails.IsPmc ?? false + ? preparedBotBase.Info.Side // Use side to get usec.json or bear.json when bot will be PMC + : botGenerationDetails.Role; + var botJsonTemplateClone = _cloner.Clone(_botHelper.GetBotTemplate(botRole)); + if (botJsonTemplateClone is not null) + { + _logger.Error($"Unable to retrieve: {botRole} bot template, cannot generate bot of this type"); + } + + return GenerateBot(sessionId, preparedBotBase, botJsonTemplateClone, botGenerationDetails); } /// @@ -168,7 +185,12 @@ public class BotGenerator /// Cloned bot base public BotBase GetPreparedBotBase(string botRole, string botSide, string difficulty) { - throw new NotImplementedException(); + var botBaseClone = GetCloneOfBotBase(); + botBaseClone.Info.Settings.Role = botRole; + botBaseClone.Info.Side = botSide; + botBaseClone.Info.Settings.BotDifficulty = difficulty; + + return botBaseClone; } /// @@ -393,10 +415,12 @@ public class BotGenerator return; } - foreach (var equipmentKvP in blacklist.Gear) { + foreach (var equipmentKvP in blacklist.Gear) + { var equipmentDict = botJsonTemplate.BotInventory.Equipment[equipmentKvP.Key]; - foreach (var blacklistedTpl in equipmentKvP.Value) { + foreach (var blacklistedTpl in equipmentKvP.Value) + { // Set weighting to 0, will never be picked equipmentDict[blacklistedTpl] = 0; } @@ -421,7 +445,37 @@ public class BotGenerator /// Bot to filter public void RemoveBlacklistedLootFromBotTemplate(BotTypeInventory botInventory) { - throw new NotImplementedException(); + List lootContainersToFilter = ["Backpack", "Pockets", "TacticalVest"]; + var props = botInventory.Items.GetType().GetProperties(); + + // Remove blacklisted loot from loot containers + foreach (var lootContainerKey in lootContainersToFilter) + { + var prop = props.FirstOrDefault(x => x.Name.ToLower() == lootContainerKey.ToLower()); + var propValue = (Dictionary)prop.GetValue(botInventory.Items); + + // No container, skip + if (propValue?.Count == 0) + { + continue; + } + + List tplsToRemove = []; + foreach (var item in propValue) + { + if (ItemFilterService.IsLootableItemBlacklisted(item.Key)) + { + tplsToRemove.Add(item.Key); + } + } + + foreach (var blacklistedTplToRemove in tplsToRemove) + { + propValue.Remove(blacklistedTplToRemove); + } + + prop.SetValue(botInventory.Items, propValue); + } } /// @@ -432,7 +486,19 @@ public class BotGenerator /// Generation details public void SetBotAppearance(BotBase bot, Appearance appearance, BotGenerationDetails botGenerationDetails) { - throw new NotImplementedException(); + // Choose random values by weight + bot.Customization.Head = _weightedRandomHelper.GetWeightedValue(appearance.Head); + bot.Customization.Feet = _weightedRandomHelper.GetWeightedValue(appearance.Feet); + bot.Customization.Body = _weightedRandomHelper.GetWeightedValue(appearance.Body); + + var bodyGlobalDictDb = _databaseService.GetGlobals().Configuration.Customization.Body; + var chosenBodyTemplate = _databaseService.GetCustomization()[bot.Customization.Body]; + + // Some bodies have matching hands, look up body to see if this is the case + var chosenBody = bodyGlobalDictDb[chosenBodyTemplate?.Name.Trim()]; + bot.Customization.Hands = chosenBody?.IsNotRandom ?? false + ? chosenBody.Hands // Has fixed hands for chosen body, update to match + : _weightedRandomHelper.GetWeightedValue(appearance.Hands); // Hands can be random, choose any from weighted dict } /// @@ -441,7 +507,8 @@ public class BotGenerator /// Generated bot array, ready to send to client public void LogPmcGeneratedCount(List output) { - throw new NotImplementedException(); + var pmcCount = output.Aggregate(0, (acc, cur) => { return cur.Info.Side == "Bear" || cur.Info.Side == "Usec" ? acc + 1 : acc; }); + _logger.Debug($"Generated {output.Count} total bots. Replaced ${pmcCount} with PMCs"); } /// @@ -452,7 +519,105 @@ public class BotGenerator /// Health object public BotBaseHealth GenerateHealth(BotTypeHealth healthObj, bool playerScav = false) { - throw new NotImplementedException(); + var bodyParts = playerScav + ? GetLowestHpBody(healthObj.BodyParts) + : _randomUtil.GetArrayValue(healthObj.BodyParts); + + BotBaseHealth health = new() + { + Hydration = new() + { + Current = _randomUtil.GetInt((int)healthObj.Hydration.Min, (int)healthObj.Hydration.Max), + Maximum = healthObj.Hydration.Max + }, + Energy = new() + { + Current = _randomUtil.GetInt((int)healthObj.Energy.Min, (int)healthObj.Energy.Max), + Maximum = healthObj.Energy.Max + }, + Temperature = new() + { + Current = _randomUtil.GetInt((int)healthObj.Temperature.Min, (int)healthObj.Temperature.Max), + Maximum = healthObj.Temperature.Max + }, + BodyParts = new Dictionary() + { + { + "Head", new BodyPartHealth + { + Health = new() + { + Current = _randomUtil.GetInt((int)bodyParts.Head.Min, (int)bodyParts.Head.Max), + Maximum = Math.Round(bodyParts.Head.Max ?? 0) + } + } + }, + { + "Chest", new BodyPartHealth + { + Health = new() + { + Current = _randomUtil.GetInt((int)bodyParts.Chest.Min, (int)bodyParts.Chest.Max), + Maximum = Math.Round(bodyParts.Chest.Max ?? 0) + } + } + }, + { + "Stomach", new BodyPartHealth + { + Health = new() + { + Current = _randomUtil.GetInt((int)bodyParts.Stomach.Min, (int)bodyParts.Stomach.Max), + Maximum = Math.Round(bodyParts.Stomach.Max ?? 0) + } + } + }, + { + "LeftArm", new BodyPartHealth + { + Health = new() + { + Current = _randomUtil.GetInt((int)bodyParts.LeftArm.Min, (int)bodyParts.LeftArm.Max), + Maximum = Math.Round(bodyParts.LeftArm.Max ?? 0) + } + } + }, + { + "RightArm", new BodyPartHealth + { + Health = new() + { + Current = _randomUtil.GetInt((int)bodyParts.RightArm.Min, (int)bodyParts.RightArm.Max), + Maximum = Math.Round(bodyParts.RightArm.Max ?? 0) + } + } + }, + { + "LeftLeg", new BodyPartHealth + { + Health = new() + { + Current = _randomUtil.GetInt((int)bodyParts.LeftLeg.Min, (int)bodyParts.LeftLeg.Max), + Maximum = Math.Round(bodyParts.LeftLeg.Max ?? 0) + } + } + }, + { + "RightLeg", new BodyPartHealth + { + Health = new() + { + Current = _randomUtil.GetInt((int)bodyParts.RightLeg.Min, (int)bodyParts.RightLeg.Max), + Maximum = Math.Round(bodyParts.RightLeg.Max ?? 0) + } + } + } + }, + UpdateTime = _timeUtil.GetTimeStamp(), + Immortal = false + }; + + return health; } /// @@ -460,9 +625,33 @@ public class BotGenerator /// /// Body parts to sum up /// Lowest hp collection - public BodyPart? GetLowestHpBody(List bodies) // TODO: there are two types of body parts + public BodyPart? GetLowestHpBody(List bodies) { - throw new NotImplementedException(); + if (bodies.Count == 0) + return null; + + BodyPart result = new(); + var props = result.GetType().GetProperties(); + double? currentHighest = double.MaxValue; + foreach (var bodyPart in bodies) + { + double? hpTotal = 0; + + foreach (var prop in props) + { + var value = (MinMax)prop.GetValue(bodyPart); + hpTotal += value.Max; + } + + if (hpTotal < currentHighest) + { + // Found collection with lower value that previous, use it + currentHighest = hpTotal; + result = bodyPart; + } + } + + return result; } /// @@ -472,7 +661,14 @@ public class BotGenerator /// Skills public Skills GenerateSkills(BotDbSkills botSkills) { - throw new NotImplementedException(); + var skillsToReturn = new Skills + { + Common = GetSkillsWithRandomisedProgressValue(botSkills.Common, true), + Mastering = GetSkillsWithRandomisedProgressValue(botSkills.Mastering, false), + Points = 0 + }; + + return skillsToReturn; } /// @@ -481,19 +677,49 @@ public class BotGenerator /// Skills to randomise /// Are the skills 'common' skills /// Skills with randomised progress values as an array - public List GetSkillsWithRandomisedProgressValue(Dictionary skills, bool isCommonSkills) + public List GetSkillsWithRandomisedProgressValue(Dictionary skills, bool isCommonSkills) { - throw new NotImplementedException(); + if (!skills.Any()) + return new List(); + + return skills.Select(kvp => + { + // Get skill from dict, skip if not found + var skill = kvp.Value; + if (skill == null) + { + return null; + } + + // All skills have id and progress props + var skillToAdd = new BaseSkill + { + Id = kvp.Key, + Progress = _randomUtil.GetInt((int)skill.Min, (int)skill.Max) + }; + + // Common skills have additional props + if (isCommonSkills) + { + ((Common)skillToAdd).PointsEarnedDuringSession = 0; + ((Common)skillToAdd).LastAccess = 0; + } + + return skillToAdd; + }).Where(baseSkill => baseSkill != null).ToList(); } /// /// Generate an id+aid for a bot and apply /// /// bot to update - /// updated IBotBase object // TODO: Node server claims this in summary but is void + /// public void AddIdsToBot(BotBase bot) { - throw new NotImplementedException(); + var botId = _hashUtil.Generate(); + + bot.Id = botId; + bot.Aid = _hashUtil.GenerateAccountId(); } /// @@ -503,7 +729,30 @@ public class BotGenerator /// Profile to update public void GenerateInventoryId(BotBase profile) { - throw new NotImplementedException(); + var newInventoryItemId = _hashUtil.Generate(); + + foreach (var item in profile.Inventory.Items) { + // Root item found, update its _id value to newly generated id + if (item.Template == ItemTpl.INVENTORY_DEFAULT) { + item.Id = newInventoryItemId; + + continue; + } + + // Optimisation - skip items without a parentId + // They are never linked to root inventory item + we already handled root item above + if (item.ParentId is null) { + continue; + } + + // Item is a child of root inventory item, update its parentId value to newly generated id + if (item.ParentId == profile.Inventory.Equipment) { + item.ParentId = newInventoryItemId; + } + } + + // Update inventory equipment id to new one we generated + profile.Inventory.Equipment = newInventoryItemId; } /// @@ -513,19 +762,57 @@ public class BotGenerator /// /// bot info object to update /// Chosen game version - public string SetRandomisedGameVersionAndCategory(Info botInfo) // TODO: there are two types of Info + public string SetRandomisedGameVersionAndCategory(Info botInfo) { - throw new NotImplementedException(); + // Special case + if (botInfo.Nickname?.ToLower() == "nikita") { + botInfo.GameVersion = GameEditions.UNHEARD; + botInfo.MemberCategory = MemberCategory.DEVELOPER; + + return botInfo.GameVersion; + } + + // Choose random weighted game version for bot + botInfo.GameVersion = _weightedRandomHelper.GetWeightedValue(_pmcConfig.GameVersionWeight); + + // Choose appropriate member category value + switch (botInfo.GameVersion) { + case GameEditions.EDGE_OF_DARKNESS: + botInfo.MemberCategory = MemberCategory.UNIQUE_ID; + break; + case GameEditions.UNHEARD: + botInfo.MemberCategory = MemberCategory.UNHEARD; + break; + default: + // Everyone else gets a weighted randomised category + botInfo.MemberCategory = _weightedRandomHelper.GetWeightedValue(_pmcConfig.AccountTypeWeight); + break; + } + + // Ensure selected category matches + botInfo.SelectedMemberCategory = botInfo.MemberCategory; + + return botInfo.GameVersion; } /// /// Add a side-specific (usec/bear) dogtag item to a bots inventory /// /// bot to add dogtag to - /// Bot with dogtag added // TODO: Node server claims this in summary but is void + /// public void AddDogtagToBot(BotBase bot) { - throw new NotImplementedException(); + Item inventoryItem = new () { + Id = _hashUtil.Generate(), + Template = GetDogtagTplByGameVersionAndSide(bot.Info.Side, bot.Info.GameVersion), + ParentId = bot.Inventory.Equipment, + SlotId = "Dogtag", + Upd = new () { + SpawnedInSession = true, + }, + }; + + bot.Inventory.Items.Add(inventoryItem); } /// @@ -536,7 +823,25 @@ public class BotGenerator /// item tpl public string GetDogtagTplByGameVersionAndSide(string side, string gameVersion) { - throw new NotImplementedException(); + if (side.ToLower() == "usec") { + switch (gameVersion) { + case GameEditions.EDGE_OF_DARKNESS: + return ItemTpl.BARTER_DOGTAG_USEC_EOD; + case GameEditions.UNHEARD: + return ItemTpl.BARTER_DOGTAG_USEC_TUE; + default: + return ItemTpl.BARTER_DOGTAG_USEC; + } + } + + switch (gameVersion) { + case GameEditions.EDGE_OF_DARKNESS: + return ItemTpl.BARTER_DOGTAG_BEAR_EOD; + case GameEditions.UNHEARD: + return ItemTpl.BARTER_DOGTAG_BEAR_TUE; + default: + return ItemTpl.BARTER_DOGTAG_BEAR; + } } /// @@ -545,6 +850,9 @@ public class BotGenerator /// Pmc object to adjust public void SetPmcPocketsByGameVersion(BotBase bot) { - throw new NotImplementedException(); + if (bot.Info.GameVersion == GameEditions.UNHEARD) { + var pockets = bot.Inventory.Items.FirstOrDefault((item) => item.SlotId == "Pockets"); + pockets.Template = ItemTpl.POCKETS_1X4_TUE; + } } } diff --git a/Core/Models/Eft/Common/Tables/BotBase.cs b/Core/Models/Eft/Common/Tables/BotBase.cs index 65828e7d..b61ed6e9 100644 --- a/Core/Models/Eft/Common/Tables/BotBase.cs +++ b/Core/Models/Eft/Common/Tables/BotBase.cs @@ -300,15 +300,17 @@ public class BaseJsonSkills public class Skills { - public List? Common { get; set; } + public List? Common { get; set; } - public List? Mastering { get; set; } + public List? Mastering { get; set; } public double? Points { get; set; } } public class BaseSkill { + public int? PointsEarnedDuringSession { get; set; } + public long? LastAccess { get; set; } public string? Id { get; set; } public double? Progress { get; set; } @@ -321,8 +323,7 @@ public class BaseSkill public class Common : BaseSkill { - public int? PointsEarnedDuringSession { get; set; } - public long? LastAccess { get; set; } + } public class Mastering : BaseSkill diff --git a/Core/Models/Spt/Config/PmcConfig.cs b/Core/Models/Spt/Config/PmcConfig.cs index 09ee02e5..a3be7e67 100644 --- a/Core/Models/Spt/Config/PmcConfig.cs +++ b/Core/Models/Spt/Config/PmcConfig.cs @@ -12,11 +12,11 @@ public class PmcConfig : BaseConfig /** What game version should the PMC have */ [JsonPropertyName("gameVersionWeight")] - public Dictionary GameVersionWeight { get; set; } + public Dictionary GameVersionWeight { get; set; } /** What account type should the PMC have */ [JsonPropertyName("accountTypeWeight")] - public Dictionary AccountTypeWeight { get; set; } + public Dictionary AccountTypeWeight { get; set; } /** Global whitelist/blacklist of vest loot for PMCs */ [JsonPropertyName("vestLoot")] diff --git a/Core/Services/ItemFilterService.cs b/Core/Services/ItemFilterService.cs index ed6a865f..bc4622d9 100644 --- a/Core/Services/ItemFilterService.cs +++ b/Core/Services/ItemFilterService.cs @@ -89,4 +89,9 @@ public class ItemFilterService { throw new NotImplementedException(); } + + public static bool IsLootableItemBlacklisted(string itemKey) + { + throw new NotImplementedException(); + } }