Expanded max loot in pmc container systems

Now allows for per-map multipliers + pocket + vest loot uses same level-based value as backpack

labs multiplier = 2
labyrinth = 4

Removed nullability from some properties
This commit is contained in:
Chomp
2025-07-17 18:25:31 +01:00
parent 3463f4e186
commit 50f684537d
11 changed files with 3197 additions and 3105 deletions
File diff suppressed because it is too large Load Diff
@@ -418,6 +418,7 @@ public class BotController(
LocationSpecificPmcLevelOverride = GetPmcLevelRangeForMap(raidSettings?.Location), // Min/max levels for PMCs to generate within
IsPlayerScav = false,
AllPmcsHaveSameNameAsPlayer = allPmcsHaveSameNameAsPlayer,
Location = raidSettings?.Location,
};
}
@@ -0,0 +1,54 @@
using SPTarkov.Server.Core.Models.Spt.Config;
namespace SPTarkov.Server.Core.Extensions
{
/// <summary>
/// Get the rouble amount for the desired container, multiplied by the current map bot will spawn on
/// </summary>
public static class LootContainerSettingsExtensions
{
public static double GetRoubleValue(
this LootContainerSettings settings,
int botLevel,
string locationId
)
{
var roubleTotalByLevel = GetContainerRoubleTotalByLevel(
botLevel,
settings.TotalRubByLevel
);
// Get multiplier for map, use default if map not found
if (!settings.LocationMultipler.TryGetValue(locationId, out var multiplier))
{
settings.LocationMultipler.TryGetValue("default", out multiplier);
}
return roubleTotalByLevel * multiplier;
}
/// <summary>
/// Gets the rouble cost total for loot in a bots backpack by the bots level
/// Will return 0 for non PMCs
/// </summary>
/// <param name="botLevel">level of the bot</param>
/// <param name="containerLootValuesPool">Pocket/vest/backpack</param>
/// <returns>rouble amount</returns>
private static double GetContainerRoubleTotalByLevel(
int botLevel,
List<MinMaxLootValue> containerLootValuesPool
)
{
var matchingValue = containerLootValuesPool.FirstOrDefault(minMaxValue =>
botLevel >= minMaxValue.Min && botLevel <= minMaxValue.Max
);
if (matchingValue is null)
{
return 1;
}
return matchingValue.Value;
}
}
}
@@ -131,10 +131,9 @@ public class BotGenerator(
);
// 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 botRole = botGenerationDetails.IsPmc
? 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 null)
{
@@ -195,7 +194,7 @@ public class BotGenerator(
);
// Only filter bot equipment, never players
if (!botGenerationDetails.IsPlayerScav.GetValueOrDefault(false))
if (!botGenerationDetails.IsPlayerScav)
{
botEquipmentFilterService.FilterBotEquipment(
sessionId,
@@ -213,15 +212,12 @@ public class BotGenerator(
);
// Only Pmcs should have a lower nickname
bot.Info.LowerNickname = botGenerationDetails.IsPmc.GetValueOrDefault(false)
bot.Info.LowerNickname = botGenerationDetails.IsPmc
? bot.Info.Nickname.ToLowerInvariant()
: string.Empty;
// Only run when generating a 'fake' playerscav, not actual player scav
if (
!botGenerationDetails.IsPlayerScav.GetValueOrDefault(false)
&& ShouldSimulatePlayerScav(botRoleLowercase)
)
if (!botGenerationDetails.IsPlayerScav && ShouldSimulatePlayerScav(botRoleLowercase))
{
botNameService.AddRandomPmcNameToBotMainProfileNicknameProperty(bot);
SetRandomisedGameVersionAndCategory(bot.Info);
@@ -242,12 +238,7 @@ public class BotGenerator(
RemoveBlacklistedLootFromBotTemplate(botJsonTemplate.BotInventory);
// Remove hideout data if bot is not a PMC or pscav - match what live sends
if (
!(
botGenerationDetails.IsPmc.GetValueOrDefault(false)
|| botGenerationDetails.IsPlayerScav.GetValueOrDefault(false)
)
)
if (!(botGenerationDetails.IsPmc || botGenerationDetails.IsPlayerScav))
{
bot.Hideout = null;
}
@@ -280,14 +271,11 @@ public class BotGenerator(
customisation.Value.Name.Equals(chosenVoiceName, StringComparison.OrdinalIgnoreCase)
)
.Key;
bot.Health = GenerateHealth(
botJsonTemplate.BotHealth,
botGenerationDetails.IsPlayerScav.GetValueOrDefault(false)
);
bot.Health = GenerateHealth(botJsonTemplate.BotHealth, botGenerationDetails.IsPlayerScav);
bot.Skills = GenerateSkills(botJsonTemplate.BotSkills);
bot.Info.PrestigeLevel = 0;
if (botGenerationDetails.IsPmc.GetValueOrDefault(false))
if (botGenerationDetails.IsPmc)
{
bot.Info.IsStreamerModeAvailable = true; // Set to true so client patches can pick it up later - client sometimes alters botrole to assaultGroup
SetRandomisedGameVersionAndCategory(bot.Info);
@@ -304,7 +292,7 @@ public class BotGenerator(
sessionId,
botJsonTemplate,
botRoleLowercase,
botGenerationDetails.IsPmc.GetValueOrDefault(false),
botGenerationDetails,
bot.Info.Level.Value,
bot.Info.GameVersion
);
@@ -764,9 +752,7 @@ public class BotGenerator(
public void AddIdsToBot(BotBase bot, BotGenerationDetails botGenerationDetails)
{
bot.Id = new MongoId();
bot.Aid = botGenerationDetails.IsPmc.GetValueOrDefault(false)
? hashUtil.GenerateAccountId()
: 0;
bot.Aid = botGenerationDetails.IsPmc ? hashUtil.GenerateAccountId() : 0;
}
/// <summary>
@@ -63,7 +63,7 @@ public class BotInventoryGenerator(
/// <param name="sessionId">Session id</param>
/// <param name="botJsonTemplate">Base json db file for the bot having its loot generated</param>
/// <param name="botRole">Role bot has (assault/pmcBot)</param>
/// <param name="isPmc">Is bot being converted into a pmc</param>
/// <param name="botGenerationDetails">Details related to generating a bot</param>
/// <param name="botLevel">Level of bot being generated</param>
/// <param name="chosenGameVersion">Game version for bot, only really applies for PMCs</param>
/// <returns>PmcInventory object with equipment/weapons/loot</returns>
@@ -71,7 +71,7 @@ public class BotInventoryGenerator(
MongoId sessionId,
BotType botJsonTemplate,
string botRole,
bool isPmc,
BotGenerationDetails botGenerationDetails,
int botLevel,
string chosenGameVersion
)
@@ -80,6 +80,8 @@ public class BotInventoryGenerator(
var wornItemChances = botJsonTemplate.BotChances;
var itemGenerationLimitsMinMax = botJsonTemplate.BotGeneration;
var isPmc = botGenerationDetails.IsPmc;
// Generate base inventory with no items
var botInventory = GenerateInventoryBase();
@@ -116,6 +118,7 @@ public class BotInventoryGenerator(
botLootGenerator.GenerateLoot(
sessionId,
botJsonTemplate,
botGenerationDetails,
isPmc,
botRole,
botInventory,
@@ -30,7 +30,7 @@ public class BotLevelGenerator(
BotBase bot
)
{
if (!botGenerationDetails.IsPmc.GetValueOrDefault(false))
if (!botGenerationDetails.IsPmc)
{
return new RandomisedBotLevelResult { Exp = 0, Level = 1 };
}
@@ -1,4 +1,5 @@
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Extensions;
using SPTarkov.Server.Core.Helpers;
using SPTarkov.Server.Core.Models.Common;
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
@@ -62,6 +63,7 @@ public class BotLootGenerator(
/// </summary>
/// <param name="sessionId">Session id</param>
/// <param name="botJsonTemplate">Clone of Base JSON db file for the bot having its loot generated</param>
/// <param name="botGenerationDetails">Details relating to generating a bot</param>
/// <param name="isPmc">Will bot be a pmc</param>
/// <param name="botRole">Role of bot, e.g. assault</param>
/// <param name="botInventory">Inventory to add loot to</param>
@@ -69,6 +71,7 @@ public class BotLootGenerator(
public void GenerateLoot(
MongoId sessionId,
BotType botJsonTemplate,
BotGenerationDetails botGenerationDetails,
bool isPmc,
string botRole,
BotBaseInventory botInventory,
@@ -300,7 +303,13 @@ public class BotLootGenerator(
);
}
var backpackLootRoubleTotal = GetBackpackRoubleTotalByLevel(botLevel, isPmc);
var backpackLootRoubleTotal = isPmc
? _pmcConfig.LootSettings.Backpack.GetRoubleValue(
botLevel,
botGenerationDetails.Location
)
: 0;
AddLootFromPool(
botLootCacheService.GetLootFromCache(
botRole,
@@ -314,12 +323,16 @@ public class BotLootGenerator(
botInventory,
botRole,
botItemLimits,
backpackLootRoubleTotal ?? 0,
backpackLootRoubleTotal,
isPmc,
filledContainerIds
);
}
var vestLootRoubleTotal = isPmc
? _pmcConfig.LootSettings.Vest.GetRoubleValue(botLevel, botGenerationDetails.Location)
: 0;
// TacticalVest - generate loot if they have one
if (containersBotHasAvailable.Contains(EquipmentSlots.TacticalVest))
// Vest
@@ -337,12 +350,16 @@ public class BotLootGenerator(
botInventory,
botRole,
botItemLimits,
_pmcConfig.MaxVestLootTotalRub,
vestLootRoubleTotal,
isPmc,
filledContainerIds
);
}
var pocketLootRoubleTotal = isPmc
? _pmcConfig.LootSettings.Pocket.GetRoubleValue(botLevel, botGenerationDetails.Location)
: 0;
// Pockets
AddLootFromPool(
botLootCacheService.GetLootFromCache(
@@ -357,7 +374,7 @@ public class BotLootGenerator(
botInventory,
botRole,
botItemLimits,
_pmcConfig.MaxPocketLootTotalRub,
pocketLootRoubleTotal,
isPmc,
filledContainerIds
);
@@ -401,26 +418,6 @@ public class BotLootGenerator(
return matchingValue;
}
/// <summary>
/// Gets the rouble cost total for loot in a bots backpack by the bots levl
/// Will return 0 for non PMCs
/// </summary>
/// <param name="botLevel">Bots level</param>
/// <param name="isPmc">Is the bot a PMC</param>
/// <returns>int</returns>
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;
}
/// <summary>
/// Get an array of the containers a bot has on them (pockets/backpack/vest)
/// </summary>
@@ -12,19 +12,19 @@ public record BotGenerationDetails
/// Should the bot be generated as a PMC
/// </summary>
[JsonPropertyName("isPmc")]
public bool? IsPmc { get; set; }
public bool IsPmc { get; set; }
/// <summary>
/// assault/pmcBot etc
/// </summary>
[JsonPropertyName("role")]
public string? Role { get; set; }
public string Role { get; set; }
/// <summary>
/// Side of bot
/// </summary>
[JsonPropertyName("side")]
public string? Side { get; set; }
public string Side { get; set; }
/// <summary>
/// Active players current level
@@ -57,7 +57,7 @@ public record BotGenerationDetails
/// How many to create and store
/// </summary>
[JsonPropertyName("botCountToGenerate")]
public int? BotCountToGenerate { get; set; }
public int BotCountToGenerate { get; set; }
/// <summary>
/// Desired difficulty of the bot
@@ -69,11 +69,16 @@ public record BotGenerationDetails
/// Will the generated bot be a player scav
/// </summary>
[JsonPropertyName("isPlayerScav")]
public bool? IsPlayerScav { get; set; }
public bool IsPlayerScav { get; set; }
[JsonPropertyName("eventRole")]
public string? EventRole { get; set; }
[JsonPropertyName("allPmcsHaveSameNameAsPlayer")]
public bool? AllPmcsHaveSameNameAsPlayer { get; set; }
public bool AllPmcsHaveSameNameAsPlayer { get; set; }
/// <summary>
/// Map bots will be spawned on
/// </summary>
public string? Location { get; set; }
}
@@ -106,14 +106,8 @@ public record PmcConfig : BaseConfig
Dictionary<string, Dictionary<string, double>>
> PmcType { get; set; }
[JsonPropertyName("maxBackpackLootTotalRub")]
public required List<MinMaxLootValue> MaxBackpackLootTotalRub { get; set; }
[JsonPropertyName("maxPocketLootTotalRub")]
public required int MaxPocketLootTotalRub { get; set; }
[JsonPropertyName("maxVestLootTotalRub")]
public required int MaxVestLootTotalRub { get; set; }
[JsonPropertyName("lootSettings")]
public required PmcLootSettings LootSettings { get; set; }
/// <summary>
/// How many levels above player level can a PMC be
@@ -158,6 +152,27 @@ public record PmcConfig : BaseConfig
public required Dictionary<string, List<BossLocationSpawn>> CustomPmcWaves { get; set; }
}
public record PmcLootSettings
{
[JsonPropertyName("pocket")]
public LootContainerSettings Pocket { get; set; }
[JsonPropertyName("vest")]
public LootContainerSettings Vest { get; set; }
[JsonPropertyName("backpack")]
public LootContainerSettings Backpack { get; set; }
}
public record LootContainerSettings
{
[JsonPropertyName("totalRubByLevel")]
public List<MinMaxLootValue> TotalRubByLevel { get; set; }
[JsonPropertyName("locationMultipler")]
public Dictionary<string, double> LocationMultipler { get; set; }
}
public record HostilitySettings
{
[JsonExtensionData]
@@ -41,7 +41,7 @@ public class BotEquipmentFilterService(
{
var pmcProfile = profileHelper.GetPmcProfile(sessionId);
var botRole = botGenerationDetails.IsPmc ?? false ? "pmc" : botGenerationDetails.Role;
var botRole = botGenerationDetails.IsPmc ? "pmc" : botGenerationDetails.Role;
var botEquipmentBlacklist = GetBotEquipmentBlacklist(botRole, botLevel);
var botEquipmentWhitelist = GetBotEquipmentWhitelist(botRole, botLevel);
var botWeightingAdjustments = GetBotWeightingAdjustments(botRole, botLevel);
@@ -54,15 +54,14 @@ public class BotNameService(
// Never show for players
var showTypeInNickname =
!botGenerationDetails.IsPlayerScav.GetValueOrDefault(false)
&& _botConfig.ShowTypeInNickname;
!botGenerationDetails.IsPlayerScav && _botConfig.ShowTypeInNickname;
var roleShouldBeUnique = uniqueRoles?.Contains(botRole.ToLowerInvariant());
var attempts = 0;
while (attempts <= 5)
{
// Get bot name with leading/trailing whitespace removed
var name = isPmc.GetValueOrDefault(false) // Explicit handling of PMCs, all other bots will get "first_name last_name"
var name = isPmc // Explicit handling of PMCs, all other bots will get "first_name last_name"
? botHelper.GetPmcNicknameOfMaxLength(
_botConfig.BotNameLengthLimit,
botGenerationDetails.Side
@@ -78,10 +77,7 @@ public class BotNameService(
}
// Replace pmc bot names with player name + prefix
if (
botGenerationDetails.IsPmc.GetValueOrDefault(false)
&& botGenerationDetails.AllPmcsHaveSameNameAsPlayer.GetValueOrDefault(false)
)
if (botGenerationDetails.IsPmc && botGenerationDetails.AllPmcsHaveSameNameAsPlayer)
{
var prefix = serverLocalisationService.GetRandomTextThatMatchesPartialKey(
"pmc-name_prefix_"