using System.Text.Json; using Microsoft.EntityFrameworkCore; using DorkNet.Server.Data; using DorkNet.Server.Data.Entities; namespace DorkNet.Server.Services; /// /// Per-quest reward chest contents. The end-of-quest gift box /// (RRO quest / Stunt Runner) is the existing gifts/generate + /// gifts/consume flow: the client asks the server to generate a /// gift, the box spawns showing it, and opening it grants the item. /// /// The generate request carries no quest id (only GiftContext / /// IsGameGift / Message), so the quest is identified by the room the /// player is in — . This /// service maps that room id to the quest's item pool /// (data/quest_rewards.json) and resolves one to a real store /// item to award. When a room has no configured pool the caller falls /// back to the generic random-wardrobe gift. /// public sealed class QuestRewardService(DorkNetDbContext db) { private sealed class QuestRewardFile { public Dictionary> rooms { get; set; } = new(); } private static readonly Lazy>> _map = new(Load); /// Room id → candidate item slugs for that quest's chest. public static IReadOnlyDictionary> Map => _map.Value; public static bool HasRewardsForRoom(long roomId) => Map.TryGetValue(roomId, out var slugs) && slugs.Count > 0; /// Resolve one active store item for the quest chest in /// , chosen deterministically from /// so repeated generate calls in the same /// context are stable. Returns null when the room has no configured /// pool or none of its slugs resolve to an active item. public async Task PickForRoomAsync(long roomId, int seed) { if (!Map.TryGetValue(roomId, out var slugs) || slugs.Count == 0) return null; var items = await db.StoreItems .Where(i => i.IsActive && slugs.Contains(i.Slug)) .ToListAsync(); if (items.Count == 0) return null; // Order deterministically by (id, seed) hash so the same room + // seed always yields the same pick without persisting state. return items .OrderBy(i => HashCode.Combine(i.Id, seed)) .First(); } private static Dictionary> Load() { var candidates = new[] { Path.Combine(AppContext.BaseDirectory, "data", "quest_rewards.json"), Path.Combine(AppContext.BaseDirectory, "Data", "quest_rewards.json"), Path.Combine(Directory.GetCurrentDirectory(), "data", "quest_rewards.json"), Path.Combine(Directory.GetCurrentDirectory(), "DorkNet.Server", "Data", "quest_rewards.json"), }; var path = candidates.FirstOrDefault(File.Exists); if (path is null) return new(); try { using var fs = File.OpenRead(path); var data = JsonSerializer.Deserialize(fs, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new QuestRewardFile(); var map = new Dictionary>(); foreach (var (key, slugs) in data.rooms) { if (!long.TryParse(key, out var roomId)) continue; var clean = (slugs ?? new()) .Where(s => !string.IsNullOrWhiteSpace(s)) .Select(s => s.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); if (clean.Count > 0) map[roomId] = clean; } return map; } catch { return new(); } } }