Implemented ChangeRepeatableQuest

This commit is contained in:
Chomp
2025-01-24 22:05:31 +00:00
parent 6e64f08089
commit d2ac96dcf5
2 changed files with 267 additions and 6 deletions
@@ -1,4 +1,3 @@
using System.Runtime.InteropServices.JavaScript;
using SptCommon.Annotations;
using Core.Helpers;
using Core.Models.Eft.Common;
@@ -3,9 +3,11 @@ using Core.Helpers;
using Core.Models.Eft.Common;
using Core.Models.Eft.Common.Tables;
using Core.Models.Eft.ItemEvent;
using Core.Models.Eft.Profile;
using Core.Models.Eft.Quests;
using Core.Models.Enums;
using Core.Models.Spt.Config;
using Core.Models.Spt.Quests;
using Core.Models.Spt.Repeatable;
using Core.Models.Utils;
using Core.Routers;
@@ -23,7 +25,7 @@ public class RepeatableQuestController(
TimeUtil _timeUtil,
HashUtil _hashUtil,
RandomUtil _randomUtil,
HttpResponseUtil _responseUtil,
HttpResponseUtil _httpResponseUtil,
ProfileHelper _profileHelper,
ProfileFixerService _profileFixerService,
LocalisationService _localisationService,
@@ -39,10 +41,270 @@ public class RepeatableQuestController(
{
protected QuestConfig _questConfig = _configServer.GetConfig<QuestConfig>();
public ItemEventRouterResponse ChangeRepeatableQuest(PmcData pmcData, RepeatableQuestChangeRequest info,
string sessionId)
public ItemEventRouterResponse ChangeRepeatableQuest(PmcData pmcData, RepeatableQuestChangeRequest changeRequest,
string sessionID)
{
throw new NotImplementedException();
var output = _eventOutputHolder.GetOutput(sessionID);
var fullProfile = _profileHelper.GetFullProfile(sessionID);
// Check for existing quest in (daily/weekly/scav arrays)
var repeatables = GetRepeatableById(changeRequest.QuestId, pmcData);
var questToReplace = repeatables.Quest;
var repeatablesOfTypeInProfile = repeatables.RepeatableType;
if (repeatables.RepeatableType is null || repeatables.Quest is null)
{
// Unable to find quest being replaced
var message = _localisationService.GetText("quest-unable_to_find_repeatable_to_replace");
_logger.Error(message);
return _httpResponseUtil.AppendErrorToOutput(output, message);
}
// Subtype name of quest - daily/weekly/scav
var repeatableTypeLower = repeatablesOfTypeInProfile.Name.ToLower();
// Save for later standing loss calculation
var replacedQuestTraderId = questToReplace.TraderId;
// Update active quests to exclude the quest we're replacing
repeatablesOfTypeInProfile.ActiveQuests = repeatablesOfTypeInProfile.ActiveQuests.Where(
quest => quest.Id != changeRequest.QuestId
)
.ToList();
// Save for later cost calculations
var previousChangeRequirement = _cloner.Clone(
repeatablesOfTypeInProfile.ChangeRequirement[changeRequest.QuestId]
);
// Delete the replaced quest change requirement data as we're going to add new data below
repeatablesOfTypeInProfile.ChangeRequirement.Remove(changeRequest.QuestId);
// Get config for this repeatable sub-type (daily/weekly/scav)
var repeatableConfig = _questConfig.RepeatableQuests.FirstOrDefault(
config => config.Name == repeatablesOfTypeInProfile.Name
);
// If the configuration dictates to replace with the same quest type, adjust the available quest types
if (repeatableConfig?.KeepDailyQuestTypeOnReplacement is not null)
{
repeatableConfig.Types = [questToReplace.Type.ToString()];
}
// Generate meta-data for what type/levelrange of quests can be generated for player
var allowedQuestTypes = GenerateQuestPool(repeatableConfig, pmcData.Info.Level);
var newRepeatableQuest = AttemptToGenerateRepeatableQuest(
sessionID,
pmcData,
allowedQuestTypes,
repeatableConfig
);
if (newRepeatableQuest is null)
{
// Unable to find quest being replaced
var message =
$"Unable to generate repeatable quest of type: {repeatableTypeLower} to replace trader: ${replacedQuestTraderId} quest ${changeRequest.QuestId}";
_logger.Error(message);
return _httpResponseUtil.AppendErrorToOutput(output, message);
}
// Add newly generated quest to daily/weekly/scav type array
newRepeatableQuest.Side = repeatableConfig.Side;
repeatablesOfTypeInProfile.ActiveQuests.Add(newRepeatableQuest);
_logger.Debug(
$"Removing: {repeatableConfig.Name} quest: {questToReplace.Id} from trader: {questToReplace.TraderId} as its been replaced"
);
RemoveQuestFromProfile(fullProfile, questToReplace.Id);
// Delete the replaced quest change requirement from profile
CleanUpRepeatableChangeRequirements(repeatablesOfTypeInProfile, questToReplace.Id);
// Add replacement quests change requirement data to profile
repeatablesOfTypeInProfile.ChangeRequirement[newRepeatableQuest.Id] = new ChangeRequirement
{
ChangeCost = newRepeatableQuest.ChangeCost,
ChangeStandingCost = _randomUtil.GetArrayValue([0, 0.01])
};
// Check if we should charge player for replacing quest
var isFreeToReplace = UseFreeRefreshIfAvailable(
fullProfile,
repeatablesOfTypeInProfile,
repeatableTypeLower
);
if (!isFreeToReplace)
{
// Reduce standing with trader for not doing their quest
var traderOfReplacedQuest = pmcData.TradersInfo[replacedQuestTraderId];
traderOfReplacedQuest.Standing -= previousChangeRequirement.ChangeStandingCost;
var charismaBonus = _profileHelper.GetSkillFromProfile(pmcData, SkillTypes.Charisma)?.Progress ?? 0;
foreach (var cost in previousChangeRequirement.ChangeCost)
{
// Not free, Charge player + appy charisma bonus to cost of replacement
cost.Count = Math.Truncate(cost.Count.Value * (1 - Math.Truncate(charismaBonus / 100) * 0.001));
_paymentService.AddPaymentToOutput(pmcData, cost.TemplateId, cost.Count.Value, sessionID, output);
if (output.Warnings.Count > 0)
{
return output;
}
}
}
// Clone data before we send it to client
var repeatableToChangeClone = _cloner.Clone(repeatablesOfTypeInProfile);
// Purge inactive repeatables
repeatableToChangeClone.InactiveQuests = [];
// Nullguard
output.ProfileChanges[sessionID].RepeatableQuests ??= [];
// Update client output with new repeatable
output.ProfileChanges[sessionID].RepeatableQuests.Add(repeatableToChangeClone);
return output;
}
/**
* Some accounts have access to free repeatable quest refreshes
* Track the usage of them inside players profile
* @param fullProfile Player profile
* @param repeatableSubType Can be daily / weekly / scav repeatable
* @param repeatableTypeName Subtype of repeatable quest: daily / weekly / scav
* @returns Is the repeatable being replaced for free
*/
protected bool UseFreeRefreshIfAvailable(SptProfile? fullProfile, PmcDataRepeatableQuest repeatableSubType,
string repeatableTypeName)
{
// No free refreshes, exit early
if (repeatableSubType.FreeChangesAvailable <= 0)
{
// Reset counter to 0
repeatableSubType.FreeChangesAvailable = 0;
return false;
}
// Only certain game versions have access to free refreshes
var hasAccessToFreeRefreshSystem = _profileHelper.HasAccessToRepeatableFreeRefreshSystem(
fullProfile.CharacterData.PmcData
);
// If the player has access and available refreshes:
if (hasAccessToFreeRefreshSystem)
{
// Initialize/retrieve free refresh count for the desired subtype: daily/weekly
fullProfile.SptData.FreeRepeatableRefreshUsedCount ??= new Dictionary<string, int>();
var repeatableRefreshCounts = fullProfile.SptData.FreeRepeatableRefreshUsedCount;
repeatableRefreshCounts.TryAdd(repeatableTypeName, 0); // Set to 0 if undefined
// Increment the used count and decrement the available count.
repeatableRefreshCounts[repeatableTypeName]++;
repeatableSubType.FreeChangesAvailable--;
return true;
}
return false;
}
/**
* Clean up the repeatables `changeRequirement` dictionary of expired data
* @param repeatablesOfTypeInProfile The repeatables that have the replaced and new quest
* @param replacedQuestId Id of the replaced quest
*/
private void CleanUpRepeatableChangeRequirements(PmcDataRepeatableQuest repeatablesOfTypeInProfile,
string replacedQuestId)
{
if (repeatablesOfTypeInProfile.ActiveQuests.Count == 1)
{
// Only one repeatable quest being replaced (e.g. scav_daily), remove everything ready for new quest requirement to be added
// Will assist in cleanup of existing profiles data
repeatablesOfTypeInProfile.ChangeRequirement.Clear();
}
else
{
// Multiple active quests of this type (e.g. daily or weekly) are active, just remove the single replaced quest
repeatablesOfTypeInProfile.ChangeRequirement.Remove(replacedQuestId);
}
}
private RepeatableQuest AttemptToGenerateRepeatableQuest(string sessionId, PmcData pmcData,
QuestTypePool questTypePool, RepeatableQuestConfig repeatableConfig)
{
const int maxAttempts = 10;
RepeatableQuest newRepeatableQuest = null;
var attempts = 0;
while (attempts < maxAttempts && questTypePool.Types.Count > 0)
{
newRepeatableQuest = _repeatableQuestGenerator.GenerateRepeatableQuest(
sessionId,
pmcData.Info.Level.Value,
pmcData.TradersInfo,
questTypePool,
repeatableConfig
);
if (newRepeatableQuest is not null)
{
// Successfully generated a quest, exit loop
break;
}
attempts++;
}
if (attempts > maxAttempts)
{
_logger.Debug("We were stuck in repeatable quest generation. This should never happen. Please report");
}
return newRepeatableQuest;
}
private void RemoveQuestFromProfile(SptProfile? fullProfile, string questToReplaceId)
{
// Find quest we're replacing in pmc profile quests array and remove it
_questHelper.FindAndRemoveQuestFromArrayIfExists(questToReplaceId, fullProfile.CharacterData.PmcData.Quests);
// Find quest we're replacing in scav profile quests array and remove it
if (fullProfile.CharacterData.ScavData is not null)
{
_questHelper.FindAndRemoveQuestFromArrayIfExists(
questToReplaceId,
fullProfile.CharacterData.ScavData.Quests
);
}
}
/**
* Find a repeatable (daily/weekly/scav) from a players profile by its id
* @param questId Id of quest to find
* @param pmcData Profile that contains quests to look through
* @returns IGetRepeatableByIdResult
*/
protected GetRepeatableByIdResult GetRepeatableById(string questId, PmcData pmcData)
{
foreach (var repeatablesInProfile in pmcData.RepeatableQuests)
{
// Check for existing quest in (daily/weekly/scav arrays)
var questToReplace =
repeatablesInProfile.ActiveQuests.FirstOrDefault(repeatable => repeatable.Id == questId);
if (questToReplace is null)
{
// Not found, skip to next repeatable sub-type
continue;
}
return new GetRepeatableByIdResult { Quest = questToReplace, RepeatableType = repeatablesInProfile };
}
return null;
}
public List<PmcDataRepeatableQuest> GetClientRepeatableQuests(string sessionID)
@@ -135,7 +397,7 @@ public class RepeatableQuestController(
fullProfile.SptData.FreeRepeatableRefreshUsedCount[repeatableTypeLower] = 0;
// Create stupid redundant change requirements from quest data
generatedRepeatables.ChangeRequirement = new();
generatedRepeatables.ChangeRequirement = new Dictionary<string, ChangeRequirement>();
foreach (var quest in generatedRepeatables.ActiveQuests)
generatedRepeatables.ChangeRequirement.TryAdd(
quest.Id,