diff --git a/Libraries/Core/Callbacks/InventoryCallbacks.cs b/Libraries/Core/Callbacks/InventoryCallbacks.cs
index 74fb7022..6b70468f 100644
--- a/Libraries/Core/Callbacks/InventoryCallbacks.cs
+++ b/Libraries/Core/Callbacks/InventoryCallbacks.cs
@@ -23,9 +23,8 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse MoveItem(PmcData pmcData, InventoryMoveRequestData info, string sessionID, ItemEventRouterResponse output)
{
- _inventoryController.MoveItem(pmcData, info, sessionID, output);
-
- return output;
+ _inventoryController.MoveItem(pmcData, info, sessionID, output);
+ return output;
}
///
@@ -38,10 +37,8 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse RemoveItem(PmcData pmcData, InventoryRemoveRequestData info, string sessionID, ItemEventRouterResponse output)
{
- // _inventoryController.RemoveItem(pmcData, info, sessionID, output);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ _inventoryController.RemoveItem(pmcData, info, sessionID, output);
+ return output;
}
///
@@ -54,10 +51,8 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse SplitItem(PmcData pmcData, InventorySplitRequestData info, string sessionID, ItemEventRouterResponse output)
{
- // _inventoryController.SplitItem(pmcData, info, sessionID, output);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ _inventoryController.SplitItem(pmcData, info, sessionID, output);
+ return output;
}
///
@@ -70,10 +65,8 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse MergeItem(PmcData pmcData, InventoryMergeRequestData info, string sessionID, ItemEventRouterResponse output)
{
- // _inventoryController.MergeItem(pmcData, info, sessionID, output);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ _inventoryController.MergeItem(pmcData, info, sessionID, output);
+ return output;
}
///
@@ -86,10 +79,8 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse TransferItem(PmcData pmcData, InventoryTransferRequestData info, string sessionID, ItemEventRouterResponse output)
{
- // _inventoryController.TransferItem(pmcData, info, sessionID, output);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ _inventoryController.TransferItem(pmcData, info, sessionID, output);
+ return output;
}
///
@@ -101,10 +92,7 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse SwapItem(PmcData pmcData, InventorySwapRequestData info, string sessionID)
{
- // _inventoryController.SwapItem(pmcData, info, sessionID, output);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ return _inventoryController.SwapItem(pmcData, info, sessionID);
}
///
@@ -116,10 +104,7 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse FoldItem(PmcData pmcData, InventoryFoldRequestData info, string sessionID)
{
- // _inventoryController.FoldItem(pmcData, info, sessionID, output);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ return _inventoryController.FoldItem(pmcData, info, sessionID);
}
///
@@ -131,10 +116,7 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse ToggleItem(PmcData pmcData, InventoryToggleRequestData info, string sessionID)
{
- // _inventoryController.ToggleItem(pmcData, info, sessionID, output);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ return _inventoryController.ToggleItem(pmcData, info, sessionID);
}
///
@@ -146,10 +128,7 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse TagItem(PmcData pmcData, InventoryTagRequestData info, string sessionID)
{
- // _inventoryController.TagItem(pmcData, info, sessionID, output);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ return _inventoryController.TagItem(pmcData, info, sessionID);
}
///
@@ -162,10 +141,8 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse BindItem(PmcData pmcData, InventoryBindRequestData info, string sessionID, ItemEventRouterResponse output)
{
- // _inventoryController.BindItem(pmcData, info, sessionID, output);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ _inventoryController.BindItem(pmcData, info, sessionID, output);
+ return output;
}
///
@@ -178,10 +155,8 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse UnBindItem(PmcData pmcData, InventoryBindRequestData info, string sessionID, ItemEventRouterResponse output)
{
- // _inventoryController.UnBindItem(pmcData, info, sessionID, output);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ _inventoryController.UnBindItem(pmcData, info, sessionID, output);
+ return output;
}
///
@@ -194,10 +169,8 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse ExamineItem(PmcData pmcData, InventoryExamineRequestData info, string sessionID, ItemEventRouterResponse output)
{
- // _inventoryController.ExamineItem(pmcData, info, sessionID, output);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ _inventoryController.ExamineItem(pmcData, info, sessionID, output);
+ return output;
}
///
@@ -209,10 +182,7 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse ReadEncyclopedia(PmcData pmcData, InventoryReadEncyclopediaRequestData info, string sessionID)
{
- // _inventoryController.ReadEncyclopedia(pmcData, info, sessionID, output);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ return _inventoryController.ReadEncyclopedia(pmcData, info, sessionID);
}
///
@@ -225,10 +195,8 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse SortInventory(PmcData pmcData, InventorySortRequestData info, string sessionID, ItemEventRouterResponse output)
{
- // _inventoryController.SortInventory(pmcData, info, sessionID, output);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ _inventoryController.SortInventory(pmcData, info, sessionID, output);
+ return output;
}
///
@@ -241,10 +209,8 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse CreateMapMarker(PmcData pmcData, InventoryCreateMarkerRequestData info, string sessionID, ItemEventRouterResponse output)
{
- // _inventoryController.CreateMapMarker(pmcData, info, sessionID, output);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ _inventoryController.CreateMapMarker(pmcData, info, sessionID, output);
+ return output;
}
///
@@ -257,10 +223,8 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse DeleteMapMarker(PmcData pmcData, InventoryDeleteMarkerRequestData info, string sessionID, ItemEventRouterResponse output)
{
- // _inventoryController.DeleteMapMarker(pmcData, info, sessionID, output);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ _inventoryController.DeleteMapMarker(pmcData, info, sessionID, output);
+ return output;
}
///
@@ -273,10 +237,8 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse EditMapMarker(PmcData pmcData, InventoryEditMarkerRequestData info, string sessionID, ItemEventRouterResponse output)
{
- // _inventoryController.EditMapMarker(pmcData, info, sessionID, output);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ _inventoryController.EditMapMarker(pmcData, info, sessionID, output);
+ return output;
}
///
@@ -290,10 +252,8 @@ public class InventoryCallbacks(
public ItemEventRouterResponse OpenRandomLootContainer(PmcData pmcData, OpenRandomLootContainerRequestData info, string sessionID,
ItemEventRouterResponse output)
{
- // _inventoryController.OpenRandomLootContainer(pmcData, info, sessionID, output);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ _inventoryController.OpenRandomLootContainer(pmcData, info, sessionID, output);
+ return output;
}
///
@@ -306,10 +266,8 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse RedeemProfileReward(PmcData pmcData, RedeemProfileRequestData info, string sessionID, ItemEventRouterResponse output)
{
- // _inventoryController.RedeemProfileReward(pmcData, info, sessionID);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ _inventoryController.RedeemProfileReward(pmcData, info, sessionID);
+ return output;
}
///
@@ -322,10 +280,8 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse SetFavoriteItem(PmcData pmcData, SetFavoriteItems info, string sessionID, ItemEventRouterResponse output)
{
- // _inventoryController.SetFavoriteItem(pmcData, info, sessionID);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ _inventoryController.SetFavoriteItem(pmcData, info, sessionID);
+ return output;
}
///
@@ -339,10 +295,8 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse FailQuest(PmcData pmcData, FailQuestRequestData info, string sessionID, ItemEventRouterResponse output)
{
- // _inventoryController.FailQuest(pmcData, info, sessionID, output);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ _questController.FailQuest(pmcData, info, sessionID, output);
+ return output;
}
///
@@ -355,9 +309,7 @@ public class InventoryCallbacks(
///
public ItemEventRouterResponse PinOrLock(PmcData pmcData, PinOrLockItemRequest info, string sessionID, ItemEventRouterResponse output)
{
- // _inventoryController.PinOrLock(pmcData, info, sessionID, output);
- // TODO: InventoryController is not implemented rn
- // return output;
- throw new NotImplementedException();
+ _inventoryController.PinOrLock(pmcData, info, sessionID, output);
+ return output;
}
}
diff --git a/Libraries/Core/Controllers/BotController.cs b/Libraries/Core/Controllers/BotController.cs
index 89b21fb4..54c80d34 100644
--- a/Libraries/Core/Controllers/BotController.cs
+++ b/Libraries/Core/Controllers/BotController.cs
@@ -258,7 +258,7 @@ public class BotController(
requestedBot?.Role,
raidSettings.Location
);
- if (convertIntoPmcChanceMinMax is not null && botGenerationDetails.IsPmc is not null && !botGenerationDetails.IsPmc.Value)
+ if (convertIntoPmcChanceMinMax is not null && !botGenerationDetails.IsPmc.GetValueOrDefault(false))
{
// Bot has % chance to become pmc and isnt one pmc already
var convertToPmc = _botHelper.RollChanceToBePmc(convertIntoPmcChanceMinMax);
diff --git a/Libraries/Core/Controllers/InventoryController.cs b/Libraries/Core/Controllers/InventoryController.cs
index 3006d8a4..9fa52cbe 100644
--- a/Libraries/Core/Controllers/InventoryController.cs
+++ b/Libraries/Core/Controllers/InventoryController.cs
@@ -4,6 +4,7 @@ using Core.Helpers;
using Core.Models.Eft.Common;
using Core.Models.Eft.Inventory;
using Core.Models.Eft.ItemEvent;
+using Core.Models.Eft.Quests;
using Core.Models.Enums;
using Core.Models.Utils;
using Core.Routers;
@@ -93,4 +94,104 @@ public class InventoryController(
(BackendErrorCodes)228
);
}
+
+ public void PinOrLock(PmcData pmcData, PinOrLockItemRequest info, string sessionId, ItemEventRouterResponse output)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SetFavoriteItem(PmcData pmcData, SetFavoriteItems info, string sessionId)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void RedeemProfileReward(PmcData pmcData, RedeemProfileRequestData info, string sessionId)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void OpenRandomLootContainer(PmcData pmcData, OpenRandomLootContainerRequestData info, string sessionId, ItemEventRouterResponse output)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void EditMapMarker(PmcData pmcData, InventoryEditMarkerRequestData info, string sessionId, ItemEventRouterResponse output)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void DeleteMapMarker(PmcData pmcData, InventoryDeleteMarkerRequestData info, string sessionId, ItemEventRouterResponse output)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void CreateMapMarker(PmcData pmcData, InventoryCreateMarkerRequestData info, string sessionId, ItemEventRouterResponse output)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SortInventory(PmcData pmcData, InventorySortRequestData info, string sessionId, ItemEventRouterResponse output)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ItemEventRouterResponse ReadEncyclopedia(PmcData pmcData, InventoryReadEncyclopediaRequestData info, string sessionId)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void ExamineItem(PmcData pmcData, InventoryExamineRequestData info, string sessionId, ItemEventRouterResponse output)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void UnBindItem(PmcData pmcData, InventoryBindRequestData info, string sessionId, ItemEventRouterResponse output)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void BindItem(PmcData pmcData, InventoryBindRequestData info, string sessionId, ItemEventRouterResponse output)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ItemEventRouterResponse TagItem(PmcData pmcData, InventoryTagRequestData info, string sessionId)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ItemEventRouterResponse ToggleItem(PmcData pmcData, InventoryToggleRequestData info, string sessionId)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ItemEventRouterResponse FoldItem(PmcData pmcData, InventoryFoldRequestData info, string sessionId)
+ {
+ throw new NotImplementedException();
+ }
+
+ public ItemEventRouterResponse SwapItem(PmcData pmcData, InventorySwapRequestData info, string sessionId)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void TransferItem(PmcData pmcData, InventoryTransferRequestData info, string sessionId, ItemEventRouterResponse output)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void MergeItem(PmcData pmcData, InventoryMergeRequestData info, string sessionId, ItemEventRouterResponse output)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void SplitItem(PmcData pmcData, InventorySplitRequestData info, string sessionId, ItemEventRouterResponse output)
+ {
+ throw new NotImplementedException();
+ }
+
+ public void RemoveItem(PmcData pmcData, InventoryRemoveRequestData info, string sessionId, ItemEventRouterResponse output)
+ {
+ throw new NotImplementedException();
+ }
}
diff --git a/Libraries/Core/Controllers/QuestController.cs b/Libraries/Core/Controllers/QuestController.cs
index 940419f2..02bb4e1a 100644
--- a/Libraries/Core/Controllers/QuestController.cs
+++ b/Libraries/Core/Controllers/QuestController.cs
@@ -1,11 +1,19 @@
+using System.Runtime.InteropServices.JavaScript;
using SptCommon.Annotations;
using Core.Helpers;
using Core.Models.Eft.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.ItemEvent;
using Core.Models.Eft.Quests;
+using Core.Models.Enums;
+using Core.Models.Spt.Config;
using Core.Models.Utils;
+using Core.Routers;
+using Core.Servers;
+using Core.Services;
using Core.Utils;
+using Core.Utils.Cloners;
+using Product = Core.Models.Eft.ItemEvent.Product;
namespace Core.Controllers;
@@ -15,32 +23,382 @@ public class QuestController(
ISptLogger _logger,
TimeUtil _timeUtil,
HttpResponseUtil _httpResponseUtil,
+ EventOutputHolder _eventOutputHolder,
+ DatabaseService _databaseService,
+ ItemHelper _itemHelper,
+ DialogueHelper _dialogueHelper,
+ MailSendService _mailSendService,
+ ProfileHelper _profileHelper,
+ TraderHelper _traderHelper,
QuestHelper _questHelper,
- QuestRewardHelper _questRewardHelper
+ QuestRewardHelper _questRewardHelper,
+ QuestConditionHelper _questConditionHelper,
+ PlayerService _playerService,
+ LocaleService _localeService,
+ LocalisationService _localisationService,
+ ConfigServer _configServer,
+ ICloner _cloner
)
{
- public ItemEventRouterResponse CompleteQuest(PmcData pmcData, CompleteQuestRequestData info, string sessionId)
- {
- throw new NotImplementedException();
- }
-
- public ItemEventRouterResponse AcceptRepeatableQuest(PmcData pmcData, AcceptQuestRequestData info, string sessionId)
- {
- throw new NotImplementedException();
- }
-
- public ItemEventRouterResponse AcceptQuest(PmcData pmcData, AcceptQuestRequestData info, string sessionId)
- {
- throw new NotImplementedException();
- }
-
- public ItemEventRouterResponse HandoverQuest(PmcData pmcData, HandoverQuestRequestData info, string sessionId)
- {
- throw new NotImplementedException();
- }
+ protected QuestConfig _questConfig = _configServer.GetConfig();
+ protected List _questTypes = ["PickUp", "Exploration", "Elimination"];
public List GetClientQuest(string sessionId)
{
return _questHelper.GetClientQuests(sessionId);
}
+
+ public ItemEventRouterResponse AcceptQuest(PmcData pmcData, AcceptQuestRequestData acceptedQuest, string sessionID)
+ {
+ var acceptQuestResponse = _eventOutputHolder.GetOutput(sessionID);
+
+ // Does quest exist in profile
+ // Restarting a failed quest can mean quest exists in profile
+ var existingQuestStatus = pmcData.Quests.FirstOrDefault((x) => x.QId == acceptedQuest.QuestId);
+ if (existingQuestStatus is not null)
+ {
+ // Update existing
+ _questHelper.ResetQuestState(pmcData, QuestStatusEnum.Started, acceptedQuest.QuestId);
+
+ // Need to send client an empty list of completedConditions (Unsure if this does anything)
+ acceptQuestResponse.ProfileChanges[sessionID].QuestsStatus.Add(existingQuestStatus);
+ }
+ else
+ {
+ // Add new quest to server profile
+ var newQuest = _questHelper.GetQuestReadyForProfile(pmcData, QuestStatusEnum.Started, acceptedQuest);
+ pmcData.Quests.Add(newQuest);
+ }
+
+ // Create a dialog message for starting the quest.
+ // Note that for starting quests, the correct locale field is "description", not "startedMessageText".
+ var questFromDb = _questHelper.GetQuestFromDb(acceptedQuest.QuestId, pmcData);
+
+ AddTaskConditionCountersToProfile(questFromDb.Conditions.AvailableForFinish, pmcData, acceptedQuest.QuestId);
+
+ // Get messageId of text to send to player as text message in game
+ var messageId = _questHelper.GetMessageIdForQuestStart(
+ questFromDb.StartedMessageText,
+ questFromDb.Description
+ );
+
+ // Apply non-item rewards to profile + return item rewards
+ var startedQuestRewardItems = _questRewardHelper.ApplyQuestReward(
+ pmcData,
+ acceptedQuest.QuestId,
+ QuestStatusEnum.Started,
+ sessionID,
+ acceptQuestResponse
+ );
+
+ // Send started text + any starting reward items found above to player
+ _mailSendService.SendLocalisedNpcMessageToPlayer(
+ sessionID,
+ _traderHelper.GetTraderById(questFromDb.TraderId).ToString(),
+ MessageType.QUEST_START,
+ messageId,
+ startedQuestRewardItems.ToList(),
+ _timeUtil.GetHoursAsSeconds((int)_questHelper.GetMailItemRedeemTimeHoursForProfile(pmcData))
+ );
+
+ // Having accepted new quest, look for newly unlocked quests and inform client of them
+ var newlyAccessibleQuests = _questHelper.GetNewlyAccessibleQuestsWhenStartingQuest(
+ acceptedQuest.QuestId,
+ sessionID
+ );
+ if (newlyAccessibleQuests.Count > 0)
+ {
+ acceptQuestResponse.ProfileChanges[sessionID].Quests.AddRange(newlyAccessibleQuests);
+ }
+
+ return acceptQuestResponse;
+ }
+
+ private void AddTaskConditionCountersToProfile(List? questConditions, PmcData pmcData, string questId)
+ {
+ foreach (var condition in questConditions)
+ {
+ if (pmcData.TaskConditionCounters.TryGetValue(condition.Id, out var counter))
+ {
+ _logger.Error(
+ $"Unable to add new task condition counter: {condition.ConditionType} for qeust: {questId} to profile: {pmcData.SessionId} as it already exists:"
+ );
+ }
+
+ switch (condition.ConditionType)
+ {
+ case "SellItemToTrader":
+ pmcData.TaskConditionCounters[condition.Id] = new TaskConditionCounter
+ {
+ Id = condition.Id,
+ SourceId = questId,
+ Type = condition.ConditionType,
+ Value = 0,
+ };
+ break;
+ }
+ }
+ }
+
+ public ItemEventRouterResponse AcceptRepeatableQuest(PmcData pmcData, AcceptQuestRequestData acceptedQuest, string sessionID)
+ {
+ // Create and store quest status object inside player profile
+ var newRepeatableQuest = _questHelper.GetQuestReadyForProfile(
+ pmcData,
+ QuestStatusEnum.Started,
+ acceptedQuest
+ );
+ pmcData.Quests.Add(newRepeatableQuest);
+
+ // Look for the generated quest cache in profile.RepeatableQuests
+ var repeatableQuestProfile = GetRepeatableQuestFromProfile(pmcData, acceptedQuest);
+ if (repeatableQuestProfile is null)
+ {
+ _logger.Error(
+ _localisationService.GetText(
+ "repeatable-accepted_repeatable_quest_not_found_in_active_quests",
+ acceptedQuest.QuestId
+ )
+ );
+
+ throw new Exception(_localisationService.GetText("repeatable-unable_to_accept_quest_see_log"));
+ }
+
+ // Some scav quests need to be added to scav profile for them to show up in-raid
+ if (repeatableQuestProfile.Side == "Scav" && _questTypes.Contains(repeatableQuestProfile.Type.ToString()))
+ {
+ var fullProfile = _profileHelper.GetFullProfile(sessionID);
+ if (fullProfile.CharacterData.ScavData.Quests is null)
+ {
+ fullProfile.CharacterData.ScavData.Quests = [];
+ }
+
+ fullProfile.CharacterData.ScavData.Quests.Add(newRepeatableQuest);
+ }
+
+ var response = _eventOutputHolder.GetOutput(sessionID);
+
+ return response;
+ }
+
+ private RepeatableQuest GetRepeatableQuestFromProfile(PmcData pmcData, AcceptQuestRequestData acceptedQuest)
+ {
+ foreach (var repeatableQuest in pmcData.RepeatableQuests)
+ {
+ var matchingQuest = repeatableQuest.ActiveQuests.FirstOrDefault(x => x.Id == acceptedQuest.QuestId);
+ if (matchingQuest is not null)
+ {
+ _logger.Debug($"Accepted repeatable quest {acceptedQuest.QuestId} from {repeatableQuest.Name}");
+ matchingQuest.SptRepatableGroupName = repeatableQuest.Name;
+
+ return matchingQuest;
+ }
+ }
+
+ return null;
+ }
+
+ public ItemEventRouterResponse CompleteQuest(PmcData pmcData, CompleteQuestRequestData info, string sessionId)
+ {
+ return _questHelper.CompleteQuest(pmcData, info, sessionId);
+ }
+
+
+ public ItemEventRouterResponse HandoverQuest(PmcData pmcData, HandoverQuestRequestData handoverQuestRequest, string sessionID)
+ {
+ var quest = _questHelper.GetQuestFromDb(handoverQuestRequest.QuestId, pmcData);
+ List handoverQuestTypes = ["HandoverItem", "WeaponAssembly"];
+ var output = _eventOutputHolder.GetOutput(sessionID);
+
+ var isItemHandoverQuest = true;
+ var handedInCount = 0;
+
+ // Decrement number of items handed in
+ QuestCondition? handoverRequirements = null;
+ foreach (var condition in quest.Conditions.AvailableForFinish)
+ {
+ if (condition.Id == handoverQuestRequest.ConditionId && handoverQuestTypes.Contains(condition.ConditionType))
+ {
+ handedInCount = int.Parse((string)condition.Value);
+ isItemHandoverQuest = condition.ConditionType == handoverQuestTypes.FirstOrDefault();
+ handoverRequirements = condition;
+
+ if (pmcData.TaskConditionCounters.TryGetValue("ConditionId", out var counter))
+ {
+ handedInCount -= (int)(counter.Value ?? 0);
+
+ if (handedInCount <= 0)
+ {
+ _logger.Error(
+ _localisationService.GetText(
+ "repeatable-quest_handover_failed_condition_already_satisfied",
+ new
+ {
+ questId = handoverQuestRequest.QuestId,
+ conditionId = handoverQuestRequest.ConditionId,
+ profileCounter = counter.Value,
+ value = handedInCount
+ }
+ )
+ );
+
+ return output;
+ }
+
+ break;
+ }
+ }
+ }
+
+ if (isItemHandoverQuest && handedInCount == 0)
+ {
+ return ShowRepeatableQuestInvalidConditionError(handoverQuestRequest, output);
+ }
+
+ var totalItemCountToRemove = 0;
+ foreach (var itemHandover in handoverQuestRequest.Items)
+ {
+ var matchingItemInProfile = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == itemHandover.Id);
+ if (!(matchingItemInProfile is not null && handoverRequirements.Target.List.Contains(matchingItemInProfile.Template)))
+ {
+ // Item handed in by player doesnt match what was requested
+ return ShowQuestItemHandoverMatchError(
+ handoverQuestRequest,
+ matchingItemInProfile,
+ handoverRequirements,
+ output
+ );
+ }
+
+ // Remove the right quantity of given items
+ var itemCountToRemove = Math.Min(itemHandover.Count ?? 0, handedInCount - totalItemCountToRemove);
+ totalItemCountToRemove += itemCountToRemove;
+ if (itemHandover.Count - itemCountToRemove > 0)
+ {
+ // Remove single item with no children
+ _questHelper.ChangeItemStack(
+ pmcData,
+ itemHandover.Id,
+ itemHandover.Count - itemCountToRemove ?? 0,
+ sessionID,
+ output
+ );
+ if (totalItemCountToRemove == handedInCount)
+ {
+ break;
+ }
+ }
+ else
+ {
+ // Remove item with children
+ var toRemove = _itemHelper.FindAndReturnChildrenByItems(pmcData.Inventory.Items, itemHandover.Id);
+ var index = pmcData.Inventory.Items.Count;
+
+ // Important: don't tell the client to remove the attachments, it will handle it
+ output.ProfileChanges[sessionID]
+ .Items.DeletedItems.Add(
+ new Product
+ {
+ Id = itemHandover.Id
+ }
+ );
+
+ // Important: loop backward when removing items from the array we're looping on
+ while (index-- > 0)
+ {
+ if (toRemove.Contains(pmcData.Inventory.Items[index].Id))
+ {
+ var removedItem = _cloner.Clone(pmcData.Inventory.Items[index]);
+ pmcData.Inventory.Items.RemoveAt(index);
+ // Remove the item
+
+ // If the removed item has a numeric `location` property, re-calculate all the child
+ // element `location` properties of the parent so they are sequential, while retaining order
+ if (removedItem.Location.GetType() == typeof(int))
+ {
+ var childItems = _itemHelper.FindAndReturnChildrenAsItems(
+ pmcData.Inventory.Items,
+ removedItem.ParentId
+ );
+ childItems.RemoveAt(0); // Remove the parent
+
+ // Sort by the current `location` and update
+ childItems.Sort((a, b) => (((int)a.Location) > ((int)b.Location) ? 1 : -1));
+
+ for (int i = 0; i < childItems.Count; i++)
+ {
+ childItems[i].Location = i;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ UpdateProfileTaskConditionCounterValue(
+ pmcData,
+ handoverQuestRequest.ConditionId,
+ handoverQuestRequest.QuestId,
+ totalItemCountToRemove
+ );
+
+ return output;
+ }
+
+ private ItemEventRouterResponse ShowRepeatableQuestInvalidConditionError(HandoverQuestRequestData handoverQuestRequest, ItemEventRouterResponse output)
+ {
+ var errorMessage = _localisationService.GetText(
+ "repeatable-quest_handover_failed_condition_invalid",
+ new
+ {
+ questId = handoverQuestRequest.QuestId,
+ conditionId = handoverQuestRequest.ConditionId
+ }
+ );
+ _logger.Error(errorMessage);
+
+ return _httpResponseUtil.AppendErrorToOutput(output, errorMessage);
+ }
+
+ private ItemEventRouterResponse ShowQuestItemHandoverMatchError(HandoverQuestRequestData handoverQuestRequest, Item? itemHandedOver,
+ QuestCondition? handoverRequirements, ItemEventRouterResponse output)
+ {
+ var errorMessage = _localisationService.GetText(
+ "quest-handover_wrong_item",
+ new
+ {
+ questId = handoverQuestRequest.QuestId,
+ handedInTpl = itemHandedOver?.Template ?? "UNKNOWN",
+ requiredTpl = handoverRequirements.Target.List.FirstOrDefault(),
+ }
+ );
+ _logger.Error(errorMessage);
+
+ return _httpResponseUtil.AppendErrorToOutput(output, errorMessage);
+ }
+
+ private void UpdateProfileTaskConditionCounterValue(PmcData pmcData, string conditionId, string questId, int counterValue)
+ {
+ if (pmcData.TaskConditionCounters[conditionId] != null)
+ {
+ pmcData.TaskConditionCounters[conditionId].Value += counterValue;
+
+ return;
+ }
+
+ pmcData.TaskConditionCounters[conditionId] = new TaskConditionCounter
+ {
+ Id = conditionId,
+ SourceId = questId,
+ Type = "HandoverItem",
+ Value = counterValue,
+ };
+ }
+
+ public ItemEventRouterResponse FailQuest(PmcData pmcData, FailQuestRequestData request, string sessionID, ItemEventRouterResponse output)
+ {
+ _questHelper.FailQuest(pmcData, request, sessionID, output);
+
+ return output;
+ }
}
diff --git a/Libraries/Core/Generators/BotEquipmentModGenerator.cs b/Libraries/Core/Generators/BotEquipmentModGenerator.cs
index 1405f2fe..d70472d0 100644
--- a/Libraries/Core/Generators/BotEquipmentModGenerator.cs
+++ b/Libraries/Core/Generators/BotEquipmentModGenerator.cs
@@ -244,11 +244,11 @@ public class BotEquipmentModGenerator(
* @param modSlot front/back
* @returns Armor IItem
*/
- protected Item GetDefaultPresetArmorSlot(string armorItemTpl, string modSlot)
+ protected Item? GetDefaultPresetArmorSlot(string armorItemTpl, string modSlot)
{
var defaultPreset = _presetHelper.GetDefaultPreset(armorItemTpl);
- return defaultPreset?.Items.FirstOrDefault((item) => item.SlotId?.ToLower() == modSlot);
+ return defaultPreset?.Items?.FirstOrDefault((item) => item.SlotId?.ToLower() == modSlot);
}
@@ -260,11 +260,6 @@ public class BotEquipmentModGenerator(
/// Weapon + mods array
public List- GenerateModsForWeapon(string sessionId, GenerateWeaponRequest request)
{
- var pmcProfile = _profileHelper.GetPmcProfile(sessionId);
-
- // Get pool of mods that fit weapon
- var compatibleModsPool = request.ModPool[request.ParentTemplate.Id];
-
if (
!(
request.ParentTemplate.Properties.Slots.Any() ||
@@ -288,7 +283,12 @@ public class BotEquipmentModGenerator(
return request.Weapon;
}
- var botEquipConfig = _botConfig.Equipment[request.BotData.EquipmentRole];
+ var pmcProfile = _profileHelper.GetPmcProfile(sessionId);
+
+ // Get pool of mods that fit weapon
+ request.ModPool.TryGetValue(request.ParentTemplate.Id, out var compatibleModsPool);
+
+ _botConfig.Equipment.TryGetValue(request.BotData.EquipmentRole, out var botEquipConfig);
var botEquipBlacklist = _botEquipmentFilterService.GetBotEquipmentBlacklist(
request.BotData.EquipmentRole,
pmcProfile.Info.Level ?? 0
@@ -494,13 +494,26 @@ public class BotEquipmentModGenerator(
if (isRandomisableSlot && !containsModInPool && modToAddTemplate.Value.Properties.Slots.Any())
{
var modFromService = _botEquipmentModPoolService.GetModsForWeaponSlot(modToAddTemplate.Value.Id);
- if (modFromService.Keys.Any())
+ if (modFromService?.Keys.Count > 0)
{
request.ModPool[modToAddTemplate.Value.Id] = modFromService;
containsModInPool = true;
}
}
+ // Fallback when mods with REQUIRED children are not in the pool, add them and process
+ if (!containsModInPool && !isRandomisableSlot)
+ {
+ // Check for required mods the item we've added needs to be classified as 'valid'
+ var modFromService = _botEquipmentModPoolService.GetRequiredModsForWeaponSlot(modToAddTemplate.Value.Id);
+ if (modFromService?.Keys.Count > 0)
+ {
+ request.ModPool[modToAddTemplate.Value.Id] = modFromService;
+ containsModInPool = true;
+ }
+
+ }
+
if (containsModInPool)
{
GenerateWeaponRequest recursiveRequestData = new()
@@ -871,16 +884,16 @@ public class BotEquipmentModGenerator(
request.Weapon,
request.ModSlot
);
- if (chosenModResult.SlotBlocked ?? false && !(parentSlot.Required ?? false))
+ if (chosenModResult.SlotBlocked.GetValueOrDefault(false) && !parentSlot.Required.GetValueOrDefault(false))
{
// Don't bother trying to fit mod, slot is completely blocked
return null;
}
// Log if mod chosen was incompatible
- if (chosenModResult.Incompatible ?? false && !(parentSlot.Required ?? false))
+ if (chosenModResult.Incompatible.GetValueOrDefault(false) && !(parentSlot.Required.GetValueOrDefault(false)))
{
- _logger.Debug(chosenModResult.Reason);
+ _logger.Debug($"Unable to find compatible mod of type: {parentSlot.Name}, in slot: {request.ModSlot} reason: {chosenModResult.Reason}");
}
// Get random mod to attach from items db for required slots if none found above
@@ -891,14 +904,14 @@ public class BotEquipmentModGenerator(
}
// Compatible item not found + not required
- if (!(chosenModResult.Found ?? false) && parentSlot != null && (!parentSlot.Required ?? false))
+ if (!(chosenModResult.Found.GetValueOrDefault(false)) && parentSlot is not null && (!parentSlot.Required.GetValueOrDefault(false)))
{
return null;
}
- if (!(chosenModResult.Found ?? false) && parentSlot != null)
+ if (!(chosenModResult.Found ?? false) && parentSlot is not null)
{
- if (parentSlot.Required ?? false)
+ if (parentSlot.Required.GetValueOrDefault(false))
{
_logger.Warning(
$"Required slot unable to be filled, {request.ModSlot} on {request.ParentTemplate.Name} {request.ParentTemplate.Id} for weapon: {request.Weapon[0].Template}"
@@ -966,7 +979,7 @@ public class BotEquipmentModGenerator(
// Filter mod pool to only items that appear in parents allowed list
preFilteredModPool = preFilteredModPool.Where((tpl) => parentSlot.Props.Filters[0].Filter.Contains(tpl)).ToList();
- if (preFilteredModPool.Count() == 0)
+ if (preFilteredModPool.Count == 0)
{
return new() { Incompatible = true, Found = false, Reason = "No mods found in parents allowed list" };
}
@@ -995,9 +1008,9 @@ public class BotEquipmentModGenerator(
};
// Limit how many attempts to find a compatible mod can occur before giving up
- var maxBlockedAttempts = Math.Round(modPool.Count() * 0.75); // 75% of pool size
+ var maxBlockedAttempts = Math.Round(modPool.Count * 0.75); // 75% of pool size
var blockedAttemptCount = 0;
- string chosenTpl = null;
+ string chosenTpl;
while (exhaustableModPool.HasValues())
{
chosenTpl = exhaustableModPool.GetRandomValue();
@@ -1015,7 +1028,7 @@ public class BotEquipmentModGenerator(
}
// Success - Default wanted + only 1 item in pool
- if (modSpawnType == ModSpawn.DEFAULT_MOD && modPool.Count() == 1)
+ if (modSpawnType == ModSpawn.DEFAULT_MOD && modPool.Count == 1)
{
chosenModResult.Found = true;
chosenModResult.Incompatible = false;
@@ -1032,15 +1045,20 @@ public class BotEquipmentModGenerator(
if (existingItemBlockingChoice is not null)
{
// Give max of x attempts of picking a mod if blocked by another
- if (blockedAttemptCount > maxBlockedAttempts)
+ // OR Blocked and modpool only had 1 item
+ if (blockedAttemptCount > maxBlockedAttempts || modPool.Count == 1)
{
blockedAttemptCount = 0; // reset
+ //chosenModResult.SlotBlocked = true; // Later in code we try to find replacement, but only when "slotBlocked" is not true
+ chosenModResult.Reason = "Blocked";
+
break;
}
blockedAttemptCount++;
// Not compatible - Try again
+ ;
continue;
}
@@ -1073,7 +1091,7 @@ public class BotEquipmentModGenerator(
///
/// Tpls that are incompatible and should not be used
/// string array of compatible mod tpls with weapon
- public List GetFilteredModPool(HashSet modPool, List tplBlacklist)
+ public List GetFilteredModPool(HashSet modPool, HashSet tplBlacklist)
{
return modPool.Where((tpl) => !tplBlacklist.Contains(tpl)).ToList();
}
diff --git a/Libraries/Core/Generators/BotInventoryGenerator.cs b/Libraries/Core/Generators/BotInventoryGenerator.cs
index 3b2a4914..a885043c 100644
--- a/Libraries/Core/Generators/BotInventoryGenerator.cs
+++ b/Libraries/Core/Generators/BotInventoryGenerator.cs
@@ -470,7 +470,7 @@ public class BotInventoryGenerator(
// Edge case: Filter the armor items mod pool if bot exists in config dict + config has armor slot
if (_botConfig.Equipment.ContainsKey(settings.BotData.EquipmentRole) &&
- settings.RandomisationDetails is not null &&
+ settings.RandomisationDetails?.RandomisedArmorSlots != null &&
settings.RandomisationDetails.RandomisedArmorSlots.Contains(settings.RootEquipmentSlot.ToString()))
{
// Filter out mods from relevant blacklist
diff --git a/Libraries/Core/Generators/BotWeaponGenerator.cs b/Libraries/Core/Generators/BotWeaponGenerator.cs
index 6799090c..7a625bda 100644
--- a/Libraries/Core/Generators/BotWeaponGenerator.cs
+++ b/Libraries/Core/Generators/BotWeaponGenerator.cs
@@ -176,7 +176,7 @@ public class BotWeaponGenerator(
);
}
- // Use weapon preset from globals.json if weapon isnt valid
+ // Use weapon preset from globals.json if weapon isn't valid
if (!IsWeaponValid(weaponWithModsArray, botRole))
{
// Weapon is bad, fall back to weapons preset
diff --git a/Libraries/Core/Generators/WeaponGen/Implementations/ExternalInventoryMagGen.cs b/Libraries/Core/Generators/WeaponGen/Implementations/ExternalInventoryMagGen.cs
index 021bd94b..6222d1b8 100644
--- a/Libraries/Core/Generators/WeaponGen/Implementations/ExternalInventoryMagGen.cs
+++ b/Libraries/Core/Generators/WeaponGen/Implementations/ExternalInventoryMagGen.cs
@@ -95,6 +95,12 @@ public class ExternalInventoryMagGen(
break;
}
+ if (defaultMagazineTpl == BaseClasses.MAGAZINE)
+ {
+ // Magazine base type, do not use
+ break;
+ }
+
// Set chosen magazine tpl to the weapons default magazine tpl and try to fit into inventory next loop
magazineTpl = defaultMagazineTpl;
magTemplate = _itemHelper.GetItem(magazineTpl).Value;
diff --git a/Libraries/Core/Helpers/BotHelper.cs b/Libraries/Core/Helpers/BotHelper.cs
index 3de56ee1..64bfc73c 100644
--- a/Libraries/Core/Helpers/BotHelper.cs
+++ b/Libraries/Core/Helpers/BotHelper.cs
@@ -29,7 +29,7 @@ public class BotHelper(
/// BotType object
public BotType GetBotTemplate(string role)
{
- if (!_databaseService.GetBots().Types.TryGetValue(role.ToLower(), out var bot))
+ if (!_databaseService.GetBots().Types.TryGetValue(role?.ToLower(), out var bot))
{
_logger.Error($"Unable to get bot of type: {role} from DB");
@@ -51,7 +51,7 @@ public class BotHelper(
public bool IsBotBoss(string botRole)
{
- return _botConfig.Bosses.Any(x => x.ToLower() == botRole.ToLower());
+ return _botConfig.Bosses.Any(x => string.Equals(x, botRole, StringComparison.CurrentCultureIgnoreCase));
}
public bool IsBotFollower(string botRole)
@@ -59,6 +59,11 @@ public class BotHelper(
return botRole?.ToLower().StartsWith("follower") ?? false;
}
+ public bool IsBotZombie(string botRole)
+ {
+ return botRole?.ToLower().StartsWith("infected") ?? false;
+ }
+
///
/// Add a bot to the FRIENDLY_BOT_TYPES list
///
diff --git a/Libraries/Core/Helpers/DurabilityLimitsHelper.cs b/Libraries/Core/Helpers/DurabilityLimitsHelper.cs
index a5b0996b..b81934cc 100644
--- a/Libraries/Core/Helpers/DurabilityLimitsHelper.cs
+++ b/Libraries/Core/Helpers/DurabilityLimitsHelper.cs
@@ -102,6 +102,11 @@ public class DurabilityLimitsHelper(
return "follower";
}
+ if (_botHelper.IsBotZombie(botRole))
+ {
+ return "zombie";
+ }
+
var roleExistsInConfig = _botConfig.Durability.BotDurabilities.ContainsKey(botRole);
if (!roleExistsInConfig)
{
@@ -122,26 +127,9 @@ public class DurabilityLimitsHelper(
/// Current armor durability
public double GetRandomizedArmorDurability(TemplateItem? itemTemplate, string? botRole, double maxDurability)
{
- if (botRole is not null)
- {
- if (_botHelper.IsBotPmc(botRole))
- {
- return GenerateArmorDurability("pmc", maxDurability);
- }
+ var durabilityRole = GetDurabilityRole(botRole);
- if (_botHelper.IsBotBoss(botRole))
- {
- return GenerateArmorDurability("boss", maxDurability);
- }
-
- if (_botHelper.IsBotFollower(botRole))
- {
- return GenerateArmorDurability("follower", maxDurability);
- }
- }
-
-
- return GenerateArmorDurability(botRole, maxDurability);
+ return GenerateArmorDurability(durabilityRole, maxDurability);
}
protected double GenerateMaxWeaponDurability(string? botRole = null)
diff --git a/Libraries/Core/Helpers/InventoryHelper.cs b/Libraries/Core/Helpers/InventoryHelper.cs
index 20b51589..f69c7c1f 100644
--- a/Libraries/Core/Helpers/InventoryHelper.cs
+++ b/Libraries/Core/Helpers/InventoryHelper.cs
@@ -277,17 +277,16 @@ public class InventoryHelper(
protected List GetSizeByInventoryItemHash(string itemTpl, string itemID, InventoryItemHash inventoryItemHash)
{
var toDo = new List { itemID };
- var result = _itemHelper.GetItem(itemTpl);
- var tmpItem = result.Value;
+ var (key, tmpItem) = _itemHelper.GetItem(itemTpl);
// Invalid item
- if (!result.Key)
+ if (!key)
{
_logger.Error(_localisationService.GetText("inventory-invalid_item_missing_from_db", itemTpl));
}
// Item found but no _props property
- if (tmpItem is not null && tmpItem.Properties is null)
+ if (key && tmpItem.Properties is null)
{
_localisationService.GetText("inventory-item_missing_props_property", new {
itemTpl = itemTpl,
@@ -296,7 +295,7 @@ public class InventoryHelper(
}
// No item object or getItem() returned false
- if (tmpItem is null && result.Value is null)
+ if (!key && tmpItem is null)
{
// return default size of 1x1
_logger.Error(_localisationService.GetText("inventory-return_default_size", itemTpl));
@@ -305,7 +304,7 @@ public class InventoryHelper(
}
var rootItem = inventoryItemHash.ByItemId[itemID];
- var foldableWeapon = tmpItem.Properties.Foldable;
+ var isFoldable = tmpItem.Properties.Foldable;
var foldedSlot = tmpItem.Properties.FoldedSlot;
var sizeUp = 0;
@@ -323,10 +322,10 @@ public class InventoryHelper(
// Item types to ignore
var skipThisItems = new List { BaseClasses.BACKPACK, BaseClasses.SEARCHABLE_ITEM, BaseClasses.SIMPLE_CONTAINER };
- var rootFolded = rootItem?.Upd?.Foldable?.Folded == true;
+ var rootIsFolded = rootItem?.Upd?.Foldable?.Folded == true;
// The item itself is collapsible
- if (foldableWeapon is not null && string.IsNullOrEmpty(foldedSlot) && rootFolded)
+ if (isFoldable is not null && string.IsNullOrEmpty(foldedSlot) && rootIsFolded)
{
outX -= tmpItem.Properties.SizeReduceRight.Value;
}
@@ -358,12 +357,12 @@ public class InventoryHelper(
var childFoldable = itm.Properties.Foldable.GetValueOrDefault(false);
var childFolded = item.Upd?.Foldable is not null && item.Upd.Foldable.Folded == true;
- if (foldableWeapon is true && foldedSlot == item.SlotId && (rootFolded || childFolded))
+ if (isFoldable is true && foldedSlot == item.SlotId && (rootIsFolded || childFolded))
{
continue;
}
- if (childFoldable && rootFolded && childFolded)
+ if (childFoldable && rootIsFolded && childFolded)
{
continue;
}
diff --git a/Libraries/Core/Helpers/QuestHelper.cs b/Libraries/Core/Helpers/QuestHelper.cs
index edce4612..0e1c5ee0 100644
--- a/Libraries/Core/Helpers/QuestHelper.cs
+++ b/Libraries/Core/Helpers/QuestHelper.cs
@@ -204,7 +204,7 @@ public class QuestHelper(
*/
public QuestStatus GetQuestReadyForProfile(
PmcData pmcData,
- QuestStatus newState,
+ QuestStatusEnum newState,
AcceptQuestRequestData acceptedQuest
)
{
diff --git a/Libraries/Core/Models/Eft/Common/Tables/Quest.cs b/Libraries/Core/Models/Eft/Common/Tables/Quest.cs
index f6e6c837..9255b136 100644
--- a/Libraries/Core/Models/Eft/Common/Tables/Quest.cs
+++ b/Libraries/Core/Models/Eft/Common/Tables/Quest.cs
@@ -47,8 +47,8 @@ public record Quest
[JsonPropertyName("image")]
public string? Image { get; set; }
- [JsonPropertyName("type")]
- public string? RewardType { get; set; }
+ [JsonPropertyName("type")] // can be string or QuestTypeEnum
+ public string? Type { get; set; }
[JsonPropertyName("isKey")]
public bool? IsKey { get; set; }
diff --git a/Libraries/Core/Models/Spt/Bots/GenerateWeaponRequest.cs b/Libraries/Core/Models/Spt/Bots/GenerateWeaponRequest.cs
index e1ec77d4..a1fef553 100644
--- a/Libraries/Core/Models/Spt/Bots/GenerateWeaponRequest.cs
+++ b/Libraries/Core/Models/Spt/Bots/GenerateWeaponRequest.cs
@@ -43,7 +43,7 @@ public record GenerateWeaponRequest
/** Array of item tpls the weapon does not support */
[JsonPropertyName("conflictingItemTpls")]
- public List? ConflictingItemTpls { get; set; }
+ public HashSet? ConflictingItemTpls { get; set; }
}
public record BotData
diff --git a/Libraries/Core/Models/Spt/Bots/ModToSpawnRequest.cs b/Libraries/Core/Models/Spt/Bots/ModToSpawnRequest.cs
index a5a8edad..96d0d652 100644
--- a/Libraries/Core/Models/Spt/Bots/ModToSpawnRequest.cs
+++ b/Libraries/Core/Models/Spt/Bots/ModToSpawnRequest.cs
@@ -74,7 +74,7 @@ public record ModToSpawnRequest
/// List of item tpls the weapon does not support
///
[JsonPropertyName("conflictingItemTpls")]
- public List? ConflictingItemTpls { get; set; }
+ public HashSet? ConflictingItemTpls { get; set; }
[JsonPropertyName("botData")]
public BotData? BotData { get; set; }
diff --git a/Libraries/Core/Servers/Http/SptHttpListener.cs b/Libraries/Core/Servers/Http/SptHttpListener.cs
index 7d0237e6..9d41fba5 100644
--- a/Libraries/Core/Servers/Http/SptHttpListener.cs
+++ b/Libraries/Core/Servers/Http/SptHttpListener.cs
@@ -185,11 +185,11 @@ public class SptHttpListener : IHttpListener
public void SendJson(HttpResponse resp, string? output, string sessionID)
{
- if (!string.IsNullOrEmpty(output))
- resp.Body = new MemoryStream(Encoding.UTF8.GetBytes(output));
resp.StatusCode = 200;
resp.ContentType = "application/json";
resp.Headers.Append("Set-Cookie", $"PHPSESSID={sessionID}");
+ if (!string.IsNullOrEmpty(output))
+ resp.Body.WriteAsync(Encoding.UTF8.GetBytes(output)).AsTask().Wait();
resp.StartAsync().Wait();
resp.CompleteAsync().Wait();
}
diff --git a/Libraries/Core/Services/BotEquipmentFilterService.cs b/Libraries/Core/Services/BotEquipmentFilterService.cs
index c8b108b2..8e075ee4 100644
--- a/Libraries/Core/Services/BotEquipmentFilterService.cs
+++ b/Libraries/Core/Services/BotEquipmentFilterService.cs
@@ -54,7 +54,7 @@ public class BotEquipmentFilterService
pmcProfile.Info.Level ?? 0
);
- var botEquipConfig = _botEquipmentConfig[botRole];
+ var botEquipConfig = _botEquipmentConfig[botRole.ToLower()];
var randomisationDetails = _botHelper.GetBotRandomizationDetails(botLevel, botEquipConfig);
if (botEquipmentBlacklist is not null || botEquipmentWhitelist is not null)
diff --git a/Libraries/Core/Services/BotEquipmentModPoolService.cs b/Libraries/Core/Services/BotEquipmentModPoolService.cs
index f2ba4968..5473178d 100644
--- a/Libraries/Core/Services/BotEquipmentModPoolService.cs
+++ b/Libraries/Core/Services/BotEquipmentModPoolService.cs
@@ -170,6 +170,31 @@ public class BotEquipmentModPoolService
return _weaponModPool[itemTpl];
}
+ public Dictionary>? GetRequiredModsForWeaponSlot(string itemTpl)
+ {
+ var result = new Dictionary>();
+
+ // Get item from db
+ var itemDb = _itemHelper.GetItem(itemTpl).Value;
+ if (itemDb.Properties.Slots is not null)
+ {
+ // Loop over slots flagged as 'required'
+ foreach (var slot in itemDb.Properties.Slots.Where(slot => slot.Required.GetValueOrDefault(false)))
+ {
+ // Create dict entry for mod slot
+ result.Add(slot.Name, []);
+
+ // Add compatible tpls to dicts hashset
+ foreach (var compatibleItemTpl in slot.Props.Filters.FirstOrDefault().Filter)
+ {
+ result[slot.Name].Add(compatibleItemTpl);
+ }
+ }
+ }
+
+ return result;
+ }
+
/**
* Create weapon mod pool and set generated flag to true
*/
diff --git a/Libraries/Core/Utils/Json/Converters/BaseInteractionRequestDataConverter.cs b/Libraries/Core/Utils/Json/Converters/BaseInteractionRequestDataConverter.cs
index bd749049..041f903b 100644
--- a/Libraries/Core/Utils/Json/Converters/BaseInteractionRequestDataConverter.cs
+++ b/Libraries/Core/Utils/Json/Converters/BaseInteractionRequestDataConverter.cs
@@ -22,141 +22,141 @@ public class BaseInteractionRequestDataConverter : JsonConverter(jsonText, options);
- return ConvertToCorrectType(value, jsonText, options);
+ var value = JsonSerializer.Deserialize(jsonText);
+ return ConvertToCorrectType(value, jsonText);
}
- private BaseInteractionRequestData? ConvertToCorrectType(BaseInteractionRequestData? value, string jsonText, JsonSerializerOptions options)
+ private BaseInteractionRequestData? ConvertToCorrectType(BaseInteractionRequestData? value, string jsonText)
{
switch (value.Action)
{
case "CustomizationBuy":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "CustomizationSet":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "Eat":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "Heal":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "RestoreHealth":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case HideoutEventActions.HIDEOUT_UPGRADE:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case HideoutEventActions.HIDEOUT_UPGRADE_COMPLETE:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case HideoutEventActions.HIDEOUT_PUT_ITEMS_IN_AREA_SLOTS:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case HideoutEventActions.HIDEOUT_TAKE_ITEMS_FROM_AREA_SLOTS:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case HideoutEventActions.HIDEOUT_TOGGLE_AREA:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case HideoutEventActions.HIDEOUT_SINGLE_PRODUCTION_START:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case HideoutEventActions.HIDEOUT_SCAV_CASE_PRODUCTION_START:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case HideoutEventActions.HIDEOUT_CONTINUOUS_PRODUCTION_START:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case HideoutEventActions.HIDEOUT_TAKE_PRODUCTION:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case HideoutEventActions.HIDEOUT_RECORD_SHOOTING_RANGE_POINTS:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case HideoutEventActions.HIDEOUT_IMPROVE_AREA:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case HideoutEventActions.HIDEOUT_CANCEL_PRODUCTION_COMMAND:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case HideoutEventActions.HIDEOUT_CIRCLE_OF_CULTIST_PRODUCTION_START:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case HideoutEventActions.HIDEOUT_DELETE_PRODUCTION_COMMAND:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case HideoutEventActions.HIDEOUT_CUSTOMIZATION_APPLY_COMMAND:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case HideoutEventActions.HIDEOUT_CUSTOMIZATION_SET_MANNEQUIN_POSE:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "Insure":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
/////////////////////////////////////////// InventoryBaseActionRequestData
case "AddToWishList":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "RemoveFromWishList":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "ChangeWishlistItemCategory":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "TradingConfirm":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "RagFairBuyOffer":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "SellAllFromSavage":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "Repair":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "TraderRepair":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "RagFairAddOffer":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "RagFairRemoveOffer":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "RagFairRenewOffer":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "QuestAccept":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "QuestComplete":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "QuestHandover":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "RepeatableQuestChange":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case "AddNote":
case "EditNote":
case "DeleteNote":
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.MOVE:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.REMOVE:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.SPLIT:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.MERGE:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.TRANSFER:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.SWAP:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.FOLD:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.TOGGLE:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.TAG:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.BIND:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.UNBIND:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.EXAMINE:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.READ_ENCYCLOPEDIA:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.APPLY_INVENTORY_CHANGES:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.CREATE_MAP_MARKER:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.DELETE_MAP_MARKER:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.EDIT_MAP_MARKER:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.OPEN_RANDOM_LOOT_CONTAINER:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.HIDEOUT_QTE_EVENT:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.REDEEM_PROFILE_REWARD:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.SET_FAVORITE_ITEMS:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.QUEST_FAIL:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
case ItemEventActions.PIN_LOCK:
- return JsonSerializer.Deserialize(jsonText, options);
+ return JsonSerializer.Deserialize(jsonText);
default:
throw new Exception($"Unhandled action type {value.Action}, make sure the BaseInteractionRequestDataConverter has the deserialization for this action handled.");
}
diff --git a/Server/Assets/configs/bot.json b/Server/Assets/configs/bot.json
index 603e9c67..4d4e4ea9 100644
--- a/Server/Assets/configs/bot.json
+++ b/Server/Assets/configs/bot.json
@@ -288,7 +288,63 @@
"minDelta": 0,
"minLimitPercent": 15
}
- }
+ },
+ "zombie": {
+ "armor": {
+ "maxDelta": 10,
+ "minDelta": 0,
+ "minLimitPercent": 15
+ },
+ "weapon": {
+ "lowestMax": 90,
+ "highestMax": 100,
+ "maxDelta": 10,
+ "minDelta": 0,
+ "minLimitPercent": 15
+ }
+ },
+ "shooterbtr": {
+ "armor": {
+ "maxDelta": 0,
+ "minDelta": 0,
+ "minLimitPercent": 0
+ },
+ "weapon": {
+ "lowestMax": 100,
+ "highestMax": 100,
+ "maxDelta": 0,
+ "minDelta": 0,
+ "minLimitPercent": 0
+ }
+ },
+ "skier": {
+ "armor": {
+ "maxDelta": 10,
+ "minDelta": 0,
+ "minLimitPercent": 15
+ },
+ "weapon": {
+ "lowestMax": 60,
+ "highestMax": 100,
+ "maxDelta": 10,
+ "minDelta": 0,
+ "minLimitPercent": 15
+ }
+ },
+ "peacemaker": {
+ "armor": {
+ "maxDelta": 10,
+ "minDelta": 0,
+ "minLimitPercent": 15
+ },
+ "weapon": {
+ "lowestMax": 60,
+ "highestMax": 100,
+ "maxDelta": 10,
+ "minDelta": 0,
+ "minLimitPercent": 15
+ }
+ }
}
},
"lootItemResourceRandomization": {
@@ -925,6 +981,9 @@
},
"assaultgroup": {},
"gifter": {
+ "nvgIsActiveChanceDayPercent": 0,
+ "nvgIsActiveChanceNightPercent": 90,
+ "faceShieldIsActiveChancePercent": 0,
"lightIsActiveDayChancePercent": 25,
"lightIsActiveNightChancePercent": 75,
"laserIsActiveChancePercent": 75