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's name (the seeded RRO slug, e.g. /// GoldenTrophy / Crescendo / StuntRunner) to the /// quest's item pool (data/quest_rewards.json) and resolves one /// to a real store item to award. Keyed by name rather than the numeric /// id because seed ids are array-order-derived and brittle. 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 name (case-insensitive) → candidate item slugs. public static IReadOnlyDictionary> Map => _map.Value; /// Resolve one active store item for the quest chest in the /// room with id , 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.Count == 0) return null; var name = await db.Rooms.Where(r => r.Id == roomId).Select(r => r.Name).FirstOrDefaultAsync(); if (string.IsNullOrWhiteSpace(name) || !Map.TryGetValue(name, 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(StringComparer.OrdinalIgnoreCase); try { using var fs = File.OpenRead(path); var data = JsonSerializer.Deserialize(fs, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new QuestRewardFile(); var map = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (var (key, slugs) in data.rooms) { if (string.IsNullOrWhiteSpace(key) || key.StartsWith('_')) continue; var clean = (slugs ?? new()) .Where(s => !string.IsNullOrWhiteSpace(s)) .Select(s => s.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); if (clean.Count > 0) map[key.Trim()] = clean; } return map; } catch { return new(StringComparer.OrdinalIgnoreCase); } } }