From 017b03cb737f883d452134285c93520a795a3806 Mon Sep 17 00:00:00 2001 From: losunet Date: Mon, 24 Feb 2025 11:38:44 +0800 Subject: [PATCH] feat: better relic logic & farming config --- Common/Configuration/ConfigContainer.cs | 6 + Common/Data/Excel/MappingInfoExcel.cs | 128 ++++++++++++++- Common/Data/Excel/MonsterDropExcel.cs | 3 +- Common/Database/Inventory/InventoryData.cs | 67 +++++++- Common/Util/GameConstants.cs | 2 +- GameServer/Game/Inventory/InventoryManager.cs | 153 +++++++++++------- .../Item/HandlerComposeSelectedRelicCsReq.cs | 10 +- .../Packet/Recv/Item/HandlerSellItemCsReq.cs | 2 +- .../HandlerReserveStaminaExchangeCsReq.cs | 29 ++++ .../Item/PacketComposeSelectedRelicScRsp.cs | 14 +- .../PacketReserveStaminaExchangeScRsp.cs | 17 ++ 11 files changed, 369 insertions(+), 62 deletions(-) create mode 100644 GameServer/Server/Packet/Recv/Player/HandlerReserveStaminaExchangeCsReq.cs create mode 100644 GameServer/Server/Packet/Send/Player/PacketReserveStaminaExchangeScRsp.cs diff --git a/Common/Configuration/ConfigContainer.cs b/Common/Configuration/ConfigContainer.cs index e17a7f93..f6422ed5 100644 --- a/Common/Configuration/ConfigContainer.cs +++ b/Common/Configuration/ConfigContainer.cs @@ -85,6 +85,12 @@ public class ServerOption public ServerProfile ServerProfile { get; set; } = new(); public bool AutoCreateUser { get; set; } = true; public bool SavePersonalDebugFile { get; set; } = false; + public int FarmingDropRate { get; set; } = 1; + + public int ValidFarmingDropRate() + { + return Math.Max(Math.Min(FarmingDropRate, 999), 1); + } } public class ServerAnnounce diff --git a/Common/Data/Excel/MappingInfoExcel.cs b/Common/Data/Excel/MappingInfoExcel.cs index b8cc180f..930c3130 100644 --- a/Common/Data/Excel/MappingInfoExcel.cs +++ b/Common/Data/Excel/MappingInfoExcel.cs @@ -1,4 +1,7 @@ -using EggLink.DanhengServer.Enums.Item; +using EggLink.DanhengServer.Database.Inventory; +using EggLink.DanhengServer.Enums.Item; +using EggLink.DanhengServer.Util; +using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -183,6 +186,129 @@ public class MappingInfoExcel : ExcelResource } } } + + public List GenerateRelicDrops() + { + var relicsMap = new Dictionary>(); + foreach (var relic in DropRelicItemList) + { + GameData.ItemConfigData.TryGetValue(relic.ItemID, out var itemData); + if (itemData == null) continue; + switch (itemData.Rarity) + { + case ItemRarityEnum.NotNormal: + AddRelicToMap(relic, 2, relicsMap); + break; + case ItemRarityEnum.Rare: + AddRelicToMap(relic, 3, relicsMap); + break; + case ItemRarityEnum.VeryRare: + AddRelicToMap(relic, 4, relicsMap); + break; + case ItemRarityEnum.SuperRare: + AddRelicToMap(relic, 5, relicsMap); + break; + default: + continue; + } + } + + List drops = []; + // Add higher rarity relics first + for (var rarity = 5; rarity >= 2; rarity--) + { + var count = GetRelicCountByWorldLevel(rarity) * + ConfigManager.Config.ServerOption.ValidFarmingDropRate(); + if (count <= 0) continue; + if (!relicsMap.TryGetValue(rarity, out var value)) continue; + if (value.IsNullOrEmpty()) continue; + while (count > 0) + { + var relic = value.RandomElement(); + drops.Add(new ItemData + { + ItemId = relic.ItemID, + Count = 1 + }); + count--; + } + } + + return drops; + } + + private void AddRelicToMap(MappingInfoItem relic, int rarity, Dictionary> relicsMap) + { + if (relicsMap.TryGetValue(rarity, out var value)) + { + value.Add(relic); + } + else + { + relicsMap.Add(rarity, [relic]); + } + } + + private int GetRelicCountByWorldLevel(int rarity) + { + return WorldLevel switch + { + 1 => rarity switch + { + 2 => 6, + 3 => 3, + 4 => 1, + 5 => 0, + _ => 0 + }, + 2 => rarity switch + { + 2 => 2, + 3 => 4, + 4 => 2 + LuckyRelicDropped(), + 5 => 0, + _ => 0 + }, + 3 => rarity switch + { + 2 => 0, + 3 => 4, + 4 => 2, + 5 => 1, + _ => 0 + }, + 4 => rarity switch + { + 2 => 0, + 3 => 3, + 4 => 2 + LuckyRelicDropped(), + 5 => 1 + LuckyRelicDropped(), + _ => 0 + }, + 5 => rarity switch + { + 2 => 0, + 3 => 1 + LuckyRelicDropped(), + 4 => 3, + 5 => 2, + _ => 0 + }, + 6 => rarity switch + { + 2 => 0, + 3 => 0, + 4 => 5, + 5 => 2 + LuckyRelicDropped(), + _ => 0 + }, + _ => 0 + }; + } + + private int LuckyRelicDropped() + { + return Random.Shared.Next(100) < 25 ? 1 : 0; + } } public class MappingInfoItem diff --git a/Common/Data/Excel/MonsterDropExcel.cs b/Common/Data/Excel/MonsterDropExcel.cs index a39688fd..4a95647a 100644 --- a/Common/Data/Excel/MonsterDropExcel.cs +++ b/Common/Data/Excel/MonsterDropExcel.cs @@ -1,4 +1,5 @@ using EggLink.DanhengServer.Database.Inventory; +using EggLink.DanhengServer.Util; using Newtonsoft.Json; namespace EggLink.DanhengServer.Data.Excel; @@ -54,7 +55,7 @@ public class MonsterDropExcel : ExcelResource result.Add(new ItemData { ItemId = item.ItemID, - Count = count + Count = count * ConfigManager.Config.ServerOption.ValidFarmingDropRate() }); } diff --git a/Common/Database/Inventory/InventoryData.cs b/Common/Database/Inventory/InventoryData.cs index 46948dcc..2fa78031 100644 --- a/Common/Database/Inventory/InventoryData.cs +++ b/Common/Database/Inventory/InventoryData.cs @@ -1,5 +1,6 @@ using EggLink.DanhengServer.Data; using EggLink.DanhengServer.Data.Excel; +using EggLink.DanhengServer.Enums.Item; using EggLink.DanhengServer.Proto; using EggLink.DanhengServer.Util; using SqlSugar; @@ -60,15 +61,22 @@ public class ItemData public void AddRandomRelicSubAffix(int count = 1) { + // Avoid illegal count of relic sub affixes + if (count is < 1 or > 4 || SubAffixes.Count >= 4) return; GameData.RelicConfigData.TryGetValue(ItemId, out var config); if (config == null) return; GameData.RelicSubAffixData.TryGetValue(config.SubAffixGroup, out var affixes); if (affixes == null) return; + // Avoid same property on both main affix and sub affixes + GameData.RelicMainAffixData.TryGetValue(config.MainAffixGroup, out var mainAffixes); + if (mainAffixes == null) return; + var mainProperty = mainAffixes[MainAffix].Property; + var rollPool = new List(); foreach (var affix in affixes.Values) - if (SubAffixes.Find(x => x.Id == affix.AffixID) == null) + if (affix.Property != mainProperty & SubAffixes.Find(x => x.Id == affix.AffixID) == null) rollPool.Add(affix); for (var i = 0; i < count; i++) @@ -77,11 +85,68 @@ public class ItemData ItemSubAffix subAffix = new(affixConfig, 1); SubAffixes.Add(subAffix); rollPool.Remove(affixConfig); + if (SubAffixes.Count >= 4) break; } } + /** + * Init relic sub affixes based on rarity + * 20% chance to get one more affix + * r3 1-2 + * r4 2-3 + * r5 3-4 + */ + public void InitRandomRelicSubAffixesByRarity(ItemRarityEnum rarity = ItemRarityEnum.Unknown) + { + if (rarity == ItemRarityEnum.Unknown) + { + GameData.ItemConfigData.TryGetValue(ItemId, out var config); + if (config == null) return; + rarity = config.Rarity; + } + + int initSubAffixesCount; + switch (rarity) + { + case ItemRarityEnum.Rare: + initSubAffixesCount = 1 + LuckyRelicSubAffixCount(); + break; + case ItemRarityEnum.VeryRare: + initSubAffixesCount = 2 + LuckyRelicSubAffixCount(); + break; + case ItemRarityEnum.SuperRare: + initSubAffixesCount = 3 + LuckyRelicSubAffixCount(); + break; + default: + return; + } + + AddRandomRelicSubAffix(initSubAffixesCount); + } + + public int LuckyRelicSubAffixCount() + { + return Random.Shared.Next(100) < 20 ? 1 : 0; + } + #endregion + public int CalcTotalExpGained() + { + if (Level <= 0) return Exp; + GameData.RelicConfigData.TryGetValue(ItemId, out var costExcel); + if (costExcel == null) return 0; + var exp = 0; + for (var i = 0; i < Level; i++) + { + GameData.RelicExpTypeData.TryGetValue(costExcel.ExpType * 100 + i, out var typeExcel); + if (typeExcel != null) + exp += typeExcel.Exp; + } + + return exp + Exp; + } + #region Serialization public Material ToMaterialProto() diff --git a/Common/Util/GameConstants.cs b/Common/Util/GameConstants.cs index 63c7bb25..b53759f2 100644 --- a/Common/Util/GameConstants.cs +++ b/Common/Util/GameConstants.cs @@ -3,7 +3,7 @@ public static class GameConstants { public const int INVENTORY_MAX_EQUIPMENT = 1500; - public const int INVENTORY_MAX_RELIC = 1500; + public const int INVENTORY_MAX_RELIC = 2000; public const int INVENTORY_MAX_MATERIAL = 2000; public const int MAX_LINEUP_COUNT = 9; diff --git a/GameServer/Game/Inventory/InventoryManager.cs b/GameServer/Game/Inventory/InventoryManager.cs index 0326c312..fc90a76f 100644 --- a/GameServer/Game/Inventory/InventoryManager.cs +++ b/GameServer/Game/Inventory/InventoryManager.cs @@ -13,6 +13,7 @@ using EggLink.DanhengServer.GameServer.Server.Packet.Send.Scene; using EggLink.DanhengServer.Proto; using EggLink.DanhengServer.Util; using Google.Protobuf.Collections; +using Microsoft.Net.Http.Headers; namespace EggLink.DanhengServer.GameServer.Game.Inventory; @@ -99,7 +100,7 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player) var item = await PutItem(itemId, 1, 1, level: 0, uniqueId: ++Data.NextUniqueId); item.AddRandomRelicMainAffix(); - item.AddRandomRelicSubAffix(3); + item.InitRandomRelicSubAffixesByRarity(); Data.RelicItems.Find(x => x.UniqueId == item.UniqueId)!.SubAffixes = item.SubAffixes; itemData = item; break; @@ -459,43 +460,20 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player) items.Add(new ItemData { ItemId = item.ItemID, - Count = amount + Count = amount * (item.ItemID == 22 + ? 1 + : ConfigManager.Config.ServerOption.ValidFarmingDropRate()) }); } } - // randomize the order of the relics - var relics = mapping.DropRelicItemList.OrderBy(x => Random.Shared.Next()).ToList(); - - var relic5Count = Random.Shared.Next(worldLevel - 4, worldLevel - 2); - var relic4Count = worldLevel - 2; - foreach (var relic in relics) - { - var random = Random.Shared.Next(0, 101); - - if (random <= relic.Chance) - { - var amount = relic.ItemNum > 0 - ? relic.ItemNum - : Random.Shared.Next(relic.MinCount, relic.MaxCount + 1); - - GameData.ItemConfigData.TryGetValue(relic.ItemID, out var itemData); - if (itemData == null) continue; - - if (itemData.Rarity == ItemRarityEnum.SuperRare && relic5Count > 0) - relic5Count--; - else if (itemData.Rarity == ItemRarityEnum.VeryRare && relic4Count > 0) - relic4Count--; - else - continue; - - items.Add(new ItemData - { - ItemId = relic.ItemID, - Count = 1 - }); - } - } + // Generate relics + var relicDrops = mapping.GenerateRelicDrops(); + + // Let AddItem notify relics count exceeding limit + items.AddRange(Data.RelicItems.Count + relicDrops.Count - 1 > GameConstants.INVENTORY_MAX_RELIC + ? relicDrops[..(GameConstants.INVENTORY_MAX_RELIC - Data.RelicItems.Count + 1)] + : relicDrops); foreach (var item in items) { @@ -621,42 +599,73 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player) await RemoveItem(2, (int)(composeConfig.CoinCost * req.Count)); + var relicId = (int)req.ComposeRelicId; + GameData.RelicConfigData.TryGetValue(relicId, out var itemConfig); + GameData.RelicSubAffixData.TryGetValue(itemConfig!.SubAffixGroup, out var subAffixConfig); + // Add relic - var subAffixes = req.SubAffixIdList.Select(subId => ((int)subId, 1)).ToList(); + var mainAffix = (int)req.MainAffixId; + var itemData = new ItemData + { + ItemId = relicId, + Level = 0, + UniqueId = ++Data.NextUniqueId, + MainAffix = mainAffix, + SubAffixes = req.SubAffixIdList.Select(subId => new ItemSubAffix(subAffixConfig![(int)subId], 1)).ToList(), + Count = 1 + }; + if (mainAffix == 0) itemData.AddRandomRelicMainAffix(); + itemData.AddRandomRelicSubAffix(3 - itemData.SubAffixes.Count + itemData.LuckyRelicSubAffixCount()); + await AddItem(itemData, notify: false); - var (_, relic) = await HandleRelic( - (int)req.ComposeRelicId, ++Data.NextUniqueId, 0, (int)req.MainAffixId, subAffixes); - - return relic; + return itemData; } public async ValueTask ReforgeRelic(int uniqueId) { - var relic = Data.RelicItems.FirstOrDefault(x => x.UniqueId == uniqueId); - await RemoveItem(relic!.ItemId, 1, uniqueId, false); + var relic = Data.RelicItems.First(x => x.UniqueId == uniqueId); var totalCount = 0; var subAffixes = new List<(int, int)>(); foreach (var sub in relic.SubAffixes) { - totalCount += sub.Count; - subAffixes.Add((sub.Id, 0)); + totalCount = totalCount + sub.Count - 1; + subAffixes.Add((sub.Id, 1)); } - var remainCount = totalCount; - for (var i = 0; i < subAffixes.Count - 1; i++) + while (totalCount > 0) { - var count = new Random().Next(1, remainCount - (subAffixes.Count - i - 1)); - subAffixes[i] = (subAffixes[i].Item1, count); - remainCount -= count; + var idx = Random.Shared.Next(subAffixes.Count); + var cur = subAffixes[idx]; + subAffixes[idx] = (cur.Item1, cur.Item2 + 1); + totalCount--; + } + + GameData.RelicConfigData.TryGetValue(relic.ItemId, out var itemConfig); + GameData.RelicSubAffixData.TryGetValue(itemConfig!.SubAffixGroup, out var subAffixConfig); + + for (var i = 0; i < subAffixes.Count; i++) + { + var (subId, subLevel) = subAffixes[i]; + subAffixConfig!.TryGetValue(subId, out var subAffix); + var aff = new ItemSubAffix(subAffix!, subLevel); + relic.SubAffixes[i] = aff; + } + + if (relic.EquipAvatar > 0) + { + var avatar = Player.AvatarManager!.GetAvatar(relic.EquipAvatar); + await Player.SendPacket(new PacketPlayerSyncScNotify(avatar!, relic)); + } + else + { + await Player.SendPacket(new PacketPlayerSyncScNotify(relic)); } - subAffixes[^1] = (subAffixes[^1].Item1, remainCount); - await HandleRelic(relic.ItemId, uniqueId, relic.Level, relic.MainAffix, subAffixes); await RemoveItem(238, 1); } - public async ValueTask> SellItem(ItemCostData costData) + public async ValueTask> SellItem(ItemCostData costData, bool toMaterial = false) { List items = []; Dictionary itemMap = []; @@ -683,10 +692,46 @@ public class InventoryManager(PlayerInstance player) : BasePlayerManager(player) removeItems.Add((itemData.ItemId, 1, (int)cost.RelicUniqueId)); GameData.ItemConfigData.TryGetValue(itemData.ItemId, out var itemConfig); if (itemConfig == null) continue; - foreach (var returnItem in itemConfig.ReturnItemIDList) // return items + if (itemConfig.Rarity != ItemRarityEnum.SuperRare || toMaterial) { - if (!itemMap.ContainsKey(returnItem.ItemID)) itemMap[returnItem.ItemID] = 0; - itemMap[returnItem.ItemID] += returnItem.ItemNum; + foreach (var returnItem in itemConfig.ReturnItemIDList) // basic return items + { + itemMap.TryAdd(returnItem.ItemID, 0); + itemMap[returnItem.ItemID] += returnItem.ItemNum; + } + + var expReturned = (int)(itemData.CalcTotalExpGained() * 0.8); + + var credit = (int)(expReturned * 1.5); + if (credit > 0) + { + itemMap.TryAdd(2, 0); + itemMap[2] += (int)(expReturned * 1.5); + } + + var lostGoldFragCnt = expReturned / 500; + if (lostGoldFragCnt > 0) + { + itemMap.TryAdd(232, 0); + itemMap[232] += lostGoldFragCnt; + } + + var lostGoldLightdust = expReturned % 500 / 100; + if (lostGoldLightdust > 0) + { + itemMap.TryAdd(231, 0); + itemMap[231] += lostGoldLightdust; + } + } + else + { + var expGained = itemData.CalcTotalExpGained(); + var remainsCnt = (int)(10 + expGained * 0.005144); + if (remainsCnt > 0) + { + itemMap.TryAdd(235, 0); + itemMap[235] += remainsCnt; + } } } else diff --git a/GameServer/Server/Packet/Recv/Item/HandlerComposeSelectedRelicCsReq.cs b/GameServer/Server/Packet/Recv/Item/HandlerComposeSelectedRelicCsReq.cs index f8dacf98..b79d3b45 100644 --- a/GameServer/Server/Packet/Recv/Item/HandlerComposeSelectedRelicCsReq.cs +++ b/GameServer/Server/Packet/Recv/Item/HandlerComposeSelectedRelicCsReq.cs @@ -1,6 +1,7 @@ using EggLink.DanhengServer.GameServer.Server.Packet.Send.Item; using EggLink.DanhengServer.Kcp; using EggLink.DanhengServer.Proto; +using EggLink.DanhengServer.Util; namespace EggLink.DanhengServer.GameServer.Server.Packet.Recv.Item; @@ -11,10 +12,15 @@ public class HandlerComposeSelectedRelicCsReq : Handler { var req = ComposeSelectedRelicCsReq.Parser.ParseFrom(data); var player = connection.Player!; - var item = await player.InventoryManager!.ComposeRelic(req); + if (player.InventoryManager!.Data.RelicItems.Count >= GameConstants.INVENTORY_MAX_RELIC) + { + await connection.SendPacket(new PacketComposeSelectedRelicScRsp(req.ComposeId, Retcode.RetRelicExceedLimit)); + return; + } + var item = await player.InventoryManager.ComposeRelic(req); if (item == null) { - await connection.SendPacket(new PacketComposeSelectedRelicScRsp()); + await connection.SendPacket(new PacketComposeSelectedRelicScRsp(req.ComposeId)); return; } diff --git a/GameServer/Server/Packet/Recv/Item/HandlerSellItemCsReq.cs b/GameServer/Server/Packet/Recv/Item/HandlerSellItemCsReq.cs index 3475f8f0..7583ffc1 100644 --- a/GameServer/Server/Packet/Recv/Item/HandlerSellItemCsReq.cs +++ b/GameServer/Server/Packet/Recv/Item/HandlerSellItemCsReq.cs @@ -10,7 +10,7 @@ public class HandlerSellItemCsReq : Handler public override async Task OnHandle(Connection connection, byte[] header, byte[] data) { var req = SellItemCsReq.Parser.ParseFrom(data); - var items = await connection.Player!.InventoryManager!.SellItem(req.CostData); + var items = await connection.Player!.InventoryManager!.SellItem(req.CostData, req.ToMaterial); await connection.SendPacket(new PacketSellItemScRsp(items)); } } \ No newline at end of file diff --git a/GameServer/Server/Packet/Recv/Player/HandlerReserveStaminaExchangeCsReq.cs b/GameServer/Server/Packet/Recv/Player/HandlerReserveStaminaExchangeCsReq.cs new file mode 100644 index 00000000..8357d163 --- /dev/null +++ b/GameServer/Server/Packet/Recv/Player/HandlerReserveStaminaExchangeCsReq.cs @@ -0,0 +1,29 @@ +using EggLink.DanhengServer.GameServer.Server.Packet.Send.Player; +using EggLink.DanhengServer.Kcp; +using EggLink.DanhengServer.Proto; + +namespace EggLink.DanhengServer.GameServer.Server.Packet.Recv.Player; + +[Opcode(CmdIds.ReserveStaminaExchangeCsReq)] +public class HandlerReserveStaminaExchangeCsReq : Handler +{ + public async override Task OnHandle(Connection connection, byte[] header, byte[] data) + { + var req = ReserveStaminaExchangeCsReq.Parser.ParseFrom(data); + var player = connection.Player; + if (player == null) return; + var amount = req.Num; + if (amount <= 0 || player.Data.StaminaReserve < amount) + { + await connection.SendPacket(new PacketReserveStaminaExchangeScRsp(0)); + } + else + { + player.Data.StaminaReserve -= amount; + player.Data.Stamina += (int)amount; + + await connection.SendPacket(new PacketStaminaInfoScNotify(player)); + await connection.SendPacket(new PacketReserveStaminaExchangeScRsp(amount)); + } + } +} \ No newline at end of file diff --git a/GameServer/Server/Packet/Send/Item/PacketComposeSelectedRelicScRsp.cs b/GameServer/Server/Packet/Send/Item/PacketComposeSelectedRelicScRsp.cs index a8abcf42..6a340714 100644 --- a/GameServer/Server/Packet/Send/Item/PacketComposeSelectedRelicScRsp.cs +++ b/GameServer/Server/Packet/Send/Item/PacketComposeSelectedRelicScRsp.cs @@ -6,16 +6,28 @@ namespace EggLink.DanhengServer.GameServer.Server.Packet.Send.Item; public class PacketComposeSelectedRelicScRsp : BasePacket { - public PacketComposeSelectedRelicScRsp() : base(CmdIds.ComposeSelectedRelicScRsp) + public PacketComposeSelectedRelicScRsp(uint composeId) : base(CmdIds.ComposeSelectedRelicScRsp) { var proto = new ComposeSelectedRelicScRsp { + ComposeId = composeId, Retcode = 1 }; SetData(proto); } + public PacketComposeSelectedRelicScRsp(uint composeId, Retcode retcode) : base(CmdIds.ComposeSelectedRelicScRsp) + { + var proto = new ComposeSelectedRelicScRsp + { + ComposeId = composeId, + Retcode = (uint)retcode + }; + + SetData(proto); + } + public PacketComposeSelectedRelicScRsp(uint composeId, ItemData item) : base(CmdIds.ComposeSelectedRelicScRsp) { diff --git a/GameServer/Server/Packet/Send/Player/PacketReserveStaminaExchangeScRsp.cs b/GameServer/Server/Packet/Send/Player/PacketReserveStaminaExchangeScRsp.cs new file mode 100644 index 00000000..948983e7 --- /dev/null +++ b/GameServer/Server/Packet/Send/Player/PacketReserveStaminaExchangeScRsp.cs @@ -0,0 +1,17 @@ +using EggLink.DanhengServer.Kcp; +using EggLink.DanhengServer.Proto; + +namespace EggLink.DanhengServer.GameServer.Server.Packet.Send.Player; + +public class PacketReserveStaminaExchangeScRsp : BasePacket +{ + public PacketReserveStaminaExchangeScRsp(uint amount) : base(CmdIds.ReserveStaminaExchangeScRsp) + { + var proto = new ReserveStaminaExchangeScRsp(); + + if (amount > 0) proto.Num = amount; + else proto.Retcode = (uint)Retcode.RetFail; + + SetData(proto); + } +} \ No newline at end of file