From bc69a072e1a9dac26b64011c1c929a44761e41b0 Mon Sep 17 00:00:00 2001 From: Kei-Luna Date: Mon, 25 May 2026 12:58:34 +0900 Subject: [PATCH] FishingServer_ConvertFood --- Common/Data/Excel/FishingFoodExcel.cs | 89 +++++++ Common/Data/GameData.cs | 1 + .../Fishing/FishingServer_ConvertFood.cs | 245 ++++++++++++++++++ 3 files changed, 335 insertions(+) create mode 100644 Common/Data/Excel/FishingFoodExcel.cs create mode 100644 GameServer/Server/CallGS/Handlers/Fishing/FishingServer_ConvertFood.cs diff --git a/Common/Data/Excel/FishingFoodExcel.cs b/Common/Data/Excel/FishingFoodExcel.cs new file mode 100644 index 0000000..0f3d7c5 --- /dev/null +++ b/Common/Data/Excel/FishingFoodExcel.cs @@ -0,0 +1,89 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace MikuSB.Data.Excel; + +[ResourceEntity("dlc/fishing/food.json")] +public class FishingFoodExcel : ExcelResource +{ + [JsonProperty("ID")] public uint Id { get; set; } + [JsonProperty("FoodType")] public JToken? FoodTypeRaw { get; set; } + [JsonProperty("NeedItem")] public JToken? NeedItemRaw { get; set; } + [JsonProperty("CreateItems")] public JToken? CreateItemsRaw { get; set; } + [JsonProperty("EffectTime")] public JToken? EffectTimeRaw { get; set; } + [JsonProperty("FishingLevel")] public JToken? FishingLevelRaw { get; set; } + [JsonProperty("SeasonId")] public JToken? SeasonIdRaw { get; set; } + [JsonProperty("BaitNum")] public JToken? BaitNumRaw { get; set; } + [JsonProperty("FoodArea")] public JToken? FoodAreaRaw { get; set; } + + [JsonIgnore] public uint FoodType => ReadUInt(FoodTypeRaw); + [JsonIgnore] public uint EffectTime => ReadUInt(EffectTimeRaw); + [JsonIgnore] public uint FishingLevel => ReadUInt(FishingLevelRaw); + [JsonIgnore] public uint SeasonId => ReadUInt(SeasonIdRaw); + [JsonIgnore] public List> NeedItem => ReadNestedUIntList(NeedItemRaw); + [JsonIgnore] public List CreateItems => ReadUIntList(CreateItemsRaw); + [JsonIgnore] public List BaitNum => ReadUIntList(BaitNumRaw); + [JsonIgnore] public List FoodArea => ReadUIntList(FoodAreaRaw); + + public override uint GetId() => Id; + + public override void Loaded() + { + GameData.FishingFoodData[Id] = this; + } + + private static int ReadInt(JToken? token) + { + if (token == null) + return 0; + + return token.Type switch + { + JTokenType.Integer => token.Value(), + JTokenType.Float => (int)token.Value(), + JTokenType.String when int.TryParse(token.Value(), out var value) => value, + _ => 0 + }; + } + + private static uint ReadUInt(JToken? token) + { + var value = ReadInt(token); + return value > 0 ? (uint)value : 0; + } + + private static List ReadUIntList(JToken? token) + { + if (token is not JArray array) + return []; + + var result = new List(array.Count); + foreach (var item in array) + { + var value = ReadUInt(item); + if (value > 0) + result.Add(value); + } + + return result; + } + + private static List> ReadNestedUIntList(JToken? token) + { + if (token is not JArray array) + return []; + + var result = new List>(array.Count); + foreach (var row in array.OfType()) + { + var values = new List(row.Count); + foreach (var item in row) + { + values.Add(ReadUInt(item)); + } + result.Add(values); + } + + return result; + } +} diff --git a/Common/Data/GameData.cs b/Common/Data/GameData.cs index f9780e5..196d768 100644 --- a/Common/Data/GameData.cs +++ b/Common/Data/GameData.cs @@ -56,6 +56,7 @@ public static class GameData public static Dictionary VirCaptureCaptureRegionData { get; private set; } = []; public static Dictionary VirCaptureLevelListData { get; private set; } = []; public static Dictionary MonsterCardData { get; private set; } = []; + public static Dictionary FishingFoodData { get; private set; } = []; } public static class GameResourceTemplateId diff --git a/GameServer/Server/CallGS/Handlers/Fishing/FishingServer_ConvertFood.cs b/GameServer/Server/CallGS/Handlers/Fishing/FishingServer_ConvertFood.cs new file mode 100644 index 0000000..1597cf8 --- /dev/null +++ b/GameServer/Server/CallGS/Handlers/Fishing/FishingServer_ConvertFood.cs @@ -0,0 +1,245 @@ +using MikuSB.Data; +using MikuSB.Data.Excel; +using MikuSB.Database; +using MikuSB.Database.Inventory; +using MikuSB.Database.Player; +using MikuSB.Enums.Item; +using MikuSB.Proto; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace MikuSB.GameServer.Server.CallGS.Handlers.Fishing; + +[CallGSApi("FishingServer_ConvertFood")] +public class FishingServer_ConvertFood : ICallGSHandler +{ + private const uint FishingGroupId = 32; + private const uint CashGroupId = 1; + private const uint FoodBaseSid = 30000; + private const uint FoodAvaTimeSubType = 1; + private const uint ExploreAvaTimeSubType = 2; + + public async Task Handle(Connection connection, string param, ushort seqNo) + { + var player = connection.Player!; + var req = JsonSerializer.Deserialize(param); + if (req == null || req.FoodId <= 0 || req.Num <= 0) + { + await CallGSRouter.SendScript(connection, "FishingServer_ConvertFood", "{\"sError\":\"error.BadParam\"}"); + return; + } + + if (!GameData.FishingFoodData.TryGetValue((uint)req.FoodId, out var food)) + { + await CallGSRouter.SendScript(connection, "FishingServer_ConvertFood", "{\"sError\":\"error.BadParam\"}"); + return; + } + + var count = Math.Max(1u, req.Num); + var sync = new NtfSyncPlayer(); + + if (!HasEnoughMaterials(player.InventoryManager.InventoryData, food.NeedItem, count) || + !HasEnoughCash(player.Data, food.BaitNum, count)) + { + await CallGSRouter.SendScript(connection, "FishingServer_ConvertFood", "{\"sError\":\"tip.girlcard_cmd_err\"}"); + return; + } + + ConsumeMaterials(player.InventoryManager.InventoryData, food.NeedItem, count, sync.Items); + ConsumeCash(player, food.BaitNum, count, sync); + + var response = new JsonObject + { + ["nFoodID"] = req.FoodId + }; + + switch (food.FoodType) + { + case 1: + ApplyFoodDuration(player, food, FoodAvaTimeSubType, count, sync); + break; + case 2: + { + var rewards = await CreateItemsAsync(player, sync, food.CreateItems, count); + response["tbBait"] = rewards; + break; + } + case 3: + ApplyFoodDuration(player, food, ExploreAvaTimeSubType, count, sync); + break; + default: + await CallGSRouter.SendScript(connection, "FishingServer_ConvertFood", "{\"sError\":\"error.BadParam\"}"); + return; + } + + DatabaseHelper.SaveDatabaseType(player.InventoryManager.InventoryData); + DatabaseHelper.SaveDatabaseType(player.Data); + + await CallGSRouter.SendScript(connection, "FishingServer_ConvertFood", response.ToJsonString(), sync); + } + + private static bool HasEnoughMaterials(InventoryData inventory, IEnumerable> costs, uint multiplier) + { + foreach (var cost in costs) + { + if (cost.Count < 5) + return false; + + var templateId = GameResourceTemplateId.FromGdpl(cost[0], cost[1], cost[2], cost[3]); + var item = inventory.Items.Values.FirstOrDefault(x => x.TemplateId == templateId); + var needCount = checked(cost[4] * multiplier); + if (item == null || item.ItemCount < needCount) + return false; + } + + return true; + } + + private static void ConsumeMaterials(InventoryData inventory, IEnumerable> costs, uint multiplier, ICollection syncItems) + { + foreach (var cost in costs) + { + var templateId = GameResourceTemplateId.FromGdpl(cost[0], cost[1], cost[2], cost[3]); + var item = inventory.Items.Values.First(x => x.TemplateId == templateId); + var needCount = checked(cost[4] * multiplier); + item.ItemCount -= needCount; + + if (item.ItemCount == 0) + { + inventory.Items.Remove(item.UniqueId); + var proto = item.ToProto(); + proto.Count = 0; + syncItems.Add(proto); + } + else + { + syncItems.Add(item.ToProto()); + } + } + } + + private static bool HasEnoughCash(PlayerGameData data, IReadOnlyList baitNum, uint multiplier) + { + if (baitNum.Count < 2) + return true; + + var moneyType = baitNum[0]; + var need = checked(baitNum[1] * multiplier); + var sid = moneyType * 2 + 1; + var attr = data.Attrs.FirstOrDefault(x => x.Gid == CashGroupId && x.Sid == sid); + return (attr?.Val ?? 0) >= need; + } + + private static void ConsumeCash(MikuSB.GameServer.Game.Player.PlayerInstance player, IReadOnlyList baitNum, uint multiplier, NtfSyncPlayer sync) + { + if (baitNum.Count < 2) + return; + + var moneyType = baitNum[0]; + var sid = moneyType * 2 + 1; + var need = checked(baitNum[1] * multiplier); + var attr = GetOrCreateAttr(player.Data, CashGroupId, sid); + attr.Val -= need; + SyncAttr(player, sync, attr); + } + + private static void ApplyFoodDuration(MikuSB.GameServer.Game.Player.PlayerInstance player, FishingFoodExcel food, uint subType, uint count, NtfSyncPlayer sync) + { + var sid = FoodBaseSid + food.Id * 10 + subType; + var attr = GetOrCreateAttr(player.Data, FishingGroupId, sid); + var now = (uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var startTime = Math.Max(attr.Val, now); + attr.Val = checked(startTime + food.EffectTime * count); + SyncAttr(player, sync, attr); + } + + private static async Task CreateItemsAsync(MikuSB.GameServer.Game.Player.PlayerInstance player, NtfSyncPlayer sync, IReadOnlyList createItem, uint multiplier) + { + var rewards = new JsonArray(); + if (createItem.Count < 5) + return rewards; + + var itemType = (ItemTypeEnum)createItem[0]; + var detail = createItem[1]; + var particular = createItem[2]; + var level = createItem[3]; + var totalCount = checked(createItem[4] * multiplier); + + switch (itemType) + { + case ItemTypeEnum.TYPE_SUPPLIES: + { + var templateId = (uint)GameResourceTemplateId.FromGdpl(createItem[0], detail, particular, level); + if (GameData.SuppliesData.TryGetValue(templateId, out var supplies)) + { + var item = await player.InventoryManager.AddSuppliesItem(supplies, totalCount, sendPacket: false); + if (item != null) + sync.Items.Add(item.ToProto()); + } + break; + } + case ItemTypeEnum.TYPE_USEABLE: + { + var item = AddOtherItem(player.InventoryManager.InventoryData, detail, particular, level, totalCount); + if (item != null) + sync.Items.Add(item.ToProto()); + break; + } + } + + rewards.Add(new JsonArray((int)createItem[0], (int)detail, (int)particular, (int)level, (int)totalCount)); + return rewards; + } + + private static BaseGameItemInfo? AddOtherItem(InventoryData inventory, uint detail, uint particular, uint level, uint count) + { + var templateId = (uint)GameResourceTemplateId.FromGdpl((uint)ItemTypeEnum.TYPE_USEABLE, 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(PlayerGameData data, uint gid, uint sid) + { + var attr = data.Attrs.FirstOrDefault(x => x.Gid == gid && x.Sid == sid); + if (attr != null) + return attr; + + attr = new PlayerAttr { Gid = gid, Sid = sid, Val = 0 }; + data.Attrs.Add(attr); + return attr; + } + + private static void SyncAttr(MikuSB.GameServer.Game.Player.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 FishingConvertFoodParam +{ + [JsonPropertyName("nFoodID")] + public int FoodId { get; set; } + + [JsonPropertyName("nNum")] + public uint Num { get; set; } +}