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