using Microsoft.EntityFrameworkCore; using RadicadeServer.API.Storefront; using RadicadeServer.DB; using RadicadeServer.Extensions; using RadicadeServer.RecNet.Accounts.Local; using RadicadeServer.RecNet.Avatar; using RadicadeServer.RecNet.Enums.Store; using RadicadeServer.RecNet.Rooms; namespace RadicadeServer.Utils { public static class RoomJoinGiftUtils { private const string ClaimKeyPrefix = "Radicade.RoomJoinGift."; /// /// Whether a room's own Rooms row describes a box at all. A room can hold several boxes in /// RoomJoinGifts; this answers for the single one that row still carries, which is /// the whole story for a room saved before that table existed. /// public static bool HasConfiguredReward(RoomData room) { return room.JoinGiftCurrencyAmount > 0 || room.JoinGiftXp > 0 || !string.IsNullOrWhiteSpace(room.JoinGiftAvatarItem) || !string.IsNullOrWhiteSpace(room.JoinGiftEquipmentItem) || !string.IsNullOrWhiteSpace(room.JoinGiftConsumableItem); } public static bool HasConfiguredReward(RoomJoinGift gift) { return gift.CurrencyAmount > 0 || gift.Xp > 0 || !string.IsNullOrWhiteSpace(gift.AvatarItem) || !string.IsNullOrWhiteSpace(gift.EquipmentItem) || !string.IsNullOrWhiteSpace(gift.ConsumableItem); } /// /// The single box a room's own row describes, as a gift row. Used for a room that has no /// rows in RoomJoinGifts: it is the same box, read from where it has always lived. /// public static RoomJoinGift Snapshot(RoomData room) { return new RoomJoinGift { RoomJoinGiftId = 0, RoomId = room.RoomId, SortOrder = 0, Version = room.JoinGiftVersion, Enabled = room.JoinGiftEnabled, OncePerPlayer = room.JoinGiftOncePerPlayer, Title = room.JoinGiftTitle, Message = room.JoinGiftMessage, AvatarItem = room.JoinGiftAvatarItem, AvatarItemType = room.JoinGiftAvatarItemType, EquipmentItem = room.JoinGiftEquipmentItem, EquipmentModificationGuid = room.JoinGiftEquipmentModificationGuid, ConsumableItem = room.JoinGiftConsumableItem, CurrencyType = room.JoinGiftCurrencyType, CurrencyAmount = room.JoinGiftCurrencyAmount, Xp = room.JoinGiftXp, Context = room.JoinGiftContext, Rarity = room.JoinGiftRarity, FromPlayerId = room.JoinGiftFromPlayerId }; } /// The boxes a room hands out, in the order the panel lists them. public static async Task> ConfiguredGiftsAsync(RecDBContext ctx, long roomId) { return await ctx.RoomJoinGifts .Where(x => x.RoomId == roomId) .OrderBy(x => x.SortOrder) .ThenBy(x => x.RoomJoinGiftId) .ToListAsync(); } /// /// Hands every box this room is configured to give to the player. A room with several /// boxes gives several; a box the player has already claimed under its current version is /// skipped, and each box is claimed on its own so changing one does not silence the rest. /// public static async Task TryDeliverAsync(RecDBContext ctx, long playerId, RoomData room) { // The room's own switch stays the master: a room that is not handing anything out // hands out nothing, whichever boxes its list happens to hold. if (!room.JoinGiftEnabled) { return false; } var configured = await ConfiguredGiftsAsync(ctx, room.RoomId); // A room with no list still has the one box its Rooms row describes. var boxes = configured.Count > 0 ? configured : [Snapshot(room)]; var delivered = false; foreach (var box in boxes) { if (!box.Enabled) { continue; } if (!HasConfiguredReward(box)) { continue; } var claimKey = ClaimKey(room.RoomId, box); if (box.OncePerPlayer && await HasClaimedAsync(ctx, playerId, claimKey)) { continue; } var giftBox = new Gift { GiftDropId = NewGiftDropId(room.RoomId, box.Version, box.RoomJoinGiftId), ItemSetId = 0, ItemSetFriendlyName = room.Name, FriendlyName = string.IsNullOrWhiteSpace(box.Title) ? "Room Visit Gift" : box.Title, Tooltip = box.Message ?? "", IsQuery = false, Unique = false, SubscribersOnly = false, ConsumableItemDesc = box.ConsumableItem ?? "", AvatarItemDesc = box.AvatarItem ?? "", AvatarItemType = box.AvatarItemType, EquipmentPrefabName = box.EquipmentItem ?? "", EquipmentModificationGuid = box.EquipmentModificationGuid ?? "", CurrencyType = box.CurrencyType, Currency = Math.Max(0, box.CurrencyAmount), Xp = Math.Max(0, box.Xp), Context = box.Context, Rarity = box.Rarity, // The box names its sender to the client, which shows it on the box; a room that // names no sender leaves it at zero rather than inventing one. FromPlayerId = Math.Max(0, box.FromPlayerId) }; await StorefrontShared.PersistGiftBoxAsync(ctx, playerId, giftBox); if (box.OncePerPlayer) { await MarkClaimedAsync(ctx, playerId, claimKey); } delivered = true; } return delivered; } public static async Task TryDeliverAsync(RecDBContext ctx, long playerId, long roomId) { var room = await ctx.Rooms.FirstOrDefaultAsync(x => x.RoomId == roomId); if (room == null) { return false; } return await TryDeliverAsync(ctx, playerId, room); } /// /// A gift drop id for one box. Two boxes handed out in the same millisecond must not share /// an id - the player's boxes are a list keyed by it, so the second would overwrite the /// first - so a listed box also mixes in which row it came from. A room's own single box /// keeps the id shape it has always had. /// private static long NewGiftDropId(long roomId, int version, long giftId) { var timePart = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() % 1_000_000_000; var roomPart = Math.Abs(roomId % 100_000); var versionPart = Math.Abs(version % 1_000); if (giftId <= 0) return 9_000_000_000_000 + roomPart * 10_000_000 + versionPart * 10_000 + timePart % 10_000; var giftPart = giftId % 1_000; return 9_000_000_000_000 + roomPart * 1_000_000_000 + giftPart * 1_000_000 + versionPart * 1_000 + timePart % 1_000; } /// /// The key a player's once-per-player claim is remembered under. A room's own single box /// keeps the key it has always used, so this change hands nobody a second copy of a box /// they already claimed; a listed box adds its row and version. /// private static string ClaimKey(long roomId, RoomJoinGift box) { if (box.RoomJoinGiftId <= 0) return $"{ClaimKeyPrefix}{roomId}.{box.Version}"; return $"{ClaimKeyPrefix}{roomId}.{box.RoomJoinGiftId}.{box.Version}"; } private static async Task HasClaimedAsync(RecDBContext ctx, long playerId, string key) { var settings = await ctx.PlayerSettings.FirstOrDefaultAsync(x => x.AccountId == playerId); if (settings == null) return false; return settings.DeserializeSettings().Any(x => x.Key == key && x.Value == "1"); } private static async Task MarkClaimedAsync(RecDBContext ctx, long playerId, string key) { var settings = await ctx.PlayerSettings.FirstOrDefaultAsync(x => x.AccountId == playerId); if (settings == null) { settings = new PlayerSettings { AccountId = playerId }; await ctx.PlayerSettings.AddAsync(settings); } var list = settings.DeserializeSettings(); var existing = list.FirstOrDefault(x => x.Key == key); if (existing == null) list.Add(new Setting { Key = key, Value = "1" }); else existing.Value = "1"; settings.Settings = list.ToJson(); } } }