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();
+ }
}