NEO-121: Address review nits for quest telemetry hook sites.

Add schemaVersion to quest_start comment, thread playerId/questId into
DenyAccept, and merge duplicate deny snapshot helpers.
pull/160/head
VinPropane 2026-06-07 12:33:34 -04:00
parent 89affe2b32
commit 88d01778a1
3 changed files with 29 additions and 39 deletions

View File

@ -56,7 +56,7 @@
- **`quest_start`:** after successful **`TryActivate`** in **`TryAccept`**.
- **`quest_step_complete`:** after successful **`TryAdvanceStep`** store commit.
- **`quest_complete`:** after first-time **`TryMarkComplete`** commit (not idempotent replay).
- **`quest_accept_denied`:** **`DenyAccept`** helper on all **`TryAccept`** deny paths.
- **`quest_accept_denied`:** **`DenyAccept(playerId, questId, …)`** on all **`TryAccept`** deny paths; optional accept context on **`DenyFromProgressSnapshot`** for activate-failure rereads.
- **Docs:** `server/README.md` quest telemetry subsection; E7.M1 module + alignment register + E7M1 backlog updated.
- **Manual QA:** `docs/manual-qa/NEO-121.md`.

View File

@ -37,9 +37,9 @@ NEO-121 adds **comment-only** E9.M1 telemetry hook sites to **`QuestStateOperati
## Nits
1. **Nit:** Plan technical approach lists optional **`schemaVersion`** in **`quest_start`** planned payload; the inline comment omits it. Harmless for comment-only work; consider adding for parity when touching the file again.
1. ~~**Nit:** Plan technical approach lists optional **`schemaVersion`** in **`quest_start`** planned payload; the inline comment omits it. Harmless for comment-only work; consider adding for parity when touching the file again.~~ **Done.** Added optional **`schemaVersion`** to **`quest_start`** inline comment.
2. **Nit:** **`DenyFromProgressSnapshotAccept`** duplicates **`DenyFromProgressSnapshot`** except for **`DenyAccept`** vs **`Deny`** — intentional funneling, but E9.M1 wiring may want **`playerId`** / **`questId`** threaded into **`DenyAccept`** (comment already documents those fields; implementer will need caller context).
2. ~~**Nit:** **`DenyFromProgressSnapshotAccept`** duplicates **`DenyFromProgressSnapshot`** except for **`DenyAccept`** vs **`Deny`** — intentional funneling, but E9.M1 wiring may want **`playerId`** / **`questId`** threaded into **`DenyAccept`** (comment already documents those fields; implementer will need caller context).~~ **Done.** Merged into **`DenyFromProgressSnapshot`** with optional accept-deny context; **`DenyAccept`** now takes **`playerId`** and **`questId`** at all call sites.
## Verification

View File

@ -25,22 +25,22 @@ public static class QuestStateOperations
if (!questRegistry.TryNormalizeKnown(questId, out var normalizedQuestId) ||
!questRegistry.TryGetDefinition(normalizedQuestId, out var definition))
{
return DenyAccept(QuestStateReasonCodes.UnknownQuest);
return DenyAccept(playerId, questId, QuestStateReasonCodes.UnknownQuest);
}
if (!progressStore.CanWritePlayer(playerId))
{
return DenyAccept(QuestStateReasonCodes.UnknownPlayer);
return DenyAccept(playerId, normalizedQuestId, QuestStateReasonCodes.UnknownPlayer);
}
if (progressStore.TryGetProgress(playerId, normalizedQuestId, out var existing))
{
if (existing.Status == QuestProgressStatus.Completed)
{
return DenyAccept(QuestStateReasonCodes.AlreadyCompleted, existing);
return DenyAccept(playerId, normalizedQuestId, QuestStateReasonCodes.AlreadyCompleted, existing);
}
return DenyAccept(QuestStateReasonCodes.AlreadyActive, existing);
return DenyAccept(playerId, normalizedQuestId, QuestStateReasonCodes.AlreadyActive, existing);
}
foreach (var prerequisiteId in definition.PrerequisiteQuestIds)
@ -48,7 +48,7 @@ public static class QuestStateOperations
if (!progressStore.TryGetProgress(playerId, prerequisiteId, out var prerequisite) ||
prerequisite.Status != QuestProgressStatus.Completed)
{
return DenyAccept(QuestStateReasonCodes.PrerequisiteIncomplete);
return DenyAccept(playerId, normalizedQuestId, QuestStateReasonCodes.PrerequisiteIncomplete);
}
}
@ -56,7 +56,8 @@ public static class QuestStateOperations
{
// --- Telemetry hook site (NEO-121): future E9.M1 catalog event `quest_start` ---
// TODO(E9.M1): catalog emit — first activation only (deny paths excluded).
// Planned payload fields: playerId, questId, currentStepIndex (0). No ingest or ILogger here (comments-only).
// Planned payload fields: playerId, questId, currentStepIndex (0), optional schemaVersion.
// No ingest or ILogger here (comments-only).
QuestObjectiveWiring.TryProcessInventoryHasItemForQuest(
playerId,
@ -224,11 +225,11 @@ public static class QuestStateOperations
string playerId,
string questId,
QuestStepState attemptSnapshot) =>
DenyFromProgressSnapshotAccept(attemptSnapshot, QuestStateReasonCodes.AlreadyActive)
DenyFromProgressSnapshot(attemptSnapshot, QuestStateReasonCodes.AlreadyActive, playerId, questId)
?? (progressStore.TryGetProgress(playerId, questId, out var reread)
? DenyFromProgressSnapshotAccept(reread, QuestStateReasonCodes.AlreadyActive)
? DenyFromProgressSnapshot(reread, QuestStateReasonCodes.AlreadyActive, playerId, questId)
: null)
?? DenyAccept(QuestStateReasonCodes.UnknownPlayer);
?? DenyAccept(playerId, questId, QuestStateReasonCodes.UnknownPlayer);
private static QuestStateOperationResult DenyAdvanceFailure(
IPlayerQuestStateStore progressStore,
@ -244,7 +245,9 @@ public static class QuestStateOperations
private static QuestStateOperationResult? DenyFromProgressSnapshot(
QuestStepState? snapshot,
string activeOrIntermediateReasonCode)
string activeOrIntermediateReasonCode,
string? acceptDenyPlayerId = null,
string? acceptDenyQuestId = null)
{
if (snapshot is null)
{
@ -253,34 +256,16 @@ public static class QuestStateOperations
if (snapshot.Status == QuestProgressStatus.Completed)
{
return Deny(QuestStateReasonCodes.AlreadyCompleted, snapshot);
return acceptDenyPlayerId is not null
? DenyAccept(acceptDenyPlayerId, acceptDenyQuestId ?? string.Empty, QuestStateReasonCodes.AlreadyCompleted, snapshot)
: Deny(QuestStateReasonCodes.AlreadyCompleted, snapshot);
}
if (snapshot.Status == QuestProgressStatus.Active)
{
return Deny(activeOrIntermediateReasonCode, snapshot);
}
return null;
}
private static QuestStateOperationResult? DenyFromProgressSnapshotAccept(
QuestStepState? snapshot,
string activeOrIntermediateReasonCode)
{
if (snapshot is null)
{
return null;
}
if (snapshot.Status == QuestProgressStatus.Completed)
{
return DenyAccept(QuestStateReasonCodes.AlreadyCompleted, snapshot);
}
if (snapshot.Status == QuestProgressStatus.Active)
{
return DenyAccept(activeOrIntermediateReasonCode, snapshot);
return acceptDenyPlayerId is not null
? DenyAccept(acceptDenyPlayerId, acceptDenyQuestId ?? string.Empty, activeOrIntermediateReasonCode, snapshot)
: Deny(activeOrIntermediateReasonCode, snapshot);
}
return null;
@ -289,11 +274,16 @@ public static class QuestStateOperations
private static QuestStateOperationResult Success(QuestStepState snapshot) =>
new(Success: true, ReasonCode: null, Snapshot: snapshot);
private static QuestStateOperationResult DenyAccept(string reasonCode, QuestStepState? snapshot = null)
private static QuestStateOperationResult DenyAccept(
string playerId,
string questId,
string reasonCode,
QuestStepState? snapshot = null)
{
// --- Telemetry hook site (NEO-121): future E9.M1 catalog event `quest_accept_denied` ---
// TODO(E9.M1): catalog emit — reasonCode from QuestStateReasonCodes (unknown_quest, prerequisite_incomplete, etc.).
// Planned payload fields: playerId, questId, reasonCode. No ingest or ILogger here (comments-only).
// Planned payload fields: playerId, questId, reasonCode (caller context threaded for E9.M1 wiring).
// No ingest or ILogger here (comments-only).
return Deny(reasonCode, snapshot);
}