424 lines
16 KiB
C#
424 lines
16 KiB
C#
using Core.Generators;
|
|
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.Spt.Repeatable;
|
|
using Core.Models.Utils;
|
|
using Core.Routers;
|
|
using Core.Servers;
|
|
using Core.Services;
|
|
using Core.Utils;
|
|
using Core.Utils.Cloners;
|
|
using SptCommon.Annotations;
|
|
|
|
namespace Core.Controllers;
|
|
|
|
[Injectable]
|
|
public class RepeatableQuestController(
|
|
ISptLogger<RepeatableQuestChangeRequest> _logger,
|
|
TimeUtil _timeUtil,
|
|
HashUtil _hashUtil,
|
|
RandomUtil _randomUtil,
|
|
HttpResponseUtil _responseUtil,
|
|
ProfileHelper _profileHelper,
|
|
ProfileFixerService _profileFixerService,
|
|
LocalisationService _localisationService,
|
|
EventOutputHolder _eventOutputHolder,
|
|
PaymentService _paymentService,
|
|
RepeatableQuestGenerator _repeatableQuestGenerator,
|
|
RepeatableQuestHelper _repeatableQuestHelper,
|
|
QuestHelper _questHelper,
|
|
DatabaseService _databaseService,
|
|
ConfigServer _configServer,
|
|
ICloner _cloner
|
|
)
|
|
{
|
|
protected QuestConfig _questConfig = _configServer.GetConfig<QuestConfig>();
|
|
|
|
public ItemEventRouterResponse ChangeRepeatableQuest(PmcData pmcData, RepeatableQuestChangeRequest info,
|
|
string sessionId)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
public List<PmcDataRepeatableQuest> GetClientRepeatableQuests(string sessionID)
|
|
{
|
|
var returnData = new List<PmcDataRepeatableQuest>();
|
|
var fullProfile = _profileHelper.GetFullProfile(sessionID);
|
|
var pmcData = fullProfile.CharacterData.PmcData;
|
|
var currentTime = _timeUtil.GetTimeStamp();
|
|
|
|
// Daily / weekly / Daily_Savage
|
|
foreach (var repeatableConfig in _questConfig.RepeatableQuests)
|
|
{
|
|
// Get daily/weekly data from profile, add empty object if missing
|
|
var generatedRepeatables = GetRepeatableQuestSubTypeFromProfile(repeatableConfig, pmcData);
|
|
var repeatableTypeLower = repeatableConfig.Name.ToLower();
|
|
|
|
var canAccessRepeatables = CanProfileAccessRepeatableQuests(repeatableConfig, pmcData);
|
|
if (!canAccessRepeatables)
|
|
{
|
|
// Don't send any repeatables, even existing ones
|
|
continue;
|
|
}
|
|
|
|
// Existing repeatables are still valid, add to return data and move to next sub-type
|
|
if (currentTime < generatedRepeatables.EndTime - 1)
|
|
{
|
|
returnData.Add(generatedRepeatables);
|
|
|
|
_logger.Debug($"[Quest Check] {repeatableTypeLower} quests are still valid.");
|
|
|
|
continue;
|
|
}
|
|
|
|
// Current time is past expiry time
|
|
|
|
// Set endtime to be now + new duration
|
|
generatedRepeatables.EndTime = currentTime + repeatableConfig.ResetTime;
|
|
generatedRepeatables.InactiveQuests = [];
|
|
_logger.Debug($"Generating new {repeatableTypeLower}");
|
|
|
|
// Put old quests to inactive (this is required since only then the client makes them fail due to non-completion)
|
|
// Also need to push them to the "inactiveQuests" list since we need to remove them from offraidData.profile.Quests
|
|
// after a raid (the client seems to keep quests internally and we want to get rid of old repeatable quests)
|
|
// and remove them from the PMC's Quests and RepeatableQuests[i].activeQuests
|
|
ProcessExpiredQuests(generatedRepeatables, pmcData);
|
|
|
|
// Create dynamic quest pool to avoid generating duplicates
|
|
var questTypePool = GenerateQuestPool(repeatableConfig, pmcData.Info.Level);
|
|
|
|
// Add repeatable quests of this loops sub-type (daily/weekly)
|
|
for (var i = 0; i < GetQuestCount(repeatableConfig, pmcData); i++)
|
|
{
|
|
var quest = new RepeatableQuest();
|
|
var lifeline = 0;
|
|
while (quest.Id is null && questTypePool.Types.Count > 0)
|
|
{
|
|
quest = _repeatableQuestGenerator.GenerateRepeatableQuest(
|
|
sessionID,
|
|
pmcData.Info.Level ?? 0,
|
|
pmcData.TradersInfo,
|
|
questTypePool,
|
|
repeatableConfig
|
|
);
|
|
lifeline++;
|
|
if (lifeline > 10)
|
|
{
|
|
_logger.Debug(
|
|
"We were stuck in repeatable quest generation. This should never happen. Please report"
|
|
);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// check if there are no more quest types available
|
|
if (questTypePool.Types.Count == 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
quest.Side = repeatableConfig.Side;
|
|
generatedRepeatables.ActiveQuests.Add(quest);
|
|
}
|
|
|
|
// Nullguard
|
|
fullProfile.SptData.FreeRepeatableRefreshUsedCount ??= new Dictionary<string, int>();
|
|
|
|
|
|
// Reset players free quest count for this repeatable sub-type as we're generating new repeatables for this group (daily/weekly)
|
|
fullProfile.SptData.FreeRepeatableRefreshUsedCount[repeatableTypeLower] = 0;
|
|
|
|
// Create stupid redundant change requirements from quest data
|
|
foreach (var quest in generatedRepeatables.ActiveQuests)
|
|
generatedRepeatables.ChangeRequirement[quest.Id] = new ChangeRequirement
|
|
{
|
|
ChangeCost = quest.ChangeCost,
|
|
ChangeStandingCost = _randomUtil.GetArrayValue([0, 0.01]) // Randomise standing cost to replace
|
|
};
|
|
|
|
// Reset free repeatable values in player profile to defaults
|
|
generatedRepeatables.FreeChanges = repeatableConfig.FreeChanges;
|
|
generatedRepeatables.FreeChangesAvailable = repeatableConfig.FreeChanges;
|
|
|
|
returnData.Add(
|
|
new PmcDataRepeatableQuest
|
|
{
|
|
Id = repeatableConfig.Id,
|
|
Name = generatedRepeatables.Name,
|
|
EndTime = generatedRepeatables.EndTime,
|
|
ActiveQuests = generatedRepeatables.ActiveQuests,
|
|
InactiveQuests = generatedRepeatables.InactiveQuests,
|
|
ChangeRequirement = generatedRepeatables.ChangeRequirement,
|
|
FreeChanges = generatedRepeatables.FreeChanges,
|
|
FreeChangesAvailable = generatedRepeatables.FreeChanges
|
|
}
|
|
);
|
|
}
|
|
|
|
return returnData;
|
|
}
|
|
|
|
private PmcDataRepeatableQuest GetRepeatableQuestSubTypeFromProfile(RepeatableQuestConfig repeatableConfig,
|
|
PmcData pmcData)
|
|
{
|
|
// Get from profile, add if missing
|
|
var repeatableQuestDetails = pmcData.RepeatableQuests.FirstOrDefault(
|
|
repeatable => repeatable.Name == repeatableConfig.Name
|
|
);
|
|
if (repeatableQuestDetails is null)
|
|
{
|
|
// Not in profile, generate
|
|
var hasAccess = _profileHelper.HasAccessToRepeatableFreeRefreshSystem(pmcData);
|
|
repeatableQuestDetails = new PmcDataRepeatableQuest
|
|
{
|
|
Id = repeatableConfig.Id,
|
|
Name = repeatableConfig.Name,
|
|
ActiveQuests = [],
|
|
InactiveQuests = [],
|
|
EndTime = 0,
|
|
FreeChanges = hasAccess ? repeatableConfig.FreeChanges : 0,
|
|
FreeChangesAvailable = hasAccess ? repeatableConfig.FreeChangesAvailable : 0
|
|
};
|
|
|
|
// Add base object that holds repeatable data to profile
|
|
pmcData.RepeatableQuests.Add(repeatableQuestDetails);
|
|
}
|
|
|
|
return repeatableQuestDetails;
|
|
}
|
|
|
|
private bool CanProfileAccessRepeatableQuests(RepeatableQuestConfig repeatableConfig, PmcData pmcData)
|
|
{
|
|
// PMC and daily quests not unlocked yet
|
|
if (repeatableConfig.Side == "Pmc" && !PlayerHasDailyPmcQuestsUnlocked(pmcData, repeatableConfig))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Scav and daily quests not unlocked yet
|
|
if (repeatableConfig.Side == "Scav" && !PlayerHasDailyScavQuestsUnlocked(pmcData))
|
|
{
|
|
_logger.Debug("Daily scav quests still locked, Intel center not built");
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Does player have daily pmc quests unlocked
|
|
* @param pmcData Player profile to check
|
|
* @param repeatableConfig Config of daily type to check
|
|
* @returns True if unlocked
|
|
*/
|
|
private bool PlayerHasDailyPmcQuestsUnlocked(PmcData pmcData, RepeatableQuestConfig repeatableConfig)
|
|
{
|
|
return pmcData.Info.Level >= repeatableConfig.MinPlayerLevel;
|
|
}
|
|
|
|
/**
|
|
* Does player have daily scav quests unlocked
|
|
* @param pmcData Player profile to check
|
|
* @returns True if unlocked
|
|
*/
|
|
private bool PlayerHasDailyScavQuestsUnlocked(PmcData pmcData)
|
|
{
|
|
return pmcData?.Hideout?.Areas?.FirstOrDefault(hideoutArea => hideoutArea.Type == HideoutAreas.INTEL_CENTER)
|
|
?.Level >=
|
|
1;
|
|
}
|
|
|
|
private void ProcessExpiredQuests(PmcDataRepeatableQuest generatedRepeatables, PmcData pmcData)
|
|
{
|
|
var questsToKeep = new List<RepeatableQuest>();
|
|
foreach (var activeQuest in generatedRepeatables.ActiveQuests)
|
|
{
|
|
var questStatusInProfile = pmcData.Quests.FirstOrDefault(quest => quest.QId == activeQuest.Id);
|
|
if (questStatusInProfile is null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Keep finished quests in list so player can hand in
|
|
if (questStatusInProfile.Status == QuestStatusEnum.AvailableForFinish)
|
|
{
|
|
questsToKeep.Add(activeQuest);
|
|
_logger.Debug(
|
|
$"Keeping repeatable quest: {activeQuest.Id} in activeQuests since it is available to hand in"
|
|
);
|
|
|
|
continue;
|
|
}
|
|
|
|
// Clean up quest-related counters being left in profile
|
|
_profileFixerService.RemoveDanglingConditionCounters(pmcData);
|
|
|
|
// Remove expired quest from pmc.quest array
|
|
pmcData.Quests = pmcData.Quests.Where(quest => quest.QId != activeQuest.Id).ToList();
|
|
|
|
// Store in inactive array
|
|
generatedRepeatables.InactiveQuests.Add(activeQuest);
|
|
}
|
|
|
|
generatedRepeatables.ActiveQuests = questsToKeep;
|
|
}
|
|
|
|
private QuestTypePool GenerateQuestPool(RepeatableQuestConfig repeatableConfig, int? pmcLevel)
|
|
{
|
|
var questPool = CreateBaseQuestPool(repeatableConfig);
|
|
|
|
// Get the allowed locations based on the PMC's level
|
|
var locations = GetAllowedLocationsForPmcLevel(repeatableConfig.Locations, pmcLevel.Value);
|
|
|
|
// Populate Exploration and Pickup quest locations
|
|
foreach (var (location, value) in locations)
|
|
if (location != ELocationName.any)
|
|
{
|
|
questPool.Pool.Exploration.Locations[location] = value;
|
|
questPool.Pool.Pickup.Locations[location] = value;
|
|
}
|
|
|
|
// Add "any" to pickup quest pool
|
|
questPool.Pool.Pickup.Locations[ELocationName.any] = ["any"];
|
|
|
|
var eliminationConfig = _repeatableQuestHelper.GetEliminationConfigByPmcLevel(pmcLevel.Value, repeatableConfig);
|
|
var targetsConfig =
|
|
_repeatableQuestHelper.ProbabilityObjectArray<Target, string, BossInfo>(eliminationConfig.Targets);
|
|
|
|
// Populate Elimination quest targets and their locations
|
|
foreach (var targetKvP in targetsConfig)
|
|
// Target is boss
|
|
if (targetKvP.Data.IsBoss.GetValueOrDefault(false))
|
|
{
|
|
questPool.Pool.Elimination.Targets[targetKvP.Key] = new TargetLocation { Locations = ["any"] };
|
|
}
|
|
else
|
|
{
|
|
// Non-boss targets
|
|
var possibleLocations = locations.Keys;
|
|
|
|
var allowedLocations =
|
|
targetKvP.Key == "Savage"
|
|
? possibleLocations.Where(
|
|
location => location != ELocationName.laboratory
|
|
) // Exclude labs for Savage targets.
|
|
: possibleLocations;
|
|
|
|
questPool.Pool.Elimination.Targets[targetKvP.Key] = new TargetLocation
|
|
{ Locations = allowedLocations.Select(x => x.ToString()).ToList() };
|
|
}
|
|
|
|
return questPool;
|
|
}
|
|
|
|
private QuestTypePool CreateBaseQuestPool(RepeatableQuestConfig repeatableConfig)
|
|
{
|
|
return new QuestTypePool
|
|
{
|
|
Types = _cloner.Clone(repeatableConfig.Types),
|
|
Pool = new QuestPool
|
|
{
|
|
Exploration = new ExplorationPool
|
|
{
|
|
Locations = new Dictionary<ELocationName, List<string>>()
|
|
},
|
|
Elimination = new EliminationPool
|
|
{
|
|
Targets = new Dictionary<string, TargetLocation>()
|
|
},
|
|
Pickup = new ExplorationPool
|
|
{
|
|
Locations = new Dictionary<ELocationName, List<string>>()
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
private Dictionary<ELocationName, List<string>> GetAllowedLocationsForPmcLevel(
|
|
Dictionary<ELocationName, List<string>> locations, int pmcLevel)
|
|
{
|
|
var allowedLocation = new Dictionary<ELocationName, List<string>>();
|
|
|
|
foreach (var (location, value) in locations)
|
|
{
|
|
var locationNames = new List<string>();
|
|
foreach (var locationName in value)
|
|
if (IsPmcLevelAllowedOnLocation(locationName, pmcLevel))
|
|
{
|
|
locationNames.Add(locationName);
|
|
}
|
|
|
|
if (locationNames.Count > 0)
|
|
{
|
|
allowedLocation[location] = locationNames;
|
|
}
|
|
}
|
|
|
|
return allowedLocation;
|
|
}
|
|
|
|
/**
|
|
* Return true if the given pmcLevel is allowed on the given location
|
|
* @param location The location name to check
|
|
* @param pmcLevel The level of the pmc
|
|
* @returns True if the given pmc level is allowed to access the given location
|
|
*/
|
|
protected bool IsPmcLevelAllowedOnLocation(string location, int pmcLevel)
|
|
{
|
|
// All PMC levels are allowed for 'any' location requirement
|
|
if (location == ELocationName.any.ToString())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var locationBase = _databaseService.GetLocation(location.ToLower())?.Base;
|
|
if (locationBase is null)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return pmcLevel <= locationBase.RequiredPlayerLevelMax && pmcLevel >= locationBase.RequiredPlayerLevelMin;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get count of repeatable quests profile should have access to
|
|
/// </summary>
|
|
/// <param name="repeatableConfig"></param>
|
|
/// <param name="pmcData">Player profile</param>
|
|
/// <returns>Quest count</returns>
|
|
private int GetQuestCount(RepeatableQuestConfig repeatableConfig, PmcData pmcData)
|
|
{
|
|
var questCount = repeatableConfig.NumQuests.GetValueOrDefault(0);
|
|
if (questCount == 0)
|
|
{
|
|
_logger.Warning($"Repeatable {repeatableConfig.Name} quests have a count of 0");
|
|
}
|
|
|
|
// Add elite bonus to daily quests
|
|
if (repeatableConfig.Name.ToLower() == "daily" &&
|
|
_profileHelper.HasEliteSkillLevel(SkillTypes.Charisma, pmcData)
|
|
)
|
|
{
|
|
// Elite charisma skill gives extra daily quest(s)
|
|
questCount += _databaseService
|
|
.GetGlobals()
|
|
.Configuration
|
|
.SkillsSettings
|
|
.Charisma
|
|
.BonusSettings
|
|
.EliteBonusSettings
|
|
.RepeatableQuestExtraCount
|
|
.GetValueOrDefault(0);
|
|
}
|
|
|
|
return questCount;
|
|
}
|
|
}
|