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);
}
}
}