using NeonSprawl.Server.Game.Factions;
using NeonSprawl.Server.Game.Items;
using NeonSprawl.Server.Game.Mastery;
using NeonSprawl.Server.Game.Rewards;
using NeonSprawl.Server.Game.Skills;
namespace NeonSprawl.Server.Game.Quests;
///
/// NEO-118: best-effort quest objective evaluation on gather/craft/encounter success and
/// snapshot passes (accept / step advance / inventory mutations).
///
public static class QuestObjectiveWiring
{
private const int MaxStepCompletionIterations = 8;
/// Credits objectives after a successful gather grant.
public static void TryProcessGatherSuccess(
string playerId,
string itemId,
int quantityGranted,
IQuestDefinitionRegistry questRegistry,
IPlayerQuestStateStore progressStore,
IPlayerInventoryStore inventoryStore,
IItemDefinitionRegistry itemRegistry,
ISkillDefinitionRegistry skillRegistry,
IPlayerSkillProgressionStore skillProgressionStore,
ISkillLevelCurve levelCurve,
PerkUnlockEngine perkUnlockEngine,
IPlayerPerkStateStore perkStore,
IRewardDeliveryStore deliveryStore,
IFactionStandingStore standingStore,
IReputationDeltaStore auditStore,
TimeProvider timeProvider,
ILogger? logger = null)
{
if (quantityGranted <= 0 || itemId.Trim().Length == 0)
{
return;
}
try
{
var normalizedItemId = itemId.Trim();
foreach (var definition in questRegistry.GetDefinitionsInIdOrder())
{
if (!TryGetActiveProgress(playerId, definition.Id, progressStore, out var progress))
{
continue;
}
ApplyGatherItemObjectives(
playerId,
definition,
progress,
normalizedItemId,
quantityGranted,
progressStore);
}
RefreshInventoryHasItemCountersForPlayer(
playerId,
questRegistry,
progressStore,
inventoryStore,
itemRegistry);
TryCompleteAllActiveQuests(
playerId,
questRegistry,
progressStore,
inventoryStore,
itemRegistry,
skillRegistry,
skillProgressionStore,
levelCurve,
perkUnlockEngine,
perkStore,
deliveryStore,
standingStore,
auditStore,
timeProvider);
}
catch (Exception ex)
{
if (logger?.IsEnabled(LogLevel.Debug) is true)
{
logger.LogDebug(ex, "Quest gather wiring skipped for player {PlayerId}", playerId);
}
}
}
/// Credits objectives after a successful craft.
public static void TryProcessCraftSuccess(
string playerId,
string recipeId,
int batchQuantity,
IQuestDefinitionRegistry questRegistry,
IPlayerQuestStateStore progressStore,
IPlayerInventoryStore inventoryStore,
IItemDefinitionRegistry itemRegistry,
ISkillDefinitionRegistry skillRegistry,
IPlayerSkillProgressionStore skillProgressionStore,
ISkillLevelCurve levelCurve,
PerkUnlockEngine perkUnlockEngine,
IPlayerPerkStateStore perkStore,
IRewardDeliveryStore deliveryStore,
IFactionStandingStore standingStore,
IReputationDeltaStore auditStore,
TimeProvider timeProvider,
ILogger? logger = null)
{
if (batchQuantity <= 0 || recipeId.Trim().Length == 0)
{
return;
}
try
{
var normalizedRecipeId = recipeId.Trim();
foreach (var definition in questRegistry.GetDefinitionsInIdOrder())
{
if (!TryGetActiveProgress(playerId, definition.Id, progressStore, out var progress))
{
continue;
}
ApplyCraftRecipeObjectives(
playerId,
definition,
progress,
normalizedRecipeId,
batchQuantity,
progressStore);
}
RefreshInventoryHasItemCountersForPlayer(
playerId,
questRegistry,
progressStore,
inventoryStore,
itemRegistry);
TryCompleteAllActiveQuests(
playerId,
questRegistry,
progressStore,
inventoryStore,
itemRegistry,
skillRegistry,
skillProgressionStore,
levelCurve,
perkUnlockEngine,
perkStore,
deliveryStore,
standingStore,
auditStore,
timeProvider);
}
catch (Exception ex)
{
if (logger?.IsEnabled(LogLevel.Debug) is true)
{
logger.LogDebug(ex, "Quest craft wiring skipped for player {PlayerId}", playerId);
}
}
}
/// Credits objectives after encounter completion.
public static void TryProcessEncounterCompleteSuccess(
string playerId,
string encounterId,
IQuestDefinitionRegistry questRegistry,
IPlayerQuestStateStore progressStore,
IPlayerInventoryStore inventoryStore,
IItemDefinitionRegistry itemRegistry,
ISkillDefinitionRegistry skillRegistry,
IPlayerSkillProgressionStore skillProgressionStore,
ISkillLevelCurve levelCurve,
PerkUnlockEngine perkUnlockEngine,
IPlayerPerkStateStore perkStore,
IRewardDeliveryStore deliveryStore,
IFactionStandingStore standingStore,
IReputationDeltaStore auditStore,
TimeProvider timeProvider,
ILogger? logger = null)
{
if (encounterId.Trim().Length == 0)
{
return;
}
try
{
var normalizedEncounterId = encounterId.Trim();
foreach (var definition in questRegistry.GetDefinitionsInIdOrder())
{
if (!TryGetActiveProgress(playerId, definition.Id, progressStore, out var progress))
{
continue;
}
ApplyEncounterCompleteObjectives(
playerId,
definition,
progress,
normalizedEncounterId,
progressStore);
}
RefreshInventoryHasItemCountersForPlayer(
playerId,
questRegistry,
progressStore,
inventoryStore,
itemRegistry);
TryCompleteAllActiveQuests(
playerId,
questRegistry,
progressStore,
inventoryStore,
itemRegistry,
skillRegistry,
skillProgressionStore,
levelCurve,
perkUnlockEngine,
perkStore,
deliveryStore,
standingStore,
auditStore,
timeProvider);
}
catch (Exception ex)
{
if (logger?.IsEnabled(LogLevel.Debug) is true)
{
logger.LogDebug(ex, "Quest encounter wiring skipped for player {PlayerId}", playerId);
}
}
}
/// Re-evaluates counters for every active quest.
public static void RefreshInventoryHasItemCountersForPlayer(
string playerId,
IQuestDefinitionRegistry questRegistry,
IPlayerQuestStateStore progressStore,
IPlayerInventoryStore inventoryStore,
IItemDefinitionRegistry itemRegistry)
{
foreach (var definition in questRegistry.GetDefinitionsInIdOrder())
{
TryProcessInventoryHasItemForQuest(
playerId,
definition.Id,
questRegistry,
progressStore,
inventoryStore,
itemRegistry);
}
}
///
/// GET polling hook (NEO-119): refresh counters and
/// auto-advance/complete active quests before a progress snapshot is returned.
///
public static void TryRefreshInventoryProgressForPlayer(
string playerId,
IQuestDefinitionRegistry questRegistry,
IPlayerQuestStateStore progressStore,
IPlayerInventoryStore inventoryStore,
IItemDefinitionRegistry itemRegistry,
ISkillDefinitionRegistry skillRegistry,
IPlayerSkillProgressionStore skillProgressionStore,
ISkillLevelCurve levelCurve,
PerkUnlockEngine perkUnlockEngine,
IPlayerPerkStateStore perkStore,
IRewardDeliveryStore deliveryStore,
IFactionStandingStore standingStore,
IReputationDeltaStore auditStore,
TimeProvider timeProvider,
ILogger? logger = null)
{
try
{
RefreshInventoryHasItemCountersForPlayer(
playerId,
questRegistry,
progressStore,
inventoryStore,
itemRegistry);
TryCompleteAllActiveQuests(
playerId,
questRegistry,
progressStore,
inventoryStore,
itemRegistry,
skillRegistry,
skillProgressionStore,
levelCurve,
perkUnlockEngine,
perkStore,
deliveryStore,
standingStore,
auditStore,
timeProvider);
}
catch (Exception ex)
{
if (logger?.IsEnabled(LogLevel.Debug) is true)
{
logger.LogDebug(ex, "Quest progress GET refresh skipped for player {PlayerId}", playerId);
}
}
}
/// Re-evaluates on the active step for one quest.
public static void TryProcessInventoryHasItemForQuest(
string playerId,
string questId,
IQuestDefinitionRegistry questRegistry,
IPlayerQuestStateStore progressStore,
IPlayerInventoryStore inventoryStore,
IItemDefinitionRegistry itemRegistry)
{
if (!TryGetActiveProgress(playerId, questId, progressStore, out var progress) ||
!questRegistry.TryGetDefinition(questId, out var definition) ||
!inventoryStore.TryGetSnapshot(playerId, out var inventory))
{
return;
}
var stepIndex = progress.CurrentStepIndex;
if (stepIndex < 0 || stepIndex >= definition.Steps.Count)
{
return;
}
foreach (var objective in definition.Steps[stepIndex].Objectives)
{
if (!string.Equals(objective.Kind, QuestObjectiveKinds.InventoryHasItem, StringComparison.Ordinal) ||
objective.ItemId is null)
{
continue;
}
var required = GetRequiredQuantity(objective);
var held = PlayerInventoryOperations.GetHeldQuantity(inventory, objective.ItemId, itemRegistry);
var newCount = Math.Min(held, required);
var current = GetCounter(progress, objective.Id);
if (newCount == current)
{
continue;
}
progressStore.TryUpdateObjectiveCounter(playerId, questId, objective.Id, newCount, out _);
}
}
/// Advances or completes the quest when the active step's objectives are satisfied.
public static void TryCompleteStepIfSatisfied(
string playerId,
string questId,
IQuestDefinitionRegistry questRegistry,
IPlayerQuestStateStore progressStore,
IPlayerInventoryStore inventoryStore,
IItemDefinitionRegistry itemRegistry,
ISkillDefinitionRegistry skillRegistry,
IPlayerSkillProgressionStore skillProgressionStore,
ISkillLevelCurve levelCurve,
PerkUnlockEngine perkUnlockEngine,
IPlayerPerkStateStore perkStore,
IRewardDeliveryStore deliveryStore,
IFactionStandingStore standingStore,
IReputationDeltaStore auditStore,
TimeProvider timeProvider)
{
for (var iteration = 0; iteration < MaxStepCompletionIterations; iteration++)
{
if (!TryGetActiveProgress(playerId, questId, progressStore, out var progress) ||
!questRegistry.TryGetDefinition(questId, out var definition))
{
return;
}
var stepIndex = progress.CurrentStepIndex;
if (stepIndex < 0 || stepIndex >= definition.Steps.Count)
{
return;
}
inventoryStore.TryGetSnapshot(playerId, out var inventory);
var step = definition.Steps[stepIndex];
if (!IsStepSatisfied(step, progress.ObjectiveCounters, inventory, itemRegistry))
{
return;
}
if (stepIndex >= definition.Steps.Count - 1)
{
QuestStateOperations.TryMarkComplete(
playerId,
questId,
questRegistry,
progressStore,
itemRegistry,
inventoryStore,
skillRegistry,
skillProgressionStore,
levelCurve,
perkUnlockEngine,
perkStore,
deliveryStore,
standingStore,
auditStore,
timeProvider);
return;
}
var advance = QuestStateOperations.TryAdvanceStep(
playerId,
questId,
stepIndex + 1,
questRegistry,
progressStore,
inventoryStore,
itemRegistry,
skillRegistry,
skillProgressionStore,
levelCurve,
perkUnlockEngine,
perkStore,
deliveryStore,
standingStore,
auditStore,
timeProvider);
if (!advance.Success)
{
return;
}
}
}
private static void TryCompleteAllActiveQuests(
string playerId,
IQuestDefinitionRegistry questRegistry,
IPlayerQuestStateStore progressStore,
IPlayerInventoryStore inventoryStore,
IItemDefinitionRegistry itemRegistry,
ISkillDefinitionRegistry skillRegistry,
IPlayerSkillProgressionStore skillProgressionStore,
ISkillLevelCurve levelCurve,
PerkUnlockEngine perkUnlockEngine,
IPlayerPerkStateStore perkStore,
IRewardDeliveryStore deliveryStore,
IFactionStandingStore standingStore,
IReputationDeltaStore auditStore,
TimeProvider timeProvider)
{
foreach (var definition in questRegistry.GetDefinitionsInIdOrder())
{
if (TryGetActiveProgress(playerId, definition.Id, progressStore, out _))
{
TryCompleteStepIfSatisfied(
playerId,
definition.Id,
questRegistry,
progressStore,
inventoryStore,
itemRegistry,
skillRegistry,
skillProgressionStore,
levelCurve,
perkUnlockEngine,
perkStore,
deliveryStore,
standingStore,
auditStore,
timeProvider);
}
}
}
private static void ApplyGatherItemObjectives(
string playerId,
QuestDefRow definition,
QuestStepState progress,
string itemId,
int quantityGranted,
IPlayerQuestStateStore progressStore)
{
var stepIndex = progress.CurrentStepIndex;
if (stepIndex < 0 || stepIndex >= definition.Steps.Count)
{
return;
}
foreach (var objective in definition.Steps[stepIndex].Objectives)
{
if (!string.Equals(objective.Kind, QuestObjectiveKinds.GatherItem, StringComparison.Ordinal) ||
objective.ItemId is null ||
!string.Equals(objective.ItemId, itemId, StringComparison.Ordinal))
{
continue;
}
var required = GetRequiredQuantity(objective);
var current = GetCounter(progress, objective.Id);
var newCount = Math.Min(current + quantityGranted, required);
if (newCount == current)
{
continue;
}
progressStore.TryUpdateObjectiveCounter(playerId, definition.Id, objective.Id, newCount, out _);
progressStore.TryGetProgress(playerId, definition.Id, out progress);
}
}
private static void ApplyCraftRecipeObjectives(
string playerId,
QuestDefRow definition,
QuestStepState progress,
string recipeId,
int batchQuantity,
IPlayerQuestStateStore progressStore)
{
var stepIndex = progress.CurrentStepIndex;
if (stepIndex < 0 || stepIndex >= definition.Steps.Count)
{
return;
}
foreach (var objective in definition.Steps[stepIndex].Objectives)
{
if (!string.Equals(objective.Kind, QuestObjectiveKinds.CraftRecipe, StringComparison.Ordinal) ||
objective.RecipeId is null ||
!string.Equals(objective.RecipeId, recipeId, StringComparison.Ordinal))
{
continue;
}
var required = GetRequiredQuantity(objective);
var current = GetCounter(progress, objective.Id);
var newCount = Math.Min(current + batchQuantity, required);
if (newCount == current)
{
continue;
}
progressStore.TryUpdateObjectiveCounter(playerId, definition.Id, objective.Id, newCount, out _);
progressStore.TryGetProgress(playerId, definition.Id, out progress);
}
}
private static void ApplyEncounterCompleteObjectives(
string playerId,
QuestDefRow definition,
QuestStepState progress,
string encounterId,
IPlayerQuestStateStore progressStore)
{
var stepIndex = progress.CurrentStepIndex;
if (stepIndex < 0 || stepIndex >= definition.Steps.Count)
{
return;
}
foreach (var objective in definition.Steps[stepIndex].Objectives)
{
if (!string.Equals(objective.Kind, QuestObjectiveKinds.EncounterComplete, StringComparison.Ordinal) ||
objective.EncounterId is null ||
!string.Equals(objective.EncounterId, encounterId, StringComparison.Ordinal))
{
continue;
}
if (GetCounter(progress, objective.Id) >= 1)
{
continue;
}
progressStore.TryUpdateObjectiveCounter(playerId, definition.Id, objective.Id, 1, out _);
progressStore.TryGetProgress(playerId, definition.Id, out progress);
}
}
private static bool IsStepSatisfied(
QuestStepDefRow step,
IReadOnlyDictionary counters,
PlayerInventorySnapshot inventory,
IItemDefinitionRegistry itemRegistry)
{
foreach (var objective in step.Objectives)
{
var required = GetRequiredQuantity(objective);
if (string.Equals(objective.Kind, QuestObjectiveKinds.InventoryHasItem, StringComparison.Ordinal))
{
if (objective.ItemId is null)
{
return false;
}
var held = PlayerInventoryOperations.GetHeldQuantity(inventory, objective.ItemId, itemRegistry);
if (held < required)
{
return false;
}
continue;
}
if (GetCounter(counters, objective.Id) < required)
{
return false;
}
}
return step.Objectives.Count > 0;
}
private static bool TryGetActiveProgress(
string playerId,
string questId,
IPlayerQuestStateStore progressStore,
out QuestStepState progress) =>
progressStore.TryGetProgress(playerId, questId, out progress!) &&
progress.Status == QuestProgressStatus.Active;
private static int GetRequiredQuantity(QuestObjectiveDefRow objective) =>
objective.Quantity is > 0 ? objective.Quantity.Value : 1;
private static int GetCounter(QuestStepState progress, string objectiveId) =>
GetCounter(progress.ObjectiveCounters, objectiveId);
private static int GetCounter(IReadOnlyDictionary counters, string objectiveId) =>
counters.TryGetValue(objectiveId, out var count) ? count : 0;
}