Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/src/i18n/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -814,10 +814,11 @@ const en = {
voteRewardsPollIntervalHint: 'How often the background sweep checks this provider for new votes. 5 min is a good default.',
voteRewardsRewardType: 'Reward type',
voteRewardsRewardPoints: 'Points',
voteRewardsRewardVipGift: 'VIP Gift (not yet implemented)',
voteRewardsRewardVipGift: 'VIP Gift',
voteRewardsRewardCdKey: 'CD Key (not yet implemented)',
voteRewardsPointsAmount: 'Points to grant per vote',
voteRewardsVipGiftTemplate: 'VIP gift template name',
voteRewardsVipGiftTemplateHint: 'The Name of an existing VIP gift whose player_id is the sentinel "_template_". Create it via the VIP Gifts admin tab — its items + commands will be cloned for each voter, who claims them with /vip on next login.',
voteRewardsCdKeyTemplate: 'CD key template ID',
voteRewardsBroadcastTemplate: 'In-game broadcast template',
voteRewardsBroadcastHint: 'Sent to the whole server when an online player\'s vote is granted. Tokens: {player}, {reward}. Empty = silent.',
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/i18n/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -818,10 +818,11 @@ const ja: Messages = {
voteRewardsPollIntervalHint: 'How often the background sweep checks this provider for new votes. 5 min is a good default.',
voteRewardsRewardType: 'Reward type',
voteRewardsRewardPoints: 'Points',
voteRewardsRewardVipGift: 'VIP Gift (not yet implemented)',
voteRewardsRewardVipGift: 'VIP Gift',
voteRewardsRewardCdKey: 'CD Key (not yet implemented)',
voteRewardsPointsAmount: 'Points to grant per vote',
voteRewardsVipGiftTemplate: 'VIP gift template name',
voteRewardsVipGiftTemplateHint: 'The Name of an existing VIP gift whose player_id is the sentinel "_template_". Create it via the VIP Gifts admin tab — its items + commands will be cloned for each voter, who claims them with /vip on next login.',
voteRewardsCdKeyTemplate: 'CD key template ID',
voteRewardsBroadcastTemplate: 'In-game broadcast template',
voteRewardsBroadcastHint: 'Sent to the whole server when an online player\'s vote is granted. Tokens: {player}, {reward}. Empty = silent.',
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/i18n/locales/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -818,10 +818,11 @@ const ko: Messages = {
voteRewardsPollIntervalHint: 'How often the background sweep checks this provider for new votes. 5 min is a good default.',
voteRewardsRewardType: 'Reward type',
voteRewardsRewardPoints: 'Points',
voteRewardsRewardVipGift: 'VIP Gift (not yet implemented)',
voteRewardsRewardVipGift: 'VIP Gift',
voteRewardsRewardCdKey: 'CD Key (not yet implemented)',
voteRewardsPointsAmount: 'Points to grant per vote',
voteRewardsVipGiftTemplate: 'VIP gift template name',
voteRewardsVipGiftTemplateHint: 'The Name of an existing VIP gift whose player_id is the sentinel "_template_". Create it via the VIP Gifts admin tab — its items + commands will be cloned for each voter, who claims them with /vip on next login.',
voteRewardsCdKeyTemplate: 'CD key template ID',
voteRewardsBroadcastTemplate: 'In-game broadcast template',
voteRewardsBroadcastHint: 'Sent to the whole server when an online player\'s vote is granted. Tokens: {player}, {reward}. Empty = silent.',
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/i18n/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -818,10 +818,11 @@ const zhCN: Messages = {
voteRewardsPollIntervalHint: 'How often the background sweep checks this provider for new votes. 5 min is a good default.',
voteRewardsRewardType: 'Reward type',
voteRewardsRewardPoints: 'Points',
voteRewardsRewardVipGift: 'VIP Gift (not yet implemented)',
voteRewardsRewardVipGift: 'VIP Gift',
voteRewardsRewardCdKey: 'CD Key (not yet implemented)',
voteRewardsPointsAmount: 'Points to grant per vote',
voteRewardsVipGiftTemplate: 'VIP gift template name',
voteRewardsVipGiftTemplateHint: 'The Name of an existing VIP gift whose player_id is the sentinel "_template_". Create it via the VIP Gifts admin tab — its items + commands will be cloned for each voter, who claims them with /vip on next login.',
voteRewardsCdKeyTemplate: 'CD key template ID',
voteRewardsBroadcastTemplate: 'In-game broadcast template',
voteRewardsBroadcastHint: 'Sent to the whole server when an online player\'s vote is granted. Tokens: {player}, {reward}. Empty = silent.',
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/i18n/locales/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -818,10 +818,11 @@ const zhTW: Messages = {
voteRewardsPollIntervalHint: 'How often the background sweep checks this provider for new votes. 5 min is a good default.',
voteRewardsRewardType: 'Reward type',
voteRewardsRewardPoints: 'Points',
voteRewardsRewardVipGift: 'VIP Gift (not yet implemented)',
voteRewardsRewardVipGift: 'VIP Gift',
voteRewardsRewardCdKey: 'CD Key (not yet implemented)',
voteRewardsPointsAmount: 'Points to grant per vote',
voteRewardsVipGiftTemplate: 'VIP gift template name',
voteRewardsVipGiftTemplateHint: 'The Name of an existing VIP gift whose player_id is the sentinel "_template_". Create it via the VIP Gifts admin tab — its items + commands will be cloned for each voter, who claims them with /vip on next login.',
voteRewardsCdKeyTemplate: 'CD key template ID',
voteRewardsBroadcastTemplate: 'In-game broadcast template',
voteRewardsBroadcastHint: 'Sent to the whole server when an online player\'s vote is granted. Tokens: {player}, {reward}. Empty = silent.',
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/views/SettingsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1374,8 +1374,8 @@ onMounted(() => {

<div class="form-group" v-if="provider.rewardType === 'vip_gift'">
<label class="form-label">{{ t('settings.voteRewardsVipGiftTemplate') }}</label>
<InputText v-model="voteRewardsSettings.providers[idx].vipGiftTemplateName" class="form-input" disabled />
<small class="settings-hint">{{ t('settings.voteRewardsRewardVipGift') }}</small>
<InputText v-model="voteRewardsSettings.providers[idx].vipGiftTemplateName" class="form-input" />
<small class="settings-hint">{{ t('settings.voteRewardsVipGiftTemplateHint') }}</small>
</div>

<div class="form-group" v-if="provider.rewardType === 'cd_key'">
Expand Down
35 changes: 35 additions & 0 deletions src/KitsuneCommand/Data/Repositories/VipGiftRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,26 @@ public interface IVipGiftRepository
void SetGiftItems(int giftId, IEnumerable<int> itemDefinitionIds);
void SetGiftCommands(int giftId, IEnumerable<int> commandDefinitionIds);

// Templates: a "template" gift is a vip_gift row with the sentinel
// player_id "_template_". Admins create one via the regular VIP Gifts
// admin UI by typing "_template_" as the player_id; the VoteRewards
// feature looks them up by name and clones them for voters.
VipGift GetTemplateByName(string name);

// Claiming
void MarkAsClaimed(int giftId);
}

/// <summary>
/// The sentinel player_id used to mark a vip_gift row as a template that
/// other features (e.g. VoteRewards) can clone from. Underscore-prefixed
/// to avoid colliding with any real Steam_/Epic_ cross-platform id.
/// </summary>
public static class VipGiftSentinels
{
public const string TemplatePlayerId = "_template_";
}

public class VipGiftRepository : IVipGiftRepository
{
private readonly DbConnectionFactory _db;
Expand Down Expand Up @@ -170,6 +186,25 @@ public void SetGiftCommands(int giftId, IEnumerable<int> commandDefinitionIds)
}
}

// ─── Templates ────────────────────────────────────────────

/// <summary>
/// Returns the first template gift matching the given name. If multiple
/// rows share the same name + sentinel player_id (admin error / accident),
/// the lowest id wins so behavior is deterministic across requests.
/// Returns null if no template with that name exists.
/// </summary>
public VipGift GetTemplateByName(string name)
{
using var conn = _db.CreateConnection();
return conn.QueryFirstOrDefault<VipGift>(@"
SELECT * FROM vip_gifts
WHERE player_id = @TemplateId AND name = @Name
ORDER BY id ASC
LIMIT 1",
new { TemplateId = VipGiftSentinels.TemplatePlayerId, Name = name });
}

// ─── Claiming ────────────────────────────────────────────

public void MarkAsClaimed(int giftId)
Expand Down
62 changes: 53 additions & 9 deletions src/KitsuneCommand/Features/VoteRewardsFeature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
using KitsuneCommand.Data.Repositories;
using KitsuneCommand.Features.VoteRewards;
using KitsuneCommand.Features.VoteRewards.Providers;
// VipGift template-clone is deferred to v1.5; the dispatch case currently throws
// NotImplementedException, so we don't carry the entity import yet.
using Newtonsoft.Json;

namespace KitsuneCommand.Features
Expand All @@ -35,6 +33,7 @@ public class VoteRewardsFeature : FeatureBase<VoteRewardsSettings>
private readonly ISettingsRepository _settingsRepo;
private readonly IVoteGrantRepository _grantRepo;
private readonly IPointsRepository _pointsRepo;
private readonly IVipGiftRepository _vipGiftRepo;
private readonly LivePlayerManager _playerManager;
private const string SettingsKey = "VoteRewards";

Expand All @@ -55,12 +54,14 @@ public VoteRewardsFeature(
ISettingsRepository settingsRepo,
IVoteGrantRepository grantRepo,
IPointsRepository pointsRepo,
IVipGiftRepository vipGiftRepo,
LivePlayerManager playerManager)
: base(eventBus, config)
{
_settingsRepo = settingsRepo;
_grantRepo = grantRepo;
_pointsRepo = pointsRepo;
_vipGiftRepo = vipGiftRepo;
_playerManager = playerManager;
}

Expand Down Expand Up @@ -366,13 +367,56 @@ private string DispatchReward(string steamId, string playerName, VoteProviderSet
}

case VoteRewardType.VipGift:
// Reserved for v1.5 — needs a template-clone path: look up the
// template VIP gift by name, insert a copy for this voter, and
// mirror its item + command junctions into the new row. The
// shape and storage are ready; the clone wiring is pending.
// Admins selecting this today get an explicit error rather
// than a silent no-op.
throw new NotImplementedException("VIP-gift vote rewards are not yet wired up. Use Points for now.");
{
// Clone a pre-built template gift for this voter. The admin
// creates the template via the regular VIP Gifts admin UI
// by typing the sentinel player_id "_template_" — that keeps
// it out of any real player's pending-gift list, but lets
// VoteRewards find it by name and copy it.
if (string.IsNullOrWhiteSpace(cfg.VipGiftTemplateName))
{
throw new InvalidOperationException(
"VipGiftTemplateName is empty — set the template name in the Vote Rewards settings.");
}

var template = _vipGiftRepo.GetTemplateByName(cfg.VipGiftTemplateName);
if (template == null)
{
throw new InvalidOperationException(
$"No VIP gift template named '{cfg.VipGiftTemplateName}' (player_id = '{VipGiftSentinels.TemplatePlayerId}'). " +
$"Create it via the VIP Gifts admin UI first.");
}

// Read the template's items + commands BEFORE insert, so a
// partial failure doesn't leave a half-built gift in the
// voter's pending list (the items would be missing).
var templateItems = _vipGiftRepo.GetItemsForGift(template.Id).Select(i => i.Id).ToList();
var templateCommands = _vipGiftRepo.GetCommandsForGift(template.Id).Select(c => c.Id).ToList();

var voterGift = new VipGift
{
PlayerId = playerId,
PlayerName = resolvedName,
Name = template.Name,
Description = string.IsNullOrWhiteSpace(template.Description)
? $"Vote reward from {cfg.Key}"
: template.Description,
// One-time gift — claim_period stays null. If a server
// wants repeatable vote rewards, that's a per-vote
// grant, not a per-claim period.
ClaimPeriod = null,
};
var newId = _vipGiftRepo.Insert(voterGift);
_vipGiftRepo.SetGiftItems(newId, templateItems);
_vipGiftRepo.SetGiftCommands(newId, templateCommands);

var itemCount = templateItems.Count;
var commandCount = templateCommands.Count;
var summary = itemCount == 0 && commandCount == 0
? "(empty template — fix the template before next vote)"
: $"{itemCount} item(s) + {commandCount} command(s)";
return $"VIP gift '{template.Name}' [{summary}] (claim with /vip)";
}

case VoteRewardType.CdKey:
// Reserved — needs CdKey template lookup wiring. Leaving the
Expand Down
Loading