Skip to content

Add Vote Rewards feature with 7daystodie-servers.com provider#46

Merged
AdaInTheLab merged 1 commit into
mainfrom
feature/vote-rewards
Apr 30, 2026
Merged

Add Vote Rewards feature with 7daystodie-servers.com provider#46
AdaInTheLab merged 1 commit into
mainfrom
feature/vote-rewards

Conversation

@AdaInTheLab
Copy link
Copy Markdown
Collaborator

Summary

Players who vote for the server at listing sites (7daystodie-servers.com, gtop100, etc.) earn an in-game reward. This PR ships the full backend with one provider — adding more is now a one-class change.

  • Pluggable provider abstraction — `IVoteSiteProvider` exposes 3 methods (`ListRecentVoters`, `GetClaimStatus`, `MarkClaimed`). `SevenDtdServersProvider` is the v1 adapter (~150 lines).
  • Background sweep — 60s timer ticks; per-provider `PollIntervalMinutes` (default 5) gates the actual API calls. Fire-and-forget on a Task so the timer thread never blocks on network I/O.
  • /vote chat command — on-demand claim for the calling player, off the game thread, replies via `pm` once the round-trip completes. Cooldown gated.
  • Idempotency — `vote_grants` table is unique on `(provider, steam_id, vote_date)`. Both the sweep and `/vote` insert FIRST, grant second; on UNIQUE-violation we skip without granting. Two sweeps racing or a sweep + a `/vote` racing cannot double-grant.
  • REST endpoints at `/api/voterewards/{settings,grants,grants/count}` for the upcoming Vue settings tab.

v1 reward type

Only Points dispatch is wired (uses existing `PointsRepository.AdjustPoints`). `VipGift` and `CdKey` throw `NotImplementedException` with a clear message — the template-clone wiring for VIP-gift rewards is the natural next iteration. Defaulting providers to disabled means upgrades won't accidentally start granting points before an admin has reviewed the config.

Files

File Purpose
`Config/Migrations/009_vote_grants.sql` Audit / idempotency table
`Data/Entities/VoteGrant.cs` Entity
`Data/Repositories/VoteGrantRepository.cs` TryInsert (UNIQUE-aware), GetRecent, GetForPlayer
`Features/VoteRewardsSettings.cs` Top-level + per-provider POCOs
`Features/VoteRewards/IVoteSiteProvider.cs` Provider abstraction
`Features/VoteRewards/Providers/SevenDtdServersProvider.cs` v1 adapter
`Features/VoteRewardsFeature.cs` Sweep loop + dispatch + claim primitive
`Features/ChatCommandService.cs` `/vote` command
`Features/ChatCommandSettings.cs` `VoteEnabled`, `VoteCooldownSeconds`
`Web/Controllers/VoteRewardsController.cs` REST surface

DI is convention-based — `*Repository` types and `IFeature` types register automatically, so no `ServiceRegistry` edits.

Test plan

  • Boot KC, confirm migration `009_vote_grants.sql` applies and feature reports `VoteRewards feature enabled` in the log
  • PUT `/api/voterewards/settings` with master enabled + 7daystodie-servers configured (real API key, RewardType=points, PointsAmount=100)
  • Vote on 7daystodie-servers.com from a logged-in Steam account
  • Within ~5 min, the sweep grants 100 points; `vote_grants` row recorded
  • `/points` in chat shows the new balance
  • Voting again the same day is a no-op (provider returns `claimed`)
  • `/vote` chat command works on-demand and reports per-provider results
  • Two simultaneous `/vote` calls (or sweep + `/vote`) don't double-grant — vote_grants UNIQUE catches it
  • BroadcastTemplate fires `say` when the voter is online
  • Disabling the master toggle stops sweeps without restart

🤖 Generated with Claude Code

Players who vote for the server at listing sites earn an in-game reward.
v1 ships the backend end-to-end with a single provider:

- 7daystodie-servers.com adapter behind a pluggable IVoteSiteProvider
  interface (gtop100, top-7daystodieservers slot in next as one class each)
- Background sweep timer polls each enabled provider on its configured
  cadence (default 5 min) and grants any unclaimed votes
- /vote chat command for on-demand claims (off-game-thread, replies via
  pm once the network round-trip completes)
- vote_grants audit/idempotency table — unique on (provider, steam_id,
  vote_date) so racing sweeps and /vote commands can't double-grant
- REST endpoints under /api/voterewards for settings + audit log
  (frontend Settings tab is a follow-up)

v1 reward type: Points (uses existing PointsRepository.AdjustPoints).
VipGift and CdKey throw NotImplementedException with a clear message —
template-clone wiring is the next iteration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AdaInTheLab AdaInTheLab merged commit 1132252 into main Apr 30, 2026
2 checks passed
@AdaInTheLab AdaInTheLab deleted the feature/vote-rewards branch April 30, 2026 14:55
AdaInTheLab added a commit that referenced this pull request Apr 30, 2026
UI surface for the VoteRewardsFeature backend that landed in #46:

- New "Vote Rewards" tab in SettingsView (admin-only, value="9")
- Master enable toggle + per-provider config block (enabled, API key,
  server ID, poll interval, reward type, points amount, broadcast template)
- Recent grants audit table at the bottom (datetime, provider, player,
  steam id, reward type+value)
- VipGift and CdKey reward types are present as Select options but their
  associated input fields are disabled with a "(not yet implemented)" hint
  so admins know choosing them won't grant — backend throws
  NotImplementedException for these until the v1.5 dispatch lands

Wired up:
- frontend/src/api/voterewards.ts — REST client
- VoteRewardsSettings, VoteProviderSettings, VoteGrant types
- ChatCommandSettings extended with voteEnabled / voteCooldownSeconds
  to match the C# settings POCO
- en.ts has the canonical strings; ja/ko/zh-CN/zh-TW carry English
  placeholders with a "translations TBD" comment so vue-tsc's structural
  check passes — native translations are a follow-up

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AdaInTheLab added a commit that referenced this pull request Apr 30, 2026
Bug introduced (in spirit) by #46 and made very visible by #48: when
DispatchReward throws AFTER the audit/idempotency row was inserted, the
player is permanently locked out of ever claiming that vote. Future
sweeps see HasGrantForDate=true and short-circuit before retrying, even
though no reward was ever actually granted.

This always could have happened (e.g. SQLite glitch on AdjustPoints),
but #48's VipGift dispatch made it likely — a misspelled template name
in the Vote Rewards settings would silently lock out every voter on
that provider.

Fix: narrow try/catch around the DispatchReward call. On exception,
delete the audit row via the new IVoteGrantRepository.DeleteByKey so
the next sweep can retry once the admin fixes the misconfiguration.
The outer try/catch (network errors during GetClaimStatus / MarkClaimed)
is unaffected — those failure modes don't leave an orphaned row, so
they don't need rollback.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AdaInTheLab AdaInTheLab mentioned this pull request Apr 30, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant