From 63dc9936143d64e88fbcb6a4e03d66b92ff3dcdb Mon Sep 17 00:00:00 2001 From: Kei-Luna Date: Tue, 26 May 2026 09:02:03 +0900 Subject: [PATCH] IBLogic --- Common/Data/Excel/IbGoodsExcel.cs | 76 ++++ Common/Data/GameData.cs | 1 + .../CallGS/Handlers/Shop/IBLogic_BuyGoods.cs | 325 ++++++++++++++++++ .../Handlers/Shop/IBLogic_GoodsRedDot.cs | 71 ++++ 4 files changed, 473 insertions(+) create mode 100644 Common/Data/Excel/IbGoodsExcel.cs create mode 100644 GameServer/Server/CallGS/Handlers/Shop/IBLogic_BuyGoods.cs create mode 100644 GameServer/Server/CallGS/Handlers/Shop/IBLogic_GoodsRedDot.cs diff --git a/Common/Data/Excel/IbGoodsExcel.cs b/Common/Data/Excel/IbGoodsExcel.cs new file mode 100644 index 0000000..c7bbfd4 --- /dev/null +++ b/Common/Data/Excel/IbGoodsExcel.cs @@ -0,0 +1,76 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace MikuSB.Data.Excel; + +[ResourceEntity("purchase/ibgoods.json")] +public class IbGoodsExcel : ExcelResource +{ + [JsonProperty("GoodsId")] private JToken? GoodsIdRaw { get; set; } + [JsonProperty("Type")] private JToken? TypeRaw { get; set; } + [JsonProperty("PreId")] private JToken? PreIdRaw { get; set; } + [JsonProperty("LimitTimes")] private JToken? LimitTimesRaw { get; set; } + [JsonProperty("Item")] private JToken? ItemRaw { get; set; } + [JsonProperty("Cost")] private JToken? CostRaw { get; set; } + [JsonProperty("Cost2")] private JToken? Cost2Raw { get; set; } + [JsonProperty("PcId")] public string PcId { get; set; } = ""; + [JsonProperty("IosId")] public string IosId { get; set; } = ""; + [JsonProperty("AndroidId")] public string AndroidId { get; set; } = ""; + + public override uint GetId() => GoodsId; + + public override void Loaded() + { + GameData.IbGoodsData[GoodsId] = this; + } + + [JsonIgnore] + public uint GoodsId => ReadUInt(GoodsIdRaw); + + [JsonIgnore] + public int Type => (int)ReadUInt(TypeRaw); + + [JsonIgnore] + public uint PreId => ReadUInt(PreIdRaw); + + [JsonIgnore] + public uint LimitTimes => ReadUInt(LimitTimesRaw); + + [JsonIgnore] + public List Item => ReadUIntList(ItemRaw); + + [JsonIgnore] + public List Cost => ReadUIntList(CostRaw); + + [JsonIgnore] + public List Cost2 => ReadUIntList(Cost2Raw); + + public string GetProductId() => + !string.IsNullOrWhiteSpace(PcId) ? PcId : + !string.IsNullOrWhiteSpace(AndroidId) ? AndroidId : + IosId; + + private static uint ReadUInt(JToken? token) + { + if (token == null || token.Type is JTokenType.Null or JTokenType.Undefined) + return 0; + + if (token.Type == JTokenType.Integer) + return token.Value(); + + if (token.Type == JTokenType.String && uint.TryParse(token.Value(), out var value)) + return value; + + return 0; + } + + private static List ReadUIntList(JToken? token) + { + if (token is not JArray array) + return []; + + return array + .Select(entry => entry.Type == JTokenType.Integer ? entry.Value() : 0u) + .ToList(); + } +} diff --git a/Common/Data/GameData.cs b/Common/Data/GameData.cs index 64b5a43..c189f62 100644 --- a/Common/Data/GameData.cs +++ b/Common/Data/GameData.cs @@ -61,6 +61,7 @@ public static class GameData public static Dictionary DreamCardActivityData { get; private set; } = []; public static Dictionary DlcActivityData { get; private set; } = []; public static Dictionary BattlePassTimeData { get; private set; } = []; + public static Dictionary IbGoodsData { get; private set; } = []; } public static class GameResourceTemplateId diff --git a/GameServer/Server/CallGS/Handlers/Shop/IBLogic_BuyGoods.cs b/GameServer/Server/CallGS/Handlers/Shop/IBLogic_BuyGoods.cs new file mode 100644 index 0000000..d9c25d9 --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Shop/IBLogic_BuyGoods.cs @@ -0,0 +1,325 @@ +using MikuSB.Data; +using MikuSB.Data.Excel; +using MikuSB.Database; +using MikuSB.Database.Inventory; +using MikuSB.Database.Player; +using MikuSB.Enums.Item; +using MikuSB.GameServer.Game.Player; +using MikuSB.Proto; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Shop; + +[CallGSApi("IBLogic_BuyGoods")] +public class IBLogic_BuyGoods : ICallGSHandler +{ + private const uint BuyGroupId = 26; + private const uint RedGroupId = 113; + + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var req = JsonSerializer.Deserialize(param); + var player = connection.Player!; + if (req == null || + req.GoodsId == 0 || + req.Count == 0 || + !GameData.IbGoodsData.TryGetValue(req.GoodsId, out var goods)) + { + await CallGSRouter.SendScript(connection, "IBLogic_BuyGoods", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + if (goods.LimitTimes > 0) + { + var buyAttr = GetOrCreateAttr(player, BuyGroupId, req.GoodsId); + if (buyAttr.Val >= goods.LimitTimes) + { + await CallGSRouter.SendScript(connection, "IBLogic_BuyGoods", "{\"sErr\":\"tip.Mall_Limit_Buy\"}"); + return; + } + } + + var rewardItems = BuildRewardItems(goods, req); + if (rewardItems.Count == 0) + { + await CallGSRouter.SendScript(connection, "IBLogic_BuyGoods", "{\"sErr\":\"error.BadParam\"}"); + return; + } + + var sync = new NtfSyncPlayer(); + foreach (var reward in rewardItems) + await GrantRewardAsync(player, sync, reward); + + var buyCountAttr = GetOrCreateAttr(player, BuyGroupId, req.GoodsId); + buyCountAttr.Val += req.Count; + SyncAttr(player, sync, buyCountAttr); + + var redAttr = GetOrCreateAttr(player, RedGroupId, req.GoodsId); + if (redAttr.Val == 0) + { + redAttr.Val = 1; + SyncAttr(player, sync, redAttr); + } + + DatabaseHelper.SaveDatabaseType(player.Data); + DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData); + DatabaseHelper.SaveDatabaseType(player.CharacterManager.CharacterData); + + var responseGoods = new JsonArray(); + foreach (var reward in rewardItems) + { + var row = new JsonArray(); + foreach (var value in reward) + row.Add((int)value); + responseGoods.Add(row); + } + + var rsp = new JsonObject + { + ["nGoodsId"] = (int)req.GoodsId, + ["tbGoods"] = responseGoods + }; + + var productId = goods.GetProductId(); + if (!string.IsNullOrWhiteSpace(productId)) + rsp["sProductId"] = productId; + + var cost = req.Index == 2 ? goods.Cost2 : goods.Cost; + if (cost.Count >= 2) + rsp["nTotalPrice"] = (int)cost[1]; + + await CallGSRouter.SendScript(connection, "IBLogic_BuyGoods", rsp.ToJsonString(), sync); + } + + private static List> BuildRewardItems(IbGoodsExcel goods, IbBuyGoodsParam req) + { + var rewards = new List>(); + + if (goods.Item.Count >= 4) + rewards.Add(WithCount(goods.Item, req.Count)); + + if (req.SelectItem1?.Count >= 4) + rewards.Add(WithCount(req.SelectItem1, req.Count)); + + if (req.SelectItem2?.Count >= 4) + rewards.Add(WithCount(req.SelectItem2, req.Count)); + + return rewards; + } + + private static List WithCount(IReadOnlyList item, uint buyCount) + { + var reward = item.Take(5).ToList(); + while (reward.Count < 5) + reward.Add(1); + + reward[4] = Math.Max(1u, reward[4]) * Math.Max(1u, buyCount); + return reward; + } + + private static async Task GrantRewardAsync(PlayerInstance player, NtfSyncPlayer sync, IReadOnlyList reward) + { + if (reward.Count < 5) + return; + + var itemType = (ItemTypeEnum)reward[0]; + var detail = reward[1]; + var particular = reward[2]; + var level = reward[3]; + var count = Math.Max(1u, reward[4]); + + switch (itemType) + { + case ItemTypeEnum.TYPE_CARD: + for (var i = 0u; i < count; i++) + { + var character = await player.CharacterManager.AddCharacter(itemType, detail, particular, level, sendPacket: false); + if (character != null) + sync.Items.Add(character.ToProto()); + } + break; + case ItemTypeEnum.TYPE_WEAPON: + for (var i = 0u; i < count; i++) + { + var weapon = await player.InventoryManager.AddWeaponItem(itemType, detail, particular, level, sendPacket: false); + if (weapon != null) + sync.Items.Add(weapon.ToProto()); + } + break; + case ItemTypeEnum.TYPE_SUPPORT: + for (var i = 0u; i < count; i++) + { + var support = await player.InventoryManager.AddSupportCardItem(detail, particular, level, sendPacket: false); + if (support != null) + sync.Items.Add(support.ToProto()); + } + break; + case ItemTypeEnum.TYPE_SUPPLIES: + { + var templateId = (uint)GameResourceTemplateId.FromGdpl(reward[0], detail, particular, level); + if (!GameData.SuppliesData.TryGetValue(templateId, out var supplies)) + break; + + var item = await player.InventoryManager.AddSuppliesItem(supplies, count, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + break; + } + case ItemTypeEnum.TYPE_USEABLE: + { + var item = AddOtherItem(player.InventoryManager.InventoryData, reward[0], detail, particular, level, count); + if (item != null) + sync.Items.Add(item.ToProto()); + break; + } + case ItemTypeEnum.TYPE_WEAPON_PART: + for (var i = 0u; i < count; i++) + { + var item = await player.InventoryManager.AddWeaponPartItem(itemType, detail, particular, level, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + case ItemTypeEnum.TYPE_CARD_SKIN: + for (var i = 0u; i < count; i++) + { + var item = await player.InventoryManager.AddSkinItem(itemType, detail, particular, level, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + case ItemTypeEnum.TYPE_HOUSE: + for (var i = 0u; i < count; i++) + { + var item = await player.InventoryManager.AddHouseFurnitureItem(itemType, detail, particular, level, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + case ItemTypeEnum.TYPE_PROFILE: + case ItemTypeEnum.TYPE_FRAME: + case ItemTypeEnum.TYPE_BADGE: + case ItemTypeEnum.TYPE_COVER: + case ItemTypeEnum.TYPE_NAMECARD: + case ItemTypeEnum.TYPE_EXPRESSION: + case ItemTypeEnum.TYPE_BUBBLE: + case ItemTypeEnum.TYPE_ANALYST: + for (var i = 0u; i < count; i++) + { + var item = await player.InventoryManager.AddProfileItem(itemType, detail, particular, level, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + case ItemTypeEnum.TYPE_WEAPON_SKIN: + for (var i = 0u; i < count; i++) + { + var item = await player.InventoryManager.AddWeaponSkinItem(itemType, detail, particular, level, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + case ItemTypeEnum.TYPE_MANIFESTATION: + for (var i = 0u; i < count; i++) + { + var item = await player.InventoryManager.AddManifestationItem(itemType, detail, particular, level, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + case ItemTypeEnum.TYPE_CARD_SKIN_PART: + for (var i = 0u; i < count; i++) + { + var item = await player.InventoryManager.AddSkinPartItem(itemType, detail, particular, level, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + case ItemTypeEnum.TYPE_AR: + for (var i = 0u; i < count; i++) + { + var item = await player.InventoryManager.AddArItem(itemType, detail, particular, level, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + case ItemTypeEnum.TYPE_CALL: + for (var i = 0u; i < count; i++) + { + var item = await player.InventoryManager.AddCallItem(itemType, detail, particular, level, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + } + } + + private static BaseGameItemInfo? AddOtherItem(InventoryData inventory, uint genre, uint detail, uint particular, uint level, uint count) + { + var templateId = (uint)GameResourceTemplateId.FromGdpl(genre, detail, particular, level); + if (!GameData.OtherItemData.TryGetValue(templateId, out var otherItem)) + return null; + + var maxCount = otherItem.GMnum > 0 ? otherItem.GMnum : 99999u; + var existing = inventory.Items.Values.FirstOrDefault(x => x.TemplateId == templateId); + if (existing != null) + { + existing.ItemCount = Math.Min(existing.ItemCount + count, maxCount); + return existing; + } + + var item = new BaseGameItemInfo + { + TemplateId = templateId, + UniqueId = inventory.NextUniqueUid++, + ItemType = ItemTypeEnum.TYPE_USEABLE, + ItemCount = Math.Min(count, maxCount) + }; + inventory.Items[item.UniqueId] = item; + return item; + } + + private static PlayerAttr GetOrCreateAttr(PlayerInstance player, uint gid, uint sid) + { + var attr = player.Data.Attrs.FirstOrDefault(x => x.Gid == gid && x.Sid == sid); + if (attr != null) + return attr; + + attr = new PlayerAttr + { + Gid = gid, + Sid = sid + }; + player.Data.Attrs.Add(attr); + return attr; + } + + private static void SyncAttr(PlayerInstance player, NtfSyncPlayer sync, PlayerAttr attr) + { + sync.Custom[player.ToPackedAttrKey(attr.Gid, attr.Sid)] = attr.Val; + sync.Custom[player.ToShiftedAttrKey(attr.Gid, attr.Sid)] = attr.Val; + } +} + +internal sealed class IbBuyGoodsParam +{ + [JsonPropertyName("nType")] + public int Type { get; set; } + + [JsonPropertyName("nGoodsId")] + public uint GoodsId { get; set; } + + [JsonPropertyName("nCount")] + public uint Count { get; set; } + + [JsonPropertyName("nIndex")] + public int Index { get; set; } + + [JsonPropertyName("tbSelectItem1")] + public List? SelectItem1 { get; set; } + + [JsonPropertyName("tbSelectItem2")] + public List? SelectItem2 { get; set; } +} diff --git a/GameServer/Server/CallGS/Handlers/Shop/IBLogic_GoodsRedDot.cs b/GameServer/Server/CallGS/Handlers/Shop/IBLogic_GoodsRedDot.cs new file mode 100644 index 0000000..8c2bea3 --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Shop/IBLogic_GoodsRedDot.cs @@ -0,0 +1,71 @@ +using MikuSB.Database; +using MikuSB.Database.Player; +using MikuSB.GameServer.Game.Player; +using MikuSB.Proto; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Shop; + +[CallGSApi("IBLogic_GoodsRedDot")] +public class IBLogic_GoodsRedDot : ICallGSHandler +{ + private const uint RedGroupId = 113; + + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var req = JsonSerializer.Deserialize(param); + if (req?.GoodsIds == null || req.GoodsIds.Count == 0) + { + await CallGSRouter.SendScript(connection, "IBLogic_GoodsRedDot", "null"); + return; + } + + var player = connection.Player!; + var sync = new NtfSyncPlayer(); + var changed = false; + + foreach (var goodsId in req.GoodsIds.Where(x => x > 0).Distinct()) + { + var attr = GetOrCreateAttr(player, RedGroupId, goodsId); + if (attr.Val > 0) + continue; + + attr.Val = 1; + SyncAttr(player, sync, attr); + changed = true; + } + + if (changed) + DatabaseHelper.SaveDatabaseType(player.Data); + + await CallGSRouter.SendScript(connection, "IBLogic_GoodsRedDot", "null", sync); + } + + private static PlayerAttr GetOrCreateAttr(PlayerInstance player, uint gid, uint sid) + { + var attr = player.Data.Attrs.FirstOrDefault(x => x.Gid == gid && x.Sid == sid); + if (attr != null) + return attr; + + attr = new PlayerAttr + { + Gid = gid, + Sid = sid + }; + player.Data.Attrs.Add(attr); + return attr; + } + + private static void SyncAttr(PlayerInstance player, NtfSyncPlayer sync, PlayerAttr attr) + { + sync.Custom[player.ToPackedAttrKey(attr.Gid, attr.Sid)] = attr.Val; + sync.Custom[player.ToShiftedAttrKey(attr.Gid, attr.Sid)] = attr.Val; + } +} + +internal sealed class IbGoodsRedDotParam +{ + [JsonPropertyName("tbList")] + public List GoodsIds { get; set; } = []; +}