diff --git a/EpinelPS/Data/GameData.cs b/EpinelPS/Data/GameData.cs index 843daed..38aa6d8 100644 --- a/EpinelPS/Data/GameData.cs +++ b/EpinelPS/Data/GameData.cs @@ -185,6 +185,17 @@ namespace EpinelPS.Data public readonly Dictionary LostSector = []; [LoadRecord("LostSectorStageTable.json", "id", typeof(LostSectorStageTable))] public readonly Dictionary LostSectorStages = []; + [LoadRecord("ItemPieceTable.json", "id", typeof(ItemPieceTable))] + public readonly Dictionary PieceItems = []; + [LoadRecord("GachaGradeProbTable.json", "id", typeof(GachaGradeProbTable))] + public readonly Dictionary GachaGradeProb = []; + [LoadRecord("GachaListProbTable.json", "id", typeof(GachaListProbTable))] + public readonly Dictionary GachaListProb = []; + [LoadRecord("RecycleResearchStatTable.json", "id", typeof(RecycleResearchStatTable))] + public readonly Dictionary RecycleResearchStats = []; + [LoadRecord("RecycleResearchLevelTable.json", "id", typeof(RecycleResearchLevelTable))] + public readonly Dictionary RecycleResearchLevels = []; + static async Task BuildAsync() { await Load(); diff --git a/EpinelPS/Data/JsonStaticData.cs b/EpinelPS/Data/JsonStaticData.cs index 5c80859..9a9ad51 100644 --- a/EpinelPS/Data/JsonStaticData.cs +++ b/EpinelPS/Data/JsonStaticData.cs @@ -303,6 +303,30 @@ public List records = []; } + public class GachaGradeProbRecord + { + public int id; + public int group_id; + public string rare = ""; + public int prob; + public int gacha_list_id; + } + public class GachaGradeProbTable + { + public List records = []; + } + + public class GachaListProbRecord + { + public int id; + public int group_id; + public int gacha_id; + } + public class GachaListProbTable + { + public List records = []; + } + public class EventManager { public int id; @@ -812,11 +836,23 @@ public string item_type = ""; public ItemSubType item_sub_type; public int use_id; + public int use_value; } public class ItemConsumeTable { public List records = []; } + public class ItemPieceRecord + { + public int id; + public string use_type = ""; + public int use_id; + public int use_value; + } + public class ItemPieceTable + { + public List records = []; + } public class RandomItemRecord { public int id; @@ -831,6 +867,33 @@ { public List records = []; } + public class RecycleResearchStatRecord + { + public int id; + public string recycle_type = ""; + public int unlock_condition_id; + public int unlock_level; + public int attack; + public int defense; + public int hp; + } + public class RecycleResearchStatTable + { + public List records = []; + } + public class RecycleResearchLevelRecord + { + public int id; + public string recycle_type = ""; + public int recycle_level; + public int exp; + public int item_id; + public int item_value; + } + public class RecycleResearchLevelTable + { + public List records = []; + } public enum ContentOpenType { Stage, diff --git a/EpinelPS/Database/JsonDb.cs b/EpinelPS/Database/JsonDb.cs index d2cd5a2..7961feb 100644 --- a/EpinelPS/Database/JsonDb.cs +++ b/EpinelPS/Database/JsonDb.cs @@ -83,6 +83,14 @@ namespace EpinelPS.Database /// public long AvailableAt; } + public class RecycleRoomResearchProgress + { + public int Level = 1; + public int Exp; + public int Attack; + public int Defense; + public int Hp; + } public class SimroomData { public int CurrentDifficulty; @@ -266,6 +274,7 @@ namespace EpinelPS.Database public List SynchroSlots = new List(); public bool SynchroDeviceUpgraded = false; public int SynchroDeviceLevel = 200; + public Dictionary ResearchProgress = []; public ResetableData ResetableData = new(); public WeeklyResetableData WeeklyResetableData = new(); @@ -482,7 +491,7 @@ namespace EpinelPS.Database var matchingCharacterIds = GameData.Instance.CharacterTable.Where(kvp => kvp.Value.name_code == targetNameCode).Select(kvp => kvp.Key).ToHashSet(); // Step 3: Check if any of your owned characters have a 'Tid' in the set of matching IDs - return Characters.Where(ownedCharacter => matchingCharacterIds.Contains(ownedCharacter.Tid)).First(); + return Characters.Where(ownedCharacter => matchingCharacterIds.Contains(ownedCharacter.Tid)).FirstOrDefault(); } else diff --git a/EpinelPS/LobbyServer/Inventory/UsePiece.cs b/EpinelPS/LobbyServer/Inventory/UsePiece.cs index 5d3713d..edf57ff 100644 --- a/EpinelPS/LobbyServer/Inventory/UsePiece.cs +++ b/EpinelPS/LobbyServer/Inventory/UsePiece.cs @@ -1,3 +1,5 @@ +using EpinelPS.Data; +using EpinelPS.Database; using EpinelPS.Utils; namespace EpinelPS.LobbyServer.Inventory @@ -5,16 +7,204 @@ namespace EpinelPS.LobbyServer.Inventory [PacketPath("/inventory/usepiece")] public class UsePiece : LobbyMsgHandler { + private static readonly Random random = new Random(); + protected override async Task HandleAsync() { + // TODO: If this process takes too long, consider to avoid using function chain. + /* + * Req Contains: + * Isn: long value, the item serial number of the piece + * Count: int value, how many time + */ var req = await ReadData(); var user = GetUser(); - var response = new ResUsePiece(); - - // TODO + + var piece = user.Items.FirstOrDefault(x => x.Isn == req.Isn) ?? throw new InvalidDataException("cannot find piece with isn " + req.Isn); + if (req.Count * 50 > piece.Count) throw new Exception("count mismatch"); + + piece.Count -= req.Count * 50; + if (piece.Count == 0) user.Items.Remove(piece); + + ItemPieceRecord? pItem = GameData.Instance.PieceItems + .FirstOrDefault(x => x.Value.id == piece.ItemType).Value + ?? throw new Exception("cannot find piece id " + piece.ItemType); + + var probList = GameData.Instance.GachaGradeProb + .Where(x => x.Key == pItem.use_id) + .SelectMany(grade => GameData.Instance.GachaListProb.Where(list => list.Value.group_id == grade.Value.gacha_list_id)) + .Select(i => i.Value); + var allCharacters = probList.SelectMany(e => GameData.Instance.CharacterTable.Values.Where(c => c.id == e.gacha_id)); + + NetRewardData reward = new(); + var selectedCharacters = Enumerable.Range(1, req.Count) + .Select(_ => SelectRandomCharacter(allCharacters, pItem.id)); + + int totalBodyLabels = 0; + foreach (var character in selectedCharacters) + { + ItemData? spareItem = user.Items.FirstOrDefault(i => i.ItemType == character.piece_id); + + if (user.GetCharacter(character.id) is Database.Character ownedCharacter) + { + // If the character already exists, we can increase its piece count + int maxLimitBroken = GetValueByRarity(character.original_rare, 0, 2, 11); + bool canIncreaseItem = character.original_rare != "R" && ownedCharacter.Grade + (spareItem?.Count ?? 0) < maxLimitBroken; + (int newSpareItemCount, int dissoluteCharacterCount) = canIncreaseItem ? (1, 0) : (0, 1); + if (canIncreaseItem) + { + if (spareItem != null) + { + spareItem.Count = newSpareItemCount; + } + else + { + spareItem = new() + { + ItemType = character.piece_id, + Csn = 0, + Count = newSpareItemCount, + Level = 0, + Exp = 0, + Position = 0, + Corp = 0, + Isn = user.GenerateUniqueItemId() + }; + user.Items.Add(spareItem); + } + + reward.UserItems.Add(NetUtils.UserItemDataToNet(spareItem)); + reward.Character.Add(GetNetCharacterData(ownedCharacter)); + } + else + { + // If we cannot increase the item, we give body label instead + int bodyLabel = GetValueByRarity(character.original_rare, 150, 200, 6000); + totalBodyLabels += bodyLabel * dissoluteCharacterCount; + reward.Character.Add(GetNetCharacterData(ownedCharacter, bodyLabel)); + } + } + else + { + var csn = user.GenerateUniqueCharacterId(); + reward.UserCharacters.Add(new NetUserCharacterDefaultData + { + CostumeId = 0, + Csn = csn, + Grade = 0, + Lv = 1, + Skill1Lv = 1, + Skill2Lv = 1, + Tid = character.id, + UltiSkillLv = 1 + }); + reward.Character.Add(new NetCharacterData + { + Csn = user.GenerateUniqueCharacterId(), + Tid = character.id, + }); + user.Characters.Add(new Database.Character + { + CostumeId = 0, + Csn = csn, + Grade = 0, + Level = 1, + Skill1Lvl = 1, + Skill2Lvl = 1, + Tid = character.id, + UltimateLevel = 1 + }); + + // Add "New Character" Badge + user.AddBadge(BadgeContents.NikkeNew, character.name_code.ToString()); + user.AddTrigger(TriggerType.ObtainCharacter, 1, character.name_code); + if (character.original_rare == "SSR") + { + user.AddTrigger(TriggerType.ObtainCharacterSSR, 1); + } + else + { + user.AddTrigger(TriggerType.ObtainCharacterNew, 1); + } + + if (character.original_rare == "SSR" || character.original_rare == "SR") + { + user.BondInfo.Add(new() { NameCode = character.name_code, Lv = 1 }); + } + } + + user.AddTrigger(TriggerType.GachaCharacter, 0, 0); + } + + reward.Currency.Add(new NetCurrencyData() { Type = (int)CurrencyType.DissolutionPoint, Value = totalBodyLabels }); + user.AddCurrency(CurrencyType.DissolutionPoint, totalBodyLabels); + reward.UserItems.Add(NetUtils.UserItemDataToNet(piece)); + + response.Reward = reward; + + JsonDb.Save(); await WriteDataAsync(response); } + + private CharacterRecord SelectRandomCharacter(IEnumerable characters, int pieceId) + { + var gradeProb = GetPieceGradeProb(pieceId); + var rCharacters = characters.Where(c => c.original_rare == "R"); + var srCharacters = characters.Where(c => c.original_rare == "SR"); + var ssrCharacters = characters.Where(c => c.original_rare == "SSR"); + + double roll = random.NextDouble() * 100; + + if (0.0 < gradeProb.RProb && roll < gradeProb.RProb && rCharacters.Any()) + { + return rCharacters.ElementAt(random.Next(rCharacters.Count())); + } + else if (0.0 < gradeProb.SRProb && roll < gradeProb.RProb + gradeProb.SRProb && srCharacters.Any()) + { + return srCharacters.ElementAt(random.Next(srCharacters.Count())); + } + else if (0.0 < gradeProb.SSRProb && roll < gradeProb.RProb + gradeProb.SRProb + gradeProb.SSRProb && ssrCharacters.Any()) + { + return ssrCharacters.ElementAt(random.Next(ssrCharacters.Count())); + } + else + { + throw new Exception("No characters available for the given value."); + } + } + + // TODO: find the file where the grade probability is stored. + // TODO: Add Helm Mold and Laplace Mold + private PieceGradeProb GetPieceGradeProb(int pieceId) => pieceId switch + { + 5310301 => new PieceGradeProb(0.0, 38.9997, 61.0003), // High quality Mold + 5310302 or 5310303 or 5310304 or 5310305 => new PieceGradeProb(19.9998, 29.9997, 50.0005), // Manufacturer Mold + 5310306 or 5310307 or 5310308 or 5310309 => new PieceGradeProb(0.0, 0.0, 100.0), // New Commander Mold or Perfect Mold + 5330201 or 5359001 => new PieceGradeProb(0.0, 78.9993, 21.0007), // Mid quality Mold + _ => throw new Exception("unknown piece id") + }; + + private int GetValueByRarity(string rarity, int rValue, int srValue, int ssrValue) => rarity switch + { + "R" => rValue, + "SR" => srValue, + "SSR" => ssrValue, + _ => throw new Exception($"Unknown character rarity: {rarity}") + }; + + private NetCharacterData GetNetCharacterData(Database.Character character, int bodyLabel = 0) + { + return new NetCharacterData + { + Csn = character.Csn, + Tid = character.Tid, + PieceCount = bodyLabel == 0 ? 1 : 0, + CurrencyValue = bodyLabel + }; + } } + + internal record PieceGradeProb(double RProb, double SRProb, double SSRProb); } diff --git a/EpinelPS/LobbyServer/Inventory/UseTimeReward.cs b/EpinelPS/LobbyServer/Inventory/UseTimeReward.cs new file mode 100644 index 0000000..c56b03e --- /dev/null +++ b/EpinelPS/LobbyServer/Inventory/UseTimeReward.cs @@ -0,0 +1,52 @@ +using EpinelPS.Data; +using EpinelPS.Database; +using EpinelPS.Utils; + +namespace EpinelPS.LobbyServer.Inventory +{ + [PacketPath("/inventory/usetimereward")] + public class UseTimeReward : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + /* + * Req Contains: + * Isn: long value + * Count: int value, how many items to use + */ + var req = await ReadData(); + var user = GetUser(); + var response = new ResUseTimeReward(); + + var timeReward = user.Items.Where(x => x.Isn == req.Isn).FirstOrDefault() ?? throw new InvalidDataException("cannot find time reward with isn " + req.Isn); + if (req.Count > timeReward.Count) throw new Exception("count mismatch"); + + timeReward.Count -= req.Count; + if (timeReward.Count == 0) user.Items.Remove(timeReward); + + ItemConsumeRecord? cItem = GameData.Instance.ConsumableItems + .FirstOrDefault(x => x.Value.id == timeReward.ItemType).Value + ?? throw new Exception("cannot find box id " + timeReward.ItemType); + + // TODO: find out where these numbers come from + (CurrencyType itemType, long amount) = cItem.use_id switch + { + 1 => (CurrencyType.Gold, NetUtils.GetOutpostRewardAmount(user, CurrencyType.Gold, TimeSpan.FromSeconds(cItem.use_value).TotalMinutes, false)), + 2 => (CurrencyType.CharacterExp, NetUtils.GetOutpostRewardAmount(user, CurrencyType.CharacterExp, TimeSpan.FromSeconds(cItem.use_value).TotalMinutes, false)), + 4 => (CurrencyType.CharacterExp2, NetUtils.GetOutpostRewardAmount(user, CurrencyType.CharacterExp2, TimeSpan.FromSeconds(cItem.use_value).TotalMinutes, false)), + _ => throw new Exception("unknown use_id " + cItem.use_id) + }; + + NetRewardData reward = new(); + RewardUtils.AddSingleCurrencyObject(user, ref reward, itemType, amount); + + response.Reward = reward; + // update client side item count + response.Reward.UserItems.Add(NetUtils.UserItemDataToNet(timeReward)); + + JsonDb.Save(); + + await WriteDataAsync(response); + } + } +} diff --git a/EpinelPS/LobbyServer/Outpost/GetRecycleRoomData.cs b/EpinelPS/LobbyServer/Outpost/GetRecycleRoomData.cs index 3002a46..1589842 100644 --- a/EpinelPS/LobbyServer/Outpost/GetRecycleRoomData.cs +++ b/EpinelPS/LobbyServer/Outpost/GetRecycleRoomData.cs @@ -1,4 +1,5 @@ -using EpinelPS.Utils; +using EpinelPS.Database; +using EpinelPS.Utils; namespace EpinelPS.LobbyServer.Outpost { @@ -8,10 +9,19 @@ namespace EpinelPS.LobbyServer.Outpost protected override async Task HandleAsync() { var req = await ReadData(); - - // TODO: save these things + var user = GetUser(); var response = new ResGetRecycleRoomData(); + response.Recycle.AddRange(user.ResearchProgress.Select(progress => + { + return new NetUserRecycleRoomData() + { + Tid = progress.Key, + Lv = progress.Value.Level, + Exp = progress.Value.Exp + }; + })); + await WriteDataAsync(response); } } diff --git a/EpinelPS/LobbyServer/Outpost/Recycle/LevelUpResearch.cs b/EpinelPS/LobbyServer/Outpost/Recycle/LevelUpResearch.cs new file mode 100644 index 0000000..d0da37c --- /dev/null +++ b/EpinelPS/LobbyServer/Outpost/Recycle/LevelUpResearch.cs @@ -0,0 +1,108 @@ +using EpinelPS.Data; +using EpinelPS.Database; +using EpinelPS.Utils; + +namespace EpinelPS.LobbyServer.Outpost.Recycle +{ + [PacketPath("/outpost/RecycleRoom/LevelUpResearch")] + public class LevelUpResearch : LobbyMsgHandler + { + protected override async Task HandleAsync() + { + /* + * Req Contains: + * Tid: int value, research tid + * Items: int value, used items. + */ + var req = await ReadData(); + var user = GetUser(); + var response = new ResRecycleLevelUpResearch(); + + user.ResearchProgress.TryGetValue(req.Tid, out var progress); + + // Check progress is null, null means research is not unlocked. + if (progress != null) + { + AddProgressToResearch(response, user, progress, req); + } + + JsonDb.Save(); + + await WriteDataAsync(response); + } + + private void AddProgressToResearch(ResRecycleLevelUpResearch response, User user, RecycleRoomResearchProgress progress, ReqRecycleLevelUpResearch req) + { + GameData.Instance.RecycleResearchStats.TryGetValue(req.Tid, out var statRecord); + if (statRecord is null) + return; + var levelRecord = GameData.Instance.RecycleResearchLevels.Values.Where(e => e.recycle_type == statRecord.recycle_type) + .FirstOrDefault(e => e.recycle_level == progress.Level); + if (levelRecord is null) + return; + + if (statRecord.recycle_type == "Personal") // main research + { + var usedItem = user.Items.FirstOrDefault(e => e.ItemType == levelRecord.item_id); // item_id equals level-up item's tid. + if (usedItem is null || usedItem.Count < levelRecord.item_value) + return; + + usedItem.Count -= levelRecord.item_value; + response.Items.Add(NetUtils.UserItemDataToNet(usedItem)); + + progress.Level += 1; + progress.Hp = statRecord.hp * progress.Level; + response.Recycle = new() + { + Tid = req.Tid, + Lv = progress.Level, + }; + } + else if (statRecord.recycle_type == "Class" || statRecord.recycle_type == "Corporation") // class research or corporation research + { + var netItem = req.Items.Single(); + var usedItem = user.Items.FirstOrDefault(e => e.ItemType == netItem.Tid); + if (usedItem is null) + return; + + usedItem.Count -= netItem.Count; + response.Items.Add(NetUtils.UserItemDataToNet(usedItem)); + (int newLevel, int newExp) = CalcCorpAndClassLevelUp(statRecord.recycle_type, netItem.Count, progress.Level, progress.Exp); + progress.Level = newLevel; + progress.Exp = newExp; + response.Recycle = new() + { + Tid = req.Tid, + Lv = newLevel, + Exp = newExp, + }; + } + else + { + throw new Exception($"unknown recycle type {statRecord.recycle_type}"); + } + } + + // First: level, Second: exp + private (int, int) CalcCorpAndClassLevelUp(string researchType, int itemCount, int startLevel = 1, int startExp = 0) + { + // levelRecord.exp is required exp to level up. + var levelRecords = GameData.Instance.RecycleResearchLevels.Values.Where(e => e.recycle_type == researchType && e.recycle_level > startLevel); + + foreach (var record in levelRecords) + { + if (itemCount < record.exp) + { + startExp += itemCount; + break; + } + + itemCount -= record.exp - startExp; + startLevel += 1; + startExp = 0; + } + + return (startLevel, startExp); + } + } +} diff --git a/EpinelPS/LobbyServer/Outpost/Recycle/RunResearch.cs b/EpinelPS/LobbyServer/Outpost/Recycle/RunResearch.cs index df7641f..c14c964 100644 --- a/EpinelPS/LobbyServer/Outpost/Recycle/RunResearch.cs +++ b/EpinelPS/LobbyServer/Outpost/Recycle/RunResearch.cs @@ -1,9 +1,6 @@ +using EpinelPS.Data; +using EpinelPS.Database; using EpinelPS.Utils; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace EpinelPS.LobbyServer.Outpost.Recycle { @@ -13,10 +10,33 @@ namespace EpinelPS.LobbyServer.Outpost.Recycle protected override async Task HandleAsync() { var req = await ReadData(); - + var user = GetUser(); var response = new ResRecycleRunResearch(); - // TODO - + + user.ResearchProgress.TryGetValue(req.Tid, out var progress); + + // Check progress is null, non-null means research is already unlocked. + if (progress is null) + { + var researchRecord = GameData.Instance.RecycleResearchStats.Values.FirstOrDefault(e => e.id == req.Tid) + ?? throw new Exception("not found research record with tid " + req.Tid); + progress = new() + { + Attack = researchRecord.attack, + Defense = researchRecord.defense, + Hp = researchRecord.hp, + }; + user.ResearchProgress.Add(req.Tid, progress); + } + response.Recycle = new() + { + Tid = req.Tid, + Lv = progress.Level, + Exp = progress.Exp, + }; + + JsonDb.Save(); + await WriteDataAsync(response); } } diff --git a/EpinelPS/Utils/NetUtils.cs b/EpinelPS/Utils/NetUtils.cs index f55603e..f39c6e8 100644 --- a/EpinelPS/Utils/NetUtils.cs +++ b/EpinelPS/Utils/NetUtils.cs @@ -393,7 +393,10 @@ namespace EpinelPS.Utils Logging.WriteLine("TODO: reward_value_max", LogType.Warning); } - RewardUtils.AddSingleObject(user, ref ret, winningRecord.reward_id, winningRecord.reward_type, winningRecord.reward_value_min); + if (winningRecord.reward_type == "Currency") + RewardUtils.AddSingleCurrencyObject(user, ref ret, (CurrencyType)winningRecord.reward_id, winningRecord.reward_value_min); + else + RewardUtils.AddSingleObject(user, ref ret, winningRecord.reward_id, winningRecord.reward_type, winningRecord.reward_value_min); } JsonDb.Save(); diff --git a/EpinelPS/Utils/RewardUtils.cs b/EpinelPS/Utils/RewardUtils.cs index 45d5015..faa2d44 100644 --- a/EpinelPS/Utils/RewardUtils.cs +++ b/EpinelPS/Utils/RewardUtils.cs @@ -73,17 +73,43 @@ namespace EpinelPS.Utils { if (!string.IsNullOrEmpty(item.reward_type)) { - if (item.reward_percent != 1000000) + if (item.reward_type == "Currency") { - Logging.WriteLine("WARNING: ignoring percent: " + item.reward_percent / 10000.0 + ", item will be added anyways", LogType.Warning); + AddSingleCurrencyObject(user, ref ret, (CurrencyType)item.reward_id, item.reward_value); } + else + { + if (item.reward_percent != 1000000) + { + Logging.WriteLine("WARNING: ignoring percent: " + item.reward_percent / 10000.0 + ", item will be added anyways", LogType.Warning); + } - AddSingleObject(user, ref ret, item.reward_id, item.reward_type, item.reward_value); + AddSingleObject(user, ref ret, item.reward_id, item.reward_type, item.reward_value); + } } } return ret; } + public static void AddSingleCurrencyObject(User user, ref NetRewardData ret, CurrencyType currencyType, long rewardCount) + { + bool found = user.Currency.Any(pair => pair.Key == currencyType); + + if (found) + { + user.Currency[currencyType] += rewardCount; + } + else + { + user.Currency.Add(currencyType, rewardCount); + } + ret.Currency.Add(new NetCurrencyData() + { + FinalValue = found ? user.GetCurrencyVal(currencyType) : rewardCount, + Value = rewardCount, + Type = (int)currencyType + }); + } /// /// Adds a single item to users inventory, and also adds it to ret parameter. /// @@ -98,37 +124,6 @@ namespace EpinelPS.Utils if (rewardId != 0 || !string.IsNullOrEmpty(rewardType)) { if (string.IsNullOrEmpty(rewardType) || string.IsNullOrWhiteSpace(rewardType)) { } - else if (rewardType == "Currency") - { - bool found = false; - foreach (var currentReward in user.Currency) - { - if (currentReward.Key == (CurrencyType)rewardId) - { - user.Currency[currentReward.Key] += rewardCount; - - ret.Currency.Add(new NetCurrencyData() - { - FinalValue = user.Currency[currentReward.Key], - Value = rewardCount, - Type = rewardId - }); - found = true; - break; - } - } - - if (!found) - { - user.Currency.Add((CurrencyType)rewardId, rewardCount); - ret.Currency.Add(new NetCurrencyData() - { - FinalValue = rewardCount, - Value = rewardCount, - Type = rewardId - }); - } - } else if (rewardType == "Item" || rewardType.StartsWith("Equipment_")) { // Check if user already has said item. If it is level 1, increase item count.