1527 lines
60 KiB
C#
1527 lines
60 KiB
C#
using System.Collections.Frozen;
|
|
using SPTarkov.DI.Annotations;
|
|
using SPTarkov.Server.Core.Extensions;
|
|
using SPTarkov.Server.Core.Generators;
|
|
using SPTarkov.Server.Core.Helpers;
|
|
using SPTarkov.Server.Core.Models.Common;
|
|
using SPTarkov.Server.Core.Models.Eft.Common;
|
|
using SPTarkov.Server.Core.Models.Eft.Common.Tables;
|
|
using SPTarkov.Server.Core.Models.Eft.Hideout;
|
|
using SPTarkov.Server.Core.Models.Eft.Inventory;
|
|
using SPTarkov.Server.Core.Models.Eft.ItemEvent;
|
|
using SPTarkov.Server.Core.Models.Enums;
|
|
using SPTarkov.Server.Core.Models.Enums.Hideout;
|
|
using SPTarkov.Server.Core.Models.Spt.Config;
|
|
using SPTarkov.Server.Core.Models.Utils;
|
|
using SPTarkov.Server.Core.Routers;
|
|
using SPTarkov.Server.Core.Servers;
|
|
using SPTarkov.Server.Core.Services;
|
|
using SPTarkov.Server.Core.Utils;
|
|
using SPTarkov.Server.Core.Utils.Cloners;
|
|
|
|
namespace SPTarkov.Server.Core.Controllers;
|
|
|
|
[Injectable]
|
|
public class HideoutController(
|
|
ISptLogger<HideoutController> logger,
|
|
TimeUtil timeUtil,
|
|
DatabaseService databaseService,
|
|
InventoryHelper inventoryHelper,
|
|
ItemHelper itemHelper,
|
|
SaveServer saveServer,
|
|
PresetHelper presetHelper,
|
|
PaymentHelper paymentHelper,
|
|
EventOutputHolder eventOutputHolder,
|
|
HttpResponseUtil httpResponseUtil,
|
|
ProfileHelper profileHelper,
|
|
HideoutHelper hideoutHelper,
|
|
ScavCaseRewardGenerator scavCaseRewardGenerator,
|
|
ServerLocalisationService serverLocalisationService,
|
|
ProfileActivityService profileActivityService,
|
|
FenceService fenceService,
|
|
CircleOfCultistService circleOfCultistService,
|
|
ICloner cloner,
|
|
ConfigServer configServer
|
|
)
|
|
{
|
|
public static readonly MongoId NameTaskConditionCountersCraftingId = new("673f5d6fdd6ed700c703afdc");
|
|
|
|
protected readonly FrozenSet<HideoutAreas> _areasWithResources =
|
|
[
|
|
HideoutAreas.AirFilteringUnit,
|
|
HideoutAreas.WaterCollector,
|
|
HideoutAreas.Generator,
|
|
HideoutAreas.BitcoinFarm,
|
|
];
|
|
|
|
protected readonly HideoutConfig _hideoutConfig = configServer.GetConfig<HideoutConfig>();
|
|
|
|
/// <summary>
|
|
/// Handle HideoutUpgrade event
|
|
/// Start a hideout area upgrade
|
|
/// </summary>
|
|
/// <param name="pmcData">Player profile</param>
|
|
/// <param name="request">Start upgrade request</param>
|
|
/// <param name="sessionID">Session/player id</param>
|
|
/// <param name="output">Client response</param>
|
|
public void StartUpgrade(PmcData pmcData, HideoutUpgradeRequestData request, MongoId sessionID, ItemEventRouterResponse output)
|
|
{
|
|
var items = request
|
|
.Items.Select(reqItem =>
|
|
{
|
|
var item = pmcData.Inventory.Items.FirstOrDefault(invItem => invItem.Id == reqItem.Id);
|
|
return new { inventoryItem = item, requestedItem = reqItem };
|
|
})
|
|
.ToList();
|
|
|
|
// If it's not money, its construction / barter items
|
|
foreach (var item in items)
|
|
{
|
|
if (item.inventoryItem is null)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-unable_to_find_item_in_inventory", item.requestedItem.Id));
|
|
httpResponseUtil.AppendErrorToOutput(output);
|
|
|
|
return;
|
|
}
|
|
|
|
if (
|
|
paymentHelper.IsMoneyTpl(item.inventoryItem.Template)
|
|
&& item.inventoryItem.Upd is not null
|
|
&& item.inventoryItem.Upd.StackObjectsCount is not null
|
|
&& item.inventoryItem.Upd.StackObjectsCount > item.requestedItem.Count
|
|
)
|
|
{
|
|
item.inventoryItem.Upd.StackObjectsCount -= item.requestedItem.Count;
|
|
}
|
|
else
|
|
{
|
|
inventoryHelper.RemoveItem(pmcData, item.inventoryItem.Id, sessionID, output);
|
|
}
|
|
}
|
|
|
|
// Construction time management
|
|
var profileHideoutArea = pmcData.Hideout.Areas.FirstOrDefault(area => area.Type == request.AreaType);
|
|
if (profileHideoutArea is null)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-unable_to_find_area", request.AreaType));
|
|
httpResponseUtil.AppendErrorToOutput(output);
|
|
|
|
return;
|
|
}
|
|
|
|
var hideoutDataDb = databaseService.GetTables().Hideout.Areas.FirstOrDefault(area => area.Type == request.AreaType);
|
|
if (hideoutDataDb is null)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-unable_to_find_area_in_database", request.AreaType));
|
|
httpResponseUtil.AppendErrorToOutput(output);
|
|
|
|
return;
|
|
}
|
|
|
|
var ctime = hideoutDataDb.Stages[(profileHideoutArea.Level + 1).ToString()].ConstructionTime;
|
|
if (ctime > 0)
|
|
{
|
|
if (profileHelper.IsDeveloperAccount(sessionID))
|
|
{
|
|
ctime = 40;
|
|
}
|
|
|
|
var timestamp = timeUtil.GetTimeStamp();
|
|
|
|
profileHideoutArea.CompleteTime = (int)Math.Round(timestamp + ctime.Value);
|
|
profileHideoutArea.Constructing = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle HideoutUpgradeComplete event
|
|
/// Complete a hideout area upgrade
|
|
/// </summary>
|
|
/// <param name="pmcData">Player profile</param>
|
|
/// <param name="request">Completed upgrade request</param>
|
|
/// <param name="sessionID">Session/player id</param>
|
|
/// <param name="output">Client response</param>
|
|
public void UpgradeComplete(
|
|
PmcData pmcData,
|
|
HideoutUpgradeCompleteRequestData request,
|
|
MongoId sessionID,
|
|
ItemEventRouterResponse output
|
|
)
|
|
{
|
|
var hideout = databaseService.GetHideout();
|
|
var globals = databaseService.GetGlobals();
|
|
|
|
var profileHideoutArea = pmcData.Hideout.Areas.FirstOrDefault(area => area.Type == request.AreaType);
|
|
if (profileHideoutArea is null)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-unable_to_find_area", request.AreaType));
|
|
httpResponseUtil.AppendErrorToOutput(output);
|
|
|
|
return;
|
|
}
|
|
|
|
// Upgrade profile values
|
|
profileHideoutArea.Level++;
|
|
profileHideoutArea.CompleteTime = 0;
|
|
profileHideoutArea.Constructing = false;
|
|
|
|
var hideoutData = hideout.Areas.FirstOrDefault(area => area.Type == profileHideoutArea.Type);
|
|
if (hideoutData is null)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-unable_to_find_area_in_database", request.AreaType));
|
|
httpResponseUtil.AppendErrorToOutput(output);
|
|
|
|
return;
|
|
}
|
|
|
|
// Apply bonuses
|
|
if (!hideoutData.Stages.TryGetValue(profileHideoutArea.Level.ToString(), out var hideoutStage))
|
|
{
|
|
logger.Error($"Stage level: {profileHideoutArea.Level} not found for area: {request.AreaType}");
|
|
|
|
return;
|
|
}
|
|
var bonuses = hideoutStage.Bonuses;
|
|
if (bonuses?.Count > 0)
|
|
{
|
|
foreach (var bonus in bonuses)
|
|
{
|
|
hideoutHelper.ApplyPlayerUpgradesBonuses(pmcData, bonus);
|
|
}
|
|
}
|
|
|
|
// Upgrade includes a container improvement/addition
|
|
if (!string.IsNullOrEmpty(hideoutStage.Container))
|
|
{
|
|
AddContainerImprovementToProfile(output, sessionID, pmcData, profileHideoutArea, hideoutData, hideoutStage);
|
|
}
|
|
|
|
// Upgrading water collector / med station
|
|
if (profileHideoutArea.Type == HideoutAreas.WaterCollector || profileHideoutArea.Type == HideoutAreas.MedStation)
|
|
{
|
|
SetWallVisibleIfPrereqsMet(pmcData);
|
|
}
|
|
|
|
// Cleanup temporary buffs/debuffs from wall if complete
|
|
if (profileHideoutArea.Type == HideoutAreas.EmergencyWall && profileHideoutArea.Level == 6)
|
|
{
|
|
hideoutHelper.RemoveHideoutWallBuffsAndDebuffs(hideoutData, pmcData);
|
|
}
|
|
|
|
// Add Skill Points Per Area Upgrade
|
|
profileHelper.AddSkillPointsToPlayer(
|
|
pmcData,
|
|
SkillTypes.HideoutManagement,
|
|
globals.Configuration.SkillsSettings.HideoutManagement.SkillPointsPerAreaUpgrade
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Upgrade wall status to visible in profile if medstation/water collector are both level 1
|
|
/// </summary>
|
|
/// <param name="pmcData">Player profile</param>
|
|
protected void SetWallVisibleIfPrereqsMet(PmcData pmcData)
|
|
{
|
|
var medStation = pmcData.Hideout.Areas.FirstOrDefault(area => area.Type == HideoutAreas.MedStation);
|
|
var waterCollector = pmcData.Hideout.Areas.FirstOrDefault(area => area.Type == HideoutAreas.WaterCollector);
|
|
if (medStation?.Level >= 1 && waterCollector?.Level >= 1)
|
|
{
|
|
var wall = pmcData.Hideout.Areas.FirstOrDefault(area => area.Type == HideoutAreas.EmergencyWall);
|
|
if (wall?.Level == 0)
|
|
{
|
|
wall.Level = 3;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a stash upgrade to profile
|
|
/// </summary>
|
|
/// <param name="output">Client response</param>
|
|
/// <param name="sessionId">Session/Player id</param>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="profileParentHideoutArea"></param>
|
|
/// <param name="dbHideoutArea">Area of hideout player is upgrading</param>
|
|
/// <param name="hideoutStage">Stage player is upgrading to</param>
|
|
protected void AddContainerImprovementToProfile(
|
|
ItemEventRouterResponse output,
|
|
MongoId sessionId,
|
|
PmcData pmcData,
|
|
BotHideoutArea profileParentHideoutArea,
|
|
HideoutArea dbHideoutArea,
|
|
Stage hideoutStage
|
|
)
|
|
{
|
|
// Add key/value to `hideoutAreaStashes` dictionary - used to link hideout area to inventory stash by its id
|
|
// Key is the enums value stored as a string, e.g. "27" for cultist circle
|
|
var keyForHideoutAreaStash = ((int)dbHideoutArea.Type).ToString();
|
|
if (!pmcData.Inventory.HideoutAreaStashes.ContainsKey(keyForHideoutAreaStash))
|
|
{
|
|
if (!pmcData.Inventory.HideoutAreaStashes.TryAdd(keyForHideoutAreaStash, dbHideoutArea.Id))
|
|
{
|
|
logger.Error($"Unable to add key: {dbHideoutArea.Type} to HideoutAreaStashes");
|
|
}
|
|
}
|
|
|
|
// Add/upgrade stash item in player inventory
|
|
AddUpdateInventoryItemToProfile(sessionId, pmcData, dbHideoutArea, hideoutStage);
|
|
|
|
// Edge case, add/update `stand1/stand2/stand3` children
|
|
if (dbHideoutArea.Type == HideoutAreas.EquipmentPresetsStand)
|
|
// Can have multiple 'standx' children depending on upgrade level
|
|
{
|
|
AddMissingPresetStandItemsToProfile(sessionId, hideoutStage, pmcData, dbHideoutArea, output);
|
|
}
|
|
|
|
AddContainerUpgradeToClientOutput(sessionId, keyForHideoutAreaStash, dbHideoutArea, hideoutStage, output);
|
|
|
|
// Some hideout areas (Gun stand) have child areas linked to it
|
|
var childDbArea = databaseService.GetHideout().Areas.FirstOrDefault(area => area.ParentArea == dbHideoutArea.Id);
|
|
if (childDbArea is null)
|
|
{
|
|
// No child db area, we're complete
|
|
return;
|
|
}
|
|
|
|
// Add key/value to `hideoutAreaStashes` dictionary - used to link hideout area to inventory stash by its id
|
|
var childAreaTypeKey = ((int)childDbArea.Type).ToString();
|
|
if (!pmcData.Inventory.HideoutAreaStashes.ContainsKey(childAreaTypeKey))
|
|
{
|
|
pmcData.Inventory.HideoutAreaStashes[childAreaTypeKey] = childDbArea.Id;
|
|
}
|
|
|
|
// Set child area level to same as parent area
|
|
pmcData.Hideout.Areas.FirstOrDefault(hideoutArea => hideoutArea.Type == childDbArea.Type).Level = pmcData
|
|
.Hideout.Areas.FirstOrDefault(area => area.Type == profileParentHideoutArea.Type)
|
|
.Level;
|
|
|
|
// Add/upgrade stash item in player inventory
|
|
if (!childDbArea.Stages.TryGetValue(profileParentHideoutArea.Level.ToString(), out var childDbAreaStage))
|
|
{
|
|
logger.Error($"Unable to find stage: {profileParentHideoutArea.Level} of area: {dbHideoutArea.Id}");
|
|
|
|
return;
|
|
}
|
|
|
|
AddUpdateInventoryItemToProfile(sessionId, pmcData, childDbArea, childDbAreaStage);
|
|
|
|
// Inform client of the changes
|
|
AddContainerUpgradeToClientOutput(sessionId, childAreaTypeKey, childDbArea, childDbAreaStage, output);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add an inventory item to profile from a hideout area stage data
|
|
/// </summary>
|
|
/// <param name="sessionId">Session/Player id</param>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="dbHideoutArea">Hideout area from db being upgraded</param>
|
|
/// <param name="hideoutStage">Stage area upgraded to</param>
|
|
protected void AddUpdateInventoryItemToProfile(MongoId sessionId, PmcData pmcData, HideoutArea dbHideoutArea, Stage hideoutStage)
|
|
{
|
|
var existingInventoryItem = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == dbHideoutArea.Id);
|
|
if (existingInventoryItem is not null)
|
|
{
|
|
// Update existing items container tpl to point to new id (tpl)
|
|
existingInventoryItem.Template = hideoutStage.Container.Value;
|
|
|
|
return;
|
|
}
|
|
|
|
// Add new item as none exists (don't inform client of newContainerItem, will be done in `profileChanges.changedHideoutStashes`)
|
|
var newContainerItem = new Item { Id = dbHideoutArea.Id, Template = hideoutStage.Container.Value };
|
|
pmcData.Inventory.Items.Add(newContainerItem);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Include container upgrade in client response
|
|
/// </summary>
|
|
/// <param name="sessionId">Session/Player id</param>
|
|
/// <param name="changedHideoutStashesKey">Key of hideout area that's been upgraded</param>
|
|
/// <param name="hideoutDbData"></param>
|
|
/// <param name="hideoutStage"></param>
|
|
/// <param name="output">Client response</param>
|
|
protected void AddContainerUpgradeToClientOutput(
|
|
MongoId sessionId,
|
|
string changedHideoutStashesKey,
|
|
HideoutArea hideoutDbData,
|
|
Stage hideoutStage,
|
|
ItemEventRouterResponse output
|
|
)
|
|
{
|
|
// Ensure ChangedHideoutStashes isn't null
|
|
output.ProfileChanges[sessionId].ChangedHideoutStashes ??= new Dictionary<string, HideoutStashItem>();
|
|
|
|
// Inform client of changes
|
|
output.ProfileChanges[sessionId].ChangedHideoutStashes[changedHideoutStashesKey] = new HideoutStashItem
|
|
{
|
|
Id = hideoutDbData.Id,
|
|
Template = hideoutStage.Container,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle HideoutPutItemsInAreaSlots
|
|
/// Create item in hideout slot item array, remove item from player inventory
|
|
/// </summary>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="addItemToHideoutRequest">request from client to place item in area slot</param>
|
|
/// <param name="sessionID">Session/Player id</param>
|
|
/// <returns>ItemEventRouterResponse</returns>
|
|
public ItemEventRouterResponse PutItemsInAreaSlots(
|
|
PmcData pmcData,
|
|
HideoutPutItemInRequestData addItemToHideoutRequest,
|
|
MongoId sessionID
|
|
)
|
|
{
|
|
var output = eventOutputHolder.GetOutput(sessionID);
|
|
|
|
var itemsToAdd = addItemToHideoutRequest.Items.Select(kvp =>
|
|
{
|
|
var item = pmcData.Inventory.Items.FirstOrDefault(invItem => invItem.Id == kvp.Value.Id);
|
|
return new
|
|
{
|
|
inventoryItem = item,
|
|
requestedItem = kvp.Value,
|
|
slot = kvp.Key,
|
|
};
|
|
});
|
|
|
|
var hideoutArea = pmcData.Hideout.Areas.FirstOrDefault(area => area.Type == addItemToHideoutRequest.AreaType);
|
|
if (hideoutArea is null)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-unable_to_find_area_in_database", addItemToHideoutRequest.AreaType));
|
|
return httpResponseUtil.AppendErrorToOutput(output);
|
|
}
|
|
|
|
foreach (var item in itemsToAdd)
|
|
{
|
|
if (item.inventoryItem is null)
|
|
{
|
|
logger.Error(
|
|
serverLocalisationService.GetText(
|
|
"hideout-unable_to_find_item_in_inventory",
|
|
new { itemId = item.requestedItem.Id, area = hideoutArea.Type }
|
|
)
|
|
);
|
|
return httpResponseUtil.AppendErrorToOutput(output);
|
|
}
|
|
|
|
// Add item to area.slots
|
|
var destinationLocationIndex = int.Parse(item.slot);
|
|
var hideoutSlotIndex = hideoutArea.Slots.FindIndex(slot => slot.LocationIndex == destinationLocationIndex);
|
|
if (hideoutSlotIndex == -1)
|
|
{
|
|
logger.Error(
|
|
$"Unable to put item: {item.requestedItem.Id} into slot as slot cannot be found for area: {addItemToHideoutRequest.AreaType}, skipping"
|
|
);
|
|
continue;
|
|
}
|
|
|
|
hideoutArea.Slots[hideoutSlotIndex].Items =
|
|
[
|
|
new HideoutItem
|
|
{
|
|
Id = item.inventoryItem.Id,
|
|
Template = item.inventoryItem.Template,
|
|
Upd = item.inventoryItem.Upd,
|
|
},
|
|
];
|
|
|
|
inventoryHelper.RemoveItem(pmcData, item.inventoryItem.Id, sessionID, output);
|
|
}
|
|
|
|
// Trigger a forced update
|
|
hideoutHelper.UpdatePlayerHideout(sessionID);
|
|
|
|
return output;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle HideoutTakeItemsFromAreaSlots event
|
|
/// Remove item from hideout area and place into player inventory
|
|
/// </summary>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="request">Take item out of area request</param>
|
|
/// <param name="sessionID">Session/Player id</param>
|
|
/// <returns>ItemEventRouterResponse</returns>
|
|
public ItemEventRouterResponse TakeItemsFromAreaSlots(PmcData pmcData, HideoutTakeItemOutRequestData request, MongoId sessionID)
|
|
{
|
|
var output = eventOutputHolder.GetOutput(sessionID);
|
|
|
|
var hideoutArea = pmcData.Hideout.Areas.FirstOrDefault(area => area.Type == request.AreaType);
|
|
if (hideoutArea is null)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-unable_to_find_area", request.AreaType));
|
|
return httpResponseUtil.AppendErrorToOutput(output);
|
|
}
|
|
|
|
if (hideoutArea.Slots is null || hideoutArea.Slots.Count == 0)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-unable_to_find_item_to_remove_from_area", hideoutArea.Type));
|
|
return httpResponseUtil.AppendErrorToOutput(output);
|
|
}
|
|
|
|
// Handle areas that have resources that can be placed in/taken out of slots from the area
|
|
if (_areasWithResources.Contains(hideoutArea.Type ?? HideoutAreas.NotSet))
|
|
{
|
|
var response = RemoveResourceFromArea(sessionID, pmcData, request, output, hideoutArea);
|
|
|
|
// Force a refresh of productions/hideout areas with resources
|
|
hideoutHelper.UpdatePlayerHideout(sessionID);
|
|
return response;
|
|
}
|
|
|
|
throw new Exception(serverLocalisationService.GetText("hideout-unhandled_remove_item_from_area_request", hideoutArea.Type));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find resource item in hideout area, add copy to player inventory, remove Item from hideout slot
|
|
/// </summary>
|
|
/// <param name="sessionID">Session/Player id</param>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="removeResourceRequest">client request</param>
|
|
/// <param name="output">Client response</param>
|
|
/// <param name="hideoutArea">Area fuel is being removed from</param>
|
|
/// <returns>ItemEventRouterResponse</returns>
|
|
protected ItemEventRouterResponse RemoveResourceFromArea(
|
|
MongoId sessionID,
|
|
PmcData pmcData,
|
|
HideoutTakeItemOutRequestData removeResourceRequest,
|
|
ItemEventRouterResponse output,
|
|
BotHideoutArea hideoutArea
|
|
)
|
|
{
|
|
var slotIndexToRemove = removeResourceRequest.Slots?.FirstOrDefault();
|
|
if (slotIndexToRemove is null)
|
|
{
|
|
logger.Error(
|
|
$"Unable to remove resource from area: {removeResourceRequest.AreaType} slot as no slots found in request, RESTART CLIENT IMMEDIATELY"
|
|
);
|
|
|
|
return output;
|
|
}
|
|
|
|
// Assume only one item in slot
|
|
var itemToReturn = hideoutArea.Slots?.FirstOrDefault(slot => slot.LocationIndex == slotIndexToRemove)?.Items?.FirstOrDefault();
|
|
if (itemToReturn is null)
|
|
{
|
|
logger.Error(
|
|
$"Unable to remove resource from area: {removeResourceRequest.AreaType} slot as no item found, RESTART CLIENT IMMEDIATELY"
|
|
);
|
|
|
|
return output;
|
|
}
|
|
|
|
// Add the item found in hideout area slot to player stash
|
|
var request = new AddItemDirectRequest
|
|
{
|
|
ItemWithModsToAdd = [itemToReturn.ConvertToItem()],
|
|
FoundInRaid = itemToReturn.Upd?.SpawnedInSession,
|
|
Callback = null,
|
|
UseSortingTable = false,
|
|
};
|
|
|
|
inventoryHelper.AddItemToStash(sessionID, request, pmcData, output);
|
|
if (output.Warnings?.Count > 0)
|
|
// Adding to stash failed, drop out - don't remove item from hideout area slot
|
|
{
|
|
return output;
|
|
}
|
|
|
|
// Remove items from slot, keep locationIndex object
|
|
var hideoutSlotIndex = hideoutArea.Slots.FindIndex(slot => slot.LocationIndex == slotIndexToRemove);
|
|
hideoutArea.Slots[hideoutSlotIndex].Items = null;
|
|
|
|
return output;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle HideoutToggleArea event
|
|
/// Toggle area on/off
|
|
/// </summary>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="request">Toggle area request</param>
|
|
/// <param name="sessionID">Session/Player id</param>
|
|
/// <returns>ItemEventRouterResponse</returns>
|
|
public ItemEventRouterResponse ToggleArea(PmcData pmcData, HideoutToggleAreaRequestData request, MongoId sessionID)
|
|
{
|
|
var output = eventOutputHolder.GetOutput(sessionID);
|
|
|
|
// Force a production update (occur before area is toggled as it could be generator and doing it after generator enabled would cause incorrect calculaton of production progress)
|
|
hideoutHelper.UpdatePlayerHideout(sessionID);
|
|
|
|
var hideoutArea = pmcData.Hideout.Areas.FirstOrDefault(area => area.Type == request.AreaType);
|
|
if (hideoutArea is null)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-unable_to_find_area", request.AreaType));
|
|
return httpResponseUtil.AppendErrorToOutput(output);
|
|
}
|
|
|
|
hideoutArea.Active = request.Enabled;
|
|
|
|
return output;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle HideoutSingleProductionStart event
|
|
/// </summary>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="request"></param>
|
|
/// <param name="sessionID">Session/Player id</param>
|
|
/// <returns>ItemEventRouterResponse</returns>
|
|
public ItemEventRouterResponse SingleProductionStart(
|
|
PmcData pmcData,
|
|
HideoutSingleProductionStartRequestData request,
|
|
MongoId sessionID
|
|
)
|
|
{
|
|
// Start production
|
|
hideoutHelper.RegisterProduction(pmcData, request, sessionID);
|
|
|
|
// Find the recipe of the production
|
|
var recipe = databaseService.GetHideout().Production.Recipes.FirstOrDefault(production => production.Id == request.RecipeId);
|
|
|
|
// Find the actual amount of items we need to remove because body can send weird data
|
|
var recipeRequirementsClone = cloner.Clone(recipe.Requirements.Where(r => r.Type == "Item" || r.Type == "Tool"));
|
|
|
|
List<IdWithCount> itemsToDelete = [];
|
|
var output = eventOutputHolder.GetOutput(sessionID);
|
|
itemsToDelete.AddRange(request.Tools);
|
|
itemsToDelete.AddRange(request.Items);
|
|
|
|
foreach (var itemToDelete in itemsToDelete)
|
|
{
|
|
var itemToCheck = pmcData.Inventory.Items.FirstOrDefault(i => i.Id == itemToDelete.Id);
|
|
var requirement = recipeRequirementsClone.FirstOrDefault(requirement => requirement.TemplateId == itemToCheck.Template);
|
|
|
|
// Handle tools not having a `count`, but always only requiring 1
|
|
var requiredCount = requirement.Count ?? 1;
|
|
if (requiredCount <= 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
inventoryHelper.RemoveItemByCount(pmcData, itemToDelete.Id, requiredCount, sessionID, output);
|
|
|
|
// Tools don't have a count
|
|
if (requirement.Type != "Tool")
|
|
{
|
|
requirement.Count -= (int)itemToDelete.Count;
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle HideoutScavCaseProductionStart event
|
|
/// Handles event after clicking 'start' on the scav case hideout page
|
|
/// </summary>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="request"></param>
|
|
/// <param name="sessionID">Session/Player id</param>
|
|
/// <returns>ItemEventRouterResponse</returns>
|
|
public ItemEventRouterResponse ScavCaseProductionStart(PmcData pmcData, HideoutScavCaseStartRequestData request, MongoId sessionID)
|
|
{
|
|
var output = eventOutputHolder.GetOutput(sessionID);
|
|
|
|
foreach (var requestedItem in request.Items)
|
|
{
|
|
var inventoryItem = pmcData.Inventory.Items.FirstOrDefault(item => item.Id == requestedItem.Id);
|
|
if (inventoryItem is null)
|
|
{
|
|
logger.Error(
|
|
serverLocalisationService.GetText(
|
|
"hideout-unable_to_find_scavcase_requested_item_in_profile_inventory",
|
|
requestedItem.Id
|
|
)
|
|
);
|
|
return httpResponseUtil.AppendErrorToOutput(output);
|
|
}
|
|
|
|
if (inventoryItem.Upd?.StackObjectsCount is not null && inventoryItem.Upd.StackObjectsCount > requestedItem.Count)
|
|
{
|
|
inventoryItem.Upd.StackObjectsCount -= requestedItem.Count;
|
|
}
|
|
else
|
|
{
|
|
inventoryHelper.RemoveItem(pmcData, requestedItem.Id, sessionID, output);
|
|
}
|
|
}
|
|
|
|
var recipe = databaseService.GetHideout().Production?.ScavRecipes?.FirstOrDefault(r => r.Id == request.RecipeId);
|
|
if (recipe is null)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-unable_to_find_scav_case_recipie_in_database", request.RecipeId));
|
|
|
|
return httpResponseUtil.AppendErrorToOutput(output);
|
|
}
|
|
|
|
// @Important: Here we need to be very exact:
|
|
// - normal recipe: Production time value is stored in attribute "productionTime" with small "p"
|
|
// - scav case recipe: Production time value is stored in attribute "ProductionTime" with capital "P"
|
|
var adjustedCraftTime =
|
|
recipe.ProductionTime
|
|
- hideoutHelper.GetSkillProductionTimeReduction(
|
|
pmcData,
|
|
recipe.ProductionTime ?? 0,
|
|
SkillTypes.Crafting,
|
|
databaseService.GetGlobals().Configuration.SkillsSettings.Crafting.CraftTimeReductionPerLevel
|
|
);
|
|
|
|
var modifiedScavCaseTime = GetScavCaseTime(pmcData, adjustedCraftTime);
|
|
|
|
pmcData.Hideout.Production[request.RecipeId] = hideoutHelper.InitProduction(
|
|
request.RecipeId,
|
|
(int)(profileHelper.IsDeveloperAccount(sessionID) ? 40 : modifiedScavCaseTime),
|
|
false
|
|
);
|
|
pmcData.Hideout.Production[request.RecipeId].SptIsScavCase = true;
|
|
|
|
return output;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adjust scav case time based on fence standing
|
|
/// </summary>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="productionTime">Time to complete scav case in seconds</param>
|
|
/// <returns>Adjusted scav case time in seconds</returns>
|
|
protected double? GetScavCaseTime(PmcData pmcData, double? productionTime)
|
|
{
|
|
var fenceLevel = fenceService.GetFenceInfo(pmcData);
|
|
if (fenceLevel is null)
|
|
{
|
|
return productionTime;
|
|
}
|
|
|
|
return productionTime * fenceLevel.ScavCaseTimeModifier;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Start production of continuously created item
|
|
/// </summary>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="request">Continuous production request</param>
|
|
/// <param name="sessionID">Session/Player id</param>
|
|
/// <returns>ItemEventRouterResponse</returns>
|
|
public ItemEventRouterResponse ContinuousProductionStart(
|
|
PmcData pmcData,
|
|
HideoutContinuousProductionStartRequestData request,
|
|
MongoId sessionID
|
|
)
|
|
{
|
|
hideoutHelper.RegisterProduction(pmcData, request, sessionID);
|
|
|
|
return eventOutputHolder.GetOutput(sessionID);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle HideoutTakeProduction event
|
|
/// Take completed item out of hideout area and place into player inventory
|
|
/// </summary>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="request">Remove production from area request</param>
|
|
/// <param name="sessionID">Session/Player id</param>
|
|
/// <returns></returns>
|
|
public ItemEventRouterResponse TakeProduction(PmcData pmcData, HideoutTakeProductionRequestData request, MongoId sessionID)
|
|
{
|
|
var output = eventOutputHolder.GetOutput(sessionID);
|
|
var hideoutDb = databaseService.GetHideout();
|
|
|
|
if (request.RecipeId == HideoutHelper.BitcoinProductionId)
|
|
{
|
|
// Ensure server and client are in-sync when player presses 'get items' on farm
|
|
hideoutHelper.UpdatePlayerHideout(sessionID);
|
|
hideoutHelper.GetBTC(pmcData, request, sessionID, output);
|
|
|
|
return output;
|
|
}
|
|
|
|
var recipe = hideoutDb.Production.Recipes.FirstOrDefault(r => r.Id == request.RecipeId);
|
|
if (recipe is not null)
|
|
{
|
|
HandleRecipe(sessionID, recipe, pmcData, request, output);
|
|
|
|
return output;
|
|
}
|
|
|
|
var scavCase = hideoutDb.Production.ScavRecipes.FirstOrDefault(r => r.Id == request.RecipeId);
|
|
if (scavCase is not null)
|
|
{
|
|
HandleScavCase(sessionID, pmcData, request, output);
|
|
|
|
return output;
|
|
}
|
|
|
|
logger.Error(serverLocalisationService.GetText("hideout-unable_to_find_production_in_profile_by_recipie_id", request.RecipeId));
|
|
|
|
return httpResponseUtil.AppendErrorToOutput(output);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Take recipe-type production out of hideout area and place into player inventory
|
|
/// </summary>
|
|
/// <param name="sessionID">Session/Player id</param>
|
|
/// <param name="recipe">Completed recipe of item</param>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="request">Remove production from area request</param>
|
|
/// <param name="output">Client response</param>
|
|
protected void HandleRecipe(
|
|
MongoId sessionID,
|
|
HideoutProduction recipe,
|
|
PmcData pmcData,
|
|
HideoutTakeProductionRequestData request,
|
|
ItemEventRouterResponse output
|
|
)
|
|
{
|
|
// Validate that we have a matching production
|
|
var productionDict = pmcData.Hideout.Production;
|
|
MongoId? prodId = null;
|
|
foreach (var (productionId, production) in productionDict)
|
|
{
|
|
// Skip undefined production objects
|
|
if (production is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (production.RecipeId != request.RecipeId)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Production or ScavCase
|
|
prodId = productionId; // Set to objects key
|
|
break;
|
|
}
|
|
|
|
// If we're unable to find the production, send an error to the client
|
|
if (prodId is null)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-unable_to_find_production_in_profile_by_recipie_id", request.RecipeId));
|
|
|
|
httpResponseUtil.AppendErrorToOutput(
|
|
output,
|
|
serverLocalisationService.GetText("hideout-unable_to_find_production_in_profile_by_recipie_id", request.RecipeId)
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
// Variables for management of skill
|
|
var craftingExpAmount = 0;
|
|
|
|
var counterHoursCrafting = GetCustomSptHoursCraftingTaskConditionCounter(pmcData, recipe);
|
|
var totalCraftingHours = counterHoursCrafting.Value;
|
|
|
|
// Array of arrays of item + children
|
|
List<List<Item>> itemAndChildrenToSendToPlayer = [];
|
|
|
|
// Reward is weapon/armor preset, handle differently compared to 'normal' items
|
|
var rewardIsPreset = presetHelper.HasPreset(recipe.EndProduct);
|
|
if (rewardIsPreset)
|
|
{
|
|
itemAndChildrenToSendToPlayer = HandlePresetReward(recipe);
|
|
}
|
|
|
|
HandleStackableState(recipe, itemAndChildrenToSendToPlayer, rewardIsPreset);
|
|
|
|
// Recipe has an `isEncoded` requirement for reward(s), Add `RecodableComponent` property
|
|
if (recipe.IsEncoded ?? false)
|
|
{
|
|
foreach (var reward in itemAndChildrenToSendToPlayer)
|
|
{
|
|
itemHelper.AddUpdObjectToItem(reward.FirstOrDefault());
|
|
|
|
reward.FirstOrDefault().Upd.RecodableComponent = new UpdRecodableComponent { IsEncoded = true };
|
|
}
|
|
}
|
|
|
|
// Build an array of the tools that need to be returned to the player
|
|
List<List<Item>> toolsToSendToPlayer = [];
|
|
pmcData.Hideout.Production.TryGetValue(prodId.Value, out Production hideoutProduction);
|
|
if (hideoutProduction.SptRequiredTools?.Count > 0)
|
|
{
|
|
foreach (var tool in hideoutProduction.SptRequiredTools)
|
|
{
|
|
toolsToSendToPlayer.Add([tool]);
|
|
}
|
|
}
|
|
|
|
// Check if the recipe is the same as the last one - get bonus when crafting same thing multiple times
|
|
var area = pmcData.Hideout.Areas.FirstOrDefault(area => area.Type == recipe.AreaType);
|
|
if (area is not null && request.RecipeId != area.LastRecipe)
|
|
// 1 point per craft upon the end of production for alternating between 2 different crafting recipes in the same module
|
|
{
|
|
craftingExpAmount += _hideoutConfig.ExpCraftAmount; // Default is 10
|
|
}
|
|
|
|
// Update variable with time spent crafting item(s)
|
|
// 1 point per 8 hours of crafting
|
|
totalCraftingHours += recipe.ProductionTime;
|
|
if (totalCraftingHours / _hideoutConfig.HoursForSkillCrafting >= 1)
|
|
{
|
|
// Spent enough time crafting to get a bonus xp multiplier
|
|
var multiplierCrafting = Math.Floor(totalCraftingHours.Value / _hideoutConfig.HoursForSkillCrafting);
|
|
craftingExpAmount += (int)(1 * multiplierCrafting);
|
|
totalCraftingHours -= _hideoutConfig.HoursForSkillCrafting * multiplierCrafting;
|
|
}
|
|
|
|
// Make sure we can fit both the craft result and tools in the stash
|
|
var totalResultItems = new List<List<Item>>();
|
|
totalResultItems.AddRange(itemAndChildrenToSendToPlayer);
|
|
totalResultItems.AddRange(toolsToSendToPlayer);
|
|
|
|
if (!inventoryHelper.CanPlaceItemsInInventory(sessionID, totalResultItems))
|
|
{
|
|
httpResponseUtil.AppendErrorToOutput(
|
|
output,
|
|
serverLocalisationService.GetText("inventory-no_stash_space"),
|
|
BackendErrorCodes.NotEnoughSpace
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Add the tools to the stash, we have to do this individually due to FiR state potentially being different
|
|
foreach (var toolItem in toolsToSendToPlayer)
|
|
{
|
|
// Note: FIR state will be based on the first item's SpawnedInSession property per item group
|
|
var addToolsRequest = new AddItemsDirectRequest
|
|
{
|
|
ItemsWithModsToAdd = [toolItem],
|
|
FoundInRaid = toolItem.FirstOrDefault()?.Upd?.SpawnedInSession ?? false,
|
|
UseSortingTable = false,
|
|
Callback = null,
|
|
};
|
|
|
|
inventoryHelper.AddItemsToStash(sessionID, addToolsRequest, pmcData, output);
|
|
if (output.Warnings?.Count > 0)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Add the crafting result to the stash, marked as FiR
|
|
var addItemsRequest = new AddItemsDirectRequest
|
|
{
|
|
ItemsWithModsToAdd = itemAndChildrenToSendToPlayer,
|
|
FoundInRaid = true,
|
|
UseSortingTable = false,
|
|
Callback = null,
|
|
};
|
|
inventoryHelper.AddItemsToStash(sessionID, addItemsRequest, pmcData, output);
|
|
if (output.Warnings?.Count > 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// - increment skill point for crafting
|
|
// - delete the production in profile Hideout.Production
|
|
// Hideout Management skill
|
|
// ? use a configuration variable for the value?
|
|
var globals = databaseService.GetGlobals();
|
|
profileHelper.AddSkillPointsToPlayer(
|
|
pmcData,
|
|
SkillTypes.HideoutManagement,
|
|
globals.Configuration.SkillsSettings.HideoutManagement.SkillPointsPerCraft,
|
|
true
|
|
);
|
|
|
|
// Add Crafting skill to player profile
|
|
if (craftingExpAmount > 0)
|
|
{
|
|
profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.Crafting, craftingExpAmount);
|
|
|
|
var intellectAmountToGive = 0.5 * Math.Round((double)(craftingExpAmount / 15));
|
|
if (intellectAmountToGive > 0)
|
|
{
|
|
profileHelper.AddSkillPointsToPlayer(pmcData, SkillTypes.Intellect, intellectAmountToGive);
|
|
}
|
|
}
|
|
|
|
area.LastRecipe = request.RecipeId;
|
|
|
|
// Update profiles hours crafting value
|
|
counterHoursCrafting.Value = totalCraftingHours;
|
|
|
|
// Continuous crafts have special handling in EventOutputHolder.updateOutputProperties()
|
|
hideoutProduction.SptIsComplete = true;
|
|
hideoutProduction.SptIsContinuous = recipe.Continuous;
|
|
|
|
// Continuous recipes need the craft time refreshed as it gets created once on initial craft and stays the same regardless of what
|
|
// production.json is set to
|
|
if (recipe.Continuous.GetValueOrDefault(false))
|
|
{
|
|
hideoutProduction.ProductionTime = hideoutHelper.GetAdjustedCraftTimeWithSkills(pmcData, recipe.Id, true);
|
|
}
|
|
|
|
// Flag normal (not continuous) crafts as complete
|
|
if (!recipe.Continuous ?? false)
|
|
{
|
|
hideoutProduction.InProgress = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensure non-stackable items are 'unstacked'
|
|
/// </summary>
|
|
/// <param name="recipe"></param>
|
|
/// <param name="itemAndChildrenToSendToPlayer"></param>
|
|
/// <param name="rewardIsPreset">Reward is a preset</param>
|
|
protected void HandleStackableState(HideoutProduction recipe, List<List<Item>> itemAndChildrenToSendToPlayer, bool rewardIsPreset)
|
|
{
|
|
var rewardIsStackable = itemHelper.IsItemTplStackable(recipe.EndProduct);
|
|
if (rewardIsStackable.GetValueOrDefault(false))
|
|
{
|
|
// Create root item
|
|
var rewardToAdd = new Item
|
|
{
|
|
Id = new MongoId(),
|
|
Template = recipe.EndProduct,
|
|
Upd = new Upd { StackObjectsCount = recipe.Count },
|
|
};
|
|
|
|
// Split item into separate items with acceptable stack sizes
|
|
var splitReward = itemHelper.SplitStackIntoSeparateItems(rewardToAdd);
|
|
itemAndChildrenToSendToPlayer.AddRange(splitReward);
|
|
|
|
return;
|
|
}
|
|
|
|
// Not stackable, may have to send multiple of reward
|
|
|
|
// Add the first reward item to array when not a preset (first preset added above earlier)
|
|
if (!rewardIsPreset)
|
|
{
|
|
itemAndChildrenToSendToPlayer.Add([new Item { Id = new MongoId(), Template = recipe.EndProduct }]);
|
|
}
|
|
|
|
// Add multiple of item if recipe requests it
|
|
// Start index at one so we ignore first item in array
|
|
var countOfItemsToReward = recipe.Count;
|
|
for (var index = 1; index < countOfItemsToReward; index++)
|
|
{
|
|
var firstItemWithChildrenClone = cloner.Clone(itemAndChildrenToSendToPlayer.FirstOrDefault()).ReplaceIDs().ToList();
|
|
|
|
itemAndChildrenToSendToPlayer.AddRange([firstItemWithChildrenClone]);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// </summary>
|
|
/// <param name="recipe"></param>
|
|
/// <returns></returns>
|
|
protected List<List<Item>> HandlePresetReward(HideoutProduction recipe)
|
|
{
|
|
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
|
|
var presetAndModsClone = cloner.Clone(defaultPreset.Items).ReplaceIDs().ToList();
|
|
|
|
presetAndModsClone.RemapRootItemId();
|
|
|
|
// Store preset items in array
|
|
return [presetAndModsClone];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create our own craft counter
|
|
/// Get the "CounterHoursCrafting" TaskConditionCounter from a profile
|
|
/// </summary>
|
|
/// <param name="pmcData">Profile to get counter from</param>
|
|
/// <param name="recipe">Recipe being crafted</param>
|
|
/// <returns>TaskConditionCounter</returns>
|
|
protected TaskConditionCounter GetCustomSptHoursCraftingTaskConditionCounter(PmcData pmcData, HideoutProduction recipe)
|
|
{
|
|
// Add if doesn't exist
|
|
pmcData.TaskConditionCounters.TryAdd(
|
|
NameTaskConditionCountersCraftingId,
|
|
new TaskConditionCounter
|
|
{
|
|
Id = recipe.Id,
|
|
Type = "CounterCrafting",
|
|
SourceId = NameTaskConditionCountersCraftingId,
|
|
Value = 0,
|
|
}
|
|
);
|
|
|
|
return pmcData.TaskConditionCounters.GetValueOrDefault(NameTaskConditionCountersCraftingId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handles generating scav case rewards and sending to player inventory
|
|
/// </summary>
|
|
/// <param name="sessionID">Session/Player id</param>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="request">Get rewards from scavcase craft request</param>
|
|
/// <param name="output">Client response</param>
|
|
protected void HandleScavCase(
|
|
MongoId sessionID,
|
|
PmcData pmcData,
|
|
HideoutTakeProductionRequestData request,
|
|
ItemEventRouterResponse output
|
|
)
|
|
{
|
|
var ongoingProductions = pmcData.Hideout.Production;
|
|
MongoId? prodId = null;
|
|
foreach (var (ongoingProdId, ongoingProduction) in ongoingProductions)
|
|
// Production or ScavCase
|
|
{
|
|
if (ongoingProduction?.RecipeId == request.RecipeId)
|
|
{
|
|
prodId = ongoingProdId; // Set to objects key
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (prodId == null)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-unable_to_find_production_in_profile_by_recipie_id", request.RecipeId));
|
|
|
|
httpResponseUtil.AppendErrorToOutput(output);
|
|
|
|
return;
|
|
}
|
|
|
|
// Create rewards for scav case
|
|
var scavCaseRewards = scavCaseRewardGenerator.Generate(request.RecipeId);
|
|
|
|
var addItemsRequest = new AddItemsDirectRequest
|
|
{
|
|
ItemsWithModsToAdd = scavCaseRewards,
|
|
FoundInRaid = true,
|
|
Callback = null,
|
|
UseSortingTable = false,
|
|
};
|
|
|
|
inventoryHelper.AddItemsToStash(sessionID, addItemsRequest, pmcData, output);
|
|
if (output.Warnings?.Count > 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Remove the old production from output object before its sent to client
|
|
output.ProfileChanges[sessionID].Production.Remove(request.RecipeId);
|
|
|
|
// Flag as complete - will be cleaned up later by hideoutController.update()
|
|
pmcData.Hideout.Production[prodId.Value].SptIsComplete = true;
|
|
|
|
// Crafting complete, flag
|
|
pmcData.Hideout.Production[prodId.Value].InProgress = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle HideoutQuickTimeEvent on client/game/profile/items/moving
|
|
/// Called after completing workout at gym
|
|
/// </summary>
|
|
/// <param name="sessionId">Session/Player id</param>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="request">QTE result object</param>
|
|
/// <param name="output">Client response</param>
|
|
public void HandleQTEEventOutcome(MongoId sessionId, PmcData pmcData, HandleQTEEventRequestData request, ItemEventRouterResponse output)
|
|
{
|
|
// {
|
|
// "Action": "HideoutQuickTimeEvent",
|
|
// "results": [true, false, true, true, true, true, true, true, true, false, false, false, false, false, false],
|
|
// "id": "63b16feb5d012c402c01f6ef",
|
|
// "timestamp": 1672585349
|
|
// }
|
|
|
|
// Skill changes are done in
|
|
// /client/hideout/workout (applyWorkoutChanges).
|
|
|
|
var qteDb = databaseService.GetHideout().Qte;
|
|
var relevantQte = qteDb.FirstOrDefault(qte => qte.Id == request.Id);
|
|
foreach (var outcome in request.Results)
|
|
{
|
|
if (outcome)
|
|
{
|
|
// Success
|
|
pmcData.Health.Energy.Current += relevantQte.Results[QteEffectType.singleSuccessEffect].Energy;
|
|
pmcData.Health.Hydration.Current += relevantQte.Results[QteEffectType.singleSuccessEffect].Hydration;
|
|
}
|
|
else
|
|
{
|
|
// Failed
|
|
pmcData.Health.Energy.Current += relevantQte.Results[QteEffectType.singleFailEffect].Energy;
|
|
pmcData.Health.Hydration.Current += relevantQte.Results[QteEffectType.singleFailEffect].Hydration;
|
|
}
|
|
}
|
|
|
|
if (pmcData.Health.Energy.Current < 1)
|
|
{
|
|
pmcData.Health.Energy.Current = 1;
|
|
}
|
|
|
|
if (pmcData.Health.Hydration.Current < 1)
|
|
{
|
|
pmcData.Health.Hydration.Current = 1;
|
|
}
|
|
|
|
HandleMusclePain(pmcData, relevantQte.Results[QteEffectType.finishEffect]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Apply mild/severe muscle pain after gym use
|
|
/// </summary>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="finishEffect">Effect data to apply after completing QTE gym event</param>
|
|
protected void HandleMusclePain(PmcData pmcData, QteResult finishEffect)
|
|
{
|
|
var hasMildPain = pmcData.Health.BodyParts["Chest"].Effects?.ContainsKey("MildMusclePain");
|
|
var hasSeverePain = pmcData.Health.BodyParts["Chest"].Effects?.ContainsKey("SevereMusclePain");
|
|
|
|
// Has no muscle pain at all, add mild
|
|
if (!hasMildPain.GetValueOrDefault(false) && !hasSeverePain.GetValueOrDefault(false))
|
|
{
|
|
// Nullguard
|
|
pmcData.Health.BodyParts["Chest"].Effects ??= new Dictionary<string, BodyPartEffectProperties>();
|
|
pmcData.Health.BodyParts["Chest"].Effects["MildMusclePain"] = new BodyPartEffectProperties
|
|
{
|
|
Time = finishEffect.RewardEffects.FirstOrDefault().Time, // TODO - remove hard coded access, get value properly
|
|
};
|
|
|
|
return;
|
|
}
|
|
|
|
if (hasMildPain.GetValueOrDefault(false))
|
|
{
|
|
// Already has mild pain, remove mild and add severe
|
|
pmcData.Health.BodyParts["Chest"].Effects.Remove("MildMusclePain");
|
|
|
|
pmcData.Health.BodyParts["Chest"].Effects["SevereMusclePain"] = new BodyPartEffectProperties
|
|
{
|
|
Time = finishEffect.RewardEffects.FirstOrDefault().Time,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Record a high score from the shooting range into a player profiles `overallcounters`
|
|
/// </summary>
|
|
/// <param name="sessionId">Session/Player id</param>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="request">shooting range score request></param>
|
|
public void RecordShootingRangePoints(MongoId sessionId, PmcData pmcData, RecordShootingRangePoints request)
|
|
{
|
|
const string shootingRangeKey = "ShootingRangePoints";
|
|
var overallCounterItems = pmcData.Stats.Eft.OverallCounters.Items;
|
|
|
|
// Find counter by key
|
|
var shootingRangeHighScore = overallCounterItems.FirstOrDefault(counter => counter.Key.Contains(shootingRangeKey));
|
|
if (shootingRangeHighScore is null)
|
|
{
|
|
// Counter not found, add blank one
|
|
overallCounterItems.Add(new CounterKeyValue { Key = [shootingRangeKey], Value = 0 });
|
|
shootingRangeHighScore = overallCounterItems.FirstOrDefault(counter => counter.Key.Contains(shootingRangeKey));
|
|
}
|
|
|
|
shootingRangeHighScore.Value = request.Points;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle client/game/profile/items/moving - HideoutImproveArea
|
|
/// </summary>
|
|
/// <param name="sessionId">Session/Player id</param>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="request">Improve area request</param>
|
|
/// <returns>ItemEventRouterResponse</returns>
|
|
public ItemEventRouterResponse ImproveArea(MongoId sessionId, PmcData pmcData, HideoutImproveAreaRequestData request)
|
|
{
|
|
var output = eventOutputHolder.GetOutput(sessionId);
|
|
|
|
// Create mapping of required item with corresponding item from player inventory
|
|
var items = request.Items.Select(reqItem =>
|
|
{
|
|
var item = pmcData.Inventory.Items.FirstOrDefault(invItem => invItem.Id == reqItem.Id);
|
|
return new { inventoryItem = item, requestedItem = reqItem };
|
|
});
|
|
|
|
// If it's not money, its construction / barter items
|
|
foreach (var item in items)
|
|
{
|
|
if (item.inventoryItem is null)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-unable_to_find_item_in_inventory", item.requestedItem.Id));
|
|
return httpResponseUtil.AppendErrorToOutput(output);
|
|
}
|
|
|
|
if (
|
|
paymentHelper.IsMoneyTpl(item.inventoryItem.Template)
|
|
&& item.inventoryItem.Upd is not null
|
|
&& item.inventoryItem.Upd.StackObjectsCount is not null
|
|
&& item.inventoryItem.Upd.StackObjectsCount > item.requestedItem.Count
|
|
)
|
|
{
|
|
item.inventoryItem.Upd.StackObjectsCount -= item.requestedItem.Count;
|
|
}
|
|
else
|
|
{
|
|
inventoryHelper.RemoveItem(pmcData, item.inventoryItem.Id, sessionId, output);
|
|
}
|
|
}
|
|
|
|
var profileHideoutArea = pmcData.Hideout.Areas.FirstOrDefault(x => x.Type == request.AreaType);
|
|
if (profileHideoutArea is null)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-unable_to_find_area", request.AreaType));
|
|
return httpResponseUtil.AppendErrorToOutput(output);
|
|
}
|
|
|
|
var hideoutDbData = databaseService.GetHideout().Areas.FirstOrDefault(area => area.Type == request.AreaType);
|
|
if (hideoutDbData is null)
|
|
{
|
|
logger.Error(serverLocalisationService.GetText("hideout-unable_to_find_area_in_database", request.AreaType));
|
|
return httpResponseUtil.AppendErrorToOutput(output);
|
|
}
|
|
|
|
// Add all improvements to output object
|
|
var improvements = hideoutDbData.Stages[profileHideoutArea.Level.ToString()].Improvements;
|
|
var timestamp = timeUtil.GetTimeStamp();
|
|
|
|
// nullguard
|
|
output.ProfileChanges[sessionId].Improvements ??= [];
|
|
|
|
foreach (var stageImprovement in improvements)
|
|
{
|
|
var improvementDetails = new HideoutImprovement
|
|
{
|
|
Completed = false,
|
|
ImproveCompleteTimestamp = (long)(timestamp + stageImprovement.ImprovementTime),
|
|
};
|
|
output.ProfileChanges[sessionId].Improvements[stageImprovement.Id] = improvementDetails;
|
|
|
|
pmcData.Hideout.Improvements ??= [];
|
|
pmcData.Hideout.Improvements[stageImprovement.Id] = improvementDetails;
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle client/game/profile/items/moving HideoutCancelProductionCommand
|
|
/// </summary>
|
|
/// <param name="sessionId">Session/Player id</param>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="request">Cancel production request data</param>
|
|
/// <returns>ItemEventRouterResponse</returns>
|
|
public ItemEventRouterResponse CancelProduction(MongoId sessionId, PmcData pmcData, HideoutCancelProductionRequestData request)
|
|
{
|
|
var output = eventOutputHolder.GetOutput(sessionId);
|
|
|
|
var craftToCancel = pmcData.Hideout.Production[request.RecipeId];
|
|
if (craftToCancel is null)
|
|
{
|
|
var errorMessage = $"Unable to find craft {request.RecipeId} to cancel";
|
|
logger.Error(errorMessage);
|
|
|
|
return httpResponseUtil.AppendErrorToOutput(output, errorMessage);
|
|
}
|
|
|
|
// Null out production data so client gets informed when response send back
|
|
pmcData.Hideout.Production[request.RecipeId] = null;
|
|
|
|
// TODO - handle timestamp somehow?
|
|
|
|
return output;
|
|
}
|
|
|
|
public ItemEventRouterResponse CircleOfCultistProductionStart(
|
|
MongoId sessionId,
|
|
PmcData pmcData,
|
|
HideoutCircleOfCultistProductionStartRequestData request
|
|
)
|
|
{
|
|
return circleOfCultistService.StartSacrifice(sessionId, pmcData, request);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle HideoutDeleteProductionCommand event
|
|
/// </summary>
|
|
/// <param name="sessionId">Session/Player id</param>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="request">Delete production request</param>
|
|
/// <returns>ItemEventRouterResponse</returns>
|
|
public ItemEventRouterResponse HideoutDeleteProductionCommand(
|
|
MongoId sessionId,
|
|
PmcData pmcData,
|
|
HideoutDeleteProductionRequestData request
|
|
)
|
|
{
|
|
var output = eventOutputHolder.GetOutput(sessionId);
|
|
|
|
pmcData.Hideout.Production[request.RecipeId] = null;
|
|
output.ProfileChanges[sessionId].Production = null;
|
|
|
|
return output;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle HideoutCustomizationApply event
|
|
/// </summary>
|
|
/// <param name="sessionId">Session/Player id</param>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="request">Apply hideout customisation request</param>
|
|
/// <returns>ItemEventRouterResponse</returns>
|
|
public ItemEventRouterResponse HideoutCustomizationApply(
|
|
MongoId sessionId,
|
|
PmcData pmcData,
|
|
HideoutCustomizationApplyRequestData request
|
|
)
|
|
{
|
|
var output = eventOutputHolder.GetOutput(sessionId);
|
|
|
|
var itemDetails = databaseService.GetHideout().Customisation.Globals.FirstOrDefault(cust => cust.Id == request.OfferId);
|
|
if (itemDetails is null)
|
|
{
|
|
logger.Error($"Unable to find customisation: {request.OfferId} in db, cannot apply to hideout");
|
|
|
|
return output;
|
|
}
|
|
|
|
pmcData.Hideout.Customization[GetHideoutCustomisationType(itemDetails.Type)] = itemDetails.ItemId.Value;
|
|
|
|
return output;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Map an internal customisation type to a client hideout customisation type
|
|
/// </summary>
|
|
/// <param name="type"></param>
|
|
/// <returns>hideout customisation type</returns>
|
|
protected string? GetHideoutCustomisationType(string? type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case "wall":
|
|
return "Wall";
|
|
case "floor":
|
|
return "Floor";
|
|
case "light":
|
|
return "Light";
|
|
case "ceiling":
|
|
return "Ceiling";
|
|
case "shootingRangeMark":
|
|
return "ShootingRangeMark";
|
|
default:
|
|
logger.Warning($"Unknown {type}, unable to map");
|
|
return type;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add stand1/stand2/stand3 inventory items to profile, depending on passed in hideout stage
|
|
/// </summary>
|
|
/// <param name="sessionId">Session/Player id</param>
|
|
/// <param name="equipmentPresetStage">Current EQUIPMENT_PRESETS_STAND stage data</param>
|
|
/// <param name="pmcData">Players PMC profile</param>
|
|
/// <param name="equipmentPresetHideoutArea"></param>
|
|
/// <param name="output">Client response</param>
|
|
protected void AddMissingPresetStandItemsToProfile(
|
|
MongoId sessionId,
|
|
Stage equipmentPresetStage,
|
|
PmcData pmcData,
|
|
HideoutArea equipmentPresetHideoutArea,
|
|
ItemEventRouterResponse output
|
|
)
|
|
{
|
|
// Each slot is a single Mannequin
|
|
var slots = itemHelper.GetItem(equipmentPresetStage.Container.Value).Value.Properties.Slots;
|
|
foreach (var mannequinSlot in slots)
|
|
{
|
|
// Check if we've already added this mannequin
|
|
var existingMannequin = pmcData.Inventory.Items.FirstOrDefault(item =>
|
|
item.ParentId == equipmentPresetHideoutArea.Id && item.SlotId == mannequinSlot.Name
|
|
);
|
|
|
|
// No child, add it
|
|
if (existingMannequin is null)
|
|
{
|
|
var standId = new MongoId();
|
|
var mannequinToAdd = new Item
|
|
{
|
|
Id = standId,
|
|
Template = ItemTpl.INVENTORY_DEFAULT,
|
|
ParentId = equipmentPresetHideoutArea.Id,
|
|
SlotId = mannequinSlot.Name,
|
|
};
|
|
pmcData.Inventory.Items.Add(mannequinToAdd);
|
|
|
|
// Add pocket child item
|
|
var mannequinPocketItemToAdd = new Item
|
|
{
|
|
Id = new MongoId(),
|
|
Template = pmcData
|
|
.Inventory.Items.FirstOrDefault(item => item.SlotId == "Pockets" && item.ParentId == pmcData.Inventory.Equipment)
|
|
.Template, // Same pocket tpl as players profile (unheard get bigger, matching pockets etc)
|
|
ParentId = standId,
|
|
SlotId = "Pockets",
|
|
};
|
|
pmcData.Inventory.Items.Add(mannequinPocketItemToAdd);
|
|
output.ProfileChanges[sessionId].Items.NewItems.Add(mannequinToAdd);
|
|
output.ProfileChanges[sessionId].Items.NewItems.Add(mannequinPocketItemToAdd);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle HideoutCustomizationSetMannequinPose event
|
|
/// </summary>
|
|
/// <param name="sessionId">Session id</param>
|
|
/// <param name="pmcData">Player profile</param>
|
|
/// <param name="request">Client request</param>
|
|
/// <returns></returns>
|
|
public ItemEventRouterResponse HideoutCustomizationSetMannequinPose(
|
|
MongoId sessionId,
|
|
PmcData pmcData,
|
|
HideoutCustomizationSetMannequinPoseRequest request
|
|
)
|
|
{
|
|
if (request.Poses is null)
|
|
{
|
|
logger.Warning("this really shouldnt be possible, but a request has come in with a pose change without poses");
|
|
return eventOutputHolder.GetOutput(sessionId);
|
|
}
|
|
|
|
foreach (var (poseKey, poseValue) in request.Poses)
|
|
{
|
|
// Nullguard
|
|
pmcData.Hideout.MannequinPoses ??= [];
|
|
pmcData.Hideout.MannequinPoses[poseKey] = poseValue;
|
|
}
|
|
|
|
return eventOutputHolder.GetOutput(sessionId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle client/hideout/qte/list
|
|
/// Get quick time event list for hideout
|
|
/// </summary>
|
|
/// <param name="sessionId">Session/Player id</param>
|
|
/// <returns></returns>
|
|
public List<QteData> GetQteList(MongoId sessionId)
|
|
{
|
|
return databaseService.GetHideout().Qte;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called every `hideoutConfig.runIntervalSeconds` seconds as part of onUpdate event
|
|
/// Updates hideout craft times
|
|
/// </summary>
|
|
public void Update()
|
|
{
|
|
foreach (var (sessionId, profile) in saveServer.GetProfiles())
|
|
{
|
|
if (
|
|
profile.CharacterData?.PmcData?.Hideout is not null
|
|
&& profileActivityService.ActiveWithinLastMinutes(sessionId, _hideoutConfig.UpdateProfileHideoutWhenActiveWithinMinutes)
|
|
)
|
|
{
|
|
hideoutHelper.UpdatePlayerHideout(sessionId);
|
|
}
|
|
}
|
|
}
|
|
}
|