using Yuanshen.Common.Data.Excel; using Yuanshen.Gameserver.Controllers.Attributes; using Yuanshen.Gameserver.Controllers.Result; using Yuanshen.Gameserver.Database; using Yuanshen.Gameserver.Game; using Yuanshen.Gameserver.Network.Session; using Yuanshen.Protocol; using Microsoft.Extensions.Logging; namespace Yuanshen.Gameserver.Controllers; [NetController] internal sealed class GachaController : ControllerBase { private const uint MaxConstellation = 6; private const uint StarglitterItemId = 221; private static readonly Random Random = new(); [NetCommand(CmdType.GetGachaInfoReq)] public ValueTask OnGetGachaInfoReq(GachaBannerTable banners) { GetGachaInfoRsp rsp = new() { Retcode = Retcode.RET_SUCC, GachaRandom = (uint)Random.Next(1, int.MaxValue) }; foreach (GachaBanner banner in banners.Banners) rsp.GachaInfoList.Add(banner.Info.Clone()); return ValueTask.FromResult(Response(CmdType.GetGachaInfoRsp, rsp)); } [NetCommand(CmdType.DoGachaReq)] public async ValueTask OnDoGachaReq(NetSession session, Player player, SceneManager scene, PlayerPersistence persistence, GachaBannerTable banners, ExcelTableCollection excel, ILogger logger) { DoGachaReq req = Packet!.DecodeBody(); GachaBanner? banner = banners.Find(req.GachaType, req.GachaScheduleId); if (banner is null) return Response(CmdType.DoGachaRsp, new DoGachaRsp { Retcode = Retcode.RET_GACHA_SCHEDULE_NOT_MATCH }); uint times = req.GachaTimes == 10 ? 10u : 1u; uint costItemId = banner.Info.CostItemId; uint costItemNum = times == 10 ? banner.Info.TenCostItemNum : banner.Info.CostItemNum; if (player.Materials.GetValueOrDefault(costItemId) < costItemNum) { logger.LogInformation("Gacha refused: need {num}x {item}", costItemNum, costItemId); return Response(CmdType.DoGachaRsp, new DoGachaRsp { Retcode = Retcode.RET_GACHA_COST_ITEM_NOT_ENOUGH }); } player.TakeMaterial(costItemId, costItemNum); StoreItemChangeNotify store = new() { StoreType = StoreType.StorePack }; store.ItemList.Add(BuildMaterial(player, costItemId)); List added = new(); List upgraded = new(); DoGachaRsp rsp = new() { Retcode = Retcode.RET_SUCC, GachaType = req.GachaType, GachaTimes = times, GachaScheduleId = req.GachaScheduleId, NewGachaRandom = (uint)Random.Next(1, int.MaxValue), CostItemId = costItemId, CostItemNum = banner.Info.CostItemNum, TenCostItemId = banner.Info.TenCostItemId, TenCostItemNum = banner.Info.TenCostItemNum, LeftGachaTimes = banner.Info.LeftGachaTimes, GachaTimesLimit = banner.Info.GachaTimesLimit }; for (uint pull = 0; pull < times; pull++) { uint itemId = Draw(banner, player); bool isNew = await AwardAsync(player, excel, itemId, store, added, upgraded); (string label, string kind) = Describe(excel, itemId); player.GachaLog.Add(new GachaLogEntry { GachaType = req.GachaType, ItemId = itemId, RankLevel = RankOf(banner, itemId), Name = label, Kind = kind, Time = DateTimeOffset.UtcNow.ToUnixTimeSeconds() }); rsp.GachaItemList.Add(new GachaItem { GachaItem_ = new ItemParam { ItemId = itemId, Count = 1 }, IsGachaItemNew = isNew }); } await session.NotifyAsync(CmdType.StoreItemChangeNotify, store); foreach (GameAvatar avatar in added) await session.NotifyAsync(CmdType.AvatarAddNotify, new AvatarAddNotify { Avatar = avatar.AsAvatarInfo(), IsInTeam = false }); if (added.Count != 0 || upgraded.Count != 0) await scene.RefreshAvatarDataAsync(); if (upgraded.Count != 0) await scene.RefreshTeamAsync(); await persistence.SaveAsync(); logger.LogInformation("Gacha {type} x{times} -> {items}", req.GachaType, times, string.Join(", ", rsp.GachaItemList.Select(item => item.GachaItem_.ItemId))); return Response(CmdType.DoGachaRsp, rsp); } private static uint Draw(GachaBanner banner, Player player) { uint gachaType = banner.Info.GachaType; uint counter = player.GachaPity.GetValueOrDefault(gachaType) + 1; bool forced = banner.PityCount != 0 && counter >= banner.PityCount; GachaBucket bucket = forced ? banner.Buckets.FirstOrDefault(candidate => banner.PityTypes.Contains(candidate.Type)) ?? PickBucket(banner) : PickBucket(banner); player.GachaPity[gachaType] = banner.PityTypes.Contains(bucket.Type) ? 0 : counter; if (!banner.FeaturedPools.TryGetValue(bucket.Type, out IReadOnlyList? featured) || featured.Count == 0) return bucket.ItemIds[Random.Next(bucket.ItemIds.Count)]; bool carriesGuarantee = banner.GuaranteeTypes.Contains(bucket.Type); bool takeFeatured = (carriesGuarantee && player.GachaGuarantee.GetValueOrDefault(gachaType) != 0) || Random.Next(100) < banner.FeaturedRate; if (carriesGuarantee) player.GachaGuarantee[gachaType] = takeFeatured ? 0u : 1u; return takeFeatured ? featured[Random.Next(featured.Count)] : bucket.ItemIds[Random.Next(bucket.ItemIds.Count)]; } private static (string Label, string Kind) Describe(ExcelTableCollection excel, uint itemId) { WeaponExcel? weapon = excel.GetTable(ExcelType.Weapon).Items.OfType() .FirstOrDefault(candidate => candidate.Id == itemId); if (weapon is not null) return (FriendlyWeapon(weapon.WeaponType), "weapon"); MaterialExcel? material = excel.GetTable(ExcelType.Material).Items.OfType() .FirstOrDefault(candidate => candidate.Id == itemId); uint avatarId = material?.GainedAvatarId ?? 0; if (avatarId == 0) return ($"Item {itemId}", "item"); AvatarExcel? avatar = excel.GetTable(ExcelType.Avatar).Items.OfType() .FirstOrDefault(candidate => candidate.Id == avatarId); string icon = avatar?.IconName ?? string.Empty; int cut = icon.LastIndexOf('_'); string name = cut >= 0 && cut + 1 < icon.Length ? icon[(cut + 1)..] : $"Avatar {avatarId}"; return (name, "avatar"); } private static string FriendlyWeapon(string weaponType) => weaponType switch { "WEAPON_SWORD_ONE_HAND" => "Sword", "WEAPON_CLAYMORE" => "Claymore", "WEAPON_POLE" => "Polearm", "WEAPON_CATALYST" => "Catalyst", "WEAPON_BOW" => "Bow", _ => "Weapon" }; private static uint RankOf(GachaBanner banner, uint itemId) { foreach (GachaBucket bucket in banner.Buckets) { if (bucket.ItemIds.Contains(itemId)) return bucket.Type.EndsWith("S5") ? 5u : bucket.Type.EndsWith("S4") ? 4u : 3u; } foreach ((string type, IReadOnlyList pool) in banner.FeaturedPools) { if (pool.Contains(itemId)) return type.EndsWith("S5") ? 5u : type.EndsWith("S4") ? 4u : 3u; } return 3; } private static GachaBucket PickBucket(GachaBanner banner) { uint roll = (uint)Random.Next(10000); uint accumulated = 0; foreach (GachaBucket bucket in banner.Buckets) { accumulated += bucket.Weight; if (roll < accumulated) return bucket; } return banner.Buckets[^1]; } private static async ValueTask AwardAsync(Player player, ExcelTableCollection excel, uint itemId, StoreItemChangeNotify store, List added, List upgraded) { bool isWeapon = excel.GetTable(ExcelType.Weapon).Items.OfType().Any(weapon => weapon.Id == itemId); if (isWeapon) { store.ItemList.Add(player.CreateWeapon(itemId).AsItem()); return await ValueTask.FromResult(true); } MaterialExcel? material = excel.GetTable(ExcelType.Material).Items.OfType() .FirstOrDefault(candidate => candidate.Id == itemId); uint avatarId = material?.GainedAvatarId ?? 0; if (avatarId == 0) { player.GiveMaterial(itemId, 1); store.ItemList.Add(BuildMaterial(player, itemId)); return await ValueTask.FromResult(false); } GameAvatar? owned = player.Avatars.FirstOrDefault(avatar => avatar.AvatarId == avatarId); if (owned is null) { GameAvatar? granted = player.GrantAvatar(avatarId); if (granted is not null) { added.Add(granted); return await ValueTask.FromResult(true); } player.GiveMaterial(itemId, 1); store.ItemList.Add(BuildMaterial(player, itemId)); return await ValueTask.FromResult(false); } if (owned.Constellation < MaxConstellation) { owned.Constellation++; upgraded.Add(owned); return await ValueTask.FromResult(false); } AvatarExcel? avatarExcel = excel.GetTable(ExcelType.Avatar).Items.OfType() .FirstOrDefault(candidate => candidate.Id == avatarId); uint starglitter = avatarExcel?.QualityType == "QUALITY_ORANGE" ? 25u : 5u; player.GiveMaterial(StarglitterItemId, starglitter); store.ItemList.Add(BuildMaterial(player, StarglitterItemId)); return await ValueTask.FromResult(false); } private static Item BuildMaterial(Player player, uint itemId) => new() { ItemId = itemId, Guid = player.MaterialGuid(itemId), Material = new Material { Count = player.Materials.GetValueOrDefault(itemId) } }; }