Merge branch 'main' of https://github.com/sp-tarkov/server-csharp
This commit is contained in:
@@ -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('/');
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user