This commit is contained in:
Alex
2025-01-15 11:50:55 +00:00
127 changed files with 13130 additions and 9952 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
using Core.Annotations;
using Core.Annotations;
using Core.Context;
using Core.Controllers;
using Core.Models.Eft.Bot;
@@ -60,7 +60,7 @@ public class BotCallbacks
if (difficulty == "core")
return _httpResponseUtil.NoBody(_botController.GetBotCoreDifficulty());
var raidConfig = (GetRaidConfigurationRequestData)_applicationContext.GetLatestValue(ContextVariableType.RAID_CONFIGURATION)?.Value;
var raidConfig = _applicationContext.GetLatestValue(ContextVariableType.RAID_CONFIGURATION)?.GetValue<GetRaidConfigurationRequestData>();
return _httpResponseUtil.NoBody(_botController.GetBotDifficulty(type, difficulty, raidConfig));
}
+5 -3
View File
@@ -49,7 +49,8 @@ public class DataCallbacks
/// <returns></returns>
public string GetSettings(string url, EmptyRequestData info, string sessionID)
{
return _httpResponseUtil.GetBody(_databaseService.GetSettings());
var returns = _httpResponseUtil.GetBody(_databaseService.GetSettings());
return returns;
}
/// <summary>
@@ -62,9 +63,10 @@ public class DataCallbacks
public string GetGlobals(string url, EmptyRequestData info, string sessionID)
{
var globals = _databaseService.GetGlobals();
globals.Time = _timeUtil.GetTimeStamp();
globals.Time = _timeUtil.GetTimeStamp() / 1000;
var returns = _httpResponseUtil.GetBody(globals);
return _httpResponseUtil.GetBody(globals);
return returns;
}
/// <summary>
+47 -129
View File
@@ -2,6 +2,7 @@ using Core.Annotations;
using Core.Controllers;
using Core.DI;
using Core.Models.Eft.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Hideout;
using Core.Models.Eft.ItemEvent;
using Core.Models.Enums;
@@ -13,9 +14,9 @@ namespace Core.Callbacks;
[Injectable(InjectableTypeOverride = typeof(OnUpdate), TypePriority = OnUpdateOrder.HideoutCallbacks)]
public class HideoutCallbacks : OnUpdate
{
protected HideoutController _hideoutController;
protected ConfigServer _configServer;
protected HideoutConfig _hideoutConfig;
private readonly HideoutController _hideoutController;
private readonly ConfigServer _configServer;
private readonly HideoutConfig _hideoutConfig;
public HideoutCallbacks
(
@@ -31,227 +32,144 @@ public class HideoutCallbacks : OnUpdate
/// <summary>
/// Handle HideoutUpgrade event
/// </summary>
/// <param name="pmcData"></param>
/// <param name="info"></param>
/// <param name="sessionID"></param>
/// <returns></returns>
public ItemEventRouterResponse Upgrade(PmcData pmcData, HideoutUpgradeRequestData info, string sessionID, ItemEventRouterResponse output)
public ItemEventRouterResponse Upgrade(PmcData pmcData, HideoutUpgradeRequestData request, string sessionID, ItemEventRouterResponse output)
{
// _hideoutController.StartUpgrade(pmcData, info, sessionID, output);
// TODO: HideoutController is not implemented rn
_hideoutController.StartUpgrade(pmcData, request, sessionID, output);
return output;
}
/// <summary>
/// Handle HideoutUpgradeComplete event
/// </summary>
/// <param name="pmcData"></param>
/// <param name="info"></param>
/// <param name="sessionID"></param>
/// <returns></returns>
public ItemEventRouterResponse UpgradeComplete(PmcData pmcData, HideoutUpgradeCompleteRequestData info, string sessionID, ItemEventRouterResponse output)
public ItemEventRouterResponse UpgradeComplete(PmcData pmcData, HideoutUpgradeCompleteRequestData request, string sessionID, ItemEventRouterResponse output)
{
// _hideoutController.UpgradeComplete(pmcData, info, sessionID, output);
// TODO: HideoutController is not implemented rn
_hideoutController.UpgradeComplete(pmcData, request, sessionID, output);
return output;
}
/// <summary>
/// Handle HideoutPutItemsInAreaSlots
/// </summary>
/// <param name="pmcData"></param>
/// <param name="info"></param>
/// <param name="sessionID"></param>
/// <returns></returns>
public ItemEventRouterResponse PutItemsInAreaSlots(PmcData pmcData, HideoutPutItemInRequestData info, string sessionID)
public ItemEventRouterResponse PutItemsInAreaSlots(PmcData pmcData, HideoutPutItemInRequestData request, string sessionID)
{
// return _hideoutController.PutItemsInAreaSlots(pmcData, info, sessionID);
// TODO: HideoutController is not implemented rn
throw new NotImplementedException();
return _hideoutController.PutItemsInAreaSlots(pmcData, request, sessionID);
}
/// <summary>
/// Handle HideoutTakeItemsFromAreaSlots event
/// </summary>
/// <param name="pmcData"></param>
/// <param name="info"></param>
/// <param name="sessionID"></param>
/// <returns></returns>
public ItemEventRouterResponse TakeItemsFromAreaSlots(PmcData pmcData, HideoutTakeItemOutRequestData info, string sessionID)
public ItemEventRouterResponse TakeItemsFromAreaSlots(PmcData pmcData, HideoutTakeItemOutRequestData request, string sessionID)
{
// return _hideoutController.TakeItemsFromAreaSlots(pmcData, info, sessionID);
// TODO: HideoutController is not implemented rn
throw new NotImplementedException();
return _hideoutController.TakeItemsFromAreaSlots(pmcData, request, sessionID);
}
/// <summary>
/// Handle HideoutToggleArea event
/// </summary>
/// <param name="pmcData"></param>
/// <param name="info"></param>
/// <param name="sessionID"></param>
/// <returns></returns>
public ItemEventRouterResponse ToggleArea(PmcData pmcData, HideoutToggleAreaRequestData info, string sessionID)
public ItemEventRouterResponse ToggleArea(PmcData pmcData, HideoutToggleAreaRequestData request, string sessionID)
{
// return _hideoutController.ToggleArea(pmcData, info, sessionID);
// TODO: HideoutController is not implemented rn
throw new NotImplementedException();
return _hideoutController.ToggleArea(pmcData, request, sessionID);
}
/// <summary>
/// Handle HideoutSingleProductionStart event
/// </summary>
/// <param name="pmcData"></param>
/// <param name="info"></param>
/// <param name="sessionID"></param>
/// <returns></returns>
public ItemEventRouterResponse SingleProductionStart(PmcData pmcData, HideoutSingleProductionStartRequestData info, string sessionID)
public ItemEventRouterResponse SingleProductionStart(PmcData pmcData, HideoutSingleProductionStartRequestData request, string sessionID)
{
// return _hideoutController.SingleProductionStart(pmcData, info, sessionID);
// TODO: HideoutController is not implemented rn
throw new NotImplementedException();
return _hideoutController.SingleProductionStart(pmcData, request, sessionID);
}
/// <summary>
/// Handle HideoutScavCaseProductionStart event
/// </summary>
/// <param name="pmcData"></param>
/// <param name="info"></param>
/// <param name="sessionID"></param>
/// <returns></returns>
public ItemEventRouterResponse ScavCaseProductionStart(PmcData pmcData, HideoutScavCaseStartRequestData info, string sessionID)
public ItemEventRouterResponse ScavCaseProductionStart(PmcData pmcData, HideoutScavCaseStartRequestData request, string sessionID)
{
// return _hideoutController.ScavCaseProductionStart(pmcData, info, sessionID);
// TODO: HideoutController is not implemented rn
throw new NotImplementedException();
return _hideoutController.ScavCaseProductionStart(pmcData, request, sessionID);
}
/// <summary>
/// Handle HideoutContinuousProductionStart
/// </summary>
/// <param name="pmcData"></param>
/// <param name="info"></param>
/// <param name="sessionID"></param>
/// <returns></returns>
public ItemEventRouterResponse ContinuousProductionStart(PmcData pmcData, HideoutContinuousProductionStartRequestData info, string sessionID)
public ItemEventRouterResponse ContinuousProductionStart(PmcData pmcData, HideoutContinuousProductionStartRequestData request, string sessionID)
{
// return _hideoutController.ContinuousProductionStart(pmcData, info, sessionID);
// TODO: HideoutController is not implemented rn
throw new NotImplementedException();
return _hideoutController.ContinuousProductionStart(pmcData, request, sessionID);
}
/// <summary>
/// Handle HideoutTakeProduction event
/// </summary>
/// <param name="pmcData"></param>
/// <param name="info"></param>
/// <param name="sessionID"></param>
/// <returns></returns>
public ItemEventRouterResponse TakeProduction(PmcData pmcData, HideoutTakeProductionRequestData info, string sessionID)
public ItemEventRouterResponse TakeProduction(PmcData pmcData, HideoutTakeProductionRequestData request, string sessionID)
{
// return _hideoutController.TakeProduction(pmcData, info, sessionID);
// TODO: HideoutController is not implemented rn
throw new NotImplementedException();
return _hideoutController.TakeProduction(pmcData, request, sessionID);
}
/// <summary>
/// Handle HideoutQuickTimeEvent
/// </summary>
/// <param name="pmcData"></param>
/// <param name="info"></param>
/// <param name="sessionID"></param>
/// <param name="output"></param>
/// <returns></returns>
public ItemEventRouterResponse HandleQTEEvent(PmcData pmcData, HandleQTEEventRequestData info, string sessionID, ItemEventRouterResponse output)
public ItemEventRouterResponse HandleQTEEvent(PmcData pmcData, HandleQTEEventRequestData request, string sessionID, ItemEventRouterResponse output)
{
// _hideoutController.HandleQTEEventOutcome(sessionID, pmcData, info, output);
// TODO: HideoutController is not implemented rn
_hideoutController.HandleQTEEventOutcome(sessionID, pmcData, request, output);
return output;
}
/// <summary>
/// Handle client/game/profile/items/moving - RecordShootingRangePoints
/// </summary>
/// <param name="pmcData"></param>
/// <param name="info"></param>
/// <param name="sessionID"></param>
/// <param name="output"></param>
/// <returns></returns>
public ItemEventRouterResponse RecordShootingRangePoints(PmcData pmcData, RecordShootingRangePoints info, string sessionID, ItemEventRouterResponse output)
public ItemEventRouterResponse RecordShootingRangePoints(PmcData pmcData, RecordShootingRangePoints request, string sessionID, ItemEventRouterResponse output)
{
// _hideoutController.RecordShootingRangePoints(sessionID, pmcData, info);
// TODO: HideoutController is not implemented rn
_hideoutController.RecordShootingRangePoints(sessionID, pmcData, request);
return output;
}
/// <summary>
/// Handle client/game/profile/items/moving - RecordShootingRangePoints
/// </summary>
/// <param name="pmcData"></param>
/// <param name="info"></param>
/// <param name="sessionID"></param>
/// <returns></returns>
public ItemEventRouterResponse ImproveArea(PmcData pmcData, HideoutImproveAreaRequestData info, string sessionID)
public ItemEventRouterResponse ImproveArea(PmcData pmcData, HideoutImproveAreaRequestData request, string sessionID)
{
// return _hideoutController.ImproveArea(sessionID, pmcData, info);
// TODO: HideoutController is not implemented rn
throw new NotImplementedException();
return _hideoutController.ImproveArea(sessionID, pmcData, request);
}
/// <summary>
/// Handle client/game/profile/items/moving - HideoutCancelProductionCommand
/// </summary>
/// <param name="pmcData"></param>
/// <param name="info"></param>
/// <param name="sessionID"></param>
/// <returns></returns>
public ItemEventRouterResponse CancelProduction(PmcData pmcData, HideoutImproveAreaRequestData info, string sessionID)
public ItemEventRouterResponse CancelProduction(PmcData pmcData, HideoutImproveAreaRequestData request, string sessionID)
{
// return _hideoutController.CancelProduction(sessionID, pmcData, info);
// TODO: HideoutController is not implemented rn
throw new NotImplementedException();
return _hideoutController.CancelProduction(sessionID, pmcData, request);
}
/// <summary>
/// Handle client/game/profile/items/moving - HideoutCircleOfCultistProductionStart
/// </summary>
/// <param name="pmcData"></param>
/// <param name="info"></param>
/// <param name="sessionID"></param>
/// <returns></returns>
public ItemEventRouterResponse CicleOfCultistProductionStart(PmcData pmcData, HideoutCircleOfCultistProductionStartRequestData info, string sessionID)
public ItemEventRouterResponse CicleOfCultistProductionStart(PmcData pmcData, HideoutCircleOfCultistProductionStartRequestData request, string sessionID)
{
// return _hideoutController.CicleOfCultistProductionStart(sessionID, pmcData, info);
// TODO: HideoutController is not implemented rn
throw new NotImplementedException();
return _hideoutController.CicleOfCultistProductionStart(sessionID, pmcData, request);
}
/// <summary>
/// Handle client/game/profile/items/moving - HideoutDeleteProductionCommand
/// </summary>
/// <param name="pmcData"></param>
/// <param name="info"></param>
/// <param name="sessionID"></param>
/// <returns></returns>
public ItemEventRouterResponse HideoutDeleteProductionCommand(PmcData pmcData, HideoutDeleteProductionRequestData info, string sessionID)
public ItemEventRouterResponse HideoutDeleteProductionCommand(PmcData pmcData, HideoutDeleteProductionRequestData request, string sessionID)
{
// return _hideoutController.HideoutDeleteProductionCommand(sessionID, pmcData, info);
// TODO: HideoutController is not implemented rn
throw new NotImplementedException();
return _hideoutController.HideoutDeleteProductionCommand(sessionID, pmcData, request);
}
/// <summary>
/// Handle client/game/profile/items/moving - HideoutCustomizationApply
/// </summary>
/// <param name="pmcData"></param>
/// <param name="info"></param>
/// <param name="sessionID"></param>
/// <returns></returns>
public ItemEventRouterResponse HideoutCustomizationApplyCommand(PmcData pmcData, HideoutCustomizationApplyRequestData info, string sessionID)
public ItemEventRouterResponse HideoutCustomizationApplyCommand(PmcData pmcData, HideoutCustomizationApplyRequestData request, string sessionID)
{
// return _hideoutController.HideoutCustomizationApply(sessionID, pmcData, info);
// TODO: HideoutController is not implemented rn
throw new NotImplementedException();
return _hideoutController.HideoutCustomizationApply(sessionID, pmcData, request);
}
/**
* Handle client/game/profile/items/moving - hideoutCustomizationSetMannequinPose
*/
public ItemEventRouterResponse HideoutCustomizationSetMannequinPose(PmcData pmcData, HideoutCustomizationSetMannequinPoseRequest request, string sessionId) {
return _hideoutController.HideoutCustomizationSetMannequinPose(sessionId, pmcData, request);
}
public async Task<bool> OnUpdate(long timeSinceLastRun)
+2 -2
View File
@@ -1,4 +1,4 @@
using Core.Annotations;
using Core.Annotations;
using Core.Context;
using Core.DI;
using Core.Servers;
@@ -18,7 +18,7 @@ public class HttpCallbacks : OnLoad
public async Task OnLoad()
{
_httpServer.Load((WebApplicationBuilder) _applicationContext.GetLatestValue(ContextVariableType.APP_BUILDER).Value);
_httpServer.Load( _applicationContext.GetLatestValue(ContextVariableType.APP_BUILDER).GetValue<WebApplicationBuilder>());
_applicationContext.ClearValues(ContextVariableType.APP_BUILDER);
}
+27 -6
View File
@@ -1,8 +1,29 @@
namespace Core.Context;
namespace Core.Context;
public class ContextVariable(object value, ContextVariableType contextVariableType)
public class ContextVariable
{
public object Value { get; } = value;
public DateTime Timestamp { get; } = DateTime.Now;
public ContextVariableType Type { get; } = contextVariableType;
}
private readonly object _value;
private readonly ContextVariableType _internalType;
private readonly DateTime _timestamp;
public ContextVariable(object value, ContextVariableType contextVariableInternalType)
{
_value = value;
_timestamp = DateTime.Now;
_internalType = contextVariableInternalType;
}
public T GetValue<T>() {
return (T)_value;
}
public DateTime GetTimestamp()
{
return _timestamp;
}
public ContextVariableType GetContextType()
{
return _internalType;
}
}
+263 -18
View File
@@ -1,4 +1,4 @@
using Core.Annotations;
using Core.Annotations;
using Core.Context;
using Core.Generators;
using Core.Helpers;
@@ -14,7 +14,6 @@ using Core.Servers;
using Core.Services;
using Core.Utils;
using Core.Utils.Cloners;
using Condition = Core.Models.Spt.Config.Condition;
using ILogger = Core.Models.Utils.ILogger;
namespace Core.Controllers;
@@ -33,6 +32,7 @@ public class BotController
protected MatchBotDeatilsCacheService _matchBotDeatilsCacheService;
protected LocalisationService _localisationService;
protected SeasonalEventService _seasonalEventService;
private readonly MatchBotDetailsCacheService _matchBotDetailsCacheService;
protected ProfileHelper _profileHelper;
protected ConfigServer _configServer;
protected ApplicationContext _applicationContext;
@@ -54,6 +54,7 @@ public class BotController
MatchBotDeatilsCacheService matchBotDeatilsCacheService,
LocalisationService localisationService,
SeasonalEventService seasonalEventService,
MatchBotDetailsCacheService matchBotDetailsCacheService,
ProfileHelper profileHelper,
ConfigServer configServer,
ApplicationContext applicationContext,
@@ -71,6 +72,7 @@ public class BotController
_matchBotDeatilsCacheService = matchBotDeatilsCacheService;
_localisationService = localisationService;
_seasonalEventService = seasonalEventService;
_matchBotDetailsCacheService = matchBotDetailsCacheService;
_profileHelper = profileHelper;
_configServer = configServer;
_applicationContext = applicationContext;
@@ -129,33 +131,264 @@ public class BotController
public List<BotBase> Generate(string sessionId, GenerateBotsRequestData info)
{
throw new NotImplementedException();
var pmcProfile = _profileHelper.GetPmcProfile(sessionId);
// Use this opportunity to create and cache bots for later retrieval
var multipleBotTypesRequested = info.Conditions.Count > 1;
if (multipleBotTypesRequested)
{
return GenerateMultipleBotsAndCache(info, pmcProfile, sessionId);
}
return ReturnSingleBotFromCache(sessionId, info);
}
public async Task<List<BotBase>> GenerateMultipleBotsAndCache()
public List<BotBase> GenerateMultipleBotsAndCache(GenerateBotsRequestData request, PmcData pmcProfile, string sessionId)
{
throw new NotImplementedException();
var raidSettings = GetMostRecentRaidSettings();
var allPmcsHaveSameNameAsPlayer = _randomUtil.GetChance100(
_pmcConfig.AllPMCsHavePlayerNameWithRandomPrefixChance);
// Map conditions to promises for bot generation
foreach (var condition in request.Conditions)
{
var botGenerationDetails = GetBotGenerationDetailsForWave(
condition,
pmcProfile,
allPmcsHaveSameNameAsPlayer,
raidSettings,
_botConfig.PresetBatch.GetValueOrDefault(condition.Role, 15),
_botHelper.IsBotPmc(condition.Role));
// Generate bots for the current condition
GenerateWithBotDetails(condition, botGenerationDetails, sessionId);
}
return [];
}
private void GenerateWithBotDetails(GenerateCondition condition, BotGenerationDetails botGenerationDetails, string sessionId)
{
var isEventBot = condition.Role.ToLower().Contains("event");
if (isEventBot)
{
// Add eventRole data + reassign role property to be base type
botGenerationDetails.EventRole = condition.Role;
botGenerationDetails.Role = _seasonalEventService.GetBaseRoleForEventBot(
botGenerationDetails.EventRole);
}
// Create a compound key to store bots in cache against
var cacheKey = _botGenerationCacheService.CreateCacheKey(
botGenerationDetails.EventRole ?? botGenerationDetails.Role,
botGenerationDetails.BotDifficulty);
// Get number of bots we have in cache
var botCacheCount = _botGenerationCacheService.GetCachedBotCount(cacheKey);
if (botCacheCount >= botGenerationDetails.BotCountToGenerate)
{
_logger.Debug($"Cache already has sufficient bots: { botCacheCount}");
return;
}
// We're below desired count, add bots to cache
var botsToGenerate = botGenerationDetails.BotCountToGenerate - botCacheCount;
var progressWriter = new ProgressWriter(botGenerationDetails.BotCountToGenerate.Value);
_logger.Debug($"Generating { botsToGenerate} bots for cacheKey: {cacheKey}");
for (var i = 0; i < botsToGenerate; i++)
{
try
{
var detailsClone = _cloner.Clone(botGenerationDetails);
GenerateSingleBotAndStoreInCache(detailsClone, sessionId, cacheKey);
progressWriter.Increment();
}
catch (Exception e)
{
_logger.Error($"Failed to generate bot #{i + 1}: {e.Message}");
}
}
_logger.Debug($"Generated { botGenerationDetails.BotCountToGenerate} { botGenerationDetails.Role}" +
$"({botGenerationDetails.EventRole ?? botGenerationDetails.Role ?? ""}) { botGenerationDetails.BotDifficulty}bots");
}
private List<BotBase> ReturnSingleBotFromCache(string sessionId, GenerateBotsRequestData request)
{
var pmcProfile = _profileHelper.GetPmcProfile(sessionId);
var requestedBot = request.Conditions[0];
var raidSettings = GetMostRecentRaidSettings();
// Create generation request for when cache is empty
var condition = new GenerateCondition{
Role = requestedBot.Role,
Limit= 5,
Difficulty= requestedBot.Difficulty,
};
var botGenerationDetails = GetBotGenerationDetailsForWave(
condition,
pmcProfile,
false,
raidSettings,
_botConfig.PresetBatch[requestedBot.Role],
_botHelper.IsBotPmc(requestedBot.Role)
);
// Event bots need special actions to occur, set data up for them
var isEventBot = requestedBot.Role.ToLower().Contains("event");
if (isEventBot)
{
// Add eventRole data + reassign role property
botGenerationDetails.EventRole = requestedBot.Role;
botGenerationDetails.Role = _seasonalEventService.GetBaseRoleForEventBot(
botGenerationDetails.EventRole);
}
// Does non pmc bot have a chance of being converted into a pmc
var convertIntoPmcChanceMinMax = GetPmcConversionMinMaxForLocation(
requestedBot.Role,
raidSettings?.Location);
if (convertIntoPmcChanceMinMax is not null && !botGenerationDetails.IsPmc.Value)
{
// Bot has % chance to become pmc and isnt one pmc already
var convertToPmc = _botHelper.RollChanceToBePmc(convertIntoPmcChanceMinMax);
if (convertToPmc)
{
// Update requirements
botGenerationDetails.IsPmc = true;
botGenerationDetails.Role = _botHelper.GetRandomizedPmcRole();
botGenerationDetails.Side = _botHelper.GetPmcSideByRole(botGenerationDetails.Role);
botGenerationDetails.BotDifficulty = GetPmcDifficulty(requestedBot.Difficulty);
botGenerationDetails.BotCountToGenerate = _botConfig.PresetBatch[botGenerationDetails.Role];
}
}
// Only convert to boss when not already converted to PMC & Boss Convert is enabled
var bossConvertEnabled = _botConfig.AssaultToBossConversion.BossConvertEnabled;
var bossConvertMinMax = _botConfig.AssaultToBossConversion.BossConvertMinMax;
var bossesToConvertToWeights = _botConfig.AssaultToBossConversion.BossesToConvertToWeights;
if (bossConvertEnabled && !botGenerationDetails.IsPmc.Value)
{
var bossConvertPercent = bossConvertMinMax[requestedBot.Role.ToLower()];
if (bossConvertPercent is not null)
{
// Roll a percentage check if we should convert scav to boss
if (
_randomUtil.GetChance100(_randomUtil.GetDouble(bossConvertPercent.Min.Value, bossConvertPercent.Max.Value))
)
{
UpdateBotGenerationDetailsToRandomBoss(botGenerationDetails, bossesToConvertToWeights);
}
}
}
// Create a compound key to store bots in cache against
var cacheKey = _botGenerationCacheService.CreateCacheKey(
botGenerationDetails.EventRole ?? botGenerationDetails.Role,
botGenerationDetails.BotDifficulty);
// Check cache for bot using above key
if (!_botGenerationCacheService.CacheHasBotWithKey(cacheKey))
{
// No bot in cache, generate new and store in cache
GenerateSingleBotAndStoreInCache(botGenerationDetails, sessionId, cacheKey);
_logger.Debug($"Generated { botGenerationDetails.BotCountToGenerate} " +
$"{ botGenerationDetails.Role} ({botGenerationDetails.EventRole ?? ""}) {botGenerationDetails.BotDifficulty} bots");
}
var desiredBot = _botGenerationCacheService.GetBot(cacheKey);
_botGenerationCacheService.StoreUsedBot(desiredBot);
return [desiredBot];
}
private void GenerateSingleBotAndStoreInCache(BotGenerationDetails botGenerationDetails, string sessionId, string cacheKey)
{
var botToCache = _botGenerator.PrepareAndGenerateBot(sessionId, botGenerationDetails);
_botGenerationCacheService.StoreBots(cacheKey, [botToCache]);
// Store bot details in cache so post-raid PMC messages can use data
_matchBotDetailsCacheService.CacheBot(botToCache);
}
private void UpdateBotGenerationDetailsToRandomBoss(BotGenerationDetails botGenerationDetails, Dictionary<string, int> bossesToConvertToWeights)
{
// Seems Actual bosses have the same Brain issues like PMC gaining Boss Brains We can't use all bosses
botGenerationDetails.Role = _weightedRandomHelper.GetWeightedValue(bossesToConvertToWeights);
// Bosses are only ever 'normal'
botGenerationDetails.BotDifficulty = "normal";
botGenerationDetails.BotCountToGenerate = _botConfig.PresetBatch[botGenerationDetails.Role];
}
private string GetPmcDifficulty(string requestedBotDifficulty)
{
var difficulty = _pmcConfig.Difficulty.ToLower();
return difficulty switch
{
"asonline" => requestedBotDifficulty,
"random" => _botDifficultyHelper.ChooseRandomDifficulty(),
_ => _pmcConfig.Difficulty
};
}
private MinMax GetPmcConversionMinMaxForLocation(string requestedBotRole, string location)
{
var mapSpecificConversionValues = _pmcConfig.ConvertIntoPmcChance.GetValueOrDefault(location.ToLower(), null);
if (mapSpecificConversionValues is null)
{
return _pmcConfig.ConvertIntoPmcChance["default"][requestedBotRole];
}
return mapSpecificConversionValues[requestedBotRole.ToLower()];
}
public GetRaidConfigurationRequestData GetMostRecentRaidSettings()
{
throw new NotImplementedException();
var raidSettings = _applicationContext
.GetLatestValue(ContextVariableType.RAID_CONFIGURATION)
.GetValue<GetRaidConfigurationRequestData>();
if (raidSettings is null)
{
_logger.Warning(_localisationService.GetText("bot-unable_to_load_raid_settings_from_appcontext"));
}
return raidSettings;
}
public MinMax GetPmcLevelRangeForMap(string location)
{
throw new NotImplementedException();
return _pmcConfig.LocationSpecificPmcLevelOverride.GetValueOrDefault(location.ToLower(), null);
}
public BotGenerationDetails GetBotGenerationDetailsForWave(Condition condition, PmcData pmcProfile, bool AllPmcsHaveSameNameAsPlayer,
GetRaidConfigurationRequestData raidSettings, int botCountToGenerate, bool generateAsPmc)
public BotGenerationDetails GetBotGenerationDetailsForWave(
GenerateCondition condition,
PmcData pmcProfile,
bool allPmcsHaveSameNameAsPlayer,
GetRaidConfigurationRequestData raidSettings,
int botCountToGenerate,
bool generateAsPmc)
{
throw new NotImplementedException();
}
public int GetPlayerLevelFromProfile()
{
throw new NotImplementedException();
return new BotGenerationDetails{
IsPmc = generateAsPmc,
Side = generateAsPmc ? _botHelper.GetPmcSideByRole(condition.Role) : "Savage",
Role = condition.Role,
PlayerLevel = pmcProfile.Info.Level.Value,
PlayerName = pmcProfile.Info.Nickname,
BotRelativeLevelDeltaMax = _pmcConfig.BotRelativeLevelDeltaMax,
BotRelativeLevelDeltaMin = _pmcConfig.BotRelativeLevelDeltaMin,
BotCountToGenerate = botCountToGenerate,
BotDifficulty = condition.Difficulty,
LocationSpecificPmcLevelOverride = this.GetPmcLevelRangeForMap(raidSettings?.Location), // Min/max levels for PMCs to generate within
IsPlayerScav = false,
AllPmcsHaveSameNameAsPlayer = allPmcsHaveSameNameAsPlayer,
};
}
public int GetBotLimit(string type)
@@ -181,11 +414,23 @@ public class BotController
public int GetBotCap(string location)
{
throw new NotImplementedException();
var botCap = _botConfig.MaxBotCap[location.ToLower()];
if (location == "default")
{
_logger.Warning(
_localisationService.GetText("bot-no_bot_cap_found_for_location", location.ToLower()));
}
return botCap;
}
public object GetAiBotBrainTypes() // TODO: Returns `any` in the node server
public object GetAiBotBrainTypes()
{
throw new NotImplementedException();
// TODO: Returns `any` in the node server
return new {
pmc = _pmcConfig.PmcType,
assault = _botConfig.AssaultBrainType,
playerScav = _botConfig.PlayerScavBrainType,
};
}
}
+24 -1
View File
@@ -5,6 +5,7 @@ using Core.Models.Eft.Dialog;
using Core.Models.Eft.HttpResponse;
using Core.Models.Eft.Profile;
using Core.Models.Enums;
using Core.Models.Spt.Config;
using Core.Servers;
namespace Core.Controllers;
@@ -14,17 +15,23 @@ public class DialogueController
{
private readonly DialogueHelper _dialogueHelper;
private readonly ProfileHelper _profileHelper;
private readonly ConfigServer _configServer;
private readonly SaveServer _saveServer;
private readonly List<IDialogueChatBot> _dialogueChatBots;
private readonly CoreConfig _coreConfig;
public DialogueController(
DialogueHelper dialogueHelper,
ProfileHelper profileHelper,
ConfigServer configServer,
SaveServer saveServer)
{
_dialogueHelper = dialogueHelper;
_profileHelper = profileHelper;
_configServer = configServer;
_saveServer = saveServer;
_coreConfig = _configServer.GetConfig<CoreConfig>(ConfigTypes.CORE);
}
/// <summary>
@@ -54,7 +61,7 @@ public class DialogueController
public GetFriendListDataResponse GetFriendList(string sessionId)
{
// Add all chatbots to the friends list
var friends = _dialogueChatBots.Select((bot) => bot.GetChatBot()).ToList();
var friends = GetActiveChatBots();
// Add any friends the user has after the chatbots
var profile = _profileHelper.GetFullProfile(sessionId);
@@ -82,6 +89,22 @@ public class DialogueController
};
}
private List<UserDialogInfo> GetActiveChatBots()
{
var activeBots = new List<UserDialogInfo>();
var chatBotConfig = _coreConfig.Features.ChatbotFeatures;
foreach (var bot in _dialogueChatBots)
{
var botData = bot.GetChatBot();
if (chatBotConfig.EnabledBots.ContainsKey(botData.Id)) {
activeBots.Add(botData);
}
}
return activeBots;
}
/// <summary>
/// Handle client/mail/dialog/list
/// Create array holding trader dialogs and mail interactions with player
+184 -1
View File
@@ -1,9 +1,192 @@
using Core.Annotations;
using Core.Generators;
using Core.Helpers;
using Core.Models.Eft.Common;
using Core.Models.Eft.Hideout;
using Core.Models.Eft.ItemEvent;
using Core.Models.Enums;
using Core.Models.Spt.Config;
using Core.Routers;
using Core.Servers;
using Core.Services;
using Core.Utils;
using Core.Utils.Cloners;
using ILogger = Core.Models.Utils.ILogger;
namespace Core.Controllers;
[Injectable]
public class HideoutController
{
// TODO
private readonly ILogger _logger;
private readonly HashUtil _hashUtil;
private readonly TimeUtil _timeUtil;
private readonly DatabaseService _databaseService;
private readonly RandomUtil _randomUtil;
private readonly InventoryHelper _inventoryHelper;
private readonly ItemHelper _itemHelper;
private readonly SaveServer _saveServer;
private readonly PlayerService _playerService;
private readonly PresetHelper _presetHelper;
private readonly PaymentHelper _paymentHelper;
private readonly EventOutputHolder _eventOutputHolder;
private readonly HttpResponseUtil _httpResponseUtil;
private readonly ProfileHelper _profileHelper;
private readonly HideoutHelper _hideoutHelper;
private readonly ScavCaseRewardGenerator _scavCaseRewardGenerator;
private readonly LocalisationService _localisationService;
private readonly ProfileActivityService _profileActivityService;
private readonly FenceService _fenceService;
private readonly CircleOfCultistService _circleOfCultistService;
private readonly ICloner _cloner;
private readonly ConfigServer _configServer;
private readonly HideoutConfig _hideoutConfig;
public HideoutController(
ILogger logger,
HashUtil hashUtil,
TimeUtil timeUtil,
DatabaseService databaseService,
RandomUtil randomUtil,
InventoryHelper inventoryHelper,
ItemHelper itemHelper,
SaveServer saveServer,
PlayerService playerService,
PresetHelper presetHelper,
PaymentHelper paymentHelper,
EventOutputHolder eventOutputHolder,
HttpResponseUtil httpResponseUtil,
ProfileHelper profileHelper,
HideoutHelper hideoutHelper,
ScavCaseRewardGenerator scavCaseRewardGenerator,
LocalisationService localisationService,
ProfileActivityService profileActivityService,
FenceService fenceService,
CircleOfCultistService circleOfCultistService,
ICloner cloner,
ConfigServer configServer)
{
_logger = logger;
_hashUtil = hashUtil;
_timeUtil = timeUtil;
_databaseService = databaseService;
_randomUtil = randomUtil;
_inventoryHelper = inventoryHelper;
_itemHelper = itemHelper;
_saveServer = saveServer;
_playerService = playerService;
_presetHelper = presetHelper;
_paymentHelper = paymentHelper;
_eventOutputHolder = eventOutputHolder;
_httpResponseUtil = httpResponseUtil;
_profileHelper = profileHelper;
_hideoutHelper = hideoutHelper;
_scavCaseRewardGenerator = scavCaseRewardGenerator;
_localisationService = localisationService;
_profileActivityService = profileActivityService;
_fenceService = fenceService;
_circleOfCultistService = circleOfCultistService;
_cloner = cloner;
_configServer = configServer;
_hideoutConfig = _configServer.GetConfig<HideoutConfig>(ConfigTypes.HIDEOUT);
}
public void StartUpgrade(PmcData pmcData, HideoutUpgradeRequestData info, string sessionId, ItemEventRouterResponse output)
{
throw new NotImplementedException();
}
public void UpgradeComplete(PmcData pmcData, HideoutUpgradeCompleteRequestData request, string sessionId, ItemEventRouterResponse output)
{
throw new NotImplementedException();
}
public ItemEventRouterResponse PutItemsInAreaSlots(PmcData pmcData, HideoutPutItemInRequestData request, string sessionId)
{
throw new NotImplementedException();
}
public ItemEventRouterResponse TakeItemsFromAreaSlots(PmcData pmcData, HideoutTakeItemOutRequestData request, string sessionId)
{
throw new NotImplementedException();
}
public ItemEventRouterResponse ToggleArea(PmcData pmcData, HideoutToggleAreaRequestData request, string sessionId)
{
throw new NotImplementedException();
}
public ItemEventRouterResponse SingleProductionStart(PmcData pmcData, HideoutSingleProductionStartRequestData request, string sessionId)
{
throw new NotImplementedException();
}
public ItemEventRouterResponse ScavCaseProductionStart(PmcData pmcData, HideoutScavCaseStartRequestData request, string sessionId)
{
throw new NotImplementedException();
}
public ItemEventRouterResponse ContinuousProductionStart(PmcData pmcData, HideoutContinuousProductionStartRequestData request, string sessionId)
{
throw new NotImplementedException();
}
public ItemEventRouterResponse TakeProduction(PmcData pmcData, HideoutTakeProductionRequestData request, string sessionId)
{
throw new NotImplementedException();
}
public void HandleQTEEventOutcome(string sessionId, PmcData pmcData, HandleQTEEventRequestData request, ItemEventRouterResponse output)
{
throw new NotImplementedException();
}
public void RecordShootingRangePoints(string sessionId, PmcData pmcData, RecordShootingRangePoints request)
{
throw new NotImplementedException();
}
public ItemEventRouterResponse ImproveArea(string sessionId, PmcData pmcData, HideoutImproveAreaRequestData request)
{
throw new NotImplementedException();
}
public ItemEventRouterResponse CancelProduction(string sessionId, PmcData pmcData, HideoutImproveAreaRequestData request)
{
throw new NotImplementedException();
}
public ItemEventRouterResponse CicleOfCultistProductionStart(string sessionId, PmcData pmcData, HideoutCircleOfCultistProductionStartRequestData request)
{
throw new NotImplementedException();
}
public ItemEventRouterResponse HideoutDeleteProductionCommand(string sessionId, PmcData pmcData, HideoutDeleteProductionRequestData request)
{
throw new NotImplementedException();
}
public ItemEventRouterResponse HideoutCustomizationApply(string sessionId, PmcData pmcData, HideoutCustomizationApplyRequestData request)
{
throw new NotImplementedException();
}
/// <summary>
/// Handle HideoutCustomizationSetMannequinPose event
/// </summary>
/// <param name="sessionId">Session id</param>
/// <param name="pmcData">Player profile</param>
/// <param name="request">Client request</param>
/// <returns></returns>
public ItemEventRouterResponse HideoutCustomizationSetMannequinPose(string sessionId, PmcData pmcData, HideoutCustomizationSetMannequinPoseRequest request)
{
foreach (var poseKvP in request.Poses)
{
pmcData.Hideout.MannequinPoses[poseKvP.Key] = poseKvP.Value;
}
return _eventOutputHolder.GetOutput(sessionId);
}
}
+3 -5
View File
@@ -175,7 +175,7 @@ public class TraderController
_traderHelper.LevelUp(trader.Key, pmcData);
}
// traders.Sort((a, b) => SortByTraderId(a, b));
traders.Sort((a, b) => SortByTraderId(a, b));
return traders;
}
@@ -185,11 +185,9 @@ public class TraderController
/// <param name="traderA">First trader to compare</param>
/// <param name="traderB">Second trader to compare</param>
/// <returns>1,-1 or 0</returns>
private int SortByTraderId(
TraderBase traderA,
TraderBase traderB)
private int SortByTraderId(TraderBase traderA, TraderBase traderB)
{
throw new NotImplementedException();
return string.Compare(traderA.Id, traderB.Id);
}
/// <summary>
+546 -37
View File
@@ -1,4 +1,5 @@
using Core.Annotations;
using Core.Helpers;
using Core.Models.Common;
using Core.Models.Eft.Common;
using Core.Models.Eft.Common.Tables;
@@ -6,7 +7,9 @@ using Core.Models.Enums;
using Core.Models.Enums.RaidSettings;
using Core.Models.Spt.Bots;
using Core.Models.Spt.Config;
using Core.Servers;
using Core.Services;
using Core.Utils;
using Core.Utils.Cloners;
using BodyPart = Core.Models.Eft.Common.Tables.BodyPart;
using ILogger = Core.Models.Utils.ILogger;
@@ -17,20 +20,65 @@ namespace Core.Generators;
public class BotGenerator
{
private readonly ILogger _logger;
private readonly HashUtil _hashUtil;
private readonly RandomUtil _randomUtil;
private readonly TimeUtil _timeUtil;
private readonly ProfileHelper _profileHelper;
private readonly DatabaseService _databaseService;
private readonly BotInventoryGenerator _botInventoryGenerator;
private readonly BotLevelGenerator _botLevelGenerator;
private readonly BotEquipmentFilterService _botEquipmentFilterService;
private readonly WeightedRandomHelper _weightedRandomHelper;
private readonly BotHelper _botHelper;
private readonly BotGeneratorHelper _botGeneratorHelper;
private readonly SeasonalEventService _seasonalEventService;
private readonly ItemFilterService _itemFilterService;
private readonly BotNameService _botNameService;
private readonly ConfigServer _configServer;
private readonly ICloner _cloner;
private BotConfig _botConfig;
private PmcConfig _pmcConfig;
public BotGenerator(
ILogger logger,
HashUtil hashUtil,
RandomUtil randomUtil,
TimeUtil timeUtil,
ProfileHelper profileHelper,
DatabaseService databaseService,
BotInventoryGenerator botInventoryGenerator,
BotLevelGenerator botLevelGenerator,
BotEquipmentFilterService botEquipmentFilterService,
WeightedRandomHelper weightedRandomHelper,
BotHelper botHelper,
BotGeneratorHelper botGeneratorHelper,
SeasonalEventService seasonalEventService,
ItemFilterService itemFilterService,
BotNameService botNameService,
ConfigServer configServer,
ICloner cloner
)
)
{
_logger = logger;
_hashUtil = hashUtil;
_randomUtil = randomUtil;
_timeUtil = timeUtil;
_profileHelper = profileHelper;
_databaseService = databaseService;
_botInventoryGenerator = botInventoryGenerator;
_botLevelGenerator = botLevelGenerator;
_botEquipmentFilterService = botEquipmentFilterService;
_weightedRandomHelper = weightedRandomHelper;
_botHelper = botHelper;
_botGeneratorHelper = botGeneratorHelper;
_seasonalEventService = seasonalEventService;
_itemFilterService = itemFilterService;
_botNameService = botNameService;
_configServer = configServer;
_cloner = cloner;
_botConfig = _configServer.GetConfig<BotConfig>(ConfigTypes.BOT);
_pmcConfig = _configServer.GetConfig<PmcConfig>(ConfigTypes.PMC);
}
/// <summary>
@@ -49,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,
@@ -108,7 +157,23 @@ public class BotGenerator
/// <returns>constructed bot</returns>
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);
}
/// <summary>
@@ -120,7 +185,12 @@ public class BotGenerator
/// <returns>Cloned bot base</returns>
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;
}
/// <summary>
@@ -140,35 +210,154 @@ public class BotGenerator
/// <param name="botJsonTemplate">Bot template from db/bots/x.json</param>
/// <param name="botGenerationDetails">details on how to generate the bot</param>
/// <returns>BotBase object</returns>
public BotBase GenerateBot(string sessionId, BotBase bot, BotType botJsonTemplate, BotGenerationDetails botGenerationDetails)
public BotBase GenerateBot(
string sessionId,
BotBase bot,
BotType botJsonTemplate,
BotGenerationDetails botGenerationDetails)
{
_logger.Error("NOT IMPLEMENTED BotGenerator.GenerateBot");
bot.Inventory.Items = [];
var botRoleLowercase = botGenerationDetails.Role.ToLower();
var botLevel = _botLevelGenerator.GenerateBotLevel(
botJsonTemplate.BotExperience.Level,
botGenerationDetails,
bot);
// Only filter bot equipment, never players
if (!botGenerationDetails.IsPlayerScav.GetValueOrDefault(false))
{
_botEquipmentFilterService.FilterBotEquipment(
sessionId,
botJsonTemplate,
botLevel.Level.Value,
botGenerationDetails);
}
bot.Info.Nickname = _botNameService.GenerateUniqueBotNickname(
botJsonTemplate,
botGenerationDetails,
botRoleLowercase,
_botConfig.BotRolesThatMustHaveUniqueName);
bot.Info.LowerNickname = bot.Info.Nickname.ToLower();
// Only run when generating a 'fake' playerscav, not actual player scav
if (!botGenerationDetails.IsPlayerScav.GetValueOrDefault(false) && ShouldSimulatePlayerScav(botRoleLowercase))
{
_botNameService.AddRandomPmcNameToBotMainProfileNicknameProperty(bot);
SetRandomisedGameVersionAndCategory(bot.Info);
}
if (!_seasonalEventService.ChristmasEventEnabled())
{
// Process all bots EXCEPT gifter, he needs christmas items
if (botGenerationDetails.Role != "gifter")
{
_seasonalEventService.RemoveChristmasItemsFromBotInventory(
botJsonTemplate.BotInventory,
botGenerationDetails.Role);
}
}
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)))
{
bot.Hideout = null;
}
bot.Info.Experience = botLevel.Exp;
bot.Info.Level = botLevel.Level;
bot.Info.Settings.Experience = GetExperienceRewardForKillByDifficulty(
botJsonTemplate.BotExperience.Reward,
botGenerationDetails.BotDifficulty,
botGenerationDetails.Role);
bot.Info.Settings.StandingForKill = GetStandingChangeForKillByDifficulty(
botJsonTemplate.BotExperience.StandingForKill,
botGenerationDetails.BotDifficulty,
botGenerationDetails.Role);
bot.Info.Settings.AggressorBonus = GetAgressorBonusByDifficulty(
botJsonTemplate.BotExperience.StandingForKill,
botGenerationDetails.BotDifficulty,
botGenerationDetails.Role);
bot.Info.Settings.UseSimpleAnimator = botJsonTemplate.BotExperience.UseSimpleAnimator ?? false;
bot.Info.Voice = _weightedRandomHelper.GetWeightedValue(botJsonTemplate.BotAppearance.Voice);
bot.Health = GenerateHealth(botJsonTemplate.BotHealth, botGenerationDetails.IsPlayerScav.GetValueOrDefault(false));
bot.Skills = GenerateSkills(botJsonTemplate.BotSkills); // TODO: fix bad type, bot jsons store skills in dict, output needs to be array
if (botGenerationDetails.IsPmc.GetValueOrDefault(false))
{
bot.Info.IsStreamerModeAvailable = true; // Set to true so client patches can pick it up later - client sometimes alters botrole to assaultGroup
SetRandomisedGameVersionAndCategory(bot.Info);
if (bot.Info.GameVersion == GameEditions.UNHEARD)
{
AddAdditionalPocketLootWeightsForUnheardBot(botJsonTemplate);
}
}
// Add drip
SetBotAppearance(bot, botJsonTemplate.BotAppearance, botGenerationDetails);
// Filter out blacklisted gear from the base template
FilterBlacklistedGear(botJsonTemplate, botGenerationDetails);
bot.Inventory = _botInventoryGenerator.GenerateInventory(
sessionId,
botJsonTemplate,
botRoleLowercase,
botGenerationDetails.IsPmc.GetValueOrDefault(false),
botLevel.Level.Value,
bot.Info.GameVersion);
if (_botConfig.BotRolesWithDogTags.Contains(botRoleLowercase))
{
AddDogtagToBot(bot);
}
// Generate new bot ID
AddIdsToBot(bot);
// Generate new inventory ID
GenerateInventoryId(bot);
// Set role back to originally requested now its been generated
if (botGenerationDetails.EventRole is not null)
{
bot.Info.Settings.Role = botGenerationDetails.EventRole;
}
return bot;
}
/// <summary>
/// Should this bot have a name like "name (Pmc Name)" and be alterd by client patch to be hostile to player
/// Should this bot have a name like "name (Pmc Name)" and be altered by client patch to be hostile to player
/// </summary>
/// <param name="botRole">Role bot has</param>
/// <returns>True if name should be simulated pscav</returns>
public bool ShouldSimulatePlayerScav(string botRole)
{
throw new NotImplementedException();
return botRole == "assault" && _randomUtil.GetChance100(_botConfig.ChanceAssaultScavHasPlayerScavName);
}
/// <summary>
/// Get exp for kill by bot difficulty
/// </summary>
/// <param name="experience">Dict of difficulties and experience</param>
/// <param name="experiences">Dict of difficulties and experience</param>
/// <param name="botDifficulty">the killed bots difficulty</param>
/// <param name="role">Role of bot (optional, used for error logging)</param>
/// <returns>Experience for kill</returns>
public int GetExperienceRewardForKillByDifficulty(Dictionary<string, MinMax> experience, string botDifficulty, string role)
public double GetExperienceRewardForKillByDifficulty(Dictionary<string, MinMax> experiences, string botDifficulty, string role)
{
throw new NotImplementedException();
var result = experiences[botDifficulty.ToLower()];
if (result is null)
{
_logger.Debug("Unable to find experience for kill value for: ${ role} ${ botDifficulty}, falling back to `normal`");
return _randomUtil.GetDouble(experiences["normal"].Min.Value, experiences["normal"].Max.Value);
}
return _randomUtil.GetDouble(result.Min.Value, result.Max.Value);
}
/// <summary>
@@ -178,9 +367,16 @@ public class BotGenerator
/// <param name="botDifficulty">Difficulty of bot to look up</param>
/// <param name="role">Role of bot (optional, used for error logging)</param>
/// <returns>Standing change value</returns>
public int GetStandingChangeForKillByDifficulty(Dictionary<string, int> standingForKill, string botDifficulty, string role)
public double GetStandingChangeForKillByDifficulty(Dictionary<string, double> standingsForKill, string botDifficulty, string role)
{
throw new NotImplementedException();
if (!standingsForKill.TryGetValue(botDifficulty.ToLower(), out var result))
{
_logger.Warning($"Unable to find standing for kill value for: {role} {botDifficulty}, falling back to `normal`");
return standingsForKill["normal"];
}
return result;
}
/// <summary>
@@ -190,9 +386,16 @@ public class BotGenerator
/// <param name="botDifficulty">Difficulty of bot to look up</param>
/// <param name="role">Role of bot (optional, used for error logging)</param>
/// <returns>Standing change value</returns>
public int GetAgressorBonusByDifficulty(Dictionary<string, int> aggressorBonus, string botDifficulty, string role)
public double GetAgressorBonusByDifficulty(Dictionary<string, double> aggressorBonuses, string botDifficulty, string role)
{
throw new NotImplementedException();
if (!aggressorBonuses.TryGetValue(botDifficulty.ToLower(), out var result))
{
_logger.Warning($"Unable to find aggressor bonus for kill value for: {role} {botDifficulty}, falling back to `normal`");
return aggressorBonuses["normal"];
}
return result;
}
/// <summary>
@@ -202,7 +405,26 @@ public class BotGenerator
/// <param name="botGenerationDetails">Generation details of bot</param>
public void FilterBlacklistedGear(BotType botJsonTemplate, BotGenerationDetails botGenerationDetails)
{
throw new NotImplementedException();
var blacklist = _botEquipmentFilterService.GetBotEquipmentBlacklist(
_botGeneratorHelper.GetBotEquipmentRole(botGenerationDetails.Role),
botGenerationDetails.PlayerLevel.GetValueOrDefault(1));
if (blacklist?.Gear is null)
{
// Nothing to filter by
return;
}
foreach (var equipmentKvP in blacklist.Gear)
{
var equipmentDict = botJsonTemplate.BotInventory.Equipment[equipmentKvP.Key];
foreach (var blacklistedTpl in equipmentKvP.Value)
{
// Set weighting to 0, will never be picked
equipmentDict[blacklistedTpl] = 0;
}
}
}
/// <summary>
@@ -211,7 +433,10 @@ public class BotGenerator
/// <param name="botJsonTemplate">Bot data to adjust</param>
public void AddAdditionalPocketLootWeightsForUnheardBot(BotType botJsonTemplate)
{
throw new NotImplementedException();
// Adjust pocket loot weights to allow for 5 or 6 items
var pocketWeights = botJsonTemplate.BotGeneration.Items["pocketLoot"].Weights;
pocketWeights["5"] = 1;
pocketWeights["6"] = 1;
}
/// <summary>
@@ -220,7 +445,37 @@ public class BotGenerator
/// <param name="botInventory">Bot to filter</param>
public void RemoveBlacklistedLootFromBotTemplate(BotTypeInventory botInventory)
{
throw new NotImplementedException();
List<string> 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<string, double>)prop.GetValue(botInventory.Items);
// No container, skip
if (propValue?.Count == 0)
{
continue;
}
List<string> 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);
}
}
/// <summary>
@@ -231,7 +486,19 @@ public class BotGenerator
/// <param name="botGenerationDetails">Generation details</param>
public void SetBotAppearance(BotBase bot, Appearance appearance, BotGenerationDetails botGenerationDetails)
{
throw new NotImplementedException();
// Choose random values by weight
bot.Customization.Head = _weightedRandomHelper.GetWeightedValue<string>(appearance.Head);
bot.Customization.Feet = _weightedRandomHelper.GetWeightedValue<string>(appearance.Feet);
bot.Customization.Body = _weightedRandomHelper.GetWeightedValue<string>(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<string>(appearance.Hands); // Hands can be random, choose any from weighted dict
}
/// <summary>
@@ -240,7 +507,8 @@ public class BotGenerator
/// <param name="output">Generated bot array, ready to send to client</param>
public void LogPmcGeneratedCount(List<BotBase> 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");
}
/// <summary>
@@ -249,9 +517,107 @@ public class BotGenerator
/// <param name="healthObj">health object from bot json</param>
/// <param name="playerScav">Is a pscav bot being generated</param>
/// <returns>Health object</returns>
public Health GenerateHealth(Health healthObj, bool playerScav = false)
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<string, BodyPartHealth>()
{
{
"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;
}
/// <summary>
@@ -259,9 +625,33 @@ public class BotGenerator
/// </summary>
/// <param name="bodies">Body parts to sum up</param>
/// <returns>Lowest hp collection</returns>
public BodyPart? GetLowestHpBody(List<BodyPart> bodies) // TODO: there are two types of body parts
public BodyPart? GetLowestHpBody(List<BodyPart> 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;
}
/// <summary>
@@ -269,9 +659,16 @@ public class BotGenerator
/// </summary>
/// <param name="botSkills">Skills that should have their progress value randomised</param>
/// <returns>Skills</returns>
public Skills GenerateSkills(BaseJsonSkills botSkills)
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;
}
/// <summary>
@@ -280,19 +677,49 @@ public class BotGenerator
/// <param name="skills">Skills to randomise</param>
/// <param name="isCommonSkills">Are the skills 'common' skills</param>
/// <returns>Skills with randomised progress values as an array</returns>
public List<BaseSkill> GetSkillsWithRandomisedProgressValue(Dictionary<string, BaseSkill> skills, bool isCommonSkills)
public List<BaseSkill> GetSkillsWithRandomisedProgressValue(Dictionary<string, MinMax>? skills, bool isCommonSkills)
{
throw new NotImplementedException();
if (skills is null)
return [];
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();
}
/// <summary>
/// Generate an id+aid for a bot and apply
/// </summary>
/// <param name="bot">bot to update</param>
/// <returns>updated IBotBase object</returns> // TODO: Node server claims this in summary but is void
/// <returns></returns>
public void AddIdsToBot(BotBase bot)
{
throw new NotImplementedException();
var botId = _hashUtil.Generate();
bot.Id = botId;
bot.Aid = _hashUtil.GenerateAccountId();
}
/// <summary>
@@ -302,7 +729,30 @@ public class BotGenerator
/// <param name="profile">Profile to update</param>
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;
}
/// <summary>
@@ -312,19 +762,57 @@ public class BotGenerator
/// </summary>
/// <param name="botInfo">bot info object to update</param>
/// <returns>Chosen game version</returns>
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<MemberCategory>(_pmcConfig.AccountTypeWeight);
break;
}
// Ensure selected category matches
botInfo.SelectedMemberCategory = botInfo.MemberCategory;
return botInfo.GameVersion;
}
/// <summary>
/// Add a side-specific (usec/bear) dogtag item to a bots inventory
/// </summary>
/// <param name="bot">bot to add dogtag to</param>
/// <returns>Bot with dogtag added</returns> // TODO: Node server claims this in summary but is void
/// <returns></returns>
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);
}
/// <summary>
@@ -335,7 +823,25 @@ public class BotGenerator
/// <returns>item tpl</returns>
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;
}
}
/// <summary>
@@ -344,6 +850,9 @@ public class BotGenerator
/// <param name="bot">Pmc object to adjust</param>
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;
}
}
}
+270 -10
View File
@@ -1,19 +1,55 @@
using Core.Annotations;
using Core.Annotations;
using Core.Context;
using Core.Helpers;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Match;
using Core.Models.Enums;
using Core.Models.Spt.Bots;
using Core.Models.Spt.Config;
using Core.Servers;
using Core.Utils;
using Equipment = Core.Models.Eft.Common.Tables.Equipment;
using ILogger = Core.Models.Utils.ILogger;
namespace Core.Generators;
[Injectable]
public class BotInventoryGenerator
{
private readonly ILogger _logger;
private readonly HashUtil _hashUtil;
private readonly BotLootGenerator _botLootGenerator;
private readonly BotHelper _botHelper;
private readonly BotGeneratorHelper _botGeneratorHelper;
private readonly WeatherHelper _weatherHelper;
private readonly ProfileHelper _profileHelper;
private readonly ConfigServer _configServer;
private readonly ApplicationContext _applicationContext;
private BotConfig _botConfig;
public BotInventoryGenerator()
public BotInventoryGenerator(
ILogger logger,
HashUtil hashUtil,
BotLootGenerator botLootGenerator,
BotHelper botHelper,
BotGeneratorHelper botGeneratorHelper,
WeatherHelper weatherHelper,
ProfileHelper profileHelper,
ConfigServer configServer,
ApplicationContext applicationContext
)
{
_logger = logger;
_hashUtil = hashUtil;
_botLootGenerator = botLootGenerator;
_botHelper = botHelper;
_botGeneratorHelper = botGeneratorHelper;
_weatherHelper = weatherHelper;
_profileHelper = profileHelper;
_configServer = configServer;
_applicationContext = applicationContext;
_botConfig = _configServer.GetConfig<BotConfig>(ConfigTypes.BOT);
}
/// <summary>
@@ -26,9 +62,45 @@ public class BotInventoryGenerator
/// <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>
public BotBaseInventory generateInventory(string sessionId, BotType botJsonTemplate, string botRole, bool isPmc, int botLevel, string chosenGameVersion)
public BotBaseInventory GenerateInventory(string sessionId, BotType botJsonTemplate, string botRole, bool isPmc, int botLevel, string chosenGameVersion)
{
throw new NotImplementedException();
var templateInventory = botJsonTemplate.BotInventory;
var wornItemChances = botJsonTemplate.BotChances;
var itemGenerationLimitsMinMax = botJsonTemplate.BotGeneration;
// Generate base inventory with no items
var botInventory = GenerateInventoryBase();
// Get generated raid details bot will be spawned in
var raidConfig = _applicationContext
.GetLatestValue(ContextVariableType.RAID_CONFIGURATION)
?.GetValue<GetRaidConfigurationRequestData>();
GenerateAndAddEquipmentToBot(
sessionId,
templateInventory,
wornItemChances,
botRole,
botInventory,
botLevel,
chosenGameVersion,
raidConfig);
// Roll weapon spawns (primary/secondary/holster) and generate a weapon for each roll that passed
GenerateAndAddWeaponsToBot(
templateInventory,
wornItemChances,
sessionId,
botInventory,
botRole,
isPmc,
itemGenerationLimitsMinMax,
botLevel);
// Pick loot and add to bots containers (rig/backpack/pockets/secure)
_botLootGenerator.GenerateLoot(sessionId, botJsonTemplate, isPmc, botRole, botInventory, botLevel);
return botInventory;
}
/// <summary>
@@ -37,7 +109,31 @@ public class BotInventoryGenerator
/// <returns>PmcInventory object</returns>
public BotBaseInventory GenerateInventoryBase()
{
throw new NotImplementedException();
var equipmentId = _hashUtil.Generate();
var stashId = _hashUtil.Generate();
var questRaidItemsId = _hashUtil.Generate();
var questStashItemsId = _hashUtil.Generate();
var sortingTableId = _hashUtil.Generate();
return new BotBaseInventory{
Items =
[
new() { Id = equipmentId, Template = ItemTpl.INVENTORY_DEFAULT },
new() { Id = stashId, Template = ItemTpl.STASH_STANDARD_STASH_10X30 },
new() { Id = questRaidItemsId, Template = ItemTpl.STASH_QUESTRAID },
new() { Id = questStashItemsId, Template = ItemTpl.STASH_QUESTOFFLINE },
new() { Id = sortingTableId, Template = ItemTpl.SORTINGTABLE_SORTING_TABLE }
],
Equipment = equipmentId,
Stash = stashId,
QuestRaidItems = questRaidItemsId,
QuestStashItems = questStashItemsId,
SortingTable = sortingTableId,
HideoutAreaStashes = { },
FastPanel = { },
FavoriteItems = [],
HideoutCustomizationStashId = "",
};
}
/// <summary>
@@ -51,10 +147,168 @@ public class BotInventoryGenerator
/// <param name="botLevel">Level of bot</param>
/// <param name="chosenGameVersion">Game version for bot, only really applies for PMCs</param>
/// <param name="raidConfig">RadiConfig</param>
public void GenerateAndAddEquipmentToBot(string sessionId, BotBaseInventory templateInventory, Chances wornItemChances, string botRole,
public void GenerateAndAddEquipmentToBot(string sessionId, BotTypeInventory templateInventory, Chances wornItemChances, string botRole,
BotBaseInventory botInventory, int botLevel, string chosenGameVersion, GetRaidConfigurationRequestData raidConfig)
{
throw new NotImplementedException();
// These will be handled later
var excludedSlots = new List<EquipmentSlots>(){
EquipmentSlots.Pockets,
EquipmentSlots.FirstPrimaryWeapon,
EquipmentSlots.SecondPrimaryWeapon,
EquipmentSlots.Holster,
EquipmentSlots.ArmorVest,
EquipmentSlots.TacticalVest,
EquipmentSlots.FaceCover,
EquipmentSlots.Headwear,
EquipmentSlots.Earpiece
};
_botConfig.Equipment.TryGetValue(_botGeneratorHelper.GetBotEquipmentRole(botRole), out var botEquipConfig);
var randomistionDetails = _botHelper.GetBotRandomizationDetails(botLevel, botEquipConfig);
// Apply nighttime changes if its nighttime + there's changes to make
if (
randomistionDetails?.NighttimeChanges is not null &&
raidConfig is not null &&
_weatherHelper.IsNightTime(raidConfig.TimeVariant)
)
{
foreach (var equipmentSlotKvP in (randomistionDetails.NighttimeChanges.EquipmentModsModifiers)) {
// Never let mod chance go outside of 0 - 100
randomistionDetails.EquipmentMods[equipmentSlotKvP.Key] = Math.Min(
Math.Max( randomistionDetails.EquipmentMods[equipmentSlotKvP.Key] + equipmentSlotKvP.Value, 0), 100);
}
}
// Get profile of player generating bots, we use their level later on
var pmcProfile = _profileHelper.GetPmcProfile(sessionId);
var botEquipmentRole = _botGeneratorHelper.GetBotEquipmentRole(botRole);
// Iterate over all equipment slots of bot, do it in specifc order to reduce conflicts
// e.g. ArmorVest should be generated after TactivalVest
// or FACE_COVER before HEADWEAR
foreach (var equipmentSlotKvP in templateInventory.Equipment) {
// Skip some slots as they need to be done in a specific order + with specific parameter values
// e.g. Weapons
if (excludedSlots.Contains(equipmentSlotKvP.Key)) {
continue;
}
GenerateEquipment( new GenerateEquipmentProperties
{
RootEquipmentSlot = equipmentSlotKvP.Key,
RootEquipmentPool = equipmentSlotKvP.Value,
ModPool = templateInventory.Mods,
SpawnChances = wornItemChances,
BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole },
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GeneratingPlayerLevel = pmcProfile.Info.Level,
});
}
// Generate below in specific order
GenerateEquipment( new GenerateEquipmentProperties
{
RootEquipmentSlot = EquipmentSlots.Pockets,
// Unheard profiles have unique sized pockets, TODO - handle this somewhere else in a better way
RootEquipmentPool =
chosenGameVersion == GameEditions.UNHEARD
? new Dictionary<string, double>{ [ItemTpl.POCKETS_1X4_TUE] = 1 }
: templateInventory.Equipment[EquipmentSlots.Pockets],
ModPool = templateInventory.Mods,
SpawnChances = wornItemChances,
BotData = new BotData{ Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole },
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GenerateModsBlacklist = [ItemTpl.POCKETS_1X4_TUE],
GeneratingPlayerLevel = pmcProfile.Info.Level,
});
GenerateEquipment( new GenerateEquipmentProperties
{
RootEquipmentSlot = EquipmentSlots.FaceCover,
RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.FaceCover],
ModPool = templateInventory.Mods,
SpawnChances = wornItemChances,
BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole },
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GeneratingPlayerLevel = pmcProfile.Info.Level,
});
GenerateEquipment( new GenerateEquipmentProperties
{
RootEquipmentSlot = EquipmentSlots.Headwear,
RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.Headwear],
ModPool = templateInventory.Mods,
SpawnChances = wornItemChances,
BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole },
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GeneratingPlayerLevel = pmcProfile.Info.Level,
});
GenerateEquipment(new GenerateEquipmentProperties
{
RootEquipmentSlot = EquipmentSlots.Earpiece,
RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.Earpiece],
ModPool = templateInventory.Mods,
SpawnChances = wornItemChances,
BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole },
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GeneratingPlayerLevel = pmcProfile.Info.Level,
});
var hasArmorVest = GenerateEquipment( new GenerateEquipmentProperties
{
RootEquipmentSlot = EquipmentSlots.ArmorVest,
RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.ArmorVest],
ModPool = templateInventory.Mods,
SpawnChances = wornItemChances,
BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole },
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GeneratingPlayerLevel = pmcProfile.Info.Level,
});
// Bot has no armor vest and flagged to be forced to wear armored rig in this event
if (botEquipConfig.ForceOnlyArmoredRigWhenNoArmor.GetValueOrDefault(false) && !hasArmorVest) {
// Filter rigs down to only those with armor
FilterRigsToThoseWithProtection(templateInventory.Equipment, botRole);
}
// Optimisation - Remove armored rigs from pool
if (hasArmorVest) {
// Filter rigs down to only those with armor
FilterRigsToThoseWithoutProtection(templateInventory.Equipment, botRole);
}
// Bot is flagged as always needing a vest
if (botEquipConfig.ForceRigWhenNoVest.GetValueOrDefault(false) && !hasArmorVest) {
wornItemChances.EquipmentChances["TacticalVest"] = 100;
}
GenerateEquipment( new GenerateEquipmentProperties
{
RootEquipmentSlot = EquipmentSlots.Earpiece,
RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.Earpiece],
ModPool = templateInventory.Mods,
SpawnChances = wornItemChances,
BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole },
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GeneratingPlayerLevel = pmcProfile.Info.Level,
});
}
/// <summary>
@@ -62,7 +316,12 @@ public class BotInventoryGenerator
/// </summary>
/// <param name="templateEquipment">Equpiment to filter TacticalVest of</param>
/// <param name="botRole">Role of bot vests are being filtered for</param>
public void FilterRigsToThoseWithProtection(Equipment templateEquipment, string botRole)
public void FilterRigsToThoseWithProtection(Dictionary<EquipmentSlots, Dictionary<string, double>> templateEquipment, string botRole)
{
throw new NotImplementedException();
}
public void FilterRigsToThoseWithoutProtection(Dictionary<EquipmentSlots, Dictionary<string, double>> templateEquipment, string botRole, bool allowEmptyResult = true)
{
throw new NotImplementedException();
}
@@ -85,7 +344,8 @@ public class BotInventoryGenerator
/// <returns>true when item added</returns>
public bool GenerateEquipment(GenerateEquipmentProperties settings)
{
throw new NotImplementedException();
_logger.Error("NOT IMPLEMENTED - GenerateEquipment");
return true;
}
/// <summary>
@@ -110,7 +370,7 @@ public class BotInventoryGenerator
/// <param name="isPmc">Is the bot being generated as a pmc</param>
/// <param name="itemGenerationLimitsMinMax">Limits for items the bot can have</param>
/// <param name="botLevel">level of bot having weapon generated</param>
public void GenerateAndAddWeaponsToBot(BotBaseInventory templateInventory, Chances equipmentChances, string sessionId, BotBaseInventory botInventory,
public void GenerateAndAddWeaponsToBot(BotTypeInventory templateInventory, Chances equipmentChances, string sessionId, BotBaseInventory botInventory,
string botRole,
bool isPmc, Generation itemGenerationLimitsMinMax, int botLevel)
{
+69 -6
View File
@@ -1,16 +1,32 @@
using Core.Annotations;
using Core.Annotations;
using Core.Models.Common;
using Core.Models.Eft.Bot;
using Core.Models.Eft.Common.Tables;
using Core.Models.Spt.Bots;
using Core.Services;
using Core.Utils;
using ILogger = Core.Models.Utils.ILogger;
namespace Core.Generators;
[Injectable]
public class BotLevelGenerator
{
public BotLevelGenerator()
private readonly ILogger _logger;
private readonly RandomUtil _randomUtil;
private readonly MathUtil _mathUtil;
private readonly DatabaseService _databaseService;
public BotLevelGenerator(
ILogger logger,
RandomUtil randomUtil,
MathUtil mathUtil,
DatabaseService databaseService)
{
_logger = logger;
_randomUtil = randomUtil;
_mathUtil = mathUtil;
_databaseService = databaseService;
}
/// <summary>
@@ -22,12 +38,30 @@ public class BotLevelGenerator
/// <returns>IRandomisedBotLevelResult object</returns>
public RandomisedBotLevelResult GenerateBotLevel(MinMax levelDetails, BotGenerationDetails botGenerationDetails, BotBase bot)
{
throw new NotImplementedException();
var expTable = _databaseService.GetGlobals().Configuration.Exp.Level.ExperienceTable;
var botLevelRange = GetRelativeBotLevelRange(botGenerationDetails, levelDetails, expTable.Length);
// Get random level based on the exp table.
int exp = 0;
var level = int.Parse(ChooseBotLevel(botLevelRange.Min.Value, botLevelRange.Max.Value, 1, 1.15)
.ToString()); // TODO - nasty double to string to int conversion
for (var i = 0; i < level; i++)
{
exp += expTable[i].Experience.Value;
}
// Sprinkle in some random exp within the level, unless we are at max level.
if (level < expTable.Length - 1)
{
exp += _randomUtil.GetInt(0, expTable[level].Experience.Value - 1);
}
return new RandomisedBotLevelResult { Level = level, Exp = exp };
}
public int ChooseBotLevel(int min, int max, int shift, int number)
public double ChooseBotLevel(double min, double max, int shift, double number)
{
throw new NotImplementedException();
return _randomUtil.GetBiasedRandomNumber(min, max, shift, number);
}
/// <summary>
@@ -39,6 +73,35 @@ public class BotLevelGenerator
/// <returns>A MinMax of the lowest and highest level to generate the bots</returns>
public MinMax GetRelativeBotLevelRange(BotGenerationDetails botGenerationDetails, MinMax levelDetails, int maxAvailableLevel)
{
throw new NotImplementedException();
var isPmc = botGenerationDetails.IsPmc.GetValueOrDefault(false);
var pmcOverride = botGenerationDetails.LocationSpecificPmcLevelOverride;
var minPossibleLevel = isPmc && pmcOverride is not null
? Math.Min(
Math.Max(levelDetails.Min.Value, pmcOverride.Min.Value), // Biggest between json min and the botgen min
maxAvailableLevel // Fallback if value above is crazy (default is 79)
)
: Math.Min(levelDetails.Min.Value, maxAvailableLevel); // Not pmc with override or non-pmc
var maxPossibleLevel = isPmc && pmcOverride is not null
? Math.Min(pmcOverride.Max.Value, maxAvailableLevel) // Was a PMC and they have a level override
: Math.Min(levelDetails.Max.Value, maxAvailableLevel); // Not pmc with override or non-pmc
var minLevel = botGenerationDetails.PlayerLevel.HasValue
? botGenerationDetails.PlayerLevel.Value
: 0 - botGenerationDetails.BotRelativeLevelDeltaMin.Value;
var maxLevel = botGenerationDetails.PlayerLevel.HasValue
? botGenerationDetails.PlayerLevel.Value
: 0 + botGenerationDetails.BotRelativeLevelDeltaMin.Value;
// Bound the level to the min/max possible
maxLevel = Math.Min(Math.Max(maxLevel, minPossibleLevel), maxPossibleLevel);
minLevel = Math.Min(Math.Max(minLevel, minPossibleLevel), maxPossibleLevel);
return new MinMax
{
Min = minLevel,
Max = maxLevel,
};
}
}
+64 -15
View File
@@ -16,19 +16,25 @@ public class WeatherGenerator
private readonly SeasonalEventService _seasonalEventService;
private readonly WeatherHelper _weatherHelper;
private readonly ConfigServer _configServer;
private readonly WeightedRandomHelper _weightedRandomHelper;
private readonly RandomUtil _randomUtil;
private readonly WeatherConfig _weatherConfig;
public WeatherGenerator(
TimeUtil timeUtil,
SeasonalEventService seasonalEventService,
WeatherHelper weatherHelper,
ConfigServer configServer)
ConfigServer configServer,
WeightedRandomHelper weightedRandomHelper,
RandomUtil randomUtil
)
{
_timeUtil = timeUtil;
_seasonalEventService = seasonalEventService;
_weatherHelper = weatherHelper;
_configServer = configServer;
_weightedRandomHelper = weightedRandomHelper;
_randomUtil = randomUtil;
_weatherConfig = _configServer.GetConfig<WeatherConfig>();
}
@@ -81,7 +87,36 @@ public class WeatherGenerator
*/
public Weather GenerateWeather(Season currentSeason, long? timestamp = null)
{
throw new NotImplementedException();
var weatherValues = GetWeatherValuesBySeason(currentSeason);
var clouds = GetWeightedClouds(weatherValues);
// Force rain to off if no clouds
var rain = clouds <= 0.6 ? 0 : GetWeightedRain(weatherValues);
// TODO: Ensure Weather settings match Ts Server GetRandomDouble produces a decimal value way higher than ts server
var result = new Weather
{
Pressure = GetRandomDouble(weatherValues.Pressure.Min ?? 0, weatherValues.Pressure.Max ?? 0),
Temperature = 0,
Fog = GetWeightedFog(weatherValues),
RainIntensity =
rain > 1 ? GetRandomDouble(weatherValues.RainIntensity.Min ?? 0, weatherValues.RainIntensity.Max ?? 0) : 0,
Rain = rain,
WindGustiness = GetRandomDouble(weatherValues.WindGustiness.Min ?? 0, weatherValues.WindGustiness.Max ?? 0, 2),
WindDirection = GetWeightedWindDirection(weatherValues),
WindSpeed = GetWeightedWindSpeed(weatherValues),
Cloud = clouds,
Time = "",
Date = "",
Timestamp = 0,
SptInRaidTimestamp = 0
};
SetCurrentDateTime(result, timestamp);
result.Temperature = GetRaidTemperature(weatherValues, result.SptInRaidTimestamp ?? 0);
return result;
}
protected SeasonalValues GetWeatherValuesBySeason(Season currentSeason)
@@ -92,7 +127,7 @@ public class WeatherGenerator
return this._weatherConfig.Weather.SeasonValues["default"];
}
return value;
return value!;
}
/**
@@ -101,9 +136,15 @@ public class WeatherGenerator
* @param inRaidTimestamp What time is the raid running at
* @returns Timestamp
*/
protected double GetRaidTemperature(SeasonalValues weather, int inRaidTimestamp)
protected double GetRaidTemperature(SeasonalValues weather, long inRaidTimestamp)
{
throw new NotImplementedException();
// Convert timestamp to date so we can get current hour and check if its day or night
var currentRaidTime = new DateTime(inRaidTimestamp);
var minMax = _weatherHelper.IsHourAtNightTime(currentRaidTime.Hour)
? weather.Temp.Night
: weather.Temp.Day;
return Math.Round(_randomUtil.GetDouble(minMax.Min ?? 0, minMax.Max ?? 0), 2);
}
/**
@@ -111,38 +152,46 @@ public class WeatherGenerator
* @param weather Object to update
* @param timestamp OPTIONAL, define timestamp used
*/
protected void SetCurrentDateTime(Weather weather, int? timestamp = null)
protected void SetCurrentDateTime(Weather weather, long? timestamp = null)
{
throw new NotImplementedException();
var inRaidTime = _weatherHelper.GetInRaidTime(timestamp);
var normalTime = GetBsgFormattedTime(inRaidTime);
var formattedDate = _timeUtil.FormatDate(timestamp.HasValue ? _timeUtil.GetDateTimeFromTimeStamp(timestamp.Value) : DateTime.UtcNow);
var datetimeBsgFormat = $"{formattedDate} {normalTime}";
weather.Timestamp = timestamp ?? _timeUtil.GetTimeStampFromEpoch(inRaidTime); // matches weather.date We use to divide by 1000
weather.Date = formattedDate; // matches weather.timestamp
weather.Time = datetimeBsgFormat; // matches weather.timestamp
weather.SptInRaidTimestamp = _timeUtil.GetTimeStampFromEpoch(inRaidTime);
}
protected WindDirection GetWeightedWindDirection(SeasonalValues weather)
{
throw new NotImplementedException();
return _weightedRandomHelper.WeightedRandom(weather.WindDirection.Values, weather.WindDirection.Weights).Item;
}
protected double GetWeightedClouds(SeasonalValues weather)
{
throw new NotImplementedException();
return _weightedRandomHelper.WeightedRandom(weather.Clouds.Values, weather.Clouds.Weights).Item;
}
protected double GetWeightedWindSpeed(SeasonalValues weather)
{
throw new NotImplementedException();
return _weightedRandomHelper.WeightedRandom(weather.WindSpeed.Values, weather.WindSpeed.Weights).Item;
}
protected double GetWeightedFog(SeasonalValues weather)
{
throw new NotImplementedException();
return _weightedRandomHelper.WeightedRandom(weather.Fog.Values, weather.Fog.Weights).Item;
}
protected double GetWeightedRain(SeasonalValues weather)
{
throw new NotImplementedException();
return _weightedRandomHelper.WeightedRandom(weather.Rain.Values, weather.Rain.Weights).Item;
}
protected double GetRandomFloat(double min, double max, int precision = 3)
protected double GetRandomDouble(double min, double max, int precision = 3)
{
throw new NotImplementedException();
return Math.Round(_randomUtil.GetDouble(min, max), precision);
}
}
+18 -2
View File
@@ -1,13 +1,25 @@
using Core.Annotations;
using Core.Annotations;
using Core.Models.Eft.Common.Tables;
using Core.Models.Enums;
using Core.Models.Spt.Config;
using Core.Servers;
namespace Core.Helpers;
[Injectable]
public class BotGeneratorHelper
{
private readonly ConfigServer _configServer;
private readonly PmcConfig _pmcConfig;
public BotGeneratorHelper(
ConfigServer configServer
)
{
_configServer = configServer;
_pmcConfig = _configServer.GetConfig<PmcConfig>(ConfigTypes.PMC);
}
/// <summary>
/// Adds properties to an item
/// e.g. Repairable / HasHinge / Foldable / MaxDurability
@@ -84,7 +96,11 @@ public class BotGeneratorHelper
/// <returns>Equipment role (e.g. pmc / assault / bossTagilla)</returns>
public string GetBotEquipmentRole(string botRole)
{
throw new NotImplementedException();
string[] pmcs = [_pmcConfig.UsecType.ToLower(), _pmcConfig.BearType.ToLower()];
return pmcs.Contains(
botRole.ToLower())
? "pmc"
: botRole;
}
/// <summary>
+97 -16
View File
@@ -1,8 +1,12 @@
using Core.Annotations;
using Core.Models.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Match;
using Core.Models.Enums;
using Core.Models.Spt.Config;
using Core.Servers;
using Core.Services;
using Core.Utils;
using ILogger = Core.Models.Utils.ILogger;
namespace Core.Helpers;
@@ -12,14 +16,30 @@ public class BotHelper
{
private readonly ILogger _logger;
private readonly DatabaseService _databaseService;
private readonly RandomUtil _randomUtil;
private readonly ConfigServer _configServer;
public BotHelper(
private readonly BotConfig _botConfig;
private readonly PmcConfig _pmcConfig;
private readonly List<string> _pmcNames = ["usec", "bear", "pmc", "pmcbear", "pmcusec"];
public BotHelper
(
ILogger logger,
DatabaseService databaseService)
DatabaseService databaseService,
RandomUtil randomUtil,
ConfigServer configServer
)
{
_logger = logger;
_databaseService = databaseService;
_randomUtil = randomUtil;
_configServer = configServer;
_botConfig = configServer.GetConfig<BotConfig>(ConfigTypes.BOT);
_pmcConfig = configServer.GetConfig<PmcConfig>(ConfigTypes.PMC);
}
/// <summary>
/// Get a template object for the specified botRole from bots.types db
/// </summary>
@@ -44,17 +64,17 @@ public class BotHelper
/// <returns>true if is pmc</returns>
public bool IsBotPmc(string botRole)
{
throw new NotImplementedException();
return _pmcNames.Contains(botRole?.ToLower());
}
public bool IsBotBoss(string botRole)
{
throw new NotImplementedException();
return _botConfig.Bosses.Any(x => x.ToLower() == botRole.ToLower());
}
public bool IsBotFollower(string botRole)
{
throw new NotImplementedException();
return botRole?.ToLower().StartsWith("follower") ?? false;
}
/// <summary>
@@ -64,7 +84,15 @@ public class BotHelper
/// <param name="typeToAdd">bot type to add to friendly list</param>
public void AddBotToFriendlyList(DifficultyCategories difficultySettings, string typeToAdd)
{
throw new NotImplementedException();
var friendlyBotTypesKey = "FRIENDLY_BOT_TYPES";
// Null guard
if (difficultySettings.Mind[friendlyBotTypesKey] is null)
{
difficultySettings.Mind[friendlyBotTypesKey] = new List<string>();
}
((List<string>)difficultySettings.Mind[friendlyBotTypesKey]).Add(typeToAdd);
}
/// <summary>
@@ -74,17 +102,44 @@ public class BotHelper
/// <param name="typesToAdd">bot type to add to revenge list</param>
public void AddBotToRevengeList(DifficultyCategories difficultySettings, string[] typesToAdd)
{
throw new NotImplementedException();
var revengePropKey = "REVENGE_BOT_TYPES";
// Nothing to add
if (typesToAdd is null)
{
return;
}
// Null guard
if (difficultySettings.Mind[revengePropKey] is null)
{
difficultySettings.Mind[revengePropKey] = new List<string>();
}
var revengeArray = (List<string>)difficultySettings.Mind[revengePropKey];
foreach (var botTypeToAdd in typesToAdd)
{
if (!revengeArray.Contains(botTypeToAdd))
{
revengeArray.Add(botTypeToAdd);
}
}
}
public bool RollChanceToBePmc(MinMax botConvertMinMax)
{
throw new NotImplementedException();
return _randomUtil.GetChance100(_randomUtil.GetInt((int)botConvertMinMax.Min, (int)botConvertMinMax.Max));
}
protected void GetPmcConversionValuesForLocation(string location)
protected Dictionary<string, MinMax> GetPmcConversionValuesForLocation(string location)
{
throw new NotImplementedException();
var result = _pmcConfig.ConvertIntoPmcChance[location.ToLower()];
if (result is null)
{
_pmcConfig.ConvertIntoPmcChance = new();
}
return result;
}
/// <summary>
@@ -94,7 +149,10 @@ public class BotHelper
/// <returns>True if role is PMC</returns>
public bool BotRoleIsPmc(string botRole)
{
throw new NotImplementedException();
List<string> ListToCheck = [_pmcConfig.UsecType.ToLower(), _pmcConfig.BearType.ToLower()];
return ListToCheck.Contains(
botRole.ToLower()
);
}
/// <summary>
@@ -105,7 +163,15 @@ public class BotHelper
/// <returns>RandomisationDetails</returns>
public RandomisationDetails GetBotRandomizationDetails(int botLevel, EquipmentFilters botEquipConfig)
{
throw new NotImplementedException();
// No randomisation details found, skip
if (botEquipConfig is null || botEquipConfig.Randomisation is null)
{
return null;
}
return botEquipConfig.Randomisation.FirstOrDefault(
(randDetails) => botLevel >= randDetails.LevelRange.Min && botLevel <= randDetails.LevelRange.Max
);
}
/// <summary>
@@ -114,7 +180,7 @@ public class BotHelper
/// <returns>pmc role</returns>
public string GetRandomizedPmcRole()
{
throw new NotImplementedException();
return _randomUtil.GetChance100(_pmcConfig.IsUsec) ? _pmcConfig.UsecType : _pmcConfig.BearType;
}
/// <summary>
@@ -124,7 +190,13 @@ public class BotHelper
/// <returns>side (usec/bear)</returns>
public string GetPmcSideByRole(string botRole)
{
throw new NotImplementedException();
if (_pmcConfig.BearType.ToLower() == botRole.ToLower())
return "Bear";
if (_pmcConfig.UsecType.ToLower() == botRole.ToLower())
return "Usec";
return GetRandomizedPmcSide();
}
/// <summary>
@@ -133,7 +205,7 @@ public class BotHelper
/// <returns>pmc side as string</returns>
protected string GetRandomizedPmcSide()
{
throw new NotImplementedException();
return _randomUtil.GetChance100(_pmcConfig.IsUsec) ? "Usec" : "Bear";
}
/// <summary>
@@ -144,6 +216,15 @@ public class BotHelper
/// <returns>name of PMC</returns>
public string GetPmcNicknameOfMaxLength(int maxLength, string side = null)
{
throw new NotImplementedException();
var randomType = (side is not null) ? side : (_randomUtil.GetInt(0, 1) == 0) ? "usec" : "bear";
var allNames = _databaseService.GetBots().Types[randomType.ToLower()].FirstNames;
var filteredNames = allNames.Where((name) => name.Length <= maxLength);
if (filteredNames.Count() == 0) {
_logger.Warning($"Unable to filter: {randomType} PMC names to only those under: {maxLength}, none found that match that criteria, selecting from entire name pool instead`,\n");
return _randomUtil.GetStringCollectionValue(allNames);
}
return _randomUtil.GetStringCollectionValue(filteredNames);
}
}
+102 -3
View File
@@ -1,12 +1,46 @@
using Core.Annotations;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Profile;
using Core.Servers;
using Core.Services;
using Core.Utils;
using ILogger = Core.Models.Utils.ILogger;
namespace Core.Helpers;
[Injectable]
public class DialogueHelper
{
private readonly ILogger _logger;
private readonly HashUtil _hashUtil;
private readonly SaveServer _saveServer;
private readonly DatabaseServer _databaseServer;
private readonly NotifierHelper _notifierHelper;
private readonly NotificationSendHelper _notificationSendHelper;
private readonly LocalisationService _localisationService;
private readonly ItemHelper _itemHelper;
public DialogueHelper
(
ILogger logger,
HashUtil hashUtil,
SaveServer saveServer,
DatabaseServer databaseServer,
NotifierHelper notifierHelper,
NotificationSendHelper notificationSendHelper,
LocalisationService localisationService,
ItemHelper itemHelper
)
{
_logger = logger;
_hashUtil = hashUtil;
_saveServer = saveServer;
_databaseServer = databaseServer;
_notifierHelper = notifierHelper;
_localisationService = localisationService;
_itemHelper = itemHelper;
}
/// <summary>
/// Get the preview contents of the last message in a dialogue.
/// </summary>
@@ -14,7 +48,23 @@ public class DialogueHelper
/// <returns>MessagePreview</returns>
public MessagePreview GetMessagePreview(Models.Eft.Profile.Dialogue dialogue)
{
throw new NotImplementedException();
// The last message of the dialogue should be shown on the preview.
var message = dialogue.Messages[dialogue.Messages.Count - 1];
MessagePreview result = new()
{
DateTime = message?.DateTime,
MessageType = message?.MessageType,
TemplateId = message?.TemplateId,
UserId = dialogue?.Id
};
if (message?.Text is not null)
result.Text = message.Text;
if (message?.SystemData is not null)
result.SystemData = message?.SystemData;
return result;
}
/// <summary>
@@ -26,7 +76,36 @@ public class DialogueHelper
/// <returns></returns>
public List<Item> GetMessageItemContents(string messageID, string sessionID, string itemId)
{
throw new NotImplementedException();
var dialogueData = _saveServer.GetProfile(sessionID).DialogueRecords;
foreach (var dialogue in dialogueData)
{
var message = dialogueData[dialogue.Key].Messages.FirstOrDefault(x => x.Id == messageID);
if (message is null)
continue;
if (message.Id == messageID)
{
var attachmentsNew = _saveServer.GetProfile(sessionID).DialogueRecords[dialogue.Key].AttachmentsNew;
if (attachmentsNew > 0)
_saveServer.GetProfile(sessionID).DialogueRecords[dialogue.Key].AttachmentsNew = attachmentsNew - 1;
// Check reward count when item being moved isn't in reward list
// If count is 0, it means after this move occurs the reward array will be empty and all rewards collected
if (message.Items.Data is null)
message.Items.Data = new();
var rewardItems = message.Items.Data?.Where(x => x.Id != itemId);
if (rewardItems.Count() == 0)
{
message.RewardCollected = true;
message.HasRewards = false;
}
return message.Items.Data;
}
}
return new List<Item>();
}
/// <summary>
@@ -36,6 +115,26 @@ public class DialogueHelper
/// <returns>Dialog dictionary</returns>
public Dictionary<string, Models.Eft.Profile.Dialogue> GetDialogsForProfile(string sessionId)
{
throw new NotImplementedException();
var profile = _saveServer.GetProfile(sessionId);
if (profile.DialogueRecords is null)
profile.DialogueRecords = new();
return profile.DialogueRecords;
}
public Models.Eft.Profile.Dialogue? GetDialogueFromProfile(string profileId, string dialogueId)
{
Models.Eft.Profile.Dialogue? returnDialogue = null;
var dialogues = GetDialogsForProfile(profileId);
foreach (var dialogue in dialogues.Values)
{
if (dialogue.Id == dialogueId)
returnDialogue = dialogue;
break;
}
return returnDialogue;
}
}
+2 -2
View File
@@ -1044,9 +1044,9 @@ public class ItemHelper
throw new NotImplementedException();
}
public bool IsOfBaseclass(string valueEncyclopedia, string weapon)
public bool IsOfBaseclass(string tpl, List<string> baseClassTpls)
{
throw new NotImplementedException();
return _itemBaseClassService.ItemHasBaseClass(tpl, baseClassTpls);
}
}
+1 -1
View File
@@ -57,7 +57,7 @@ public class PresetHelper
var tempPresets = _databaseService.GetGlobals().ItemPresets;
tempPresets = tempPresets.Where(p =>
p.Value.Encyclopedia != null &&
_itemHelper.IsOfBaseclass(p.Value.Encyclopedia, BaseClasses.WEAPON)).ToDictionary();
_itemHelper.IsOfBaseclass(p.Value.Encyclopedia, [BaseClasses.WEAPON])).ToDictionary();
}
return _defaultWeaponPresets;
+3 -3
View File
@@ -506,7 +506,7 @@ public class ProfileHelper
/// <param name="pmcData">Player profile</param>
/// <param name="skill">Skill to look up and return value from</param>
/// <returns>Common skill object from desired profile</returns>
public Common? GetSkillFromProfile(PmcData pmcData, SkillTypes skill)
public BaseSkill? GetSkillFromProfile(PmcData pmcData, SkillTypes skill)
{
var skillToReturn = pmcData?.Skills?.Common.FirstOrDefault(s => s.Id == skill.ToString());
if (skillToReturn == null)
@@ -662,9 +662,9 @@ public class ProfileHelper
}
var customisationTemplateDb = _databaseService.GetTemplates().Customization;
var matchingCustomisation = customisationTemplateDb[reward.Target];
var matchingCustomisation = customisationTemplateDb.GetValueOrDefault(reward.Target, null);
if (matchingCustomisation != null)
if (matchingCustomisation is null)
{
var rewardToStore = new CustomisationStorage
{
+4 -2
View File
@@ -306,9 +306,11 @@ public class TraderHelper
/// </summary>
/// <param name="traderEnumValue">The trader enum value to validate</param>
/// <returns>The validated trader enum value as a string, or an empty string if invalid</returns>
public string GetValidTraderIdByEnumValue(object traderEnumValue) // TODO: param was Traders
public string GetValidTraderIdByEnumValue(string traderEnumValue) // TODO: param was Traders
{
throw new NotImplementedException();
var traderId = _databaseService.GetTraders();
var id = traderId.FirstOrDefault(x => x.Value.Base.Nickname.ToLower() == traderEnumValue.ToLower()).Key;
return id;
}
/// <summary>
+7 -5
View File
@@ -35,16 +35,18 @@ public class WeatherHelper
/// </summary>
/// <param name="currentDate">(new Date())</param>
/// <returns>Date object of current in-raid time</returns>
public DateTime GetInRaidTime(double? timestamp = null)
public DateTime GetInRaidTime(long? timestamp = null)
{
// tarkov time = (real time * 7 % 24 hr) + 3 hour
var russiaOffsetMilliseconds = _timeUtil.GetHoursAsSeconds(3) * 1000;
var twentyFourHoursMilliseconds = _timeUtil.GetHoursAsSeconds(24) * 1000;
var currentTimestampMilliSeconds = (timestamp is not null) ? timestamp : _timeUtil.GetTimeStamp();
var currentTimestampMilliSeconds = timestamp.HasValue
? timestamp ?? 0
: (DateTime.UtcNow - DateTime.UnixEpoch).TotalMilliseconds;
return new DateTime().AddMilliseconds(
(russiaOffsetMilliseconds + russiaOffsetMilliseconds * _weatherConfig.Acceleration) %
twentyFourHoursMilliseconds);
return _timeUtil.GetDateTimeFromTimeStamp((long)
(russiaOffsetMilliseconds + currentTimestampMilliSeconds * _weatherConfig.Acceleration) %
twentyFourHoursMilliseconds);
}
/// <summary>
+11 -5
View File
@@ -1,4 +1,4 @@
using Core.Annotations;
using Core.Annotations;
using Core.Models.Spt.Helper;
using ILogger = Core.Models.Utils.ILogger;
@@ -16,13 +16,19 @@ public class WeightedRandomHelper
}
/// <summary>
/// Choos an item from the passed in array based on the weightings of each
/// Choose an item from the passed in array based on the weightings of each
/// </summary>
/// <param name="itemArray">Items and weights to use</param>
/// <param name="values">Items and weights to use</param>
/// <returns>Chosen item from array</returns>
public T GetWeightedValue<T>(Dictionary<string, object> itemArray)
public T GetWeightedValue<T>(Dictionary<T, int> values) where T : notnull
{
throw new NotImplementedException();
var itemKeys = values.Keys.ToList();
var weights = values.Values.ToList();
var chosenItem = WeightedRandom<T>(itemKeys, weights);
return chosenItem.Item;
// SORRY IF THIS BLEW UP, I DONT SEE A REASON ITS GENERIC - CWX
}
/// <summary>
+1
View File
@@ -6,6 +6,7 @@ public class MinMax
{
[JsonPropertyName("type")]
public string? Type { get; set; }
[JsonPropertyName("max")]
public double? Max { get; set; }
@@ -9,4 +9,4 @@ public class RandomisedBotLevelResult
[JsonPropertyName("exp")]
public int? Exp { get; set; }
}
}
+40 -5
View File
@@ -1,4 +1,4 @@
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Hideout;
namespace Core.Models.Eft.Common;
@@ -540,7 +540,7 @@ public class Config
public BTRSettings? BTRSettings { get; set; }
[JsonPropertyName("EventType")]
public string[] EventType { get; set; }
public List<string> EventType { get; set; }
[JsonPropertyName("WalkSpeed")]
public XYZ? WalkSpeed { get; set; }
@@ -1294,13 +1294,13 @@ public class Mastering
public class Customization
{
[JsonPropertyName("SavageHead")]
public Dictionary<string, Dictionary<string, object>>? Head { get; set; }
public Dictionary<string, WildHead>? Head { get; set; }
[JsonPropertyName("SavageBody")]
public Dictionary<string, Dictionary<string, object>>? Body { get; set; }
public Dictionary<string, WildBody>? Body { get; set; }
[JsonPropertyName("SavageFeet")]
public Dictionary<string, Dictionary<string, object>>? Feet { get; set; }
public Dictionary<string, WildFeet>? Feet { get; set; }
[JsonPropertyName("CustomizationVoice")]
public List<CustomizationVoice>? VoiceOptions { get; set; }
@@ -1309,6 +1309,41 @@ public class Customization
public BodyParts? BodyParts { get; set; }
}
public class WildHead
{
[JsonPropertyName("head")]
public string? Head { get; set; }
[JsonPropertyName("isNotRandom")]
public bool? IsNotRandom { get; set; }
[JsonPropertyName("NotRandom")]
public bool? NotRandom { get; set; }
}
public class WildBody
{
[JsonPropertyName("body")]
public string? Body { get; set; }
[JsonPropertyName("hands")]
public string? Hands { get; set; }
[JsonPropertyName("isNotRandom")]
public bool? IsNotRandom { get; set; }
}
public class WildFeet
{
[JsonPropertyName("feet")]
public string? Feet { get; set; }
[JsonPropertyName("isNotRandom")]
public bool? IsNotRandom { get; set; }
[JsonPropertyName("NotRandom")]
public bool? NotRandom { get; set; }
}
public class CustomizationVoice
{
[JsonPropertyName("voice")]
+2 -2
View File
@@ -521,7 +521,7 @@ public class BotLocationModifier
public double? AccuracySpeed { get; set; }
[JsonPropertyName("AdditionalHostilitySettings")]
public AdditionalHostilitySettings[] AdditionalHostilitySettings { get; set; }
public List<AdditionalHostilitySettings> AdditionalHostilitySettings { get; set; }
[JsonPropertyName("DistToActivate")]
public double? DistanceToActivate { get; set; }
@@ -939,4 +939,4 @@ public enum WildSpawnType
pmcbot,
bosskilla,
bossknight
}
}
+6 -5
View File
@@ -300,15 +300,17 @@ public class BaseJsonSkills
public class Skills
{
public List<Common>? Common { get; set; }
public List<BaseSkill>? Common { get; set; }
public List<Mastering>? Mastering { get; set; }
public List<BaseSkill>? 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
@@ -511,7 +512,7 @@ public class Hideout
public Dictionary<string, HideoutImprovement>? Improvements { get; set; }
public HideoutCounters? HideoutCounters { get; set; }
public double? Seed { get; set; }
public List<string>? MannequinPoses { get; set; }
public Dictionary<string, string>? MannequinPoses { get; set; }
[JsonPropertyName("sptUpdateLastRunTimestamp")]
public long? SptUpdateLastRunTimestamp { get; set; }
+2 -1
View File
@@ -1,5 +1,6 @@
using System.Text.Json.Serialization;
using Core.Models.Common;
using Core.Models.Enums;
using Core.Utils.Json.Converters;
namespace Core.Models.Eft.Common.Tables;
@@ -332,7 +333,7 @@ public class BodyPart
public class BotTypeInventory
{
[JsonPropertyName("equipment")]
public Dictionary<string, Dictionary<string, double>>? Equipment { get; set; }
public Dictionary<EquipmentSlots, Dictionary<string, double>>? Equipment { get; set; }
public GlobalAmmo? Ammo { get; set; }
+9 -2
View File
@@ -11,6 +11,7 @@ public class Item
public string? Template { get; set; }
[JsonPropertyName("parentId")]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public string? ParentId { get; set; }
[JsonPropertyName("slotId")]
@@ -108,11 +109,17 @@ public class UpdMap
public class MapMarker
{
[JsonPropertyName("Type")]
public string? Type { get; set; }
[JsonPropertyName("X")]
public int? X { get; set; }
public double? X { get; set; }
[JsonPropertyName("Y")]
public int? Y { get; set; }
public double? Y { get; set; }
[JsonPropertyName("Note")]
public string? Note { get; set; }
}
public class UpdTag
@@ -119,7 +119,7 @@ public class ChangeRequirement
public List<ChangeCost?>? ChangeCost { get; set; }
[JsonPropertyName("changeStandingCost")]
public int? ChangeStandingCost { get; set; }
public double? ChangeStandingCost { get; set; }
}
public class ChangeCost
@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
namespace Core.Models.Eft.Hideout
{
public class HideoutCustomizationSetMannequinPoseRequest
{
[JsonPropertyName("Action")]
public string? Action { get; set; } = "HideoutCustomizationSetMannequinPose";
[JsonPropertyName("poses")]
public Dictionary<string, string> Poses { get; set; }
[JsonPropertyName("timestamp")]
public double Timestamp { get; set; }
}
}
@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using Core.Models.Eft.Common.Tables;
namespace Core.Models.Eft.Inventory;
@@ -13,18 +14,3 @@ public class InventoryCreateMarkerRequestData : InventoryBaseActionRequestData
[JsonPropertyName("mapMarker")]
public MapMarker? MapMarker { get; set; }
}
public class MapMarker
{
[JsonPropertyName("Type")]
public string? Type { get; set; }
[JsonPropertyName("X")]
public double? X { get; set; }
[JsonPropertyName("Y")]
public double? Y { get; set; }
[JsonPropertyName("Note")]
public string? Note { get; set; }
}
@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using Core.Models.Eft.Common.Tables;
namespace Core.Models.Eft.Inventory;
@@ -18,4 +19,4 @@ public class InventoryEditMarkerRequestData : InventoryBaseActionRequestData
[JsonPropertyName("mapMarker")]
public MapMarker? MapMarker { get; set; }
}
}
+3 -3
View File
@@ -1,4 +1,4 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
using Core.Models.Enums;
using Core.Models.Enums.RaidSettings;
using Core.Models.Enums.RaidSettings.TimeAndWeather;
@@ -17,7 +17,7 @@ public class RaidSettings
public bool? IsLocationTransition { get; set; }
[JsonPropertyName("timeVariant")]
public DateTime? TimeVariant { get; set; }
public DateTimeEnum TimeVariant { get; set; }
[JsonPropertyName("metabolismDisabled")]
public bool? MetabolismDisabled { get; set; }
@@ -93,4 +93,4 @@ public class WavesSettings
[JsonPropertyName("isTaggedAndCursed")]
public bool? IsTaggedAndCursed { get; set; }
}
}
+16 -16
View File
@@ -1,19 +1,19 @@
namespace Core.Models.Enums;
namespace Core.Models.Enums;
public enum EquipmentSlots
{
HEADWEAR,
EARPIECE,
FACE_COVER,
ARMOR_VEST,
EYEWEAR,
ARM_BAND,
TACTICAL_VEST,
POCKETS,
BACKPACK,
SECURED_CONTAINER,
FIRST_PRIMARY_WEAPON,
SECOND_PRIMARY_WEAPON,
HOLSTER,
SCABBARD
}
Headwear,
Earpiece,
FaceCover,
ArmorVest,
Eyewear,
ArmBand,
TacticalVest,
Pockets,
Backpack,
SecuredContainer,
FirstPrimaryWeapon,
SecondPrimaryWeapon,
Holster,
Scabbard
}
+2 -1
View File
@@ -1,4 +1,4 @@
namespace Core.Models.Enums;
namespace Core.Models.Enums;
public class HideoutEventActions
{
@@ -17,4 +17,5 @@ public class HideoutEventActions
public const string HIDEOUT_CIRCLE_OF_CULTIST_PRODUCTION_START = "HideoutCircleOfCultistProductionStart";
public const string HIDEOUT_DELETE_PRODUCTION_COMMAND = "HideoutDeleteProductionCommand";
public const string HIDEOUT_CUSTOMIZATION_APPLY_COMMAND = "HideoutCustomizationApply";
public const string HIDEOUT_CUSTOMIZATION_SET_MANNEQUIN_POSE = "HideoutCustomizationSetMannequinPose";
}
+2 -2
View File
@@ -43,13 +43,13 @@ public class BotGenerationDetails
/// Delta of highest level of bot e.g. 50 means 50 levels above player
/// </summary>
[JsonPropertyName("botRelativeLevelDeltaMax")]
public int? BotRelativeLevelDeltaMax { get; set; }
public double? BotRelativeLevelDeltaMax { get; set; }
/// <summary>
/// Delta of lowest level of bot e.g. 50 means 50 levels below player
/// </summary>
[JsonPropertyName("botRelativeLevelDeltaMin")]
public int? BotRelativeLevelDeltaMin { get; set; }
public double? BotRelativeLevelDeltaMin { get; set; }
/// <summary>
/// How many to create and store
@@ -1,5 +1,6 @@
using System.Text.Json.Serialization;
using Core.Models.Eft.Common.Tables;
using Core.Models.Enums;
using Core.Models.Spt.Config;
namespace Core.Models.Spt.Bots;
@@ -10,13 +11,13 @@ public class GenerateEquipmentProperties
/// Root Slot being generated
/// </summary>
[JsonPropertyName("rootEquipmentSlot")]
public string? RootEquipmentSlot { get; set; }
public EquipmentSlots RootEquipmentSlot { get; set; }
/// <summary>
/// Equipment pool for root slot being generated
/// </summary>
[JsonPropertyName("rootEquipmentPool")]
public Dictionary<string, int>? RootEquipmentPool { get; set; }
public Dictionary<string, double>? RootEquipmentPool { get; set; }
[JsonPropertyName("modPool")]
public GlobalMods? ModPool { get; set; }
@@ -53,4 +54,4 @@ public class GenerateEquipmentProperties
[JsonPropertyName("generatingPlayerLevel")]
public int? GeneratingPlayerLevel { get; set; }
}
}
+2 -1
View File
@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Text.Json.Serialization;
using Core.Models.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Enums;
namespace Core.Models.Spt.Config;
@@ -479,7 +480,7 @@ public class EquipmentFilterDetails
/// Key: equipment slot name e.g. FirstPrimaryWeapon, value: item tpls
/// </summary>
[JsonPropertyName("gear")]
public Dictionary<string, List<string>>? Gear { get; set; }
public Dictionary<EquipmentSlots, List<string>>? Gear { get; set; }
/// <summary>
/// Key: cartridge type e.g. Caliber23x75, value: item tpls
+5 -8
View File
@@ -1,4 +1,4 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
using Core.Models.Eft.Game;
namespace Core.Models.Spt.Config;
@@ -172,15 +172,9 @@ public class ServerFeatures
public class ChatbotFeatures
{
[JsonPropertyName("sptFriendEnabled")]
public bool SptFriendEnabled { get; set; }
[JsonPropertyName("sptFriendGiftsEnabled")]
public bool SptFriendGiftsEnabled { get; set; }
[JsonPropertyName("commandoEnabled")]
public bool CommandoEnabled { get; set; }
[JsonPropertyName("commandoFeatures")]
public CommandoFeatures CommandoFeatures { get; set; }
@@ -189,10 +183,13 @@ public class ChatbotFeatures
[JsonPropertyName("ids")]
public Dictionary<string, string> Ids { get; set; }
[JsonPropertyName("enabledBots")]
public Dictionary<string, bool> EnabledBots { get; set; }
}
public class CommandoFeatures
{
[JsonPropertyName("giveCommandEnabled")]
public bool GiveCommandEnabled { get; set; }
}
}
+7 -1
View File
@@ -1,4 +1,4 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
using Core.Models.Common;
using Core.Models.Eft.Common;
@@ -132,6 +132,12 @@ public class LocationConfig : BaseConfig
[JsonPropertyName("reserveRaiderSpawnChanceOverrides")]
public ReserveRaiderSpawnChanceOverrides ReserveRaiderSpawnChanceOverrides { get; set; }
/// <summary>
/// Containers to remove all children from when generating static/loose loot
/// </summary>
[JsonPropertyName("tplsToStripChildItemsFrom")]
public List<string> TplsToStripChildItemsFrom { get; set; }
/// <summary>
/// Map ids players cannot visit
/// </summary>
+2 -1
View File
@@ -1,5 +1,6 @@
using System.Text.Json.Serialization;
using Core.Models.Eft.Common.Tables;
using Core.Models.Enums;
namespace Core.Models.Spt.Config;
@@ -24,7 +25,7 @@ public class KarmaLevel
public Dictionary<string, GenerationData> ItemLimits { get; set; }
[JsonPropertyName("equipmentBlacklist")]
public Dictionary<string, string[]> EquipmentBlacklist { get; set; }
public Dictionary<EquipmentSlots, string[]> EquipmentBlacklist { get; set; }
[JsonPropertyName("labsAccessCardChancePercent")]
public double? LabsAccessCardChancePercent { get; set; }
+16 -4
View File
@@ -12,11 +12,11 @@ public class PmcConfig : BaseConfig
/** What game version should the PMC have */
[JsonPropertyName("gameVersionWeight")]
public Dictionary<string, double> GameVersionWeight { get; set; }
public Dictionary<string, int> GameVersionWeight { get; set; }
/** What account type should the PMC have */
[JsonPropertyName("accountTypeWeight")]
public Dictionary<MemberCategory, double> AccountTypeWeight { get; set; }
public Dictionary<MemberCategory, int> AccountTypeWeight { get; set; }
/** Global whitelist/blacklist of vest loot for PMCs */
[JsonPropertyName("vestLoot")]
@@ -76,7 +76,7 @@ public class PmcConfig : BaseConfig
public Dictionary<string, Dictionary<string, Dictionary<string, double>>> PmcType { get; set; }
[JsonPropertyName("maxBackpackLootTotalRub")]
public List<IMinMaxLootValue> MaxBackpackLootTotalRub { get; set; }
public List<MinMaxLootValue> MaxBackpackLootTotalRub { get; set; }
[JsonPropertyName("maxPocketLootTotalRub")]
public double MaxPocketLootTotalRub { get; set; }
@@ -115,6 +115,9 @@ public class PmcConfig : BaseConfig
[JsonPropertyName("addPrefixToSameNamePMCAsPlayerChance")]
public int? AddPrefixToSameNamePMCAsPlayerChance { get; set; }
[JsonPropertyName("lootItemLimitsRub")]
public List<MinMaxLootValue>? LootItemLimitsRub { get; set; }
}
public class HostilitySettings
@@ -164,8 +167,17 @@ public class SlotLootSettings
public List<string> Blacklist { get; set; }
}
public class IMinMaxLootValue : MinMax
public class MinMaxLootValue : MinMax
{
[JsonPropertyName("value")]
public double Value { get; set; }
[JsonPropertyName("backpack")]
public MinMax Backpack { get; set; }
[JsonPropertyName("pocket")]
public MinMax Pocket { get; set; }
[JsonPropertyName("vest")]
public MinMax Vest { get; set; }
}
+53 -13
View File
@@ -15,11 +15,11 @@ public class SeasonalEventConfig : BaseConfig
/** event / botType / equipSlot / itemid */
[JsonPropertyName("eventGear")]
public Dictionary<string, Dictionary<string, Dictionary<string, Dictionary<string, int>>>> EventGear { get; set; }
public Dictionary<SeasonalEventType, Dictionary<string, Dictionary<string, Dictionary<string, int>>>> EventGear { get; set; }
/** event / bot type / equipSlot / itemid */
[JsonPropertyName("eventLoot")]
public Dictionary<string, Dictionary<string, Dictionary<string, Dictionary<string, int>>>> EventLoot { get; set; }
public Dictionary<SeasonalEventType, Dictionary<string, Dictionary<string, Dictionary<string, int>>>> EventLoot { get; set; }
[JsonPropertyName("events")]
public List<SeasonalEvent> Events { get; set; }
@@ -77,38 +77,78 @@ public class SeasonalEvent
public int EndMonth { get; set; }
[JsonPropertyName("settings")]
public Dictionary<string, object> Settings { get; set; } // TODO: Type was Record<string, ISeasonalEventSettings | IZombieSettings>
public SeasonalEventSettings? Settings { get; set; }
[JsonPropertyName("setting")]
public Dictionary<string, object> SettingsDoNOTUse { set => Settings = value; }
public SeasonalEventSettings? SettingsDoNOTUse { set => Settings = value; }
}
public class SeasonalEventSettings
{
[JsonPropertyName("enabled")]
public bool Enabled { get; set; }
[JsonPropertyName("enableSummoning")]
public bool? EnableSummoning { get; set; }
[JsonPropertyName("enableHalloweenHideout")]
public bool? EnableHalloweenHideout { get; set; }
[JsonPropertyName("enableChristmasHideout")]
public bool? EnableChristmasHideout { get; set; }
[JsonPropertyName("enableSanta")]
public bool? EnableSanta { get; set; }
[JsonPropertyName("adjustBotAppearances")]
public bool? AdjustBotAppearances { get; set; }
[JsonPropertyName("addEventGearToBots")]
public bool? AddEventGearToBots { get; set; }
[JsonPropertyName("addEventLootToBots")]
public bool? AddEventLootToBots { get; set; }
[JsonPropertyName("removeEntryRequirement")]
public List<string>? RemoveEntryRequirement { get; set; }
[JsonPropertyName("replaceBotHostility")]
public bool? ReplaceBotHostility { get; set; }
[JsonPropertyName("forceSeason")]
public Season? ForceSeason { get; set; }
[JsonPropertyName("zombieSettings")]
public ZombieSettings? ZombieSettings { get; set; }
[JsonPropertyName("disableBosses")]
public List<string>? DisableBosses { get; set; }
[JsonPropertyName("disableWaves")]
public List<string>? DisableWaves { get; set; }
}
public class ZombieSettings : SeasonalEventSettings
public class ZombieSettings
{
[JsonPropertyName("enabled")]
public bool? Enabled { get; set; }
[JsonPropertyName("mapInfectionAmount")]
public Dictionary<string, int> MapInfectionAmount { get; set; }
public Dictionary<string, int>? MapInfectionAmount { get; set; }
[JsonPropertyName("disableBosses")]
public List<string> DisableBosses { get; set; }
public List<string>? DisableBosses { get; set; }
[JsonPropertyName("disableWaves")]
public List<string> DisableWaves { get; set; }
public List<string>? DisableWaves { get; set; }
}
public class GifterSetting
{
[JsonPropertyName("map")]
public string Map { get; set; }
public string? Map { get; set; }
[JsonPropertyName("zones")]
public string Zones { get; set; }
public string? Zones { get; set; }
[JsonPropertyName("spawnChance")]
public int SpawnChance { get; set; }
public int? SpawnChance { get; set; }
}
+1 -1
View File
@@ -53,7 +53,7 @@ public class SendMessageDetails
/// Optional - How long items will be stored in mail before expiry
/// </summary>
[JsonPropertyName("itemsMaxStorageLifetimeSeconds")]
public int? ItemsMaxStorageLifetimeSeconds { get; set; }
public long? ItemsMaxStorageLifetimeSeconds { get; set; }
/// <summary>
/// Optional - Used when sending messages from traders who send text from locale json
@@ -1,4 +1,4 @@
using Core.Annotations;
using Core.Annotations;
using Core.Callbacks;
using Core.DI;
using Core.Models.Eft.Common;
@@ -11,7 +11,7 @@ namespace Core.Routers.ItemEvents;
[Injectable(InjectableTypeOverride = typeof(ItemEventRouterDefinition))]
public class HideoutItemEventRouter : ItemEventRouterDefinition
{
protected HideoutCallbacks _hideoutCallbacks;
private readonly HideoutCallbacks _hideoutCallbacks;
public HideoutItemEventRouter
(
@@ -38,7 +38,8 @@ public class HideoutItemEventRouter : ItemEventRouterDefinition
new HandledRoute(HideoutEventActions.HIDEOUT_CANCEL_PRODUCTION_COMMAND, false),
new HandledRoute(HideoutEventActions.HIDEOUT_CIRCLE_OF_CULTIST_PRODUCTION_START, false),
new HandledRoute(HideoutEventActions.HIDEOUT_DELETE_PRODUCTION_COMMAND, false),
new HandledRoute(HideoutEventActions.HIDEOUT_CUSTOMIZATION_APPLY_COMMAND, false)
new HandledRoute(HideoutEventActions.HIDEOUT_CUSTOMIZATION_APPLY_COMMAND, false),
new HandledRoute(HideoutEventActions.HIDEOUT_CUSTOMIZATION_SET_MANNEQUIN_POSE, false)
};
}
@@ -75,6 +76,8 @@ public class HideoutItemEventRouter : ItemEventRouterDefinition
return _hideoutCallbacks.HideoutDeleteProductionCommand(pmcData, body as HideoutDeleteProductionRequestData, sessionID);
case HideoutEventActions.HIDEOUT_CUSTOMIZATION_APPLY_COMMAND:
return _hideoutCallbacks.HideoutCustomizationApplyCommand(pmcData, body as HideoutCustomizationApplyRequestData, sessionID);
case HideoutEventActions.HIDEOUT_CUSTOMIZATION_SET_MANNEQUIN_POSE:
return _hideoutCallbacks.HideoutCustomizationSetMannequinPose(pmcData, body as HideoutCustomizationSetMannequinPoseRequest, sessionID);
default:
throw new Exception($"HideoutItemEventRouter being used when it cant handle route {url}");
}
+18 -2
View File
@@ -1,13 +1,26 @@
using Core.Annotations;
using Core.Models.Eft.Common.Tables;
using Core.Models.Enums;
using Core.Models.Spt.Bots;
using Core.Models.Spt.Config;
using Core.Servers;
namespace Core.Services;
[Injectable(InjectionType.Singleton)]
public class BotEquipmentFilterService
{
private readonly ConfigServer _configServer;
private readonly BotConfig _botConfig;
public BotEquipmentFilterService(
ConfigServer configServer)
{
_configServer = configServer;
_botConfig = _configServer.GetConfig<BotConfig>(ConfigTypes.BOT);
}
/// <summary>
/// Filter a bots data to exclude equipment and cartridges defines in the botConfig
/// </summary>
@@ -74,9 +87,12 @@ public class BotEquipmentFilterService
/// <param name="botRole">Role of the bot we want the blacklist for</param>
/// <param name="playerLevel">Level of the player</param>
/// <returns>EquipmentBlacklistDetails object</returns>
public EquipmentFilterDetails GetBotEquipmentBlacklist(string botRole, int playerLevel)
public EquipmentFilterDetails? GetBotEquipmentBlacklist(string botRole, double playerLevel)
{
throw new NotImplementedException();
var blacklistDetailsForBot = _botConfig.Equipment.GetValueOrDefault(botRole, null);
return blacklistDetailsForBot?.Blacklist?.FirstOrDefault(
(equipmentFilter) => playerLevel >= equipmentFilter.LevelRange.Min && playerLevel <= equipmentFilter.LevelRange.Max);
}
/// <summary>
+109 -6
View File
@@ -1,12 +1,47 @@
using Core.Annotations;
using Core.Annotations;
using Core.Models.Eft.Common.Tables;
using Core.Models.Spt.Bots;
using Core.Helpers;
using Core.Models.Enums;
using Core.Models.Spt.Config;
using Core.Servers;
using ILogger = Core.Models.Utils.ILogger;
using Core.Utils;
namespace Core.Services;
[Injectable(InjectionType.Singleton)]
public class BotNameService
{
private readonly ILogger _logger;
private readonly BotHelper _botHelper;
private readonly RandomUtil _randomUtil;
private readonly LocalisationService _localisationService;
private readonly DatabaseService _databaseService;
private readonly ConfigServer _configServer;
private readonly BotConfig _botConfig;
private readonly HashSet<string> _usedNameCache;
public BotNameService(
ILogger logger,
BotHelper botHelper,
RandomUtil randomUtil,
LocalisationService localisationService,
DatabaseService databaseService,
ConfigServer configServer)
{
_logger = logger;
_botHelper = botHelper;
_randomUtil = randomUtil;
_localisationService = localisationService;
_databaseService = databaseService;
_configServer = configServer;
_botConfig = _configServer.GetConfig<BotConfig>(ConfigTypes.BOT);
_usedNameCache = new HashSet<string>();
}
/// <summary>
/// Clear out any entries in Name Set
/// </summary>
@@ -22,7 +57,6 @@ public class BotNameService
/// <param name="botGenerationDetails"></param>
/// <param name="botRole">role of bot e.g. assault</param>
/// <param name="uniqueRoles">Lowercase roles to always make unique</param>
/// <param name="sessionId">OPTIONAL: profile session id</param>
/// <returns>Nickname for bot</returns>
public string GenerateUniqueBotNickname(
BotType botJsonTemplate,
@@ -30,7 +64,68 @@ public class BotNameService
string botRole,
List<string> uniqueRoles = null)
{
throw new NotImplementedException();
var isPmc = botGenerationDetails.IsPmc;
// Never show for players
var showTypeInNickname = !botGenerationDetails.IsPlayerScav.GetValueOrDefault(false) && _botConfig.ShowTypeInNickname;
var roleShouldBeUnique = uniqueRoles?.Contains(botRole.ToLower());
var isUnique = true;
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"
? _botHelper.GetPmcNicknameOfMaxLength(_botConfig.BotNameLengthLimit, botGenerationDetails.Side)
: $"{ _randomUtil.GetArrayValue(botJsonTemplate.FirstNames)} {_randomUtil.GetArrayValue(botJsonTemplate.LastNames)}";
name = name.Trim();
// Config is set to add role to end of bot name
if (showTypeInNickname)
{
name += $" { botRole}";
}
// Replace pmc bot names with player name + prefix
if (botGenerationDetails.IsPmc.GetValueOrDefault(false) && botGenerationDetails.AllPmcsHaveSameNameAsPlayer.GetValueOrDefault(false))
{
var prefix = _localisationService.GetRandomTextThatMatchesPartialKey("pmc-name_prefix_");
name = $"{prefix} { name}";
}
// Is this a role that must be unique
if (roleShouldBeUnique.GetValueOrDefault(false))
{
// Check name in cache
isUnique = _usedNameCache.Contains(name);
if (!isUnique)
{
// Not unique
if (attempts >= 5)
{
// 5 attempts to generate a name, pool probably isn't big enough
var genericName = $"{ botGenerationDetails.Side} { _randomUtil.GetInt(100000, 999999)}";
_logger.Debug($"Failed to find unique name for: { botRole} ${ botGenerationDetails.Side} after 5 attempts, using: ${ genericName}");
return genericName;
}
attempts++;
// Try again
continue;
}
}
// Add bot name to cache to prevent being used again
_usedNameCache.Add(name);
return name;
}
// Should never reach here
return $"BOT {botRole} {botGenerationDetails.BotDifficulty}";
}
/// <summary>
@@ -39,15 +134,23 @@ public class BotNameService
/// <param name="bot">Bot to update</param>
public void AddRandomPmcNameToBotMainProfileNicknameProperty(BotBase bot)
{
throw new NotImplementedException();
// Simulate bot looking like a player scav with the PMC name in brackets.
// E.g. "ScavName (PMC Name)"
bot.Info.MainProfileNickname = GetRandomPmcName();
}
/// <summary>
/// Choose a random PMC name from bear or usec bot jsons
/// </summary>
/// <returns>PMC name as string</returns>
protected string GetRandomPMCName()
protected string GetRandomPmcName()
{
throw new NotImplementedException();
var bots = _databaseService.GetBots().Types;
var pmcNames = new List<string>();
pmcNames.AddRange(bots["usec"].FirstNames);
pmcNames.AddRange(bots["bear"].FirstNames);
return _randomUtil.GetArrayValue(pmcNames);
}
}
+140 -12
View File
@@ -2,7 +2,9 @@ using Core.Annotations;
using Core.Helpers;
using Core.Models.Enums;
using Core.Models.Spt.Config;
using Core.Models.Spt.Dialog;
using Core.Servers;
using Core.Utils;
using ILogger = Core.Models.Utils.ILogger;
namespace Core.Services;
@@ -11,18 +13,34 @@ namespace Core.Services;
public class GiftService
{
private readonly ILogger _logger;
private readonly ConfigServer _configServer;
private readonly MailSendService _mailSendService;
private readonly LocalisationService _localisationService;
private readonly HashUtil _hashUtil;
private readonly TimeUtil _timeUtil;
private readonly ProfileHelper _profileHelper;
private readonly ConfigServer _configServer;
private readonly GiftsConfig _giftConfig;
public GiftService(
public GiftService
(
ILogger logger,
ConfigServer configServer,
ProfileHelper profileHelper)
MailSendService mailSendService,
LocalisationService localisationService,
HashUtil hashUtil,
TimeUtil timeUtil,
ProfileHelper profileHelper,
ConfigServer configServer
)
{
_logger = logger;
_configServer = configServer;
_mailSendService = mailSendService;
_localisationService = localisationService;
_hashUtil = hashUtil;
_timeUtil = timeUtil;
_profileHelper = profileHelper;
_configServer = configServer;
_giftConfig = _configServer.GetConfig<GiftsConfig>();
}
@@ -34,7 +52,7 @@ public class GiftService
*/
public bool GiftExists(string giftId)
{
throw new NotImplementedException();
return _giftConfig.Gifts[giftId] is not null;
}
public Gift GetGiftById(string giftId)
@@ -50,7 +68,7 @@ public class GiftService
*/
public Dictionary<string, Gift> GetGifts()
{
throw new NotImplementedException();
return _giftConfig.Gifts;
}
/**
@@ -59,7 +77,7 @@ public class GiftService
*/
public List<string> GetGiftIds()
{
throw new NotImplementedException();
return _giftConfig.Gifts.Keys.ToList();
}
/**
@@ -90,7 +108,96 @@ public class GiftService
_logger.Warning($"Gift {giftId} has items but no collection time limit, defaulting to 48 hours");
}
throw new NotImplementedException();
// Handle system messsages
if (giftData.Sender == GiftSenderType.System)
{
// Has a localisable text id to send to player
if (giftData.LocaleTextId is not null)
{
_mailSendService.SendLocalisedSystemMessageToPlayer(
playerId,
giftData.LocaleTextId,
giftData.Items,
giftData.ProfileChangeEvents,
_timeUtil.GetHoursAsSeconds(giftData.CollectionTimeHours ?? 1));
}
else
{
_mailSendService.SendSystemMessageToPlayer(
playerId,
giftData.MessageText,
giftData.Items,
_timeUtil.GetHoursAsSeconds(giftData.CollectionTimeHours ?? 1),
giftData.ProfileChangeEvents);
}
}
// Handle user messages
else if (giftData.Sender == GiftSenderType.User)
{
_mailSendService.SendUserMessageToPlayer(
playerId,
giftData.SenderDetails,
giftData.MessageText,
giftData.Items,
_timeUtil.GetHoursAsSeconds(giftData.CollectionTimeHours ?? 1));
}
else if (giftData.Sender == GiftSenderType.Trader)
{
if (giftData.LocaleTextId is not null)
{
_mailSendService.SendLocalisedNpcMessageToPlayer(
playerId,
giftData.Trader,
MessageType.MESSAGE_WITH_ITEMS,
giftData.LocaleTextId,
giftData.Items,
_timeUtil.GetHoursAsSeconds(giftData.CollectionTimeHours ?? 1),
null,
null
);
}
else
{
_mailSendService.SendLocalisedNpcMessageToPlayer(
playerId,
giftData.Trader,
MessageType.MESSAGE_WITH_ITEMS,
giftData.MessageText,
giftData.Items,
_timeUtil.GetHoursAsSeconds(giftData.CollectionTimeHours ?? 1),
null,
null
);
}
}
else
{
// TODO: further split out into different message systems like above SYSTEM method
// Trader / ragfair
SendMessageDetails details = new () {
RecipientId = playerId,
Sender = GetMessageType(giftData),
SenderDetails = new ()
{
Id = GetSenderId(giftData),
Aid = 1234567, // TODO - pass proper aid value
Info = null,
},
MessageText = giftData.MessageText,
Items = giftData.Items,
ItemsMaxStorageLifetimeSeconds = _timeUtil.GetHoursAsSeconds(giftData.CollectionTimeHours ?? 0),
};
if (giftData.Trader is not null) {
details.Trader = giftData.Trader;
}
_mailSendService.SendMessageToPlayer(details);
}
_profileHelper.FlagGiftReceivedInProfile(playerId, giftId, maxGiftsToSendCount);
return GiftSentResult.SUCCESS;
}
/**
@@ -98,9 +205,19 @@ public class GiftService
* @param giftData Gift to send player
* @returns trader/user/system id
*/
protected string? GetSenderId(Gift giftData)
private string? GetSenderId(Gift giftData)
{
throw new NotImplementedException();
if (giftData.Sender == GiftSenderType.Trader)
{
return Enum.GetName(typeof(GiftSenderType), giftData.Sender);
}
if (giftData.Sender == GiftSenderType.User)
{
return giftData.Sender.ToString();
}
return null;
}
/**
@@ -110,7 +227,18 @@ public class GiftService
*/
protected MessageType? GetMessageType(Gift giftData)
{
throw new NotImplementedException();
switch (giftData.Sender)
{
case GiftSenderType.System:
return MessageType.SYSTEM_MESSAGE;
case GiftSenderType.Trader:
return MessageType.NPC_TRADER;
case GiftSenderType.User:
return MessageType.USER_MESSAGE;
default:
_logger.Error(_localisationService.GetText("gift-unable_to_handle_message_type_command", giftData.Sender));
return null;
}
}
/**
+41 -1
View File
@@ -1,10 +1,38 @@
using Core.Annotations;
using Core.Annotations;
using Core.Models.Enums;
using Core.Models.Spt.Config;
using Core.Servers;
using Core.Utils.Cloners;
using ILogger = Core.Models.Utils.ILogger;
namespace Core.Services;
[Injectable(InjectionType.Singleton)]
public class ItemFilterService
{
private readonly ILogger _logger;
private readonly ICloner _cloner;
private readonly DatabaseServer _databaseServer;
private readonly ConfigServer _configServer;
private readonly HashSet<string> _lootableItemBlacklistCache = [];
private readonly ItemConfig _itemConfig;
public ItemFilterService(
ILogger logger,
ICloner cloner,
DatabaseServer databaseServer,
ConfigServer configServer
)
{
_logger = logger;
_cloner = cloner;
_databaseServer = databaseServer;
_configServer = configServer;
_itemConfig = _configServer.GetConfig<ItemConfig>(ConfigTypes.ITEM);
}
/**
* Check if the provided template id is blacklisted in config/item.json/blacklist
* @param tpl template id
@@ -89,4 +117,16 @@ public class ItemFilterService
{
throw new NotImplementedException();
}
public bool IsLootableItemBlacklisted(string itemKey)
{
if (_lootableItemBlacklistCache.Count == 0)
{
foreach (var item in _itemConfig.LootableItemBlacklist) {
_lootableItemBlacklistCache.Add(item);
}
}
return _lootableItemBlacklistCache.Contains(itemKey);
}
}
+565
View File
@@ -0,0 +1,565 @@
using Core.Annotations;
using Core.Helpers;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Profile;
using Core.Models.Enums;
using Core.Models.Spt.Dialog;
using Core.Servers;
using Core.Utils;
using ILogger = Core.Models.Utils.ILogger;
namespace Core.Services;
[Injectable]
public class MailSendService
{
private readonly ILogger _logger;
private readonly HashUtil _hashUtil;
private readonly TimeUtil _timeUtil;
private readonly SaveServer _saveServer;
private readonly DatabaseService _databaseService;
private readonly NotifierHelper _notifierHelper;
private readonly DialogueHelper _dialogueHelper;
private readonly NotificationSendHelper _notificationSendHelper;
private readonly LocalisationService _localisationService;
private readonly ItemHelper _itemHelper;
private readonly TraderHelper _traderHelper;
private const string _systemSenderId = "59e7125688a45068a6249071";
private readonly List<MessageType> _messageTypes = [MessageType.NPC_TRADER, MessageType.FLEAMARKET_MESSAGE];
private readonly List<string> _slotNames = ["hideout", "main"];
public MailSendService
(
ILogger logger,
HashUtil hashUtil,
TimeUtil timeUtil,
SaveServer saveServer,
DatabaseService databaseService,
NotifierHelper notifierHelper,
DialogueHelper dialogueHelper,
NotificationSendHelper notificationSendHelper,
LocalisationService localisationService,
ItemHelper itemHelper,
TraderHelper traderHelper
)
{
_logger = logger;
_hashUtil = hashUtil;
_timeUtil = timeUtil;
_saveServer = saveServer;
_databaseService = databaseService;
_notifierHelper = notifierHelper;
_dialogueHelper = dialogueHelper;
_localisationService = localisationService;
_itemHelper = itemHelper;
_traderHelper = traderHelper;
}
/**
* Send a message from an NPC (e.g. prapor) to the player with or without items using direct message text, do not look up any locale
* @param sessionId The session ID to send the message to
* @param trader The trader sending the message
* @param messageType What type the message will assume (e.g. QUEST_SUCCESS)
* @param message Text to send to the player
* @param items Optional items to send to player
* @param maxStorageTimeSeconds Optional time to collect items before they expire
*/
public void SendDirectNpcMessageToPlayer(
string sessionId,
string trader,
MessageType messageType,
string message,
List<Item>? items,
long? maxStorageTimeSeconds,
SystemData? systemData,
MessageContentRagfair? ragfair
)
{
if (trader is null)
{
_logger.Error(_localisationService.GetText("mailsend-missing_trader", new
{
MessageType = messageType,
SessionId = sessionId,
}));
return;
}
SendMessageDetails details = new()
{
RecipientId = sessionId,
Sender = messageType,
DialogType = MessageType.NPC_TRADER,
Trader = trader,
MessageText = message,
Items = new ()
};
// Add items to message
if (items?.Count > 0)
{
details.Items.AddRange(items);
details.ItemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800;
}
if (systemData is not null)
details.SystemData = systemData;
if (ragfair is not null)
details.RagfairDetails = ragfair;
SendMessageToPlayer(details);
}
/**
* Send a message from an NPC (e.g. prapor) to the player with or without items
* @param sessionId The session ID to send the message to
* @param trader The trader sending the message
* @param messageType What type the message will assume (e.g. QUEST_SUCCESS)
* @param messageLocaleId The localised text to send to player
* @param items Optional items to send to player
* @param maxStorageTimeSeconds Optional time to collect items before they expire
*/
public void SendLocalisedNpcMessageToPlayer(
string sessionId,
string trader,
MessageType messageType,
string messageLocaleId,
List<Item>? items,
long? maxStorageTimeSeconds,
SystemData? systemData,
MessageContentRagfair? ragfair
)
{
if (trader is null)
{
_logger.Error(_localisationService.GetText("mailsend-missing_trader", new
{
MessageType = messageType,
SessionId = sessionId,
}));
return;
}
SendMessageDetails details = new()
{
RecipientId = sessionId,
Sender = messageType,
DialogType = MessageType.NPC_TRADER,
Trader = trader,
TemplateId = messageLocaleId,
Items = new()
};
// add items to message
if (items?.Count > 0)
{
details.Items.AddRange(items);
details.ItemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800;
}
if (systemData is not null)
details.SystemData = systemData;
if (ragfair is not null)
details.RagfairDetails = ragfair;
SendMessageToPlayer(details);
}
/**
* Send a message from SYSTEM to the player with or without items
* @param sessionId The session ID to send the message to
* @param message The text to send to player
* @param items Optional items to send to player
* @param maxStorageTimeSeconds Optional time to collect items before they expire
*/
public void SendSystemMessageToPlayer(
string sessionId,
string message,
List<Item>? items,
long? maxStorageTimeSeconds,
List<ProfileChangeEvent>? profileChangeEvents)
{
SendMessageDetails details = new()
{
RecipientId = sessionId,
Sender = MessageType.SYSTEM_MESSAGE,
MessageText = message,
Items = new()
};
// add items to message
if (items?.Count > 0)
{
var rootItemParentId = _hashUtil.Generate();
details.Items.AddRange(_itemHelper.AdoptOrphanedItems(rootItemParentId, items));
details.ItemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800;
}
if ((profileChangeEvents?.Count ?? 0) > 0)
details.ProfileChangeEvents = profileChangeEvents;
SendMessageToPlayer(details);
}
public void SendLocalisedSystemMessageToPlayer(
string sessionId,
string messageLocaleId,
List<Item>? items,
List<ProfileChangeEvent>? profileChangeEvents,
long? maxStorageTimeSeconds
)
{
SendMessageDetails details = new()
{
RecipientId = sessionId,
Sender = MessageType.SYSTEM_MESSAGE,
TemplateId = messageLocaleId,
Items = new()
};
// add items to message
if (items?.Count > 0)
{
details.Items.AddRange(items);
details.ItemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800;
}
if ((profileChangeEvents?.Count ?? 0) > 0)
details.ProfileChangeEvents = profileChangeEvents;
SendMessageToPlayer(details);
}
/**
* Send a USER message to a player with or without items
* @param sessionId The session ID to send the message to
* @param senderId Who is sending the message
* @param message The text to send to player
* @param items Optional items to send to player
* @param maxStorageTimeSeconds Optional time to collect items before they expire
*/
public void SendUserMessageToPlayer(
string sessionId,
UserDialogInfo senderDetails,
string message,
List<Item>? items,
long? maxStorageTimeSeconds
)
{
SendMessageDetails details = new()
{
RecipientId = sessionId,
Sender = MessageType.USER_MESSAGE,
SenderDetails = senderDetails,
MessageText = message,
Items = new ()
};
// add items to message
if (items?.Count > 0)
{
details.Items.AddRange(items);
details.ItemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800;
}
SendMessageToPlayer(details);
}
/**
* Large function to send messages to players from a variety of sources (SYSTEM/NPC/USER)
* Helper functions in this class are available to simplify common actions
* @param messageDetails Details needed to send a message to the player
*/
public void SendMessageToPlayer(SendMessageDetails messageDetails)
{
// Get dialog, create if doesn't exist
var senderDialog = GetDialog(messageDetails);
// Flag dialog as containing a new message to player
senderDialog.New++;
// Craft message
var message = CreateDialogMessage(senderDialog.Id, messageDetails);
// Create items array
// Generate item stash if we have rewards.
var itemsToSendToPlayer = ProcessItemsBeforeAddingToMail(senderDialog.Type, messageDetails);
// If there's items to send to player, flag dialog as containing attachments
if ((itemsToSendToPlayer.Data?.Count ?? 0) > 0)
{
senderDialog.AttachmentsNew += 1;
}
// Store reward items inside message and set appropriate flags inside message
AddRewardItemsToMessage(message, itemsToSendToPlayer, messageDetails.ItemsMaxStorageLifetimeSeconds);
if (messageDetails.ProfileChangeEvents is not null)
message.ProfileChangeEvents = messageDetails.ProfileChangeEvents;
// Add message to dialog
senderDialog.Messages.Add(message);
// TODO: clean up old code here
// Offer Sold notifications are now separate from the main notification
if (
_messageTypes.Contains(senderDialog.Type ?? MessageType.SYSTEM_MESSAGE) &&
messageDetails?.RagfairDetails is not null
)
{
var offerSoldMessage = _notifierHelper.CreateRagfairOfferSoldNotification(
message,
messageDetails.RagfairDetails
);
_notificationSendHelper.SendMessage(messageDetails.RecipientId, offerSoldMessage);
message.MessageType = MessageType.MESSAGE_WITH_ITEMS; // Should prevent getting the same notification popup twice
}
// Send message off to player so they get it in client
var notificationMessage = _notifierHelper.CreateNewMessageNotification(message);
_notificationSendHelper.SendMessage(messageDetails.RecipientId, notificationMessage);
}
public void SendPlayerMessageToNpc(string sessionId, string targetNpcId, string message)
{
var playerProfile = _saveServer.GetProfile(sessionId);
var dialogWithNpc = playerProfile.DialogueRecords[targetNpcId];
if (dialogWithNpc is null)
{
_logger.Error(_localisationService.GetText("mailsend-missing_npc_dialog", targetNpcId));
return;
}
dialogWithNpc.Messages.Add(new()
{
Id = _hashUtil.Generate(),
DateTime = _timeUtil.GetTimeStamp(),
HasRewards = false,
UserId = playerProfile.CharacterData.PmcData.Id,
MessageType = MessageType.USER_MESSAGE,
RewardCollected = false,
Text = message
});
}
private Message CreateDialogMessage(string dialogId, SendMessageDetails messageDetails)
{
Message message = new()
{
Id = _hashUtil.Generate(),
UserId = dialogId,
MessageType = messageDetails.DialogType,
DateTime = _timeUtil.GetTimeStamp(),
Text = (messageDetails.TemplateId is not null) ? "" : messageDetails.MessageText,
TemplateId = messageDetails.TemplateId,
HasRewards = false,
RewardCollected = false,
SystemData = messageDetails.SystemData is not null ? messageDetails.SystemData : null,
ProfileChangeEvents = (messageDetails.ProfileChangeEvents?.Count == 0) ? messageDetails.ProfileChangeEvents : null
};
// Clean up empty system data
// if (message.SystemData is null) {
// delete message.SystemData;
// }
// Clean up empty template id
// if (message.TemplateId is null)
// delete message.templateId;
return message;
}
/**
* Add items to message and adjust various properties to reflect the items being added
* @param message Message to add items to
* @param itemsToSendToPlayer Items to add to message
* @param maxStorageTimeSeconds total time items are stored in mail before being deleted
*/
private void AddRewardItemsToMessage(Message message, MessageItems? itemsToSendToPlayer, long? maxStorageTimeSeconds)
{
if ((itemsToSendToPlayer?.Data?.Count ?? 0) > 0)
{
message.Items = itemsToSendToPlayer;
message.HasRewards = true;
message.MaxStorageTime = maxStorageTimeSeconds;
message.RewardCollected = false;
}
}
private MessageItems ProcessItemsBeforeAddingToMail(MessageType? dialogType, SendMessageDetails messageDetails)
{
var items = _databaseService.GetItems();
MessageItems itemsToSendToPlayer = new();
if ((messageDetails.Items?.Count ?? 0) > 0)
{
// Find base item that should be the 'primary' + have its parent id be used as the dialogs 'stash' value
var parentItem = GetBaseItemFromRewards(messageDetails.Items);
if (parentItem is null)
{
_localisationService.GetText("mailsend-missing_parent", new
{
TraderId = messageDetails.Trader,
Sender = messageDetails.Sender,
});
return itemsToSendToPlayer;
}
// No parent id, generate random id and add (doesn't need to be actual parentId from db, only unique)
if (parentItem?.ParentId is null)
parentItem.ParentId = _hashUtil.Generate();
itemsToSendToPlayer = new()
{
Stash = parentItem.ParentId, Data = new()
};
// Ensure Ids are unique and cont collide with items in player inventory later
messageDetails.Items = _itemHelper.ReplaceIDs(messageDetails.Items);
foreach (var reward in messageDetails.Items)
{
// Ensure item exists in items db
var itemTemplate = items[reward.Template];
if (itemTemplate is null)
{
_logger.Error(_localisationService.GetText("dialog-missing_item_template", new
{
Tpl = reward.Template,
Type = dialogType,
}));
continue;
}
// Ensure every 'base/root' item has the same parentId + has a slotid of 'main'
if (!(reward.SlotId is not null) || reward.SlotId == "hideout" || reward.ParentId == parentItem.ParentId)
{
// Reward items NEED a parent id + slotid
reward.ParentId = parentItem.ParentId;
reward.SlotId = "main";
}
// Boxes can contain sub-items
if (_itemHelper.IsOfBaseclass(itemTemplate.Id, [BaseClasses.AMMO_BOX]))
{
var boxAndCartridges = new List<Item>();
boxAndCartridges.Add(reward);
_itemHelper.AddCartridgesToAmmoBox(boxAndCartridges, itemTemplate);
// Push box + cartridge children into array
itemsToSendToPlayer.Data.AddRange(boxAndCartridges);
}
else
{
if (itemTemplate.Properties.StackSlots is not null)
_logger.Error(_localisationService.GetText("mail-unable_to_give_gift_not_handled", itemTemplate.Id));
// Item is sanitised and ready to be pushed into holding array
itemsToSendToPlayer.Data.Add(reward);
}
}
// Remove empty data property if no rewards
// if (itemsToSendToPlayer.Data.Count == 0)
// delete itemsToSendToPlayer.data;
}
return itemsToSendToPlayer;
}
/**
* Try to find the most correct item to be the 'primary' item in a reward mail
* @param items Possible items to choose from
* @returns Chosen 'primary' item
*/
private Item GetBaseItemFromRewards(List<Item>? items)
{
// Only one item in reward, return it
if (items?.Count == 1)
return items[0];
// Find first item with slotId that indicates its a 'base' item
var item = items.FirstOrDefault(x => _slotNames.Contains(x.SlotId ?? ""));
if (item is not null)
return item;
// Not a singlular item + no items have a hideout/main slotid
// Look for first item without parent id
item = items.FirstOrDefault(x => x.ParentId is null);
if (item is not null)
return item;
// Just return first item in array
return items[0];
}
/**
* Get a dialog with a specified entity (user/trader)
* Create and store empty dialog if none exists in profile
* @param messageDetails Data on what message should do
* @returns Relevant Dialogue
*/
private Dialogue GetDialog(SendMessageDetails messageDetails)
{
var dialogsInProfile = _dialogueHelper.GetDialogsForProfile(messageDetails.RecipientId);
var senderId = GetMessageSenderIdByType(messageDetails);
if (senderId is null)
throw new Exception(_localisationService.GetText("mail-unable_to_find_message_sender_by_id", messageDetails.Sender));
// Does dialog exist
var senderDialog = dialogsInProfile.FirstOrDefault(x => x.Key == senderId).Value;
if (senderDialog is null)
{
// create if doesnt
dialogsInProfile[senderId] = senderDialog = new Dialogue()
{
Id = senderId,
Type = (messageDetails.DialogType is not null) ? messageDetails.DialogType : messageDetails.Sender,
Messages = new(),
Pinned = false,
New = 0,
AttachmentsNew = 0
};
senderDialog = dialogsInProfile[senderId];
}
return senderDialog;
}
/**
* Get the appropriate sender id by the sender enum type
* @param messageDetails
* @returns gets an id of the individual sending it
*/
private string? GetMessageSenderIdByType(SendMessageDetails messageDetails)
{
if (messageDetails.Sender == MessageType.SYSTEM_MESSAGE)
return _systemSenderId;
if (messageDetails.Sender == MessageType.NPC_TRADER || messageDetails.DialogType == MessageType.NPC_TRADER)
return (messageDetails.Trader is not null) ? _traderHelper.GetValidTraderIdByEnumValue(messageDetails.Trader) : null;
if (messageDetails.Sender == MessageType.USER_MESSAGE)
return messageDetails.SenderDetails?.Id;
if (messageDetails.SenderDetails?.Id is not null)
return messageDetails.SenderDetails.Id;
if (messageDetails.Trader is not null)
return _traderHelper.GetValidTraderIdByEnumValue(messageDetails.Trader);
_logger.Warning($"Unable to handle message of type: {messageDetails.Sender}");
return null;
}
}
+47 -192
View File
@@ -1,16 +1,29 @@
using Core.Annotations;
using System.Text.RegularExpressions;
using Core.Annotations;
using Core.Models.Eft.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Inventory;
using Core.Models.Eft.Profile;
using Core.Models.Enums;
using Core.Models.Spt.Dialog;
using ILogger = Core.Models.Utils.ILogger;
using MapMarker = Core.Models.Eft.Common.Tables.MapMarker;
namespace Core.Services;
[Injectable(InjectionType.Singleton)]
[Injectable]
public class MapMarkerService
{
private readonly ILogger _logger;
public MapMarkerService
(
ILogger logger
)
{
_logger = logger;
}
/// <summary>
/// Add note to a map item in player inventory
/// </summary>
@@ -19,7 +32,17 @@ public class MapMarkerService
/// <returns>Item</returns>
public Item CreateMarkerOnMap(PmcData pmcData, InventoryCreateMarkerRequestData request)
{
throw new NotImplementedException();
// Get map from inventory
var mapItem = pmcData?.Inventory?.Items?.FirstOrDefault((i) => i?.Id == request?.Item);
// add marker to map item
mapItem.Upd.Map = mapItem?.Upd?.Map ?? new() { Markers = new() };
// Update request note with text, then add to maps upd
request.MapMarker.Note = SanitiseMapMarkerText(request.MapMarker.Note);
mapItem?.Upd?.Map?.Markers?.Add(request.MapMarker);
return mapItem;
}
/// <summary>
@@ -30,7 +53,16 @@ public class MapMarkerService
/// <returns>Item</returns>
public Item DeleteMarkerFromMap(PmcData pmcData, InventoryDeleteMarkerRequestData request)
{
throw new NotImplementedException();
// Get map from inventory
var mapItem = pmcData.Inventory.Items.FirstOrDefault((item) => item.Id == request.Item);
// remove marker
var markers = mapItem.Upd.Map.Markers.Where((marker) => {
return marker.X != request.X && marker.Y != request.Y;
}).ToList();
mapItem.Upd.Map.Markers = markers;
return mapItem;
}
/// <summary>
@@ -41,7 +73,16 @@ public class MapMarkerService
/// <returns>Item</returns>
public Item EditMarkerOnMap(PmcData pmcData, InventoryEditMarkerRequestData request)
{
throw new NotImplementedException();
// Get map from inventory
var mapItem = pmcData.Inventory.Items.FirstOrDefault((item) => item.Id == request.Item);
// edit marker
var indexOfExistingNote = mapItem.Upd.Map.Markers.IndexOf(request.MapMarker);
request.MapMarker.Note = SanitiseMapMarkerText(request.MapMarker.Note);
mapItem.Upd.Map.Markers.RemoveAt(indexOfExistingNote);
mapItem.Upd.Map.Markers.Add(request.MapMarker);
return mapItem;
}
/// <summary>
@@ -51,192 +92,6 @@ public class MapMarkerService
/// <returns>Sanitised map marker text</returns>
protected string SanitiseMapMarkerText(string mapNoteText)
{
throw new NotImplementedException();
}
/// <summary>
/// Send a message from an NPC (e.g. prapor) to the player with or without items using direct message text, do not look up any locale
/// </summary>
/// <param name="sessionId">The session ID to send the message to</param>
/// <param name="trader">The trader sending the message</param>
/// <param name="messageType">What type the message will assume (e.g. QUEST_SUCCESS)</param>
/// <param name="message">Text to send to the player</param>
/// <param name="items">Optional items to send to player</param>
/// <param name="maxStorageTimeSeconds">Optional time to collect items before they expire</param>
public void SendDirectNpcMessageToPlayer(
string sessionId,
object trader,
MessageType messageType,
string message,
List<Item> items = null,
int? maxStorageTimeSeconds = null,
SystemData systemData = null,
MessageContentRagfair ragfair = null)
{
throw new NotImplementedException();
}
/// <summary>
/// Send a message from an NPC (e.g. prapor) to the player with or without items
/// </summary>
/// <param name="sessionId">The session ID to send the message to</param>
/// <param name="trader">The trader sending the message</param>
/// <param name="messageType">What type the message will assume (e.g. QUEST_SUCCESS)</param>
/// <param name="messageLocaleId">The localised text to send to player</param>
/// <param name="items">Optional items to send to player</param>
/// <param name="maxStorageTimeSeconds">Optional time to collect items before they expire</param>
public void SendLocalisedNpcMessageToPlayer(
string sessionId,
object trader,
MessageType messageType,
string messageLocaleId,
List<Item> items = null,
int? maxStorageTimeSeconds = null,
SystemData systemData = null,
MessageContentRagfair ragfair = null)
{
throw new NotImplementedException();
}
/// <summary>
/// Send a message from SYSTEM to the player with or without items
/// </summary>
/// <param name="sessionId">The session ID to send the message to</param>
/// <param name="message">The text to send to player</param>
/// <param name="items">Optional items to send to player</param>
/// <param name="maxStorageTimeSeconds">Optional time to collect items before they expire</param>
public void SendSystemMessageToPlayer(
string sessionId,
string message,
List<Item> items = null,
int? maxStorageTimeSeconds = null,
List<ProfileChangeEvent> profileChangeEvents = null)
{
throw new NotImplementedException();
}
/// <summary>
/// Send a message from SYSTEM to the player with or without items with localised text
/// </summary>
/// <param name="sessionId">The session ID to send the message to</param>
/// <param name="messageLocaleId">Id of key from locale file to send to player</param>
/// <param name="items">Optional items to send to player</param>
/// <param name="maxStorageTimeSeconds">Optional time to collect items before they expire</param>
public void SendLocalisedSystemMessageToPlayer(
string sessionId,
string messageLocaleId,
List<Item> items = null,
List<ProfileChangeEvent> profileChangeEvents = null,
int? maxStorageTimeSeconds = null)
{
throw new NotImplementedException();
}
/// <summary>
/// Send a USER message to a player with or without items
/// </summary>
/// <param name="sessionId">The session ID to send the message to</param>
/// <param name="senderDetails">Who is sending the message</param>
/// <param name="message">The text to send to player</param>
/// <param name="items">Optional items to send to player</param>
/// <param name="maxStorageTimeSeconds">Optional time to collect items before they expire</param>
public void SendUserMessageToPlayer(
string sessionId,
UserDialogInfo senderDetails,
string message,
List<Item> items = null,
int? maxStorageTimeSeconds = null)
{
throw new NotImplementedException();
}
/// <summary>
/// Large function to send messages to players from a variety of sources (SYSTEM/NPC/USER)
/// Helper functions in this class are available to simplify common actions
/// </summary>
/// <param name="messageDetails">Details needed to send a message to the player</param>
public void SendMessageToPlayer(SendMessageDetails messageDetails)
{
throw new NotImplementedException();
}
/// <summary>
/// Send a message from the player to an NPC
/// </summary>
/// <param name="sessionId">Player id</param>
/// <param name="targetNpcId">NPC message is sent to</param>
/// <param name="message">Text to send to NPC</param>
public void SendPlayerMessageToNpc(string sessionId, string targetNpcId, string message)
{
throw new NotImplementedException();
}
/// <summary>
/// Create a message for storage inside a dialog in the player profile
/// </summary>
/// <param name="senderDialog">Id of dialog that will hold the message</param>
/// <param name="messageDetails">Various details on what the message must contain/do</param>
/// <returns>Message</returns>
protected Message CreateDialogMessage(string dialogId, SendMessageDetails messageDetails)
{
throw new NotImplementedException();
}
/// <summary>
/// Add items to message and adjust various properties to reflect the items being added
/// </summary>
/// <param name="message">Message to add items to</param>
/// <param name="itemsToSendToPlayer">Items to add to message</param>
/// <param name="maxStorageTimeSeconds">total time items are stored in mail before being deleted</param>
protected void AddRewardItemsToMessage(
Message message,
List<MessageItems> itemsToSendToPlayer,
int? maxStorageTimeSeconds)
{
throw new NotImplementedException();
}
/// <summary>
/// perform various sanitising actions on the items before they're considered ready for insertion into message
/// </summary>
/// <param name="dialogType">The type of the dialog that will hold the reward items being processed</param>
/// <param name="messageDetails"></param>
/// <returns>Sanitised items</returns>
protected List<MessageItems> ProcessItemsBeforeAddingToMail(
MessageType dialogType,
SendMessageDetails messageDetails)
{
throw new NotImplementedException();
}
/// <summary>
/// Try to find the most correct item to be the 'primary' item in a reward mail
/// </summary>
/// <param name="items">Possible items to choose from</param>
/// <returns>Chosen 'primary' item</returns>
protected Item GetBaseItemFromRewards(List<Item> items)
{
throw new NotImplementedException();
}
/// <summary>
/// Get a dialog with a specified entity (user/trader)
/// Create and store empty dialog if none exists in profile
/// </summary>
/// <param name="messageDetails">Data on what message should do</param>
/// <returns>Relevant Dialogue</returns>
protected Dialogue GetDialog(SendMessageDetails messageDetails)
{
throw new NotImplementedException();
}
/// <summary>
/// Get the appropriate sender id by the sender enum type
/// </summary>
/// <param name="messageDetails"></param>
/// <returns>gets an id of the individual sending it</returns>
protected string GetMessageSenderIdByType(SendMessageDetails messageDetails)
{
throw new NotImplementedException();
return Regex.Replace(mapNoteText, @"[^\p{L}\d\s]", "", RegexOptions.CultureInvariant);
}
}
@@ -0,0 +1,55 @@
using Core.Annotations;
using Core.Models.Eft.Common.Tables;
using Core.Models.Spt.Bots;
using System.Text.RegularExpressions;
using ILogger = Core.Models.Utils.ILogger;
namespace Core.Services
{
[Injectable(InjectionType.Singleton)]
public class MatchBotDetailsCacheService
{
private readonly ILogger _logger;
private readonly LocalisationService _localisationService;
private readonly Dictionary<string, BotBase> _botDetailsCache;
public MatchBotDetailsCacheService(
ILogger logger,
LocalisationService localisationService)
{
_logger = logger;
_localisationService = localisationService;
_botDetailsCache = new();
}
public void CacheBot(BotBase botToCache)
{
if (botToCache.Info.Nickname is null)
{
_logger.Warning($"Unable to cache: { botToCache.Info.Settings.Role} bot with id: ${ botToCache.Id} as it lacks a nickname");
return;
}
var key = $"{botToCache.Info.Nickname.Trim()}{botToCache.Info.Side}";
_botDetailsCache.TryAdd(key, botToCache);
}
public void ClearCache()
{
_botDetailsCache.Clear();
}
public BotBase GetBotByNameAndSide(string botName, string botSide)
{
var botInCache = _botDetailsCache.GetValueOrDefault($"{botName}{botSide}`", null);
if (botInCache is null)
{
_logger.Warning($"Bot not found in match bot cache: {botName.ToLower()} { botSide}");
}
return botInCache;
}
}
}
+491 -50
View File
@@ -1,40 +1,122 @@
using Core.Annotations;
using Core.Annotations;
using Core.Helpers;
using Core.Models.Eft.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Enums;
using Core.Models.Spt.Config;
using Core.Servers;
using ILogger = Core.Models.Utils.ILogger;
namespace Core.Services;
[Injectable(InjectionType.Singleton)]
public class SeasonalEventService
{
private readonly ILogger _logger;
private readonly DatabaseService _databaseService;
//private readonly DatabaseImporter _databaseImporter;
private readonly GiftService _giftService;
private readonly LocalisationService _localisationService;
private readonly BotHelper _botHelper;
private readonly ProfileHelper _profileHelper;
private readonly ConfigServer _configServer;
private bool _christmasEventActive = false;
private bool _halloweenEventActive = false;
private readonly SeasonalEventConfig _seasonalEventConfig;
private readonly QuestConfig _questConfig;
private readonly HttpConfig _httpConfig;
private readonly WeatherConfig _weatherConfig;
private readonly LocationConfig _locationConfig;
private List<SeasonalEvent> _currentlyActiveEvents = [];
private readonly IReadOnlyList<string> _christmasEventItems =
[
ItemTpl.FACECOVER_FAKE_WHITE_BEARD,
ItemTpl.BARTER_CHRISTMAS_TREE_ORNAMENT_RED,
ItemTpl.BARTER_CHRISTMAS_TREE_ORNAMENT_VIOLET,
ItemTpl.BARTER_CHRISTMAS_TREE_ORNAMENT_SILVER,
ItemTpl.HEADWEAR_DED_MOROZ_HAT,
ItemTpl.HEADWEAR_SANTA_HAT,
ItemTpl.BACKPACK_SANTAS_BAG,
ItemTpl.RANDOMLOOTCONTAINER_NEW_YEAR_GIFT_BIG,
ItemTpl.RANDOMLOOTCONTAINER_NEW_YEAR_GIFT_MEDIUM,
ItemTpl.RANDOMLOOTCONTAINER_NEW_YEAR_GIFT_SMALL
];
private readonly IReadOnlyList<string> _halloweenEventItems =
[
ItemTpl.FACECOVER_SPOOKY_SKULL_MASK,
ItemTpl.RANDOMLOOTCONTAINER_PUMPKIN_RAND_LOOT_CONTAINER,
ItemTpl.HEADWEAR_JACKOLANTERN_TACTICAL_PUMPKIN_HELMET,
ItemTpl.FACECOVER_FACELESS_MASK,
ItemTpl.FACECOVER_JASON_MASK,
ItemTpl.FACECOVER_MISHA_MAYOROV_MASK,
ItemTpl.FACECOVER_SLENDER_MASK,
ItemTpl.FACECOVER_GHOUL_MASK,
ItemTpl.FACECOVER_HOCKEY_PLAYER_MASK_CAPTAIN,
ItemTpl.FACECOVER_HOCKEY_PLAYER_MASK_BRAWLER,
ItemTpl.FACECOVER_HOCKEY_PLAYER_MASK_QUIET
];
public SeasonalEventService
(
ILogger logger,
DatabaseService databaseService,
//DatabaseImporter databaseImporter,
GiftService giftService,
LocalisationService localisationService,
BotHelper botHelper,
ProfileHelper profileHelper,
ConfigServer configServer
)
{
_logger = logger;
_databaseService = databaseService;
//_databaseImporter = databaseImporter;
_giftService = giftService;
_localisationService = localisationService;
_botHelper = botHelper;
_profileHelper = profileHelper;
_configServer = configServer;
_seasonalEventConfig = _configServer.GetConfig<SeasonalEventConfig>(ConfigTypes.SEASONAL_EVENT);
_questConfig = _configServer.GetConfig<QuestConfig>(ConfigTypes.QUEST);
_httpConfig = _configServer.GetConfig<HttpConfig>(ConfigTypes.HTTP);
_weatherConfig = _configServer.GetConfig<WeatherConfig>(ConfigTypes.WEATHER);
_locationConfig = _configServer.GetConfig<LocationConfig>(ConfigTypes.LOCATION);
}
/// <summary>
/// Get an array of christmas items found in bots inventories as loot
/// </summary>
/// <returns>array</returns>
public string[] GetChristmasEventItems()
public IEnumerable<string> GetChristmasEventItems()
{
throw new NotImplementedException();
return _christmasEventItems;
}
/// <summary>
/// Get an array of halloween items found in bots inventories as loot
/// </summary>
/// <returns>array</returns>
public string[] GetHalloweenEventItems()
public IEnumerable<string> GetHalloweenEventItems()
{
throw new NotImplementedException();
return _halloweenEventItems;
}
public bool ItemIsChristmasRelated(string itemTpl)
{
throw new NotImplementedException();
return _christmasEventItems.Contains(itemTpl);
}
public bool ItemIsHalloweenRelated(string itemTpl)
{
throw new NotImplementedException();
return _halloweenEventItems.Contains(itemTpl);
}
/// <summary>
@@ -44,7 +126,7 @@ public class SeasonalEventService
/// <returns></returns>
public bool ItemIsSeasonalRelated(string itemTpl)
{
throw new NotImplementedException();
return _christmasEventItems.Contains(itemTpl) || _halloweenEventItems.Contains(itemTpl);
}
/// <summary>
@@ -53,7 +135,7 @@ public class SeasonalEventService
/// <returns>Array of active events</returns>
public List<SeasonalEvent> GetActiveEvents()
{
throw new NotImplementedException();
return _currentlyActiveEvents;
}
/// <summary>
@@ -62,9 +144,20 @@ public class SeasonalEventService
/// or, if halloween and christmas are inactive, return both sets of items
/// </summary>
/// <returns>array of tpl strings</returns>
public string[] GetInactiveSeasonalEventItems()
public List<string> GetInactiveSeasonalEventItems()
{
throw new NotImplementedException();
var items = new List<string>();
if (!ChristmasEventEnabled())
{
items.AddRange(_christmasEventItems);
}
if (!HalloweenEventEnabled())
{
items.AddRange(_halloweenEventItems);
}
return items;
}
/// <summary>
@@ -73,7 +166,7 @@ public class SeasonalEventService
/// <returns>true if event is active</returns>
public bool SeasonalEventEnabled()
{
throw new NotImplementedException();
return _christmasEventActive || _halloweenEventActive;
}
/// <summary>
@@ -82,7 +175,7 @@ public class SeasonalEventService
/// <returns>true if active</returns>
public bool ChristmasEventEnabled()
{
throw new NotImplementedException();
return _christmasEventActive;
}
/// <summary>
@@ -91,7 +184,7 @@ public class SeasonalEventService
/// <returns>true if active</returns>
public bool HalloweenEventEnabled()
{
throw new NotImplementedException();
return _halloweenEventActive;
}
/// <summary>
@@ -100,7 +193,7 @@ public class SeasonalEventService
/// <returns>true if seasonal events should be checked for</returns>
public bool IsAutomaticEventDetectionEnabled()
{
throw new NotImplementedException();
return _seasonalEventConfig.EnableSeasonalEventDetection;
}
/// <summary>
@@ -110,7 +203,7 @@ public class SeasonalEventService
/// <returns>bots with equipment changes</returns>
protected Dictionary<string, Dictionary<string, Dictionary<string, int>>> GetEventBotGear(SeasonalEventType eventType)
{
throw new NotImplementedException();
return _seasonalEventConfig.EventGear.GetValueOrDefault(eventType, null);
}
/// <summary>
@@ -120,7 +213,7 @@ public class SeasonalEventService
/// <returns>bots with loot changes</returns>
protected Dictionary<string, Dictionary<string, Dictionary<string, int>>> GetEventBotLoot(SeasonalEventType eventType)
{
throw new NotImplementedException();
return _seasonalEventConfig.EventLoot.GetValueOrDefault(eventType, null);
}
/// <summary>
@@ -129,7 +222,7 @@ public class SeasonalEventService
/// <returns>Record with event name + start/end date</returns>
public List<SeasonalEvent> GetEventDetails()
{
throw new NotImplementedException();
return _seasonalEventConfig.Events;
}
/// <summary>
@@ -140,7 +233,13 @@ public class SeasonalEventService
/// <returns>true if related</returns>
public bool IsQuestRelatedToEvent(string questId, SeasonalEventType eventType)
{
throw new NotImplementedException();
var eventQuestData = _questConfig.EventQuests.GetValueOrDefault(questId, null);
if (eventQuestData?.Season == eventType)
{
return true;
}
return false;
}
/// <summary>
@@ -148,7 +247,14 @@ public class SeasonalEventService
/// </summary>
public void EnableSeasonalEvents()
{
throw new NotImplementedException();
if (_currentlyActiveEvents.Count > 0)
{
var globalConfig = _databaseService.GetGlobals().Configuration;
foreach (var activeEvent in _currentlyActiveEvents)
{
UpdateGlobalEvents(globalConfig, activeEvent);
}
}
}
/// <summary>
@@ -158,7 +264,17 @@ public class SeasonalEventService
/// <returns>True if event was successfully force enabled</returns>
public bool ForceSeasonalEvent(SeasonalEventType eventType)
{
throw new NotImplementedException();
var globalConfig = _databaseService.GetGlobals().Configuration;
var seasonEvent = _seasonalEventConfig.Events.FirstOrDefault((e) => e.Type == eventType);
if (seasonEvent is null)
{
_logger.Warning($"Unable to force event: {eventType} as it cannot be found in events config");
return false;
}
UpdateGlobalEvents(globalConfig, seasonEvent);
return true;
}
/// <summary>
@@ -166,7 +282,25 @@ public class SeasonalEventService
/// </summary>
public void CacheActiveEvents()
{
throw new NotImplementedException();
var currentDate = DateTimeOffset.UtcNow.DateTime;
var seasonalEvents = GetEventDetails();
// reset existing data
_currentlyActiveEvents = new();
// Add active events to array
foreach (var events in seasonalEvents)
{
if (!events.Enabled)
{
continue;
}
if (DateIsBetweenTwoDates(currentDate, events.StartMonth, events.StartDay, events.EndMonth, events.EndDay))
{
_currentlyActiveEvents.Add(events);
}
}
}
/// <summary>
@@ -175,7 +309,30 @@ public class SeasonalEventService
/// <returns>Season enum value</returns>
public Season GetActiveWeatherSeason()
{
throw new NotImplementedException();
if (_weatherConfig.OverrideSeason.HasValue)
{
return _weatherConfig.OverrideSeason.Value;
}
var currentDate = new DateTime();
foreach (var seasonRange in _weatherConfig.SeasonDates)
{
if (
DateIsBetweenTwoDates(
currentDate,
seasonRange.StartMonth,
seasonRange.StartDay,
seasonRange.EndMonth,
seasonRange.EndDay)
)
{
return seasonRange.SeasonType;
}
}
_logger.Warning(_localisationService.GetText("season-no_matching_season_found_for_date"));
return Season.SUMMER;
}
/// <summary>
@@ -189,9 +346,12 @@ public class SeasonalEventService
/// <param name="endMonth">Upper bound for month</param>
/// <param name="endDay">Upper bound for day</param>
/// <returns>True when inside date range</returns>
protected bool DateIsBetweenTwoDates(DateTime dateToCheck, int startMonth, int startDay, int endMonth, int endDay)
private bool DateIsBetweenTwoDates(DateTime dateToCheck, int startMonth, int startDay, int endMonth, int endDay)
{
throw new NotImplementedException();
var eventStartDate = new DateTime(dateToCheck.Year, startMonth, startDay);
var eventEndDate = new DateTime(dateToCheck.Year, endMonth, endDay, 23, 59, 0);
return dateToCheck >= eventStartDate && dateToCheck <= eventEndDate;
}
/// <summary>
@@ -199,9 +359,78 @@ public class SeasonalEventService
/// </summary>
/// <param name="botInventory">Bots inventory to iterate over</param>
/// <param name="botRole">the role of the bot being processed</param>
public void RemoveChristmasItemsFromBotInventory(BotBaseInventory botInventory, string botRole)
public void RemoveChristmasItemsFromBotInventory(BotTypeInventory botInventory, string botRole)
{
throw new NotImplementedException();
var christmasItems = GetChristmasEventItems();
List<EquipmentSlots> equipmentSlotsToFilter = [EquipmentSlots.FaceCover, EquipmentSlots.Headwear, EquipmentSlots.Backpack, EquipmentSlots.TacticalVest];
List<string> lootContainersToFilter = ["Backpack", "Pockets", "TacticalVest"];
// Remove christmas related equipment
foreach (var equipmentSlotKey in equipmentSlotsToFilter)
{
if (botInventory.Equipment[equipmentSlotKey] is null)
{
_logger.Warning(
_localisationService.GetText("seasonal-missing_equipment_slot_on_bot", new
{
EquipmentSlot = equipmentSlotKey,
BotRole = botRole,
})
);
}
Dictionary<string, double> equipment = botInventory.Equipment[equipmentSlotKey];
botInventory.Equipment[equipmentSlotKey] = equipment.Where(i => !_christmasEventItems.Contains(i.Key)).ToDictionary();
}
// Remove christmas related loot from loot containers
var props = botInventory.Items.GetType().GetProperties();
foreach (var lootContainerKey in lootContainersToFilter)
{
var prop = (Dictionary<string, double>?)props.FirstOrDefault(p => p.Name.ToLower() == lootContainerKey.ToLower()).GetValue(botInventory.Items);
if (prop is null)
{
_logger.Warning(
_localisationService.GetText("seasonal-missing_loot_container_slot_on_bot", new
{
LootContainer = lootContainerKey,
BotRole = botRole,
})
);
}
List<string> tplsToRemove = [];
foreach (var tplKey in prop)
{
if (christmasItems.Contains(tplKey.Key))
{
tplsToRemove.Add(tplKey.Key);
}
}
foreach (var tplToRemove in tplsToRemove)
{
prop.Remove(tplToRemove);
}
// Get non-christmas items
var nonChristmasTpls = prop.Where(tpl => !christmasItems.Contains(tpl.Key));
if (nonChristmasTpls.Count() == 0)
{
continue;
}
Dictionary<string, double> intermediaryDict = new();
foreach (var tpl in nonChristmasTpls)
{
intermediaryDict[tpl.Key] = prop[tpl.Key];
}
// Replace the original containerItems with the updated one
prop = intermediaryDict;
}
}
/// <summary>
@@ -209,44 +438,229 @@ public class SeasonalEventService
/// </summary>
/// <param name="globalConfig">globals.json</param>
/// <param name="event">Name of the event to enable. e.g. Christmas</param>
protected void UpdateGlobalEvents(Config globalConfig, SeasonalEvent eventType)
private void UpdateGlobalEvents(Config globalConfig, SeasonalEvent eventType)
{
throw new NotImplementedException();
_logger.Success(_localisationService.GetText("season-event_is_active", eventType.Type));
_christmasEventActive = false;
_halloweenEventActive = false;
switch (eventType.Type)
{
case SeasonalEventType.Halloween:
ApplyHalloweenEvent(eventType, globalConfig);
break;
case SeasonalEventType.Christmas:
ApplyChristmasEvent(eventType, globalConfig);
break;
case SeasonalEventType.NewYears:
ApplyNewYearsEvent(eventType, globalConfig);
break;
case SeasonalEventType.AprilFools:
AddGifterBotToMaps();
AddLootItemsToGifterDropItemsList();
AddEventGearToBots(SeasonalEventType.Halloween);
AddEventGearToBots(SeasonalEventType.Christmas);
AddEventLootToBots(SeasonalEventType.Christmas);
AddEventBossesToMaps(SeasonalEventType.Halloween.ToString());
EnableHalloweenSummonEvent();
AddPumpkinsToScavBackpacks();
RenameBitcoin();
EnableSnow();
break;
default:
// Likely a mod event
HandleModEvent(eventType, globalConfig);
break;
}
}
protected void ApplyHalloweenEvent(SeasonalEvent eventType, Config globalConfig)
private void ApplyHalloweenEvent(SeasonalEvent eventType, Config globalConfig)
{
throw new NotImplementedException();
_halloweenEventActive = true;
globalConfig.EventType = globalConfig.EventType.Where((x) => x != "None").ToList();
globalConfig.EventType.Add("Halloween");
globalConfig.EventType.Add("HalloweenIllumination");
globalConfig.Health.ProfileHealthSettings.DefaultStimulatorBuff = "Buffs_Halloween";
AddEventGearToBots(eventType.Type);
AdjustZryachiyMeleeChance();
if (eventType.Settings?.EnableSummoning ?? false)
{
EnableHalloweenSummonEvent();
AddEventBossesToMaps("halloweensummon");
}
if (eventType.Settings?.ZombieSettings?.Enabled ?? false)
{
ConfigureZombies(eventType.Settings.ZombieSettings);
}
if (eventType.Settings?.RemoveEntryRequirement is not null)
{
RemoveEntryRequirement(eventType.Settings.RemoveEntryRequirement);
}
if (eventType.Settings?.ReplaceBotHostility ?? false)
{
ReplaceBotHostility(_seasonalEventConfig.HostilitySettingsForEvent.FirstOrDefault(x => x.Key == "zombies").Value);
}
if (eventType.Settings?.AdjustBotAppearances ?? false)
{
AdjustBotAppearanceValues(eventType.Type);
}
AddPumpkinsToScavBackpacks();
AdjustTraderIcons(eventType.Type);
}
protected void ApplyChristmasEvent(SeasonalEvent eventType, Config globalConfig)
private void ApplyChristmasEvent(SeasonalEvent eventType, Config globalConfig)
{
throw new NotImplementedException();
_christmasEventActive = true;
if (eventType.Settings?.EnableChristmasHideout ?? false)
{
globalConfig.EventType = globalConfig.EventType.Where((x) => x != "None").ToList();
globalConfig.EventType.Add("Christmas");
}
AddEventGearToBots(eventType.Type);
AddEventLootToBots(eventType.Type);
if (eventType.Settings?.EnableSanta ?? false)
{
AddGifterBotToMaps();
AddLootItemsToGifterDropItemsList();
}
EnableDancingTree();
if (eventType.Settings?.AdjustBotAppearances ?? false)
{
AdjustBotAppearanceValues(eventType.Type);
}
}
protected void ApplyNewYearsEvent(SeasonalEvent eventType, Config globalConfig)
private void ApplyNewYearsEvent(SeasonalEvent eventType, Config globalConfig)
{
throw new NotImplementedException();
_christmasEventActive = true;
if (eventType.Settings?.EnableChristmasHideout ?? false)
{
globalConfig.EventType = globalConfig.EventType.Where((x) => x != "None").ToList();
globalConfig.EventType.Add("Christmas");
}
AddEventGearToBots(SeasonalEventType.Christmas);
AddEventLootToBots(SeasonalEventType.Christmas);
if (eventType.Settings?.EnableSanta ?? false)
{
AddGifterBotToMaps();
AddLootItemsToGifterDropItemsList();
}
EnableDancingTree();
if (eventType.Settings?.AdjustBotAppearances ?? false)
{
AdjustBotAppearanceValues(SeasonalEventType.Christmas);
}
}
protected void AdjustBotAppearanceValues(SeasonalEventType season)
private void AdjustBotAppearanceValues(SeasonalEventType season)
{
throw new NotImplementedException();
var adjustments = _seasonalEventConfig.BotAppearanceChanges[season];
if (adjustments is null)
{
return;
}
foreach (var botTypeKey in adjustments)
{
var botDb = _databaseService.GetBots().Types[botTypeKey.Key];
if (botDb is null)
{
continue;
}
var botAppearanceAdjustments = botTypeKey.Value;
foreach (var appearanceKey in botAppearanceAdjustments)
{
var weightAdjustments = appearanceKey.Value;
var props = botDb.BotAppearance.GetType().GetProperties();
foreach (var itemKey in weightAdjustments)
{
var prop = props.FirstOrDefault(x => x.Name.ToLower() == appearanceKey.Key.ToLower());
var propValue = (Dictionary<string, int>)prop.GetValue(botDb.BotAppearance);
propValue[itemKey.Key] = weightAdjustments[itemKey.Key];
prop.SetValue(botDb.BotAppearance, propValue);
}
}
}
}
protected void ReplaceBotHostility(Dictionary<string, AdditionalHostilitySettings[]> hostilitySettings)
private void ReplaceBotHostility(Dictionary<string, List<AdditionalHostilitySettings>> hostilitySettings)
{
throw new NotImplementedException();
var locations = _databaseService.GetLocations();
var ignoreList = _locationConfig.NonMaps;
var useDefault = hostilitySettings is null;
var props = locations.GetType().GetProperties();
foreach (var locationProp in props)
{
if (ignoreList.Contains(locationProp.Name))
{
continue;
}
Location location = (Location)locationProp.GetValue(locations);
if (location?.Base?.BotLocationModifier?.AdditionalHostilitySettings is null)
{
continue;
}
List<AdditionalHostilitySettings> newHostilitySettings = useDefault ? new() : hostilitySettings[locationProp.Name];
if (newHostilitySettings is null)
{
continue;
}
location.Base.BotLocationModifier.AdditionalHostilitySettings = new();
}
}
protected void RemoveEntryRequirement(List<string> locationIds)
private void RemoveEntryRequirement(List<string> locationIds)
{
throw new NotImplementedException();
foreach (var locationId in locationIds)
{
var location = _databaseService.GetLocation(locationId);
location.Base.AccessKeys = [];
location.Base.AccessKeysPvE = [];
}
}
public void GivePlayerSeasonalGifts(string sessionId)
{
throw new NotImplementedException();
if (_currentlyActiveEvents is null)
{
return;
}
foreach (var seasonEvent in _currentlyActiveEvents)
{
switch (seasonEvent.Type)
{
case SeasonalEventType.Christmas:
GiveGift(sessionId, "Christmas2022");
break;
case SeasonalEventType.NewYears:
GiveGift(sessionId, "NewYear2023");
GiveGift(sessionId, "NewYear2024");
break;
}
}
}
/// <summary>
@@ -254,7 +668,21 @@ public class SeasonalEventService
/// </summary>
protected void AdjustZryachiyMeleeChance()
{
throw new NotImplementedException();
var zyrach = _databaseService.GetBots().Types.FirstOrDefault(x => x.Key.ToLower() == "bosszryachiy");
var value = new Dictionary<string, double>();
foreach (var chance in zyrach.Value.BotChances.EquipmentChances)
{
if (chance.Key.ToLower() == "Scabbard")
{
value.Add(chance.Key, 100);
continue;
}
value.Add(chance.Key, chance.Value);
}
zyrach.Value.BotChances.EquipmentChances = value;
}
/// <summary>
@@ -262,7 +690,7 @@ public class SeasonalEventService
/// </summary>
protected void EnableHalloweenSummonEvent()
{
throw new NotImplementedException();
_databaseService.GetGlobals().Configuration.EventSettings.EventActive = true;
}
protected void ConfigureZombies(ZombieSettings zombieSettings)
@@ -319,7 +747,12 @@ public class SeasonalEventService
/// </summary>
protected void AddLootItemsToGifterDropItemsList()
{
throw new NotImplementedException();
var gifterBot = _databaseService.GetBots().Types["gifter"];
var items = gifterBot.BotInventory.Items.Backpack.Keys.ToList();
gifterBot.BotDifficulty.Easy.Patrol["ITEMS_TO_DROP"] = items;
gifterBot.BotDifficulty.Normal.Patrol["ITEMS_TO_DROP"] = items;
gifterBot.BotDifficulty.Hard.Patrol["ITEMS_TO_DROP"] = items;
gifterBot.BotDifficulty.Impossible.Patrol["ITEMS_TO_DROP"] = items;
}
/// <summary>
@@ -345,12 +778,16 @@ public class SeasonalEventService
/// </summary>
protected void AddPumpkinsToScavBackpacks()
{
throw new NotImplementedException();
_databaseService.GetBots().Types["assault"].BotInventory.Items.Backpack[
ItemTpl.RANDOMLOOTCONTAINER_PUMPKIN_RAND_LOOT_CONTAINER
] = 400;
}
protected void RenameBitcoin()
{
throw new NotImplementedException();
var enLocale = _databaseService.GetLocales().Global["en"];
enLocale[$"{ItemTpl.BARTER_PHYSICAL_BITCOIN} Name"] = "Physical SPT Coin";
enLocale[$"{ItemTpl.BARTER_PHYSICAL_BITCOIN} ShortName"] = "0.2SPT";
}
/// <summary>
@@ -381,7 +818,11 @@ public class SeasonalEventService
/// <param name="giftKey">Key of gift to give</param>
protected void GiveGift(string playerId, string giftKey)
{
throw new NotImplementedException();
var gitftData = _giftService.GetGiftById(giftKey);
if (!_profileHelper.PlayerHasRecievedMaxNumberOfGift(playerId, giftKey, gitftData.MaxToSendPlayer ?? 5))
{
_giftService.SendGiftToPlayer(playerId, giftKey);
}
}
/// <summary>
@@ -391,7 +832,7 @@ public class SeasonalEventService
/// <returns>Bot role as string</returns>
public string GetBaseRoleForEventBot(string eventBotRole)
{
throw new NotImplementedException();
return _seasonalEventConfig.EventBotMapping.GetValueOrDefault(eventBotRole, null);
}
/// <summary>
@@ -399,6 +840,6 @@ public class SeasonalEventService
/// </summary>
public void EnableSnow()
{
throw new NotImplementedException();
_weatherConfig.OverrideSeason = Season.WINTER;
}
}
+14 -25
View File
@@ -1,4 +1,3 @@
using System.Diagnostics;
using Core.Annotations;
using Core.DI;
using Core.Models.Enums;
@@ -55,31 +54,21 @@ public class App
// execute onLoad callbacks
_logger.Info(_localisationService.GetText("executing_startup_callbacks"));
/*
_logger.Debug($"OS: {os.arch()} | {os.version()} | {process.platform}");
_logger.Debug($"CPU: {os.cpus()[0]?.model} cores: {os.cpus().length}");
_logger.Debug($"RAM: {(os.totalmem() / 1024 / 1024 / 1024).toFixed(2)}GB");
_logger.Debug($"PATH: {this.encodingUtil.toBase64(process.argv[0])}");
_logger.Debug($"PATH: {this.encodingUtil.toBase64(process.execPath)}");
_logger.Debug($"Server: {ProgramStatics.SPT_VERSION || this.coreConfig.sptVersion}");
_logger.Debug($"OS: {Environment.OSVersion.Version} | {Environment.OSVersion.Platform}");
//_logger.Debug($"CPU: {ApplicationId.ProcessorArchitecture} cores: {os.cpus().length}");
//_logger.Debug($"RAM: {(os.totalmem() / 1024 / 1024 / 1024).toFixed(2)}GB");
//_logger.Debug($"PATH: {this.encodingUtil.toBase64(process.argv[0])}");
//_logger.Debug($"PATH: {this.encodingUtil.toBase64(process.execPath)}");
//_logger.Debug($"Server: {ProgramStatics.SPT_VERSION || _coreConfig.SptVersion}");
const nodeVersion = process.version.replace(/^v/, "");
if (ProgramStatics.EXPECTED_NODE && nodeVersion !== ProgramStatics.EXPECTED_NODE) {
this.logger.error(
"Node version mismatch. Required: ${ProgramStatics.EXPECTED_NODE} | Current: ${nodeVersion}",
);
process.exit(1);
}
_logger.Debug("Node: ${nodeVersion}");
if (ProgramStatics.BUILD_TIME) {
_logger.Debug("Date: ${ProgramStatics.BUILD_TIME}");
}
if (ProgramStatics.COMMIT) {
_logger.Debug("Commit: ${ProgramStatics.COMMIT}");
}
*/
//if (ProgramStatics.BUILD_TIME) {
// _logger.Debug($"Date: {ProgramStatics.BUILD_TIME}");
//}
//if (ProgramStatics.COMMIT) {
// _logger.Debug($"Commit: {ProgramStatics.COMMIT}");
//}
foreach (var onLoad in _onLoad)
await onLoad.OnLoad();
+40 -24
View File
@@ -7,8 +7,8 @@ using Core.Models.Spt.Server;
using Core.Routers;
using Core.Servers;
using Core.Services;
using Core.Utils.Cloners;
using ILogger = Core.Models.Utils.ILogger;
using Path = System.IO.Path;
namespace Core.Utils;
@@ -18,19 +18,19 @@ public class DatabaseImporter : OnLoad
private object hashedFile;
private ValidationResult valid = ValidationResult.UNDEFINED;
private string filepath;
protected HttpConfig httpConfig;
private HttpConfig httpConfig;
protected readonly ILogger _logger;
protected readonly LocalisationService _localisationService;
private readonly ILogger _logger;
private readonly LocalisationService _localisationService;
protected readonly DatabaseServer _databaseServer;
private readonly DatabaseServer _databaseServer;
protected readonly ImageRouter _imageRouter;
protected readonly EncodingUtil _encodingUtil;
protected readonly HashUtil _hashUtil;
protected readonly ImporterUtil _importerUtil;
protected readonly ConfigServer _configServer;
protected readonly FileUtil _fileUtil;
private readonly ImageRouter _imageRouter;
private readonly EncodingUtil _encodingUtil;
private readonly HashUtil _hashUtil;
private readonly ImporterUtil _importerUtil;
private readonly ConfigServer _configServer;
private readonly FileUtil _fileUtil;
public DatabaseImporter(
ILogger logger,
@@ -95,29 +95,43 @@ public class DatabaseImporter : OnLoad
await HydrateDatabase(filepath);
var imageFilePath = $"{filepath}images/";
//var directories = this.vfs.getDirs(imageFilePath);
LoadImages(imageFilePath, _fileUtil.GetDirectories(imageFilePath), [
"/files/achievement/",
"/files/CONTENT/banners/",
"/files/handbook/",
"/files/Hideout/",
"/files/launcher/",
"/files/prestige/",
"/files/quest/icon/",
"/files/trader/avatar/",
]);
CreateRouteMapping(imageFilePath, "files");
}
private void CreateRouteMapping(string directory, string newBasePath)
{
var directoryContent = GetAllFilesInDirectory(directory);
foreach (var fileNameWithPath in directoryContent) {
var bsgPath = $"/{newBasePath}/{_fileUtil.StripExtension(fileNameWithPath)}";
var sptPath = $"{directory}{ fileNameWithPath}";
_imageRouter.AddRoute(bsgPath, sptPath);
}
}
private List<string> GetAllFilesInDirectory(string directoryPath)
{
List<string> result = [];
result.AddRange(Directory.GetFiles(directoryPath));
foreach (var subdirectory in Directory.GetDirectories(directoryPath))
{
result.AddRange(GetAllFilesInDirectory(subdirectory));
}
return result;
}
/**
* Read all json files in database folder and map into a json object
* @param filepath path to database folder
*/
protected async Task HydrateDatabase(string filepath)
protected async Task HydrateDatabase(string filePath)
{
_logger.Info(_localisationService.GetText("importing_database"));
var dataToImport = (DatabaseTables) await _importerUtil.LoadRecursiveAsync(
$"{filepath}database/",
$"{filePath}database/",
typeof(DatabaseTables),
OnReadValidate
);
@@ -183,9 +197,11 @@ public class DatabaseImporter : OnLoad
}
/**
* absolute dogshit, do not use
* Find and map files with image router inside a designated path
* @param filepath Path to find files in
*/
[Obsolete]
public void LoadImages(string filepath, string[] directories, List<string> routes)
{
for (var i = 0; i < directories.Length; i++)
+1 -1
View File
@@ -106,7 +106,7 @@ public class HashUtil
var random = new Random();
return random.Next() * (max - min + 1) + min;
return random.Next(min, max + 1);
}
}
+8 -4
View File
@@ -36,6 +36,7 @@ public class ImporterUtil
)
{
var tasks = new List<Task>();
var dictionaryLock = new object();
var result = Activator.CreateInstance(loadedType);
// get all filepaths
@@ -66,8 +67,11 @@ public class ImporterUtil
if (onObjectDeserialized != null)
onObjectDeserialized(file, fileDeserialized);
setMethod.Invoke(result,
isDictionary ? [_fileUtil.StripExtension(file), fileDeserialized] : [fileDeserialized]);
lock (dictionaryLock)
{
setMethod.Invoke(result,
isDictionary ? [_fileUtil.StripExtension(file), fileDeserialized] : [fileDeserialized]);
}
}
catch (Exception e)
{
@@ -80,7 +84,6 @@ public class ImporterUtil
// deep tree search
foreach (var directory in directories)
{
var dictionaryLock = new object();
tasks.Add(
Task.Factory.StartNew(() =>
{
@@ -125,7 +128,8 @@ public class ImporterUtil
{
var matchedProperty = type.GetProperties()
.FirstOrDefault(prop =>
prop.Name.ToLower() == _fileUtil.StripExtension(propertyName).ToLower());
string.Equals(prop.Name.ToLower(), _fileUtil.StripExtension(propertyName).ToLower(),
StringComparison.Ordinal));
if (matchedProperty == null)
throw new Exception(
$"Unable to find property '{_fileUtil.StripExtension(propertyName)}' for type '{type.Name}'");
+5 -1
View File
@@ -1,5 +1,7 @@
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Unicode;
using Core.Annotations;
using Core.Models.Enums;
using Core.Models.Spt.Dialog;
@@ -15,6 +17,7 @@ public class JsonUtil
WriteIndented = false,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
UnmappedMemberHandling = JsonUnmappedMemberHandling.Disallow,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
Converters =
{
new ListOrTConverterFactory(),
@@ -25,7 +28,8 @@ public class JsonUtil
new EftEnumConverter<ProfileChangeEventType>(),
new EftEnumConverter<QuestStatusEnum>(),
new EftEnumConverter<QuestRewardType>(),
new EftEnumConverter<SideType>()
new EftEnumConverter<SideType>(),
new EftEnumConverter<BonusSkillType>()
}
};
private static readonly JsonSerializerOptions jsonSerializerOptionsIndented = new(jsonSerializerOptionsNoIndent)
+33
View File
@@ -0,0 +1,33 @@
namespace Core.Utils
{
public class ProgressWriter
{
private readonly int _total;
private readonly int? _maxBarLength;
private readonly string? _barFillChar;
private readonly string? _barEmptyChar;
public ProgressWriter(int total, int maxBarLength, string barFillChar, string barEmptyChar)
{
_total = total;
_maxBarLength = maxBarLength;
_barFillChar = barFillChar;
_barEmptyChar = barEmptyChar;
}
public ProgressWriter(int total)
{
_total = total;
_maxBarLength = 25;
_barFillChar = "\u2593";
_barEmptyChar = "\u2591";
}
public void Increment()
{
// TODO - implement
}
}
}
+75 -3
View File
@@ -1,5 +1,6 @@
using System.Security.Cryptography;
using System.Security.Cryptography;
using Core.Annotations;
using ILogger = Core.Models.Utils.ILogger;
namespace Core.Utils;
@@ -7,6 +8,16 @@ namespace Core.Utils;
[Injectable(InjectionType.Singleton)]
public class RandomUtil
{
private readonly ILogger _logger;
public RandomUtil
(
ILogger logger
)
{
_logger = logger;
}
public readonly Random Random = new();
/// <summary>
@@ -300,7 +311,7 @@ public class RandomUtil
/// <returns>A biased random number within the specified range.</returns>
public double GetBiasedRandomNumber(double min, double max, double shift, double n)
{
/***
/**
* This function generates a random number based on a gaussian distribution with an option to add a bias via shifting.
*
* Here's an example graph of how the probabilities can be distributed:
@@ -314,8 +325,63 @@ public class RandomUtil
* Here's a place where you can play around with the 'n' and 'shift' values to see how the distribution changes:
* http://jsfiddle.net/e08cumyx/
*/
if (max < min)
{
_logger.Error($"Invalid argument, Bounded random number generation max is smaller than min({max} < {min}");
return -1;
}
throw new NotImplementedException("This honestly went over my head...");
if (n < 1)
{
_logger.Error($"Invalid argument, 'n' must be 1 or greater(received {n})");
return -1;
}
if (min == max)
{
return min;
}
if (shift > max - min)
{
/**
* If a rolled number is out of bounds (due to bias being applied), we roll it again.
* As the shifting increases, the chance of rolling a number within bounds decreases.
* A shift that is equal to the available range only has a 50% chance of rolling correctly, theoretically halving performance.
* Shifting even further drops the success chance very rapidly - so we want to warn against that
**/
_logger.Warning(
"Bias shift for random number generation is greater than the range of available numbers. This will have a severe performance impact");
_logger.Warning($"min-> {min}; max-> {max}; shift-> {shift}");
}
var biasedMin = shift >= 0 ? min - shift : min;
var biasedMax = shift < 0 ? max + shift : max;
double num;
do
{
num = GetBoundedGaussian(biasedMin, biasedMax, n);
} while (num < min || num > max);
return num;
}
private double GetBoundedGaussian(double start, double end, double n)
{
return Math.Round(start + GetGaussianRandom(n) * (end - start + 1));
}
private double GetGaussianRandom(double n)
{
var rand = 0d;
for (var i = 0; i < n; i += 1)
{
rand += GetSecureRandomNumber();
}
return rand / n;
}
/// <summary>
@@ -378,4 +444,10 @@ public class RandomUtil
? parts[1].Length
: 0;
}
public T GetArrayValue<T>(IEnumerable<T> list)
{
var rand = new Random();
return list.ElementAt(rand.Next(0, list.Count()));
}
}
+59 -38
View File
@@ -12,7 +12,7 @@ public class TimeUtil
/// </summary>
/// <param name="dateTime">The date to format in UTC.</param>
/// <returns>The formatted time as 'HH-MM-SS'.</returns>
public string FormatTime(DateTime dateTime)
public string FormatTime(DateTimeOffset dateTime)
{
var hour = Pad(dateTime.ToUniversalTime().Hour);
var minute = Pad(dateTime.ToUniversalTime().Minute);
@@ -26,7 +26,7 @@ public class TimeUtil
/// </summary>
/// <param name="dateTime">The date to format in UTC.</param>
/// <returns>The formatted date as 'YYYY-MM-DD'.</returns>
public string FormatDate(DateTime dateTime)
public string FormatDate(DateTimeOffset dateTime)
{
var day = Pad(dateTime.ToUniversalTime().Day);
var month = Pad(dateTime.ToUniversalTime().Month);
@@ -41,7 +41,7 @@ public class TimeUtil
/// <returns>The current date as 'YYYY-MM-DD'.</returns>
public string GetDate()
{
return FormatDate(DateTime.Now);
return FormatDate(DateTimeOffset.UtcNow);
}
/// <summary>
@@ -50,7 +50,7 @@ public class TimeUtil
/// <returns>The current time as 'HH-MM-SS'.</returns>
public string GetTime()
{
return FormatTime(DateTime.Now);
return FormatTime(DateTimeOffset.UtcNow);
}
/// <summary>
@@ -67,12 +67,14 @@ public class TimeUtil
/// </summary>
/// <param name="dateTime">datetime to get the time stamp for, if null it uses current date.</param>
/// <returns>Unix epoch for the start of day of the calculated date</returns>
public long GetStartOfDayTimeStamp(DateTime? dateTime)
public long GetStartOfDayTimeStamp(long? timestamp)
{
var now = dateTime ?? DateTime.Now;
return new DateTimeOffset(new DateTime(now.Year, now.Month, now.Day, 0, 0, 0))
.ToUnixTimeSeconds();
DateTime now = timestamp.HasValue
? DateTimeOffset.FromUnixTimeMilliseconds(timestamp.Value).DateTime
: DateTime.Now;
DateTime startOfDay = new DateTime(now.Year, now.Month, now.Day, 0, 0, 0);
return ((DateTimeOffset)startOfDay).ToUnixTimeMilliseconds();
}
/// <summary>
@@ -82,7 +84,7 @@ public class TimeUtil
/// <returns></returns>
public long GetTimeStampFromNowDays(int daysFromNow)
{
return DateTimeOffset.Now.AddDays(daysFromNow).ToUnixTimeSeconds();
return DateTimeOffset.UtcNow.AddDays(daysFromNow).ToUnixTimeSeconds();
}
/// <summary>
@@ -92,16 +94,17 @@ public class TimeUtil
/// <returns></returns>
public long GetTimeStampFromNowHours(int hoursFromNow)
{
return DateTimeOffset.Now.AddHours(hoursFromNow).ToUnixTimeSeconds();
return DateTimeOffset.UtcNow.AddHours(hoursFromNow).ToUnixTimeSeconds();
}
/// <summary>
/// Gets the current time in UTC in a format suitable for mail in EFT.
/// </summary>
/// <returns>The current time as 'HH:MM' in UTC.</returns>
/// GetTimeMailFormat
public string GetTimeMailFormat()
{
return DateTime.UtcNow.ToString("HH:mm");
return DateTimeOffset.UtcNow.ToString("HH:mm");
}
/// <summary>
@@ -110,7 +113,7 @@ public class TimeUtil
/// <returns>The current date as 'DD.MM.YYYY' in UTC.</returns>
public string GetDateMailFormat()
{
return DateTime.UtcNow.ToString("dd.MM.yyyy");
return DateTimeOffset.UtcNow.ToString("dd.MM.yyyy");
}
/// <summary>
@@ -129,19 +132,14 @@ public class TimeUtil
/// <returns>Time stamp of the next hour in unix time seconds</returns>
public long GetTimeStampOfNextHour()
{
var now = DateTime.UtcNow;
DateTime now = DateTime.Now;
TimeSpan timeUntilNextHour = TimeSpan.FromMinutes(60 - now.Minute)
.Subtract(TimeSpan.FromSeconds(now.Second))
.Subtract(TimeSpan.FromMilliseconds(now.Millisecond));
var time = ((DateTimeOffset)now.Add(timeUntilNextHour)).ToUnixTimeSeconds();
var nextHour = new DateTime(
now.Year,
now.Month,
now.Day,
now.Hour,
0,
0,
DateTimeKind.Utc
).AddHours(1);
return new DateTimeOffset(nextHour).ToUnixTimeSeconds();
return time;
}
/// <summary>
@@ -151,19 +149,20 @@ public class TimeUtil
/// <returns>Timestamp</returns>
public long GetTodayMidnightTimeStamp()
{
var now = DateTime.UtcNow;
var midNight = new DateTime(
now.Year,
now.Month,
now.Day,
0,
0,
0,
DateTimeKind.Utc
);
return new DateTimeOffset(midNight).ToUnixTimeSeconds();
DateTime now = DateTime.Now;
int hours = now.Hour;
int minutes = now.Minute;
// If minutes greater than 0, subtract 1 hour
if (minutes > 0)
{
hours--;
}
// Create a new DateTime with the last full hour, 0 minutes, and 0 seconds
DateTime lastFullHour = new DateTime(now.Year, now.Month, now.Day, hours, 0, 0);
return ((DateTimeOffset)lastFullHour).ToUnixTimeMilliseconds();
}
/// <summary>
@@ -175,4 +174,26 @@ public class TimeUtil
{
return number.ToString().PadLeft(2, '0');
}
/// <summary>
/// Takes a timestamp and converts to its date with Epoch
/// </summary>
/// <param name="timeStamp"></param>
/// <returns></returns>
public DateTime GetDateTimeFromTimeStamp(long timeStamp)
{
return DateTimeOffset.FromUnixTimeMilliseconds(timeStamp).DateTime;
}
/// <summary>
/// Takes a timestamp and gets difference between Epoch time and time provided resulting in a unixtimestamp (date defaults to utcnow)
/// </summary>
/// <param name="date"></param>
/// <returns></returns>
public long GetTimeStampFromEpoch(DateTime? date = null)
{
var dateToCompare = date ?? DateTime.UtcNow;
return (long)(dateToCompare - DateTime.UnixEpoch).TotalSeconds;
}
}
+30
View File
@@ -0,0 +1,30 @@
using System.Diagnostics;
using Core.Annotations;
namespace Core.Utils
{
[Injectable]
public class TimerUtil
{
private readonly Stopwatch _stopwatch;
public TimerUtil()
{
_stopwatch = new Stopwatch();
_stopwatch.Start();
}
public int Stop(string unit = "sec")
{
_stopwatch.Stop();
var timePassed = _stopwatch.Elapsed;
return unit switch
{
"ns" => timePassed.Nanoseconds,
"ms" => timePassed.Milliseconds,
_ => timePassed.Seconds
};
}
}
}
+1 -1
View File
@@ -1149,7 +1149,7 @@
"backpackLoot": {
"weights": {
"0": 3,
"1": 5,
"1": 7,
"2": 12,
"3": 20,
"4": 8,
+5 -4
View File
@@ -179,12 +179,9 @@
}
},
"features": {
"autoInstallModDependencies": false,
"compressProfile": false,
"chatbotFeatures": {
"sptFriendEnabled": true,
"sptFriendGiftsEnabled": true,
"commandoEnabled": true,
"commandoFeatures": {
"giveCommandEnabled": true
},
@@ -194,7 +191,11 @@
"ids": {
"commando": "6723fd51c5924c57ce0ca01e",
"spt": "6723fd51c5924c57ce0ca01f"
}
},
"enabledBots": {
"6723fd51c5924c57ce0ca01e": true,
"6723fd51c5924c57ce0ca01f": true
}
},
"createNewProfileTypesBlacklist": []
},
+229 -199
View File
@@ -8522,209 +8522,239 @@
"associatedEvent": "Promo",
"maxToSendPlayer": 5
},
"NewYear2024": {
"items": [{
"_id": "6773b39ccf4c2fe4e50a7ec3",
"_tpl": "67124dcfa3541f2a1f0e788b",
"upd": {
"FireMode": {
"FireMode": "single"
},
"Repairable": {
"Durability": 100,
"MaxDurability": 100
}
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
}, {
"_id": "6773b39ccf4c2fe4e50a7ec4",
"_tpl": "6719023b612cc94b9008e78c",
"parentId": "6773b39ccf4c2fe4e50a7ec3",
"slotId": "mod_stock"
}, {
"_id": "6773b39ccf4c2fe4e50a7ec5",
"_tpl": "6709133fa532466d5403fb7c",
"parentId": "6773b39ccf4c2fe4e50a7ec3",
"slotId": "mod_magazine"
}, {
"_id": "6773b39ccf4c2fe4e50a7ec6",
"_tpl": "670fd0a8d8d4eae4790c8187",
"parentId": "6773b39ccf4c2fe4e50a7ec3",
"slotId": "mod_barrel"
}, {
"_id": "6773b39ccf4c2fe4e50a7ec7",
"_tpl": "6710cea62bb09af72f0e6bf8",
"parentId": "6773b39ccf4c2fe4e50a7ec6",
"slotId": "mod_mount"
}, {
"_id": "6773b39ccf4c2fe4e50a7ec8",
"_tpl": "57d17c5e2459775a5c57d17d",
"parentId": "6773b39ccf4c2fe4e50a7ec7",
"slotId": "mod_tactical_003"
}, {
"_id": "6773b39ccf4c2fe4e50a7ec9",
"_tpl": "6165ac8c290d254f5e6b2f6c",
"parentId": "6773b39ccf4c2fe4e50a7ec3",
"slotId": "mod_scope"
}, {
"_id": "6773b39ccf4c2fe4e50a7ecb",
"_tpl": "6709133fa532466d5403fb7c",
"upd": {
"StackObjectsCount": 1
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
}, {
"_id": "6773b39ccf4c2fe4e50a7ecd",
"_tpl": "6709133fa532466d5403fb7c",
"upd": {
"StackObjectsCount": 1
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
}, {
"_id": "6773b39ccf4c2fe4e50a7ecf",
"_tpl": "6709133fa532466d5403fb7c",
"upd": {
"StackObjectsCount": 1
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
}, {
"_id": "6773b39ccf4c2fe4e50a7ed1",
"_tpl": "5c0d591486f7744c505b416f",
"upd": {
"StackObjectsCount": 20
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
}, {
"_id": "6773b39ccf4c2fe4e50a7ed3",
"_tpl": "5c0d591486f7744c505b416f",
"upd": {
"StackObjectsCount": 20
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
}, {
"_id": "6773b39ccf4c2fe4e50a7ed5",
"_tpl": "5c0d591486f7744c505b416f",
"upd": {
"StackObjectsCount": 20
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
}, {
"_id": "6773b39ccf4c2fe4e50a7ed6",
"_tpl": "5ab8e79e86f7742d8b372e78",
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
}, {
"_id": "6773b39ccf4c2fe4e50a7ed7",
"_tpl": "65732688d9d89ff7ac0d9c4c",
"parentId": "6773b39ccf4c2fe4e50a7ed6",
"slotId": "Soft_armor_front"
}, {
"_id": "6773b39ccf4c2fe4e50a7ed8",
"_tpl": "657326978c1cc6dcd9098b56",
"parentId": "6773b39ccf4c2fe4e50a7ed6",
"slotId": "Soft_armor_back"
}, {
"_id": "6773b39ccf4c2fe4e50a7ed9",
"_tpl": "657326a28c1cc6dcd9098b5a",
"parentId": "6773b39ccf4c2fe4e50a7ed6",
"slotId": "Soft_armor_left"
}, {
"_id": "6773b39ccf4c2fe4e50a7eda",
"_tpl": "657326b08c1cc6dcd9098b5e",
"parentId": "6773b39ccf4c2fe4e50a7ed6",
"slotId": "soft_armor_right"
}, {
"_id": "6773b39ccf4c2fe4e50a7edb",
"_tpl": "657326bc5d3a3129fb05f36b",
"parentId": "6773b39ccf4c2fe4e50a7ed6",
"slotId": "Collar"
}, {
"_id": "6773b39ccf4c2fe4e50a7edc",
"_tpl": "656f611f94b480b8a500c0db",
"parentId": "6773b39ccf4c2fe4e50a7ed6",
"slotId": "Front_plate"
}, {
"_id": "6773b39ccf4c2fe4e50a7edd",
"_tpl": "65573fa5655447403702a816",
"parentId": "6773b39ccf4c2fe4e50a7ed6",
"slotId": "Back_plate"
}, {
"_id": "6773b39ccf4c2fe4e50a7ede",
"_tpl": "675956062f6ddfe8ff0e2806",
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
}, {
"_id": "6773b39ccf4c2fe4e50a7edf",
"_tpl": "676307c004856a0b3c0dfffd",
"parentId": "6773b39ccf4c2fe4e50a7ede",
"slotId": "Helmet_top"
}, {
"_id": "6773b39ccf4c2fe4e50a7ee0",
"_tpl": "676307b4d9ec0af3d9001fa8",
"parentId": "6773b39ccf4c2fe4e50a7ede",
"slotId": "Helmet_back"
}, {
"_id": "6773b39ccf4c2fe4e50a7ee2",
"_tpl": "674589d98dd67746010329e6",
"upd": {
"StackObjectsCount": 1
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
}, {
"_id": "6773b39ccf4c2fe4e50a7ee4",
"_tpl": "67458794e21e5d724e066976",
"upd": {
"StackObjectsCount": 1
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
}, {
"_id": "6773b39ccf4c2fe4e50a7ee6",
"_tpl": "5d40407c86f774318526545a",
"upd": {
"StackObjectsCount": 1
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
}, {
"_id": "6773b39ccf4c2fe4e50a7ee8",
"_tpl": "5d40407c86f774318526545a",
"upd": {
"StackObjectsCount": 1
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
}, {
"_id": "6773b39ccf4c2fe4e50a7eea",
"_tpl": "5d403f9186f7743cac3f229b",
"upd": {
"StackObjectsCount": 1
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
}, {
"_id": "6773b39ccf4c2fe4e50a7eec",
"_tpl": "5d403f9186f7743cac3f229b",
"upd": {
"StackObjectsCount": 1
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
"NewYear2024": {
"items": [
{
"_id": "6773b39ccf4c2fe4e50a7ec3",
"_tpl": "67124dcfa3541f2a1f0e788b",
"upd": {
"FireMode": {
"FireMode": "single"
},
"Repairable": {
"Durability": 100,
"MaxDurability": 100
}
],
"sender": "System",
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
},
{
"_id": "6773b39ccf4c2fe4e50a7ec4",
"_tpl": "6719023b612cc94b9008e78c",
"parentId": "6773b39ccf4c2fe4e50a7ec3",
"slotId": "mod_stock"
},
{
"_id": "6773b39ccf4c2fe4e50a7ec5",
"_tpl": "6709133fa532466d5403fb7c",
"parentId": "6773b39ccf4c2fe4e50a7ec3",
"slotId": "mod_magazine"
},
{
"_id": "6773b39ccf4c2fe4e50a7ec6",
"_tpl": "670fd0a8d8d4eae4790c8187",
"parentId": "6773b39ccf4c2fe4e50a7ec3",
"slotId": "mod_barrel"
},
{
"_id": "6773b39ccf4c2fe4e50a7ec7",
"_tpl": "6710cea62bb09af72f0e6bf8",
"parentId": "6773b39ccf4c2fe4e50a7ec6",
"slotId": "mod_mount"
},
{
"_id": "6773b39ccf4c2fe4e50a7ec8",
"_tpl": "57d17c5e2459775a5c57d17d",
"parentId": "6773b39ccf4c2fe4e50a7ec7",
"slotId": "mod_tactical_003"
},
{
"_id": "6773b39ccf4c2fe4e50a7ec9",
"_tpl": "6165ac8c290d254f5e6b2f6c",
"parentId": "6773b39ccf4c2fe4e50a7ec3",
"slotId": "mod_scope"
},
{
"_id": "6773b39ccf4c2fe4e50a7ecb",
"_tpl": "6709133fa532466d5403fb7c",
"upd": {
"StackObjectsCount": 1
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
},
{
"_id": "6773b39ccf4c2fe4e50a7ecd",
"_tpl": "6709133fa532466d5403fb7c",
"upd": {
"StackObjectsCount": 1
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
},
{
"_id": "6773b39ccf4c2fe4e50a7ecf",
"_tpl": "6709133fa532466d5403fb7c",
"upd": {
"StackObjectsCount": 1
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
},
{
"_id": "6773b39ccf4c2fe4e50a7ed1",
"_tpl": "5c0d591486f7744c505b416f",
"upd": {
"StackObjectsCount": 20
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
},
{
"_id": "6773b39ccf4c2fe4e50a7ed3",
"_tpl": "5c0d591486f7744c505b416f",
"upd": {
"StackObjectsCount": 20
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
},
{
"_id": "6773b39ccf4c2fe4e50a7ed5",
"_tpl": "5c0d591486f7744c505b416f",
"upd": {
"StackObjectsCount": 20
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
},
{
"_id": "6773b39ccf4c2fe4e50a7ed6",
"_tpl": "5ab8e79e86f7742d8b372e78",
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
},
{
"_id": "6773b39ccf4c2fe4e50a7ed7",
"_tpl": "65732688d9d89ff7ac0d9c4c",
"parentId": "6773b39ccf4c2fe4e50a7ed6",
"slotId": "Soft_armor_front"
},
{
"_id": "6773b39ccf4c2fe4e50a7ed8",
"_tpl": "657326978c1cc6dcd9098b56",
"parentId": "6773b39ccf4c2fe4e50a7ed6",
"slotId": "Soft_armor_back"
},
{
"_id": "6773b39ccf4c2fe4e50a7ed9",
"_tpl": "657326a28c1cc6dcd9098b5a",
"parentId": "6773b39ccf4c2fe4e50a7ed6",
"slotId": "Soft_armor_left"
},
{
"_id": "6773b39ccf4c2fe4e50a7eda",
"_tpl": "657326b08c1cc6dcd9098b5e",
"parentId": "6773b39ccf4c2fe4e50a7ed6",
"slotId": "soft_armor_right"
},
{
"_id": "6773b39ccf4c2fe4e50a7edb",
"_tpl": "657326bc5d3a3129fb05f36b",
"parentId": "6773b39ccf4c2fe4e50a7ed6",
"slotId": "Collar"
},
{
"_id": "6773b39ccf4c2fe4e50a7edc",
"_tpl": "656f611f94b480b8a500c0db",
"parentId": "6773b39ccf4c2fe4e50a7ed6",
"slotId": "Front_plate"
},
{
"_id": "6773b39ccf4c2fe4e50a7edd",
"_tpl": "65573fa5655447403702a816",
"parentId": "6773b39ccf4c2fe4e50a7ed6",
"slotId": "Back_plate"
},
{
"_id": "6773b39ccf4c2fe4e50a7ede",
"_tpl": "675956062f6ddfe8ff0e2806",
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
},
{
"_id": "6773b39ccf4c2fe4e50a7edf",
"_tpl": "676307c004856a0b3c0dfffd",
"parentId": "6773b39ccf4c2fe4e50a7ede",
"slotId": "Helmet_top"
},
{
"_id": "6773b39ccf4c2fe4e50a7ee0",
"_tpl": "676307b4d9ec0af3d9001fa8",
"parentId": "6773b39ccf4c2fe4e50a7ede",
"slotId": "Helmet_back"
},
{
"_id": "6773b39ccf4c2fe4e50a7ee2",
"_tpl": "674589d98dd67746010329e6",
"upd": {
"StackObjectsCount": 1
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
},
{
"_id": "6773b39ccf4c2fe4e50a7ee4",
"_tpl": "67458794e21e5d724e066976",
"upd": {
"StackObjectsCount": 1
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
},
{
"_id": "6773b39ccf4c2fe4e50a7ee6",
"_tpl": "5d40407c86f774318526545a",
"upd": {
"StackObjectsCount": 1
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
},
{
"_id": "6773b39ccf4c2fe4e50a7ee8",
"_tpl": "5d40407c86f774318526545a",
"upd": {
"StackObjectsCount": 1
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
},
{
"_id": "6773b39ccf4c2fe4e50a7eea",
"_tpl": "5d403f9186f7743cac3f229b",
"upd": {
"StackObjectsCount": 1
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
},
{
"_id": "6773b39ccf4c2fe4e50a7eec",
"_tpl": "5d403f9186f7743cac3f229b",
"upd": {
"StackObjectsCount": 1
},
"parentId": "6773b39ccf4c2fe4e50a7ec2",
"slotId": "main"
}
],
"sender": "System",
"messageText": "New year present!",
"collectionTimeHours": 72,
"associatedEvent": "Promo",
"maxToSendPlayer": 5
}
}
}
}
+2 -2
View File
@@ -1411,7 +1411,7 @@
"_type": "opened case - walter event quest",
"rewardCount": 4,
"foundInRaid": true,
"rewardTplPool": {"67409848d0b2f8eb9b034db9": 1}
"rewardTplPool": { "67409848d0b2f8eb9b034db9": 1 }
},
"674098588466ebb03408b210": {
"_type": "opened box - walter event quest",
@@ -1596,5 +1596,5 @@
"skillGainMultiplers": {
"Strength": 1
},
"deprioritisedMoneyContainers": ["590c60fc86f77412b13fddcf", "5d235bb686f77443f4331278"]
"deprioritisedMoneyContainers": ["590c60fc86f77412b13fddcf", "5d235bb686f77443f4331278"]
}
+264 -189
View File
@@ -52,12 +52,12 @@
"66da1b546916142b3b022777",
"670ad7f1ad195290cd00da7a",
"66ec2aa6daf127599c0c31f1",
"67654a6759116d347b0bfb86",
"5751916f24597720a27126df",
"57518f7724597720a31c09ab",
"57518fd424597720c85dbaaa",
"5a043f2c86f7741aa57b5145",
"5a0448bc86f774736f14efa8"
"67654a6759116d347b0bfb86",
"5751916f24597720a27126df",
"57518f7724597720a31c09ab",
"57518fd424597720c85dbaaa",
"5a043f2c86f7741aa57b5145",
"5a0448bc86f774736f14efa8"
],
"rewardItemTypeBlacklist": ["65649eb40bf0ed77b8044453"],
"lootableItemBlacklist": ["660bbc47c38b837877075e47", "660bc341c38b837877075e4c"],
@@ -122,21 +122,26 @@
"671d8b38b769f0d88c0950f8",
"671d8b8c0959c721a50ca838",
"660bc341c38b837877075e4c",
"67409848d0b2f8eb9b034db9",
"67449b6c89d5e1ddc603f504",
"675aab0d6b6addc02a08f097",
"675aaae1dcf102478202c537",
"675aaa9a3107dac100063331",
"675aaae75a3ab8372d0b02a7",
"675aaab74bca0b001d02f356",
"675aaa8f7f3c962069072b27",
"675aaaf674a7619a5304c233",
"675aaa003107dac10006332f",
"6764207f2fa5e32733055c4a",
"6764202ae307804338014c1a",
"6707d13e4e617ec94f0e5631",
"675dc9d37ae1a8792107ca96",
"675dcb0545b1a2d108011b2b"
"67409848d0b2f8eb9b034db9",
"67449b6c89d5e1ddc603f504",
"675aab0d6b6addc02a08f097",
"675aaae1dcf102478202c537",
"675aaa9a3107dac100063331",
"675aaae75a3ab8372d0b02a7",
"675aaab74bca0b001d02f356",
"675aaa8f7f3c962069072b27",
"675aaaf674a7619a5304c233",
"675aaa003107dac10006332f",
"6764207f2fa5e32733055c4a",
"6764202ae307804338014c1a",
"6707d13e4e617ec94f0e5631",
"675dc9d37ae1a8792107ca96",
"675dcb0545b1a2d108011b2b",
"66d9f8744827a77e870ecaf1",
"6707d0804e617ec94f0e562f",
"67449b6c89d5e1ddc603f504",
"6740987b89d5e1ddc603f4f0",
"6707d0bdaab679420007e01a"
],
"bossItems": [
"6275303a9f372d6ea97f9ec7",
@@ -183,172 +188,242 @@
"63a898a328e385334e0640a5": { "price": 20000, "parentId": "5b5f6fa186f77409407a7eb7" },
"63a897c6b1ff6e29734fcc95": { "price": 40000, "parentId": "5b5f6fa186f77409407a7eb7" }
},
"customItemGlobalPresets": [
{
"_changeWeaponName": false,
"_encyclopedia": "675956062f6ddfe8ff0e2806",
"_id": "6777b37393a9a6f10ea57501",
"_items": [
{
"_id": "6777bee3324b2e0cf7b7cfe1",
"_tpl": "675956062f6ddfe8ff0e2806"
},
{
"_id": "6777c029e3703d1f9dc47d15",
"_tpl": "676307c004856a0b3c0dfffd",
"parentId": "6777bee3324b2e0cf7b7cfe1",
"slotId": "Helmet_top"
},
{
"_id": "6777c030386bec3cd3c41b6e",
"_tpl": "676307b4d9ec0af3d9001fa8",
"parentId": "6777bee3324b2e0cf7b7cfe1",
"slotId": "Helmet_back"
}
],
"_name": "MTEK FLUX Ballistic helmet (MultiCam Alpine) Default",
"_parent": "6777bee3324b2e0cf7b7cfe1",
"_type": "Preset"
},
{
"_changeWeaponName": false,
"_encyclopedia": "6759655674aa5e0825040d62",
"_id": "6777bf37e10ffdb431ff9508",
"_items": [
{
"_id": "6777bf03ad549e66e7ad06b2",
"_tpl": "6759655674aa5e0825040d62"
},
{
"_id": "6777c02180ddaee0b54493da",
"_tpl": "676307c004856a0b3c0dfffd",
"parentId": "6777bf03ad549e66e7ad06b2",
"slotId": "Helmet_top"
},
{
"_id": "6777c00f41b022243abdac99",
"_tpl": "676307b4d9ec0af3d9001fa8",
"parentId": "6777bf03ad549e66e7ad06b2",
"slotId": "Helmet_back"
}
],
"_name": "MTEK FLUX Ballistic helmet (Olive Drab) Default",
"_parent": "6777bf03ad549e66e7ad06b2",
"_type": "Preset"
},
{
"_changeWeaponName": false,
"_encyclopedia": "67597ceea35600b4c10cea86",
"_id": "6777bfee0658691ef27d9993",
"_items": [
{
"_id": "6777bfaaaf1999ce0701bc00",
"_tpl": "67597ceea35600b4c10cea86"
},
{
"_id": "6777c01b07b5d347d6b99404",
"_tpl": "676307ded8b241b4f703a3e8",
"parentId": "6777bfaaaf1999ce0701bc00",
"slotId": "Helmet_top"
},
{
"_id": "6777c0033f374912b965ae5a",
"_tpl": "676307d3d9ec0af3d9001fac",
"parentId": "6777bfaaaf1999ce0701bc00",
"slotId": "Helmet_back"
}
],
"_name": "MTEK FLUX Ballistic helmet (Coyote) Default",
"_parent": "6777bfaaaf1999ce0701bc00",
"_type": "Preset"
},
{
"_changeWeaponName": false,
"_encyclopedia": "67597d241d5a44f2f605df06",
"_id": "6777c1244557b9b1474da362",
"_items": [
{
"_id": "6777c10f4989fc544063b5a1",
"_tpl": "67597d241d5a44f2f605df06"
},
{
"_id": "6777c1293c00b35cd6960536",
"_tpl": "676307ded8b241b4f703a3e8",
"parentId": "6777c10f4989fc544063b5a1",
"slotId": "Helmet_top"
},
{
"_id": "6777c12f8fc234973af0cf16",
"_tpl": "676307d3d9ec0af3d9001fac",
"parentId": "6777c10f4989fc544063b5a1",
"slotId": "Helmet_back"
}
],
"_name": "MTEK FLUX Ballistic helmet (Coyote/MultiCam Arid) Default",
"_parent": "6777c10f4989fc544063b5a1",
"_type": "Preset"
},
{
"_changeWeaponName": false,
"_encyclopedia": "674d91ce6e862d5a95059ed6",
"_id": "67784df15058057382b28f87",
"_items": [
{
"_id": "67784dfaf57296ecbdbfc87f",
"_tpl": "674d91ce6e862d5a95059ed6"
},
{
"_id": "67784e27ee352de60dc982a3",
"_tpl": "6575ea3060703324250610da",
"parentId": "67784dfaf57296ecbdbfc87f",
"slotId": "Soft_armor_front"
},
{
"_id": "67784e32d75f52daffe36de5",
"_tpl": "6575ea4cf6a13a7b7100adc4",
"parentId": "67784dfaf57296ecbdbfc87f",
"slotId": "Soft_armor_back"
},
{
"_id": "67784e36e778f8aace662e1d",
"_tpl": "6575ea5cf6a13a7b7100adc8",
"parentId": "67784dfaf57296ecbdbfc87f",
"slotId": "Soft_armor_left"
},
{
"_id": "67784e3c0e4afac4ebdab52d",
"_tpl": "6575ea6760703324250610de",
"parentId": "67784dfaf57296ecbdbfc87f",
"slotId": "soft_armor_right"
},
{
"_id": "67784e40832d547cb2c986bf",
"_tpl": "6575ea719c7cad336508e418",
"parentId": "67784dfaf57296ecbdbfc87f",
"slotId": "Collar"
},
{
"_id": "67784e446c92fcb0e0d8fea8",
"_tpl": "6575ea7c60703324250610e2",
"parentId": "67784dfaf57296ecbdbfc87f",
"slotId": "Groin"
},
{
"_id": "67784e473abf2a39a05dba5a",
"_tpl": "656f611f94b480b8a500c0db",
"parentId": "67784dfaf57296ecbdbfc87f",
"slotId": "Front_plate"
},
{
"_id": "67784e4b934ace8451681bee",
"_tpl": "656efaf54772930db4031ff5",
"parentId": "67784dfaf57296ecbdbfc87f",
"slotId": "Back_plate"
}
],
"_name": "Body armor 6B13 M Killa Christmas Standard",
"_parent": "67784dfaf57296ecbdbfc87f",
"_type": "Preset"
}
]
"customItemGlobalPresets": [
{
"_changeWeaponName": false,
"_encyclopedia": "675956062f6ddfe8ff0e2806",
"_id": "6777b37393a9a6f10ea57501",
"_items": [
{
"_id": "6777bee3324b2e0cf7b7cfe1",
"_tpl": "675956062f6ddfe8ff0e2806"
},
{
"_id": "6777c029e3703d1f9dc47d15",
"_tpl": "676307c004856a0b3c0dfffd",
"parentId": "6777bee3324b2e0cf7b7cfe1",
"slotId": "Helmet_top"
},
{
"_id": "6777c030386bec3cd3c41b6e",
"_tpl": "676307b4d9ec0af3d9001fa8",
"parentId": "6777bee3324b2e0cf7b7cfe1",
"slotId": "Helmet_back"
}
],
"_name": "MTEK FLUX Ballistic helmet (MultiCam Alpine) Default",
"_parent": "6777bee3324b2e0cf7b7cfe1",
"_type": "Preset"
},
{
"_changeWeaponName": false,
"_encyclopedia": "6759655674aa5e0825040d62",
"_id": "6777bf37e10ffdb431ff9508",
"_items": [
{
"_id": "6777bf03ad549e66e7ad06b2",
"_tpl": "6759655674aa5e0825040d62"
},
{
"_id": "6777c02180ddaee0b54493da",
"_tpl": "676307c004856a0b3c0dfffd",
"parentId": "6777bf03ad549e66e7ad06b2",
"slotId": "Helmet_top"
},
{
"_id": "6777c00f41b022243abdac99",
"_tpl": "676307b4d9ec0af3d9001fa8",
"parentId": "6777bf03ad549e66e7ad06b2",
"slotId": "Helmet_back"
}
],
"_name": "MTEK FLUX Ballistic helmet (Olive Drab) Default",
"_parent": "6777bf03ad549e66e7ad06b2",
"_type": "Preset"
},
{
"_changeWeaponName": false,
"_encyclopedia": "67597ceea35600b4c10cea86",
"_id": "6777bfee0658691ef27d9993",
"_items": [
{
"_id": "6777bfaaaf1999ce0701bc00",
"_tpl": "67597ceea35600b4c10cea86"
},
{
"_id": "6777c01b07b5d347d6b99404",
"_tpl": "676307ded8b241b4f703a3e8",
"parentId": "6777bfaaaf1999ce0701bc00",
"slotId": "Helmet_top"
},
{
"_id": "6777c0033f374912b965ae5a",
"_tpl": "676307d3d9ec0af3d9001fac",
"parentId": "6777bfaaaf1999ce0701bc00",
"slotId": "Helmet_back"
}
],
"_name": "MTEK FLUX Ballistic helmet (Coyote) Default",
"_parent": "6777bfaaaf1999ce0701bc00",
"_type": "Preset"
},
{
"_changeWeaponName": false,
"_encyclopedia": "67597d241d5a44f2f605df06",
"_id": "6777c1244557b9b1474da362",
"_items": [
{
"_id": "6777c10f4989fc544063b5a1",
"_tpl": "67597d241d5a44f2f605df06"
},
{
"_id": "6777c1293c00b35cd6960536",
"_tpl": "676307ded8b241b4f703a3e8",
"parentId": "6777c10f4989fc544063b5a1",
"slotId": "Helmet_top"
},
{
"_id": "6777c12f8fc234973af0cf16",
"_tpl": "676307d3d9ec0af3d9001fac",
"parentId": "6777c10f4989fc544063b5a1",
"slotId": "Helmet_back"
}
],
"_name": "MTEK FLUX Ballistic helmet (Coyote/MultiCam Arid) Default",
"_parent": "6777c10f4989fc544063b5a1",
"_type": "Preset"
},
{
"_changeWeaponName": false,
"_encyclopedia": "674d91ce6e862d5a95059ed6",
"_id": "67784df15058057382b28f87",
"_items": [
{
"_id": "67784dfaf57296ecbdbfc87f",
"_tpl": "674d91ce6e862d5a95059ed6"
},
{
"_id": "67784e27ee352de60dc982a3",
"_tpl": "6575ea3060703324250610da",
"parentId": "67784dfaf57296ecbdbfc87f",
"slotId": "Soft_armor_front"
},
{
"_id": "67784e32d75f52daffe36de5",
"_tpl": "6575ea4cf6a13a7b7100adc4",
"parentId": "67784dfaf57296ecbdbfc87f",
"slotId": "Soft_armor_back"
},
{
"_id": "67784e36e778f8aace662e1d",
"_tpl": "6575ea5cf6a13a7b7100adc8",
"parentId": "67784dfaf57296ecbdbfc87f",
"slotId": "Soft_armor_left"
},
{
"_id": "67784e3c0e4afac4ebdab52d",
"_tpl": "6575ea6760703324250610de",
"parentId": "67784dfaf57296ecbdbfc87f",
"slotId": "soft_armor_right"
},
{
"_id": "67784e40832d547cb2c986bf",
"_tpl": "6575ea719c7cad336508e418",
"parentId": "67784dfaf57296ecbdbfc87f",
"slotId": "Collar"
},
{
"_id": "67784e446c92fcb0e0d8fea8",
"_tpl": "6575ea7c60703324250610e2",
"parentId": "67784dfaf57296ecbdbfc87f",
"slotId": "Groin"
},
{
"_id": "67784e473abf2a39a05dba5a",
"_tpl": "656f611f94b480b8a500c0db",
"parentId": "67784dfaf57296ecbdbfc87f",
"slotId": "Front_plate"
},
{
"_id": "67784e4b934ace8451681bee",
"_tpl": "656efaf54772930db4031ff5",
"parentId": "67784dfaf57296ecbdbfc87f",
"slotId": "Back_plate"
}
],
"_name": "Body armor 6B13 M Killa Christmas Standard",
"_parent": "67784dfaf57296ecbdbfc87f",
"_type": "Preset"
},
{
"_changeWeaponName": false,
"_encyclopedia": "6745895717824b1ec20570a6",
"_id": "677d14927f8ee8353d85918d",
"_items": [
{
"_id": "677d14a27757dcc54a3054fb",
"_tpl": "6745895717824b1ec20570a6"
},
{
"_id": "677d14abef2285ed18ee3e62",
"_tpl": "657baaf0b7e9ca9a02045c02",
"parentId": "677d14a27757dcc54a3054fb",
"slotId": "Helmet_top"
},
{
"_id": "677d14b0024eda59d1544794",
"_tpl": "657bab6ec6f689d3a205b85f",
"parentId": "677d14a27757dcc54a3054fb",
"slotId": "Helmet_back"
},
{
"_id": "677d14bb1dfea5b33fa31337",
"_tpl": "657babc6f58ba5a6250107a2",
"parentId": "677d14a27757dcc54a3054fb",
"slotId": "Helmet_ears"
}
],
"_name": "6B47 Ratnik-BSh helmet (EMR Arctic cover) default",
"_parent": "677d14a27757dcc54a3054fb",
"_type": "Preset"
},
{
"_changeWeaponName": false,
"_encyclopedia": "6759af0f9c8a538dd70bfae6",
"_id": "677e90e191de7ae4136e3967",
"_items": [
{
"_id": "677e90d1fc28426ede1448bd",
"_tpl": "6759af0f9c8a538dd70bfae6"
},
{
"_id": "677e90e71b6c92662b1b5cce",
"_tpl": "6571133d22996eaf11088200",
"parentId": "677e90d1fc28426ede1448bd",
"slotId": "Helmet_top"
},
{
"_id": "677e90ef6b6b559c36d31485",
"_tpl": "6571138e818110db4600aa71",
"parentId": "677e90d1fc28426ede1448bd",
"slotId": "Helmet_back"
},
{
"_id": "677e90f2e2de53f5b48dd35d",
"_tpl": "657112fa818110db4600aa6b",
"parentId": "677e90d1fc28426ede1448bd",
"slotId": "Helmet_ears"
},
{
"_id": "677e90f61cc7ed9f89331cac",
"_tpl": "5c0e842486f77443a74d2976",
"parentId": "677e90d1fc28426ede1448bd",
"slotId": "mod_equipment"
}
],
"_name": "Maska-1SCh bulletproof helmet (Christmas Edition) default",
"_parent": "677e90d1fc28426ede1448bd",
"_type": "Preset"
}
]
}
+6
View File
@@ -539,5 +539,11 @@
"nonTriggered": 80,
"triggered": 90
},
"tplsToStripChildItemsFrom": [
"63a8970d7108f713591149f5",
"63a897c6b1ff6e29734fcc95",
"63a898a328e385334e0640a5",
"634959225289190e5e773b3b"
],
"nonMaps": ["base", "develop", "hideout", "privatearea", "suburbs", "terminal", "town"]
}
+78 -78
View File
@@ -123,20 +123,20 @@
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
},
"drugs": {
"weights": {
"0": 1,
"1": 1
},
"whitelist": {}
"whitelist": []
},
"stims": {
"weights": {
"0": 1
},
"whitelist": {}
"whitelist": []
},
"looseLoot": {
"weights": {
@@ -146,21 +146,21 @@
"3": 1,
"4": 1
},
"whitelist": {}
"whitelist": []
},
"magazines": {
"weights": {
"1": 2,
"2": 1
},
"whitelist": {}
"whitelist": []
},
"grenades": {
"weights": {
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
}
},
"lootItemsToAddChancePercent": {}
@@ -208,20 +208,20 @@
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
},
"drugs": {
"weights": {
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
},
"stims": {
"weights": {
"0": 1
},
"whitelist": {}
"whitelist": []
},
"looseLoot": {
"weights": {
@@ -231,21 +231,21 @@
"3": 1,
"4": 1
},
"whitelist": {}
"whitelist": []
},
"magazines": {
"weights": {
"1": 2,
"2": 1
},
"whitelist": {}
"whitelist": []
},
"grenades": {
"weights": {
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
}
},
"lootItemsToAddChancePercent": {}
@@ -293,20 +293,20 @@
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
},
"drugs": {
"weights": {
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
},
"stims": {
"weights": {
"0": 1
},
"whitelist": {}
"whitelist": []
},
"looseLoot": {
"weights": {
@@ -316,21 +316,21 @@
"3": 1,
"4": 1
},
"whitelist": {}
"whitelist": []
},
"magazines": {
"weights": {
"1": 2,
"2": 1
},
"whitelist": {}
"whitelist": []
},
"grenades": {
"weights": {
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
}
},
"lootItemsToAddChancePercent": {}
@@ -378,20 +378,20 @@
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
},
"drugs": {
"weights": {
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
},
"stims": {
"weights": {
"0": 1
},
"whitelist": {}
"whitelist": []
},
"looseLoot": {
"weights": {
@@ -401,21 +401,21 @@
"3": 1,
"4": 1
},
"whitelist": {}
"whitelist": []
},
"magazines": {
"weights": {
"1": 2,
"2": 1
},
"whitelist": {}
"whitelist": []
},
"grenades": {
"weights": {
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
}
},
"lootItemsToAddChancePercent": {}
@@ -463,20 +463,20 @@
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
},
"drugs": {
"weights": {
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
},
"stims": {
"weights": {
"0": 1
},
"whitelist": {}
"whitelist": []
},
"looseLoot": {
"weights": {
@@ -486,21 +486,21 @@
"3": 1,
"4": 1
},
"whitelist": {}
"whitelist": []
},
"magazines": {
"weights": {
"1": 2,
"2": 1
},
"whitelist": {}
"whitelist": []
},
"grenades": {
"weights": {
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
}
},
"lootItemsToAddChancePercent": {}
@@ -548,20 +548,20 @@
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
},
"drugs": {
"weights": {
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
},
"stims": {
"weights": {
"0": 1
},
"whitelist": {}
"whitelist": []
},
"looseLoot": {
"weights": {
@@ -571,21 +571,21 @@
"3": 1,
"4": 1
},
"whitelist": {}
"whitelist": []
},
"magazines": {
"weights": {
"1": 2,
"2": 1
},
"whitelist": {}
"whitelist": []
},
"grenades": {
"weights": {
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
}
},
"lootItemsToAddChancePercent": {}
@@ -633,20 +633,20 @@
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
},
"drugs": {
"weights": {
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
},
"stims": {
"weights": {
"0": 1
},
"whitelist": {}
"whitelist": []
},
"looseLoot": {
"weights": {
@@ -656,21 +656,21 @@
"3": 1,
"4": 1
},
"whitelist": {}
"whitelist": []
},
"magazines": {
"weights": {
"1": 1,
"2": 1
},
"whitelist": {}
"whitelist": []
},
"grenades": {
"weights": {
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
}
},
"lootItemsToAddChancePercent": {}
@@ -718,7 +718,7 @@
"0": 2,
"1": 1
},
"whitelist": {}
"whitelist": []
},
"drugs": {
"weights": {
@@ -726,13 +726,13 @@
"1": 1,
"2": 1
},
"whitelist": {}
"whitelist": []
},
"stims": {
"weights": {
"0": 1
},
"whitelist": {}
"whitelist": []
},
"looseLoot": {
"weights": {
@@ -741,7 +741,7 @@
"3": 1,
"4": 1
},
"whitelist": {}
"whitelist": []
},
"magazines": {
"weights": {
@@ -749,7 +749,7 @@
"2": 2,
"3": 1
},
"whitelist": {}
"whitelist": []
},
"grenades": {
"weights": {
@@ -757,7 +757,7 @@
"1": 1,
"2": 1
},
"whitelist": {}
"whitelist": []
}
},
"lootItemsToAddChancePercent": {
@@ -808,42 +808,42 @@
"1": 1,
"2": 2
},
"whitelist": {}
"whitelist": []
},
"drugs": {
"weights": {
"1": 1,
"2": 2
},
"whitelist": {}
"whitelist": []
},
"stims": {
"weights": {
"0": 1,
"1": 1
},
"whitelist": {}
"whitelist": []
},
"looseLoot": {
"weights": {
"2": 2,
"3": 1
},
"whitelist": {}
"whitelist": []
},
"magazines": {
"weights": {
"2": 2,
"3": 1
},
"whitelist": {}
"whitelist": []
},
"grenades": {
"weights": {
"1": 1,
"2": 1
},
"whitelist": {}
"whitelist": []
}
},
"lootItemsToAddChancePercent": {
@@ -895,21 +895,21 @@
"1": 1,
"2": 2
},
"whitelist": {}
"whitelist": []
},
"drugs": {
"weights": {
"1": 1,
"2": 2
},
"whitelist": {}
"whitelist": []
},
"stims": {
"weights": {
"1": 5,
"2": 1
},
"whitelist": {}
"whitelist": []
},
"looseLoot": {
"weights": {
@@ -918,7 +918,7 @@
"4": 1,
"5": 1
},
"whitelist": {}
"whitelist": []
},
"magazines": {
"weights": {
@@ -926,14 +926,14 @@
"3": 2,
"4": 1
},
"whitelist": {}
"whitelist": []
},
"grenades": {
"weights": {
"1": 1,
"2": 2
},
"whitelist": {}
"whitelist": []
}
},
"lootItemsToAddChancePercent": {
@@ -984,21 +984,21 @@
"weights": {
"2": 1
},
"whitelist": {}
"whitelist": []
},
"drugs": {
"weights": {
"2": 1,
"3": 2
},
"whitelist": {}
"whitelist": []
},
"stims": {
"weights": {
"1": 1,
"2": 1
},
"whitelist": {}
"whitelist": []
},
"looseLoot": {
"weights": {
@@ -1007,7 +1007,7 @@
"5": 1,
"6": 1
},
"whitelist": {}
"whitelist": []
},
"magazines": {
"weights": {
@@ -1015,14 +1015,14 @@
"3": 2,
"4": 1
},
"whitelist": {}
"whitelist": []
},
"grenades": {
"weights": {
"2": 1,
"3": 2
},
"whitelist": {}
"whitelist": []
}
},
"lootItemsToAddChancePercent": {
@@ -1074,21 +1074,21 @@
"2": 5,
"3": 1
},
"whitelist": {}
"whitelist": []
},
"drugs": {
"weights": {
"2": 1,
"3": 2
},
"whitelist": {}
"whitelist": []
},
"stims": {
"weights": {
"1": 1,
"2": 1
},
"whitelist": {}
"whitelist": []
},
"looseLoot": {
"weights": {
@@ -1097,21 +1097,21 @@
"5": 2,
"6": 1
},
"whitelist": {}
"whitelist": []
},
"magazines": {
"weights": {
"3": 2,
"4": 2
},
"whitelist": {}
"whitelist": []
},
"grenades": {
"weights": {
"2": 2,
"3": 1
},
"whitelist": {}
"whitelist": []
}
},
"lootItemsToAddChancePercent": {
@@ -1180,21 +1180,21 @@
"2": 1,
"3": 2
},
"whitelist": {}
"whitelist": []
},
"drugs": {
"weights": {
"3": 1,
"4": 2
},
"whitelist": {}
"whitelist": []
},
"stims": {
"weights": {
"2": 1,
"3": 2
},
"whitelist": {}
"whitelist": []
},
"looseLoot": {
"weights": {
@@ -1202,21 +1202,21 @@
"6": 2,
"7": 1
},
"whitelist": {}
"whitelist": []
},
"magazines": {
"weights": {
"3": 1,
"4": 4
},
"whitelist": {}
"whitelist": []
},
"grenades": {
"weights": {
"3": 1,
"4": 2
},
"whitelist": {}
"whitelist": []
}
},
"lootItemsToAddChancePercent": {
+61 -17
View File
@@ -151,9 +151,15 @@
"675dc9d37ae1a8792107ca96",
"675dcb0545b1a2d108011b2b",
"6707d13e4e617ec94f0e5631",
"67408903268737ef6908d432",
"67499b9b909d2013670a5029",
"6638a5474e92f038531e210e"
"67408903268737ef6908d432",
"67499b9b909d2013670a5029",
"6638a5474e92f038531e210e",
"66d9f8744827a77e870ecaf1",
"6707d0804e617ec94f0e562f",
"67449b6c89d5e1ddc603f504",
"6740987b89d5e1ddc603f4f0",
"6707d0bdaab679420007e01a",
"66d9f7e7099cf6adcc07a369"
],
"useDifficultyOverride": false,
"difficulty": "AsOnline",
@@ -717,6 +723,40 @@
"value": 2500000
}
],
"lootItemLimitsRub": [
{
"min": 46,
"max": 64,
"backpack": {
"min": 5000,
"max": 0
},
"pocket": {
"min": 5000,
"max": 0
},
"vest": {
"min": 5000,
"max": 0
}
},
{
"min": 65,
"max": 100,
"backpack": {
"min": 10000,
"max": 0
},
"pocket": {
"min": 10000,
"max": 0
},
"vest": {
"min": 10000,
"max": 0
}
}
],
"maxPocketLootTotalRub": 50000,
"maxVestLootTotalRub": 50000,
"convertIntoPmcChance": {
@@ -726,8 +766,8 @@
"max": 35
},
"cursedassault": {
"min": 15,
"max": 30
"min": 0,
"max": 0
},
"pmcbot": {
"min": 5,
@@ -772,8 +812,8 @@
"hostilitySettings": {
"pmcusec": {
"additionalEnemyTypes": [
"arenaFighterEvent",
"marksman",
"arenaFighterEvent",
"marksman",
"peacemaker",
"skier",
"ravangeZryachiyEvent",
@@ -796,14 +836,16 @@
"followerBigPipe",
"followerBirdEye",
"followerBully",
"followerBoarClose1",
"followerBoarClose2"
"followerBoarClose1",
"followerBoarClose2"
],
"additionalFriendlyTypes": ["gifter", "shooterBTR", "sectactPriestEvent", "peacefullZryachiyEvent"],
"chancedEnemies": [{
"chancedEnemies": [
{
"EnemyChance": 85,
"Role": "pmcUSEC"
}, {
},
{
"EnemyChance": 100,
"Role": "pmcBEAR"
}
@@ -815,8 +857,8 @@
},
"pmcbear": {
"additionalEnemyTypes": [
"arenaFighterEvent",
"marksman",
"arenaFighterEvent",
"marksman",
"peacemaker",
"skier",
"ravangeZryachiyEvent",
@@ -839,14 +881,16 @@
"followerBigPipe",
"followerBirdEye",
"followerBully",
"followerBoarClose1",
"followerBoarClose2"
"followerBoarClose1",
"followerBoarClose2"
],
"additionalFriendlyTypes": ["gifter", "shooterBTR", "sectactPriestEvent", "peacefullZryachiyEvent"],
"chancedEnemies": [{
"chancedEnemies": [
{
"EnemyChance": 85,
"Role": "pmcBEAR"
}, {
},
{
"EnemyChance": 100,
"Role": "pmcUSEC"
}
+2 -2
View File
@@ -16,8 +16,8 @@
"5e381b0286f77420e3417a74",
"5e4d4ac186f774264f758336",
"639136d68ba6894d155e77cf",
"6613f3007f6666d56807c929",
"6613f307fca4f2f386029409"
"6613f3007f6666d56807c929",
"6613f307fca4f2f386029409"
],
"profileBlacklist": {
"unheard_edition": ["666314a50aa5c7436c00908a"]
+19 -7
View File
@@ -147,6 +147,18 @@
"max": 1
}
},
"5c164d2286f774194c5e69fa": {
"_name": "KEYCARD",
"conditionChance": 0.04,
"current": {
"min": 0,
"max": 1
},
"max": {
"min": 0.25,
"max": 1
}
},
"5448e5284bdc2dcb718b4567": {
"_name": "VEST",
"conditionChance": 0.2,
@@ -337,13 +349,13 @@
"newPriceHandbookMultiplier": 11
}
},
"itemPriceOverrideRouble":{
"66bc98a01a47be227a5e956e": 500000,
"63a8970d7108f713591149f5": 50000,
"63a898a328e385334e0640a5": 100000,
"63a897c6b1ff6e29734fcc95": 200000,
"674d91ce6e862d5a95059ed6": 250000
}
"itemPriceOverrideRouble": {
"66bc98a01a47be227a5e956e": 500000,
"63a8970d7108f713591149f5": 50000,
"63a898a328e385334e0640a5": 100000,
"63a897c6b1ff6e29734fcc95": 200000,
"674d91ce6e862d5a95059ed6": 250000
}
},
"tieredFlea": {
"enabled": false,
+44 -44
View File
@@ -98,13 +98,13 @@
"5a43957686f7742a2c2f11b0": 150
}
},
"bosskilla": {
"bosskilla": {
"ArmorVest": {
"674d91ce6e862d5a95059ed6": 200
},
"Headwear": {
"6759af0f9c8a538dd70bfae6": 200
}
"Headwear": {
"6759af0f9c8a538dd70bfae6": 200
}
},
"bossknight": {
"FaceCover": {
@@ -242,7 +242,7 @@
"bear": {
"FaceCover": {
"5c1a1e3f2e221602b66cc4c2": 30,
"675ac888803644528007b3f6": 30
"675ac888803644528007b3f6": 30
},
"Headwear": {
"5a43957686f7742a2c2f11b0": 80,
@@ -252,7 +252,7 @@
"usec": {
"FaceCover": {
"5c1a1e3f2e221602b66cc4c2": 30,
"675ac888803644528007b3f6": 30
"675ac888803644528007b3f6": 30
},
"Headwear": {
"5a43957686f7742a2c2f11b0": 80,
@@ -261,28 +261,28 @@
}
}
},
"eventLoot": {
"christmas": {
"assault": {
"Pockets": {
"5df8a6a186f77412640e2e80": 2200,
"5df8a72c86f77412640e2e83": 2200,
"5df8a77486f77412672a1e3f": 2200
},
"Backpack": {
"5df8a6a186f77412640e2e80": 2200,
"5df8a72c86f77412640e2e83": 2200,
"5df8a77486f77412672a1e3f": 2200,
"63a8970d7108f713591149f5": 250
},
"TacticalVest": {
"5df8a6a186f77412640e2e80": 2200,
"5df8a72c86f77412640e2e83": 2200,
"5df8a77486f77412672a1e3f": 2200
}
}
}
},
"eventLoot": {
"christmas": {
"assault": {
"Pockets": {
"5df8a6a186f77412640e2e80": 2200,
"5df8a72c86f77412640e2e83": 2200,
"5df8a77486f77412672a1e3f": 2200
},
"Backpack": {
"5df8a6a186f77412640e2e80": 2200,
"5df8a72c86f77412640e2e83": 2200,
"5df8a77486f77412672a1e3f": 2200,
"63a8970d7108f713591149f5": 250
},
"TacticalVest": {
"5df8a6a186f77412640e2e80": 2200,
"5df8a72c86f77412640e2e83": 2200,
"5df8a77486f77412672a1e3f": 2200
}
}
}
},
"eventBotMapping": {
"peacefullZryachiyEvent": "bossZryachiy",
"sectactPriestEvent": "sectantPriest",
@@ -8363,9 +8363,9 @@
"endDay": "31",
"endMonth": "12",
"settings": {
"enableChristmasHideout": true,
"enableChristmasHideout": true,
"enableSanta": true,
"adjustBotAppearances": true
"adjustBotAppearances": true
}
},
{
@@ -8376,24 +8376,24 @@
"startMonth": "1",
"endDay": "1",
"endMonth": "1",
"settings": {
"enableChristmasHideout": true,
"settings": {
"enableChristmasHideout": true,
"enableSanta": true,
"adjustBotAppearances": true
"adjustBotAppearances": true
}
},
{
{
"enabled": true,
"name": "christmas January",
"type": "CHRISTMAS",
"startDay": "2",
"startMonth": "1",
"endDay": "7",
"endDay": "14",
"endMonth": "1",
"settings": {
"enableChristmasHideout": true,
"enableChristmasHideout": true,
"enableSanta": true,
"adjustBotAppearances": true
"adjustBotAppearances": true
}
},
{
@@ -12314,11 +12314,11 @@
]
}
},
"christmasContainerIds": [
"container_custom_DesignStuff_00427",
"container_Shopping_Mall_DesignStuff_00808",
"container_Lighthouse_DesignStuff_00001",
"container_shoreline_DesignStuff_00418",
"container_woods_design_stuff_00328"
]
"christmasContainerIds": [
"container_custom_DesignStuff_00427",
"container_Shopping_Mall_DesignStuff_00808",
"container_Lighthouse_DesignStuff_00001",
"container_shoreline_DesignStuff_00418",
"container_woods_design_stuff_00328"
]
}
+1 -1
View File
@@ -365,7 +365,7 @@
"65ddcc9cfa85b9f17d0dfb07",
"660312cc4d6cdfa6f500c703",
"6655e35b6bc645cb7b059912",
"6759673c76e93d8eb20b2080"
"6759673c76e93d8eb20b2080"
],
"coopExtractGift": {
"sendGift": true,
+16 -8
View File
@@ -97,56 +97,64 @@
"weights": [1, 2]
}
},
"seasonDates": [{
"seasonDates": [
{
"seasonType": 0,
"name": "SUMMER",
"startDay": "2",
"startMonth": "6",
"endDay": "15",
"endMonth": "10"
}, {
},
{
"seasonType": 1,
"name": "AUTUMN",
"startDay": "15",
"startMonth": "10",
"endDay": "1",
"endMonth": "11"
}, {
},
{
"seasonType": 4,
"name": "AUTUMN_LATE",
"startDay": "1",
"startMonth": "11",
"endDay": "21",
"endMonth": "12"
}, {
},
{
"seasonType": 2,
"name": "WINTER_START",
"startDay": "21",
"startMonth": "12",
"endDay": "31",
"endMonth": "12"
}, {
},
{
"seasonType": 2,
"name": "WINTER_END",
"startDay": "1",
"startMonth": "1",
"endDay": "9",
"endMonth": "1"
}, {
},
{
"seasonType": 5,
"name": "SPRING_EARLY",
"startDay": "9",
"startMonth": "1",
"endDay": "25",
"endMonth": "3"
}, {
},
{
"seasonType": 3,
"name": "SPRING",
"startDay": "25",
"startMonth": "3",
"endDay": "2",
"endMonth": "6"
}, {
},
{
"seasonType": 4,
"name": "STORM",
"startDay": "24",
+1 -3
View File
@@ -76,9 +76,7 @@
"DamageHistory": {
"LethalDamagePart": "Head",
"LethalDamage": null,
"BodyParts": {
"Head": []
}
"BodyParts": []
},
"LastPlayerState": null,
"TotalInGameTime": 0,
+35
View File
@@ -34670,6 +34670,41 @@
"MaxInLobby": 0,
"MaxInRaid": 0,
"TemplateId": "676aa450fe1fc45172014df2"
},
{
"MaxInLobby": 0,
"MaxInRaid": 5,
"TemplateId": "5b7c710788a4506dec015957"
},
{
"MaxInLobby": 0,
"MaxInRaid": 5,
"TemplateId": "5c093db286f7740a1b2617e3"
},
{
"MaxInLobby": 0,
"MaxInRaid": 5,
"TemplateId": "5c0a840b86f7742ffa4f2482"
},
{
"MaxInLobby": 0,
"MaxInRaid": 5,
"TemplateId": "5aafbcd986f7745e590fff23"
},
{
"MaxInLobby": 0,
"MaxInRaid": 5,
"TemplateId": "59fb016586f7746d0d4b423a"
},
{
"MaxInLobby": 0,
"MaxInRaid": 5,
"TemplateId": "59fb042886f7746c5005a7b2"
},
{
"MaxInLobby": 0,
"MaxInRaid": 5,
"TemplateId": "5b6d9ce188a4501afc1b2b25"
}
],
"RunddansSettings": {
+2 -2
View File
@@ -3355,7 +3355,7 @@
"count": 1,
"isEncoded": false,
"isFunctional": false,
"isSpawnedInSession": true,
"isSpawnedInSession": false,
"templateId": "6389c7f115805221fb410466",
"type": "Item"
}
@@ -4626,7 +4626,7 @@
"count": 1,
"isEncoded": false,
"isFunctional": false,
"isSpawnedInSession": true,
"isSpawnedInSession": false,
"templateId": "6389c85357baa773a825b356",
"type": "Item"
}
File diff suppressed because it is too large Load Diff
@@ -20273,6 +20273,7 @@
"672dd5b83804f9bc8e0a610e 0": "Survey reward",
"676ae11cf444b79e7306745f 0": "Items that were saved when achieving the new level of prestige",
"676bc75c4859905179061aff 0": "Prestige rewards",
"6776e324810eb26b880fb4a5 0": "They say tools are in short supply these days, even OLI can't save the day. Good thing I ordered those tape measures in bulk back then. Here, take this — Ill help you out now, and well settle up later, one way or another.",
"Arena/UI/Match_leaving_warning_body 0": "If you leave the match, you'll put your allies at disadvantage./nYou'll lose your reward and rating and could receive a temporary ban.",
"Arena/UI/Match_leaving_warning_header 0": "Warning! You are leaving the match.",
"5fc615710b735e7b024c76ed Name": "Boss sanitar",
@@ -20273,6 +20273,7 @@
"672dd5b83804f9bc8e0a610e 0": "Survey reward",
"676ae11cf444b79e7306745f 0": "Items that were saved when achieving the new level of prestige",
"676bc75c4859905179061aff 0": "Prestige rewards",
"6776e324810eb26b880fb4a5 0": "They say tools are in short supply these days, even OLI can't save the day. Good thing I ordered those tape measures in bulk back then. Here, take this — Ill help you out now, and well settle up later, one way or another.",
"Arena/UI/Match_leaving_warning_body 0": "If you leave the match, you'll put your allies at disadvantage./nYou'll lose your reward and rating and could receive a temporary ban.",
"Arena/UI/Match_leaving_warning_header 0": "Warning! You are leaving the match.",
"5fc615710b735e7b024c76ed Name": "Boss sanitar",
@@ -20273,6 +20273,7 @@
"672dd5b83804f9bc8e0a610e 0": "Survey reward",
"676ae11cf444b79e7306745f 0": "Items that were saved when achieving the new level of prestige",
"676bc75c4859905179061aff 0": "Prestige rewards",
"6776e324810eb26b880fb4a5 0": "They say tools are in short supply these days, even OLI can't save the day. Good thing I ordered those tape measures in bulk back then. Here, take this — Ill help you out now, and well settle up later, one way or another.",
"Arena/UI/Match_leaving_warning_body 0": "If you leave the match, you'll put your allies at disadvantage./nYou'll lose your reward and rating and could receive a temporary ban.",
"Arena/UI/Match_leaving_warning_header 0": "Warning! You are leaving the match.",
"5fc615710b735e7b024c76ed Name": "Boss sanitar",
@@ -20273,6 +20273,7 @@
"672dd5b83804f9bc8e0a610e 0": "Survey reward",
"676ae11cf444b79e7306745f 0": "Items that were saved when achieving the new level of prestige",
"676bc75c4859905179061aff 0": "Prestige rewards",
"6776e324810eb26b880fb4a5 0": "They say tools are in short supply these days, even OLI can't save the day. Good thing I ordered those tape measures in bulk back then. Here, take this — Ill help you out now, and well settle up later, one way or another.",
"Arena/UI/Match_leaving_warning_body 0": "If you leave the match, you'll put your allies at disadvantage./nYou'll lose your reward and rating and could receive a temporary ban.",
"Arena/UI/Match_leaving_warning_header 0": "Warning! You are leaving the match.",
"5fc615710b735e7b024c76ed Name": "Boss sanitar",
@@ -20273,6 +20273,7 @@
"672dd5b83804f9bc8e0a610e 0": "Survey reward",
"676ae11cf444b79e7306745f 0": "Items that were saved when achieving the new level of prestige",
"676bc75c4859905179061aff 0": "Prestige rewards",
"6776e324810eb26b880fb4a5 0": "They say tools are in short supply these days, even OLI can't save the day. Good thing I ordered those tape measures in bulk back then. Here, take this — Ill help you out now, and well settle up later, one way or another.",
"Arena/UI/Match_leaving_warning_body 0": "If you leave the match, you'll put your allies at disadvantage./nYou'll lose your reward and rating and could receive a temporary ban.",
"Arena/UI/Match_leaving_warning_header 0": "Warning! You are leaving the match.",
"5fc615710b735e7b024c76ed Name": "Boss sanitar",
@@ -20273,6 +20273,7 @@
"672dd5b83804f9bc8e0a610e 0": "Survey reward",
"676ae11cf444b79e7306745f 0": "Items that were saved when achieving the new level of prestige",
"676bc75c4859905179061aff 0": "Prestige rewards",
"6776e324810eb26b880fb4a5 0": "They say tools are in short supply these days, even OLI can't save the day. Good thing I ordered those tape measures in bulk back then. Here, take this — Ill help you out now, and well settle up later, one way or another.",
"Arena/UI/Match_leaving_warning_body 0": "If you leave the match, you'll put your allies at disadvantage./nYou'll lose your reward and rating and could receive a temporary ban.",
"Arena/UI/Match_leaving_warning_header 0": "Warning! You are leaving the match.",
"5fc615710b735e7b024c76ed Name": "Boss sanitar",
@@ -20273,6 +20273,7 @@
"672dd5b83804f9bc8e0a610e 0": "Survey reward",
"676ae11cf444b79e7306745f 0": "Items that were saved when achieving the new level of prestige",
"676bc75c4859905179061aff 0": "Prestige rewards",
"6776e324810eb26b880fb4a5 0": "They say tools are in short supply these days, even OLI can't save the day. Good thing I ordered those tape measures in bulk back then. Here, take this — Ill help you out now, and well settle up later, one way or another.",
"Arena/UI/Match_leaving_warning_body 0": "If you leave the match, you'll put your allies at disadvantage./nYou'll lose your reward and rating and could receive a temporary ban.",
"Arena/UI/Match_leaving_warning_header 0": "Warning! You are leaving the match.",
"5fc615710b735e7b024c76ed Name": "Boss sanitar",
@@ -20273,6 +20273,7 @@
"672dd5b83804f9bc8e0a610e 0": "Survey reward",
"676ae11cf444b79e7306745f 0": "Items that were saved when achieving the new level of prestige",
"676bc75c4859905179061aff 0": "Prestige rewards",
"6776e324810eb26b880fb4a5 0": "They say tools are in short supply these days, even OLI can't save the day. Good thing I ordered those tape measures in bulk back then. Here, take this — Ill help you out now, and well settle up later, one way or another.",
"Arena/UI/Match_leaving_warning_body 0": "If you leave the match, you'll put your allies at disadvantage./nYou'll lose your reward and rating and could receive a temporary ban.",
"Arena/UI/Match_leaving_warning_header 0": "Warning! You are leaving the match.",
"5fc615710b735e7b024c76ed Name": "Boss sanitar",
@@ -20273,6 +20273,7 @@
"672dd5b83804f9bc8e0a610e 0": "Survey reward",
"676ae11cf444b79e7306745f 0": "Items that were saved when achieving the new level of prestige",
"676bc75c4859905179061aff 0": "Prestige rewards",
"6776e324810eb26b880fb4a5 0": "They say tools are in short supply these days, even OLI can't save the day. Good thing I ordered those tape measures in bulk back then. Here, take this — Ill help you out now, and well settle up later, one way or another.",
"Arena/UI/Match_leaving_warning_body 0": "If you leave the match, you'll put your allies at disadvantage./nYou'll lose your reward and rating and could receive a temporary ban.",
"Arena/UI/Match_leaving_warning_header 0": "Warning! You are leaving the match.",
"5fc615710b735e7b024c76ed Name": "Boss sanitar",
@@ -20273,6 +20273,7 @@
"672dd5b83804f9bc8e0a610e 0": "Survey reward",
"676ae11cf444b79e7306745f 0": "Items that were saved when achieving the new level of prestige",
"676bc75c4859905179061aff 0": "Prestige rewards",
"6776e324810eb26b880fb4a5 0": "They say tools are in short supply these days, even OLI can't save the day. Good thing I ordered those tape measures in bulk back then. Here, take this — Ill help you out now, and well settle up later, one way or another.",
"Arena/UI/Match_leaving_warning_body 0": "If you leave the match, you'll put your allies at disadvantage./nYou'll lose your reward and rating and could receive a temporary ban.",
"Arena/UI/Match_leaving_warning_header 0": "Warning! You are leaving the match.",
"5fc615710b735e7b024c76ed Name": "Boss sanitar",
@@ -20273,6 +20273,7 @@
"672dd5b83804f9bc8e0a610e 0": "Survey reward",
"676ae11cf444b79e7306745f 0": "Items that were saved when achieving the new level of prestige",
"676bc75c4859905179061aff 0": "Prestige rewards",
"6776e324810eb26b880fb4a5 0": "They say tools are in short supply these days, even OLI can't save the day. Good thing I ordered those tape measures in bulk back then. Here, take this — Ill help you out now, and well settle up later, one way or another.",
"Arena/UI/Match_leaving_warning_body 0": "If you leave the match, you'll put your allies at disadvantage./nYou'll lose your reward and rating and could receive a temporary ban.",
"Arena/UI/Match_leaving_warning_header 0": "Warning! You are leaving the match.",
"5fc615710b735e7b024c76ed Name": "Boss sanitar",
@@ -20273,6 +20273,7 @@
"672dd5b83804f9bc8e0a610e 0": "Survey reward",
"676ae11cf444b79e7306745f 0": "Items that were saved when achieving the new level of prestige",
"676bc75c4859905179061aff 0": "Prestige rewards",
"6776e324810eb26b880fb4a5 0": "They say tools are in short supply these days, even OLI can't save the day. Good thing I ordered those tape measures in bulk back then. Here, take this — Ill help you out now, and well settle up later, one way or another.",
"Arena/UI/Match_leaving_warning_body 0": "If you leave the match, you'll put your allies at disadvantage./nYou'll lose your reward and rating and could receive a temporary ban.",
"Arena/UI/Match_leaving_warning_header 0": "Warning! You are leaving the match.",
"5fc615710b735e7b024c76ed Name": "Boss sanitar",
@@ -20273,6 +20273,7 @@
"672dd5b83804f9bc8e0a610e 0": "Survey reward",
"676ae11cf444b79e7306745f 0": "Items that were saved when achieving the new level of prestige",
"676bc75c4859905179061aff 0": "Prestige rewards",
"6776e324810eb26b880fb4a5 0": "They say tools are in short supply these days, even OLI can't save the day. Good thing I ordered those tape measures in bulk back then. Here, take this — Ill help you out now, and well settle up later, one way or another.",
"Arena/UI/Match_leaving_warning_body 0": "If you leave the match, you'll put your allies at disadvantage./nYou'll lose your reward and rating and could receive a temporary ban.",
"Arena/UI/Match_leaving_warning_header 0": "Warning! You are leaving the match.",
"5fc615710b735e7b024c76ed Name": "Boss sanitar",
@@ -20273,6 +20273,7 @@
"672dd5b83804f9bc8e0a610e 0": "Survey reward",
"676ae11cf444b79e7306745f 0": "Items that were saved when achieving the new level of prestige",
"676bc75c4859905179061aff 0": "Prestige rewards",
"6776e324810eb26b880fb4a5 0": "They say tools are in short supply these days, even OLI can't save the day. Good thing I ordered those tape measures in bulk back then. Here, take this — Ill help you out now, and well settle up later, one way or another.",
"Arena/UI/Match_leaving_warning_body 0": "If you leave the match, you'll put your allies at disadvantage./nYou'll lose your reward and rating and could receive a temporary ban.",
"Arena/UI/Match_leaving_warning_header 0": "Warning! You are leaving the match.",
"5fc615710b735e7b024c76ed Name": "Boss sanitar",
@@ -20273,6 +20273,7 @@
"672dd5b83804f9bc8e0a610e 0": "Награда за прохождение опроса",
"676ae11cf444b79e7306745f 0": "Предметы сохраненные при переходе на новый уровень престижа",
"676bc75c4859905179061aff 0": "Письмо с наградами за престиж",
"6776e324810eb26b880fb4a5 0": "Говорят, что с инструментами сейчас совсем туго стало, даже OLI не спасает. Правильно я сделал, что оптом тогда эти рулетки заказал. Держи, сейчас я тебе помогу, а сочтёмся как-нибудь потом.",
"Arena/UI/Match_leaving_warning_body 0": "Если вы покинете матч, то вы подставляете своих союзников в невыгодное положение./nВы потеряете награду и рейтинг, а также можете получить временный бан.",
"Arena/UI/Match_leaving_warning_header 0": "Внимание! Вы покидаете матч.",
"5fc615710b735e7b024c76ed Name": "Boss sanitar",
@@ -27978,7 +27979,7 @@
"675c044e3691199fe911a641": "Найти и пометить четвертый особый груз TerraGroup Маркером MS2000 на локации Таможня",
"675c3fbeb402d4fa5589516f": "Найти и пометить пятый особый груз TerraGroup Маркером MS2000 на локации Таможня",
"675c3fd3a2c0bad5f70af01c": "Найти и пометить шестой особый груз TerraGroup Маркером MS2000 на локации Таможня",
"675c3fdd5af984e99db7b4e1": "Найти и пометить седьмрй особый груз TerraGroup Маркером MS2000 на локации Таможня",
"675c3fdd5af984e99db7b4e1": "Найти и пометить седьмой особый груз TerraGroup Маркером MS2000 на локации Таможня",
"675c03d1f7da9792a405549a acceptPlayerMessage": "",
"675c03d1f7da9792a405549a declinePlayerMessage": "",
"675c03d1f7da9792a405549a completePlayerMessage": "",
@@ -20273,6 +20273,7 @@
"672dd5b83804f9bc8e0a610e 0": "Survey reward",
"676ae11cf444b79e7306745f 0": "Items that were saved when achieving the new level of prestige",
"676bc75c4859905179061aff 0": "Prestige rewards",
"6776e324810eb26b880fb4a5 0": "They say tools are in short supply these days, even OLI can't save the day. Good thing I ordered those tape measures in bulk back then. Here, take this — Ill help you out now, and well settle up later, one way or another.",
"Arena/UI/Match_leaving_warning_body 0": "If you leave the match, you'll put your allies at disadvantage./nYou'll lose your reward and rating and could receive a temporary ban.",
"Arena/UI/Match_leaving_warning_header 0": "Warning! You are leaving the match.",
"5fc615710b735e7b024c76ed Name": "Boss sanitar",
@@ -20273,6 +20273,7 @@
"672dd5b83804f9bc8e0a610e 0": "Survey reward",
"676ae11cf444b79e7306745f 0": "Items that were saved when achieving the new level of prestige",
"676bc75c4859905179061aff 0": "Prestige rewards",
"6776e324810eb26b880fb4a5 0": "They say tools are in short supply these days, even OLI can't save the day. Good thing I ordered those tape measures in bulk back then. Here, take this — Ill help you out now, and well settle up later, one way or another.",
"Arena/UI/Match_leaving_warning_body 0": "If you leave the match, you'll put your allies at disadvantage./nYou'll lose your reward and rating and could receive a temporary ban.",
"Arena/UI/Match_leaving_warning_header 0": "Warning! You are leaving the match.",
"5fc615710b735e7b024c76ed Name": "Boss sanitar",
@@ -131,8 +131,6 @@
"modloader-error_parsing_mod_load_order": "حدث خطأ أثناء تحليل ترتيب المودات.\n\n\n\n\n\n\n",
"modloader-incompatibilities_not_string_array": "يجب أن يكون Mod %s package.json الخاصية 'incompatibilities' مصفوفة سلاسل",
"modloader-incompatible_mod_found": "المود {{author}}-{{name}} غير متوافق مع {{incompatibleModName}}.\n\n\n\n\n\n\n",
"modloader-installing_external_dependencies": "تثبيت الاعتمادات للإضافة: {{name}} بواسطة: {{author}}",
"modloader-installing_external_dependencies_disabled": "الإضافة: {{name}}: {{author}} تتطلب اعتمادات خارجية ولكن الميزة معطلة حاليًا، اذهب إلى \"{{configPath}}\", اضبط \"{{configOption}}\" إلى \"صحيح\" و أعد تشغيل الخادم.\nبتمكينك هذا فإنك تتحمل كل المسؤولية عن ما سيتم تحميله من قبل {{name}} إلى جهازك.",
"modloader-invalid_version_property": "ملف package.json للمود %s يحتوي على سلسلة إصدار غير صالحة.\n\n\n\n\n\n\n",
"modloader-is_client_mod": "المود (%s) هو مود خاص بالعميل ويجب وضعه في المجلد التالي: /spt/bepinex/plugins\n\n\n\n\n\n\n",
"modloader-load_order_conflict": "{{modOneName}} و {{modTwoName}} لديهما متطلبات تعارض في ترتيب التحميل، لا يمكن للخادم أن يبدأ حتى يتم حل هذا وسيتم إيقاف التشغيل.\n\n\n\n\n\n\n",
@@ -143,8 +143,6 @@
"modloader-error_parsing_mod_load_order": "Chyba parsování načítání pořadí módů",
"modloader-incompatibilities_not_string_array": "Vlastnost módu %s package.json 'incompatibility' by měla být pole pro řetězec",
"modloader-incompatible_mod_found": "Modifikace {{author}}-{{name}} je nekompatibilní s {{incompatibleModName}}",
"modloader-installing_external_dependencies": "Instalace závislostí pro mód: {{name}} od autora: {{author}}",
"modloader-installing_external_dependencies_disabled": "Mód: {{name}} od: {{author}} vyžaduje externí závislosti, ale funkce je momentálně zakázána, přejděte na \"{{configPath}}\", nastavte \"{{configOption}}\" na true a restartujte server.\nPovolením této funkce přijímáte veškerou odpovědnost za to, co {{name}} stáhne do vašeho počítače.",
"modloader-invalid_version_property": "Mód %s package.json obsahuje neplatnou verzi řetězce",
"modloader-is_client_mod": "Mód (%s) je klientský mód a měl by být umístěn v následující složce: /spt/bepinex/plugins",
"modloader-load_order_conflict": "`{{modOneName}}` a `{{modTwoName}}` mají protichůdné požadavky na pořadí načítání, server není schopen spustit, dokud nebude opraven a nebude vypnut",
@@ -591,7 +589,7 @@
"watermark-discord_url": "https://discord.sp-tarkov.com",
"watermark-do_not_report": "NENAHLAŠUJ TO",
"watermark-free_of_charge": "Tento produkt je zdarma",
"watermark-issue_tracker_url": "https://dev.sp-tarkov.com/SPT/Server/issues",
"watermark-issue_tracker_url": "https://github.com/sp-tarkov/server/",
"watermark-modding_disabled": "TATO VERZE MÁ ZÁKÁZÁNO MÓDOVÁNÍ SERVERU",
"watermark-no_support": "ŽÁDNÁ PODPORA NEBUDE POSKYTNUTA",
"watermark-not_an_issue": "TOTO NENÍ CHYBA",
@@ -199,8 +199,6 @@
"modloader-error_parsing_mod_load_order": "Fehler beim Parsen der Mod-Ladereihenfolge",
"modloader-incompatibilities_not_string_array": "Die Mod %s package.json Eigenschaft 'Inkompatibilitäten' sollte ein String-Array sein",
"modloader-incompatible_mod_found": "Mod {{author}}-{{name}} ist nicht kompatibel mit {{incompatibleModName}}",
"modloader-installing_external_dependencies": "Abhängigkeiten für Mod: {{name}} von: {{author}} werden installieren",
"modloader-installing_external_dependencies_disabled": "Mod: {{name}} von: {{author}} erfordert externe Abhängigkeiten, aber die Funktion ist derzeit deaktiviert. Gehe zu '{{configPath}}', setze '{{configOption}}' auf 'true' und starte den Server neu.\nWenn du dies aktivierst, übernimmst du die volle Verantwortung dafür, was {{name}} auf deinen Computer herunterlädt.",
"modloader-invalid_sptVersion_field": "Mod: %s enthält eine ungültige ´semver´ Zeichenkette im ´sptversions´ Feld. Beispiele für gültige Werte: https://github.com/npm/node-semver#versions",
"modloader-invalid_version_property": "Mod %s package.json enthält eine ungültige Versionszeichenfolge",
"modloader-is_client_mod": "Mod (%s) ist ein Client-Mod und sollte im folgenden Ordner abgelegt werden: /spt/bepinex/plugins",
@@ -708,7 +706,7 @@
"watermark-discord_url": "https://discord.sp-tarkov.com",
"watermark-do_not_report": "MELDEN UNTERSAGT",
"watermark-free_of_charge": "Diese Arbeit ist kostenfrei",
"watermark-issue_tracker_url": "https://dev.sp-tarkov.com/SPT/Server/issues",
"watermark-issue_tracker_url": "https://github.com/sp-tarkov/server/",
"watermark-modding_disabled": "BEI DIESEM BUILD IST DAS SERVER-MODDING DEAKTIVIERT",
"watermark-no_support": "ES WIRD KEIN SUPPORT GEWÄHRT",
"watermark-not_an_issue": "DAS IST KEIN PROBLEM",

Some files were not shown because too many files have changed in this diff Show More