private static void HandleRandomHair(GameSession session, PacketReader packet) { int shopId = packet.ReadInt(); bool useVoucher = packet.ReadBool(); BeautyMetadata beautyShop = BeautyMetadataStorage.GetShopById(shopId); List <BeautyItem> beautyItems = BeautyMetadataStorage.GetGenderItems(beautyShop.ShopId, session.Player.Gender); if (!HandleShopPay(session, beautyShop, useVoucher)) { return; } // Grab random hair Random random = new Random(); int indexHair = random.Next(beautyItems.Count); BeautyItem chosenHair = beautyItems[indexHair]; //Grab a preset hair and length of hair ItemMetadata beautyItemData = ItemMetadataStorage.GetMetadata(chosenHair.ItemId); int indexPreset = random.Next(beautyItemData.HairPresets.Count); HairPresets chosenPreset = beautyItemData.HairPresets[indexPreset]; //Grab random front hair length double chosenFrontLength = random.NextDouble() * (beautyItemData.HairPresets[indexPreset].MaxScale - beautyItemData.HairPresets[indexPreset].MinScale) + beautyItemData.HairPresets[indexPreset].MinScale; //Grab random back hair length double chosenBackLength = random.NextDouble() * (beautyItemData.HairPresets[indexPreset].MaxScale - beautyItemData.HairPresets[indexPreset].MinScale) + beautyItemData.HairPresets[indexPreset].MinScale; // Grab random preset color ColorPaletteMetadata palette = ColorPaletteMetadataStorage.GetMetadata(2); // pick from palette 2. Seems like it's the correct palette for basic hair colors int indexColor = random.Next(palette.DefaultColors.Count); MixedColor color = palette.DefaultColors[indexColor]; Dictionary <ItemSlot, Item> equippedInventory = session.Player.GetEquippedInventory(InventoryTab.Gear); Item newHair = new Item(chosenHair.ItemId) { Color = EquipColor.Argb(color, indexColor, palette.PaletteId), HairD = new HairData((float)chosenBackLength, (float)chosenFrontLength, chosenPreset.BackPositionCoord, chosenPreset.BackPositionRotation, chosenPreset.FrontPositionCoord, chosenPreset.FrontPositionRotation), IsTemplate = false }; //Remove old hair if (session.Player.Equips.Remove(ItemSlot.HR, out Item previousHair)) { previousHair.Slot = -1; session.Player.HairInventory.RandomHair = previousHair; // store the previous hair session.FieldManager.BroadcastPacket(EquipmentPacket.UnequipItem(session.FieldPlayer, previousHair)); } equippedInventory[ItemSlot.HR] = newHair; session.FieldManager.BroadcastPacket(EquipmentPacket.EquipItem(session.FieldPlayer, newHair, ItemSlot.HR)); session.Send(BeautyPacket.RandomHairOption(previousHair, newHair)); }
public static void ChangeHair(GameSession session, int hairId, out Item previousHair, out Item newHair) { Random random = Random.Shared; //Grab a preset hair and length of hair ItemCustomizeMetadata customize = ItemMetadataStorage.GetMetadata(hairId).Customize; int indexPreset = random.Next(customize.HairPresets.Count); HairPresets chosenPreset = customize.HairPresets[indexPreset]; //Grab random front hair length double chosenFrontLength = random.NextDouble() * (customize.HairPresets[indexPreset].MaxScale - customize.HairPresets[indexPreset].MinScale) + customize.HairPresets[indexPreset].MinScale; //Grab random back hair length double chosenBackLength = random.NextDouble() * (customize.HairPresets[indexPreset].MaxScale - customize.HairPresets[indexPreset].MinScale) + customize.HairPresets[indexPreset].MinScale; // Grab random preset color ColorPaletteMetadata palette = ColorPaletteMetadataStorage.GetMetadata(2); // pick from palette 2. Seems like it's the correct palette for basic hair colors int indexColor = random.Next(palette.DefaultColors.Count); MixedColor color = palette.DefaultColors[indexColor]; newHair = new(hairId) { Color = EquipColor.Argb(color, indexColor, palette.PaletteId), HairData = new((float)chosenBackLength, (float)chosenFrontLength, chosenPreset.BackPositionCoord, chosenPreset.BackPositionRotation, chosenPreset.FrontPositionCoord, chosenPreset.FrontPositionRotation), IsEquipped = true, OwnerCharacterId = session.Player.CharacterId, OwnerCharacterName = session.Player.Name }; Dictionary <ItemSlot, Item> cosmetics = session.Player.Inventory.Cosmetics; //Remove old hair if (cosmetics.Remove(ItemSlot.HR, out previousHair)) { previousHair.Slot = -1; session.Player.HairInventory.RandomHair = previousHair; // store the previous hair DatabaseManager.Items.Delete(previousHair.Uid); session.FieldManager.BroadcastPacket(EquipmentPacket.UnequipItem(session.Player.FieldPlayer, previousHair)); } cosmetics[ItemSlot.HR] = newHair; }
protected override List <ItemMetadata> Parse() { // Item boxes Dictionary <string, List <ItemContent> > itemDrops = new Dictionary <string, List <ItemContent> >(); foreach (PackFileEntry entry in Resources.XmlFiles) { if (!entry.Name.StartsWith("table/individualitemdrop") && !entry.Name.StartsWith("table/na/individualitemdrop")) { continue; } XmlDocument innerDocument = Resources.XmlMemFile.GetDocument(entry.FileHeader); XmlNodeList individualBoxItems = innerDocument.SelectNodes($"/ms2/individualDropBox"); foreach (XmlNode individualBoxItem in individualBoxItems) { // Skip locales other than NA and null string locale = string.IsNullOrEmpty(individualBoxItem.Attributes["locale"]?.Value) ? "" : individualBoxItem.Attributes["locale"].Value; if (locale != "NA" && locale != "") { continue; } if (individualBoxItem.Attributes["minCount"].Value.Contains(".")) { continue; } string box = individualBoxItem.Attributes["individualDropBoxID"].Value; int id = int.Parse(individualBoxItem.Attributes["item"].Value); int minAmount = int.Parse(individualBoxItem.Attributes["minCount"].Value); int maxAmount = int.Parse(individualBoxItem.Attributes["maxCount"].Value); int dropGroup = int.Parse(individualBoxItem.Attributes["dropGroup"].Value); int smartDropRate = string.IsNullOrEmpty(individualBoxItem.Attributes["smartDropRate"]?.Value) ? 0 : int.Parse(individualBoxItem.Attributes["smartDropRate"].Value); int contentRarity = string.IsNullOrEmpty(individualBoxItem.Attributes["PackageUIShowGrade"]?.Value) ? 0 : int.Parse(individualBoxItem.Attributes["PackageUIShowGrade"].Value); int enchant = string.IsNullOrEmpty(individualBoxItem.Attributes["enchantLevel"]?.Value) ? 0 : int.Parse(individualBoxItem.Attributes["enchantLevel"].Value); int id2 = string.IsNullOrEmpty(individualBoxItem.Attributes["item2"]?.Value) ? 0 : int.Parse(individualBoxItem.Attributes["item2"].Value); ItemContent content = new ItemContent(id, minAmount, maxAmount, dropGroup, smartDropRate, contentRarity, enchant, id2); if (itemDrops.ContainsKey(box)) { itemDrops[box].Add(content); } else { itemDrops[box] = new List <ItemContent>() { content }; } } } // Item breaking ingredients Dictionary <int, List <ItemBreakReward> > rewards = new Dictionary <int, List <ItemBreakReward> >(); foreach (PackFileEntry entry in Resources.XmlFiles) { if (!entry.Name.StartsWith("table/itembreakingredient")) { continue; } XmlDocument innerDocument = Resources.XmlMemFile.GetDocument(entry.FileHeader); XmlNodeList individualItems = innerDocument.SelectNodes($"/ms2/item"); foreach (XmlNode nodes in individualItems) { string locale = string.IsNullOrEmpty(nodes.Attributes["locale"]?.Value) ? "" : nodes.Attributes["locale"].Value; if (locale != "NA" && locale != "") { continue; } int itemID = int.Parse(nodes.Attributes["ItemID"].Value); rewards[itemID] = new List <ItemBreakReward>(); int ingredientItemID1 = string.IsNullOrEmpty(nodes.Attributes["IngredientItemID1"]?.Value) ? 0 : int.Parse(nodes.Attributes["IngredientItemID1"].Value); int ingredientCount1 = string.IsNullOrEmpty(nodes.Attributes["IngredientCount1"]?.Value) ? 0 : int.Parse(nodes.Attributes["IngredientCount1"].Value); rewards[itemID].Add(new ItemBreakReward(ingredientItemID1, ingredientCount1)); int ingredientItemID2 = string.IsNullOrEmpty(nodes.Attributes["IngredientItemID2"]?.Value) ? 0 : int.Parse(nodes.Attributes["IngredientItemID2"].Value); int ingredientCount2 = string.IsNullOrEmpty(nodes.Attributes["IngredientCount2"]?.Value) ? 0 : int.Parse(nodes.Attributes["IngredientCount2"].Value); rewards[itemID].Add(new ItemBreakReward(ingredientItemID2, ingredientCount2)); int ingredientItemID3 = string.IsNullOrEmpty(nodes.Attributes["IngredientItemID3"]?.Value) ? 0 : int.Parse(nodes.Attributes["IngredientItemID3"].Value); int ingredientCount3 = string.IsNullOrEmpty(nodes.Attributes["IngredientCount3"]?.Value) ? 0 : int.Parse(nodes.Attributes["IngredientCount3"].Value); rewards[itemID].Add(new ItemBreakReward(ingredientItemID3, ingredientCount3)); } } // Items List <ItemMetadata> items = new List <ItemMetadata>(); foreach (PackFileEntry entry in Resources.XmlFiles) { if (!entry.Name.StartsWith("item/")) { continue; } ItemMetadata metadata = new ItemMetadata(); string filename = Path.GetFileNameWithoutExtension(entry.Name); int itemId = int.Parse(filename); if (items.Exists(item => item.Id == itemId)) { //Console.WriteLine($"Duplicate {entry.Name} was already added."); continue; } metadata.Id = itemId; Debug.Assert(metadata.Id > 0, $"Invalid Id {metadata.Id} from {itemId}"); // Parse XML XmlDocument document = Resources.XmlMemFile.GetDocument(entry.FileHeader); XmlNode item = document.SelectSingleNode("ms2/environment"); // Tag XmlNode basic = item.SelectSingleNode("basic"); metadata.Tag = basic.Attributes["stringTag"].Value; // Gear/Cosmetic slot XmlNode slots = item.SelectSingleNode("slots"); XmlNode slot = slots.FirstChild; bool slotResult = Enum.TryParse <ItemSlot>(slot.Attributes["name"].Value, out metadata.Slot); if (!slotResult && !string.IsNullOrEmpty(slot.Attributes["name"].Value)) { Console.WriteLine($"Failed to parse item slot for {itemId}: {slot.Attributes["name"].Value}"); } int totalSlots = slots.SelectNodes("slot").Count; if (totalSlots > 1) { if (metadata.Slot == ItemSlot.CL || metadata.Slot == ItemSlot.PA) { metadata.IsDress = true; } else if (metadata.Slot == ItemSlot.RH || metadata.Slot == ItemSlot.LH) { metadata.IsTwoHand = true; } } // Hair data if (slot.Attributes["name"].Value == "HR") { int assetNodeCount = slot.SelectNodes("asset").Count; XmlNode asset = slot.FirstChild; XmlNode scaleNode = slot.SelectSingleNode("scale"); if (assetNodeCount == 3) // This hair has a front and back positionable hair { XmlNode backHair = asset.NextSibling; // back hair info XmlNode frontHair = backHair.NextSibling; // front hair info int backHairNodes = backHair.SelectNodes("custom").Count; CoordF[] bPosCord = new CoordF[backHairNodes]; CoordF[] bPosRotation = new CoordF[backHairNodes]; CoordF[] fPosCord = new CoordF[backHairNodes]; CoordF[] fPosRotation = new CoordF[backHairNodes]; for (int i = 0; i < backHairNodes; i++) { foreach (XmlNode backPresets in backHair) { if (backPresets.Name == "custom") { bPosCord[i] = CoordF.Parse(backPresets.Attributes["position"].Value); bPosRotation[i] = CoordF.Parse(backPresets.Attributes["rotation"].Value); } } foreach (XmlNode frontPresets in frontHair) { if (frontPresets.Name == "custom") { fPosCord[i] = CoordF.Parse(frontPresets.Attributes["position"].Value); fPosRotation[i] = CoordF.Parse(frontPresets.Attributes["position"].Value); } } HairPresets hairPresets = new HairPresets() { }; hairPresets.BackPositionCoord = bPosCord[i]; hairPresets.BackPositionRotation = bPosRotation[i]; hairPresets.FrontPositionCoord = fPosCord[i]; hairPresets.FrontPositionRotation = fPosRotation[i]; if (scaleNode != null) { hairPresets.MinScale = float.Parse(scaleNode.Attributes["min"].Value ?? "0"); hairPresets.MaxScale = float.Parse(scaleNode.Attributes["max"].Value ?? "0"); } else { hairPresets.MinScale = 0; hairPresets.MaxScale = 0; } metadata.HairPresets.Add(hairPresets); } } else if (assetNodeCount == 2) // This hair only has back positionable hair { XmlNode backHair = asset.NextSibling; // back hair info int backHairNodes = backHair.SelectNodes("custom").Count; CoordF[] bPosCord = new CoordF[backHairNodes]; CoordF[] bPosRotation = new CoordF[backHairNodes]; for (int i = 0; i < backHairNodes; i++) { foreach (XmlNode backPresets in backHair) { if (backPresets.Name == "custom") { bPosCord[i] = CoordF.Parse(backPresets.Attributes["position"].Value); bPosRotation[i] = CoordF.Parse(backPresets.Attributes["rotation"].Value); } } HairPresets hairPresets = new HairPresets() { }; hairPresets.BackPositionCoord = bPosCord[i]; hairPresets.BackPositionRotation = bPosRotation[i]; hairPresets.FrontPositionCoord = CoordF.Parse("0, 0, 0"); hairPresets.FrontPositionRotation = CoordF.Parse("0, 0, 0"); if (scaleNode != null) { hairPresets.MinScale = float.Parse(scaleNode.Attributes["min"].Value ?? "0"); hairPresets.MaxScale = float.Parse(scaleNode.Attributes["max"].Value ?? "0"); } else { hairPresets.MinScale = 0; hairPresets.MaxScale = 0; } metadata.HairPresets.Add(hairPresets); } } else // hair does not have back or front positionable hair { HairPresets hairPresets = new HairPresets() { }; hairPresets.BackPositionCoord = CoordF.Parse("0, 0, 0"); hairPresets.BackPositionRotation = CoordF.Parse("0, 0, 0"); hairPresets.FrontPositionCoord = CoordF.Parse("0, 0, 0"); hairPresets.FrontPositionRotation = CoordF.Parse("0, 0, 0"); if (scaleNode != null) { hairPresets.MinScale = float.Parse(scaleNode.Attributes["min"].Value ?? "0"); hairPresets.MaxScale = float.Parse(scaleNode.Attributes["max"].Value ?? "0"); } else { hairPresets.MinScale = 0; hairPresets.MaxScale = 0; } metadata.HairPresets.Add(hairPresets); } } // Color data XmlNode customize = item.SelectSingleNode("customize"); metadata.ColorIndex = int.Parse(customize.Attributes["defaultColorIndex"].Value); metadata.ColorPalette = int.Parse(customize.Attributes["colorPalette"].Value); // Badge slot XmlNode gem = item.SelectSingleNode("gem"); bool gemResult = Enum.TryParse <GemSlot>(gem.Attributes["system"].Value, out metadata.Gem); if (!gemResult && !string.IsNullOrEmpty(gem.Attributes["system"].Value)) { Console.WriteLine($"Failed to parse badge slot for {itemId}: {gem.Attributes["system"].Value}"); } // Inventory tab and max stack size XmlNode property = item.SelectSingleNode("property"); try { byte type = byte.Parse(property.Attributes["type"].Value); byte subType = byte.Parse(property.Attributes["subtype"].Value); bool skin = byte.Parse(property.Attributes["skin"].Value) != 0; metadata.Tab = GetTab(type, subType, skin); metadata.IsTemplate = byte.Parse(property.Attributes["skinType"]?.Value ?? "0") == 99; // sales price XmlNode sell = property.SelectSingleNode("sell"); metadata.SellPrice = string.IsNullOrEmpty(sell.Attributes["price"]?.Value) ? null : sell.Attributes["price"].Value.Split(',').Select(int.Parse).ToList(); metadata.SellPriceCustom = string.IsNullOrEmpty(sell.Attributes["priceCustom"]?.Value) ? null : sell.Attributes["priceCustom"].Value.Split(',').Select(int.Parse).ToList(); } catch (Exception e) { Console.WriteLine($"Failed to parse tab slot for {itemId}: {e.Message}"); } metadata.StackLimit = int.Parse(property.Attributes["slotMax"].Value); // Rarity XmlNode option = item.SelectSingleNode("option"); int rarity = 1; if (option.Attributes["constant"].Value.Length == 1) { rarity = int.Parse(option.Attributes["constant"].Value); } metadata.Rarity = rarity; // Item boxes XmlNode function = item.SelectSingleNode("function"); string contentType = function.Attributes["name"].Value; metadata.FunctionData.Name = contentType; if (contentType == "OpenItemBox" || contentType == "SelectItemBox") { // selection boxes are SelectItemBox and 1,boxid // normal boxes are OpenItemBox and 0,1,0,boxid // fragments are OpenItemBox and 0,1,0,boxid,required_amount List <string> parameters = new List <string>(function.Attributes["parameter"].Value.Split(",")); // Remove empty params parameters.RemoveAll(param => param.Length == 0); if (parameters.Count >= 2) { string boxId = contentType == "OpenItemBox" ? parameters[3] : parameters[1]; foreach (KeyValuePair <string, List <ItemContent> > box in itemDrops) // Search for box id and set the rewards previously parsed { if (box.Key == boxId) { metadata.Content = box.Value; break; } } } } else if (contentType == "ChatEmoticonAdd") { string rawParameter = function.Attributes["parameter"].Value; string decodedParameter = HttpUtility.HtmlDecode(rawParameter); XmlDocument xmlParameter = new XmlDocument(); xmlParameter.LoadXml(decodedParameter); XmlNode functionParameters = xmlParameter.SelectSingleNode("v"); metadata.FunctionData.Id = byte.Parse(functionParameters.Attributes["id"].Value); int durationSec = 0; if (functionParameters.Attributes["durationSec"] != null) { durationSec = int.Parse(functionParameters.Attributes["durationSec"].Value); } metadata.FunctionData.Duration = durationSec; } else if (contentType == "OpenMassive") { string rawParameter = function.Attributes["parameter"].Value; string cleanParameter = rawParameter.Remove(1, 1); // remove the unwanted space string decodedParameter = HttpUtility.HtmlDecode(cleanParameter); XmlDocument xmlParameter = new XmlDocument(); xmlParameter.LoadXml(decodedParameter); XmlNode functionParameters = xmlParameter.SelectSingleNode("v"); metadata.FunctionData.FieldId = int.Parse(functionParameters.Attributes["fieldID"].Value); metadata.FunctionData.Duration = int.Parse(functionParameters.Attributes["portalDurationTick"].Value); metadata.FunctionData.Capacity = byte.Parse(functionParameters.Attributes["maxCount"].Value); } else if (contentType == "LevelPotion") { string rawParameter = function.Attributes["parameter"].Value; string decodedParameter = HttpUtility.HtmlDecode(rawParameter); XmlDocument xmlParameter = new XmlDocument(); xmlParameter.LoadXml(decodedParameter); XmlNode functionParameters = xmlParameter.SelectSingleNode("v"); metadata.FunctionData.TargetLevel = byte.Parse(functionParameters.Attributes["targetLevel"].Value); } else if (contentType == "VIPCoupon") { string rawParameter = function.Attributes["parameter"].Value; string decodedParameter = HttpUtility.HtmlDecode(rawParameter); XmlDocument xmlParameter = new XmlDocument(); xmlParameter.LoadXml(decodedParameter); XmlNode functionParameters = xmlParameter.SelectSingleNode("v"); metadata.FunctionData.Duration = int.Parse(functionParameters.Attributes["period"].Value); } else if (contentType == "HongBao") { string rawParameter = function.Attributes["parameter"].Value; string decodedParameter = HttpUtility.HtmlDecode(rawParameter); XmlDocument xmlParameter = new XmlDocument(); xmlParameter.LoadXml(decodedParameter); XmlNode functionParameters = xmlParameter.SelectSingleNode("v"); metadata.FunctionData.Id = int.Parse(functionParameters.Attributes["itemId"].Value); metadata.FunctionData.Count = short.Parse(functionParameters.Attributes["totalCount"].Value); metadata.FunctionData.TotalUser = byte.Parse(functionParameters.Attributes["totalUser"].Value); metadata.FunctionData.Duration = int.Parse(functionParameters.Attributes["durationSec"].Value); } else if (contentType == "SuperWorldChat") { string[] parameters = function.Attributes["parameter"].Value.Split(","); metadata.FunctionData.Id = int.Parse(parameters[0]); // only storing the first parameter. Not sure if the server uses the other 2. } else if (contentType == "OpenGachaBox") { string[] parameters = function.Attributes["parameter"].Value.Split(","); metadata.FunctionData.Id = int.Parse(parameters[0]); // only storing the first parameter. Unknown what the second parameter is used for. } else if (contentType == "OpenCoupleEffectBox") { string[] parameters = function.Attributes["parameter"].Value.Split(","); metadata.FunctionData.Id = int.Parse(parameters[0]); metadata.FunctionData.Rarity = byte.Parse(parameters[1]); } else if (contentType == "InstallBillBoard") { AdBalloonData balloon = new AdBalloonData(); string rawParameter = function.Attributes["parameter"].Value; string decodedParameter = HttpUtility.HtmlDecode(rawParameter); XmlDocument xmlParameter = new XmlDocument(); xmlParameter.LoadXml(decodedParameter); XmlNode functionParameters = xmlParameter.SelectSingleNode("v"); balloon.InteractId = int.Parse(functionParameters.Attributes["interactID"].Value); balloon.Duration = int.Parse(functionParameters.Attributes["durationSec"].Value); balloon.Model = functionParameters.Attributes["model"].Value; if (functionParameters.Attributes["asset"] != null) { balloon.Asset = functionParameters.Attributes["asset"].Value; } balloon.NormalState = functionParameters.Attributes["normal"].Value; balloon.Reactable = functionParameters.Attributes["reactable"].Value; if (functionParameters.Attributes["scale"] != null) { balloon.Scale = float.Parse(functionParameters.Attributes["scale"].Value); } metadata.AdBalloonData = balloon; } else if (contentType == "TitleScroll" || contentType == "ItemExchangeScroll" || contentType == "OpenInstrument" || contentType == "StoryBook" || contentType == "FishingRod") { metadata.FunctionData.Id = int.Parse(function.Attributes["parameter"].Value); } // Music score charges XmlNode musicScore = item.SelectSingleNode("MusicScore"); int playCount = int.Parse(musicScore.Attributes["playCount"].Value); metadata.PlayCount = playCount; string fileName = musicScore.Attributes["fileName"].Value; metadata.IsCustomScore = bool.Parse(musicScore.Attributes["isCustomNote"].Value); metadata.FileName = fileName; // Shop ID from currency items if (item["Shop"] != null) { XmlNode shop = item.SelectSingleNode("Shop"); metadata.ShopID = int.Parse(shop.Attributes["systemShopID"].Value); } XmlNode skill = item.SelectSingleNode("skill"); int skillID = int.Parse(skill.Attributes["skillID"].Value); metadata.SkillID = skillID; XmlNode limit = item.SelectSingleNode("limit"); bool enableBreak = byte.Parse(limit.Attributes["enableBreak"].Value) == 1; metadata.EnableBreak = enableBreak; int level = int.Parse(limit.Attributes["levelLimit"].Value); metadata.Level = level; if (!string.IsNullOrEmpty(limit.Attributes["recommendJobs"].Value)) { List <string> recommendJobs = new List <string>(limit.Attributes["recommendJobs"].Value.Split(",")); foreach (string recommendJob in recommendJobs) { metadata.RecommendJobs.Add(int.Parse(recommendJob)); } } metadata.Gender = byte.Parse(limit.Attributes["genderLimit"].Value); // Item breaking ingredients if (rewards.ContainsKey(itemId)) { metadata.BreakRewards = rewards[itemId]; } items.Add(metadata); } return(items); }
protected override List <ItemMetadata> Parse() { // Item breaking ingredients Dictionary <int, List <ItemBreakReward> > rewards = new Dictionary <int, List <ItemBreakReward> >(); foreach (PackFileEntry entry in Resources.XmlReader.Files) { if (!entry.Name.StartsWith("table/itembreakingredient")) { continue; } XmlDocument innerDocument = Resources.XmlReader.GetXmlDocument(entry); XmlNodeList individualItems = innerDocument.SelectNodes($"/ms2/item"); foreach (XmlNode nodes in individualItems) { string locale = nodes.Attributes["locale"]?.Value ?? ""; if (locale != "NA" && locale != "") { continue; } int itemID = int.Parse(nodes.Attributes["ItemID"].Value); rewards[itemID] = new List <ItemBreakReward>(); int ingredientItemID1 = int.Parse(nodes.Attributes["IngredientItemID1"]?.Value ?? "0"); int ingredientCount1 = int.Parse(nodes.Attributes["IngredientCount1"]?.Value ?? "0"); rewards[itemID].Add(new ItemBreakReward(ingredientItemID1, ingredientCount1)); _ = int.TryParse(nodes.Attributes["IngredientItemID2"]?.Value ?? "0", out int ingredientItemID2); _ = int.TryParse(nodes.Attributes["IngredientCount2"]?.Value ?? "0", out int ingredientCount2); rewards[itemID].Add(new ItemBreakReward(ingredientItemID2, ingredientCount2)); _ = int.TryParse(nodes.Attributes["IngredientItemID3"]?.Value ?? "0", out int ingredientItemID3); _ = int.TryParse(nodes.Attributes["IngredientCount3"]?.Value ?? "0", out int ingredientCount3); rewards[itemID].Add(new ItemBreakReward(ingredientItemID3, ingredientCount3)); } } // Item rarity Dictionary <int, int> rarities = new Dictionary <int, int>(); foreach (PackFileEntry entry in Resources.XmlReader.Files) { if (!entry.Name.StartsWith("table/na/itemwebfinder")) { continue; } XmlDocument innerDocument = Resources.XmlReader.GetXmlDocument(entry); XmlNodeList nodes = innerDocument.SelectNodes($"/ms2/key"); foreach (XmlNode node in nodes) { int itemId = int.Parse(node.Attributes["id"].Value); int rarity = int.Parse(node.Attributes["grade"].Value); rarities[itemId] = rarity; } } // Items List <ItemMetadata> items = new List <ItemMetadata>(); foreach (PackFileEntry entry in Resources.XmlReader.Files) { if (!entry.Name.StartsWith("item/")) { continue; } ItemMetadata metadata = new ItemMetadata(); string filename = Path.GetFileNameWithoutExtension(entry.Name); int itemId = int.Parse(filename); if (items.Exists(item => item.Id == itemId)) { continue; } metadata.Id = itemId; Debug.Assert(metadata.Id > 0, $"Invalid Id {metadata.Id} from {itemId}"); // Parse XML XmlDocument document = Resources.XmlReader.GetXmlDocument(entry); XmlNode item = document.SelectSingleNode("ms2/environment"); // Tag XmlNode basic = item.SelectSingleNode("basic"); metadata.Tag = basic.Attributes["stringTag"].Value; // Gear/Cosmetic slot XmlNode slots = item.SelectSingleNode("slots"); XmlNode slot = slots.FirstChild; bool slotResult = Enum.TryParse(slot.Attributes["name"].Value, out metadata.Slot); if (!slotResult && !string.IsNullOrEmpty(slot.Attributes["name"].Value)) { Console.WriteLine($"Failed to parse item slot for {itemId}: {slot.Attributes["name"].Value}"); } int totalSlots = slots.SelectNodes("slot").Count; if (totalSlots > 1) { if (metadata.Slot == ItemSlot.CL || metadata.Slot == ItemSlot.PA) { metadata.IsDress = true; } else if (metadata.Slot == ItemSlot.RH || metadata.Slot == ItemSlot.LH) { metadata.IsTwoHand = true; } } // Hair data if (slot.Attributes["name"].Value == "HR") { int assetNodeCount = slot.SelectNodes("asset").Count; XmlNode asset = slot.FirstChild; XmlNode scaleNode = slot.SelectSingleNode("scale"); if (assetNodeCount == 3) // This hair has a front and back positionable hair { XmlNode backHair = asset.NextSibling; // back hair info XmlNode frontHair = backHair.NextSibling; // front hair info int backHairNodes = backHair.SelectNodes("custom").Count; CoordF[] bPosCord = new CoordF[backHairNodes]; CoordF[] bPosRotation = new CoordF[backHairNodes]; CoordF[] fPosCord = new CoordF[backHairNodes]; CoordF[] fPosRotation = new CoordF[backHairNodes]; for (int i = 0; i < backHairNodes; i++) { foreach (XmlNode backPresets in backHair) { if (backPresets.Name == "custom") { bPosCord[i] = CoordF.Parse(backPresets.Attributes["position"].Value); bPosRotation[i] = CoordF.Parse(backPresets.Attributes["rotation"].Value); } } foreach (XmlNode frontPresets in frontHair) { if (frontPresets.Name == "custom") { fPosCord[i] = CoordF.Parse(frontPresets.Attributes["position"].Value); fPosRotation[i] = CoordF.Parse(frontPresets.Attributes["position"].Value); } } HairPresets hairPresets = new HairPresets() { }; hairPresets.BackPositionCoord = bPosCord[i]; hairPresets.BackPositionRotation = bPosRotation[i]; hairPresets.FrontPositionCoord = fPosCord[i]; hairPresets.FrontPositionRotation = fPosRotation[i]; hairPresets.MinScale = float.Parse(scaleNode?.Attributes["min"]?.Value ?? "0"); hairPresets.MaxScale = float.Parse(scaleNode?.Attributes["max"]?.Value ?? "0"); metadata.HairPresets.Add(hairPresets); } } else if (assetNodeCount == 2) // This hair only has back positionable hair { XmlNode backHair = asset.NextSibling; // back hair info int backHairNodes = backHair.SelectNodes("custom").Count; CoordF[] bPosCord = new CoordF[backHairNodes]; CoordF[] bPosRotation = new CoordF[backHairNodes]; for (int i = 0; i < backHairNodes; i++) { foreach (XmlNode backPresets in backHair) { if (backPresets.Name == "custom") { bPosCord[i] = CoordF.Parse(backPresets.Attributes["position"].Value); bPosRotation[i] = CoordF.Parse(backPresets.Attributes["rotation"].Value); } } HairPresets hairPresets = new HairPresets() { }; hairPresets.BackPositionCoord = bPosCord[i]; hairPresets.BackPositionRotation = bPosRotation[i]; hairPresets.FrontPositionCoord = CoordF.Parse("0, 0, 0"); hairPresets.FrontPositionRotation = CoordF.Parse("0, 0, 0"); hairPresets.MinScale = float.Parse(scaleNode?.Attributes["min"]?.Value ?? "0"); hairPresets.MaxScale = float.Parse(scaleNode?.Attributes["max"]?.Value ?? "0"); metadata.HairPresets.Add(hairPresets); } } else // hair does not have back or front positionable hair { HairPresets hairPresets = new HairPresets() { }; hairPresets.BackPositionCoord = CoordF.Parse("0, 0, 0"); hairPresets.BackPositionRotation = CoordF.Parse("0, 0, 0"); hairPresets.FrontPositionCoord = CoordF.Parse("0, 0, 0"); hairPresets.FrontPositionRotation = CoordF.Parse("0, 0, 0"); hairPresets.MinScale = float.Parse(scaleNode?.Attributes["min"]?.Value ?? "0"); hairPresets.MaxScale = float.Parse(scaleNode?.Attributes["max"]?.Value ?? "0"); metadata.HairPresets.Add(hairPresets); } } // Color data XmlNode customize = item.SelectSingleNode("customize"); metadata.ColorIndex = int.Parse(customize.Attributes["defaultColorIndex"].Value); metadata.ColorPalette = int.Parse(customize.Attributes["colorPalette"].Value); // Badge slot XmlNode gem = item.SelectSingleNode("gem"); bool gemResult = Enum.TryParse(gem.Attributes["system"].Value, out metadata.Gem); if (!gemResult && !string.IsNullOrEmpty(gem.Attributes["system"].Value)) { Console.WriteLine($"Failed to parse badge slot for {itemId}: {gem.Attributes["system"].Value}"); } // Inventory tab and max stack size XmlNode property = item.SelectSingleNode("property"); try { byte type = byte.Parse(property.Attributes["type"].Value); byte subType = byte.Parse(property.Attributes["subtype"].Value); bool skin = byte.Parse(property.Attributes["skin"].Value) != 0; metadata.Tab = GetTab(type, subType, skin); metadata.IsTemplate = byte.Parse(property.Attributes["skinType"]?.Value ?? "0") == 99; metadata.TradeableCount = byte.Parse(property.Attributes["tradableCount"].Value); metadata.RepackageCount = byte.Parse(property.Attributes["rePackingLimitCount"].Value); metadata.RepackageItemConsumeCount = byte.Parse(property.Attributes["rePackingItemConsumeCount"].Value); // sales price XmlNode sell = property.SelectSingleNode("sell"); metadata.SellPrice = sell.Attributes["price"]?.Value.Split(',').Select(int.Parse).ToList() ?? null; metadata.SellPriceCustom = sell.Attributes["priceCustom"]?.Value.Split(',').Select(int.Parse).ToList() ?? null; } catch (Exception e) { Console.WriteLine($"Failed to parse tab slot for {itemId}: {e.Message}"); } metadata.StackLimit = int.Parse(property.Attributes["slotMax"].Value); // Rarity XmlNode option = item.SelectSingleNode("option"); metadata.OptionStatic = int.Parse(option.Attributes["static"].Value); metadata.OptionRandom = int.Parse(option.Attributes["random"].Value); metadata.OptionConstant = int.Parse(option.Attributes["constant"].Value); metadata.OptionLevelFactor = int.Parse(option.Attributes["optionLevelFactor"].Value); XmlNode function = item.SelectSingleNode("function"); string contentType = function.Attributes["name"].Value; metadata.FunctionData.Name = contentType; // Item boxes if (contentType == "OpenItemBox") { // selection boxes are SelectItemBox and 1,boxid // normal boxes are OpenItemBox and 0,1,0,boxid // fragments are OpenItemBox and 0,1,0,boxid,required_amount if (function.Attributes["parameter"].Value.Contains('l')) { continue; // TODO: Implement these CN items. Skipping for now } List <string> parameters = new List <string>(function.Attributes["parameter"].Value.Split(",")); OpenItemBox box = new OpenItemBox(); box.RequiredItemId = int.Parse(parameters[0]); box.ReceiveOneItem = parameters[1] == "1"; box.BoxId = int.Parse(parameters[3]); box.AmountRequired = 1; if (parameters.Count == 5) { box.AmountRequired = int.Parse(parameters[4]); } metadata.FunctionData.OpenItemBox = box; } else if (contentType == "SelectItemBox") { if (function.Attributes["parameter"].Value.Contains('l')) { continue; // TODO: Implement these CN items. Skipping for now } List <string> parameters = new List <string>(function.Attributes["parameter"].Value.Split(",")); parameters.RemoveAll(param => param.Length == 0); SelectItemBox box = new SelectItemBox(); box.GroupId = int.Parse(parameters[0]); box.BoxId = int.Parse(parameters[1]); metadata.FunctionData.SelectItemBox = box; } else if (contentType == "ChatEmoticonAdd") { ChatEmoticonAdd sticker = new ChatEmoticonAdd(); string rawParameter = function.Attributes["parameter"].Value; string decodedParameter = HttpUtility.HtmlDecode(rawParameter); XmlDocument xmlParameter = new XmlDocument(); xmlParameter.LoadXml(decodedParameter); XmlNode functionParameters = xmlParameter.SelectSingleNode("v"); sticker.Id = byte.Parse(functionParameters.Attributes["id"].Value); sticker.Duration = int.Parse(functionParameters.Attributes["durationSec"]?.Value ?? "0"); metadata.FunctionData.ChatEmoticonAdd = sticker; } else if (contentType == "OpenMassive") { OpenMassiveEvent massiveEvent = new OpenMassiveEvent(); string rawParameter = function.Attributes["parameter"].Value; string cleanParameter = rawParameter.Remove(1, 1); // remove the unwanted space string decodedParameter = HttpUtility.HtmlDecode(cleanParameter); XmlDocument xmlParameter = new XmlDocument(); xmlParameter.LoadXml(decodedParameter); XmlNode functionParameters = xmlParameter.SelectSingleNode("v"); massiveEvent.FieldId = int.Parse(functionParameters.Attributes["fieldID"].Value); massiveEvent.Duration = int.Parse(functionParameters.Attributes["portalDurationTick"].Value); massiveEvent.Capacity = byte.Parse(functionParameters.Attributes["maxCount"].Value); metadata.FunctionData.OpenMassiveEvent = massiveEvent; } else if (contentType == "LevelPotion") { LevelPotion levelPotion = new LevelPotion(); string rawParameter = function.Attributes["parameter"].Value; string decodedParameter = HttpUtility.HtmlDecode(rawParameter); XmlDocument xmlParameter = new XmlDocument(); xmlParameter.LoadXml(decodedParameter); XmlNode functionParameters = xmlParameter.SelectSingleNode("v"); levelPotion.TargetLevel = byte.Parse(functionParameters.Attributes["targetLevel"].Value); metadata.FunctionData.LevelPotion = levelPotion; } else if (contentType == "VIPCoupon") { VIPCoupon coupon = new VIPCoupon(); string rawParameter = function.Attributes["parameter"].Value; string decodedParameter = HttpUtility.HtmlDecode(rawParameter); XmlDocument xmlParameter = new XmlDocument(); xmlParameter.LoadXml(decodedParameter); XmlNode functionParameters = xmlParameter.SelectSingleNode("v"); coupon.Duration = int.Parse(functionParameters.Attributes["period"].Value); metadata.FunctionData.VIPCoupon = coupon; } else if (contentType == "HongBao") { HongBaoData hongBao = new HongBaoData(); string rawParameter = function.Attributes["parameter"].Value; string decodedParameter = HttpUtility.HtmlDecode(rawParameter); XmlDocument xmlParameter = new XmlDocument(); xmlParameter.LoadXml(decodedParameter); XmlNode functionParameters = xmlParameter.SelectSingleNode("v"); hongBao.Id = int.Parse(functionParameters.Attributes["itemId"].Value); hongBao.Count = short.Parse(functionParameters.Attributes["totalCount"].Value); hongBao.TotalUsers = byte.Parse(functionParameters.Attributes["totalUser"].Value); hongBao.Duration = int.Parse(functionParameters.Attributes["durationSec"].Value); metadata.FunctionData.HongBao = hongBao; } else if (contentType == "SuperWorldChat") { string[] parameters = function.Attributes["parameter"].Value.Split(","); metadata.FunctionData.Id = int.Parse(parameters[0]); // only storing the first parameter. Not sure if the server uses the other 2. } else if (contentType == "OpenGachaBox") { string[] parameters = function.Attributes["parameter"].Value.Split(","); metadata.FunctionData.Id = int.Parse(parameters[0]); // only storing the first parameter. Unknown what the second parameter is used for. } else if (contentType == "OpenCoupleEffectBox") { OpenCoupleEffectBox box = new OpenCoupleEffectBox(); string[] parameters = function.Attributes["parameter"].Value.Split(","); box.Id = int.Parse(parameters[0]); box.Rarity = byte.Parse(parameters[1]); metadata.FunctionData.OpenCoupleEffectBox = box; } else if (contentType == "InstallBillBoard") { InstallBillboard balloon = new InstallBillboard(); string rawParameter = function.Attributes["parameter"].Value; string decodedParameter = HttpUtility.HtmlDecode(rawParameter); XmlDocument xmlParameter = new XmlDocument(); xmlParameter.LoadXml(decodedParameter); XmlNode functionParameters = xmlParameter.SelectSingleNode("v"); balloon.InteractId = int.Parse(functionParameters.Attributes["interactID"].Value); balloon.Duration = int.Parse(functionParameters.Attributes["durationSec"].Value); balloon.Model = functionParameters.Attributes["model"].Value; balloon.Asset = functionParameters.Attributes["asset"]?.Value ?? ""; balloon.NormalState = functionParameters.Attributes["normal"].Value; balloon.Reactable = functionParameters.Attributes["reactable"].Value; balloon.Scale = float.Parse(functionParameters.Attributes["scale"]?.Value ?? "0"); metadata.FunctionData.InstallBillboard = balloon; } else if (contentType == "TitleScroll" || contentType == "ItemExchangeScroll" || contentType == "OpenInstrument" || contentType == "StoryBook" || contentType == "FishingRod" || contentType == "ItemChangeBeauty" || contentType == "ItemRePackingScroll") { metadata.FunctionData.Id = int.Parse(function.Attributes["parameter"].Value); } // Music score charges XmlNode musicScore = item.SelectSingleNode("MusicScore"); metadata.PlayCount = int.Parse(musicScore.Attributes["playCount"].Value); metadata.FileName = musicScore.Attributes["fileName"].Value; metadata.IsCustomScore = bool.Parse(musicScore.Attributes["isCustomNote"].Value); // Shop ID from currency items if (item["Shop"] != null) { XmlNode shop = item.SelectSingleNode("Shop"); metadata.ShopID = int.Parse(shop.Attributes["systemShopID"].Value); } XmlNode skill = item.SelectSingleNode("skill"); metadata.SkillID = int.Parse(skill.Attributes["skillID"].Value); XmlNode limit = item.SelectSingleNode("limit"); metadata.EnableBreak = byte.Parse(limit.Attributes["enableBreak"].Value) == 1; metadata.Level = int.Parse(limit.Attributes["levelLimit"].Value); metadata.TransferType = (TransferType)byte.Parse(limit.Attributes["transferType"].Value); metadata.Sellable = byte.Parse(limit.Attributes["shopSell"].Value) == 1; metadata.RecommendJobs = limit.Attributes["recommendJobs"]?.Value.Split(",").Where(x => !string.IsNullOrEmpty(x)).Select(int.Parse).ToList(); metadata.Gender = byte.Parse(limit.Attributes["genderLimit"].Value); XmlNode installNode = item.SelectSingleNode("install"); metadata.IsCubeSolid = byte.Parse(installNode.Attributes["cubeProp"].Value) == 1; metadata.ObjectId = int.Parse(installNode.Attributes["objCode"].Value); XmlNode housingNode = item.SelectSingleNode("housing"); string value = housingNode.Attributes["categoryTag"]?.Value; if (value is not null) { List <string> categories = new List <string>(value.Split(",")); _ = short.TryParse(categories[0], out short category); metadata.HousingCategory = (ItemHousingCategory)category; } // Item breaking ingredients if (rewards.ContainsKey(itemId)) { metadata.BreakRewards = rewards[itemId]; } if (rarities.ContainsKey(itemId)) { metadata.Rarity = rarities[itemId]; } items.Add(metadata); } return(items); }