From 01d07ed903a254f4c013c90c152693692cf891f5 Mon Sep 17 00:00:00 2001 From: KaenoDev Date: Mon, 13 Jan 2025 17:56:00 +0000 Subject: [PATCH 1/3] Update buyclothing --- Core/Controllers/CustomizationController.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Core/Controllers/CustomizationController.cs b/Core/Controllers/CustomizationController.cs index d10496f3..cc95fa14 100644 --- a/Core/Controllers/CustomizationController.cs +++ b/Core/Controllers/CustomizationController.cs @@ -99,8 +99,27 @@ public class CustomizationController ItemId = suitDetails?.Id, ItemName = suitDetails?.Name, })); + + return output; + } + + PayForClothingItems(sessionId, pmcData, buyClothingRequest.Items, output); + + var profile = _saveServer.GetProfile(sessionId); + + profile.Suits.Add(suitId); + + //TODO: Merge with function _profileHelper.addHideoutCustomisationUnlock + var rewardToStore = new CustomisationStorage() + { + Id = suitId, + Source = CustomisationSource.UNLOCKED_IN_GAME, + Type = CustomisationType.SUITE + }; + profile.CustomisationUnlocks.Add(rewardToStore); + return output; } From 533d21f06cedc6f62467a236472d87fa647c3d6e Mon Sep 17 00:00:00 2001 From: CWX Date: Mon, 13 Jan 2025 18:00:53 +0000 Subject: [PATCH 2/3] implement questrewardhelper --- Core/Helpers/ItemHelper.cs | 31 +-- Core/Helpers/QuestRewardHelper.cs | 307 ++++++++++++++++++++++++++++-- 2 files changed, 307 insertions(+), 31 deletions(-) diff --git a/Core/Helpers/ItemHelper.cs b/Core/Helpers/ItemHelper.cs index df021f2f..e4aadf99 100644 --- a/Core/Helpers/ItemHelper.cs +++ b/Core/Helpers/ItemHelper.cs @@ -685,7 +685,7 @@ public class ItemHelper slot?.Name == item?.SlotId && (slot?.Required ?? false) ) ?? false; - + return itemTemplate.Key && parentTemplate.Key && (isNotRaidModdable || isRequiredSlot); } @@ -714,7 +714,7 @@ public class ItemHelper if (currentItem == null) return null; } - + return currentItem; } @@ -728,7 +728,6 @@ public class ItemHelper { // TODO: actually implement return true; - } /** @@ -976,22 +975,24 @@ public class ItemHelper throw new NotImplementedException(); } -// Update a root items _id property value to be unique -// Item to update root items _id property -// Optional: new id to use -// Returns New root id - public string RemapRootItemId(List itemWithChildren, string newId) // TODO: string newId = this.hashUtil.Generate() + // Update a root items _id property value to be unique + // Item to update root items _id property + // Optional: new id to use + // Returns New root id + // TODO: string newId used to default with _hashUtil.Generate(), Now pass this in + + public string RemapRootItemId(List itemWithChildren, string newId = null) { throw new NotImplementedException(); } -// Adopts orphaned items by resetting them as root "hideout" items. Helpful in situations where a parent has been -// deleted from a group of items and there are children still referencing the missing parent. This method will -// remove the reference from the children to the parent and set item properties to root values. -// -// The ID of the "root" of the container. -// Array of Items that should be adjusted. -// Returns Array of Items that have been adopted. + // Adopts orphaned items by resetting them as root "hideout" items. Helpful in situations where a parent has been + // deleted from a group of items and there are children still referencing the missing parent. This method will + // remove the reference from the children to the parent and set item properties to root values. + // + // The ID of the "root" of the container. + // Array of Items that should be adjusted. + // Returns Array of Items that have been adopted. public List AdoptOrphanedItems(string rootId, List items) { throw new NotImplementedException(); diff --git a/Core/Helpers/QuestRewardHelper.cs b/Core/Helpers/QuestRewardHelper.cs index 11eaf2d3..cff6ef44 100644 --- a/Core/Helpers/QuestRewardHelper.cs +++ b/Core/Helpers/QuestRewardHelper.cs @@ -8,6 +8,7 @@ using Core.Models.Spt.Config; using Core.Servers; using Core.Services; using Core.Utils; +using Core.Utils.Cloners; using ILogger = Core.Models.Utils.ILogger; namespace Core.Helpers; @@ -27,6 +28,7 @@ public class QuestRewardHelper private readonly PresetHelper _presetHelper; private readonly LocalisationService _localisationService; private readonly QuestConfig _questConfig; + private readonly ICloner _cloner; public QuestRewardHelper( ILogger logger, @@ -40,7 +42,9 @@ public class QuestRewardHelper ProfileHelper profileHelper, PresetHelper presetHelper, LocalisationService localisationService, - ConfigServer configServer) + ConfigServer configServer, + ICloner cloner + ) { _logger = logger; _hashUtil = hashUtil; @@ -53,6 +57,7 @@ public class QuestRewardHelper _profileHelper = profileHelper; _presetHelper = presetHelper; _localisationService = localisationService; + _cloner = cloner; _questConfig = configServer.GetConfig(ConfigTypes.QUEST); } @@ -66,9 +71,95 @@ public class QuestRewardHelper * @param questResponse Response to send back to client * @returns Array of reward objects */ - public IEnumerable ApplyQuestReward(PmcData profileData, string questId, QuestStatusEnum state, string sessionId, ItemEventRouterResponse questResponse) + public IEnumerable ApplyQuestReward(PmcData profileData, string questId, QuestStatusEnum state, string sessionId, + ItemEventRouterResponse questResponse) { - throw new System.NotImplementedException(); + // Repeatable quest base data is always in PMCProfile, `profileData` may be scav profile + // TODO: consider moving repeatable quest data to profile-agnostic location + var fullProfile = _profileHelper.GetFullProfile(sessionId); + var pmcProfile = fullProfile.CharacterData.PmcData; + if (pmcProfile != null) + { + _logger.Error($"Unable to get pmc profile for: {sessionId}, no rewards given"); + return Enumerable.Empty(); + } + + var questDetails = GetQuestFromDb(questId, pmcProfile); + if (questDetails != null) + { + _logger.Warning(_localisationService.GetText("quest-unable_to_find_quest_in_db_no_quest_rewards", questId)); + return Enumerable.Empty(); + } + + var questMoneyRewardBonusMultiplier = GetQuestMoneyRewardBonusMultiplier(pmcProfile); + if (questMoneyRewardBonusMultiplier > 0) // money = money + (money * intelCenterBonus / 100) + questDetails = ApplyMoneyBoost(questDetails, questMoneyRewardBonusMultiplier, state); + + // e.g. 'Success' or 'AvailableForFinish' + var questStateAsString = state.ToString(); + var gameVersion = pmcProfile.Info.GameVersion; + var questRewards = (List?)questDetails.Rewards.GetType().GetProperties().FirstOrDefault(p => + p.Name == questStateAsString).GetValue(questDetails.Rewards); + foreach (var reward in questRewards) + { + if (!QuestRewardIsForGameEdition(reward, gameVersion)) + continue; + + SkillTypes skillType; + + if (SkillTypes.TryParse(reward.Target, out skillType)) + { + _logger.Error($"Unable to get skill points for: {reward.Target}"); + continue; + } + + switch (reward.Type) + { + case QuestRewardType.Skill: + _profileHelper.AddSkillPointsToPlayer(profileData, skillType, double.Parse((string)reward.Value)); + break; + case QuestRewardType.Experience: // this must occur first as the output object needs to take the modified profile exp value + _profileHelper.AddExperienceToPmc(sessionId, int.Parse(reward.Target)); + break; + case QuestRewardType.TraderStanding: + _traderHelper.AddStandingToTrader(sessionId, reward.Target, double.Parse((string)reward.Value)); + break; + case QuestRewardType.TraderUnlock: + _traderHelper.SetTraderUnlockedState(reward.Target, true, sessionId); + break; + case QuestRewardType.Item: + // Handled by getQuestRewardItems() below + break; + case QuestRewardType.AssortmentUnlock: + // Handled by getAssort(), locked assorts are stripped out by `assortHelper.stripLockedLoyaltyAssort()` before being sent to player + break; + case QuestRewardType.Achievement: + _profileHelper.AddAchievementToProfile(fullProfile, reward.Target); + break; + case QuestRewardType.StashRows: // Add specified stash rows from quest reward - requires client restart + _profileHelper.AddStashRowsBonusToProfile(sessionId, int.Parse((string)reward.Value)); + break; + case QuestRewardType.ProductionScheme: + FindAndAddHideoutProductionIdToProfile(pmcProfile, reward, questDetails, sessionId, questResponse); + break; + case QuestRewardType.Pockets: + _profileHelper.ReplaceProfilePocketTpl(pmcProfile, reward.Target); + break; + case QuestRewardType.CustomizationDirect: + _profileHelper.AddHideoutCustomisationUnlock(fullProfile, reward, CustomisationSource.UNLOCKED_IN_GAME); + break; + default: + _logger.Error(_localisationService.GetText("quest-reward_type_not_handled", new + { + RewardType = reward.Type, + QuestId = questId, + questName = questDetails.QuestName + })); + break; + } + } + + return GetQuestRewardItems(questDetails, state, gameVersion); } /** @@ -77,9 +168,22 @@ public class QuestRewardHelper * @param gameVersion Version of game to check reward against * @returns True if it has requirement, false if it doesnt pass check */ - public bool QuestRewardIsForGameEdition(RewardDetails reward, string gameVersion) + public bool QuestRewardIsForGameEdition(QuestReward reward, string gameVersion) { - throw new System.NotImplementedException(); + if (reward?.AvailableInGameEditions?.Count > 0 && !reward.AvailableInGameEditions.Any(ge => ge == gameVersion)) + { + // Reward has edition whitelist and game version isnt in it + return false; + } + + if (reward?.NotAvailableInGameEditions?.Count > 0 && reward.NotAvailableInGameEditions.Any(ge => ge == gameVersion)) + { + // Reward has edition blacklist and game version is in it + return false; + } + + // No whitelist/blacklist or reward isnt blacklisted/whitelisted + return true; } /** @@ -102,7 +206,7 @@ public class QuestRewardHelper break; } } - + return quest; } @@ -113,7 +217,23 @@ public class QuestRewardHelper /// bonus as a percent protected double GetQuestMoneyRewardBonusMultiplier(PmcData pmcData) { - throw new NotImplementedException(); + // Check player has intel center + var moneyRewardbonuses = pmcData.Bonuses.Where(bonus => bonus.Type == BonusType.QuestMoneyReward); + + // Get a total of the quest money reward percent bonuses + var moneyRewardBonusPercent = moneyRewardbonuses.Aggregate(0D, (accumulate, bonus) => accumulate + bonus.Value ?? 0); + + // Calculate hideout management bonus as a percentage (up to 51% bonus) + var hideoutManagementSkill = _profileHelper.GetSkillFromProfile(pmcData, SkillTypes.HideoutManagement); + + // 5100 becomes 0.51, add 1 to it, 1.51 + // We multiply the money reward bonuses by the hideout management skill multipler, giving the new result + var hideoutManagementBonusMultiplier = (hideoutManagementSkill != null) + ? (1 + hideoutManagementSkill.Progress / 1000) + : 1; + + // e.g 15% * 1.4 + return moneyRewardBonusPercent * hideoutManagementBonusMultiplier ?? 1; } /** @@ -123,9 +243,23 @@ public class QuestRewardHelper * @param questStatus Status of quest to apply money boost to rewards of * @returns Updated quest */ - public Quest ApplyMoneyBoost(Quest quest, double bonusPercent, QuestStatus questStatus) + public Quest ApplyMoneyBoost(Quest quest, double bonusPercent, QuestStatusEnum questStatus) { - throw new NotImplementedException(); + var rewards = (List)quest?.Rewards.GetType().GetProperties().FirstOrDefault(p => p.Name == questStatus.ToString()) + .GetValue(quest.Rewards) ?? new(); + var currencyRewards = rewards.Where(r => + r.Type.ToString() == "Item" && + _paymentHelper.IsMoneyTpl(r.Items[0].Template)); + foreach (var reward in currencyRewards) + { + // Add % bonus to existing StackObjectsCount + var rewardItem = reward.Items[0]; + var newCurrencyAmount = Math.Floor((rewardItem.Upd.StackObjectsCount ?? 0) * (1 + bonusPercent / 100)); + rewardItem.Upd.StackObjectsCount = newCurrencyAmount; + reward.Value = newCurrencyAmount; + } + + return quest; } /// @@ -140,7 +274,22 @@ public class QuestRewardHelper protected void FindAndAddHideoutProductionIdToProfile(PmcData pmcData, QuestReward craftUnlockReward, Quest questDetails, string sessionID, ItemEventRouterResponse response) { - throw new NotImplementedException(); + var matchingProductions = GetRewardProductionMatch(craftUnlockReward, questDetails); + if (matchingProductions.Count != 1) + { + _logger.Error(_localisationService.GetText("quest-unable_to_find_matching_hideout_production", new + { + QuestName = questDetails.QuestName, + MatchCount = matchingProductions.Count + })); + + return; + } + + // Add above match to pmc profile + client response + var matchingCraftId = matchingProductions[0]?.Id; + pmcData?.UnlockedInfo?.UnlockedProductionRecipe?.Add(matchingCraftId); + response.ProfileChanges[sessionID].RecipeUnlocked[matchingCraftId] = true; } /// @@ -151,7 +300,25 @@ public class QuestRewardHelper /// Hideout craft public List GetRewardProductionMatch(QuestReward craftUnlockReward, Quest questDetails) { - throw new NotImplementedException(); + // Get hideout crafts and find those that match by areatype/required level/end product tpl - hope for just one match + var craftingRecipes = _databaseService.GetHideout().Production.Recipes; + + // Area that will be used to craft unlocked item + var desiredHideoutAreaType = int.Parse((string)craftUnlockReward.TraderId); + + var matchingProductions = craftingRecipes.Where(p => + p.AreaType == desiredHideoutAreaType && + p.Requirements.Any(r => r.Type == "QuestComplete") && + p.Requirements.Any(r => r.RequiredLevel == craftUnlockReward.LoyaltyLevel) && + p.EndProduct == craftUnlockReward.Items[0].Template); + + // More/less than single match, above filtering wasn't strict enough + if (matchingProductions.Count() != 1) + matchingProductions = matchingProductions.Where(p => + p.Requirements.Any(r => + r.QuestId == questDetails.Id)); + + return matchingProductions.ToList(); } /** @@ -160,9 +327,22 @@ public class QuestRewardHelper * @param status Quest status that holds the items (Started, Success, Fail) * @returns List of items with the correct maxStack */ - protected List GetQuestRewardItems(Quest quest, QuestStatus status, string gameVersion) + protected List GetQuestRewardItems(Quest quest, QuestStatusEnum status, string gameVersion) { - throw new NotImplementedException(); + var rewards = (List)quest?.Rewards.GetType().GetProperties().FirstOrDefault(p => p.Name == status.ToString()) + .GetValue(quest.Rewards); + + if (rewards == null) + return new(); + + // Iterate over all rewards with the desired status, flatten out items that have a type of Item + var questRewards = rewards.SelectMany(r => + r.Type.ToString() == "Item" && + QuestRewardIsForGameEdition(r, gameVersion) + ? ProcessReward(r) + : new()); + + return questRewards.ToList(); } /** @@ -172,7 +352,75 @@ public class QuestRewardHelper */ protected List ProcessReward(QuestReward questReward) { - throw new NotImplementedException(); + // item with mods to return + var rewardItems = new List(); + var targets = new List(); + var mods = new List(); + + // Is armor item that may need inserts / plates + if (questReward.Items.Count == 1 && _itemHelper.ArmorItemCanHoldMods(questReward.Items[0].Template)) + { + // Attempt to pull default preset from globals and add child items to reward (clones questReward.items) + GenerateArmorRewardChildSlots(questReward.Items[0], questReward); + } + + foreach (var rewardItem in questReward.Items) + { + _itemHelper.AddUpdObjectToItem(rewardItem); + + // Reward items are granted Found in Raid status + rewardItem.Upd.SpawnedInSession = true; + + // Is root item, fix stacks + if (rewardItem.Id == questReward.Type.ToString()) + { + // Is base reward item + if (rewardItem.ParentId != null && + rewardItem.ParentId == "hideout" && + rewardItem.Upd != null && + rewardItem.Upd.StackObjectsCount != null && + rewardItem.Upd.StackObjectsCount > 0) + { + rewardItem.Upd.StackObjectsCount = 1; + } + + targets = _itemHelper.SplitStack(rewardItem); + // splitStack created new ids for the new stacks. This would destroy the relation to possible children. + // Instead, we reset the id to preserve relations and generate a new id in the downstream loop, where we are also reparenting if required + + foreach (var target in targets) + { + target.Id = rewardItem.Id; + } + } + else + { + // Is child mod + if (questReward.Items[0].Upd.SpawnedInSession ?? false) // Propigate FiR status into child items + rewardItem.Upd.SpawnedInSession = questReward.Items[0].Upd.SpawnedInSession; + + mods.Add(rewardItem); + } + } + + // Add mods to the base items, fix ids + foreach (var target in targets) + { + // This has all the original id relations since we reset the id to the original after the splitStack + var itemsClone = new List(); + itemsClone.Add(_cloner.Clone(target)); + // Here we generate a new id for the root item + target.Id = _hashUtil.Generate(); + + foreach (var mod in mods) + { + itemsClone.Add(_cloner.Clone(mod)); + } + + rewardItems.AddRange(_itemHelper.ReparentItemAndChildren(target, itemsClone)); + } + + return rewardItems; } /** @@ -182,7 +430,34 @@ public class QuestRewardHelper */ protected void GenerateArmorRewardChildSlots(Item originalRewardRootItem, QuestReward questReward) { - throw new NotImplementedException(); - } + // Look for a default preset from globals for armor + var defaultPreset = _presetHelper.GetDefaultPreset(originalRewardRootItem.Template); + if (defaultPreset != null) + { + // Found preset, use mods to hydrate reward item + var presetAndMods = _itemHelper.ReplaceIDs(defaultPreset.Items); + var newRootId = _itemHelper.RemapRootItemId(presetAndMods, _hashUtil.Generate()); + questReward.Items = presetAndMods; + + // Find root item and set its stack count + var rootItem = questReward.Items.FirstOrDefault(i => i.Id == newRootId); + + // Remap target id to the new presets root id + questReward.Target = rootItem.Id; + + // Copy over stack count otherwise reward shows as missing in client + _itemHelper.AddUpdObjectToItem(rootItem); + + rootItem.Upd.StackObjectsCount = originalRewardRootItem.Upd.StackObjectsCount; + + return; + } + + _logger.Warning($"Unable to find default preset for armor {originalRewardRootItem.Template}, adding mods manually"); + var itemDbData = _itemHelper.GetItem(originalRewardRootItem.Template).Value; + + // Hydrate reward with only 'required' mods - necessary for things like helmets otherwise you end up with nvgs/visors etc + questReward.Items = _itemHelper.AddChildSlotItems(questReward.Items, itemDbData, null, true); + } } From b53fb999ab4c39101617bf1fa261d2fa2d2d0b4d Mon Sep 17 00:00:00 2001 From: Chomp Date: Mon, 13 Jan 2025 18:23:43 +0000 Subject: [PATCH 3/3] Improved stubbiness of `GenerateBot` --- Core/Generators/BotGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Core/Generators/BotGenerator.cs b/Core/Generators/BotGenerator.cs index 8481cdb2..53f5b3fe 100644 --- a/Core/Generators/BotGenerator.cs +++ b/Core/Generators/BotGenerator.cs @@ -112,7 +112,7 @@ public class BotGenerator { _logger.Error("NOT IMPLEMENTED BotGenerator.GenerateBot"); - return new BotBase(); + return bot; } ///