This commit is contained in:
Alex
2025-01-26 19:50:01 +00:00
152 changed files with 40495 additions and 19760 deletions
-1
View File
@@ -23,7 +23,6 @@ public class BotCallbacks(
/// <param name="info"></param>
/// <param name="sessionID"></param>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public string GetBotLimit(string url, EmptyRequestData info, string sessionID)
{
var splitUrl = url.Split('/');
+1 -2
View File
@@ -165,8 +165,7 @@ public class HideoutCallbacks(
{
if (timeSinceLastRun > _hideoutConfig.RunIntervalSeconds)
{
// TODO
// _hideoutController.Update();
_hideoutController.Update();
return true;
}
@@ -48,9 +48,9 @@ public class ItemEventCallbacks(HttpResponseUtil _httpResponseUtil, ItemEventRou
public int GetErrorCode(List<Warning> warnings)
{
// TODO: dont think this actually works
// Cast int to string to get the error code of 220 for Unknown Error.
return int.Parse((warnings[0].Code is null || warnings[0].Code == "None"
? (BackendErrorCodes.UnknownError).ToString()
? ((int) BackendErrorCodes.UnknownError).ToString()
: warnings.FirstOrDefault()?.Code) ?? string.Empty);
}
}
@@ -303,6 +303,7 @@ public class MatchCallbacks(
/// <returns></returns>
public string GetRaidConfiguration(string url, GetRaidConfigurationRequestData info, string sessionID)
{
_matchController.ConfigureOfflineRaid(info, sessionID);
return _httpResponseUtil.NullResponse();
}
+20 -29
View File
@@ -132,15 +132,13 @@ public class BotController(
public List<BotBase> Generate(string sessionId, GenerateBotsRequestData info)
{
// var pmcProfile = _profileHelper.GetPmcProfile(sessionId);
//
// // Use this opportunity to create and cache bots for later retrieval
// var multipleBotTypesRequested = info.Conditions?.Count > 1;
// return multipleBotTypesRequested
// ? GenerateMultipleBotsAndCache(info, pmcProfile, sessionId)
// : ReturnSingleBotFromCache(sessionId, info);
return new List<BotBase>();
var pmcProfile = _profileHelper.GetPmcProfile(sessionId);
// Use this opportunity to create and cache bots for later retrieval
var multipleBotTypesRequested = info.Conditions?.Count > 1;
return multipleBotTypesRequested
? GenerateMultipleBotsAndCache(info, pmcProfile, sessionId)
: ReturnSingleBotFromCache(sessionId, info);
}
private List<BotBase> GenerateMultipleBotsAndCache(GenerateBotsRequestData request, PmcData? pmcProfile, string sessionId)
@@ -205,16 +203,16 @@ public class BotController(
for (var i = 0; i < botsToGenerate; i++)
{
try
{
// 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}");
}
// }
// catch (Exception e)
// {
// _logger.Error($"Failed to generate bot #{i + 1}: {e.Message}");
// }
}
_logger.Debug(
@@ -229,12 +227,6 @@ public class BotController(
var requestedBot = request.Conditions?.FirstOrDefault();
var raidSettings = GetMostRecentRaidSettings();
if (raidSettings is null)
{
_logger.Error($"Unable to get raid settings for session {sessionId}");
return [];
}
// Create generation request for when cache is empty
var condition = new GenerateCondition
@@ -266,7 +258,7 @@ public class BotController(
// Does non pmc bot have a chance of being converted into a pmc
var convertIntoPmcChanceMinMax = GetPmcConversionMinMaxForLocation(
requestedBot?.Role,
raidSettings.Location
raidSettings?.Location
);
if (convertIntoPmcChanceMinMax is not null && !botGenerationDetails.IsPmc.GetValueOrDefault(false))
{
@@ -356,10 +348,9 @@ public class BotController(
private MinMax? GetPmcConversionMinMaxForLocation(string? requestedBotRole, string? location)
{
var mapSpecificConversionValues = _pmcConfig.ConvertIntoPmcChance!.GetValueOrDefault(location?.ToLower(), null);
return mapSpecificConversionValues is null
? _pmcConfig.ConvertIntoPmcChance.GetByJsonProp<Dictionary<string, MinMax>>("default").GetByJsonProp<MinMax>(requestedBotRole)
: mapSpecificConversionValues.GetByJsonProp<MinMax>(requestedBotRole?.ToLower());
return _pmcConfig.ConvertIntoPmcChance!.TryGetValue(location?.ToLower() ?? "", out var mapSpecificConversionValues)
? mapSpecificConversionValues.GetByJsonProp<MinMax>(requestedBotRole?.ToLower())
: _pmcConfig.ConvertIntoPmcChance.GetValueOrDefault("default")?.GetValueOrDefault(requestedBotRole);
}
private GetRaidConfigurationRequestData? GetMostRecentRaidSettings()
@@ -408,7 +399,7 @@ public class BotController(
public int GetBotCap(string location)
{
var botCap = _botConfig.MaxBotCap[location.ToLower()];
var botCap = _botConfig.MaxBotCap.FirstOrDefault(x => x.Key.ToLower() == location.ToLower());
if (location == "default")
{
_logger.Warning(
@@ -416,7 +407,7 @@ public class BotController(
);
}
return botCap;
return botCap.Value;
}
public object GetAiBotBrainTypes()
@@ -176,7 +176,7 @@ public class HideoutController(
}
// Upgrade includes a container improvement/addition
if (hideoutStage?.Container is not null)
if (!string.IsNullOrEmpty(hideoutStage?.Container))
{
AddContainerImprovementToProfile(
output,
@@ -365,7 +365,7 @@ public class HideoutController(
hideoutArea.Slots[hideoutSlotIndex].Items =
[
new HideoutItem()
new HideoutItem
{
Id = item.inventoryItem.Id,
Template = item.inventoryItem.Template,
@@ -442,7 +442,7 @@ public class HideoutController(
AddItemDirectRequest request = new AddItemDirectRequest
{
ItemWithModsToAdd = [itemToReturn],
ItemWithModsToAdd = [itemToReturn.ConvertToItem()],
FoundInRaid = itemToReturn.Upd?.SpawnedInSession,
Callback = null,
UseSortingTable = false,
@@ -666,7 +666,7 @@ public class HideoutController(
continue;
}
if (_hideoutHelper.IsProductionType(production.Value))
if (production.Value.GetType() == typeof(HideoutProduction))
{
// Production or ScavCase
if (production.Value.RecipeId == request.RecipeId)
@@ -714,7 +714,7 @@ public class HideoutController(
var defaultPreset = _presetHelper.GetDefaultPreset(recipe.EndProduct);
// Ensure preset has unique ids and is cloned so we don't alter the preset data stored in memory
List<Item> presetAndMods = _itemHelper.ReplaceIDs(defaultPreset.Items);
List<Item> presetAndMods = _itemHelper.ReplaceIDs(_cloner.Clone(defaultPreset.Items));
_itemHelper.RemapRootItemId(presetAndMods);
@@ -752,7 +752,7 @@ public class HideoutController(
var countOfItemsToReward = recipe.Count;
for (var index = 1; index < countOfItemsToReward; index++)
{
List<Item> itemAndMods = _itemHelper.ReplaceIDs(itemAndChildrenToSendToPlayer.FirstOrDefault());
List<Item> itemAndMods = _itemHelper.ReplaceIDs(_cloner.Clone(itemAndChildrenToSendToPlayer.FirstOrDefault()));
itemAndChildrenToSendToPlayer.AddRange([itemAndMods]);
}
}
@@ -922,7 +922,7 @@ public class HideoutController(
string? prodId = null;
foreach (var production in ongoingProductions)
{
if (_hideoutHelper.IsProductionType(production.Value))
if (production.Value.GetType() == typeof(HideoutProduction))
{
// Production or ScavCase
if ((production.Value).RecipeId == request.RecipeId)
@@ -1286,4 +1286,20 @@ public class HideoutController(
{
return _databaseService.GetHideout().Qte;
}
/**
* Function called every `hideoutConfig.runIntervalSeconds` seconds as part of onUpdate event
*/
public void Update() {
foreach (var sessionID in _saveServer.GetProfiles()) {
if (sessionID.Value.CharacterData.PmcData.Hideout is not null &&
_profileActivityService.ActiveWithinLastMinutes(
sessionID.Key,
_hideoutConfig.UpdateProfileHideoutWhenActiveWithinMinutes
)
) {
_hideoutHelper.UpdatePlayerHideout(sessionID.Key);
}
}
}
}
+10 -7
View File
@@ -5,17 +5,13 @@ using Core.Models.Eft.InRaid;
using Core.Models.Spt.Config;
using Core.Models.Utils;
using Core.Servers;
using Core.Services;
namespace Core.Controllers;
[Injectable]
public class InRaidController(
ISptLogger<InRaidController> _logger,
SaveServer _saveServer,
ProfileHelper _profileHelper,
LocalisationService _localisationService,
ApplicationContext _applicationContext,
ConfigServer _configServer
)
@@ -30,7 +26,7 @@ public class InRaidController(
/// <param name="info">Register player request</param>
public void AddPlayer(string sessionId, RegisterPlayerRequestData info)
{
throw new NotImplementedException();
_applicationContext.AddValue(ContextVariableType.REGISTER_PLAYER_REQUEST, info);
}
/// <summary>
@@ -42,7 +38,14 @@ public class InRaidController(
/// <param name="sessionId"></param>
public void SavePostRaidProfileForScav(ScavSaveRequestData offRaidProfileData, string sessionId)
{
throw new NotImplementedException();
var serverScavProfile = _profileHelper.GetScavProfile(sessionId);
// If equipment match overwrite existing data from update to date raid data for scavenger screen to work correctly.
// otherwise Scav inventory will be overwritten and break scav regeneration, breaking profile.
if (serverScavProfile.Inventory.Equipment == offRaidProfileData.Inventory.Equipment)
{
serverScavProfile.Inventory.Items = offRaidProfileData.Inventory.Items;
}
}
/// <summary>
@@ -61,7 +64,7 @@ public class InRaidController(
/// <returns></returns>
public double GetTraitorScavHostileChance(string url, string sessionId)
{
throw new NotImplementedException();
return _inRaidConfig.PlayerScavHostileChancePercent;
}
/// <summary>
@@ -1,4 +1,3 @@
using System.Runtime.InteropServices.JavaScript;
using Core.Helpers;
using Core.Models.Common;
using Core.Models.Eft.Common;
@@ -293,7 +293,7 @@ public class InventoryController(
UseSortingTable = true
};
_inventoryHelper.AddItemsToStash(sessionId, addItemsRequest, pmcData, output);
if (output.Warnings.Count > 0) return;
if (output.Warnings?.Count > 0) return;
}
// Find and delete opened container item from player inventory
@@ -362,7 +362,7 @@ public class InventoryController(
public void ExamineItem(PmcData pmcData, InventoryExamineRequestData request, string sessionId,
ItemEventRouterResponse output)
{
var itemId = "";
string? itemId = null;
if (request.FromOwner is not null)
{
try
@@ -375,17 +375,29 @@ public class InventoryController(
}
// get hideout item
if (request.FromOwner.Type == "HideoutProduction") itemId = request.Item;
if (request.FromOwner.Type == "HideoutProduction")
{
itemId = request.Item;
}
}
if (itemId is null)
{
// item template
if (_databaseService.GetItems().ContainsKey(request.Item)) itemId = request.Item;
if (_databaseService.GetItems().ContainsKey(request.Item))
{
itemId = request.Item;
}
}
if (itemId is null)
{
// Player inventory
var target = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == request.Item);
if (target is not null) itemId = target.Template;
if (target is not null)
{
itemId = target.Template;
}
}
if (itemId is not null)
@@ -440,14 +452,14 @@ public class InventoryController(
// Remove kvp from requested fast panel index
// TODO - does this work
pmcData.Inventory.FastPanel.Remove(request.Index.ToString());
pmcData.Inventory.FastPanel.Remove(request.Index);
}
public void BindItem(PmcData pmcData, InventoryBindRequestData bindRequest, string sessionId,
ItemEventRouterResponse output)
{
foreach (var kvp in pmcData.Inventory.FastPanel
.Where(kvp => kvp.Value == bindRequest.Index.Value.ToString()))
.Where(kvp => kvp.Value == bindRequest.Index))
{
pmcData.Inventory.FastPanel.Remove(kvp.Key);
@@ -455,7 +467,7 @@ public class InventoryController(
}
// Create link between fast panel slot and requested item
pmcData.Inventory.FastPanel[bindRequest.Index.ToString()] = bindRequest.Item;
pmcData.Inventory.FastPanel[bindRequest.Index] = bindRequest.Item;
}
public ItemEventRouterResponse TagItem(PmcData pmcData, InventoryTagRequestData request, string sessionId)
@@ -764,7 +776,7 @@ public class InventoryController(
return;
}
var profileToRemoveItemFrom = request?.FromOwner.Id == pmcData.Id
var profileToRemoveItemFrom = request.FromOwner is null || request.FromOwner?.Id == pmcData.Id
? pmcData
: _profileHelper.GetFullProfile(sessionId).CharacterData.ScavData;
@@ -59,9 +59,9 @@ public class LocationController(
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
public GetAirdropLootResponse GetAirDropLoot(GetAirdropLootRequest request)
public GetAirdropLootResponse? GetAirDropLoot(GetAirdropLootRequest? request)
{
if (request.ContainerId is not null)
if (request?.ContainerId is not null)
{
return _airdropService.GenerateCustomAirdropLoot(request);
}
@@ -23,6 +23,9 @@ public class NoteController(
NoteActionData body,
string sessionId)
{
Note newNote = new Note { Time = body.Note.Time, Text = body.Note.Text };
pmcData.Notes.DataNotes.Add(newNote);
return _eventOutputHolder.GetOutput(sessionId);
}
@@ -38,6 +41,10 @@ public class NoteController(
NoteActionData body,
string sessionId)
{
Note noteToEdit = pmcData.Notes.DataNotes[body.Index!.Value];
noteToEdit.Time = body.Note.Time;
noteToEdit.Text = body.Note.Text;
return _eventOutputHolder.GetOutput(sessionId);
}
@@ -1,6 +1,9 @@
using SptCommon.Annotations;
using Core.Helpers;
using Core.Models.Eft.Notifier;
using static System.Runtime.InteropServices.JavaScript.JSType;
using Core.Services;
using System.Diagnostics.Tracing;
namespace Core.Controllers;
@@ -8,7 +11,7 @@ namespace Core.Controllers;
public class NotifierController(
HttpServerHelper _httpServerHelper,
NotifierHelper _notifierHelper
)
)
{
/// <summary>
/// Resolve an array of session notifications.
@@ -20,7 +23,47 @@ public class NotifierController(
/// <param name="sessionId"></param>
public async Task NotifyAsync(string sessionId)
{
throw new NotImplementedException();
// TODO: Finish implementation of the NotifyAsync method
//
//return new Promise((resolve) => {
// // keep track of our timeout
// let counter = 0;
// /**
// * Check for notifications, resolve if any, otherwise poll
// * intermittently for a period of time.
// */
// var checkNotifications = () => {
// /**
// * If there are no pending messages we should either check again later
// * or timeout now with a default response.
// */
// if (!_notificationService.Has(sessionID)) {
// // have we exceeded timeout? if so reply with default ping message
// if (counter > _timeout) {
// return resolve([_notifierHelper.getDefaultNotification()]);
// }
// // check again
// setTimeout(checkNotifications, _pollInterval);
// // update our timeout counter
// counter += _pollInterval;
// return;
// }
// /**
// * Maintaining array reference is not necessary, so we can just copy and reinitialize
// */
// var messages = _notificationService.Get(sessionID);
// _notificationService.UpdateMessageOnQueue(sessionID, []);
// resolve(messages);
//};
// immediately check
// checkNotifications();
//});
}
/// <summary>
@@ -1,4 +1,4 @@
using System.Runtime.InteropServices.JavaScript;
using System.Text.Json;
using SptCommon.Annotations;
using Core.Helpers;
using Core.Models.Eft.Common;
@@ -13,6 +13,7 @@ using Core.Servers;
using Core.Services;
using Core.Utils;
using Core.Utils.Cloners;
using SptCommon.Extensions;
namespace Core.Controllers;
@@ -218,7 +219,7 @@ public class QuestController(
{
if (condition.Id == handoverQuestRequest.ConditionId && handoverQuestTypes.Contains(condition.ConditionType))
{
handedInCount = int.Parse((string)condition.Value);
handedInCount = int.Parse(condition.Value.ToString());
isItemHandoverQuest = condition.ConditionType == handoverQuestTypes.FirstOrDefault();
handoverRequirements = condition;
+192 -12
View File
@@ -15,6 +15,9 @@ using Core.Models.Spt.Config;
using Core.Models.Common;
using Core.Models.Eft.Trade;
using Core.Generators;
using System.Xml.Linq;
using System;
using Core.Models.Spt.Services;
namespace Core.Controllers;
@@ -512,9 +515,95 @@ public class RagfairController
* @param output Response to send to client
* @returns IItemEventRouterResponse
*/
private ItemEventRouterResponse CreateMultiOffer(string sessionId, AddOfferRequestData offerRequest, SptProfile fullProfile, ItemEventRouterResponse output)
private ItemEventRouterResponse CreateMultiOffer(string sessionID, AddOfferRequestData offerRequest, SptProfile fullProfile, ItemEventRouterResponse output)
{
throw new NotImplementedException();
var pmcData = fullProfile.CharacterData.PmcData;
var itemsToListCount = offerRequest.Items.Count; // Does not count stack size, only items
// multi-offers are all the same item,
// Get first item and its children and use as template
var firstListingAndChidren = _itemHelper.FindAndReturnChildrenAsItems(
pmcData.Inventory.Items,
offerRequest.Items[0]);
// Find items to be listed on flea (+ children) from player inventory
var result = GetItemsToListOnFleaFromInventory(pmcData, offerRequest.Items);
if (result.Items is null || !string.IsNullOrEmpty(result.ErrorMessage))
{
_httpResponseUtil.AppendErrorToOutput(output, result.ErrorMessage);
}
// Total count of items summed using their stack counts
var stackCountTotal = _ragfairOfferHelper.GetTotalStackCountSize(result.Items);
// When listing identical items on flea, condense separate items into one stack with a merged stack count
// e.g. 2 ammo items, stackObjectCount = 3 for each, will result in 1 stack of 6
firstListingAndChidren[0].Upd ??= new Upd{ };
firstListingAndChidren[0].Upd.StackObjectsCount = stackCountTotal;
// Create flea object
var offer = CreatePlayerOffer(sessionID, offerRequest.Requirements, firstListingAndChidren, false);
// This is the item that will be listed on flea, has merged stackObjectCount
var newRootOfferItem = offer.Items[0];
// Average offer price for single item (or whole weapon)
var averages = GetItemMinAvgMaxFleaPriceValues(new GetMarketPriceRequestData{ TemplateId = offer.Items[0].Template });
var averageOfferPrice = averages.Avg;
// Check for and apply item price modifer if it exists in config
if (_ragfairConfig.Dynamic.ItemPriceMultiplier.TryGetValue(newRootOfferItem.Template, out var itemPriceModifer))
{
averageOfferPrice *= itemPriceModifer;
}
// Get average of item+children quality
var qualityMultiplier = _itemHelper.GetItemQualityModifierForItems(offer.Items, true);
// Multiply single item price by quality
averageOfferPrice *= qualityMultiplier;
// Get price player listed items for in roubles
var playerListedPriceInRub = CalculateRequirementsPriceInRub(offerRequest.Requirements);
// Roll sale chance
var sellChancePercent = _ragfairSellHelper.CalculateSellChance(
averageOfferPrice.Value,
playerListedPriceInRub,
qualityMultiplier);
// Create array of sell times for items listed
offer.SellResults = _ragfairSellHelper.RollForSale(sellChancePercent, (int)stackCountTotal);
// Subtract flea market fee from stash
if (_ragfairConfig.Sell.Fees)
{
var taxFeeChargeFailed = ChargePlayerTaxFee(
sessionID,
newRootOfferItem,
pmcData,
playerListedPriceInRub,
(int)stackCountTotal,
offerRequest,
output);
if (taxFeeChargeFailed)
{
return output;
}
}
// Add offer to players profile + add to client response
fullProfile.CharacterData.PmcData.RagfairInfo.Offers.Add(offer);
output.ProfileChanges[sessionID].RagFairOffers.Add(offer);
// Remove items from inventory after creating offer
foreach (var itemToRemove in offerRequest.Items) {
_inventoryHelper.RemoveItem(pmcData, itemToRemove, sessionID, output);
}
return output;
}
/**
@@ -527,9 +616,94 @@ public class RagfairController
* @param output Response to send to client
* @returns IItemEventRouterResponse
*/
private ItemEventRouterResponse CreatePackOffer(string sessionId, AddOfferRequestData offerRequest, SptProfile fullProfile, ItemEventRouterResponse output)
private ItemEventRouterResponse CreatePackOffer(string sessionID, AddOfferRequestData offerRequest, SptProfile fullProfile, ItemEventRouterResponse output)
{
throw new NotImplementedException();
var pmcData = fullProfile.CharacterData.PmcData;
var itemsToListCount = offerRequest.Items.Count; // Does not count stack size, only items
// multi-offers are all the same item,
// Get first item and its children and use as template
var firstListingAndChidren = _itemHelper.FindAndReturnChildrenAsItems(
pmcData.Inventory.Items,
offerRequest.Items[0]);
// Find items to be listed on flea (+ children) from player inventory
var result = GetItemsToListOnFleaFromInventory(pmcData, offerRequest.Items);
if (result.Items is null || result.ErrorMessage is not null)
{
_httpResponseUtil.AppendErrorToOutput(output, result.ErrorMessage);
}
// Total count of items summed using their stack counts
var stackCountTotal = _ragfairOfferHelper.GetTotalStackCountSize(result.Items);
// When listing identical items on flea, condense separate items into one stack with a merged stack count
// e.g. 2 ammo items, stackObjectCount = 3 for each, will result in 1 stack of 6
firstListingAndChidren[0].Upd ??= new Upd { };
firstListingAndChidren[0].Upd.StackObjectsCount = stackCountTotal;
// Create flea object
var offer = CreatePlayerOffer(sessionID, offerRequest.Requirements, firstListingAndChidren, true);
// This is the item that will be listed on flea, has merged stackObjectCount
var newRootOfferItem = offer.Items[0];
// Single price for an item
var averages = GetItemMinAvgMaxFleaPriceValues( new GetMarketPriceRequestData{ TemplateId = firstListingAndChidren[0].Template });
var singleItemPrice = averages.Avg;
// Check for and apply item price modifer if it exists in config
if (_ragfairConfig.Dynamic.ItemPriceMultiplier.TryGetValue(newRootOfferItem.Template, out double itemPriceModifer))
{
singleItemPrice *= itemPriceModifer;
}
// Get average of item+children quality
var qualityMultiplier = _itemHelper.GetItemQualityModifierForItems(offer.Items, true);
// Multiply single item price by quality
singleItemPrice *= qualityMultiplier;
// Get price player listed items for in roubles
var playerListedPriceInRub = CalculateRequirementsPriceInRub(offerRequest.Requirements);
// Roll sale chance
var sellChancePercent = _ragfairSellHelper.CalculateSellChance(
singleItemPrice.Value * stackCountTotal,
playerListedPriceInRub,
qualityMultiplier);
// Create array of sell times for items listed + sell all at once as its a pack
offer.SellResults = _ragfairSellHelper.RollForSale(sellChancePercent, (int)stackCountTotal, true);
// Subtract flea market fee from stash
if (_ragfairConfig.Sell.Fees)
{
var taxFeeChargeFailed = ChargePlayerTaxFee(
sessionID,
newRootOfferItem,
pmcData,
playerListedPriceInRub,
(int)stackCountTotal,
offerRequest,
output);
if (taxFeeChargeFailed)
{
return output;
}
}
// Add offer to players profile + add to client response
fullProfile.CharacterData.PmcData.RagfairInfo.Offers.Add(offer);
output.ProfileChanges[sessionID].RagFairOffers.Add(offer);
// Remove items from inventory after creating offer
foreach (var itemToRemove in offerRequest.Items) {
_inventoryHelper.RemoveItem(pmcData, itemToRemove, sessionID, output);
}
return output;
}
/**
@@ -549,9 +723,9 @@ public class RagfairController
// Find items to be listed on flea from player inventory
var result = GetItemsToListOnFleaFromInventory(pmcData, offerRequest.Items);
if (result.Items is null || result.error is not null)
if (result.Items is null || result.ErrorMessage is not null)
{
_httpResponseUtil.AppendErrorToOutput(output, result.errorMessage);
_httpResponseUtil.AppendErrorToOutput(output, result.ErrorMessage);
}
// Total count of items summed using their stack counts
@@ -589,7 +763,7 @@ public class RagfairController
playerListedPriceInRub,
qualityMultiplier
);
offer.SellResult = _ragfairSellHelper.RollForSale(sellChancePercent, stackCountTotal);
offer.SellResults = _ragfairSellHelper.RollForSale(sellChancePercent, (int)stackCountTotal);
// Subtract flea market fee from stash
if (_ragfairConfig.Sell.Fees)
@@ -599,7 +773,7 @@ public class RagfairController
rootItem,
pmcData,
playerListedPriceInRub,
stackCountTotal,
(int)stackCountTotal,
offerRequest,
output
);
@@ -736,7 +910,7 @@ public class RagfairController
return requirementsPriceInRub;
}
private dynamic GetItemsToListOnFleaFromInventory(PmcData pmcData, List<string> itemIdsFromFleaOfferRequest)
private GetItemsToListOnFleaFromInventoryResult GetItemsToListOnFleaFromInventory(PmcData pmcData, List<string> itemIdsFromFleaOfferRequest)
{
List<List<Item>> itemsToReturn = [];
var errorMessage = string.Empty;
@@ -750,7 +924,7 @@ public class RagfairController
errorMessage = _localisationService.GetText("ragfair-unable_to_find_item_in_inventory", new { id = itemId });
_logger.Error(errorMessage);
return new { itemsToReturn, errorMessage };
return new GetItemsToListOnFleaFromInventoryResult { Items = itemsToReturn, ErrorMessage = errorMessage };
}
item = _itemHelper.FixItemStackCount(item);
@@ -762,10 +936,16 @@ public class RagfairController
errorMessage = _localisationService.GetText("ragfair-unable_to_find_requested_items_in_inventory");
_logger.Error(errorMessage);
return new { ErrorMessage = errorMessage };
return new GetItemsToListOnFleaFromInventoryResult { ErrorMessage = errorMessage };
}
return new { Items = itemsToReturn, ErrorMessage = errorMessage };
return new GetItemsToListOnFleaFromInventoryResult { Items = itemsToReturn, ErrorMessage = errorMessage };
}
public record GetItemsToListOnFleaFromInventoryResult
{
public List<List<Item>>? Items { get; set; }
public string? ErrorMessage { get; set; }
}
public ItemEventRouterResponse RemoveOffer(RemoveOfferRequestData removeRequest, string sessionId)
+20 -1
View File
@@ -69,6 +69,25 @@ public class RepairController(
RepairActionDataRequest body,
PmcData pmcData)
{
throw new NotImplementedException();
var output = _eventOutputHolder.GetOutput(sessionId);
// repair item
var repairDetails = _repairService.RepairItemByKit(
sessionId,
pmcData,
body.RepairKitsInfo,
body.Target,
output
);
_repairService.AddBuffToItem(repairDetails, pmcData);
// add repaired item to send to client
output.ProfileChanges[sessionId].Items.ChangedItems.Add(repairDetails.RepairedItem);
// Add skill points for repairing items
_repairService.AddRepairSkillPoints(sessionId, repairDetails, pmcData);
return output;
}
}
@@ -3,9 +3,11 @@ using Core.Helpers;
using Core.Models.Eft.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.ItemEvent;
using Core.Models.Eft.Profile;
using Core.Models.Eft.Quests;
using Core.Models.Enums;
using Core.Models.Spt.Config;
using Core.Models.Spt.Quests;
using Core.Models.Spt.Repeatable;
using Core.Models.Utils;
using Core.Routers;
@@ -23,7 +25,7 @@ public class RepeatableQuestController(
TimeUtil _timeUtil,
HashUtil _hashUtil,
RandomUtil _randomUtil,
HttpResponseUtil _responseUtil,
HttpResponseUtil _httpResponseUtil,
ProfileHelper _profileHelper,
ProfileFixerService _profileFixerService,
LocalisationService _localisationService,
@@ -39,10 +41,270 @@ public class RepeatableQuestController(
{
protected QuestConfig _questConfig = _configServer.GetConfig<QuestConfig>();
public ItemEventRouterResponse ChangeRepeatableQuest(PmcData pmcData, RepeatableQuestChangeRequest info,
string sessionId)
public ItemEventRouterResponse ChangeRepeatableQuest(PmcData pmcData, RepeatableQuestChangeRequest changeRequest,
string sessionID)
{
throw new NotImplementedException();
var output = _eventOutputHolder.GetOutput(sessionID);
var fullProfile = _profileHelper.GetFullProfile(sessionID);
// Check for existing quest in (daily/weekly/scav arrays)
var repeatables = GetRepeatableById(changeRequest.QuestId, pmcData);
var questToReplace = repeatables.Quest;
var repeatablesOfTypeInProfile = repeatables.RepeatableType;
if (repeatables.RepeatableType is null || repeatables.Quest is null)
{
// Unable to find quest being replaced
var message = _localisationService.GetText("quest-unable_to_find_repeatable_to_replace");
_logger.Error(message);
return _httpResponseUtil.AppendErrorToOutput(output, message);
}
// Subtype name of quest - daily/weekly/scav
var repeatableTypeLower = repeatablesOfTypeInProfile.Name.ToLower();
// Save for later standing loss calculation
var replacedQuestTraderId = questToReplace.TraderId;
// Update active quests to exclude the quest we're replacing
repeatablesOfTypeInProfile.ActiveQuests = repeatablesOfTypeInProfile.ActiveQuests.Where(
quest => quest.Id != changeRequest.QuestId
)
.ToList();
// Save for later cost calculations
var previousChangeRequirement = _cloner.Clone(
repeatablesOfTypeInProfile.ChangeRequirement[changeRequest.QuestId]
);
// Delete the replaced quest change requirement data as we're going to add new data below
repeatablesOfTypeInProfile.ChangeRequirement.Remove(changeRequest.QuestId);
// Get config for this repeatable sub-type (daily/weekly/scav)
var repeatableConfig = _questConfig.RepeatableQuests.FirstOrDefault(
config => config.Name == repeatablesOfTypeInProfile.Name
);
// If the configuration dictates to replace with the same quest type, adjust the available quest types
if (repeatableConfig?.KeepDailyQuestTypeOnReplacement is not null)
{
repeatableConfig.Types = [questToReplace.Type.ToString()];
}
// Generate meta-data for what type/levelrange of quests can be generated for player
var allowedQuestTypes = GenerateQuestPool(repeatableConfig, pmcData.Info.Level);
var newRepeatableQuest = AttemptToGenerateRepeatableQuest(
sessionID,
pmcData,
allowedQuestTypes,
repeatableConfig
);
if (newRepeatableQuest is null)
{
// Unable to find quest being replaced
var message =
$"Unable to generate repeatable quest of type: {repeatableTypeLower} to replace trader: ${replacedQuestTraderId} quest ${changeRequest.QuestId}";
_logger.Error(message);
return _httpResponseUtil.AppendErrorToOutput(output, message);
}
// Add newly generated quest to daily/weekly/scav type array
newRepeatableQuest.Side = repeatableConfig.Side;
repeatablesOfTypeInProfile.ActiveQuests.Add(newRepeatableQuest);
_logger.Debug(
$"Removing: {repeatableConfig.Name} quest: {questToReplace.Id} from trader: {questToReplace.TraderId} as its been replaced"
);
RemoveQuestFromProfile(fullProfile, questToReplace.Id);
// Delete the replaced quest change requirement from profile
CleanUpRepeatableChangeRequirements(repeatablesOfTypeInProfile, questToReplace.Id);
// Add replacement quests change requirement data to profile
repeatablesOfTypeInProfile.ChangeRequirement[newRepeatableQuest.Id] = new ChangeRequirement
{
ChangeCost = newRepeatableQuest.ChangeCost,
ChangeStandingCost = _randomUtil.GetArrayValue([0, 0.01])
};
// Check if we should charge player for replacing quest
var isFreeToReplace = UseFreeRefreshIfAvailable(
fullProfile,
repeatablesOfTypeInProfile,
repeatableTypeLower
);
if (!isFreeToReplace)
{
// Reduce standing with trader for not doing their quest
var traderOfReplacedQuest = pmcData.TradersInfo[replacedQuestTraderId];
traderOfReplacedQuest.Standing -= previousChangeRequirement.ChangeStandingCost;
var charismaBonus = _profileHelper.GetSkillFromProfile(pmcData, SkillTypes.Charisma)?.Progress ?? 0;
foreach (var cost in previousChangeRequirement.ChangeCost)
{
// Not free, Charge player + appy charisma bonus to cost of replacement
cost.Count = (int)Math.Truncate(cost.Count.Value * (1 - Math.Truncate(charismaBonus / 100) * 0.001));
_paymentService.AddPaymentToOutput(pmcData, cost.TemplateId, cost.Count.Value, sessionID, output);
if (output.Warnings.Count > 0)
{
return output;
}
}
}
// Clone data before we send it to client
var repeatableToChangeClone = _cloner.Clone(repeatablesOfTypeInProfile);
// Purge inactive repeatables
repeatableToChangeClone.InactiveQuests = [];
// Nullguard
output.ProfileChanges[sessionID].RepeatableQuests ??= [];
// Update client output with new repeatable
output.ProfileChanges[sessionID].RepeatableQuests.Add(repeatableToChangeClone);
return output;
}
/**
* Some accounts have access to free repeatable quest refreshes
* Track the usage of them inside players profile
* @param fullProfile Player profile
* @param repeatableSubType Can be daily / weekly / scav repeatable
* @param repeatableTypeName Subtype of repeatable quest: daily / weekly / scav
* @returns Is the repeatable being replaced for free
*/
protected bool UseFreeRefreshIfAvailable(SptProfile? fullProfile, PmcDataRepeatableQuest repeatableSubType,
string repeatableTypeName)
{
// No free refreshes, exit early
if (repeatableSubType.FreeChangesAvailable <= 0)
{
// Reset counter to 0
repeatableSubType.FreeChangesAvailable = 0;
return false;
}
// Only certain game versions have access to free refreshes
var hasAccessToFreeRefreshSystem = _profileHelper.HasAccessToRepeatableFreeRefreshSystem(
fullProfile.CharacterData.PmcData
);
// If the player has access and available refreshes:
if (hasAccessToFreeRefreshSystem)
{
// Initialize/retrieve free refresh count for the desired subtype: daily/weekly
fullProfile.SptData.FreeRepeatableRefreshUsedCount ??= new Dictionary<string, int>();
var repeatableRefreshCounts = fullProfile.SptData.FreeRepeatableRefreshUsedCount;
repeatableRefreshCounts.TryAdd(repeatableTypeName, 0); // Set to 0 if undefined
// Increment the used count and decrement the available count.
repeatableRefreshCounts[repeatableTypeName]++;
repeatableSubType.FreeChangesAvailable--;
return true;
}
return false;
}
/**
* Clean up the repeatables `changeRequirement` dictionary of expired data
* @param repeatablesOfTypeInProfile The repeatables that have the replaced and new quest
* @param replacedQuestId Id of the replaced quest
*/
private void CleanUpRepeatableChangeRequirements(PmcDataRepeatableQuest repeatablesOfTypeInProfile,
string replacedQuestId)
{
if (repeatablesOfTypeInProfile.ActiveQuests.Count == 1)
{
// Only one repeatable quest being replaced (e.g. scav_daily), remove everything ready for new quest requirement to be added
// Will assist in cleanup of existing profiles data
repeatablesOfTypeInProfile.ChangeRequirement.Clear();
}
else
{
// Multiple active quests of this type (e.g. daily or weekly) are active, just remove the single replaced quest
repeatablesOfTypeInProfile.ChangeRequirement.Remove(replacedQuestId);
}
}
private RepeatableQuest AttemptToGenerateRepeatableQuest(string sessionId, PmcData pmcData,
QuestTypePool questTypePool, RepeatableQuestConfig repeatableConfig)
{
const int maxAttempts = 10;
RepeatableQuest newRepeatableQuest = null;
var attempts = 0;
while (attempts < maxAttempts && questTypePool.Types.Count > 0)
{
newRepeatableQuest = _repeatableQuestGenerator.GenerateRepeatableQuest(
sessionId,
pmcData.Info.Level.Value,
pmcData.TradersInfo,
questTypePool,
repeatableConfig
);
if (newRepeatableQuest is not null)
{
// Successfully generated a quest, exit loop
break;
}
attempts++;
}
if (attempts > maxAttempts)
{
_logger.Debug("We were stuck in repeatable quest generation. This should never happen. Please report");
}
return newRepeatableQuest;
}
private void RemoveQuestFromProfile(SptProfile? fullProfile, string questToReplaceId)
{
// Find quest we're replacing in pmc profile quests array and remove it
_questHelper.FindAndRemoveQuestFromArrayIfExists(questToReplaceId, fullProfile.CharacterData.PmcData.Quests);
// Find quest we're replacing in scav profile quests array and remove it
if (fullProfile.CharacterData.ScavData is not null)
{
_questHelper.FindAndRemoveQuestFromArrayIfExists(
questToReplaceId,
fullProfile.CharacterData.ScavData.Quests
);
}
}
/**
* Find a repeatable (daily/weekly/scav) from a players profile by its id
* @param questId Id of quest to find
* @param pmcData Profile that contains quests to look through
* @returns IGetRepeatableByIdResult
*/
protected GetRepeatableByIdResult GetRepeatableById(string questId, PmcData pmcData)
{
foreach (var repeatablesInProfile in pmcData.RepeatableQuests)
{
// Check for existing quest in (daily/weekly/scav arrays)
var questToReplace =
repeatablesInProfile.ActiveQuests.FirstOrDefault(repeatable => repeatable.Id == questId);
if (questToReplace is null)
{
// Not found, skip to next repeatable sub-type
continue;
}
return new GetRepeatableByIdResult { Quest = questToReplace, RepeatableType = repeatablesInProfile };
}
return null;
}
public List<PmcDataRepeatableQuest> GetClientRepeatableQuests(string sessionID)
@@ -135,7 +397,7 @@ public class RepeatableQuestController(
fullProfile.SptData.FreeRepeatableRefreshUsedCount[repeatableTypeLower] = 0;
// Create stupid redundant change requirements from quest data
generatedRepeatables.ChangeRequirement = new();
generatedRepeatables.ChangeRequirement = new Dictionary<string, ChangeRequirement>();
foreach (var quest in generatedRepeatables.ActiveQuests)
generatedRepeatables.ChangeRequirement.TryAdd(
quest.Id,
@@ -72,7 +72,7 @@ public class TraderController(
}
// Create dict of pristine trader assorts on server start
if (_traderAssortService.GetPristineTraderAssort(trader.Key) != null)
if (_traderAssortService.GetPristineTraderAssort(trader.Key) == null)
{
var assortsClone = _cloner.Clone(trader.Value.Assort);
_traderAssortService.SetPristineTraderAssort(trader.Key, assortsClone);
-4
View File
@@ -7,10 +7,6 @@
<OutputType>Library</OutputType>
</PropertyGroup>
<ItemGroup>
<Folder Include="Utils\Extensions\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SptDependencyInjection\SptDependencyInjection.csproj" />
</ItemGroup>
@@ -6,12 +6,12 @@ using Core.Models.Spt.Bots;
using Core.Models.Spt.Config;
using Core.Models.Utils;
using Core.Helpers;
using Core.Models.Common;
using Core.Servers;
using Core.Services;
using Core.Utils;
using Core.Utils.Cloners;
using Core.Utils.Collections;
namespace Core.Generators;
[Injectable]
@@ -222,7 +222,156 @@ public class BotEquipmentModGenerator(
public FilterPlateModsForSlotByLevelResult FilterPlateModsForSlotByLevel(GenerateEquipmentProperties settings, string modSlot,
HashSet<string> existingPlateTplPool, TemplateItem armorItem)
{
throw new NotImplementedException();
var result = new FilterPlateModsForSlotByLevelResult
{
Result = Result.UNKNOWN_FAILURE,
PlateModTemplates = null,
};
// Not pmc or not a plate slot, return original mod pool array
if (!_itemHelper.IsRemovablePlateSlot(modSlot))
{
result.Result = Result.NOT_PLATE_HOLDING_SLOT;
result.PlateModTemplates = existingPlateTplPool;
return result;
}
// Get the front/back/side weights based on bots level
var plateSlotWeights = settings.BotEquipmentConfig?.ArmorPlateWeighting.FirstOrDefault(
(armorWeight) =>
settings.BotData.Level >= armorWeight.LevelRange.Min &&
settings.BotData.Level <= armorWeight.LevelRange.Max
);
if (plateSlotWeights is null)
{
// No weights, return original array of plate tpls
result.Result = Result.LACKS_PLATE_WEIGHTS;
result.PlateModTemplates = existingPlateTplPool;
return result;
}
// Get the specific plate slot weights (front/back/side)
if (!plateSlotWeights.Values.TryGetValue(modSlot, out var plateWeights))
{
// No weights, return original array of plate tpls
result.Result = Result.LACKS_PLATE_WEIGHTS;
result.PlateModTemplates = existingPlateTplPool;
return result;
}
// Choose a plate level based on weighting
var chosenArmorPlateLevelString = _weightedRandomHelper.GetWeightedValue(plateWeights);
// Convert the array of ids into database items
var platesFromDb = existingPlateTplPool.Select((plateTpl) => _itemHelper.GetItem(plateTpl).Value);
// Filter plates to the chosen level based on its armorClass property
var platesOfDesiredLevel = platesFromDb.Where((item) => item.Properties.ArmorClass.Value == double.Parse(chosenArmorPlateLevelString));
if (platesOfDesiredLevel.Any())
{
// Plates found
result.Result = Result.SUCCESS;
result.PlateModTemplates = platesOfDesiredLevel.Select((item) => item.Id).ToHashSet();
return result;
}
// no plates found that fit requirements, lets get creative
// Get lowest and highest plate classes available for this armor
var minMaxArmorPlateClass = GetMinMaxArmorPlateClass(platesFromDb.ToList());
// Increment plate class level in attempt to get useable plate
var findCompatiblePlateAttempts = 0;
var maxAttempts = 3;
for (var i = 0; i < maxAttempts; i++)
{
var chosenArmorPlateLevelDouble = int.Parse(chosenArmorPlateLevelString) + 1;
chosenArmorPlateLevelString = chosenArmorPlateLevelDouble.ToString();
// New chosen plate class is higher than max, then set to min and check if valid
if (chosenArmorPlateLevelDouble > minMaxArmorPlateClass.Max)
{
chosenArmorPlateLevelString = minMaxArmorPlateClass.Min.ToString();
}
findCompatiblePlateAttempts++;
platesOfDesiredLevel = platesFromDb.Where((item) => item.Properties.ArmorClass == chosenArmorPlateLevelDouble);
// Valid plates found, exit
if (platesOfDesiredLevel.Any())
{
break;
}
// No valid plate class found in 3 tries, attempt default plates
if (findCompatiblePlateAttempts >= maxAttempts)
{
_logger.Debug(
$"Plate filter too restrictive for armor: {armorItem.Name} {armorItem.Id}, unable to find plates of level: {chosenArmorPlateLevelString}, using items default plate"
);
var defaultPlate = GetDefaultPlateTpl(armorItem, modSlot);
if (defaultPlate is not null)
{
// Return Default Plates cause couldn't get lowest level available from original selection
result.Result = Result.SUCCESS;
result.PlateModTemplates = [defaultPlate];
return result;
}
// No plate found after filtering AND no default plate
// Last attempt, get default preset and see if it has a plate default
var defaultPresetPlateSlot = GetDefaultPresetArmorSlot(armorItem.Id, modSlot);
if (defaultPresetPlateSlot is not null)
{
// Found a plate, exit
var plateItem = _itemHelper.GetItem(defaultPresetPlateSlot.Template);
platesOfDesiredLevel = [plateItem.Value];
break;
}
// Everything failed, no default plate or no default preset armor plate
result.Result = Result.NO_DEFAULT_FILTER;
return result;
}
}
// Only return the items ids
result.Result = Result.SUCCESS;
result.PlateModTemplates = platesOfDesiredLevel.Select(item => item.Id).ToHashSet();
return result;
}
private MinMax GetMinMaxArmorPlateClass(List<TemplateItem> platePool)
{
platePool.Sort((x, y) => {
if (x.Properties.ArmorClass < y.Properties.ArmorClass)
{
return -1;
}
if (x.Properties.ArmorClass > y.Properties.ArmorClass)
{
return 1;
}
return 0;
});
return new MinMax {
Min = (platePool[0].Properties.ArmorClass),
Max = (platePool[platePool.Count - 1].Properties.ArmorClass),
};
}
/**
@@ -231,7 +380,7 @@ public class BotEquipmentModGenerator(
* @param modSlot front/back
* @returns Tpl of plate
*/
protected string GetDefaultPlateTpl(TemplateItem armorItem, string modSlot)
protected string? GetDefaultPlateTpl(TemplateItem armorItem, string modSlot)
{
var relatedItemDbModSlot = armorItem.Properties.Slots?.FirstOrDefault(slot => slot.Name.ToLower() == modSlot);
@@ -291,7 +440,7 @@ public class BotEquipmentModGenerator(
_botConfig.Equipment.TryGetValue(request.BotData.EquipmentRole, out var botEquipConfig);
var botEquipBlacklist = _botEquipmentFilterService.GetBotEquipmentBlacklist(
request.BotData.EquipmentRole,
pmcProfile.Info.Level ?? 0
pmcProfile?.Info?.Level ?? 0
);
var botWeaponSightWhitelist = _botEquipmentFilterService.GetBotWeaponSightWhitelist(
request.BotData.EquipmentRole
@@ -360,7 +509,7 @@ public class BotEquipmentModGenerator(
}
if (!IsModValidForSlot(modToAdd, modsParentSlot, modSlot, request.ParentTemplate, request.BotData.Role)
)
)
{
continue;
}
@@ -511,7 +660,6 @@ public class BotEquipmentModGenerator(
request.ModPool[modToAddTemplate.Value.Id] = modFromService;
containsModInPool = true;
}
}
if (containsModInPool)
@@ -936,10 +1084,13 @@ public class BotEquipmentModGenerator(
var weaponTpl = modSpawnRequest.Weapon[0].Template;
modSpawnRequest.RandomisationSettings.MinimumMagazineSize.TryGetValue(weaponTpl, out var minMagSizeFromSettings);
var minMagazineSize = minMagSizeFromSettings;
var desiredMagazineTpls = modPool.Where((magTpl) => {
var magazineDb = _itemHelper.GetItem(magTpl).Value;
return magazineDb.Properties is not null && magazineDb.Properties.Cartridges.FirstOrDefault().MaxCount >= minMagazineSize;
});
var desiredMagazineTpls = modPool.Where(
(magTpl) =>
{
var magazineDb = _itemHelper.GetItem(magTpl).Value;
return magazineDb.Properties is not null && magazineDb.Properties.Cartridges.FirstOrDefault().MaxCount >= minMagazineSize;
}
);
if (!desiredMagazineTpls.Any())
{
@@ -1370,7 +1521,8 @@ public class BotEquipmentModGenerator(
/// <param name="modTemplate">db object for modItem we get compatible mods from</param>
/// <param name="modPool">Pool of mods we are adding to</param>
/// <param name="botEquipBlacklist">A blacklist of items that cannot be picked</param>
public void AddCompatibleModsForProvidedMod(string desiredSlotName, TemplateItem modTemplate, Dictionary<string, Dictionary<string, HashSet<string>>> modPool,
public void AddCompatibleModsForProvidedMod(string desiredSlotName, TemplateItem modTemplate,
Dictionary<string, Dictionary<string, HashSet<string>>> modPool,
EquipmentFilterDetails botEquipBlacklist)
{
var desiredSlotObject = modTemplate.Properties.Slots?.FirstOrDefault((slot) => slot.Name.Contains(desiredSlotName));
@@ -1573,7 +1725,9 @@ public class BotEquipmentModGenerator(
var whitelistedSightTypes = botWeaponSightWhitelist[weaponDetails.Value.Parent];
if (whitelistedSightTypes is null)
{
_logger.Debug($"Unable to find whitelist for weapon type: {weaponDetails.Value.Parent} {weaponDetails.Value.Name}, skipping sight filtering");
_logger.Debug(
$"Unable to find whitelist for weapon type: {weaponDetails.Value.Parent} {weaponDetails.Value.Name}, skipping sight filtering"
);
return scopes;
}
+16 -10
View File
@@ -78,7 +78,7 @@ public class BotGenerator(
Id = bot.Id,
Aid = bot.Aid,
SessionId = bot.SessionId,
Savage = bot.Savage,
Savage = null,
KarmaValue = bot.KarmaValue,
Info = bot.Info,
Customization = bot.Customization,
@@ -199,7 +199,11 @@ public class BotGenerator(
botRoleLowercase,
_botConfig.BotRolesThatMustHaveUniqueName
);
bot.Info.LowerNickname = bot.Info.Nickname.ToLower();
// Only Pmcs should have a lower nickname
bot.Info.LowerNickname = botGenerationDetails.IsPmc.GetValueOrDefault(false)
? bot.Info.Nickname.ToLower()
: string.Empty;
// Only run when generating a 'fake' playerscav, not actual player scav
if (!botGenerationDetails.IsPlayerScav.GetValueOrDefault(false) && ShouldSimulatePlayerScav(botRoleLowercase))
@@ -230,7 +234,7 @@ public class BotGenerator(
bot.Info.Experience = botLevel.Exp;
bot.Info.Level = botLevel.Level;
bot.Info.Settings.Experience = GetExperienceRewardForKillByDifficulty(
bot.Info.Settings.Experience = (int)GetExperienceRewardForKillByDifficulty(
botJsonTemplate.BotExperience.Reward,
botGenerationDetails.BotDifficulty,
botGenerationDetails.Role
@@ -249,6 +253,7 @@ public class BotGenerator(
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
bot.Info.PrestigeLevel = 0;
if (botGenerationDetails.IsPmc.GetValueOrDefault(false))
{
@@ -281,7 +286,7 @@ public class BotGenerator(
}
// Generate new bot ID
AddIdsToBot(bot);
AddIdsToBot(bot, botGenerationDetails);
// Generate new inventory ID
GenerateInventoryId(bot);
@@ -579,7 +584,7 @@ public class BotGenerator(
}
}
},
UpdateTime = _timeUtil.GetTimeStamp(),
UpdateTime = 0, // 0 for pscav too
Immortal = false
};
@@ -683,13 +688,14 @@ public class BotGenerator(
/// Generate an id+aid for a bot and apply
/// </summary>
/// <param name="bot">bot to update</param>
/// <param name="botGenerationDetails"></param>
/// <returns></returns>
public void AddIdsToBot(BotBase bot)
public void AddIdsToBot(BotBase bot, BotGenerationDetails botGenerationDetails)
{
var botId = _hashUtil.Generate();
bot.Id = botId;
bot.Aid = _hashUtil.GenerateAccountId();
bot.Aid = botGenerationDetails.IsPmc.GetValueOrDefault(false) ? _hashUtil.GenerateAccountId() : 0;
}
/// <summary>
@@ -742,7 +748,7 @@ public class BotGenerator(
if (botInfo.Nickname?.ToLower() == "nikita")
{
botInfo.GameVersion = GameEditions.UNHEARD;
botInfo.MemberCategory = MemberCategory.DEVELOPER;
botInfo.MemberCategory = MemberCategory.Developer;
return botInfo.GameVersion;
}
@@ -754,10 +760,10 @@ public class BotGenerator(
switch (botInfo.GameVersion)
{
case GameEditions.EDGE_OF_DARKNESS:
botInfo.MemberCategory = MemberCategory.UNIQUE_ID;
botInfo.MemberCategory = MemberCategory.UniqueId;
break;
case GameEditions.UNHEARD:
botInfo.MemberCategory = MemberCategory.UNHEARD;
botInfo.MemberCategory = MemberCategory.Unheard;
break;
default:
// Everyone else gets a weighted randomised category
@@ -102,6 +102,7 @@ public class BotInventoryGenerator(
var questRaidItemsId = _hashUtil.Generate();
var questStashItemsId = _hashUtil.Generate();
var sortingTableId = _hashUtil.Generate();
var hideoutCustomizationStashId = _hashUtil.Generate();
return new BotBaseInventory
{
@@ -111,17 +112,18 @@ public class BotInventoryGenerator(
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 }
new() { Id = sortingTableId, Template = ItemTpl.SORTINGTABLE_SORTING_TABLE },
new() { Id = hideoutCustomizationStashId, Template = ItemTpl.HIDEOUTAREACONTAINER_CUSTOMIZATION }
],
Equipment = equipmentId,
Stash = stashId,
QuestRaidItems = questRaidItemsId,
QuestStashItems = questStashItemsId,
SortingTable = sortingTableId,
HideoutAreaStashes = { },
FastPanel = { },
HideoutAreaStashes = new Dictionary<string, string>(),
FastPanel = new Dictionary<string, string>(),
FavoriteItems = [],
HideoutCustomizationStashId = "",
HideoutCustomizationStashId = hideoutCustomizationStashId,
};
}
@@ -201,7 +203,7 @@ public class BotInventoryGenerator(
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GeneratingPlayerLevel = pmcProfile.Info.Level
GeneratingPlayerLevel = pmcProfile?.Info?.Level ?? 1
}
);
}
@@ -223,7 +225,7 @@ public class BotInventoryGenerator(
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GenerateModsBlacklist = [ItemTpl.POCKETS_1X4_TUE, ItemTpl.POCKETS_LARGE],
GeneratingPlayerLevel = pmcProfile.Info.Level,
GeneratingPlayerLevel = pmcProfile?.Info?.Level ?? 1,
}
);
@@ -238,7 +240,7 @@ public class BotInventoryGenerator(
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GeneratingPlayerLevel = pmcProfile.Info.Level,
GeneratingPlayerLevel = pmcProfile?.Info?.Level ?? 1,
}
);
@@ -253,7 +255,7 @@ public class BotInventoryGenerator(
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GeneratingPlayerLevel = pmcProfile.Info.Level,
GeneratingPlayerLevel = pmcProfile?.Info?.Level ?? 1,
}
);
@@ -268,7 +270,7 @@ public class BotInventoryGenerator(
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GeneratingPlayerLevel = pmcProfile.Info.Level,
GeneratingPlayerLevel = pmcProfile?.Info?.Level ?? 1,
}
);
@@ -283,7 +285,7 @@ public class BotInventoryGenerator(
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GeneratingPlayerLevel = pmcProfile.Info.Level,
GeneratingPlayerLevel = pmcProfile?.Info?.Level ?? 1,
}
);
@@ -310,15 +312,15 @@ public class BotInventoryGenerator(
GenerateEquipment(
new GenerateEquipmentProperties
{
RootEquipmentSlot = EquipmentSlots.Earpiece,
RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.Earpiece],
RootEquipmentSlot = EquipmentSlots.TacticalVest,
RootEquipmentPool = templateInventory.Equipment[EquipmentSlots.TacticalVest],
ModPool = templateInventory.Mods,
SpawnChances = wornItemChances,
BotData = new BotData { Role = botRole, Level = botLevel, EquipmentRole = botEquipmentRole },
Inventory = botInventory,
BotEquipmentConfig = botEquipConfig,
RandomisationDetails = randomistionDetails,
GeneratingPlayerLevel = pmcProfile.Info.Level,
GeneratingPlayerLevel = pmcProfile?.Info?.Level ?? 1,
}
);
}
@@ -395,7 +397,7 @@ public class BotInventoryGenerator(
var shouldSpawn = _randomUtil.GetChance100(spawnChance ?? 0);
if (shouldSpawn && settings.RootEquipmentPool.Any())
{
var pickedItemDb = new TemplateItem();
TemplateItem pickedItemDb = null;
var found = false;
// Limit attempts to find a compatible item as it's expensive to check them all
@@ -465,7 +467,7 @@ public class BotInventoryGenerator(
var botEquipBlacklist = _botEquipmentFilterService.GetBotEquipmentBlacklist(
settings.BotData.EquipmentRole,
(double)settings.GeneratingPlayerLevel
settings.GeneratingPlayerLevel.Value
);
// Edge case: Filter the armor items mod pool if bot exists in config dict + config has armor slot
@@ -479,11 +481,10 @@ public class BotInventoryGenerator(
botEquipBlacklist.Equipment
);
}
var itemIsOnGenerateModBlacklist = settings.GenerateModsBlacklist != null && settings.GenerateModsBlacklist.Contains(pickedItemDb.Id);
// Does item have slots for sub-mods to be inserted into
if (pickedItemDb.Properties?.Slots?.Count > 0
&& settings?.GenerateModsBlacklist is not null
&& !settings.GenerateModsBlacklist.Contains(pickedItemDb.Id))
&& !itemIsOnGenerateModBlacklist)
{
var childItemsToAdd = _botEquipmentModGenerator.GenerateModsForEquipment(
[item],
@@ -27,6 +27,11 @@ public class BotLevelGenerator(
/// <returns>IRandomisedBotLevelResult object</returns>
public RandomisedBotLevelResult GenerateBotLevel(MinMax levelDetails, BotGenerationDetails botGenerationDetails, BotBase bot)
{
if (!botGenerationDetails.IsPmc.GetValueOrDefault(false))
{
return new RandomisedBotLevelResult() { Exp = 0, Level = 1 };
}
var expTable = _databaseService.GetGlobals().Configuration.Exp.Level.ExperienceTable;
var botLevelRange = GetRelativeBotLevelRange(botGenerationDetails, levelDetails, expTable.Length);
@@ -74,6 +79,9 @@ public class BotLevelGenerator(
)
: Math.Min(levelDetails.Min.Value, maxAvailableLevel); // Not pmc with override or non-pmc
// Force min level to be 1
minPossibleLevel = Math.Max(1, minPossibleLevel);
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
@@ -33,7 +33,7 @@ public class BotWeaponGenerator(
IEnumerable<IInventoryMagGen> inventoryMagGenComponents
)
{
protected List<IInventoryMagGen> _inventoryMagGenComponents = MagGenSetUp(inventoryMagGenComponents);
protected IEnumerable<IInventoryMagGen> _inventoryMagGenComponents = MagGenSetUp(inventoryMagGenComponents);
protected BotConfig _botConfig = _configServer.GetConfig<BotConfig>();
protected PmcConfig _pmcConfig = _configServer.GetConfig<PmcConfig>();
protected RepairConfig _repairConfig = _configServer.GetConfig<RepairConfig>();
@@ -42,13 +42,8 @@ public class BotWeaponGenerator(
private static List<IInventoryMagGen> MagGenSetUp(IEnumerable<IInventoryMagGen> components)
{
var inventoryMagGens = components.ToList();
inventoryMagGens.ToList()
.Sort(
(a, b) =>
a.GetPriority() -
b.GetPriority()
);
return inventoryMagGens.ToList();
inventoryMagGens.Sort((a, b) => a.GetPriority() - b.GetPriority());
return inventoryMagGens;
}
/// <summary>
@@ -398,7 +393,7 @@ public class BotWeaponGenerator(
return;
}
var isInternalMag = magTemplate.Properties.ReloadMagType == "InternalMagazine";
var ammoTemplate = _itemHelper.GetItem(generatedWeaponResult.ChosenAmmoTemplate).Value;
if (ammoTemplate is null)
{
@@ -722,7 +717,6 @@ public class BotWeaponGenerator(
/// <param name="weaponWithMods">Weapon items list to amend</param>
/// <param name="magazine">Magazine item details we're adding cartridges to</param>
/// <param name="chosenAmmoTpl">Cartridge to put into the magazine</param>
/// <param name="newStackSize">How many cartridges should go into the magazine</param>
/// <param name="magazineTemplate">Magazines db template</param>
protected void AddOrUpdateMagazinesChildWithAmmo(List<Item> weaponWithMods, Item magazine, string chosenAmmoTpl, TemplateItem magazineTemplate)
{
@@ -736,8 +730,7 @@ public class BotWeaponGenerator(
}
// Create array with just magazine
List<Item> magazineWithCartridges = new();
magazineWithCartridges.AddRange(magazine);
List<Item> magazineWithCartridges = [magazine];
// Add full cartridge child items to above array
_itemHelper.FillMagazineWithCartridge(magazineWithCartridges, magazineTemplate, chosenAmmoTpl, 1);
@@ -7,6 +7,7 @@ using Core.Models.Utils;
using Core.Servers;
using Core.Services;
using Core.Utils;
using Core.Utils.Cloners;
namespace Core.Generators;
@@ -22,7 +23,8 @@ public class FenceBaseAssortGenerator(
SeasonalEventService seasonalEventService,
LocalisationService localisationService,
ConfigServer configServer,
FenceService fenceService
FenceService fenceService,
ICloner _cloner
)
{
protected TraderConfig traderConfig = configServer.GetConfig<TraderConfig>();
@@ -147,7 +149,7 @@ public class FenceBaseAssortGenerator(
}
// Construct preset + mods
var itemAndChildren = itemHelper.ReplaceIDs(defaultPreset.Items);
var itemAndChildren = itemHelper.ReplaceIDs(_cloner.Clone(defaultPreset.Items));
// Find root item and add some properties to it
for (var i = 0; i < itemAndChildren.Count; i++)
@@ -1,20 +0,0 @@
using SptCommon.Annotations;
using Core.Models.Eft.Common;
namespace Core.Generators;
[Injectable]
public class LocationGenerator
{
public StaticContainerProps GenerateContainerLoot(StaticContainerProps containerIn, List<StaticForcedProps> staticForced,
Dictionary<string, StaticLootDetails> staticLootDist, Dictionary<string, List<StaticAmmoDetails>> staticAmmoDist, string locationName)
{
throw new NotImplementedException();
}
public List<SpawnpointTemplate> GenerateDynamicLoot(LooseLoot dynamicLootDist, Dictionary<string, List<StaticAmmoDetails>> staticAmmoDist,
string locationName)
{
throw new NotImplementedException();
}
}
@@ -2,6 +2,7 @@ using System.Text.Json.Serialization;
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.Models.Utils;
using Core.Servers;
@@ -18,9 +19,12 @@ public class LocationLootGenerator(
ISptLogger<LocationLootGenerator> _logger,
RandomUtil _randomUtil,
MathUtil _mathUtil,
HashUtil _hashUtil,
ItemHelper _itemHelper,
InventoryHelper _inventoryHelper,
DatabaseService _databaseService,
ContainerHelper _containerHelper,
PresetHelper _presetHelper,
LocalisationService _localisationService,
SeasonalEventService _seasonalEventService,
ItemFilterService _itemFilterService,
@@ -294,7 +298,7 @@ public class LocationLootGenerator(
containerDistribution.Add(new ProbabilityObject<string, double>(x, value, value));
}
chosenContainerIds.AddRange(containerDistribution.Draw(containerData.ChosenCount));
chosenContainerIds.AddRange(containerDistribution.Draw((int)containerData.ChosenCount));
return chosenContainerIds;
}
@@ -379,12 +383,97 @@ public class LocationLootGenerator(
/// <param name="locationName">Name of the map to generate static loot for</param>
/// <returns>StaticContainerData</returns>
protected StaticContainerData AddLootToContainer(StaticContainerData staticContainer,
List<SpawnpointTemplate> staticForced,
List<StaticForced>? staticForced,
Dictionary<string, StaticLootDetails> staticLootDist,
Dictionary<string, List<StaticAmmoDetails>> staticAmmoDist, string locationName
)
{
throw new NotImplementedException();
var containerClone = _cloner.Clone(staticContainer);
var containerTpl = containerClone.Template.Items[0].Template;
// Create new unique parent id to prevent any collisions
var parentId = _hashUtil.Generate();
containerClone.Template.Root = parentId;
containerClone.Template.Items[0].Id = parentId;
var containerMap = GetContainerMapping(containerTpl);
// Choose count of items to add to container
var itemCountToAdd = GetWeightedCountOfContainerItems(containerTpl, staticLootDist, locationName);
// Get all possible loot items for container
var containerLootPool = GetPossibleLootItemsForContainer(containerTpl, staticLootDist);
// Some containers need to have items forced into it (quest keys etc)
var tplsForced = staticForced
.Where((forcedStaticProp) => forcedStaticProp.ContainerId == containerClone.Template.Id)
.Select((x) => x.ItemTpl);
// Draw random loot
// Allow money to spawn more than once in container
var failedToFitCount = 0;
var locklist = _itemHelper.GetMoneyTpls();
// Choose items to add to container, factor in weighting + lock money down
// Filter out items picked that're already in the above `tplsForced` array
var chosenTpls = containerLootPool
.Draw(itemCountToAdd, _locationConfig.AllowDuplicateItemsInStaticContainers, locklist)
.Where((tpl) => !tplsForced.Contains(tpl));
// Add forced loot to chosen item pool
var tplsToAddToContainer = tplsForced.Concat(chosenTpls);
foreach (var tplToAdd in tplsToAddToContainer)
{
var chosenItemWithChildren = CreateStaticLootItem(tplToAdd, staticAmmoDist, parentId);
if (chosenItemWithChildren is null)
{
continue;
}
var items = _locationConfig.TplsToStripChildItemsFrom.Contains(tplToAdd)
? [chosenItemWithChildren.Items[0]] // Strip children from parent
: chosenItemWithChildren.Items;
var width = chosenItemWithChildren.Width;
var height = chosenItemWithChildren.Height;
// look for open slot to put chosen item into
var result = _containerHelper.FindSlotForItem(containerMap, (int)width, (int)height);
if (!result.Success.GetValueOrDefault(false))
{
if (failedToFitCount >= _locationConfig.FitLootIntoContainerAttempts)
{
// x attempts to fit an item, container is probably full, stop trying to add more
break;
}
// Can't fit item, skip
failedToFitCount++;
continue;
}
_containerHelper.FillContainerMapWithItem(
containerMap,
result.X.Value,
result.Y.Value,
(int)width,
(int)height,
result.Rotation.GetValueOrDefault(false)
);
var rotation = result.Rotation.GetValueOrDefault(false) ? 1 : 0;
items[0].SlotId = "main";
items[0].Location = new ItemLocation{ X = result.X, Y = result.Y, R = rotation };
// Add loot to container before returning
foreach (var item in items)
{
containerClone.Template.Items.Add(item);
}
}
return containerClone;
}
/// <summary>
@@ -452,7 +541,7 @@ public class LocationLootGenerator(
/// <param name="containerTypeId">Container to get possible loot for</param>
/// <param name="staticLootDist">staticLoot.json</param>
/// <returns>ProbabilityObjectArray of item tpls + probabilty</returns>
protected object GetPossibleLootItemsForContainer(string containerTypeId,
protected ProbabilityObjectArray<ProbabilityObject<string, float?>, string, float?> GetPossibleLootItemsForContainer(string containerTypeId,
Dictionary<string, StaticLootDetails> staticLootDist) // TODO: Type Fuckery, return type was ProbabilityObjectArray<string, number>
{
var seasonalEventActive = _seasonalEventService.SeasonalEventEnabled();
@@ -510,7 +599,189 @@ public class LocationLootGenerator(
Dictionary<string, List<StaticAmmoDetails>> staticAmmoDist,
string locationName)
{
throw new NotImplementedException();
List<SpawnpointTemplate> loot = [];
List<Spawnpoint> dynamicForcedSpawnPoints = [];
// Remove christmas items from loot data
if (!_seasonalEventService.ChristmasEventEnabled())
{
dynamicLootDist.Spawnpoints = dynamicLootDist.Spawnpoints.Where(
(point) => !point.Template.Id.StartsWith("christmas")
)
.ToList();
dynamicLootDist.SpawnpointsForced = dynamicLootDist.SpawnpointsForced.Where(
(point) => !point.Template.Id.StartsWith("christmas")
)
.ToList();
}
// Build the list of forced loot from both `spawnpointsForced` and any point marked `IsAlwaysSpawn`
dynamicForcedSpawnPoints.AddRange(dynamicLootDist.SpawnpointsForced);
dynamicForcedSpawnPoints.AddRange(dynamicLootDist.Spawnpoints.Where((point) => point.Template.IsAlwaysSpawn ?? false));
// Add forced loot
AddForcedLoot(loot, dynamicForcedSpawnPoints, locationName, staticAmmoDist);
var allDynamicSpawnpoints = dynamicLootDist.Spawnpoints;
// Draw from random distribution
var desiredSpawnpointCount = Math.Round(
GetLooseLootMultiplerForLocation(locationName) *
_randomUtil.GetNormallyDistributedRandomNumber(
(double)dynamicLootDist.SpawnpointCount.Mean,
(double)dynamicLootDist.SpawnpointCount.Std
)
);
// Positions not in forced but have 100% chance to spawn
List<Spawnpoint> guaranteedLoosePoints = [];
var blacklistedSpawnpoints = _locationConfig.LooseLootBlacklist.GetValueOrDefault(locationName);
var spawnpointArray = new ProbabilityObjectArray<ProbabilityObject<string, Spawnpoint>, string, Spawnpoint>(_mathUtil, _cloner, []);
foreach (var spawnpoint in allDynamicSpawnpoints)
{
// Point is blacklsited, skip
if (blacklistedSpawnpoints?.Contains(spawnpoint.Template.Id) ?? false)
{
_logger.Debug($"Ignoring loose loot location: {spawnpoint.Template.Id}");
continue;
}
// We've handled IsAlwaysSpawn above, so skip them
if (spawnpoint.Template.IsAlwaysSpawn ?? false)
{
continue;
}
// 100%, add it to guaranteed
if (spawnpoint.Probability == 1)
{
guaranteedLoosePoints.Add(spawnpoint);
continue;
}
spawnpointArray.Add(new ProbabilityObject<string, Spawnpoint>(spawnpoint.Template.Id, spawnpoint.Probability ?? 0, spawnpoint));
}
// Select a number of spawn points to add loot to
// Add ALL loose loot with 100% chance to pool
List<Spawnpoint> chosenSpawnpoints = [];
chosenSpawnpoints.AddRange(guaranteedLoosePoints);
var randomSpawnpointCount = desiredSpawnpointCount - chosenSpawnpoints.Count;
// Only draw random spawn points if needed
if (randomSpawnpointCount > 0 && spawnpointArray.Count > 0)
{
// Add randomly chosen spawn points
foreach (var si in spawnpointArray.Draw((int)randomSpawnpointCount, false))
{
chosenSpawnpoints.Add(spawnpointArray.Data(si));
}
}
// Filter out duplicate locationIds // prob can be done better
chosenSpawnpoints = chosenSpawnpoints.GroupBy(spawnpoint => spawnpoint.LocationId).Select(group => group.First()).ToList();
// Do we have enough items in pool to fulfill requirement
var tooManySpawnPointsRequested = desiredSpawnpointCount - chosenSpawnpoints.Count > 0;
if (tooManySpawnPointsRequested)
{
_logger.Debug(
_localisationService.GetText(
"location-spawn_point_count_requested_vs_found",
new
{
requested = desiredSpawnpointCount + guaranteedLoosePoints.Count,
found = chosenSpawnpoints.Count,
mapName = locationName,
}
)
);
}
// Iterate over spawnpoints
var seasonalEventActive = _seasonalEventService.SeasonalEventEnabled();
var seasonalItemTplBlacklist = _seasonalEventService.GetInactiveSeasonalEventItems();
foreach (var spawnPoint in chosenSpawnpoints)
{
// Spawnpoint is invalid, skip it
if (spawnPoint.Template is null)
{
_logger.Warning(
_localisationService.GetText("location-missing_dynamic_template", spawnPoint.LocationId)
);
continue;
}
// Ensure no blacklisted lootable items are in pool
spawnPoint.Template.Items = spawnPoint.Template.Items.Where(
(item) => !_itemFilterService.IsLootableItemBlacklisted(item.Template)
)
.ToList();
// Ensure no seasonal items are in pool if not in-season
if (!seasonalEventActive)
{
spawnPoint.Template.Items = spawnPoint.Template.Items.Where(
(item) => !seasonalItemTplBlacklist.Contains(item.Template)
)
.ToList();
}
// Spawn point has no items after filtering, skip
if (spawnPoint.Template.Items is null || spawnPoint.Template.Items.Count == 0)
{
_logger.Warning(
_localisationService.GetText("location-spawnpoint_missing_items", spawnPoint.Template.Id)
);
continue;
}
// Get an array of allowed IDs after above filtering has occured
var validItemIds = spawnPoint.Template.Items.Select((item) => item.Id).ToList();
// Construct container to hold above filtered items, letting us pick an item for the spot
var itemArray = new ProbabilityObjectArray<ProbabilityObject<string, double?>, string, double?>(_mathUtil, _cloner, []);
foreach (var itemDist in spawnPoint.ItemDistribution)
{
if (!validItemIds.Contains(itemDist.ComposedKey.Key))
{
continue;
}
itemArray.Add(new ProbabilityObject<string, double?>(itemDist.ComposedKey.Key, itemDist.RelativeProbability ?? 0, null));
}
if (itemArray.Count == 0)
{
_logger.Warning(
_localisationService.GetText("location-loot_pool_is_empty_skipping", spawnPoint.Template.Id)
);
continue;
}
// Draw a random item from spawn points possible items
var chosenComposedKey = itemArray.Draw(1).FirstOrDefault();
var createItemResult = CreateDynamicLootItem(
chosenComposedKey,
spawnPoint.Template.Items,
staticAmmoDist
);
// Root id can change when generating a weapon, ensure ids match
spawnPoint.Template.Root = createItemResult.Items.FirstOrDefault().Id;
// Overwrite entire pool with chosen item
spawnPoint.Template.Items = createItemResult.Items;
loot.Add(spawnPoint.Template);
}
return loot;
}
/// <summary>
@@ -519,39 +790,406 @@ public class LocationLootGenerator(
/// <param name="lootLocationTemplates">List to add forced loot spawn locations to</param>
/// <param name="forcedSpawnPoints">Forced loot locations that must be added</param>
/// <param name="locationName">Name of map currently having force loot created for</param>
protected void addForcedLoot(List<SpawnpointTemplate> lootLocationTemplates,
List<SpawnpointsForced> forcedSpawnPoints, string locationName,
protected void AddForcedLoot(List<SpawnpointTemplate> lootLocationTemplates,
List<Spawnpoint> forcedSpawnPoints, string locationName,
Dictionary<string, List<StaticAmmoDetails>> staticAmmoDist)
{
throw new NotImplementedException();
var lootToForceSingleAmountOnMap = _locationConfig.ForcedLootSingleSpawnById.GetValueOrDefault(locationName);
if (lootToForceSingleAmountOnMap is not null)
{
// Process loot items defined as requiring only 1 spawn position as they appear in multiple positions on the map
foreach (var itemTpl in lootToForceSingleAmountOnMap)
{
// Get all spawn positions for item tpl in forced loot array
var items = forcedSpawnPoints.Where(
(forcedSpawnPoint) => forcedSpawnPoint.Template.Items[0].Template == itemTpl
);
if (items is null || !items.Any())
{
_logger.Debug($"Unable to adjust loot item {itemTpl} as it does not exist inside {locationName} forced loot.");
continue;
}
// Create probability array of all spawn positions for this spawn id
var spawnpointArray = new ProbabilityObjectArray<ProbabilityObject<string, Spawnpoint>, string, Spawnpoint>(_mathUtil, _cloner, []);
foreach (var si in items)
{
// use locationId as template.Id is the same across all items
spawnpointArray.Add(new ProbabilityObject<string, Spawnpoint>(si.LocationId, si.Probability ?? 0, si));
}
// Choose 1 out of all found spawn positions for spawn id and add to loot array
foreach (var spawnPointLocationId in spawnpointArray.Draw(1, false))
{
var itemToAdd = items.FirstOrDefault((item) => item.LocationId == spawnPointLocationId);
var lootItem = itemToAdd?.Template;
if (lootItem is null)
{
_logger.Warning($"Item with spawn point id {spawnPointLocationId} could not be found, skipping");
continue;
}
var createItemResult = CreateDynamicLootItem(
lootItem.Items.FirstOrDefault().Id,
lootItem.Items,
staticAmmoDist
);
// Update root ID with the dynamically generated ID
lootItem.Root = createItemResult.Items.FirstOrDefault().Id;
lootItem.Items = createItemResult.Items;
lootLocationTemplates.Add(lootItem);
}
}
}
var seasonalEventActive = _seasonalEventService.SeasonalEventEnabled();
var seasonalItemTplBlacklist = _seasonalEventService.GetInactiveSeasonalEventItems();
// Add remaining forced loot to array
foreach (var forcedLootLocation in forcedSpawnPoints)
{
var firstLootItemTpl = forcedLootLocation.Template.Items.FirstOrDefault().Template;
// Skip spawn positions processed already
if (lootToForceSingleAmountOnMap?.Contains(firstLootItemTpl) ?? false)
{
continue;
}
// Skip adding seasonal items when seasonal event is not active
if (!seasonalEventActive && seasonalItemTplBlacklist.Contains(firstLootItemTpl))
{
continue;
}
var locationTemplateToAdd = forcedLootLocation.Template;
var createItemResult = CreateDynamicLootItem(
locationTemplateToAdd.Items.FirstOrDefault().Id,
forcedLootLocation.Template.Items,
staticAmmoDist
);
// Update root ID with the dynamically generated ID
forcedLootLocation.Template.Root = createItemResult.Items.FirstOrDefault().Id;
forcedLootLocation.Template.Items = createItemResult.Items;
// Push forced location into array as long as it doesnt exist already
var existingLocation = lootLocationTemplates.Any(
(spawnPoint) => spawnPoint.Id == locationTemplateToAdd.Id
);
if (!existingLocation)
{
lootLocationTemplates.Add(locationTemplateToAdd);
}
else
{
_logger.Debug(
$"Attempted to add a forced loot location with Id: {locationTemplateToAdd.Id} to map {locationName} that already has that id in use, skipping"
);
}
}
}
// TODO: rewrite, BIG yikes
protected ContainerItem CreateStaticLootItem(string chosenTemplate,
Dictionary<string, List<StaticAmmoDetails>> staticAmmoDistribution,
string? parentIdentifier = null)
private ContainerItem CreateDynamicLootItem(string? chosenComposedKey, List<Item> items, Dictionary<string, List<StaticAmmoDetails>> staticAmmoDist)
{
throw new NotImplementedException();
var chosenItem = items.FirstOrDefault((item) => item.Id == chosenComposedKey);
var chosenTpl = chosenItem?.Template;
if (chosenTpl is null) {
throw new Exception($"Item for tpl {chosenComposedKey} was not found in the spawn point");
}
var itemTemplate = _itemHelper.GetItem(chosenTpl).Value;
if (itemTemplate is null) {
_logger.Error($"Item tpl: {chosenTpl} cannot be found in database");
}
// Item array to return
List<Item> itemWithMods = [];
// Money/Ammo - don't rely on items in spawnPoint.template.Items so we can randomise it ourselves
if (_itemHelper.IsOfBaseclasses(chosenTpl, [BaseClasses.MONEY, BaseClasses.AMMO])) {
var stackCount =
itemTemplate.Properties.StackMaxSize == 1
? 1
: _randomUtil.GetInt((int)itemTemplate.Properties.StackMinRandom, (int)itemTemplate.Properties.StackMaxRandom);
itemWithMods.Add(new Item {
Id = _hashUtil.Generate(),
Template = chosenTpl,
Upd = new Upd { StackObjectsCount = stackCount }
});
} else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.AMMO_BOX)) {
// Fill with cartridges
List<Item> ammoBoxItem = [ new Item { Id = _hashUtil.Generate(), Template = chosenTpl }];
_itemHelper.AddCartridgesToAmmoBox(ammoBoxItem, itemTemplate);
itemWithMods.AddRange(ammoBoxItem);
} else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.MAGAZINE)) {
// Create array with just magazine
List<Item> magazineItem = [new Item { Id = _hashUtil.Generate(), Template = chosenTpl }];
if (_randomUtil.GetChance100(_locationConfig.StaticMagazineLootHasAmmoChancePercent)) {
// Add randomised amount of cartridges
_itemHelper.FillMagazineWithRandomCartridge(
magazineItem,
itemTemplate, // Magazine template
staticAmmoDist,
null,
_locationConfig.MinFillLooseMagazinePercent / 100
);
}
itemWithMods.AddRange(magazineItem);
} else {
// Also used by armors to get child mods
// Get item + children and add into array we return
var itemWithChildren = _itemHelper.FindAndReturnChildrenAsItems(items, chosenItem.Id);
// Ensure all IDs are unique
itemWithChildren = _itemHelper.ReplaceIDs(_cloner.Clone(itemWithChildren));
if (_locationConfig.TplsToStripChildItemsFrom.Contains(chosenItem.Template)) {
// Strip children from parent before adding
itemWithChildren = [itemWithChildren[0]];
}
itemWithMods.AddRange(itemWithChildren);
}
// Get inventory size of item
var size = _itemHelper.GetItemSize(itemWithMods, itemWithMods[0].Id);
return new ContainerItem { Items = itemWithMods, Width = size.Width, Height = size.Height };
}
private double GetLooseLootMultiplerForLocation(string location)
{
return _locationConfig.LooseLootMultiplier[location];
}
protected double getStaticLootMultiplerForLocation(string location) {
return _locationConfig.StaticLootMultiplier[location];
}
// TODO: rewrite, BIG yikes
protected ContainerItem? CreateStaticLootItem(
string chosenTpl,
Dictionary<string, List<StaticAmmoDetails>> staticAmmoDist,
string? parentId = null)
{
var itemTemplate = _itemHelper.GetItem(chosenTpl).Value;
if (itemTemplate.Properties is null)
{
_logger.Error($"Unable to process item: ${{chosenTpl}}. it lacks _props");
return null;
}
var width = itemTemplate.Properties.Width;
var height = itemTemplate.Properties.Height;
List<Item> items = [new Item { Id = _hashUtil.Generate(), Template = chosenTpl }];
var rootItem = items.FirstOrDefault();
// Use passed in parentId as override for new item
if (!string.IsNullOrEmpty(parentId))
{
rootItem.ParentId = parentId;
}
if (
_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.MONEY) ||
_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.AMMO)
)
{
// Edge case - some ammos e.g. flares or M406 grenades shouldn't be stacked
var stackCount = itemTemplate.Properties.StackMaxSize == 1
? 1
: _randomUtil.GetInt((int)(itemTemplate.Properties.StackMinRandom), (int)(itemTemplate.Properties.StackMaxRandom));
rootItem.Upd = new Upd { StackObjectsCount = stackCount };
}
// No spawn point, use default template
else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.WEAPON))
{
List<Item> children = [];
var defaultPreset = _cloner.Clone(_presetHelper.GetDefaultPreset(chosenTpl));
if (defaultPreset?.Items is not null)
{
try
{
children = _itemHelper.ReparentItemAndChildren(defaultPreset.Items[0], defaultPreset.Items);
}
catch (Exception e)
{
// this item already broke it once without being reproducible tpl = "5839a40f24597726f856b511"; AKS-74UB Default
// 5ea03f7400685063ec28bfa8 // ppsh default
// 5ba26383d4351e00334c93d9 //mp7_devgru
_logger.Error(
_localisationService.GetText(
"location-preset_not_found",
new
{
tpl = chosenTpl,
defaultId = defaultPreset.Id,
defaultName = defaultPreset.Name,
parentId,
}
)
);
throw;
}
}
else
{
// RSP30 (62178be9d0050232da3485d9/624c0b3340357b5f566e8766/6217726288ed9f0845317459) doesn't have any default presets and kills this code below as it has no chidren to reparent
_logger.Debug($"createStaticLootItem() No preset found for weapon: {chosenTpl}");
}
rootItem = items[0];
if (rootItem is null)
{
_logger.Error(
_localisationService.GetText(
"location-missing_root_item",
new
{
tpl = chosenTpl,
parentId,
}
)
);
throw new Exception(_localisationService.GetText("location-critical_error_see_log"));
}
try
{
if (children?.Count > 0)
{
items = _itemHelper.ReparentItemAndChildren(rootItem, children);
}
}
catch (Exception e)
{
_logger.Error(
_localisationService.GetText(
"location-unable_to_reparent_item",
new
{
tpl = chosenTpl,
parentId = parentId,
}
)
);
throw;
}
// Here we should use generalized BotGenerators functions e.g. fillExistingMagazines in the future since
// it can handle revolver ammo (it's not restructured to be used here yet.)
// General: Make a WeaponController for Ragfair preset stuff and the generating weapons and ammo stuff from
// BotGenerator
var magazine = items.FirstOrDefault(item => item.SlotId == "mod_magazine");
// some weapon presets come without magazine; only fill the mag if it exists
if (magazine is not null)
{
var magTemplate = _itemHelper.GetItem(magazine.Template).Value;
var weaponTemplate = _itemHelper.GetItem(chosenTpl).Value;
// Create array with just magazine
var defaultWeapon = _itemHelper.GetItem(rootItem.Template).Value;
List<Item> magazineWithCartridges = [magazine];
_itemHelper.FillMagazineWithRandomCartridge(
magazineWithCartridges,
magTemplate,
staticAmmoDist,
weaponTemplate.Properties.AmmoCaliber,
0.25,
defaultWeapon.Properties.DefAmmo,
defaultWeapon
);
// Replace existing magazine with above array
items.Remove(magazine);
items.AddRange(magazineWithCartridges);
}
var size = _itemHelper.GetItemSize(items, rootItem.Id);
width = size.Width;
height = size.Height;
}
// No spawnpoint to fall back on, generate manually
else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.AMMO_BOX))
{
_itemHelper.AddCartridgesToAmmoBox(items, itemTemplate);
}
else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.MAGAZINE))
{
if (_randomUtil.GetChance100(_locationConfig.MagazineLootHasAmmoChancePercent))
{
// Create array with just magazine
List<Item> magazineWithCartridges = [rootItem];
_itemHelper.FillMagazineWithRandomCartridge(
magazineWithCartridges,
itemTemplate,
staticAmmoDist,
null,
_locationConfig.MinFillStaticMagazinePercent / 100
);
// Replace existing magazine with above array
items.Remove(rootItem);
items.AddRange(magazineWithCartridges);
}
}
else if (_itemHelper.ArmorItemCanHoldMods(chosenTpl))
{
var defaultPreset = _presetHelper.GetDefaultPreset(chosenTpl);
if (defaultPreset is not null)
{
List<Item> presetAndMods = _itemHelper.ReplaceIDs(_cloner.Clone(defaultPreset.Items));
_itemHelper.RemapRootItemId(presetAndMods);
// Use original items parentId otherwise item doesnt get added to container correctly
presetAndMods[0].ParentId = rootItem.ParentId;
items = presetAndMods;
}
else
{
// We make base item above, at start of function, no need to do it here
if ((itemTemplate.Properties.Slots?.Count ?? 0) > 0)
{
items = _itemHelper.AddChildSlotItems(
items,
itemTemplate,
_locationConfig.EquipmentLootSettings.ModSpawnChancePercent
);
}
}
}
return new ContainerItem { Items = items, Width = width, Height = height };
}
}
public class ContainerGroupCount
{
[JsonPropertyName("containerIdsWithProbability")]
public Dictionary<string, double> ContainerIdsWithProbability { get; set; }
public Dictionary<string, double>? ContainerIdsWithProbability { get; set; }
[JsonPropertyName("chosenCount")]
public int ChosenCount { get; set; }
public double? ChosenCount { get; set; }
}
public class ContainerItem
{
[JsonPropertyName("items")]
public List<Item> Items { get; set; }
public List<Item>? Items { get; set; }
[JsonPropertyName("width")]
public int Width { get; set; }
public double? Width { get; set; }
[JsonPropertyName("height")]
public int Height { get; set; }
public double? Height { get; set; }
}
+454 -21
View File
@@ -1,15 +1,32 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
using Core.Helpers;
using SptCommon.Annotations;
using Core.Models.Common;
using Core.Models.Eft.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Enums;
using Core.Models.Spt.Config;
using Core.Models.Spt.Services;
using Core.Models.Utils;
using Core.Services;
using Core.Utils;
using Core.Utils.Cloners;
namespace Core.Generators;
[Injectable]
public class LootGenerator(
ISptLogger<LootGenerator> _logger,
RandomUtil _randomUtil,
HashUtil _hashUtil,
ItemHelper _itemHelper,
PresetHelper _presetHelper,
DatabaseService _databaseService,
ItemFilterService _itemFilterService,
LocalisationService _localisationService,
WeightedRandomHelper _weightedRandomHelper,
RagfairLinkedItemService _ragfairLinkedItemService,
ICloner _cloner
)
{
@@ -20,7 +37,105 @@ public class LootGenerator(
/// <returns>An array of loot items</returns>
public List<Item> CreateRandomLoot(LootRequest options)
{
throw new NotImplementedException();
var result = new List<Item>();
var itemTypeCounts = InitItemLimitCounter(options.ItemLimits);
// Handle sealed weapon containers
var sealedWeaponCrateCount = _randomUtil.GetDouble(
options.WeaponCrateCount.Min.Value,
options.WeaponCrateCount.Max.Value);
if (sealedWeaponCrateCount > 0) {
// Get list of all sealed containers from db - they're all the same, just for flavor
var itemsDb = _itemHelper.GetItems();
var sealedWeaponContainerPool = (itemsDb).Where((item) =>
item.Name.Contains("event_container_airdrop"));
for (var index = 0; index < sealedWeaponCrateCount; index++) {
// Choose one at random + add to results array
var chosenSealedContainer = _randomUtil.GetArrayValue(sealedWeaponContainerPool);
result.Add( new Item{
Id = _hashUtil.Generate(),
Template = chosenSealedContainer.Id,
Upd = new Upd{
StackObjectsCount = 1,
SpawnedInSession = true
},
});
}
}
// Get items from items.json that have a type of item + not in global blacklist + base type is in whitelist
var rewardPoolResults = GetItemRewardPool(
options.ItemBlacklist,
options.ItemTypeWhitelist,
options.UseRewardItemBlacklist.GetValueOrDefault(false),
options.AllowBossItems.GetValueOrDefault(false));
// Pool has items we could add as loot, proceed
if (rewardPoolResults.ItemPool.Count > 0) {
var randomisedItemCount = _randomUtil.GetDouble(options.ItemCount.Min.Value, options.ItemCount.Max.Value);
for (var index = 0; index < randomisedItemCount; index++) {
if (!FindAndAddRandomItemToLoot(rewardPoolResults.ItemPool, itemTypeCounts, options, result)) {
// Failed to add, reduce index so we get another attempt
index--;
}
}
}
var globalDefaultPresets = _presetHelper.GetDefaultPresets().Values;
// Filter default presets to just weapons
var randomisedWeaponPresetCount = _randomUtil.GetDouble(
options.WeaponPresetCount.Min.Value,
options.WeaponPresetCount.Max.Value);
if (randomisedWeaponPresetCount > 0) {
var weaponDefaultPresets = globalDefaultPresets.Where((preset) =>
_itemHelper.IsOfBaseclass(preset.Encyclopedia, BaseClasses.WEAPON)).ToList();
if (weaponDefaultPresets.Any()) {
for (var index = 0; index < randomisedWeaponPresetCount; index++) {
if (
!FindAndAddRandomPresetToLoot(
weaponDefaultPresets,
itemTypeCounts,
rewardPoolResults.Blacklist,
result)
) {
// Failed to add, reduce index so we get another attempt
index--;
}
}
}
}
// Filter default presets to just armors and then filter again by protection level
var randomisedArmorPresetCount = _randomUtil.GetDouble(
options.ArmorPresetCount.Min.Value,
options.ArmorPresetCount.Max.Value);
if (randomisedArmorPresetCount > 0) {
var armorDefaultPresets = globalDefaultPresets.Where((preset) =>
_itemHelper.ArmorItemCanHoldMods(preset.Encyclopedia));
var levelFilteredArmorPresets = armorDefaultPresets.Where((armor) =>
IsArmorOfDesiredProtectionLevel(armor, options)).ToList();
// Add some armors to rewards
if (levelFilteredArmorPresets.Any()) {
for (var index = 0; index < randomisedArmorPresetCount; index++) {
if (
!FindAndAddRandomPresetToLoot(
levelFilteredArmorPresets,
itemTypeCounts,
rewardPoolResults.Blacklist,
result)
) {
// Failed to add, reduce index so we get another attempt
index--;
}
}
}
}
return result;
}
/// <summary>
@@ -31,7 +146,29 @@ public class LootGenerator(
/// <returns>Array of Item</returns>
public List<Item> CreateForcedLoot(Dictionary<string, MinMax> forcedLootDict)
{
throw new NotImplementedException();
var result = new List<Item>();
var forcedItems = forcedLootDict;
foreach (var forcedItemKvP in forcedItems) {
var details = forcedLootDict[forcedItemKvP.Key];
var randomisedItemCount = _randomUtil.GetDouble(details.Min.Value, details.Max.Value);
// Add forced loot item to result
var newLootItem = new Item{
Id = _hashUtil.Generate(),
Template = forcedItemKvP.Key,
Upd = new Upd{
StackObjectsCount = randomisedItemCount,
SpawnedInSession = true,
},
};
var splitResults = _itemHelper.SplitStack(newLootItem);
result.AddRange(splitResults);
}
return result;
}
/// <summary>
@@ -42,22 +179,78 @@ public class LootGenerator(
/// <param name="useRewardItemBlacklist">Should item.json reward item config be used</param>
/// <param name="allowBossItems">Should boss items be allowed in result</param>
/// <returns>results of filtering + blacklist used</returns>
protected object GetItemRewardPool(List<string> itemTplBlacklist, List<string> itemTypeWhitelist,
bool useRewardItemBlacklist, // TODO: type fuckery, return type was { itemPool: [string, ITemplateItem][]; blacklist: Set<string> }
protected ItemRewardPoolResults GetItemRewardPool(List<string> itemTplBlacklist, List<string> itemTypeWhitelist,
bool useRewardItemBlacklist,
bool allowBossItems)
{
throw new NotImplementedException();
var itemsDb = _databaseService.GetItems().Values;
var itemBlacklist = new HashSet<string>();
itemBlacklist.UnionWith(_itemFilterService.GetBlacklistedItems());
itemBlacklist.UnionWith(itemTplBlacklist);
if (useRewardItemBlacklist)
{
var itemsToAdd = _itemFilterService.GetItemRewardBlacklist();
// Get all items that match the blacklisted types and fold into item blacklist
var itemTypeBlacklist = _itemFilterService.GetItemRewardBaseTypeBlacklist();
var itemsMatchingTypeBlacklist = (itemsDb)
.Where((templateItem) => _itemHelper.IsOfBaseclasses(templateItem.Parent, itemTypeBlacklist))
.Select((templateItem) => templateItem.Id);
// Clear out blacklist
itemBlacklist = [];
itemBlacklist.UnionWith(itemBlacklist);
itemBlacklist.UnionWith(itemsToAdd);
itemBlacklist.UnionWith(itemsMatchingTypeBlacklist);
}
if (!allowBossItems)
{
foreach (var bossItem in _itemFilterService.GetBossItems()) {
itemBlacklist.Add(bossItem);
}
}
var items = itemsDb.Where(
(item) =>
!itemBlacklist.Contains(item.Id) &&
item.Type.ToLower() == "item" &&
!item.Properties.QuestItem.GetValueOrDefault(false) &&
itemTypeWhitelist.Contains(item.Parent)).ToList();
return new ItemRewardPoolResults{ ItemPool = items, Blacklist = itemBlacklist };
}
public record ItemRewardPoolResults
{
public List<TemplateItem> ItemPool { get; set; }
public HashSet<string> Blacklist { get; set; }
}
/// <summary>
/// Filter armor items by their front plates protection level - top if its a helmet
/// Filter armor items by their front plates protection level - top if it's a helmet
/// </summary>
/// <param name="armor">Armor preset to check</param>
/// <param name="options">Loot request options - armor level etc</param>
/// <returns>True if item has desired armor level</returns>
protected bool ArmorOfDesiredProtectionLevel(Preset armor, LootRequest options)
protected bool IsArmorOfDesiredProtectionLevel(Preset armor, LootRequest options)
{
throw new NotImplementedException();
string[] relevantSlots = ["front_plate", "helmet_top", "soft_armor_front"];
foreach (var slotId in relevantSlots) {
var armorItem = armor.Items.FirstOrDefault((item) => item?.SlotId?.ToLower() == slotId);
if (armorItem is null)
{
continue;
}
var armorDetails = _itemHelper.GetItem(armorItem.Template).Value;
var armorClass = armorDetails.Properties.ArmorClass;
return options.ArmorLevelWhitelist.Contains((int)armorClass.Value);
}
return false;
}
/// <summary>
@@ -65,9 +258,14 @@ public class LootGenerator(
/// </summary>
/// <param name="limits">limits as defined in config</param>
/// <returns>record, key: item tplId, value: current/max item count allowed</returns>
protected Dictionary<string, ItemLimit> InitItemLimitCounter(Dictionary<string, int> limits)
private Dictionary<string, ItemLimit> InitItemLimitCounter(Dictionary<string, double> limits)
{
throw new NotImplementedException();
var itemTypeCounts = new Dictionary<string, ItemLimit>();
foreach (var itemTypeId in limits) {
itemTypeCounts[itemTypeId.Key] = new ItemLimit() { Current = 0, Max = limits[itemTypeId.Key] };
}
return itemTypeCounts;
}
/// <summary>
@@ -78,11 +276,46 @@ public class LootGenerator(
/// <param name="options">item filters</param>
/// <param name="result">array to add found item to</param>
/// <returns>true if item was valid and added to pool</returns>
protected bool FindAndAddRandomItemToLoot(object items, object itemTypeCounts,
LootRequest options, // TODO: items type was [string, ITemplateItem][], itemTypeCounts was Record<string, { current: number; max: number }>
protected bool FindAndAddRandomItemToLoot(List<TemplateItem> items, Dictionary<string, ItemLimit> itemTypeCounts,
LootRequest options,
List<Item> result)
{
throw new NotImplementedException();
var randomItem = _randomUtil.GetArrayValue(items);
var itemLimitCount = itemTypeCounts.TryGetValue(randomItem.Parent, out var randomItemLimitCount);
if (!itemLimitCount && randomItemLimitCount?.Current > randomItemLimitCount?.Max) {
return false;
}
// Skip armors as they need to come from presets
if (_itemHelper.ArmorItemCanHoldMods(randomItem.Id)) {
return false;
}
var newLootItem = new Item {
Id = _hashUtil.Generate(),
Template = randomItem.Id,
Upd = new Upd {
StackObjectsCount = 1,
SpawnedInSession = true,
},
};
// Special case - handle items that need a stackcount > 1
if (randomItem.Properties.StackMaxSize > 1) {
newLootItem.Upd.StackObjectsCount = GetRandomisedStackCount(randomItem, options);
}
newLootItem.Template = randomItem.Id;
result.Add(newLootItem);
if (randomItemLimitCount is not null) {
// Increment item count as it's in limit array
randomItemLimitCount.Current++;
}
// Item added okay
return true;
}
/// <summary>
@@ -93,7 +326,15 @@ public class LootGenerator(
/// <returns>stack count</returns>
protected int GetRandomisedStackCount(TemplateItem item, LootRequest options)
{
throw new NotImplementedException();
var min = item.Properties.StackMinRandom;
var max = item.Properties.StackMaxSize;
if (options.ItemStackLimits.TryGetValue(item.Id, out var itemLimits)) {
min = itemLimits.Min;
max = (int?)itemLimits.Max;
}
return _randomUtil.GetInt((int)(min ?? 1), max ?? 1);
}
/// <summary>
@@ -104,11 +345,66 @@ public class LootGenerator(
/// <param name="itemBlacklist">Items to skip</param>
/// <param name="result">List to add chosen preset to</param>
/// <returns>true if preset was valid and added to pool</returns>
protected bool FindAndAddRandomPresetToLoot(List<Preset> presetPool, object itemTypeCounts,
List<string> itemBlacklist, // TODO: type fuckery, itemTypeCounts was Record<string, { current: number; max: number }>
protected bool FindAndAddRandomPresetToLoot(List<Preset> presetPool,
Dictionary<string, ItemLimit> itemTypeCounts,
HashSet<string> itemBlacklist,
List<Item> result)
{
throw new NotImplementedException();
// Choose random preset and get details from item db using encyclopedia value (encyclopedia === tplId)
var chosenPreset = _randomUtil.GetArrayValue(presetPool);
if (chosenPreset is null ) {
_logger.Warning("Unable to find random preset in given presets, skipping");
return false;
}
// No `_encyclopedia` property, not possible to reliably get root item tpl
if (chosenPreset.Encyclopedia is null) {
_logger.Debug("$Preset with id: {chosenPreset?.Id} lacks encyclopedia property, skipping");
return false;
}
// Get preset root item db details via its `_encyclopedia` property
var itemDbDetails = _itemHelper.GetItem(chosenPreset.Encyclopedia);
if (!itemDbDetails.Key) {
_logger.Debug($"$Unable to find preset with tpl: {chosenPreset.Encyclopedia}, skipping");
return false;
}
// Skip preset if root item is blacklisted
if (itemBlacklist.Contains(chosenPreset.Items[0].Template)) {
return false;
}
// Some custom mod items lack a parent property
if (itemDbDetails.Value.Parent is null) {
_logger.Error(_localisationService.GetText("loot-item_missing_parentid", itemDbDetails.Value?.Name));
return false;
}
// Check chosen preset hasn't exceeded spawn limit
var hasItemLimitCount = itemTypeCounts.TryGetValue(itemDbDetails.Value.Parent, out var itemLimitCount);
if (!hasItemLimitCount && itemLimitCount?.Current > itemLimitCount?.Max) {
return false;
}
var presetAndMods = _itemHelper.ReplaceIDs(_cloner.Clone(chosenPreset.Items));
_itemHelper.RemapRootItemId(presetAndMods);
// Add chosen preset tpl to result array
foreach (var item in presetAndMods) {
result.Add(item);
}
if (itemLimitCount is not null) {
// Increment item count as item has been chosen and its inside itemLimitCount dictionary
itemLimitCount.Current++;
}
// Item added okay
return true;
}
/// <summary>
@@ -118,7 +414,52 @@ public class LootGenerator(
/// <returns>List of items with children lists</returns>
public List<List<Item>> GetSealedWeaponCaseLoot(SealedAirdropContainerSettings containerSettings)
{
throw new NotImplementedException();
List<List<Item>> itemsToReturn = [];
// Choose a weapon to give to the player (weighted)
var chosenWeaponTpl = _weightedRandomHelper.GetWeightedValue<string>(
containerSettings.WeaponRewardWeight
);
// Get itemDb details of weapon
var weaponDetailsDb = _itemHelper.GetItem(chosenWeaponTpl);
if (!weaponDetailsDb.Key) {
_logger.Error(
_localisationService.GetText("loot-non_item_picked_as_sealed_weapon_crate_reward", chosenWeaponTpl)
);
return itemsToReturn;
}
// Get weapon preset - default or choose a random one from globals.json preset pool
var chosenWeaponPreset = containerSettings.DefaultPresetsOnly
? _presetHelper.GetDefaultPreset(chosenWeaponTpl)
: _randomUtil.GetArrayValue(_presetHelper.GetPresets(chosenWeaponTpl));
// No default preset found for weapon, choose a random one
if (chosenWeaponPreset is null) {
_logger.Warning(
_localisationService.GetText("loot-default_preset_not_found_using_random", chosenWeaponTpl)
);
chosenWeaponPreset = _randomUtil.GetArrayValue(_presetHelper.GetPresets(chosenWeaponTpl));
}
// Clean up Ids to ensure they're all unique and prevent collisions
var presetAndMods = _itemHelper.ReplaceIDs(_cloner.Clone(chosenWeaponPreset.Items));
_itemHelper.RemapRootItemId(presetAndMods);
// Add preset to return object
itemsToReturn.Add(presetAndMods);
// Get a random collection of weapon mods related to chosen weawpon and add them to result array
var linkedItemsToWeapon = _ragfairLinkedItemService.GetLinkedDbItems(chosenWeaponTpl);
itemsToReturn.AddRange(GetSealedContainerWeaponModRewards(containerSettings, linkedItemsToWeapon, chosenWeaponPreset)
);
// Handle non-weapon mod reward types
itemsToReturn.AddRange((GetSealedContainerNonWeaponModRewards(containerSettings, weaponDetailsDb.Value)));
return itemsToReturn;
}
/// <summary>
@@ -130,7 +471,69 @@ public class LootGenerator(
protected List<List<Item>> GetSealedContainerNonWeaponModRewards(SealedAirdropContainerSettings containerSettings,
TemplateItem weaponDetailsDb)
{
throw new NotImplementedException();
List<List<Item>> rewards = [];
foreach (var (rewardKey,settings) in containerSettings.RewardTypeLimits) {
var rewardCount = _randomUtil.GetDouble(settings.Min.Value, settings.Max.Value);
if (rewardCount == 0) {
continue;
}
// Edge case - ammo boxes
if (rewardKey == BaseClasses.AMMO_BOX) {
// Get ammoboxes from db
var ammoBoxesDetails = containerSettings.AmmoBoxWhitelist.Select((tpl) => {
var itemDetails = _itemHelper.GetItem(tpl);
return itemDetails.Value;
});
// Need to find boxes that matches weapons caliber
var weaponCaliber = weaponDetailsDb.Properties.AmmoCaliber;
var ammoBoxesMatchingCaliber = ammoBoxesDetails.Where((x) =>
x.Properties.AmmoCaliber == weaponCaliber);
if (!ammoBoxesMatchingCaliber.Any()) {
_logger.Debug($"No ammo box with caliber {weaponCaliber} found, skipping");
continue;
}
for (var index = 0; index < rewardCount; index++) {
var chosenAmmoBox = _randomUtil.GetArrayValue(ammoBoxesMatchingCaliber);
var ammoBoxReward = new List<Item> { new() { Id = _hashUtil.Generate(), Template = chosenAmmoBox.Id } };
_itemHelper.AddCartridgesToAmmoBox(ammoBoxReward, chosenAmmoBox);
rewards.Add(ammoBoxReward);
}
continue;
}
// Get all items of the desired type + not quest items + not globally blacklisted
var rewardItemPool = _databaseService.GetItems().Values.Where(
(item) =>
item.Parent == rewardKey &&
item.Type.ToLower() == "item" &&
_itemFilterService.IsItemBlacklisted(item.Id) &&
!(containerSettings.AllowBossItems || _itemFilterService.IsBossItem(item.Id)) &&
item.Properties.QuestItem is null
);
if (rewardItemPool.Count() == 0) {
_logger.Debug($"No items with base type of {rewardKey} found, skipping");
continue;
}
for (var index = 0; index < rewardCount; index++) {
// Choose a random item from pool
var chosenRewardItem = _randomUtil.GetArrayValue(rewardItemPool);
var rewardItem = new List<Item> { new() { Id = _hashUtil.Generate(), Template = chosenRewardItem.Id } };
rewards.Add(rewardItem);
}
}
return rewards;
}
/// <summary>
@@ -143,7 +546,37 @@ public class LootGenerator(
protected List<List<Item>> GetSealedContainerWeaponModRewards(SealedAirdropContainerSettings containerSettings, List<TemplateItem> linkedItemsToWeapon,
Preset chosenWeaponPreset)
{
throw new NotImplementedException();
List<List<Item>> modRewards = [];
foreach (var (rewardKey,settings) in containerSettings.WeaponModRewardLimits) {
var rewardCount = _randomUtil.GetDouble(settings.Min.Value, settings.Max.Value);
// Nothing to add, skip reward type
if (rewardCount == 0) {
continue;
}
// Get items that fulfil reward type criteria from items that fit on gun
var relatedItems = linkedItemsToWeapon?.Where(
(item) => item?.Parent == rewardKey && !_itemFilterService.IsItemBlacklisted(item.Id)
);
if (relatedItems is null || relatedItems.Count() == 0) {
_logger.Debug(
$"No items found to fulfil reward type: {rewardKey} for weapon: {chosenWeaponPreset.Name}, skipping type"
);
continue;
}
// Find a random item of the desired type and add as reward
for (var index = 0; index < rewardCount; index++) {
var chosenItem = _randomUtil.DrawRandomFromList(relatedItems.ToList());
var reward = new List<Item> { new Item() { Id = _hashUtil.Generate(), Template = chosenItem[0].Id } };
modRewards.Add(reward);
}
}
return modRewards;
}
/// <summary>
@@ -51,11 +51,12 @@ public class PlayerScavGenerator(
var scavKarmaLevel = GetScavKarmaLevel(pmcDataClone);
// use karma level to get correct karmaSettings
var playerScavKarmaSettings = _playerScavConfig.KarmaLevel[scavKarmaLevel.ToString()];
if (playerScavKarmaSettings == null)
if (!_playerScavConfig.KarmaLevel.TryGetValue(scavKarmaLevel.ToString(), out var playerScavKarmaSettings))
{
_logger.Error(_localisationService.GetText("scav-missing_karma_settings", scavKarmaLevel));
}
_logger.Debug($"generated player scav loadout with karma level {scavKarmaLevel}");
_logger.Debug($"Generated player scav loadout with karma level {scavKarmaLevel}");
// Edit baseBotNode values
var baseBotNode = ConstructBotBaseTemplate(playerScavKarmaSettings.BotTypeForLoot);
@@ -80,7 +81,7 @@ public class PlayerScavGenerator(
scavData.Info.Bans = [];
scavData.Info.RegistrationDate = pmcDataClone.Info.RegistrationDate;
scavData.Info.GameVersion = pmcDataClone.Info.GameVersion;
scavData.Info.MemberCategory = MemberCategory.UNIQUE_ID;
scavData.Info.MemberCategory = MemberCategory.UniqueId;
scavData.Info.LockedMoveCommands = true;
scavData.RagfairInfo = pmcDataClone.RagfairInfo;
scavData.UnlockedInfo = pmcDataClone.UnlockedInfo;
@@ -7,6 +7,7 @@ using Core.Models.Spt.Config;
using Core.Servers;
using Core.Services;
using Core.Utils;
using Core.Utils.Cloners;
namespace Core.Generators;
@@ -16,7 +17,8 @@ public class RagfairAssortGenerator(
ItemHelper itemHelper,
PresetHelper presetHelper,
SeasonalEventService seasonalEventService,
ConfigServer configServer
ConfigServer configServer,
ICloner _cloner
)
{
protected List<List<Item>> generatedAssortItems = [];
@@ -77,7 +79,7 @@ public class RagfairAssortGenerator(
foreach (var preset in presets)
{
// Update Ids and clone
var presetAndMods = itemHelper.ReplaceIDs(preset.Items);
var presetAndMods = itemHelper.ReplaceIDs(_cloner.Clone(preset.Items));
itemHelper.RemapRootItemId(presetAndMods);
// Add presets base item tpl to the processed list so its skipped later on when processing items
@@ -157,7 +157,7 @@ public class RagfairOfferGenerator(
if (isTrader) {
return new RagfairOfferUser(){
Id = userID,
MemberType = MemberCategory.TRADER
MemberType = MemberCategory.Trader
};
}
@@ -11,6 +11,7 @@ using Core.Models.Utils;
using Core.Servers;
using Core.Services;
using Core.Utils;
using Core.Utils.Cloners;
using SptCommon.Extensions;
namespace Core.Generators;
@@ -26,7 +27,8 @@ public class ScavCaseRewardGenerator(
RagfairPriceService _ragfairPriceService,
SeasonalEventService _seasonalEventService,
ItemFilterService _itemFilterService,
ConfigServer _configServer
ConfigServer _configServer,
ICloner _cloner
)
{
protected ScavCaseConfig _scavCaseConfig = _configServer.GetConfig<ScavCaseConfig>();
@@ -312,7 +314,7 @@ public class ScavCaseRewardGenerator(
}
// Ensure preset has unique ids and is cloned so we don't alter the preset data stored in memory
List<Item> presetAndMods = _itemHelper.ReplaceIDs(preset.Items);
List<Item> presetAndMods = _itemHelper.ReplaceIDs(_cloner.Clone(preset.Items));
_itemHelper.RemapRootItemId(presetAndMods);
resultItem = presetAndMods;
+55 -38
View File
@@ -45,29 +45,34 @@ public class BotGeneratorHelper(
_botConfig.LootItemResourceRandomization.TryGetValue(botRole, out var randomisationSettings);
Upd itemProperties = new();
var hasProperties = false;
if (itemTemplate?.Properties?.MaxDurability is not null)
if (itemTemplate?.Properties?.MaxDurability is not null && itemTemplate.Properties.MaxDurability > 0)
{
if (itemTemplate.Properties.WeapClass is not null)
{
// Is weapon
itemProperties.Repairable = GenerateWeaponRepairableProperties(itemTemplate, botRole);
hasProperties = true;
}
else if (itemTemplate.Properties.ArmorClass is not null)
{
// Is armor
itemProperties.Repairable = GenerateArmorRepairableProperties(itemTemplate, botRole);
hasProperties = true;
}
}
if (itemTemplate?.Properties?.HasHinge ?? false)
{
itemProperties.Togglable = new UpdTogglable { On = true };
hasProperties = true;
}
if (itemTemplate?.Properties?.Foldable ?? false)
{
itemProperties.Foldable = new UpdFoldable { Folded = false };
hasProperties = true;
}
if (itemTemplate?.Properties?.WeapFireType?.Count == 0)
@@ -75,6 +80,7 @@ public class BotGeneratorHelper(
itemProperties.FireMode = itemTemplate.Properties.WeapFireType.Contains("fullauto")
? new UpdFireMode { FireMode = "fullauto" }
: new UpdFireMode { FireMode = _randomUtil.GetArrayValue(itemTemplate.Properties.WeapFireType) };
hasProperties = true;
}
if (itemTemplate?.Properties?.MaxHpResource is not null)
@@ -86,6 +92,7 @@ public class BotGeneratorHelper(
randomisationSettings?.Meds
)
};
hasProperties = true;
}
if (itemTemplate?.Properties?.MaxResource is not null && itemTemplate.Properties?.FoodUseTime is not null)
@@ -97,6 +104,7 @@ public class BotGeneratorHelper(
randomisationSettings?.Food
),
};
hasProperties = true;
}
if (itemTemplate?.Parent == BaseClasses.FLASHLIGHT)
@@ -106,6 +114,7 @@ public class BotGeneratorHelper(
? GetBotEquipmentSettingFromConfig(botRole, "lightIsActiveNightChancePercent", 50)
: GetBotEquipmentSettingFromConfig(botRole, "lightIsActiveDayChancePercent", 25);
itemProperties.Light = new UpdLight { IsActive = _randomUtil.GetChance100(lightLaserActiveChance), SelectedMode = 0, };
hasProperties = true;
}
else if (itemTemplate?.Parent == BaseClasses.TACTICAL_COMBO)
{
@@ -120,6 +129,7 @@ public class BotGeneratorHelper(
IsActive = _randomUtil.GetChance100(lightLaserActiveChance),
SelectedMode = 0,
};
hasProperties = true;
}
if (itemTemplate?.Parent == BaseClasses.NIGHTVISION)
@@ -129,20 +139,23 @@ public class BotGeneratorHelper(
? GetBotEquipmentSettingFromConfig(botRole, "nvgIsActiveChanceNightPercent", 90)
: GetBotEquipmentSettingFromConfig(botRole, "nvgIsActiveChanceDayPercent", 15);
itemProperties.Togglable = new UpdTogglable { On = _randomUtil.GetChance100(nvgActiveChance) };
hasProperties = true;
}
// Togglable face shield
if (!(itemTemplate?.Properties?.HasHinge ?? false) || !(itemTemplate.Properties.FaceShieldComponent ?? false)) return itemProperties;
if ((itemTemplate?.Properties?.HasHinge ?? false) && (itemTemplate.Properties.FaceShieldComponent ?? false))
{
var faceShieldActiveChance = GetBotEquipmentSettingFromConfig(
botRole,
"faceShieldIsActiveChancePercent",
75
);
itemProperties.Togglable = new UpdTogglable { On = _randomUtil.GetChance100(faceShieldActiveChance) };
hasProperties = true;
}
// Get chance from botconfig for bot type, use 75% if no value found
var faceShieldActiveChance = GetBotEquipmentSettingFromConfig(
botRole,
"faceShieldIsActiveChancePercent",
75
);
itemProperties.Togglable = new UpdTogglable { On = _randomUtil.GetChance100(faceShieldActiveChance) };
return itemProperties;
return hasProperties ? itemProperties : null;
}
/// <summary>
@@ -517,13 +530,17 @@ public class BotGeneratorHelper(
foreach (var slotGrid in value?.Properties?.Grids ?? [])
{
// Grid is empty, skip or item size is bigger than grid
if (slotGrid.Props?.CellsH == 0 || slotGrid.Props?.CellsV == 0 || itemSize[0] * itemSize[1] > slotGrid.Props?.CellsV * slotGrid.Props?.CellsH)
if (slotGrid.Props?.CellsH == 0 ||
slotGrid.Props?.CellsV == 0 ||
itemSize[0] * itemSize[1] > slotGrid.Props?.CellsV * slotGrid.Props?.CellsH)
{
continue;
}
// Can't put item type in grid, skip all grids as we're assuming they have the same rules
if (!ItemAllowedInContainer(slotGrid, rootItemTplId))
{
// Multiple containers, maybe next one allows item, only break out of loop for this containers grids
// Multiple containers, maybe next one allows item, only break out of loop for the containers grids
break;
}
@@ -535,44 +552,22 @@ public class BotGeneratorHelper(
// Get root items in container we can iterate over to find out what space is free
var containerItemsToCheck = existingContainerItems.Where(x => x.SlotId == slotGrid.Name);
var itemsToRemove = new List<Item>();
var itemsToAdd = new List<Item>();
foreach (var item in containerItemsToCheck)
{
// Check item in contain for children, store for later insertion into `containerItemsToCheck`
// (used later when figuring out how much space weapon takes up)
var itemWithChildItems = _itemHelper.FindAndReturnChildrenAsItems(inventory.Items, item.Id);
if (itemWithChildItems.Count <= 1) continue;
var containerItemsWithChildren = GetContainerItemsWithChildren(containerItemsToCheck, inventory.Items);
// Store replaced item + new Child items to add later as we can't modify a collecting while looking over it
itemsToRemove.Add(item);
itemsToAdd.AddRange(itemsToAdd);
}
// Remove the base items flagged above
foreach (var item in itemsToRemove)
{
existingContainerItems.Remove(item);
}
// Add item back with its child items
existingContainerItems.AddRange(itemsToAdd);
// Get rid of items free/used spots in current grid
if (slotGrid.Props is not null)
{
// Get rid of an items free/used spots in current grid
var slotGridMap = _inventoryHelper.GetContainerMap(
slotGrid.Props.CellsH.GetValueOrDefault(),
slotGrid.Props.CellsV.GetValueOrDefault(),
existingContainerItems,
containerItemsWithChildren,
container.Id
);
// Try to fit item into grid
var findSlotResult = _containerHelper.FindSlotForItem(slotGridMap, itemSize[0], itemSize[1]);
// Open slot found, add item to inventory
// Free slot found, add item
if (findSlotResult.Success ?? false)
{
var parentItem = itemWithChildren.FirstOrDefault((i) => i.Id == rootItemId);
@@ -620,6 +615,28 @@ public class BotGeneratorHelper(
return ItemAddedResult.NO_SPACE;
}
/// <summary>
/// Take a list of items and check if they need children + add them
/// </summary>
/// <param name="containerItems"></param>
/// <param name="inventoryItems"></param>
/// <returns></returns>
protected List<Item> GetContainerItemsWithChildren(IEnumerable<Item> containerItems, List<Item> inventoryItems)
{
var result = new List<Item>();
foreach (var item in containerItems)
{
// Check item in container for children, store for later insertion into `containerItemsToCheck`
// (used later when figuring out how much space weapon takes up)
var itemWithChildItems = _itemHelper.FindAndReturnChildrenAsItems(inventoryItems, item.Id);
// Item had children, replace existing data with item + its children
result.AddRange(itemWithChildItems);
}
return result;
}
/// <summary>
/// Is the provided item allowed inside a container
/// </summary>
@@ -16,14 +16,80 @@ public abstract class AbstractDialogChatBot(
public abstract UserDialogInfo GetChatBot();
public string HandleMessage(string sessionId, SendMessageRequest request)
public string? HandleMessage(string sessionId, SendMessageRequest request)
{
throw new NotImplementedException();
if ((request.Text ?? "").Length == 0) {
_logger.Error("Command came in as empty text! Invalid data!");
return request.DialogId;
}
var splitCommand = request.Text.Split(" ");
var commandos = _chatCommands.Where((c) => c.GetCommandPrefix() == splitCommand.FirstOrDefault());
if (commandos.FirstOrDefault()?.GetCommands().Contains(splitCommand[1]) ?? false) {
return commandos.FirstOrDefault().Handle(splitCommand[1], GetChatBot(), sessionId, request);
}
if (splitCommand.FirstOrDefault().ToLower() == "help") {
_mailSendService.SendUserMessageToPlayer(
sessionId,
GetChatBot(),
"The available commands will be listed below:",
[],
null
);
// due to BSG being dumb with messages we need a mandatory timeout between messages so they get out on the right order
// TODO: there must be a better way of doing this
_ = new Timer(
__ =>
{
foreach (var chatCommand in _chatCommands)
{
_mailSendService.SendUserMessageToPlayer(
sessionId,
GetChatBot(),
$"Commands available for \"{chatCommand.GetCommandPrefix()}\" prefix:", [], null
);
_ = new Timer(
___ =>
{
foreach (var subCommand in chatCommand.GetCommands())
{
_mailSendService.SendUserMessageToPlayer(
sessionId,
GetChatBot(),
$"Subcommand {subCommand}:\\n{chatCommand.GetCommandHelp(subCommand)}",
[],
null
);
}
}, null, TimeSpan.FromMicroseconds(1000), Timeout.InfiniteTimeSpan
);
}
}, null, TimeSpan.FromMicroseconds(1000), Timeout.InfiniteTimeSpan
);
return request.DialogId;
}
_mailSendService.SendUserMessageToPlayer(
sessionId,
GetChatBot(),
GetUnrecognizedCommandMessage(),
[],
null
);
return null;
}
public void RegisterChatCommand(IChatCommand chatCommand)
{
throw new NotImplementedException();
if (_chatCommands.Any((cc) => cc.GetCommandPrefix() == chatCommand.GetCommandPrefix())) {
throw new Exception($"The command \"{chatCommand.GetCommandPrefix()}\" attempting to be registered already exists.");
}
_chatCommands.Add(chatCommand);
}
protected string GetUnrecognizedCommandMessage()
@@ -2,34 +2,67 @@
using Core.Helpers.Dialog.Commando.SptCommands;
using Core.Models.Eft.Dialog;
using Core.Models.Eft.Profile;
using Core.Models.Spt.Config;
using Core.Servers;
using Core.Services;
namespace Core.Helpers.Dialog.Commando;
[Injectable]
public class SptCommandoCommands : IChatCommand
{
protected List<ISptCommand> _sptCommands;
protected LocalisationService _localisationService;
public SptCommandoCommands(
ConfigServer configServer,
LocalisationService localisationService,
IEnumerable<ISptCommand> sptCommands
)
{
_sptCommands = sptCommands.ToList();
_localisationService = localisationService;
var coreConfigs = configServer.GetConfig<CoreConfig>();
var commandoId = coreConfigs.Features?.ChatbotFeatures.Ids.GetValueOrDefault("commando");
if (!(coreConfigs.Features.ChatbotFeatures.CommandoFeatures.GiveCommandEnabled &&
coreConfigs.Features.ChatbotFeatures.EnabledBots.ContainsKey(commandoId)))
{
var giveCommand = _sptCommands.FirstOrDefault(x => x.GetCommand().ToLower() == "give");
_sptCommands.Remove(giveCommand);
}
}
public void RegisterSptCommandoCommand(ISptCommand command)
{
throw new NotImplementedException();
if (_sptCommands.Any((c) => c.GetCommand() == command.GetCommand())) {
throw new Exception(
_localisationService.GetText(
"chat-unable_to_register_command_already_registered",
command.GetCommand()
)
);
}
_sptCommands.Add(command);
}
public string GetCommandPrefix()
{
throw new NotImplementedException();
return "spt";
}
public string GetCommandHelp(string command)
{
throw new NotImplementedException();
return _sptCommands.FirstOrDefault(c => c.GetCommand() == command)?.GetCommandHelp();
}
public List<string> GetCommands()
{
throw new NotImplementedException();
return _sptCommands.Select(c => c.GetCommand()).ToList();
}
public string Handle(string command, UserDialogInfo commandHandler, string sesssionId, SendMessageRequest request)
public string Handle(string command, UserDialogInfo commandHandler, string sessionId, SendMessageRequest request)
{
throw new NotImplementedException();
return _sptCommands
.Find((c) => c.GetCommand() == command)
.PerformAction(commandHandler, sessionId, request);
}
}
@@ -28,8 +28,8 @@ public class CommandoDialogChatBot(
Info = new UserDialogDetails
{
Level = 1,
MemberCategory = MemberCategory.DEVELOPER,
SelectedMemberCategory = MemberCategory.DEVELOPER,
MemberCategory = MemberCategory.Developer,
SelectedMemberCategory = MemberCategory.Developer,
Nickname = "Commando",
Side = "Usec"
}
@@ -38,6 +38,6 @@ public class CommandoDialogChatBot(
protected string GetUnrecognizedCommandMessage()
{
throw new NotImplementedException();
return "I'm sorry soldier, I don't recognize the command you are trying to use! Type \"help\" to see available commands.";
}
}
@@ -6,5 +6,5 @@ namespace Core.Helpers.Dialogue;
public interface IDialogueChatBot
{
public UserDialogInfo GetChatBot();
public string HandleMessage(string sessionId, SendMessageRequest request);
public string? HandleMessage(string sessionId, SendMessageRequest request);
}
@@ -7,18 +7,26 @@ using Core.Models.Spt.Config;
using Core.Models.Utils;
using Core.Servers;
using Core.Services;
using Core.Utils;
namespace Core.Helpers.Dialogue;
[Injectable]
public class SptDialogueChatBot(
ISptLogger<AbstractDialogChatBot> _logger,
MailSendService mailSendService,
IEnumerable<IChatCommand> chatCommands,
ConfigServer configServer
) : AbstractDialogChatBot(_logger, mailSendService, chatCommands)
MailSendService _mailSendService,
IEnumerable<IChatCommand> _chatCommands,
ConfigServer _configServer,
ProfileHelper _profileHelper,
RandomUtil _randomUtil,
SeasonalEventService _seasonalEventService,
GiftService _giftService,
LocalisationService _localisationService
) : AbstractDialogChatBot(_logger, _mailSendService, _chatCommands)
{
protected CoreConfig _coreConfig = configServer.GetConfig<CoreConfig>();
protected CoreConfig _coreConfig = _configServer.GetConfig<CoreConfig>();
protected WeatherConfig _weatherConfig = _configServer.GetConfig<WeatherConfig>();
protected List<string> _listOfMessages = ["hello", "hi", "sup", "yo", "hey"];
public override UserDialogInfo GetChatBot()
{
@@ -29,16 +37,202 @@ public class SptDialogueChatBot(
Info = new UserDialogDetails
{
Level = 1,
MemberCategory = MemberCategory.DEVELOPER,
SelectedMemberCategory = MemberCategory.DEVELOPER,
MemberCategory = MemberCategory.Developer,
SelectedMemberCategory = MemberCategory.Developer,
Nickname = _coreConfig.SptFriendNickname,
Side = "Usec"
}
};
}
public string HandleMessage(string sessionId, SendMessageRequest request)
public string? HandleMessage(string sessionId, SendMessageRequest request)
{
throw new NotImplementedException();
var sender = _profileHelper.GetPmcProfile(sessionId);
var sptFriendUser = GetChatBot();
var requestInput = request.Text.ToLower();
// only check if entered text is gift code when feature enabled
if (_coreConfig.Features.ChatbotFeatures.SptFriendGiftsEnabled) {
var giftSent = _giftService.SendGiftToPlayer(sessionId, request.Text);
if (giftSent == GiftSentResult.SUCCESS) {
_mailSendService.SendUserMessageToPlayer(
sessionId,
sptFriendUser,
_randomUtil.GetArrayValue([
"Hey! you got the right code!",
"A secret code, how exciting!",
"You found a gift code!",
"A gift code! incredible",
"A gift! what could it be!",
]),
[],
null
);
return null;
}
if (giftSent == GiftSentResult.FAILED_GIFT_ALREADY_RECEIVED) {
_mailSendService.SendUserMessageToPlayer(
sessionId,
sptFriendUser,
_randomUtil.GetArrayValue(["Looks like you already used that code", "You already have that!!"]),
[],
null
);
return null;
}
}
if (requestInput.Contains("love you")) {
_mailSendService.SendUserMessageToPlayer(
sessionId,
sptFriendUser,
_randomUtil.GetArrayValue([
"That's quite forward but i love you too in a purely chatbot-human way",
"I love you too buddy :3!",
"uwu",
$"love you too {sender?.Info?.Nickname}",
]),
[],
null
);
}
if (requestInput == "spt") {
_mailSendService.SendUserMessageToPlayer(
sessionId,
sptFriendUser,
_randomUtil.GetArrayValue(["Its me!!", "spt? i've heard of that project"]),
[],
null
);
}
if (requestInput == "fish") {
_mailSendService.SendUserMessageToPlayer(
sessionId,
sptFriendUser,
_randomUtil.GetArrayValue(["blub"]),
[],
null
);
}
if (_listOfMessages.Contains(requestInput)) {
_mailSendService.SendUserMessageToPlayer(
sessionId,
sptFriendUser,
_randomUtil.GetArrayValue([
"Howdy",
"Hi",
"Greetings",
"Hello",
"bonjor",
"Yo",
"Sup",
"Heyyyyy",
"Hey there",
$"Hello {sender?.Info?.Nickname}",
]),
[], null
);
}
if (requestInput == "nikita") {
_mailSendService.SendUserMessageToPlayer(
sessionId,
sptFriendUser,
_randomUtil.GetArrayValue([
"I know that guy!",
"Cool guy, he made EFT!",
"Legend",
"Remember when he said webel-webel-webel-webel, classic Nikita moment",
]), [], null
);
}
if (requestInput == "are you a bot") {
_mailSendService.SendUserMessageToPlayer(
sessionId,
sptFriendUser,
_randomUtil.GetArrayValue(["beep boop", "**sad boop**", "probably", "sometimes", "yeah lol"]),
[], null
);
}
if (requestInput == "itsonlysnowalan") {
_weatherConfig.OverrideSeason = Season.WINTER;
_mailSendService.SendUserMessageToPlayer(
sessionId,
sptFriendUser,
_randomUtil.GetArrayValue([_localisationService.GetText("chatbot-snow_enabled")]), [], null
);
}
if (requestInput == "givemesunshine") {
_weatherConfig.OverrideSeason = Season.SUMMER;
_mailSendService.SendUserMessageToPlayer(
sessionId,
sptFriendUser,
_randomUtil.GetArrayValue([_localisationService.GetText("chatbot-summer_enabled")]), [], null
);
}
if (requestInput == "veryspooky") {
var enableEventResult = _seasonalEventService.ForceSeasonalEvent(SeasonalEventType.Halloween);
if (enableEventResult) {
_mailSendService.SendUserMessageToPlayer(
sessionId,
sptFriendUser,
_randomUtil.GetArrayValue([
_localisationService.GetText("chatbot-forced_event_enabled", SeasonalEventType.Halloween)
]), [], null
);
}
}
if (requestInput == "hohoho") {
var enableEventResult = _seasonalEventService.ForceSeasonalEvent(SeasonalEventType.Christmas);
if (enableEventResult) {
_mailSendService.SendUserMessageToPlayer(
sessionId,
sptFriendUser,
_randomUtil.GetArrayValue([
_localisationService.GetText("chatbot-forced_event_enabled", SeasonalEventType.Christmas)
]), [], null
);
}
}
if (requestInput == "givemespace") {
var stashRowGiftId = "StashRows";
var maxGiftsToSendCount = _coreConfig.Features.ChatbotFeatures.CommandUseLimits[stashRowGiftId] ?? 5;
if (_profileHelper.PlayerHasRecievedMaxNumberOfGift(sessionId, stashRowGiftId, maxGiftsToSendCount)) {
_mailSendService.SendUserMessageToPlayer(
sessionId,
sptFriendUser,
_localisationService.GetText("chatbot-cannot_accept_any_more_of_gift"), [], null
);
} else {
_profileHelper.AddStashRowsBonusToProfile(sessionId, 2);
_mailSendService.SendUserMessageToPlayer(
sessionId,
sptFriendUser,
_randomUtil.GetArrayValue([
_localisationService.GetText("chatbot-added_stash_rows_please_restart"),
]), [], null
);
_profileHelper.FlagGiftReceivedInProfile(sessionId, stashRowGiftId, maxGiftsToSendCount);
}
}
return request.DialogId;
}
}
@@ -144,7 +144,7 @@ public class DurabilityLimitsHelper(
{
var lowestMaxPercent = _botConfig.Durability.Pmc.Armor.LowestMaxPercent;
var highestMaxPercent = _botConfig.Durability.Pmc.Armor.HighestMaxPercent;
var multiplier = _randomUtil.GetInt(lowestMaxPercent, highestMaxPercent);
var multiplier = _randomUtil.GetDouble(lowestMaxPercent, highestMaxPercent);
return itemMaxDurability * (multiplier / 100);
}
+250 -9
View File
@@ -3,14 +3,32 @@ using Core.Models.Eft.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Health;
using Core.Models.Eft.Profile;
using Core.Models.Spt.Config;
using Core.Models.Utils;
using Core.Servers;
using Core.Services;
using Core.Utils;
using Core.Utils.Cloners;
using SptCommon.Extensions;
using BodyPartHealth = Core.Models.Eft.Common.Tables.BodyPartHealth;
using Effects = Core.Models.Eft.Profile.Effects;
using Health = Core.Models.Eft.Profile.Health;
using Vitality = Core.Models.Eft.Profile.Vitality;
namespace Core.Helpers;
[Injectable]
public class HealthHelper
public class HealthHelper(
ISptLogger<HealthHelper> _logger,
TimeUtil _timeUtil,
SaveServer _saveServer,
DatabaseService _databaseService,
ConfigServer _configServer,
ICloner _cloner
)
{
protected HealthConfig _healthConfig = _configServer.GetConfig<HealthConfig>();
/// <summary>
/// Resets the profiles vitality/health and vitality/effects properties to their defaults
/// </summary>
@@ -18,7 +36,34 @@ public class HealthHelper
/// <returns>Updated profile</returns>
public SptProfile ResetVitality(string sessionID)
{
throw new NotImplementedException();
var profile = _saveServer.GetProfile(sessionID);
profile.VitalityData ??= new Vitality { Health = null, Effects = null };
profile.VitalityData.Health = new Health {
Hydration = 0,
Energy = 0,
Temperature = 0,
Head = 0,
Chest = 0,
Stomach = 0,
LeftArm = 0,
RightArm = 0,
LeftLeg = 0,
RightLeg = 0,
};
profile.VitalityData.Effects = new Effects {
Head = new Head(),
Chest = new Chest(),
Stomach = new Stomach(),
LeftArm = new LeftArm(),
RightArm = new RightArm(),
LeftLeg = new LeftLeg(),
RightLeg = new RightLeg(),
};
return profile;
}
/// <summary>
@@ -36,7 +81,53 @@ public class HealthHelper
string sessionID,
bool isDead)
{
throw new NotImplementedException();
var fullProfile = _saveServer.GetProfile(sessionID);
var profileEdition = fullProfile.ProfileInfo.Edition;
var profileSide = fullProfile.CharacterData.PmcData.Info.Side;
var defaultTemperature =
_databaseService.GetProfiles()
.GetByJsonProp<ProfileSides>(profileEdition)
.GetByJsonProp<TemplateSide>(profileSide.ToLower())
?.Character?.Health?.Temperature ?? new CurrentMinMax { Current = 36.6 };
StoreHydrationEnergyTempInProfile(
fullProfile,
postRaidHealth.Hydration.Current ?? 0,
postRaidHealth.Energy.Current ?? 0,
defaultTemperature.Current ?? 0 // Reset profile temp to the default to prevent very cold/hot temps persisting into next raid
);
// Store limb effects from post-raid in profile
foreach (var bodyPart in postRaidHealth.BodyParts) {
// Effects
if (postRaidHealth.BodyParts[bodyPart.Key].Effects is not null) {
// fullProfile.VitalityData.Effects[bodyPart.Key] = postRaidHealth.BodyParts[bodyPart.Key].Effects;
// TODO: this will need to change, typing is all fucked up
}
// Limb hp
if (!isDead)
{
// Player alive, not is limb alive
var byJsonProp = fullProfile.VitalityData.Health.GetByJsonProp<double>(bodyPart.Key);
byJsonProp = postRaidHealth.BodyParts[bodyPart.Key].Health.Current ?? 0;
} else {
var byJsonProp = fullProfile.VitalityData.Health.GetByJsonProp<double>(bodyPart.Key);
byJsonProp = (pmcData.Health.BodyParts[bodyPart.Key].Health.Maximum * _healthConfig.HealthMultipliers.Death) ?? 0;
}
}
TransferPostRaidLimbEffectsToProfile(postRaidHealth.BodyParts, pmcData);
// Adjust hydration/energy/temp and limb hp using temp storage hydated above
SaveHealth(pmcData, sessionID);
// Reset temp storage
ResetVitality(sessionID);
// Update last edited timestamp
pmcData.Health.UpdateTime = _timeUtil.GetTimeStamp();
}
protected void StoreHydrationEnergyTempInProfile(
@@ -45,7 +136,9 @@ public class HealthHelper
double energy,
double temprature)
{
throw new NotImplementedException();
fullProfile.VitalityData.Health.Hydration = hydration;
fullProfile.VitalityData.Health.Energy = energy;
fullProfile.VitalityData.Health.Temperature = temprature;
}
/// <summary>
@@ -55,7 +148,37 @@ public class HealthHelper
/// <param name="profileData">Player profile on server</param>
protected void TransferPostRaidLimbEffectsToProfile(Dictionary<string, BodyPartHealth> postRaidBodyParts, PmcData profileData)
{
throw new NotImplementedException();
// Iterate over each body part
List<string> effectsToIgnore = ["Dehydration", "Exhaustion"];
foreach (var bodyPartId in postRaidBodyParts) {
// Get effects on body part from profile
var bodyPartEffects = postRaidBodyParts[bodyPartId.Key].Effects;
foreach (var effect in bodyPartEffects) {
var effectDetails = bodyPartEffects[effect.Key];
// Null guard
profileData.Health.BodyParts[bodyPartId.Key].Effects ??= new Dictionary<string, BodyPartEffectProperties>();
// Effect already exists on limb in server profile, skip
var profileBodyPartEffects = profileData.Health.BodyParts[bodyPartId.Key].Effects;
if (profileBodyPartEffects[effect.Key] is not null) {
if (effectsToIgnore.Contains(effect.Key)) {
// Get rid of certain effects we dont want to persist out of raid
profileBodyPartEffects[effect.Key] = null;
}
continue;
}
if (effectsToIgnore.Contains(effect.Key)) {
// Do not pass some effects to out of raid profile
continue;
}
// Add effect to server profile
profileBodyPartEffects[effect.Key] = new BodyPartEffectProperties { Time = effectDetails.Time ?? -1 };
}
}
}
/// <summary>
@@ -73,7 +196,45 @@ public class HealthHelper
bool addEffects = true,
bool deleteExistingEffects = true)
{
throw new NotImplementedException();
var postRaidBodyParts = request.Health; // post raid health settings
var fullProfile = _saveServer.GetProfile(sessionID);
var profileEffects = fullProfile.VitalityData.Effects;
StoreHydrationEnergyTempInProfile(fullProfile, request.Hydration ?? 0, request.Energy ?? 0, request.Temperature ?? 0);
// Process request data into profile
foreach (var bodyPart in postRaidBodyParts) {
// Transfer effects from request to profile
if (bodyPart.Effects is not null) {
// profileEffects[bodyPart] = postRaidBodyParts[bodyPart].Effects;
}
if (request.IsAlive ?? false) {
// Player alive, not is limb alive
// fullProfile.VitalityData.Health[bodyPart] = postRaidBodyParts[bodyPart].Current;
} else {
// fullProfile.VitalityData.Health[bodyPart] =
// pmcData.Health.BodyParts[bodyPart].Health.Maximum * _healthConfig.HealthMultipliers.Death;
}// TODO: this will need to change, typing is all fucked up
}
// Add effects to body parts if enabled
if (addEffects) {
SaveEffects(
pmcData,
sessionID,
_cloner.Clone(_saveServer.GetProfile(sessionID).VitalityData.Effects),
deleteExistingEffects
);
}
// Adjust hydration/energy/temp and limb hp
SaveHealth(pmcData, sessionID);
ResetVitality(sessionID);
// Update last edited timestamp
pmcData.Health.UpdateTime = _timeUtil.GetTimeStamp();
}
/// <summary>
@@ -83,7 +244,40 @@ public class HealthHelper
/// <param name="sessionID">Session id</param>
protected void SaveHealth(PmcData pmcData, string sessionID)
{
throw new NotImplementedException();
// TODO: this will need to change, typing is all fucked up
// if (!_healthConfig.Save.Health) {
// return;
// }
//
// var profileHealth = _saveServer.GetProfile(sessionID).VitalityData.Health;
// foreach (var healthModifier in profileHealth) {
// let target = profileHealth[healthModifier];
//
// if (["Hydration", "Energy", "Temperature"].includes(healthModifier)) {
// // Set resources
// if (target > pmcData.Health[healthModifier].Maximum) {
// target = pmcData.Health[healthModifier].Maximum;
// }
//
// pmcData.Health[healthModifier].Current = Math.round(target);
// } else {
// // Over max, limit
// if (target > pmcData.Health.BodyParts[healthModifier].Health.Maximum) {
// target = pmcData.Health.BodyParts[healthModifier].Health.Maximum;
// }
//
// // Part was zeroed out in raid
// if (target === 0) {
// // Blacked body part
// target = Math.round(
// pmcData.Health.BodyParts[healthModifier].Health.Maximum *
// this.healthConfig.healthMultipliers.blacked,
// );
// }
//
// pmcData.Health.BodyParts[healthModifier].Health.Current = Math.round(target);
// }
// }
}
/// <summary>
@@ -101,7 +295,42 @@ public class HealthHelper
Effects bodyPartsWithEffects,
bool deleteExistingEffects = true)
{
throw new NotImplementedException();
// TODO: this will need to change, typing is all fucked up
// if (!this.healthConfig.save.effects) {
// return;
// }
//
// for (const bodyPart in bodyPartsWithEffects) {
// // clear effects from profile bodyPart
// if (deleteExistingEffects) {
// // biome-ignore lint/performance/noDelete: Delete is fine here as we entirely want to get rid of the effect.
// delete pmcData.Health.BodyParts[bodyPart].Effects;
// }
//
// for (const effectType in bodyPartsWithEffects[bodyPart]) {
// if (typeof effectType !== "string") {
// this.logger.warning(`Effect ${effectType} on body part ${bodyPart} not a string, report this`);
// }
//
// // // data can be index or the effect string (e.g. "Fracture") itself
// // const effect = /^-?\d+$/.test(effectValue) // is an int
// // ? nodeEffects[bodyPart][effectValue]
// // : effectValue;
// let time = bodyPartsWithEffects[bodyPart][effectType];
// if (time) {
// // Sometimes the value can be Infinity instead of -1, blame HealthListener.cs in modules
// if (time === "Infinity") {
// this.logger.warning(
// `Effect ${effectType} found with value of Infinity, changed to -1, this is an issue with HealthListener.cs`,
// );
// time = -1;
// }
// this.addEffect(pmcData, bodyPart, effectType, time);
// } else {
// this.addEffect(pmcData, bodyPart, effectType);
// }
// }
// }
}
/// <summary>
@@ -113,6 +342,18 @@ public class HealthHelper
/// <param name="duration">How long the effect has left in seconds (-1 by default, no duration).</param>
protected void AddEffect(PmcData pmcData, string effectBodyPart, string effectType, int duration = -1)
{
throw new NotImplementedException();
// TODO: this will need to change, typing is all fucked up
// const profileBodyPart = pmcData.Health.BodyParts[effectBodyPart];
// if (!profileBodyPart.Effects) {
// profileBodyPart.Effects = {};
// }
//
// profileBodyPart.Effects[effectType] = { Time: duration };
//
// // Delete empty property to prevent client bugs
// if (this.isEmpty(profileBodyPart.Effects)) {
// // biome-ignore lint/performance/noDelete: Delete is fine here, we're removing an empty property to prevent game bugs.
// delete profileBodyPart.Effects;
// }
}
}
File diff suppressed because it is too large Load Diff
+7 -7
View File
@@ -95,13 +95,12 @@ public class InventoryHelper(
ItemEventRouterResponse output)
{
var itemWithModsToAddClone = _cloner.Clone(request.ItemWithModsToAdd);
var rootItemToAdd = itemWithModsToAddClone.FirstOrDefault();
// Get stash layouts ready for use
var stashFS2D = GetStashSlotMap(pmcData, sessionId);
if (stashFS2D is null)
{
_logger.Error("Unable to get stash map for players: { sessionId} stash");
_logger.Error($"Unable to get stash map for players: {sessionId} stash");
return;
}
@@ -125,13 +124,13 @@ public class InventoryHelper(
SetFindInRaidStatusForItem(itemWithModsToAddClone, request.FoundInRaid.GetValueOrDefault(false));
// Remove trader properties from root item
RemoveTraderRagfairRelatedUpdProperties(rootItemToAdd.Upd);
RemoveTraderRagfairRelatedUpdProperties(itemWithModsToAddClone[0].Upd);
// Run callback
try
{
if (request.Callback is not null)
request.Callback((int) (rootItemToAdd.Upd.StackObjectsCount ?? 0));
request.Callback((int)(itemWithModsToAddClone[0].Upd.StackObjectsCount ?? 0));
}
catch (Exception ex)
{
@@ -144,12 +143,13 @@ public class InventoryHelper(
}
// Add item + mods to output and profile inventory
output.ProfileChanges[sessionId]
.Items.NewItems.AddRange(itemWithModsToAddClone.Select(x => x));
.Items.NewItems.AddRange(itemWithModsToAddClone);
pmcData.Inventory.Items.AddRange(itemWithModsToAddClone);
_logger.Debug(
$"Added {rootItemToAdd.Upd?.StackObjectsCount ?? 1} item: {rootItemToAdd.Template} with: {itemWithModsToAddClone.Count - 1} mods to inventory"
$"Added {itemWithModsToAddClone[0].Upd?.StackObjectsCount ?? 1} item: {itemWithModsToAddClone[0].Template} with: {itemWithModsToAddClone.Count - 1} mods to inventory"
);
}
@@ -918,7 +918,7 @@ public class InventoryHelper(
/// <param name="pmcData">Player profile</param>
/// <param name="sessionID">session id</param>
/// <returns>2-dimensional array</returns>
protected int[][] GetStashSlotMap(PmcData pmcData, string sessionID)
protected int[][]? GetStashSlotMap(PmcData pmcData, string sessionID)
{
var playerStashSize = GetPlayerStashSize(sessionID);
return GetContainerMap(
+138 -113
View File
@@ -1,16 +1,13 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using SptCommon.Annotations;
using Core.Models.Eft.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.ItemEvent;
using Core.Models.Enums;
using Core.Models.Utils;
using Core.Services;
using Core.Utils;
using Core.Utils.Cloners;
using Core.Utils.Collections;
using SptCommon.Extensions;
namespace Core.Helpers;
@@ -933,8 +930,92 @@ public class ItemHelper(
}
}
public void ReplaceProfileInventoryIds(BotBaseInventory inventory, List<InsuredItem>? insuredItems = null)
{
// Blacklist
var itemIdBlacklist = new HashSet<string>();
itemIdBlacklist.UnionWith(
new List<string>{
inventory.Equipment,
inventory.QuestRaidItems,
inventory.QuestStashItems,
inventory.SortingTable,
inventory.Stash,
inventory.HideoutCustomizationStashId
});
itemIdBlacklist.UnionWith(inventory.HideoutAreaStashes.Keys);
// Add insured items ids to blacklist
if (insuredItems is not null)
{
itemIdBlacklist.UnionWith(insuredItems.Select(x => x.ItemId));
}
foreach (var item in inventory.Items)
{
if (itemIdBlacklist.Contains(item.Id))
{
continue;
}
// Generate new id
var newId = _hashUtil.Generate();
// Keep copy of original id
var originalId = item.Id;
// Update items id to new one we generated
item.Id = newId;
// Find all children of item and update their parent ids to match
var childItems = inventory.Items.Where(x => x.ParentId == originalId);
foreach (var childItem in childItems)
{
childItem.ParentId = newId;
}
// Also replace in quick slot if the old ID exists.
if (inventory.FastPanel is null)
{
continue;
}
// Update quickslot id
if (inventory.FastPanel.ContainsKey(originalId))
{
inventory.FastPanel[originalId] = newId;
}
}
}
public List<Item> ReplaceIDs(List<Item> items)
{
foreach (var item in items)
{
// Generate new id
var newId = _hashUtil.Generate();
// Keep copy of original id
var originalId = item.Id;
// Update items id to new one we generated
item.Id = newId;
// Find all children of item and update their parent ids to match
var childItems = items.Where(x => x.ParentId == originalId);
foreach (var childItem in childItems)
{
childItem.ParentId = newId;
}
}
return items;
}
/// <summary>
/// Regenerate all GUIDs with new IDs, for the exception of special item types (e.g. quest, sorting table, etc.) This
/// Regenerate all GUIDs with new IDs, with the exception of special item types (e.g. quest, sorting table, etc.) This
/// function will not mutate the original items list, but will return a new list with new GUIDs.
/// </summary>
/// <param name="originalItems">Items to adjust the IDs of</param>
@@ -942,127 +1023,74 @@ public class ItemHelper(
/// <param name="insuredItems">Insured items that should not have their IDs replaced</param>
/// <param name="fastPanel">Quick slot panel</param>
/// <returns>List<Item></returns>
public List<Item> ReplaceIDs(List<Item> originalItems, PmcData pmcData = null, List<InsuredItem> insuredItems = null,
Dictionary<string, string> fastPanel = null)
public List<Item> ReplaceIDs(
List<Item> originalItems,
PmcData? pmcData = null,
List<InsuredItem>? insuredItems = null,
Dictionary<string, string>? fastPanel = null)
{
var serialisedInventory = _jsonUtil.Serialize(originalItems);
var hideoutAreaStashes = pmcData?.Inventory?.HideoutAreaStashes ?? new();
// Blacklist
var itemIdBlacklist = new HashSet<string>();
if (pmcData != null)
{
itemIdBlacklist.UnionWith(
new List<string>{
pmcData.Inventory.Equipment,
pmcData.Inventory.QuestRaidItems,
pmcData.Inventory.QuestStashItems,
pmcData.Inventory.SortingTable,
pmcData.Inventory.Stash,
pmcData.Inventory.HideoutCustomizationStashId
});
itemIdBlacklist.UnionWith(pmcData.Inventory.HideoutAreaStashes.Keys);
}
// Add insured items ids to blacklist
if (insuredItems is not null)
{
itemIdBlacklist.UnionWith(insuredItems.Select(x => x.ItemId));
}
foreach (var item in originalItems)
{
if (pmcData != null)
{
// Insured items should not be renamed. Only works for PMCs.
if (insuredItems?.FirstOrDefault(i => i.ItemId == item.Id) != null)
continue;
// Do not replace the IDs of specific types of items.
if (item.Id == pmcData?.Inventory?.Equipment ||
item.Id == pmcData?.Inventory?.QuestRaidItems ||
item.Id == pmcData?.Inventory?.QuestStashItems ||
item.Id == pmcData?.Inventory?.SortingTable ||
item.Id == pmcData?.Inventory?.Stash ||
item.Id == pmcData?.Inventory?.HideoutCustomizationStashId ||
(hideoutAreaStashes?.ContainsKey(item.Id) ?? false))
{
continue;
}
}
// Replace the ID of the item in the serialised inventory using a regular expression.
var oldId = item.Id;
var newId = _hashUtil.Generate();
serialisedInventory = serialisedInventory.Replace(oldId, newId); // Node uses regex with "g" flag to replace all instances
// Also replace in quick slot if the old ID exists.
if (fastPanel != null)
{
foreach (var itemSlot in fastPanel)
{
if (fastPanel[itemSlot.Key] == oldId)
fastPanel[itemSlot.Key] = fastPanel[itemSlot.Key].Replace(oldId, newId); // Node uses regex with "g" flag to replace all instances
}
}
}
var items = _jsonUtil.Deserialize<List<Item>>(serialisedInventory);
// fix dupe id's
var dupes = new Dictionary<string, double?>();
var newParents = new Dictionary<string, List<Item>>();
var childrenMapping = new Dictionary<string, Dictionary<string, double?>>();
var oldToNewIds = new Dictionary<string, List<string>>();
// Finding duplicate IDs involves scanning the item three times.
// First scan - Check which ids are duplicated.
// Second scan - Map parents to items.
// Third scan - Resolve IDs.
foreach (var item in items)
{
if (!dupes.TryAdd(item.Id, 0))
{
dupes[item.Id] += 1;
}
}
foreach (var item in items)
{
if (!(dupes[item.Id] > 1))
if (itemIdBlacklist.Contains(item.Id))
{
continue;
}
// Generate new id
var newId = _hashUtil.Generate();
if (!newParents.ContainsKey(item.ParentId))
// Keep copy of original id
var originalId = item.Id;
// Update items id to new one we generated
item.Id = newId;
// Find all children of item and update their parent ids to match
var childItems = originalItems.Where(x => x.ParentId == originalId);
foreach (var childItem in childItems)
{
newParents.Add(item.ParentId, []);
childItem.ParentId = newId;
}
var newParentsItems = newParents.GetValueOrDefault(item.ParentId);
newParentsItems.Add(item);
if (!oldToNewIds.ContainsKey(item.Id))
// Also replace in quick slot if the old ID exists.
if (pmcData.Inventory.FastPanel is null)
{
oldToNewIds.Add(item.Id, []);
continue;
}
var oldToNewIdsItems = oldToNewIds.GetValueOrDefault(item.Id);
oldToNewIdsItems.Add(newId);
}
foreach (var item in items)
{
if (dupes[item.Id] > 1)
// Update quickslot id
if (pmcData.Inventory.FastPanel.ContainsKey(originalId))
{
var oldId = item.Id;
var newId = oldToNewIds[oldId][0];
oldToNewIds[oldId].RemoveAt(0);
item.Id = newId;
// Extract one of the children that's also duplicated.
if (newParents.ContainsKey(oldId) && newParents[oldId].Count > 0)
{
childrenMapping[newId] = new();
for (int i = 0; i < newParents[oldId].Count; i++)
{
// Make sure we haven't already assigned another duplicate child of
// same slot and location to this parent.
var childId = GetChildId(newParents[oldId][i]);
if (!childrenMapping.ContainsKey(childId))
{
childrenMapping[newId][childId] = 1;
newParents[oldId][i].ParentId = newId;
// Some very fucking sketchy stuff on this childIndex
// No clue wth was that childIndex supposed to be, but its not
newParents[oldId].RemoveAt(i);
}
}
}
pmcData.Inventory.FastPanel[originalId] = newId;
}
}
return items;
return originalItems;
}
/// <summary>
@@ -1431,11 +1459,11 @@ public class ItemHelper(
chosenCaliber,
staticAmmoDist,
defaultCartridgeTpl,
weapon?.Properties?.Chambers[0]?.Props?.Filters[0]?.Filter
(weapon?.Properties?.Chambers?.FirstOrDefault()?.Props?.Filters?.FirstOrDefault()?.Filter) ?? null
);
if (cartridgeTpl is null)
{
_logger.Debug($"Unable to fill item: {magazine[0].Id} {magTemplate.Name} with cartridges, none found.");
_logger.Debug($"Unable to fill item: {magazine.FirstOrDefault().Id} {magTemplate.Name} with cartridges, none found.");
return;
}
@@ -1851,12 +1879,9 @@ public class ItemHelper(
// Optional: new id to use
// Returns New root id
public string RemapRootItemId(List<Item> itemWithChildren, string newId = null)
public string RemapRootItemId(List<Item> itemWithChildren, string? newId = null)
{
if (newId is null)
{
newId = _hashUtil.Generate();
}
newId ??= _hashUtil.Generate();
var rootItemExistingId = itemWithChildren[0].Id;
+1 -1
View File
@@ -25,7 +25,7 @@ public class NotifierHelper(HttpServerHelper _httpServerHelper)
EventIdentifier = dialogueMessage.Id,
OfferId = ragfairData.OfferId,
HandbookId = ragfairData.HandbookId,
Count = ragfairData.Count
Count = (int)ragfairData.Count
};
}
+1 -1
View File
@@ -510,7 +510,7 @@ public class ProfileHelper(
/// <returns>True if account is developer</returns>
public bool IsDeveloperAccount(string sessionID)
{
return GetFullProfile(sessionID)?.ProfileInfo?.Edition?.ToLower().StartsWith("spt developer") == false;
return GetFullProfile(sessionID)?.ProfileInfo?.Edition?.ToLower().StartsWith("spt developer") ?? false;
}
/// <summary>
+58 -16
View File
@@ -96,7 +96,18 @@ public class QuestHelper(
/// <returns>Reduction of cartesian product between two quest lists</returns>
public List<Quest> GetDeltaQuests(List<Quest> before, List<Quest> after)
{
throw new System.NotImplementedException();
List<string> knownQuestsIds = [];
foreach (var quest in before) {
knownQuestsIds.Add(quest.Id);
}
if (knownQuestsIds.Count != 0) {
return after.Where((q) => {
return knownQuestsIds.IndexOf(q.Id) == -1;
}).ToList();
}
return after;
}
/// <summary>
@@ -107,7 +118,43 @@ public class QuestHelper(
/// <returns>the adjusted skill progress gain</returns>
public int AdjustSkillExpForLowLevels(Common profileSkill, int progressAmount)
{
throw new System.NotImplementedException();
var currentLevel = Math.Floor((double)(profileSkill.Progress / 100));
// Only run this if the current level is under 9
if (currentLevel >= 9) {
return progressAmount;
}
// This calculates how much progress we have in the skill's starting level
var startingLevelProgress = (profileSkill.Progress % 100) * ((currentLevel + 1) / 10);
// The code below assumes a 1/10th progress skill amount
var remainingProgress = progressAmount / 10;
// We have to do this loop to handle edge cases where the provided XP bumps your level up
// See "CalculateExpOnFirstLevels" in client for original logic
var adjustedSkillProgress = 0;
while (remainingProgress > 0 && currentLevel < 9) {
// Calculate how much progress to add, limiting it to the current level max progress
var currentLevelRemainingProgress = (currentLevel + 1) * 10 - startingLevelProgress;
_logger.Debug($"currentLevelRemainingProgress: {currentLevelRemainingProgress}");
var progressToAdd = Math.Min(remainingProgress, currentLevelRemainingProgress ?? 0);
var adjustedProgressToAdd = (10 / (currentLevel + 1)) * progressToAdd;
_logger.Debug($"Progress To Add: {progressToAdd} Adjusted for level: {adjustedProgressToAdd}");
// Add the progress amount adjusted by level
adjustedSkillProgress += (int)adjustedProgressToAdd;
remainingProgress -= (int)progressToAdd;
startingLevelProgress = 0;
currentLevel++;
}
// If there's any remaining progress, add it. This handles if you go from level 8 -> 9
if (remainingProgress > 0) {
adjustedSkillProgress += remainingProgress;
}
return adjustedSkillProgress;
}
/// <summary>
@@ -289,7 +336,7 @@ public class QuestHelper(
{
return (
condition.ConditionType == "Quest" &&
(condition.Target?.Item?.Contains(startedQuestId) ?? false) &&
((condition.Target?.Item?.Contains(startedQuestId) ?? false) || (condition.Target?.List?.Contains(startedQuestId) ?? false))&&
(condition.Status?.Contains(QuestStatusEnum.Started) ?? false)
);
}
@@ -318,7 +365,7 @@ public class QuestHelper(
return false;
}
if (!QuestIsProfileWhitelisted(profile.Info.GameVersion, quest.Id))
if (QuestIsProfileWhitelisted(profile.Info.GameVersion, quest.Id))
{
return false;
}
@@ -419,7 +466,7 @@ public class QuestHelper(
*/
protected bool QuestIsProfileBlacklisted(string gameVersion, string questId)
{
var questBlacklist = _questConfig.ProfileBlacklist[gameVersion];
var questBlacklist = _questConfig.ProfileBlacklist?.GetValueOrDefault(gameVersion);
if (questBlacklist is null)
{
// Not blacklisted
@@ -902,8 +949,7 @@ public class QuestHelper(
public ItemEventRouterResponse CompleteQuest(PmcData pmcData, CompleteQuestRequestData body, string sessionID)
{
var completeQuestResponse = _eventOutputHolder.GetOutput(sessionID);
var completedQuest = GetQuestFromDb(body.QuestId, pmcData);
var preCompleteProfileQuests = _cloner.Clone(pmcData.Quests);
var completedQuestId = body.QuestId;
@@ -961,10 +1007,7 @@ public class QuestHelper(
{
completeQuestResponse.ProfileChanges[sessionID].QuestsStatus.AddRange(questStatusChanges);
}
// Recalculate level in event player leveled up
pmcData.Info.Level = _playerService.CalculateLevel(pmcData);
return completeQuestResponse;
}
@@ -1128,7 +1171,6 @@ public class QuestHelper(
*/
protected List<Quest> UpdateQuestsForGameEdition(List<Quest> quests, string gameVersion)
{
_logger.Debug("[UpdateQuestsForGameEdition] If you are hitting this method, please confirm the return is comparable to Node");
var modifiedQuests = _cloner.Clone(quests);
foreach (var quest in modifiedQuests)
{
@@ -1174,7 +1216,7 @@ public class QuestHelper(
return false;
}
return quest.Conditions.Fail.Any(condition => (condition.Target.List?.Contains(completedQuestId) ?? false));
return quest.Conditions.Fail.Any(condition => (condition.Target?.List?.Contains(completedQuestId) ?? false));
}
)
.ToList();
@@ -1279,9 +1321,9 @@ public class QuestHelper(
foreach (var quest in quests)
{
// If quest has prereq of completed quest + availableAfter value > 0 (quest has wait time)
var nextQuestWaitCondition = quest.Conditions.AvailableForStart.FirstOrDefault(
x => (x.Target?.List.Contains(completedQuestId) ?? false) && x.AvailableAfter > 0
);
var nextQuestWaitCondition = quest.Conditions?.AvailableForStart?.FirstOrDefault(
x => ((x.Target?.List?.Contains(completedQuestId) ?? false) || (x.Target?.Item?.Contains(completedQuestId) ?? false)) && x.AvailableAfter > 0
); // as we have to use the ListOrT type now, check both List and Item for the above checks
if (nextQuestWaitCondition is not null)
{
File diff suppressed because it is too large Load Diff
+19 -41
View File
@@ -13,44 +13,20 @@ using SptCommon.Annotations;
namespace Core.Helpers
{
[Injectable]
public class RewardHelper
public class RewardHelper(
ISptLogger<RewardHelper> _logger,
HashUtil _hashUtil,
TimeUtil _timeUtil,
ItemHelper _itemHelper,
DatabaseService _databaseService,
ProfileHelper _profileHelper,
LocalisationService _localisationService,
TraderHelper _traderHelper,
PresetHelper _presetHelper,
ICloner _cloner,
PlayerService _playerService
)
{
private readonly ISptLogger<RewardHelper> _logger;
private readonly HashUtil _hashUtil;
private readonly TimeUtil _timeUtil;
private readonly ItemHelper _itemHelper;
private readonly DatabaseService _databaseService;
private readonly ProfileHelper _profileHelper;
private readonly LocalisationService _localisationService;
private readonly TraderHelper _traderHelper;
private readonly PresetHelper _presetHelper;
private readonly ICloner _cloner;
public RewardHelper(
ISptLogger<RewardHelper> logger,
HashUtil hashUtil,
TimeUtil timeUtil,
ItemHelper itemHelper,
DatabaseService databaseService,
ProfileHelper profileHelper,
LocalisationService localisationService,
TraderHelper traderHelper,
PresetHelper presetHelper,
ICloner cloner
)
{
_logger = logger;
_hashUtil = hashUtil;
_timeUtil = timeUtil;
_itemHelper = itemHelper;
_databaseService = databaseService;
_profileHelper = profileHelper;
_localisationService = localisationService;
_traderHelper = traderHelper;
_presetHelper = presetHelper;
_cloner = cloner;
}
/**
* Apply the given rewards to the passed in profile
* @param rewards List of rewards to apply
@@ -100,14 +76,16 @@ namespace Core.Helpers
case RewardType.Experience:
_profileHelper.AddExperienceToPmc(
sessionId,
(int)reward.Value
int.Parse(reward.Value.ToString())
); // this must occur first as the output object needs to take the modified profile exp value
// Recalculate level in event player leveled up
pmcProfile.Info.Level = _playerService.CalculateLevel(pmcProfile);
break;
case RewardType.TraderStanding:
_traderHelper.AddStandingToTrader(
sessionId,
reward.Target,
(double)reward.Value
double.Parse(reward.Value.ToString())
);
break;
case RewardType.TraderUnlock:
@@ -235,7 +213,7 @@ namespace Core.Helpers
var craftingRecipes = _databaseService.GetHideout().Production.Recipes;
// Area that will be used to craft unlocked item
var desiredHideoutAreaType = (HideoutAreas)craftUnlockReward.TraderId;
var desiredHideoutAreaType = (HideoutAreas)int.Parse(craftUnlockReward.TraderId.ToString());
var matchingProductions = craftingRecipes.Where(
(prod) =>
@@ -383,7 +361,7 @@ namespace Core.Helpers
if (defaultPreset is not null)
{
// Found preset, use mods to hydrate reward item
var presetAndMods = _itemHelper.ReplaceIDs(defaultPreset.Items);
var presetAndMods = _itemHelper.ReplaceIDs(_cloner.Clone(defaultPreset.Items));
var newRootId = _itemHelper.RemapRootItemId(presetAndMods);
reward.Items = presetAndMods;
@@ -1,10 +1,10 @@
using SptCommon.Annotations;
using SptCommon.Annotations;
using Core.Models.Eft.Common.Tables;
namespace Core.Helpers;
[Injectable]
public class SecureContainerHelper
public class SecureContainerHelper(ItemHelper _itemHelper)
{
/// <summary>
/// Get a list of the item IDs (NOT tpls) inside a secure container
@@ -13,6 +13,16 @@ public class SecureContainerHelper
/// <returns>List of ids</returns>
public List<string> GetSecureContainerItems(List<Item> items)
{
throw new NotImplementedException();
var secureContainer = items.First((x) => x.SlotId == "SecuredContainer");
// No container found, drop out
if (secureContainer is null) {
return [];
}
var itemsInSecureContainer = _itemHelper.FindAndReturnChildrenByItems(items, secureContainer.Id);
// Return all items returned and exclude the secure container item itself
return itemsInSecureContainer.Where((x) => x != secureContainer.Id).ToList();
}
}
+7 -7
View File
@@ -1,4 +1,4 @@
using System.Text.RegularExpressions;
using System.Text.RegularExpressions;
using System.Transactions;
using SptCommon.Annotations;
using Core.Models.Eft.Common;
@@ -72,7 +72,7 @@ public class TradeHelper(
var assortHasBuyRestrictions = _itemHelper.HasBuyRestrictions(itemPurchased);
if (assortHasBuyRestrictions)
{
this.checkPurchaseIsWithinTraderItemLimit(
CheckPurchaseIsWithinTraderItemLimit(
sessionID,
pmcData,
buyRequestData.TransactionId,
@@ -82,7 +82,7 @@ public class TradeHelper(
);
// Decrement trader item count
PurchaseDetails itemPurchaseDetails = new PurchaseDetails()
PurchaseDetails itemPurchaseDetails = new PurchaseDetails
{
Items =
[
@@ -144,7 +144,7 @@ public class TradeHelper(
if (assortHasBuyRestrictions)
{
// Will throw error if check fails
this.checkPurchaseIsWithinTraderItemLimit(
CheckPurchaseIsWithinTraderItemLimit(
sessionID,
pmcData,
buyRequestData.TransactionId,
@@ -272,7 +272,7 @@ public class TradeHelper(
if (sellRequest.TransactionId == Traders.RAGMAN)
{
// Edge case, `Circulate` quest needs to track when certain items are sold to him
this.incrementCirculateSoldToTraderCounter(profileWithItemsToSell, profileToReceiveMoney, sellRequest);
IncrementCirculateSoldToTraderCounter(profileWithItemsToSell, profileToReceiveMoney, sellRequest);
}
var pattern = @"\s+";
@@ -311,7 +311,7 @@ public class TradeHelper(
_paymentService.GiveProfileMoney(profileToReceiveMoney, sellRequest.Price, sellRequest, output, sessionID);
}
protected void incrementCirculateSoldToTraderCounter(
protected void IncrementCirculateSoldToTraderCounter(
PmcData profileWithItemsToSell,
PmcData profileToReceiveMoney,
ProcessSellTradeRequestData sellRequest
@@ -396,7 +396,7 @@ public class TradeHelper(
/// <param name="assortBeingPurchased">the item from trader being bought</param>
/// <param name="assortId">Id of assort being purchased</param>
/// <param name="count">How many of the item are being bought</param>
protected void checkPurchaseIsWithinTraderItemLimit(
protected void CheckPurchaseIsWithinTraderItemLimit(
string sessionId,
PmcData pmcData,
string traderId,
+4 -1
View File
@@ -7,6 +7,9 @@ public class UtilityHelper
{
public List<T> ArrayIntersect<T>(List<T> a, List<T> b)
{
throw new NotImplementedException();
//a.Intersect(x => b.Contains(x)).ToList();
// gives error Delegate type could not be infered
return a.Where(x => b.Contains(x)).ToList();
}
}
+10 -1
View File
@@ -146,7 +146,16 @@ public record StaticContainerDetails
public List<StaticContainerData> StaticContainers { get; set; }
[JsonPropertyName("staticForced")]
public List<SpawnpointTemplate> StaticForced { get; set; }
public List<StaticForced> StaticForced { get; set; }
}
public record StaticForced
{
[JsonPropertyName("containerId")]
public string ContainerId { get; set; }
[JsonPropertyName("itemTpl")]
public string ItemTpl { get; set; }
}
public record StaticContainerData
@@ -38,7 +38,7 @@ public record LocationBase
public List<Banner>? Banners { get; set; }
[JsonPropertyName("BossLocationSpawn")]
public List<BossLocationSpawn>? BossLocationSpawn { get; set; }
public List<BossLocationSpawn> BossLocationSpawn { get; set; }
[JsonPropertyName("secretExits")]
public List<Exit>? SecretExits { get; set; }
@@ -274,7 +274,7 @@ public record LocationBase
public LocationEvents? Events { get; set; }
[JsonPropertyName("exit_access_time")]
public int? ExitAccessTime { get; set; }
public double? ExitAccessTime { get; set; }
[JsonPropertyName("ForceOnlineRaidInPVE")]
public bool? ForceOnlineRaidInPVE { get; set; }
@@ -286,7 +286,7 @@ public record LocationBase
public int? ExitCount { get; set; }
[JsonPropertyName("exit_time")]
public int? ExitTime { get; set; }
public double? ExitTime { get; set; }
[JsonPropertyName("exits")]
public List<Exit>? Exits { get; set; }
@@ -328,7 +328,7 @@ public record LocationBase
public int? UsersSummonSeconds { get; set; }
[JsonPropertyName("waves")]
public List<Wave>? Waves { get; set; }
public List<Wave> Waves { get; set; }
}
public record Transit
@@ -361,7 +361,7 @@ public record Transit
public string? Target { get; set; }
[JsonPropertyName("time")]
public int? Time { get; set; }
public long? Time { get; set; }
}
public record NonWaveGroupScenario
@@ -619,7 +619,7 @@ public record ChancedEnemy
public record MinMaxBot : MinMax
{
[JsonPropertyName("WildSpawnType")]
public object? WildSpawnType { get; set; } // TODO: Could be WildSpawnType or string
public string? WildSpawnType { get; set; } // TODO: Could be WildSpawnType or string
}
public record MinPlayerWaitTime
@@ -628,7 +628,7 @@ public record MinPlayerWaitTime
public int? MinPlayers { get; set; }
[JsonPropertyName("time")]
public int? Time { get; set; }
public long? Time { get; set; }
}
public record Preview
+1 -13
View File
@@ -9,7 +9,7 @@ public record LooseLoot
public SpawnpointCount? SpawnpointCount { get; set; }
[JsonPropertyName("spawnpointsForced")]
public List<SpawnpointsForced>? SpawnpointsForced { get; set; }
public List<Spawnpoint>? SpawnpointsForced { get; set; }
[JsonPropertyName("spawnpoints")]
public List<Spawnpoint>? Spawnpoints { get; set; }
@@ -24,18 +24,6 @@ public record SpawnpointCount
public double? Std { get; set; }
}
public record SpawnpointsForced
{
[JsonPropertyName("locationId")]
public string? LocationId { get; set; }
[JsonPropertyName("probability")]
public double? Probability { get; set; }
[JsonPropertyName("template")]
public SpawnpointTemplate? Template { get; set; }
}
public record SpawnpointTemplate
{
[JsonPropertyName("Id")]
@@ -1,5 +1,6 @@
using System.Text.Json.Serialization;
using Core.Models.Eft.ItemEvent;
using Core.Models.Eft.Notes;
using Core.Models.Eft.Ragfair;
using Core.Models.Enums;
using Core.Utils.Json;
@@ -20,6 +21,7 @@ public record BotBase
[JsonPropertyName("sessionId")]
public string? SessionId { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
[JsonPropertyName("savage")]
public string? Savage { get; set; }
@@ -44,6 +46,7 @@ public record BotBase
[JsonPropertyName("Stats")]
public Stats? Stats { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
[JsonPropertyName("Encyclopedia")]
public Dictionary<string, bool>? Encyclopedia { get; set; }
@@ -155,6 +158,8 @@ public record Info
public int? PrestigeLevel { get; set; }
public string? Voice { get; set; }
public int? Level { get; set; }
///Experience the bot has gained
public int? Experience { get; set; }
public List<Ban>? Bans { get; set; }
public bool? BannedState { get; set; }
@@ -195,7 +200,9 @@ public record BotInfoSettings
{
public string? Role { get; set; }
public string? BotDifficulty { get; set; }
public double? Experience { get; set; }
// Experience given for being killed
public int? Experience { get; set; }
public double? StandingForKill { get; set; }
public double? AggressorBonus { get; set; }
public bool? UseSimpleAnimator { get; set; }
@@ -361,6 +368,9 @@ public record EftStats
public OverallCounters? OverallCounters { get; set; }
public float? SessionExperienceMult { get; set; }
public float? ExperienceBonusMult { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public Aggressor? Aggressor { get; set; }
public List<DroppedItem>? DroppedItems { get; set; }
public List<FoundInRaidItem>? FoundInRaidItems { get; set; }
@@ -405,6 +415,14 @@ public record Victim
public string? ColliderType { get; set; }
public string? Role { get; set; }
public string? Location { get; set; }
[JsonPropertyName("GInterface186.ProfileId")]
public string? UnusedProfileId { get; set; }
[JsonPropertyName("GInterface186.Nickname")]
public string? UnusedName { get; set; }
[JsonPropertyName("GInterface186.Side")]
public string? UnusedSide { get; set; }
[JsonPropertyName("GInterface186.PrestigeLevel")]
public int? UnusedPrestige { get; set; }
}
public record SessionCounters
@@ -441,14 +459,25 @@ public record Aggressor
public string? Category { get; set; }
public string? ColliderType { get; set; }
public string? Role { get; set; }
[JsonPropertyName("GInterface186.ProfileId")]
public string? UnusedProfileId { get; set; }
[JsonPropertyName("GInterface186.Nickname")]
public string? UnusedName { get; set; }
[JsonPropertyName("GInterface186.Side")]
public string? UnusedSide { get; set; }
[JsonPropertyName("GInterface186.PrestigeLevel")]
public int? UnusedPrestige { get; set; }
}
public record DamageHistory
{
public string? LethalDamagePart { get; set; }
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public LethalDamage? LethalDamage { get; set; }
[JsonConverter(typeof(ArrayToObjectFactoryConverter))]
[JsonIgnore(Condition = JsonIgnoreCondition.Never)]
public BodyPartsDamageHistory? BodyParts { get; set; }
}
@@ -665,12 +694,6 @@ public record HideoutSlot
public List<HideoutItem>? Items { get; set; }
}
public record HideoutItem : Item
{
[JsonPropertyName("count")]
public double? Count { get; set; }
}
public record LastCompleted
{
[JsonPropertyName("$oid")]
@@ -786,9 +809,3 @@ public record Bonus
[JsonPropertyName("skillType")]
public BonusSkillType? SkillType { get; set; }
}
public record Note
{
public double? Time { get; set; }
public string? Text { get; set; }
}
@@ -6,10 +6,10 @@ namespace Core.Models.Eft.Common.Tables;
public record Item
{
[JsonPropertyName("_id")]
public required string Id { get; set; }
public string? Id { get; set; }
[JsonPropertyName("_tpl")]
public string Template { get; set; }
public string? Template { get; set; }
[JsonPropertyName("parentId")]
public string? ParentId { get; set; }
@@ -25,6 +25,42 @@ public record Item
[JsonPropertyName("upd")]
public Upd? Upd { get; set; }
public HideoutItem ConvertToHideoutItem(Item item, double? count = null)
{
return new HideoutItem()
{
Id = item.Id,
Template = item.Template,
Upd = item.Upd,
Count = count
};
}
}
public record HideoutItem
{
[JsonPropertyName("id")]
public string? Id { get; set; }
[JsonPropertyName("_tpl")]
public string? Template { get; set; }
[JsonPropertyName("upd")]
public Upd? Upd { get; set; }
[JsonPropertyName("count")]
public double? Count { get; set; }
public Item ConvertToItem()
{
return new Item
{
Id = Id,
Template = Template,
Upd = Upd,
};
}
}
public record ItemLocation
@@ -146,6 +182,9 @@ public record UpdFaceShield
{
[JsonPropertyName("Hits")]
public int? Hits { get; set; }
[JsonPropertyName("HitSeed")]
public int? HitSeed { get; set; }
}
public record UpdRepairable
@@ -257,7 +257,7 @@ public record QuestCondition
public QuestConditionCounter? Counter { get; set; }
[JsonPropertyName("plantTime")]
public int? PlantTime { get; set; }
public double? PlantTime { get; set; }
[JsonPropertyName("zoneId")]
public string? ZoneId { get; set; }
@@ -266,7 +266,7 @@ public record QuestCondition
public bool? CountInRaid { get; set; }
[JsonPropertyName("completeInSeconds")]
public int? CompleteInSeconds { get; set; }
public double? CompleteInSeconds { get; set; }
[JsonPropertyName("isEncoded")]
public bool? IsEncoded { get; set; }
@@ -5,6 +5,6 @@ namespace Core.Models.Eft.Dialog;
public record SetDialogReadRequestData : IRequestData
{
[JsonPropertyName("dialogId")]
[JsonPropertyName("dialogs")]
public List<string>? Dialogs { get; set; }
}
@@ -12,5 +12,5 @@ public record OffraidEatRequestData : BaseInteractionRequestData
public int? Count { get; set; }
[JsonPropertyName("time")]
public int? Time { get; set; }
public long? Time { get; set; }
}
@@ -7,7 +7,7 @@ public record OffraidHealRequestData : BaseInteractionRequestData
public string? Item { get; set; }
public BodyPart? Part { get; set; }
public int? Count { get; set; }
public int? Time { get; set; }
public long? Time { get; set; }
}
public enum BodyPart
@@ -147,7 +147,7 @@ public record StageImprovementRequirement
public string? Type { get; set; }
}
public record StageRequirement : RequirementBase
public record StageRequirement
{
[JsonPropertyName("areaType")]
public int? AreaType { get; set; }
@@ -181,4 +181,7 @@ public record StageRequirement : RequirementBase
[JsonPropertyName("skillLevel")]
public int? SkillLevel { get; set; }
[JsonPropertyName("type")]
public string? Type { get; set; }
}
@@ -11,3 +11,10 @@ public record HideoutContinuousProductionStartRequestData : BaseInteractionReque
[JsonPropertyName("timestamp")]
public double? Timestamp { get; set; }
}
public record HideoutProperties
{
public int? BtcFarmGcs { get; set; }
public bool IsGeneratorOn { get; set; }
public bool WaterCollectorHasFilter { get; set; }
}
@@ -56,7 +56,7 @@ public record HideoutProduction
public bool? IsCodeProduction { get; set; }
}
public record Requirement : RequirementBase
public record Requirement
{
[JsonPropertyName("templateId")]
public string? TemplateId { get; set; }
@@ -87,10 +87,7 @@ public record Requirement : RequirementBase
[JsonPropertyName("gameVersions")]
public List<string>? GameVersions { get; set; }
}
public record RequirementBase
{
[JsonPropertyName("type")]
public string? Type { get; set; }
}
+1 -1
View File
@@ -99,7 +99,7 @@ public record QteEffect
public List<SkillLevelMultiplier>? LevelMultipliers { get; set; }
[JsonPropertyName("time")]
public int? Time { get; set; }
public long? Time { get; set; }
[JsonPropertyName("weight")]
public float? Weight { get; set; }
@@ -8,5 +8,5 @@ public record InventoryBindRequestData : InventoryBaseActionRequestData
public string? Item { get; set; }
[JsonPropertyName("index")]
public int? Index { get; set; }
public string? Index { get; set; }
}
@@ -11,7 +11,7 @@ public record ItemEventRouterRequest : IRequestData
public List<BaseInteractionRequestData>? Data { get; set; }
[JsonPropertyName("tm")]
public int? Time { get; set; }
public long? Time { get; set; }
[JsonPropertyName("reload")]
public int? Reload { get; set; }
@@ -67,7 +67,7 @@ public record EndRaidResult
/// Seconds in raid
/// </summary>
[JsonPropertyName("playTime")]
public int? PlayTime { get; set; }
public double? PlayTime { get; set; }
}
public record LocationTransit
@@ -15,7 +15,7 @@ public record NoteActionData : BaseInteractionRequestData
public record Note
{
[JsonPropertyName("Time")]
public int? Time { get; set; }
public double? Time { get; set; }
[JsonPropertyName("Text")]
public string? Text { get; set; }
@@ -1,4 +1,4 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
namespace Core.Models.Eft.Profile;
@@ -8,7 +8,7 @@ public record MessageContentRagfair
public string? OfferId { get; set; }
[JsonPropertyName("count")]
public int? Count { get; set; }
public double? Count { get; set; }
[JsonPropertyName("handbookId")]
public string? HandbookId { get; set; }
@@ -9,5 +9,5 @@ public record ExtendOfferRequestData : InventoryBaseActionRequestData
public string? OfferId { get; set; }
[JsonPropertyName("renewalTime")]
public int? RenewalTime { get; set; }
public long? RenewalTime { get; set; }
}
@@ -117,5 +117,5 @@ public record SellResult
public long? SellTime { get; set; }
[JsonPropertyName("amount")]
public decimal? Amount { get; set; }
public int? Amount { get; set; }
}
+11 -11
View File
@@ -3,15 +3,15 @@
public enum MemberCategory
{
Default = 0,
DEVELOPER = 1,
UNIQUE_ID = 2,
TRADER = 4,
GROUP = 8,
SYSTEM = 16,
CHAT_MODERATOR = 32,
CHAT_MODERATOR_WITH_PERMANENT_BAN = 64,
UNIT_TEST = 128,
SHERPA = 256,
EMISSARY = 512,
UNHEARD = 1024
Developer = 1,
UniqueId = 2,
Trader = 4,
Group = 8,
System = 16,
ChatModerator = 32,
ChatModeratorWithPermanentBan = 64,
UnitTest = 128,
Sherpa = 256,
Emissary = 512,
Unheard = 1024
}
@@ -524,10 +524,13 @@ public record AdjustmentDetails
public Dictionary<string, Dictionary<string, float>> Edit { get; set; }
}
public class ArmorPlateWeights : Dictionary<string, object>
public class ArmorPlateWeights
{
[JsonPropertyName("levelRange")]
public MinMax LevelRange { get; set; }
[JsonPropertyName("values")]
public Dictionary<string, Dictionary<string, double>> Values { get; set; }
}
public record RandomisedResourceDetails
@@ -179,7 +179,7 @@ public record ChatbotFeatures
public CommandoFeatures CommandoFeatures { get; set; }
[JsonPropertyName("commandUseLimits")]
public Dictionary<string, int> CommandUseLimits { get; set; }
public Dictionary<string, int?> CommandUseLimits { get; set; }
[JsonPropertyName("ids")]
public Dictionary<string, string> Ids { get; set; }
@@ -1,5 +1,6 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
using Core.Models.Common;
using Core.Models.Eft.Hideout;
namespace Core.Models.Spt.Config;
@@ -40,6 +41,22 @@ public record HideoutConfig : BaseConfig
[JsonPropertyName("cultistCircle")]
public CultistCircleSettings CultistCircle { get; set; }
[JsonPropertyName("hideoutCraftsToAdd")]
public List<HideoutCraftToAdd> HideoutCraftsToAdd { get; set; }
}
public record HideoutCraftToAdd
{
[JsonPropertyName("requirements")]
public List<Requirement> Requirements { get; set; }
[JsonPropertyName("craftIdToCopy")]
public string CraftIdToCopy { get; set; }
[JsonPropertyName("craftOutputTpl")]
public string CraftOutputTpl { get; set; }
}
public record CultistCircleSettings
@@ -44,7 +44,7 @@ public record InRaidConfig : BaseConfig
public bool KeepFiRSecureContainerOnDeath { get; set; }
/** If enabled always keep found in raid status on items */
[JsonPropertyName("alwaysKeepFoundInRaidonRaidEnd")]
[JsonPropertyName("alwaysKeepFoundInRaidOnRaidEnd")]
public bool AlwaysKeepFoundInRaidOnRaidEnd { get; set; }
/** Percentage chance a player scav hot is hostile to the player when scavving */
@@ -1,4 +1,4 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
using Core.Models.Eft.Common;
namespace Core.Models.Spt.Config;
@@ -10,23 +10,23 @@ public record ItemConfig : BaseConfig
/** Items that should be globally blacklisted */
[JsonPropertyName("blacklist")]
public List<string> Blacklist { get; set; }
public HashSet<string> Blacklist { get; set; }
/** Items that should not be lootable from any location */
[JsonPropertyName("lootableItemBlacklist")]
public List<string> LootableItemBlacklist { get; set; }
public HashSet<string> LootableItemBlacklist { get; set; }
/** items that should not be given as rewards */
[JsonPropertyName("rewardItemBlacklist")]
public List<string> RewardItemBlacklist { get; set; }
public HashSet<string> RewardItemBlacklist { get; set; }
/** Item base types that should not be given as rewards */
[JsonPropertyName("rewardItemTypeBlacklist")]
public List<string> RewardItemTypeBlacklist { get; set; }
public HashSet<string> RewardItemTypeBlacklist { get; set; }
/** Items that can only be found on bosses */
[JsonPropertyName("bossItems")]
public List<string> BossItems { get; set; }
public HashSet<string> BossItems { get; set; }
[JsonPropertyName("handbookPriceOverride")]
public Dictionary<string, HandbookPriceOverride> HandbookPriceOverride { get; set; }
@@ -158,7 +158,7 @@ public record EquipmentLootSettings
{
// Percentage chance item will be added to equipment
[JsonPropertyName("modSpawnChancePercent")]
public Dictionary<string, int> ModSpawnChancePercent { get; set; }
public Dictionary<string, double?> ModSpawnChancePercent { get; set; }
}
public record FixEmptyBotWavesSettings
@@ -1,4 +1,4 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
using Core.Models.Eft.Common;
namespace Core.Models.Spt.Config;
@@ -14,5 +14,5 @@ public record LootConfig : BaseConfig
/** Loose loot probability adjustments to apply on game start */
[JsonPropertyName("looseLootSpawnPointAdjustments")]
public Dictionary<string, Dictionary<string, double>> LooseLootSpawnPointAdjustments { get; set; }
public Dictionary<string, Dictionary<string, double>>? LooseLootSpawnPointAdjustments { get; set; }
}
@@ -387,7 +387,7 @@ public record TieredFlea
/// key: tpl, value: playerlevel
/// </summary>
[JsonPropertyName("unlocksTpl")]
public Dictionary<string, int?> UnlocksTpl { get; set; }
public Dictionary<string, int> UnlocksTpl { get; set; }
/// <summary>
/// key: item type id, value: playerlevel
@@ -396,7 +396,7 @@ public record TieredFlea
public Dictionary<string, int> UnlocksType { get; set; }
[JsonPropertyName("ammoTplUnlocks")]
public Dictionary<string, int?> AmmoTplUnlocks { get; set; }
public Dictionary<string, int>? AmmoTplUnlocks { get; set; }
[JsonPropertyName("ammoTiersEnabled")]
public bool AmmoTiersEnabled { get; set; }
@@ -1,4 +1,4 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
using Core.Models.Eft.Common;
using Core.Models.Enums;
using Core.Utils.Json.Converters;
@@ -132,7 +132,7 @@ public record ZombieSettings
public bool? Enabled { get; set; }
[JsonPropertyName("mapInfectionAmount")]
public Dictionary<string, int>? MapInfectionAmount { get; set; }
public Dictionary<string, double>? MapInfectionAmount { get; set; }
[JsonPropertyName("disableBosses")]
public List<string>? DisableBosses { get; set; }
@@ -1,4 +1,4 @@
using System.Text.Json.Serialization;
using System.Text.Json.Serialization;
namespace Core.Models.Spt.Location;
@@ -11,5 +11,5 @@ public record RaidChanges
public double? StaticLootPercent { get; set; }
[JsonPropertyName("simulatedRaidStartSeconds")]
public int? SimulatedRaidStartSeconds { get; set; }
public double? SimulatedRaidStartSeconds { get; set; }
}
@@ -46,7 +46,7 @@ public record LootRequest
/// key: item base type: value: max count
/// </summary>
[JsonPropertyName("itemLimits")]
public Dictionary<string, int>? ItemLimits { get; set; }
public Dictionary<string, double>? ItemLimits { get; set; }
[JsonPropertyName("itemStackLimits")]
public Dictionary<string, MinMax>? ItemStackLimits { get; set; }
@@ -60,7 +60,8 @@ public class SptWebSocketConnectionHandler(
}
// Once the websocket dies, we dispose of it
_logger.Debug(_localisationService.GetText("websocket-socket_lost_deleting_handle"));
//_logger.Debug(_localisationService.GetText("websocket-socket_lost_deleting_handle"));
// this is expected and relayed via "Player has disconnected" i dont think this is needed
lock (_lockObject)
{
if (_socketAliveTimers.TryGetValue(sessionID, out var timer))
+134 -9
View File
@@ -1,17 +1,41 @@
using SptCommon.Annotations;
using SptCommon.Annotations;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Location;
using Core.Models.Enums;
using Core.Models.Spt.Services;
using Core.Servers;
using Core.Models.Spt.Config;
using Core.Models.Utils;
using Core.Generators;
using Core.Utils;
using Core.Helpers;
namespace Core.Services;
[Injectable]
public class AirdropService
public class AirdropService(
ConfigServer configServer,
ISptLogger<AirdropService> _logger,
LootGenerator _lootGenerator,
HashUtil _hashUtil,
WeightedRandomHelper _weightedRandomHelper,
LocalisationService _localisationService,
ItemFilterService _itemFilterService,
ItemHelper _itemHelper)
{
protected AirdropConfig _airdropConfig = configServer.GetConfig<AirdropConfig>();
public GetAirdropLootResponse GenerateCustomAirdropLoot(GetAirdropLootRequest request)
{
throw new NotImplementedException();
if (!_airdropConfig.CustomAirdropMapping.TryGetValue(request.ContainerId, out var customAirdropInformation)) {
_logger.Warning(
$"Unable to find data for custom airdrop {request.ContainerId}, returning random airdrop instead"
);
return GenerateAirdropLoot();
}
return GenerateAirdropLoot(customAirdropInformation);
}
/// <summary>
@@ -21,9 +45,40 @@ public class AirdropService
/// </summary>
/// <param name="forcedAirdropType">OPTIONAL - Desired airdrop type, randomised when not provided</param>
/// <returns>List of LootItem objects</returns>
public GetAirdropLootResponse GenerateAirdropLoot(string forcedAirdropType = null)
public GetAirdropLootResponse GenerateAirdropLoot(SptAirdropTypeEnum? forcedAirdropType = null)
{
throw new NotImplementedException();
var airdropType = forcedAirdropType != null ? forcedAirdropType : ChooseAirdropType();
_logger.Debug($"Chose: {airdropType} for airdrop loot");
// Common/weapon/etc
var airdropConfig = GetAirdropLootConfigByType(airdropType);
// generate loot to put into airdrop crate
var crateLoot = airdropConfig.UseForcedLoot.GetValueOrDefault(false)
? _lootGenerator.CreateForcedLoot(airdropConfig.ForcedLoot)
: _lootGenerator.CreateRandomLoot(airdropConfig);
// Create airdrop crate and add to result in first spot
var airdropCrateItem = GetAirdropCrateItem((SptAirdropTypeEnum)airdropType);
// Add crate to front of list
crateLoot.Insert(0, airdropCrateItem);
// Reparent loot items to crate we added above
foreach (var item in crateLoot) {
if (item.Id == airdropCrateItem.Id) {
// Crate itself, don't alter
continue;
}
// no parentId = root item, make item have create as parent
if (item.ParentId is null) {
item.ParentId = airdropCrateItem.Id;
item.SlotId = "main";
}
}
return new GetAirdropLootResponse { Icon = airdropConfig.Icon, Container = crateLoot };
}
/// <summary>
@@ -33,7 +88,38 @@ public class AirdropService
/// <returns>Item</returns>
protected Item GetAirdropCrateItem(SptAirdropTypeEnum airdropType)
{
throw new NotImplementedException();
var airdropContainer = new Item {
Id = _hashUtil.Generate(),
Template = "", // picked later
Upd = new Upd()
{
SpawnedInSession = true,
StackObjectsCount = 1,
},
};
switch (airdropType) {
case SptAirdropTypeEnum.foodMedical:
airdropContainer.Template = ItemTpl.LOOTCONTAINER_AIRDROP_MEDICAL_CRATE;
break;
case SptAirdropTypeEnum.barter:
airdropContainer.Template = ItemTpl.LOOTCONTAINER_AIRDROP_SUPPLY_CRATE;
break;
case SptAirdropTypeEnum.weaponArmor:
airdropContainer.Template = ItemTpl.LOOTCONTAINER_AIRDROP_WEAPON_CRATE;
break;
case SptAirdropTypeEnum.mixed:
airdropContainer.Template = ItemTpl.LOOTCONTAINER_AIRDROP_COMMON_SUPPLY_CRATE;
break;
case SptAirdropTypeEnum.radar:
airdropContainer.Template = ItemTpl.LOOTCONTAINER_AIRDROP_TECHNICAL_SUPPLY_CRATE_EVENT_1;
break;
default:
airdropContainer.Template = ItemTpl.LOOTCONTAINER_AIRDROP_COMMON_SUPPLY_CRATE;
break;
}
return airdropContainer;
}
/// <summary>
@@ -42,7 +128,9 @@ public class AirdropService
/// <returns>airdrop type value</returns>
protected SptAirdropTypeEnum ChooseAirdropType()
{
throw new NotImplementedException();
var possibleAirdropTypes = _airdropConfig.AirdropTypeWeightings;
return _weightedRandomHelper.GetWeightedValue(possibleAirdropTypes);
}
/// <summary>
@@ -50,8 +138,45 @@ public class AirdropService
/// </summary>
/// <param name="airdropType">Type of airdrop to get settings for</param>
/// <returns>LootRequest</returns>
protected LootRequest GetAirdropLootConfigByType(AirdropTypeEnum airdropType)
protected AirdropLootRequest GetAirdropLootConfigByType(SptAirdropTypeEnum? airdropType)
{
throw new NotImplementedException();
var lootSettingsByType = _airdropConfig.Loot[airdropType.ToString()];
if (lootSettingsByType is null) {
_logger.Error(
_localisationService.GetText("location-unable_to_find_airdrop_drop_config_of_type", airdropType)
);
// TODO: Get Radar airdrop to work. Atm Radar will default to common supply drop (mixed)
// Default to common
lootSettingsByType = _airdropConfig.Loot[AirdropTypeEnum.Common.ToString()];
}
// Get all items that match the blacklisted types and fold into item blacklist
var itemTypeBlacklist = _itemFilterService.GetItemRewardBaseTypeBlacklist();
var itemsMatchingTypeBlacklist = _itemHelper.GetItems()
.Where(templateItem => !string.IsNullOrEmpty(templateItem.Parent))
.Where(templateItem => _itemHelper.IsOfBaseclasses(templateItem.Parent, itemTypeBlacklist))
.Select(templateItem => templateItem.Id);
var itemBlacklist = new HashSet<string>();
itemBlacklist.UnionWith(lootSettingsByType.ItemBlacklist);
itemBlacklist.UnionWith(_itemFilterService.GetItemRewardBlacklist());
itemBlacklist.UnionWith(_itemFilterService.GetBossItems());
itemBlacklist.UnionWith(itemsMatchingTypeBlacklist);
return new AirdropLootRequest {
Icon = lootSettingsByType.Icon,
WeaponPresetCount = lootSettingsByType.WeaponPresetCount,
ArmorPresetCount = lootSettingsByType.ArmorPresetCount,
ItemCount = lootSettingsByType.ItemCount,
WeaponCrateCount = lootSettingsByType.WeaponCrateCount,
ItemBlacklist = itemBlacklist.ToList(),
ItemTypeWhitelist = lootSettingsByType.ItemTypeWhitelist,
ItemLimits = lootSettingsByType.ItemLimits,
ItemStackLimits = lootSettingsByType.ItemStackLimits,
ArmorLevelWhitelist = lootSettingsByType.ArmorLevelWhitelist,
AllowBossItems = lootSettingsByType.AllowBossItems,
UseForcedLoot = lootSettingsByType.UseForcedLoot,
ForcedLoot = lootSettingsByType.ForcedLoot,
};
}
}
@@ -51,7 +51,7 @@ public class BotEquipmentFilterService
var botWeightingAdjustments = GetBotWeightingAdjustments(botRole, botLevel);
var botWeightingAdjustmentsByPlayerLevel = GetBotWeightingAdjustmentsByPlayerLevel(
botRole,
pmcProfile.Info.Level ?? 0
pmcProfile?.Info?.Level ?? 0
);
var botEquipConfig = _botEquipmentConfig[botRole.ToLower()];
@@ -122,8 +122,8 @@ public class BotEquipmentFilterService
foreach (var itemKey in generationChanges)
{
baseBotGeneration.Items.GetByJsonProp<GenerationData>(itemKey.Key).Weights = generationChanges.GetByJsonProp<GenerationData>(itemKey.Key).Weights;
baseBotGeneration.Items.GetByJsonProp<GenerationData>(itemKey.Key).Whitelist = generationChanges.GetByJsonProp<GenerationData>(itemKey.Key).Whitelist;
baseBotGeneration.Items.GetByJsonProp<GenerationData>(itemKey.Key).Weights = generationChanges.GetValueOrDefault(itemKey.Key).Weights;
baseBotGeneration.Items.GetByJsonProp<GenerationData>(itemKey.Key).Whitelist = generationChanges.GetValueOrDefault(itemKey.Key).Whitelist;
}
}
@@ -476,7 +476,7 @@ public class BotEquipmentFilterService
foreach (var itemToEditKvP in poolAdjustmentKvP.Value)
{
// Only make change if item exists as we're editing, not adding
if (locationToUpdate[itemToEditKvP.Key] != null || locationToUpdate[itemToEditKvP.Key] == 0)
if (locationToUpdate.GetValueOrDefault(itemToEditKvP.Key) != null || locationToUpdate.GetValueOrDefault(itemToEditKvP.Key) == 0)
{
locationToUpdate[itemToEditKvP.Key] = itemToEditKvP.Value;
}
@@ -1,6 +1,7 @@
using SptCommon.Annotations;
using Core.Models.Eft.Common.Tables;
using Core.Models.Utils;
using SptCommon.Extensions;
namespace Core.Services;
@@ -12,6 +13,7 @@ public class BotGenerationCacheService(
{
protected Dictionary<string, List<BotBase>> _storedBots = new Dictionary<string, List<BotBase>>();
protected Queue<BotBase> _activeBotsInRaid = [];
protected Lock _lock = new Lock();
/**
@@ -20,11 +22,14 @@ public class BotGenerationCacheService(
*/
public void StoreBots(string key, List<BotBase> botsToStore)
{
foreach (var bot in botsToStore)
lock (_lock)
{
if (!_storedBots.TryAdd(key, [bot]))
foreach (var bot in botsToStore)
{
_storedBots[key].Add(bot);
if (!_storedBots.TryAdd(key, [bot]))
{
_storedBots[key].Add(bot);
}
}
}
}
@@ -37,21 +42,24 @@ public class BotGenerationCacheService(
*/
public BotBase? GetBot(string key)
{
if (_storedBots.TryGetValue(key, out var bots))
lock (_lock)
{
if (bots.Count > 0)
if (_storedBots.TryGetValue(key, out var bots))
{
try
if (bots.Count > 0)
{
return _activeBotsInRaid.Dequeue();
}
catch (Exception _)
{
_logger.Error(_localisationService.GetText("bot-cache_has_zero_bots_of_requested_type", key));
try
{
return bots.PopFirst();
}
catch (Exception _)
{
_logger.Error(_localisationService.GetText("bot-cache_has_zero_bots_of_requested_type", key));
}
}
}
}
_logger.Error(_localisationService.GetText("bot-no_bot_type_in_cache", key));
return null;
}
@@ -62,7 +70,10 @@ public class BotGenerationCacheService(
*/
public void StoreUsedBot(BotBase botToStore)
{
_activeBotsInRaid.Enqueue(botToStore);
lock (_lock)
{
_activeBotsInRaid.Enqueue(botToStore);
}
}
/**
@@ -73,7 +84,10 @@ public class BotGenerationCacheService(
*/
public BotBase? GetUsedBot(string profileId)
{
return _activeBotsInRaid.FirstOrDefault(x => x.Id == profileId);
lock (_lock)
{
return _activeBotsInRaid.FirstOrDefault(x => x.Id == profileId);
}
}
/**
@@ -81,8 +95,11 @@ public class BotGenerationCacheService(
*/
public void ClearStoredBots()
{
_storedBots.Clear();
_activeBotsInRaid = [];
lock (_lock)
{
_storedBots.Clear();
_activeBotsInRaid = [];
}
}
/**
+626 -61
View File
@@ -1,17 +1,46 @@
using SptCommon.Annotations;
using Core.Helpers;
using SptCommon.Annotations;
using Core.Models.Eft.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.Hideout;
using Core.Models.Eft.ItemEvent;
using Core.Models.Eft.Profile;
using Core.Models.Enums;
using Core.Models.Enums.Hideout;
using Core.Models.Spt.Config;
using Core.Models.Spt.Hideout;
using Hideout = Core.Models.Eft.Common.Tables.Hideout;
using Core.Models.Utils;
using Core.Routers;
using Core.Servers;
using Core.Utils;
using Core.Utils.Cloners;
using SptCommon.Extensions;
namespace Core.Services;
[Injectable(InjectionType.Singleton)]
public class CircleOfCultistService
public class CircleOfCultistService(
ISptLogger<CircleOfCultistService> _logger,
TimeUtil _timeUtil,
ICloner _cloner,
EventOutputHolder _eventOutputHolder,
RandomUtil _randomUtil,
HashUtil _hashUtil,
ItemHelper _itemHelper,
PresetHelper _presetHelper,
ProfileHelper _profileHelper,
InventoryHelper _inventoryHelper,
HideoutHelper _hideoutHelper,
QuestHelper _questHelper,
DatabaseService _databaseService,
ItemFilterService _itemFilterService,
SeasonalEventService _seasonalEventService,
ConfigServer _configServer
)
{
protected HideoutConfig _hideoutConfig = _configServer.GetConfig<HideoutConfig>();
public const string CircleOfCultistSlotId = "CircleOfCultistsGrid1";
/// <summary>
/// Start a sacrifice event
/// Generate rewards
@@ -27,49 +56,89 @@ public class CircleOfCultistService
HideoutCircleOfCultistProductionStartRequestData request
)
{
throw new NotImplementedException();
var cultistCircleStashId = pmcData.Inventory.HideoutAreaStashes.GetValueOrDefault(HideoutAreas.CIRCLE_OF_CULTISTS.ToString());
// `cultistRecipes` just has single recipeId
var cultistCraftData = _databaseService.GetHideout().Production.CultistRecipes.FirstOrDefault();
List<Item> sacrificedItems = GetSacrificedItems(pmcData);
var sacrificedItemCostRoubles = sacrificedItems.Aggregate(
0D,
(sum, curr) => sum + (_itemHelper.GetItemPrice(curr.Template) ?? 0)
);
var rewardAmountMultiplier = GetRewardAmountMultiplier(pmcData, _hideoutConfig.CultistCircle);
// Get the rouble amount we generate rewards with from cost of sacrified items * above multipler
var rewardAmountRoubles = Math.Round(sacrificedItemCostRoubles * rewardAmountMultiplier);
// Check if it matches any direct swap recipes
var directRewardsCache = GenerateSacrificedItemsCache(_hideoutConfig.CultistCircle.DirectRewards);
var directRewardSettings = CheckForDirectReward(sessionId, sacrificedItems, directRewardsCache);
var hasDirectReward = directRewardSettings?.Reward.Count > 0;
// Get craft time and bonus status
var craftingInfo = GetCircleCraftingInfo(
rewardAmountRoubles,
_hideoutConfig.CultistCircle,
directRewardSettings
);
// Create production in pmc profile
RegisterCircleOfCultistProduction(
sessionId,
pmcData,
cultistCraftData.Id,
sacrificedItems,
craftingInfo.Time
);
var output = _eventOutputHolder.GetOutput(sessionId);
// Remove sacrificed items from circle inventory
foreach (var item in sacrificedItems)
{
if (item.SlotId == CircleOfCultistService.CircleOfCultistSlotId)
{
_inventoryHelper.RemoveItem(pmcData, item.Id, sessionId, output);
}
}
var rewards = hasDirectReward
? GetDirectRewards(sessionId, directRewardSettings, cultistCircleStashId)
: GetRewardsWithinBudget(
GetCultistCircleRewardPool(sessionId, pmcData, craftingInfo, _hideoutConfig.CultistCircle),
rewardAmountRoubles,
cultistCircleStashId,
_hideoutConfig.CultistCircle
);
// Get the container grid for cultist stash area
var cultistStashDbItem = _itemHelper.GetItem(ItemTpl.HIDEOUTAREACONTAINER_CIRCLEOFCULTISTS_STASH_1);
// Ensure rewards fit into container
var containerGrid = _inventoryHelper.GetContainerSlotMap(cultistStashDbItem.Value.Id);
AddRewardsToCircleContainer(sessionId, pmcData, rewards, containerGrid, cultistCircleStashId, output);
return output;
}
/// <summary>
/// Attempt to add all rewards to cultist circle, if they don't fit remove one and try again until they fit
/// </summary>
/// <param name="sessionId">Session id</param>
/// <param name="pmcData">Player profile</param>
/// <param name="rewards">Rewards to send to player</param>
/// <param name="containerGrid">Cultist grid to add rewards to</param>
/// <param name="cultistCircleStashId">Stash id</param>
/// <param name="output">Client output</param>
protected void AddRewardsToCircleContainer(
string sessionId,
PmcData pmcData,
List<List<Item>> rewards,
List<List<int>> containerGrid,
string cultistCircleStashId,
ItemEventRouterResponse output
)
private double GetRewardAmountMultiplier(PmcData pmcData, CultistCircleSettings cultistCircleSettings)
{
throw new NotImplementedException();
}
// Get a randomised value to multiply the sacrificed rouble cost by
var rewardAmountMultiplier = _randomUtil.GetFloat(
(float)cultistCircleSettings.RewardPriceMultiplerMinMax.Min,
(float)cultistCircleSettings.RewardPriceMultiplerMinMax.Max
);
/// <summary>
/// Create a map of the possible direct rewards, keyed by the items needed to be sacrificed
/// </summary>
/// <param name="directRewards">Direct rewards array from hideout config</param>
/// <returns>Dictionary</returns>
protected Dictionary<string, DirectRewardSettings> GenerateSacrificedItemsCache(List<DirectRewardSettings> directRewards)
{
throw new NotImplementedException();
}
// Adjust value generated by the players hideout management skill
var hideoutManagementSkill = _profileHelper.GetSkillFromProfile(pmcData, SkillTypes.HideoutManagement);
if (hideoutManagementSkill is not null)
{
rewardAmountMultiplier *=
(float)(1 + hideoutManagementSkill.Progress / 10000); // 5100 becomes 0.51, add 1 to it, 1.51, multiply the bonus by it (e.g. 1.2 x 1.51)
}
/// <summary>
/// Get the reward amount multiple value based on players hideout management skill + configs rewardPriceMultiplerMinMax values
/// </summary>
/// <param name="pmcData">Player profile</param>
/// <param name="cultistCircleSettings">Circle config settings</param>
/// <returns>Reward Amount Multiplier</returns>
protected double GetRewardAmountMultiplier(PmcData pmcData, CultistCircleSettings cultistCircleSettings)
{
throw new NotImplementedException();
return rewardAmountMultiplier;
}
/// <summary>
@@ -88,7 +157,17 @@ public class CircleOfCultistService
double craftingTime
)
{
throw new NotImplementedException();
// Create circle production/craft object to add to player profile
var cultistProduction = _hideoutHelper.InitProduction(recipeId, craftingTime, false);
// Flag as cultist circle for code to pick up later
cultistProduction.SptIsCultistCircle = true;
// Add items player sacrificed
cultistProduction.GivenItemsInStart = sacrificedItems;
// Add circle production to profile keyed to recipe id
pmcData.Hideout.Production[recipeId] = cultistProduction;
}
/// <summary>
@@ -105,7 +184,48 @@ public class CircleOfCultistService
DirectRewardSettings directRewardSettings = null
)
{
throw new NotImplementedException();
var result = new CircleCraftDetails
{
Time = -1,
RewardType = CircleRewardType.RANDOM,
RewardAmountRoubles = (int)rewardAmountRoubles,
RewardDetails = null,
};
// Direct reward edge case
if (directRewardSettings is not null)
{
result.Time = directRewardSettings.CraftTimeSeconds;
return result;
}
var random = new Random();
// Get a threshold where sacrificed amount is between thresholds min and max
var matchingThreshold = GetMatchingThreshold(circleConfig.CraftTimeThreshholds, rewardAmountRoubles);
if (
rewardAmountRoubles >= circleConfig.HideoutCraftSacrificeThresholdRub &&
random.Next(0, 1) <= circleConfig.BonusChanceMultiplier
)
{
// Sacrifice amount is enough + passed 25% check to get hideout/task rewards
result.Time =
circleConfig.CraftTimeOverride != -1
? circleConfig.CraftTimeOverride
: circleConfig.HideoutTaskRewardTimeSeconds;
result.RewardType = CircleRewardType.HIDEOUT_TASK;
return result;
}
// Edge case, check if override exists, Otherwise use matching threshold craft time
result.Time =
circleConfig.CraftTimeOverride != -1 ? circleConfig.CraftTimeOverride : matchingThreshold.CraftTimeSeconds;
result.RewardDetails = matchingThreshold;
return result;
}
protected CraftTimeThreshold GetMatchingThreshold(
@@ -113,7 +233,26 @@ public class CircleOfCultistService
double rewardAmountRoubles
)
{
throw new NotImplementedException();
var matchingThreshold = thresholds.FirstOrDefault(
(craftThreshold) => craftThreshold.Min <= rewardAmountRoubles && craftThreshold.Max >= rewardAmountRoubles
);
// No matching threshold, make one
if (matchingThreshold is null)
{
// None found, use a defalt
_logger.Warning("Unable to find a matching cultist circle threshold, using fallback of 12 hours");
// Use first threshold value (cheapest) from parameter array, otherwise use 12 hours
var firstThreshold = thresholds.FirstOrDefault();
var craftTime = firstThreshold?.CraftTimeSeconds is not null && firstThreshold.CraftTimeSeconds > 0
? firstThreshold.CraftTimeSeconds
: _timeUtil.GetHoursAsSeconds(12);
return new CraftTimeThreshold { Min = firstThreshold?.Min ?? 1, Max = firstThreshold?.Max ?? 34999, CraftTimeSeconds = craftTime };
}
return matchingThreshold;
}
/// <summary>
@@ -123,7 +262,23 @@ public class CircleOfCultistService
/// <returns>Array of items from player inventory</returns>
protected List<Item> GetSacrificedItems(PmcData pmcData)
{
throw new NotImplementedException();
// Get root items that are in the cultist sacrifice window
var inventoryRootItemsInCultistGrid = pmcData.Inventory.Items.Where(
(item) => item.SlotId == CircleOfCultistService.CircleOfCultistSlotId
);
// Get rootitem + its children
List<Item> sacrificedItems = [];
foreach (var rootItem in inventoryRootItemsInCultistGrid)
{
var rootItemWithChildren = _itemHelper.FindAndReturnChildrenAsItems(
pmcData.Inventory.Items,
rootItem.Id
);
sacrificedItems.AddRange(rootItemWithChildren);
}
return sacrificedItems;
}
/// <summary>
@@ -140,7 +295,95 @@ public class CircleOfCultistService
CultistCircleSettings circleConfig
)
{
throw new NotImplementedException();
// Prep rewards array (reward can be item with children, hence array of arrays)
List<List<Item>> rewards = [];
// Pick random rewards until we have exhausted the sacrificed items budget
var totalRewardCost = 0;
var rewardItemCount = 0;
var failedAttempts = 0;
while (
totalRewardCost < rewardBudget &&
rewardItemTplPool.Count > 0 &&
rewardItemCount < circleConfig.MaxRewardItemCount
)
{
if (failedAttempts > circleConfig.MaxAttemptsToPickRewardsWithinBudget)
{
_logger.Warning($"Exiting reward generation after {failedAttempts} failed attempts");
break;
}
// Choose a random tpl from pool
var randomItemTplFromPool = _randomUtil.GetArrayValue(rewardItemTplPool);
// Is weapon/armor, handle differently
if (
_itemHelper.ArmorItemHasRemovableOrSoftInsertSlots(randomItemTplFromPool) ||
_itemHelper.IsOfBaseclass(randomItemTplFromPool, BaseClasses.WEAPON)
)
{
var defaultPreset = _presetHelper.GetDefaultPreset(randomItemTplFromPool);
if (defaultPreset is null)
{
_logger.Warning($"Reward tpl: {randomItemTplFromPool} lacks a default preset, skipping reward");
failedAttempts++;
continue;
}
// Ensure preset has unique ids and is cloned so we don't alter the preset data stored in memory
var presetAndMods = _itemHelper.ReplaceIDs(defaultPreset.Items);
_itemHelper.RemapRootItemId(presetAndMods);
rewardItemCount++;
totalRewardCost += (int)_itemHelper.GetItemPrice(randomItemTplFromPool);
rewards.Add(presetAndMods);
continue;
}
// Some items can have variable stack size, e.g. ammo / currency
var stackSize = GetRewardStackSize(
randomItemTplFromPool,
(int)(rewardBudget / (rewardItemCount == 0 ? 1 : rewardItemCount)) // Remaining rouble budget
);
// Not a weapon/armor, standard single item
List<Item> rewardItem =
[
new Item
{
Id = _hashUtil.Generate(),
Template = randomItemTplFromPool,
ParentId = cultistCircleStashId,
SlotId = CircleOfCultistService.CircleOfCultistSlotId,
Upd = new Upd
{
StackObjectsCount = stackSize,
SpawnedInSession = true,
},
},
];
// Edge case - item is ammo container and needs cartridges added
if (_itemHelper.IsOfBaseclass(randomItemTplFromPool, BaseClasses.AMMO_BOX))
{
var itemDetails = _itemHelper.GetItem(randomItemTplFromPool).Value;
_itemHelper.AddCartridgesToAmmoBox(rewardItem, itemDetails);
}
// Increment price of rewards to give to player + add to reward array
rewardItemCount++;
var singleItemPrice = _itemHelper.GetItemPrice(randomItemTplFromPool);
var itemPrice = singleItemPrice * stackSize;
totalRewardCost += (int)itemPrice;
rewards.Add(rewardItem);
}
return rewards;
}
/// <summary>
@@ -156,7 +399,76 @@ public class CircleOfCultistService
string cultistCircleStashId
)
{
throw new NotImplementedException();
// Prep rewards array (reward can be item with children, hence array of arrays)
List<List<Item>> rewards = [];
// Handle special case of tagilla helmets - only one reward is allowed
if (directReward.Reward.Contains(ItemTpl.FACECOVER_TAGILLAS_WELDING_MASK_GORILLA))
{
directReward.Reward = [_randomUtil.GetArrayValue(directReward.Reward)];
}
// Loop because these can include multiple rewards
foreach (var rewardTpl in directReward.Reward)
{
// Is weapon/armor, handle differently
if (
_itemHelper.ArmorItemHasRemovableOrSoftInsertSlots(rewardTpl) ||
_itemHelper.IsOfBaseclass(rewardTpl, BaseClasses.WEAPON)
)
{
var defaultPreset = _presetHelper.GetDefaultPreset(rewardTpl);
if (defaultPreset is null)
{
_logger.Warning($"Reward tpl: {rewardTpl} lacks a default preset, skipping reward");
continue;
}
// Ensure preset has unique ids and is cloned so we don't alter the preset data stored in memory
var presetAndMods = _itemHelper.ReplaceIDs(defaultPreset.Items);
_itemHelper.RemapRootItemId(presetAndMods);
rewards.Add(presetAndMods);
continue;
}
// 'Normal' item, non-preset
var stackSize = GetDirectRewardBaseTypeStackSize(rewardTpl);
List<Item> rewardItem =
[
new Item
{
Id = _hashUtil.Generate(),
Template = rewardTpl,
ParentId = cultistCircleStashId,
SlotId = CircleOfCultistService.CircleOfCultistSlotId,
Upd = new Upd
{
StackObjectsCount = stackSize,
SpawnedInSession = true,
},
},
];
// Edge case - item is ammo container and needs cartridges added
if (_itemHelper.IsOfBaseclass(rewardTpl, BaseClasses.AMMO_BOX))
{
var itemDetails = _itemHelper.GetItem(rewardTpl).Value;
_itemHelper.AddCartridgesToAmmoBox(rewardItem, itemDetails);
}
rewards.Add(rewardItem);
}
// Direct reward is not repeatable, flag collected in profile
if (!directReward.Repeatable)
{
FlagDirectRewardAsAcceptedInProfile(sessionId, directReward);
}
return rewards;
}
/// <summary>
@@ -171,7 +483,28 @@ public class CircleOfCultistService
Dictionary<string, DirectRewardSettings> directRewardsCache
)
{
throw new NotImplementedException();
// Get sacrificed tpls
var sacrificedItemTpls = sacrificedItems.Select((item) => item.Template).ToList();
sacrificedItemTpls.Sort();
// Create md5 key of the items player sacrificed so we can compare against the direct reward cache
var sacrificedItemsKey = _hashUtil.GenerateMd5ForData(string.Concat(sacrificedItemTpls, ","));
var matchingDirectReward = directRewardsCache.GetValueOrDefault(sacrificedItemsKey);
if (matchingDirectReward is null)
{
// No direct reward
return null;
}
var fullProfile = _profileHelper.GetFullProfile(sessionId);
var directRewardHash = GetDirectRewardHashKey(matchingDirectReward);
if (fullProfile.SptData.CultistRewards?.ContainsKey(directRewardHash) ?? false)
{
// Player has already received this direct reward
return null;
}
return matchingDirectReward;
}
/// <summary>
@@ -181,7 +514,15 @@ public class CircleOfCultistService
/// <returns>Key</returns>
protected string GetDirectRewardHashKey(DirectRewardSettings directReward)
{
throw new NotImplementedException();
directReward.RequiredItems.Sort();
directReward.Reward.Sort();
var required = string.Concat(directReward.RequiredItems, ",");
var reward = string.Concat(directReward.Reward, ",");
// Key is sacrificed items separated by commas, a dash, then the rewards separated by commas
var key = $"{{{required}-{reward}}}";
return _hashUtil.GenerateMd5ForData(key);
}
/// <summary>
@@ -191,7 +532,20 @@ public class CircleOfCultistService
/// <returns>stack size of item</returns>
protected int GetDirectRewardBaseTypeStackSize(string rewardTpl)
{
throw new NotImplementedException();
var itemDetails = _itemHelper.GetItem(rewardTpl);
if (!itemDetails.Key) {
_logger.Warning($"{rewardTpl} is not an item, setting stack size to 1");
return 1;
}
// Look for parent in dict
var settings = _hideoutConfig.CultistCircle.DirectRewardStackSize[itemDetails.Value.Parent];
if (settings is null) {
return 1;
}
return _randomUtil.GetInt((int)settings.Min, (int)settings.Max);
}
/// <summary>
@@ -201,7 +555,14 @@ public class CircleOfCultistService
/// <param name="directReward">Reward sent to player</param>
protected void FlagDirectRewardAsAcceptedInProfile(string sessionId, DirectRewardSettings directReward)
{
throw new NotImplementedException();
var fullProfile = _profileHelper.GetFullProfile(sessionId);
AcceptedCultistReward dataToStoreInProfile = new AcceptedCultistReward {
Timestamp = _timeUtil.GetTimeStamp(),
SacrificeItems = directReward.RequiredItems,
RewardItems = directReward.Reward,
};
fullProfile.SptData.CultistRewards[GetDirectRewardHashKey(directReward)] = dataToStoreInProfile;
}
/// <summary>
@@ -213,7 +574,31 @@ public class CircleOfCultistService
/// <returns>Size of stack</returns>
protected int GetRewardStackSize(string itemTpl, int rewardPoolRemaining)
{
throw new NotImplementedException();
if (_itemHelper.IsOfBaseclass(itemTpl, BaseClasses.AMMO)) {
var ammoTemplate = _itemHelper.GetItem(itemTpl).Value;
return _itemHelper.GetRandomisedAmmoStackSize(ammoTemplate);
}
if (_itemHelper.IsOfBaseclass(itemTpl, BaseClasses.MONEY)) {
// Get currency-specific values from config
var settings = _hideoutConfig.CultistCircle.CurrencyRewards[itemTpl];
// What % of the pool remaining should be rewarded as chosen currency
var percentOfPoolToUse = _randomUtil.GetInt((int)settings.Min, (int)settings.Max);
// Rouble amount of pool we want to reward as currency
var roubleAmountToFill = _randomUtil.GetPercentOfValue(percentOfPoolToUse, rewardPoolRemaining);
// Convert currency to roubles
var currencyPriceAsRouble = _itemHelper.GetItemPrice(itemTpl);
// How many items can we fit into chosen pool
var itemCountToReward = Math.Round((roubleAmountToFill / currencyPriceAsRouble) ?? 0);
return (int)itemCountToReward;
}
return 1;
}
/// <summary>
@@ -224,13 +609,64 @@ public class CircleOfCultistService
/// <param name="rewardType">Do we return bonus items (hideout/task items)</param>
/// <param name="cultistCircleConfig">Circle config</param>
/// <returns>Array of tpls</returns>
protected string[] GetCultistCircleRewardPool(
protected List<string> GetCultistCircleRewardPool(
string sessionId,
PmcData pmcData,
CircleCraftDetails craftingInfo,
CultistCircleSettings cultistCircleConfig)
{
throw new NotImplementedException();
var rewardPool = new HashSet<string>();
var hideoutDbData = _databaseService.GetHideout();
var itemsDb = _databaseService.GetItems();
// Get all items that match the blacklisted types and fold into item blacklist below
var itemTypeBlacklist = _itemFilterService.GetItemRewardBaseTypeBlacklist();
var itemsMatchingTypeBlacklist = itemsDb
.Where((templateItem) => _itemHelper.IsOfBaseclasses(templateItem.Key, itemTypeBlacklist))
.Select((templateItem) => templateItem.Key);
// Create set of unique values to ignore
var itemRewardBlacklist = new HashSet<string>();
itemRewardBlacklist.UnionWith(_seasonalEventService.GetInactiveSeasonalEventItems());
itemRewardBlacklist.UnionWith(_itemFilterService.GetItemRewardBlacklist());
itemRewardBlacklist.UnionWith(cultistCircleConfig.RewardItemBlacklist);
itemRewardBlacklist.UnionWith(itemsMatchingTypeBlacklist);
// Hideout and task rewards are ONLY if the bonus is active
switch (craftingInfo.RewardType) {
case CircleRewardType.RANDOM: {
// Does reward pass the high value threshold
var isHighValueReward = craftingInfo.RewardAmountRoubles >= cultistCircleConfig.HighValueThresholdRub;
GenerateRandomisedItemsAndAddToRewardPool(rewardPool, itemRewardBlacklist, isHighValueReward);
break;
}
case CircleRewardType.HIDEOUT_TASK: {
// Hideout/Task loot
AddHideoutUpgradeRequirementsToRewardPool(hideoutDbData, pmcData, itemRewardBlacklist, rewardPool);
AddTaskItemRequirementsToRewardPool(pmcData, itemRewardBlacklist, rewardPool);
// If we have no tasks or hideout stuff left or need more loot to fill it out, default to high value
if (rewardPool.Count < cultistCircleConfig.MaxRewardItemCount + 2) {
GenerateRandomisedItemsAndAddToRewardPool(rewardPool, itemRewardBlacklist, true);
}
break;
}
}
// Add custom rewards from config
if (cultistCircleConfig.AdditionalRewardItemPool.Count > 0) {
foreach (var additionalReward in cultistCircleConfig.AdditionalRewardItemPool) {
if (itemRewardBlacklist.Contains(additionalReward)) {
continue;
}
// Add tpl to reward pool
rewardPool.Add(additionalReward);
}
}
return rewardPool.ToList();
}
/// <summary>
@@ -255,12 +691,35 @@ public class CircleOfCultistService
/// <param name="itemRewardBlacklist">Items not to add to pool</param>
/// <param name="rewardPool">Pool to add items to</param>
protected void AddHideoutUpgradeRequirementsToRewardPool(
Hideout hideoutDbData,
Core.Models.Spt.Hideout.Hideout hideoutDbData,
PmcData pmcData,
HashSet<string> itemRewardBlacklist,
HashSet<string> rewardPool)
{
throw new NotImplementedException();
var dbAreas = hideoutDbData.Areas;
foreach (var profileArea in GetPlayerAccessibleHideoutAreas(pmcData.Hideout.Areas)) {
var currentStageLevel = profileArea.Level;
var areaType = profileArea.Type;
// Get next stage of area
var dbArea = dbAreas.FirstOrDefault((area) => area.Type == areaType);
var nextStageDbData = dbArea?.Stages[(currentStageLevel + 1).ToString()];
if (nextStageDbData is not null) {
// Next stage exists, gather up requirements and add to pool
var itemRequirements = GetItemRequirements(nextStageDbData.Requirements);
foreach (var rewardToAdd in itemRequirements) {
if (
itemRewardBlacklist.Contains(rewardToAdd.TemplateId) ||
!_itemHelper.IsValidItem(rewardToAdd.TemplateId)
) {
// Dont reward items sacrificed
continue;
}
_logger.Debug($"Added Hideout Loot: {_itemHelper.GetItemName(rewardToAdd.TemplateId)}");
rewardPool.Add(rewardToAdd.TemplateId);
}
}
}
}
/// <summary>
@@ -268,9 +727,16 @@ public class CircleOfCultistService
/// </summary>
/// <param name="areas">Hideout areas to iterate over</param>
/// <returns>Active area array</returns>
protected BotHideoutArea[] GetPlayerAccessibleHideoutAreas(BotHideoutArea[] areas)
protected List<BotHideoutArea> GetPlayerAccessibleHideoutAreas(List<BotHideoutArea> areas)
{
throw new NotImplementedException();
return areas.Where((area) => {
if (area.Type == HideoutAreas.CHRISTMAS_TREE && !_seasonalEventService.ChristmasEventEnabled()) {
// Christmas tree area and not Christmas, skip
return false;
}
return true;
}).ToList();
}
/// <summary>
@@ -285,7 +751,33 @@ public class CircleOfCultistService
HashSet<string> itemRewardBlacklist,
bool itemsShouldBeHighValue)
{
throw new NotImplementedException();
var allItems = _itemHelper.GetItems();
var currentItemCount = 0;
var attempts = 0;
// `currentItemCount` var will look for the correct number of items, `attempts` var will keep this from never stopping if the highValueThreshold is too high
while (
currentItemCount < _hideoutConfig.CultistCircle.MaxRewardItemCount + 2 &&
attempts < allItems.Count
) {
attempts++;
var randomItem = _randomUtil.GetArrayValue(allItems);
if (itemRewardBlacklist.Contains(randomItem.Id) || !_itemHelper.IsValidItem(randomItem.Id)) {
continue;
}
// Valuable check
if (itemsShouldBeHighValue) {
var itemValue = _itemHelper.GetItemMaxPrice(randomItem.Id);
if (itemValue < _hideoutConfig.CultistCircle.HighValueThresholdRub) {
continue;
}
}
_logger.Debug($"Added: {_itemHelper.GetItemName(randomItem.Id)}");
rewardPool.Add(randomItem.Id);
currentItemCount++;
}
return rewardPool;
}
/// <summary>
@@ -293,8 +785,81 @@ public class CircleOfCultistService
/// </summary>
/// <param name="requirements">Requirements to iterate over</param>
/// <returns>Array of item requirements</returns>
protected (StageRequirement[] StageRequirement, Requirement[] Requirement) GetItemRequirements(RequirementBase[] requirements)
protected List<StageRequirement> GetItemRequirements(List<StageRequirement> requirements)
{
throw new NotImplementedException();
return requirements.Where((requirement) => requirement.Type == "Item").ToList();
}
/// <summary>
/// Iterate over passed in hideout requirements and return the Item
/// </summary>
/// <param name="requirements">Requirements to iterate over</param>
/// <returns>Array of item requirements</returns>
protected List<Requirement> GetItemRequirements(List<Requirement> requirements)
{
return requirements.Where((requirement) => requirement.Type == "Item").ToList();
}
/// <summary>
/// Create a map of the possible direct rewards, keyed by the items needed to be sacrificed
/// </summary>
/// <param name="directRewards">Direct rewards array from hideout config</param>
/// <returns>Dictionary</returns>
protected Dictionary<string, DirectRewardSettings> GenerateSacrificedItemsCache(List<DirectRewardSettings> directRewards)
{
var result = new Dictionary<string, DirectRewardSettings>();
foreach (var rewardSettings in directRewards) {
rewardSettings.RequiredItems.Sort();
var concat = string.Concat(rewardSettings.RequiredItems, ",");
var key = _hashUtil.GenerateMd5ForData(concat);
result[key] = rewardSettings;
}
return result;
}
/// <summary>
/// Attempt to add all rewards to cultist circle, if they don't fit remove one and try again until they fit
/// </summary>
/// <param name="sessionId">Session id</param>
/// <param name="pmcData">Player profile</param>
/// <param name="rewards">Rewards to send to player</param>
/// <param name="containerGrid">Cultist grid to add rewards to</param>
/// <param name="cultistCircleStashId">Stash id</param>
/// <param name="output">Client output</param>
protected void AddRewardsToCircleContainer(
string sessionId,
PmcData pmcData,
List<List<Item>> rewards,
int[][] containerGrid,
string cultistCircleStashId,
ItemEventRouterResponse output
)
{
var canAddToContainer = false;
while (!canAddToContainer && rewards.Count > 0) {
canAddToContainer = _inventoryHelper.CanPlaceItemsInContainer(
_cloner.Clone(containerGrid), // MUST clone grid before passing in as function modifies grid
rewards
);
// Doesn't fit, remove one item
if (!canAddToContainer) {
rewards.PopFirst();
}
}
foreach (var itemToAdd in rewards) {
_inventoryHelper.PlaceItemInContainer(
containerGrid,
itemToAdd,
cultistCircleStashId,
CircleOfCultistService.CircleOfCultistSlotId
);
// Add item + mods to output and profile inventory
output.ProfileChanges[sessionId].Items.NewItems.AddRange(itemToAdd);
pmcData.Inventory.Items.AddRange(itemToAdd);
}
}
}
+16 -19
View File
@@ -32,13 +32,16 @@ public class CreateProfileService(
SaveServer _saveServer,
EventOutputHolder _eventOutputHolder,
PlayerScavGenerator _playerScavGenerator,
ICloner _cloner
ICloner _cloner,
MailSendService _mailSendService
)
{
public string CreateProfile(string sessionId, ProfileCreateRequestData request)
{
var account = _saveServer.GetProfile(sessionId).ProfileInfo;
var profileTemplate = _cloner.Clone(_databaseService.GetProfiles()?.GetByJsonProp<ProfileSides>(account.Edition)?.GetByJsonProp<TemplateSide>(request.Side.ToLower()));
var profileTemplate = _cloner.Clone(
_databaseService.GetProfiles()?.GetByJsonProp<ProfileSides>(account.Edition)?.GetByJsonProp<TemplateSide>(request.Side.ToLower())
);
var pmcData = profileTemplate.Character;
// Delete existing profile
@@ -74,12 +77,7 @@ public class CreateProfileService(
AddMissingInternalContainersToProfile(pmcData);
// Change item IDs to be unique
pmcData.Inventory.Items = _itemHelper.ReplaceIDs(
pmcData.Inventory.Items,
pmcData,
null,
pmcData.Inventory.FastPanel
);
_itemHelper.ReplaceProfileInventoryIds(pmcData.Inventory);
// Create profile
var profileDetails = new SptProfile
@@ -444,18 +442,17 @@ public class CreateProfileService(
QuestStatusEnum.Started,
sessionID,
response
);
).ToList();
/* TODO:
_mailSendService.sendLocalisedNpcMessageToPlayer(
sessionID,
this.traderHelper.getTraderById(questFromDb.traderId),
MessageType.QUEST_START,
messageId,
itemRewards,
this.timeUtil.getHoursAsSeconds(100),
);
*/
_mailSendService.SendLocalisedNpcMessageToPlayer(
sessionID,
questFromDb.TraderId,
MessageType.QUEST_START,
messageId,
itemRewards,
_timeUtil.GetHoursAsSeconds(100)
);
}
}
}
@@ -1,54 +1,111 @@
using SptCommon.Annotations;
using Core.Models.Eft.Common;
using Core.Models.Spt.Config;
using Core.Models.Utils;
using Core.Servers;
using SptCommon.Annotations;
namespace Core.Services;
[Injectable(InjectionType.Singleton)]
public class CustomLocationWaveService
public class CustomLocationWaveService(
ISptLogger<CustomLocationWaveService> _logger,
DatabaseService _databaseService,
ConfigServer _configServer)
{
protected LocationConfig _locationConfig = _configServer.GetConfig<LocationConfig>();
/// <summary>
/// Add a boss wave to a map
/// Add a boss wave to a map
/// </summary>
/// <param name="locationId">e.g. factory4_day, bigmap</param>
/// <param name="waveToAdd">Boss wave to add to map</param>
public void AddBossWaveToMap(string locationId, BossLocationSpawn waveToAdd)
{
throw new NotImplementedException();
_locationConfig.CustomWaves.Boss[locationId].Add(waveToAdd);
}
/// <summary>
/// Add a normal bot wave to a map
/// Add a normal bot wave to a map
/// </summary>
/// <param name="locationId">e.g. factory4_day, bigmap</param>
/// <param name="waveToAdd">Wave to add to map</param>
public void AddNormalWaveToMap(string locationId, Wave waveToAdd)
{
throw new NotImplementedException();
_locationConfig.CustomWaves.Normal[locationId].Add(waveToAdd);
}
/// <summary>
/// Clear all custom boss waves from a map
/// Clear all custom boss waves from a map
/// </summary>
/// <param name="locationId">e.g. factory4_day, bigmap</param>
public void ClearBossWavesForMap(string locationId)
{
throw new NotImplementedException();
_locationConfig.CustomWaves.Boss[locationId] = [];
}
/// <summary>
/// Clear all custom normal waves from a map
/// Clear all custom normal waves from a map
/// </summary>
/// <param name="locationId">e.g. factory4_day, bigmap</param>
public void ClearNormalWavesForMap(string locationId)
{
throw new NotImplementedException();
_locationConfig.CustomWaves.Normal[locationId] = [];
}
/// <summary>
/// Add custom boss and normal waves to maps found in config/location.json to db
/// Add custom boss and normal waves to maps found in config/location.json to db
/// </summary>
public void ApplyWaveChangesToAllMaps()
{
throw new NotImplementedException();
var bossWavesToApply = _locationConfig.CustomWaves.Boss;
var normalWavesToApply = _locationConfig.CustomWaves.Normal;
foreach (var mapKvP in bossWavesToApply)
{
var locationBase = _databaseService.GetLocation(mapKvP.Key).Base;
if (locationBase is null)
{
_logger.Warning($"Unable to add custom boss wave to location: ${mapKvP}, location not found");
continue;
}
foreach (var bossWave in mapKvP.Value)
{
if (locationBase.BossLocationSpawn.Any(x => x.SptId == bossWave.SptId))
{
// Already exists, skip
continue;
}
locationBase.BossLocationSpawn.Add(bossWave);
_logger.Debug(
$"Added custom boss wave to {mapKvP.Key} of type {bossWave.BossName}, time: {bossWave.Time}, chance: {bossWave.BossChance}, zone: {(string.IsNullOrEmpty(bossWave.BossZone) ? "Global" : bossWave.BossZone)}"
);
}
}
foreach (var mapKvP in normalWavesToApply)
{
var locationBase = _databaseService.GetLocation(mapKvP.Key).Base;
if (locationBase is null)
{
_logger.Warning($"Unable to add custom wave to location: {mapKvP}, location not found");
continue;
}
foreach (var normalWave in mapKvP.Value)
{
if (locationBase.Waves.Any(x => x.SptId == normalWave.SptId))
{
// Already exists, skip
continue;
}
normalWave.Number = locationBase.Waves.Count;
locationBase.Waves.Add(normalWave);
}
}
}
}
+11 -11
View File
@@ -25,7 +25,7 @@ public class FenceService(
PresetHelper presetHelper,
LocalisationService localisationService,
ConfigServer configServer,
ICloner cloner
ICloner _cloner
)
{
protected TraderConfig traderConfig = configServer.GetConfig<TraderConfig>();
@@ -116,13 +116,13 @@ public class FenceService(
}
// Clone assorts so we can adjust prices before sending to client
var assort = cloner.Clone(fenceAssort);
var assort = _cloner.Clone(fenceAssort);
AdjustAssortItemPricesByConfigMultiplier(assort, 1, traderConfig.Fence.PresetPriceMult);
// merge normal fence assorts + discount assorts if player standing is large enough
if (pmcProfile.TradersInfo[Traders.FENCE].Standing >= 6)
{
var discountAssort = cloner.Clone(fenceDiscountAssort);
var discountAssort = _cloner.Clone(fenceDiscountAssort);
AdjustAssortItemPricesByConfigMultiplier(
discountAssort,
traderConfig.Fence.DiscountOptions.ItemPriceMult,
@@ -145,7 +145,7 @@ public class FenceService(
{
// HUGE THANKS TO LACYWAY AND LEAVES FOR PROVIDING THIS SOLUTION FOR SPT TO IMPLEMENT!!
// Copy the item and its children
var clonedItems = cloner.Clone(itemHelper.FindAndReturnChildrenAsItems(items, mainItem.Id));
var clonedItems = _cloner.Clone(itemHelper.FindAndReturnChildrenAsItems(items, mainItem.Id));
var root = clonedItems[0];
var cost = GetItemPrice(root.Template, clonedItems);
@@ -292,7 +292,7 @@ public class FenceService(
*/
public TraderAssort GetRawFenceAssorts()
{
return MergeAssorts(cloner.Clone(fenceAssort), cloner.Clone(fenceDiscountAssort));
return MergeAssorts(_cloner.Clone(fenceAssort), _cloner.Clone(fenceDiscountAssort));
}
/**
@@ -675,7 +675,7 @@ public class FenceService(
{
var result = new CreateFenceAssortsResult() { SptItems = [], BarterScheme = new(), LoyalLevelItems = new() };
var baseFenceAssortClone = cloner.Clone(databaseService.GetTrader(Traders.FENCE).Assort);
var baseFenceAssortClone = _cloner.Clone(databaseService.GetTrader(Traders.FENCE).Assort);
var itemTypeLimitCounts = InitItemLimitCounter(traderConfig.Fence.ItemTypeLimits);
if (itemCounts.Item > 0)
@@ -733,7 +733,7 @@ public class FenceService(
continue;
}
var desiredAssortItemAndChildrenClone = cloner.Clone(
var desiredAssortItemAndChildrenClone = _cloner.Clone(
itemHelper.FindAndReturnChildrenAsItems(baseFenceAssortClone.Items, chosenBaseAssortRoot.Id)
);
@@ -771,7 +771,7 @@ public class FenceService(
}
// MUST randomise Ids as its possible to add the same base fence assort twice = duplicate IDs = dead client
desiredAssortItemAndChildrenClone = itemHelper.ReplaceIDs(desiredAssortItemAndChildrenClone);
desiredAssortItemAndChildrenClone = itemHelper.ReplaceIDs(_cloner.Clone(desiredAssortItemAndChildrenClone));
itemHelper.RemapRootItemId(desiredAssortItemAndChildrenClone);
var rootItemBeingAdded = desiredAssortItemAndChildrenClone[0];
@@ -807,7 +807,7 @@ public class FenceService(
assorts.SptItems.Add(desiredAssortItemAndChildrenClone);
assorts.BarterScheme[rootItemBeingAdded.Id] =
cloner.Clone(baseFenceAssortClone.BarterScheme[chosenBaseAssortRoot.Id]);
_cloner.Clone(baseFenceAssortClone.BarterScheme[chosenBaseAssortRoot.Id]);
// Only adjust item price by quality for solo items, never multi-stack
if (isSingleStack)
@@ -1015,7 +1015,7 @@ public class FenceService(
var rootItemDb = itemHelper.GetItem(randomPresetRoot.Template).Value;
var presetWithChildrenClone = cloner.Clone(
var presetWithChildrenClone = _cloner.Clone(
itemHelper.FindAndReturnChildrenAsItems(baseFenceAssort.Items, randomPresetRoot.Id)
);
@@ -1076,7 +1076,7 @@ public class FenceService(
var randomPresetRoot = randomUtil.GetArrayValue(equipmentPresetRootItems);
var rootItemDb = itemHelper.GetItem(randomPresetRoot.Template).Value;
var presetWithChildrenClone = cloner.Clone(
var presetWithChildrenClone = _cloner.Clone(
itemHelper.FindAndReturnChildrenAsItems(baseFenceAssort.Items, randomPresetRoot.Id)
);
+2 -2
View File
@@ -114,9 +114,9 @@ public class I18nService
return rawLocalizedString;
}
public string GetLocalised<T>(string key, T value) where T : IConvertible
public string GetLocalised<T>(string key, T? value) where T : IConvertible
{
var rawLocalizedString = GetLocalised(key);
return rawLocalizedString.Replace("%s", value.ToString());
return rawLocalizedString.Replace("%s", value?.ToString());
}
}
@@ -5,9 +5,9 @@ namespace Core.Services;
[Injectable(InjectionType.Singleton)]
public class InMemoryCacheService(
ICloner _cloner)
ICloner _cloner
)
{
protected Dictionary<string, object?> _cacheData = new();
// Store data into an in-memory object
@@ -68,7 +68,7 @@ public class ItemBaseClassService(
HydrateItemBaseClassCache();
}
if (itemTpl is null)
if (string.IsNullOrEmpty(itemTpl))
{
_logger.Warning("Unable to check itemTpl base class as value passed is null");
+26 -12
View File
@@ -10,14 +10,13 @@ namespace Core.Services;
public class ItemFilterService(
ISptLogger<ItemFilterService> _logger,
ICloner _cloner,
DatabaseServer _databaseServer,
ConfigServer _configServer
)
{
protected ItemConfig _itemConfig = _configServer.GetConfig<ItemConfig>();
protected HashSet<string>? _lootableItemBlacklistCache = new HashSet<string>();
protected HashSet<string>? _itemBlacklistCache = new HashSet<string>();
protected HashSet<string>? _lootableItemBlacklistCache = [];
protected HashSet<string>? _itemBlacklistCache = [];
/**
* Check if the provided template id is blacklisted in config/item.json/blacklist
@@ -26,7 +25,14 @@ public class ItemFilterService(
*/
public bool ItemBlacklisted(string tpl)
{
throw new NotImplementedException();
if (_itemBlacklistCache.Count == 0)
{
foreach (var item in _itemConfig.Blacklist) {
_itemBlacklistCache.Add(item);
}
}
return _itemBlacklistCache.Contains(tpl);
}
/**
@@ -36,7 +42,14 @@ public class ItemFilterService(
*/
public bool LootableItemBlacklisted(string tpl)
{
throw new NotImplementedException();
if (_lootableItemBlacklistCache.Count == 0)
{
foreach (var item in _itemConfig.LootableItemBlacklist) {
_itemBlacklistCache.Add(item);
}
}
return _lootableItemBlacklistCache.Contains(tpl);
}
/**
@@ -46,7 +59,7 @@ public class ItemFilterService(
*/
public bool ItemRewardBlacklisted(string tpl)
{
throw new NotImplementedException();
return _itemConfig.RewardItemBlacklist.Contains(tpl);
}
/**
@@ -55,7 +68,7 @@ public class ItemFilterService(
*/
public List<string> GetItemRewardBlacklist()
{
throw new NotImplementedException();
return _cloner.Clone(_itemConfig.RewardItemBlacklist).ToList();
}
/**
@@ -64,7 +77,7 @@ public class ItemFilterService(
*/
public List<string> GetItemRewardBaseTypeBlacklist()
{
throw new NotImplementedException();
return _cloner.Clone(_itemConfig.RewardItemTypeBlacklist).ToList();
}
/**
@@ -73,7 +86,7 @@ public class ItemFilterService(
*/
public List<string> GetBlacklistedItems()
{
return _cloner.Clone(_itemConfig.Blacklist);
return _cloner.Clone(_itemConfig.Blacklist).ToList();
}
/**
@@ -82,7 +95,7 @@ public class ItemFilterService(
*/
public List<string> GetBlacklistedLootableItems()
{
throw new NotImplementedException();
return _cloner.Clone(_itemConfig.LootableItemBlacklist).ToList();
}
/**
@@ -92,7 +105,7 @@ public class ItemFilterService(
*/
public bool BossItem(string tpl)
{
throw new NotImplementedException();
return _itemConfig.BossItems.Contains(tpl);
}
/**
@@ -101,7 +114,8 @@ public class ItemFilterService(
*/
public List<string> GetBossItems()
{
throw new NotImplementedException();
return _cloner.Clone(_itemConfig.BossItems).ToList();
}
/**
@@ -1,76 +0,0 @@
using SptCommon.Annotations;
using Core.Models.Eft.Common;
using Core.Models.Eft.Match;
namespace Core.Services;
[Injectable(InjectionType.Singleton)]
public class LegacyLocationLifecycleService
{
/// <summary>
/// Handle client/match/offline/end
/// </summary>
public void endOfflineRaid(EndOfflineRaidRequestData info, string sessionId)
{
throw new NotImplementedException();
}
/// <summary>
/// Handle when a player extracts using a car - Add rep to fence
/// </summary>
/// <param name="extractName">name of the extract used</param>
/// <param name="pmcData">Player profile</param>
/// <param name="sessionId">Session id</param>
protected void handleCarExtract(string extractName, PmcData pmcData, string sessionId)
{
throw new NotImplementedException();
}
/// <summary>
/// Get the fence rep gain from using a car or coop extract
/// </summary>
/// <param name="pmcData">Profile</param>
/// <param name="baseGain">amount gained for the first extract</param>
/// <param name="extractCount">Number of times extract was taken</param>
/// <returns>Fence standing after taking extract</returns>
protected int getFenceStandingAfterExtract(PmcData pmcData, int baseGain, int extractCount)
{
throw new NotImplementedException();
}
/// <summary>
/// Was extract by car
/// </summary>
/// <param name="extractName">name of extract</param>
/// <returns>true if car extract</returns>
protected bool extractWasViaCar(string extractName)
{
throw new NotImplementedException();
}
/// <summary>
/// Did player take a COOP extract
/// </summary>
/// <param name="extractName">Name of extract player took</param>
/// <returns>True if coop extract</returns>
protected bool extractWasViaCoop(string extractName)
{
throw new NotImplementedException();
}
/// <summary>
/// Handle when a player extracts using a coop extract - add rep to fence
/// </summary>
/// <param name="sessionId">Session/player id</param>
/// <param name="pmcData">Profile</param>
/// <param name="extractName">Name of extract taken</param>
protected void handleCoopExtract(string sessionId, PmcData pmcData, string extractName)
{
throw new NotImplementedException();
}
protected void sendCoopTakenFenceMessage(string sessionId)
{
throw new NotImplementedException();
}
}
@@ -126,7 +126,7 @@ public class LocationLifecycleService
_logger.Debug($"Starting: {request.Location}");
var playerProfile = _profileHelper.GetPmcProfile(sessionId);
var result = new StartLocalRaidResponseData
{
ServerId = $"{request.Location}.{request.PlayerSide} {_timeUtil.GetTimeStamp()}", // TODO - does this need to be more verbose - investigate client?
@@ -135,7 +135,7 @@ public class LocationLifecycleService
{
InsuredItems = playerProfile.InsuredItems
},
LocationLoot = GenerateLocationAndLoot(request.Location, request.ShouldSkipLootGeneration == false),
LocationLoot = GenerateLocationAndLoot(request.Location, !request.ShouldSkipLootGeneration ?? true),
TransitionType = TransitionType.NONE,
Transition = new Transition
{
@@ -156,6 +156,7 @@ public class LocationLifecycleService
var transitionData = _applicationContext
.GetLatestValue(ContextVariableType.TRANSIT_INFO)
?.GetValue<LocationTransit>();
if (transitionData is not null) {
_logger.Success($"Player: {sessionId} is in transit to {request.Location}");
result.Transition.TransitionType = TransitionType.COMMON;
@@ -314,7 +315,7 @@ public class LocationLifecycleService
}
// Check for a loot multipler adjustment in app context and apply if one is found
var locationConfigClone = new LocationConfig();
LocationConfig? locationConfigClone = null;
var raidAdjustments = _applicationContext
.GetLatestValue(ContextVariableType.RAID_ADJUSTMENTS)
?.GetValue<RaidChanges>();
@@ -484,6 +485,11 @@ public class LocationLifecycleService
*/
protected void HandleCarExtract(string extractName, PmcData pmcData, string sessionId)
{
pmcData.CarExtractCounts?.TryAdd(extractName, 0);
// Increment extract count value
pmcData.CarExtractCounts[extractName] += 1;
var newFenceStanding = GetFenceStandingAfterExtract(
pmcData,
_inRaidConfig.CarExtractBaseStandingGain,
@@ -512,10 +518,14 @@ public class LocationLifecycleService
*/
protected void HandleCoopExtract(string sessionId, PmcData pmcData, string extractName)
{
pmcData.CoopExtractCounts?.TryAdd(extractName, 0);
pmcData.CoopExtractCounts[extractName] += 1;
var newFenceStanding = GetFenceStandingAfterExtract(
pmcData,
_inRaidConfig.CarExtractBaseStandingGain,
pmcData.CarExtractCounts[extractName]);
_inRaidConfig.CoopExtractBaseStandingGain,
pmcData.CoopExtractCounts[extractName]);
var fenceId = Traders.FENCE;
pmcData.TradersInfo[fenceId].Standing = newFenceStanding;
@@ -524,8 +534,6 @@ public class LocationLifecycleService
_traderHelper.LevelUp(fenceId, pmcData);
pmcData.TradersInfo[fenceId].LoyaltyLevel = Math.Max((int)pmcData.TradersInfo[fenceId].LoyaltyLevel, 1);
_logger.Debug($"Car extract: {extractName} used, total times taken: {pmcData.CarExtractCounts[extractName]}");
// Copy updated fence rep values into scav profile to ensure consistency
var scavData = _profileHelper.GetScavProfile(sessionId);
scavData.TradersInfo[fenceId].Standing = pmcData.TradersInfo[fenceId].Standing;
@@ -1003,7 +1011,7 @@ public class LocationLifecycleService
MessageType.BTR_ITEMS_DELIVERY,
messageId,
items,
messageStoreTime);
(int)messageStoreTime);
}
protected void HandleInsuredItemLostEvent(
+8 -6
View File
@@ -7,6 +7,7 @@ using Core.Models.Spt.Dialog;
using Core.Models.Utils;
using Core.Servers;
using Core.Utils;
using Core.Utils.Cloners;
namespace Core.Services;
@@ -22,7 +23,8 @@ public class MailSendService(
NotificationSendHelper _notificationSendHelper,
LocalisationService _localisationService,
ItemHelper _itemHelper,
TraderHelper _traderHelper
TraderHelper _traderHelper,
ICloner _cloner
)
{
private const string _systemSenderId = "59e7125688a45068a6249071";
@@ -40,11 +42,11 @@ public class MailSendService(
*/
public void SendDirectNpcMessageToPlayer(
string sessionId,
string trader,
string? trader,
MessageType messageType,
string message,
List<Item>? items,
long? maxStorageTimeSeconds,
double? maxStorageTimeSeconds,
SystemData? systemData,
MessageContentRagfair? ragfair
)
@@ -72,14 +74,14 @@ public class MailSendService(
DialogType = MessageType.NPC_TRADER,
Trader = trader,
MessageText = message,
Items = new()
Items = []
};
// Add items to message
if (items?.Count > 0)
{
details.Items.AddRange(items);
details.ItemsMaxStorageLifetimeSeconds = maxStorageTimeSeconds ?? 172800;
details.ItemsMaxStorageLifetimeSeconds = (long?)(maxStorageTimeSeconds ?? 172800);
}
if (systemData is not null)
@@ -453,7 +455,7 @@ public class MailSendService(
};
// Ensure Ids are unique and cont collide with items in player inventory later
messageDetails.Items = _itemHelper.ReplaceIDs(messageDetails.Items);
messageDetails.Items = _itemHelper.ReplaceIDs(_cloner.Clone(messageDetails.Items));
// Ensure item exits in items db
foreach (var reward in messageDetails.Items)
+11 -6
View File
@@ -8,19 +8,22 @@ public class NotificationService
{
protected Dictionary<string, List<WsNotificationEvent>> _messageQueue = new();
public Dictionary<string, List<object>> GetMessageQueue()
public Dictionary<string, List<WsNotificationEvent>> GetMessageQueue()
{
throw new NotImplementedException();
return _messageQueue;
}
public List<object> GetMessageFromQueue(string sessionId)
public List<WsNotificationEvent>? GetMessageFromQueue(string sessionId)
{
throw new NotImplementedException();
return _messageQueue.GetValueOrDefault(sessionId);
}
public void UpdateMessageOnQueue(string sessionId, List<WsNotificationEvent> value)
{
throw new NotImplementedException();
if (_messageQueue.ContainsKey(sessionId))
{
_messageQueue[sessionId] = value;
}
}
public bool Has(string sessionID)
@@ -33,7 +36,9 @@ public class NotificationService
/// </summary>
public WsNotificationEvent Pop(string sessionID)
{
throw new NotImplementedException();
var result = Get(sessionID).First();
Get(sessionID).Remove(result);
return result;
}
/// <summary>
@@ -287,15 +287,15 @@ public class PmcChatResponseService(
protected UserDialogInfo GetVictimDetails(Victim pmcVictim)
{
var categories = new List<MemberCategory>{
MemberCategory.UNIQUE_ID,
MemberCategory.UniqueId,
MemberCategory.Default,
MemberCategory.Default,
MemberCategory.Default,
MemberCategory.Default,
MemberCategory.Default,
MemberCategory.Default,
MemberCategory.SHERPA,
MemberCategory.DEVELOPER
MemberCategory.Sherpa,
MemberCategory.Developer
};
var chosenCategory = _randomUtil.GetArrayValue(categories);
+388 -20
View File
@@ -1,51 +1,322 @@
using SptCommon.Annotations;
using Core.Models.Eft.Common;
using Core.Models.Enums;
using Core.Models.Spt.Config;
using Core.Models.Utils;
using Core.Servers;
using Core.Utils;
using Core.Utils.Cloners;
using SptCommon.Annotations;
namespace Core.Services;
[Injectable(InjectionType.Singleton)]
public class PostDbLoadService
public class PostDbLoadService(
ISptLogger<PostDbLoadService> _logger,
HashUtil _hashUtil,
DatabaseService _databaseService,
LocalisationService _localisationService,
SeasonalEventService _seasonalEventService,
CustomLocationWaveService _customLocationWaveService,
OpenZoneService _openZoneService,
ItemBaseClassService _itemBaseClassService,
ConfigServer _configServer,
ICloner _cloner)
{
protected HideoutConfig _hideoutConfig = _configServer.GetConfig<HideoutConfig>();
protected LocationConfig _locationConfig = _configServer.GetConfig<LocationConfig>();
protected LootConfig _lootConfig = _configServer.GetConfig<LootConfig>();
protected BotConfig _botConfig = _configServer.GetConfig<BotConfig>();
protected ItemConfig _itemConfig = _configServer.GetConfig<ItemConfig>();
protected RagfairConfig _ragfairConfig = _configServer.GetConfig<RagfairConfig>();
protected CoreConfig _coreConfig = _configServer.GetConfig<CoreConfig>();
public void PerformPostDbLoadActions()
{
// TODO:
// Regenerate base cache now mods are loaded and game is starting
// Mods that add items and use the baseClass service generate the cache including their items, the next mod that
// add items gets left out,causing warnings
_itemBaseClassService.HydrateItemBaseClassCache();
// Validate that only mongoIds exist in items, quests, and traders
// Kill the startup if not.
// TODO: We can probably remove this in a couple versions
_databaseService.ValidateDatabase();
if (!_databaseService.IsDatabaseValid())
{
throw new Exception("Server start failure, database invalid");
}
AddCustomLooseLootPositions();
AdjustMinReserveRaiderSpawnChance();
if (_coreConfig.Fixes.FixShotgunDispersion)
{
FixShotgunDispersions();
}
if (_locationConfig.AddOpenZonesToAllMaps)
{
_openZoneService.ApplyZoneChangesToAllMaps();
}
if (_locationConfig.AddCustomBotWavesToMaps)
{
_customLocationWaveService.ApplyWaveChangesToAllMaps();
}
if (_locationConfig.EnableBotTypeLimits)
{
AdjustMapBotLimits();
}
AdjustLooseLootSpawnProbabilities();
AdjustLocationBotValues();
if (_locationConfig.RogueLighthouseSpawnTimeSettings.Enabled)
{
FixRoguesSpawningInstantlyOnLighthouse();
}
if (_locationConfig.SplitWaveIntoSingleSpawnsSettings.Enabled)
{
//SplitBotWavesIntoSingleWaves();
}
AdjustLabsRaiderSpawnRate();
AdjustHideoutCraftTimes(_hideoutConfig.OverrideCraftTimeSeconds);
AdjustHideoutBuildTimes(_hideoutConfig.OverrideBuildTimeSeconds);
UnlockHideoutLootCrateCrafts();
CloneExistingCraftsAndAddNew();
RemovePraporTestMessage();
ValidateQuestAssortUnlocksExist();
if (_seasonalEventService.IsAutomaticEventDetectionEnabled())
{
_seasonalEventService.EnableSeasonalEvents();
}
// Flea bsg blacklist is off
if (!_ragfairConfig.Dynamic.Blacklist.EnableBsgList)
{
SetAllDbItemsAsSellableOnFlea();
}
AddMissingTraderBuyRestrictionMaxValue();
ApplyFleaPriceOverrides();
AddCustomItemPresetsToGlobals();
}
protected void CloneExistingCraftsAndAddNew()
{
var hideoutCraftDb = _databaseService.GetHideout().Production;
var craftsToAdd = _hideoutConfig.HideoutCraftsToAdd;
foreach (var craftToAdd in craftsToAdd) {
var clonedCraft = _cloner.Clone(
hideoutCraftDb.Recipes.FirstOrDefault((x) => x.Id == craftToAdd.CraftIdToCopy));
clonedCraft.Id = _hashUtil.Generate();
clonedCraft.Requirements = craftToAdd.Requirements;
clonedCraft.EndProduct = craftToAdd.CraftOutputTpl;
hideoutCraftDb.Recipes.Add(clonedCraft);
}
}
protected void AdjustMinReserveRaiderSpawnChance()
{
throw new NotImplementedException();
// Get reserve base.json
var reserveBase = _databaseService.GetLocation(ELocationName.RezervBase.ToString()).Base;
// Raiders are bosses, get only those from boss spawn array
foreach (var raiderSpawn in reserveBase.BossLocationSpawn.Where((boss) => boss.BossName == "pmcBot")) {
var isTriggered = raiderSpawn.TriggerId.Length > 0; // Empty string if not triggered
var newSpawnChance = isTriggered
? _locationConfig.ReserveRaiderSpawnChanceOverrides.Triggered
: _locationConfig.ReserveRaiderSpawnChanceOverrides.NonTriggered;
if (newSpawnChance == -1)
{
continue;
}
if (raiderSpawn.BossChance < newSpawnChance)
{
// Desired chance is bigger than existing, override it
raiderSpawn.BossChance = newSpawnChance;
}
}
}
protected void AddCustomLooseLootPositions()
{
throw new NotImplementedException();
var looseLootPositionsToAdd = _lootConfig.LooseLoot;
foreach (var (mapId, positionsToAdd) in looseLootPositionsToAdd) {
if (mapId is null)
{
_logger.Warning(_localisationService.GetText("location-unable_to_add_custom_loot_position", mapId));
continue;
}
var mapLooseLoot = _databaseService.GetLocation(mapId).LooseLoot.Value;
if (mapLooseLoot is null)
{
_logger.Warning(_localisationService.GetText("location-map_has_no_loose_loot_data", mapId));
continue;
}
foreach (var positionToAdd in positionsToAdd) {
// Exists already, add new items to existing positions pool
var existingLootPosition = mapLooseLoot.Spawnpoints.FirstOrDefault(
(x) => x.Template.Id == positionToAdd.Template.Id);
if (existingLootPosition is not null)
{
existingLootPosition.Template.Items.AddRange(positionToAdd.Template.Items);
existingLootPosition.ItemDistribution.AddRange(positionToAdd.ItemDistribution);
continue;
}
// New position, add entire object
mapLooseLoot.Spawnpoints.Add(positionToAdd);
}
}
}
// BSG have two values for shotgun dispersion, we make sure both have the same value
// BSG have two values for shotgun dispersion, we make sure both have the same value
protected void FixShotgunDispersions()
{
throw new NotImplementedException();
var itemDb = _databaseService.GetItems();
var shotguns = new List<string> { Weapons.SHOTGUN_12G_SAIGA_12K, Weapons.SHOTGUN_20G_TOZ_106, Weapons.SHOTGUN_12G_M870};
foreach (var shotgunId in shotguns) {
if (itemDb[shotgunId].Properties.ShotgunDispersion.HasValue)
{
itemDb[shotgunId].Properties.shotgunDispersion = itemDb[shotgunId].Properties.ShotgunDispersion;
}
}
}
// Apply custom limits on bot types as defined in configs/location.json/botTypeLimits
protected void AdjustMapBotLimits()
{
throw new NotImplementedException();
var mapsDb = _databaseService.GetLocations().GetDictionary();
if (_locationConfig.BotTypeLimits is null)
{
return;
}
foreach (var (mapId, limits) in _locationConfig.BotTypeLimits)
{
if (!mapsDb.TryGetValue(mapId, out var map))
{
_logger.Warning(
_localisationService.GetText("bot-unable_to_edit_limits_of_unknown_map", mapId));
continue;
}
foreach (var botToLimit in limits) {
var index = map.Base.MinMaxBots.FindIndex(x => x.WildSpawnType == botToLimit.Type);
if (index != -1)
{
// Existing bot type found in MinMaxBots array, edit
var limitObjectToUpdate = map.Base.MinMaxBots[index];
limitObjectToUpdate.Min = botToLimit.Min;
limitObjectToUpdate.Max = botToLimit.Max;
}
else
{
// Bot type not found, add new object
map.Base.MinMaxBots.Add( new MinMaxBot{
// Bot type not found, add new object
WildSpawnType = botToLimit.Type,
Min = botToLimit.Min,
Max = botToLimit.Max,
});
}
}
}
}
protected void AdjustLooseLootSpawnProbabilities()
{
throw new NotImplementedException();
if (_lootConfig.LooseLootSpawnPointAdjustments is null)
{
return;
}
foreach (var (mapId, mapAdjustments) in _lootConfig.LooseLootSpawnPointAdjustments) {
var mapLooseLootData = _databaseService.GetLocation(mapId).LooseLoot.Value;
if (mapLooseLootData is null)
{
_logger.Warning(_localisationService.GetText("location-map_has_no_loose_loot_data", mapId));
continue;
}
foreach (var (lootKey, newChanceValue) in mapAdjustments) {
var lootPostionToAdjust = mapLooseLootData.Spawnpoints.FirstOrDefault((spawnPoint) => spawnPoint.Template.Id == lootKey
);
if (lootPostionToAdjust is null)
{
_logger.Warning(
_localisationService.GetText("location-unable_to_adjust_loot_position_on_map", new {
lootKey = lootKey,
mapId = mapId }));
continue;
}
lootPostionToAdjust.Probability = newChanceValue;
}
}
}
protected void AdjustLocationBotValues()
{
throw new NotImplementedException();
var mapsDb = _databaseService.GetLocations().GetDictionary();
foreach (var (key, cap) in _botConfig.MaxBotCap) {
if (!mapsDb.TryGetValue(key, out var map))
{
continue;
}
map.Base.BotMaxPvE = cap;
map.Base.BotMax = cap;
// make values no larger than 30 secs
map.Base.BotStart = Math.Min(map.Base.BotStart.Value, 30);
}
}
// Make Rogues spawn later to allow for scavs to spawn first instead of rogues filling up all spawn positions
protected void FixRoguesSpawningInstantlyOnLighthouse()
{
throw new NotImplementedException();
var rogueSpawnDelaySeconds = _locationConfig.RogueLighthouseSpawnTimeSettings.WaitTimeSeconds;
var lighthouse = _databaseService.GetLocations().Lighthouse?.Base;
if (lighthouse is null)
{
// Just in case they remove this cursed map
return;
}
// Find Rogues that spawn instantly
var instantRogueBossSpawns = lighthouse.BossLocationSpawn
.Where((spawn) => spawn.BossName == "exUsec" && spawn.Time == -1);
foreach (var wave in instantRogueBossSpawns) {
wave.Time = rogueSpawnDelaySeconds;
}
}
// Find and split waves with large numbers of bots into smaller waves - BSG appears to reduce the size of these
@@ -58,49 +329,146 @@ public class PostDbLoadService
// Make non-trigger-spawned raiders spawn earlier + always
protected void AdjustLabsRaiderSpawnRate()
{
throw new NotImplementedException();
var labsBase = _databaseService.GetLocations().Laboratory.Base;
// Find spawns with empty string for triggerId/TriggerName
var nonTriggerLabsBossSpawns = labsBase.BossLocationSpawn.Where(
(bossSpawn) => bossSpawn.TriggerId is null && bossSpawn.TriggerName is null);
foreach (var boss in nonTriggerLabsBossSpawns) {
boss.BossChance = 100;
boss.Time /= 10;
}
}
protected void AdjustHideoutCraftTimes(int overrideSeconds)
{
throw new NotImplementedException();
if (overrideSeconds == -1)
{
return;
}
foreach (var craft in _databaseService.GetHideout().Production.Recipes) {
// Only adjust crafts ABOVE the override
craft.ProductionTime = Math.Min(craft.ProductionTime.Value, overrideSeconds);
}
}
// Adjust all hideout craft times to be no higher than the override
protected void AdjustHideoutBuildTimes(int overrideSeconds)
{
throw new NotImplementedException();
if (overrideSeconds == -1)
{
return;
}
foreach (var area in _databaseService.GetHideout().Areas) {
foreach (var (key, stage) in area.Stages) {
// Only adjust crafts ABOVE the override
stage.ConstructionTime = Math.Min(stage.ConstructionTime.Value, overrideSeconds);
}
}
}
protected void UnlockHideoutLootCrateCrafts()
{
var hideoutLootBoxCraftIds = new List<string>{
"66582be04de4820934746cea",
"6745925da9c9adf0450d5bca",
"67449c79268737ef6908d636" };
foreach (var craftId in hideoutLootBoxCraftIds) {
var recipe = _databaseService.GetHideout().Production.Recipes.FirstOrDefault((craft) => craft.Id == craftId);
if (recipe is not null)
{
recipe.Locked = false;
}
}
}
// Blank out the "test" mail message from prapor
protected void RemovePraporTestMessage()
{
throw new NotImplementedException();
// Iterate over all languages (e.g. "en", "fr")
var locales = _databaseService.GetLocales();
foreach (var localeKvP in locales.Global) {
locales.Global[localeKvP.Key].Value["61687e2c3e526901fa76baf9"] = "";
}
}
// Check for any missing assorts inside each traders assort.json data, checking against traders questassort.json
protected void ValidateQuestAssortUnlocksExist()
{
throw new NotImplementedException();
var db = _databaseService.GetTables();
var traders = db.Traders;
var quests = db.Templates.Quests;
foreach (var (traderId, traderData) in traders) {
var traderAssorts = traderData?.Assort;
if (traderAssorts is null)
{
continue;
}
// Merge started/success/fail quest assorts into one dictionary
var mergedQuestAssorts = new Dictionary<string, string>();
mergedQuestAssorts.Concat(traderData.QuestAssort["started"])
.Concat(traderData.QuestAssort["success"])
.Concat(traderData.QuestAssort["fail"]).ToDictionary();
// Loop over all assorts for trader
foreach (var (assortKey, questKey) in (mergedQuestAssorts)) {
// Does assort key exist in trader assort file
if (!traderAssorts.LoyalLevelItems.ContainsKey(assortKey))
{
// Reverse lookup of enum key by value
var messageValues = new {
traderName = traderId,
questName = quests[questKey]?.QuestName ?? "UNKNOWN",
};
_logger.Warning(
_localisationService.GetText("assort-missing_quest_assort_unlock", messageValues)
);
}
}
}
}
protected void SetAllDbItemsAsSellableOnFlea()
{
throw new NotImplementedException();
var dbItems = _databaseService.GetItems().Values.ToList();
foreach (var item in dbItems.Where(item => item.Type == "Item" &&
!item.Properties.CanSellOnRagfair.GetValueOrDefault(false) &&
!_ragfairConfig.Dynamic.Blacklist.Custom.Contains(item.Id)))
{
item.Properties.CanSellOnRagfair = true;
}
}
protected void AddMissingTraderBuyRestrictionMaxValue()
{
throw new NotImplementedException();
var restrictions = _databaseService.GetGlobals().Configuration.TradingSettings.BuyRestrictionMaxBonus;
restrictions["unheard_edition"] = new BuyRestrictionMaxBonus{ Multiplier = restrictions["edge_of_darkness"].Multiplier,
};
}
protected void ApplyFleaPriceOverrides()
{
throw new NotImplementedException();
var fleaPrices = _databaseService.GetPrices();
foreach (var (itemTpl, price) in _ragfairConfig.Dynamic.ItemPriceOverrideRouble) {
fleaPrices[itemTpl] = price;
}
}
protected void AddCustomItemPresetsToGlobals()
{
throw new NotImplementedException();
foreach (var presetToAdd in _itemConfig.CustomItemGlobalPresets) {
if (_databaseService.GetGlobals().ItemPresets.ContainsKey(presetToAdd.Id))
{
_logger.Warning($"Global ItemPreset with Id of: { presetToAdd.Id} already exists, unable to overwrite");
continue;
}
_databaseService.GetGlobals().ItemPresets.TryAdd(presetToAdd.Id, presetToAdd);
}
}
}

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