コード例 #1
0
        static async Task BatchExportItem(string path, IItemModel item, XivModelInfo secondaryModelInfo, Func <Task <List <XivRace> > > getRaces)
        {
            if (File.Exists(path))
            {
                return;
            }

            WriteLine($"Exporting {item.GetType().Name} {item.Name}: {Path.GetFileNameWithoutExtension(path)}");

            var metadata = new ExportMetadata();

            metadata.Name = item.Name;

            var mdl   = new Mdl(_gameDir, item.DataFile);
            var races = await getRaces();

            foreach (var race in races)
            {
                var mdlData = await mdl.GetMdlData(item, race, secondaryModelInfo);

                var textures = await TexTools.MaterialsHelper.GetMaterials(_gameDir, item, mdlData, race);

                var set = BatchExportSet(mdlData, textures);
                set.Name = TexTools.XivStringRaces.ToRaceGenderName(race);
                metadata.Sets.Add(set);
            }

            var metadataJson = JsonConvert.SerializeObject(metadata);

            File.WriteAllText(path, metadataJson);
        }
コード例 #2
0
        /// <summary>
        /// Gets the model info from the modelchara exd data
        /// </summary>
        /// <param name="modelCharaEx">The modelchara ex data</param>
        /// <param name="index">The index of the data</param>
        /// <returns>The XivModelInfo data</returns>
        public static XivModelInfo GetModelInfo(Dictionary <int, byte[]> modelCharaEx, int index)
        {
            var xivModelInfo = new XivModelInfo();

            // These are the offsets to relevant data
            // These will need to be changed if data gets added or removed with a patch
            const int modelDataOffset = 4;

            // Big Endian Byte Order
            using (var br = new BinaryReaderBE(new MemoryStream(modelCharaEx[index])))
            {
                xivModelInfo.ModelID = br.ReadInt16();

                br.BaseStream.Seek(modelDataOffset, SeekOrigin.Begin);
                var modelType = br.ReadByte();
                xivModelInfo.Body    = br.ReadByte();
                xivModelInfo.Variant = br.ReadByte();

                if (modelType == 2)
                {
                    xivModelInfo.ModelType = XivItemType.demihuman;
                }
                else if (modelType == 3)
                {
                    xivModelInfo.ModelType = XivItemType.monster;
                }
                else
                {
                    xivModelInfo.ModelType = XivItemType.unknown;
                }
            }

            return(xivModelInfo);
        }
コード例 #3
0
ファイル: Imc.cs プロジェクト: goaaats/xivModdingFramework
        /// <summary>
        /// Gets the relevant IMC information for a given item
        /// </summary>
        /// <param name="item">The item to get the version for</param>
        /// <param name="modelInfo">The model info of the item</param>
        /// <returns>The XivImc Data</returns>
        public XivImc GetImcInfo(IItemModel item, XivModelInfo modelInfo)
        {
            var xivImc = new XivImc();

            // These are the offsets to relevant data
            // These will need to be changed if data gets added or removed with a patch
            const int headerLength     = 4;
            const int variantLength    = 6;
            const int variantSetLength = 30;

            var index = new Index(_gameDirectory);
            var dat   = new Dat(_gameDirectory);

            var itemType = ItemType.GetItemType(item);
            var imcPath  = GetImcPath(modelInfo, itemType);

            var imcOffset = index.GetDataOffset(HashGenerator.GetHash(imcPath.Folder), HashGenerator.GetHash(imcPath.File), _dataFile);

            if (imcOffset == 0)
            {
                throw new Exception($"Could not find offest for {imcPath.Folder}/{imcPath.File}");
            }

            var imcData = dat.GetType2Data(imcOffset, _dataFile);

            using (var br = new BinaryReader(new MemoryStream(imcData)))
            {
                int variantOffset;

                if (itemType == XivItemType.weapon || itemType == XivItemType.monster)
                {
                    // weapons and monsters do not have variant sets
                    variantOffset = (modelInfo.Variant * variantLength) + headerLength;
                }
                else
                {
                    // Variant Sets contain 5 variants for each slot
                    // These can be Head, Body, Hands, Legs, Feet  or  Ears, Neck, Wrists, LRing, RRing
                    // This skips to the correct variant set, then to the correct slot within that set for the item
                    variantOffset = (modelInfo.Variant * variantSetLength) + (_slotOffsetDictionary[item.ItemCategory] * variantLength) + headerLength;
                }

                br.BaseStream.Seek(variantOffset, SeekOrigin.Begin);

                xivImc.Version = br.ReadByte();
                var unknown = br.ReadByte();
                xivImc.Mask = br.ReadUInt16();
                xivImc.Vfx  = br.ReadUInt16();
            }

            return(xivImc);
        }
コード例 #4
0
        /// <summary>
        /// Gets the IMC internal path for the given model info
        /// </summary>
        /// <param name="modelInfo">The model info of the item</param>
        /// <param name="itemType">The type of the item</param>
        /// <returns>A touple containing the Folder and File strings</returns>
        private static (string Folder, string File) GetImcPath(XivModelInfo modelInfo, XivItemType itemType)
        {
            string imcFolder;
            string imcFile;

            var modelID = modelInfo.ModelID.ToString().PadLeft(4, '0');
            var body    = modelInfo.Body.ToString().PadLeft(4, '0');

            switch (itemType)
            {
            case XivItemType.equipment:
                imcFolder = $"chara/{itemType}/e{modelID}";
                imcFile   = $"e{modelID}{ImcExtension}";
                break;

            case XivItemType.accessory:
                imcFolder = $"chara/{itemType}/a{modelID}";
                imcFile   = $"a{modelID}{ImcExtension}";
                break;

            case XivItemType.weapon:
                imcFolder = $"chara/{itemType}/w{modelID}/obj/body/b{body}";
                imcFile   = $"b{body}{ImcExtension}";
                break;

            case XivItemType.monster:
                imcFolder = $"chara/{itemType}/m{modelID}/obj/body/b{body}";
                imcFile   = $"b{body}{ImcExtension}";
                break;

            case XivItemType.demihuman:
                imcFolder = $"chara/{itemType}/d{modelID}/obj/equipment/e{body}";
                imcFile   = $"e{body}{ImcExtension}";
                break;

            default:
                imcFolder = "";
                imcFile   = "";
                break;
            }

            return(imcFolder, imcFile);
        }
コード例 #5
0
        /// <summary>
        /// Get this item's root folder in the FFXIV internal directory structure.
        /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public static string GetItemRootFolder(this IItem item)
        {
            var          primaryType   = item.GetPrimaryItemType();
            var          secondaryType = item.GetSecondaryItemType();
            var          primaryId     = "";
            var          secondaryId   = "";
            XivModelInfo modelInfo     = null;

            try {
                // Hitting this catch 60,000 time for all the UI elements is really slow.
                if (item != null && primaryType != XivItemType.ui)
                {
                    modelInfo = ((IItemModel)item).ModelInfo;
                    if (modelInfo != null)
                    {
                        primaryId   = modelInfo.PrimaryID.ToString().PadLeft(4, '0');
                        secondaryId = modelInfo.SecondaryID.ToString().PadLeft(4, '0');
                    }
                }
            } catch (Exception ex)
            {
                // No-op.  If it failed it's one of the types we're not going to use it modelInfo on anyways.
            }


            if (primaryType == XivItemType.monster)
            {
                return("chara/monster/m" + primaryId + "/obj/body/b" + secondaryId);
            }
            else if (primaryType == XivItemType.demihuman)
            {
                return("chara/demihuman/d" + primaryId + "/obj/equipment/e" + secondaryId);
            }
            else if (primaryType == XivItemType.equipment)
            {
                return("chara/equipment/e" + primaryId);
            }
            else if (primaryType == XivItemType.accessory)
            {
                return("chara/accessory/a" + primaryId);
            }
            else if (primaryType == XivItemType.ui)
            {
                if (item.SecondaryCategory == XivStrings.Paintings)
                {
                    try
                    {
                        var furnitureItem = (IItemModel)item;
                        modelInfo = furnitureItem.ModelInfo;
                        return("ui/icon/" + modelInfo.PrimaryID.ToString().PadLeft(6, '0'));
                    } catch
                    {
                        var uiItem = (XivUi)item;
                        return("ui/icon/" + uiItem.IconNumber.ToString().PadLeft(6, '0'));
                    }
                }
                else
                {
                    var uiItem = (XivUi)item;
                    return(uiItem.UiPath + uiItem.IconNumber.ToString().PadLeft(6, '0'));
                }
            }
            else if (primaryType == XivItemType.furniture)
            {
                if (item.SecondaryCategory == XivStrings.Paintings)
                {
                    try
                    {
                        var furnitureItem = (IItemModel)item;
                        modelInfo = furnitureItem.ModelInfo;
                        return("ui/icon/" + modelInfo.PrimaryID.ToString().PadLeft(6, '0'));
                    }
                    catch
                    {
                        var uiItem = (XivUi)item;
                        return("ui/icon/" + uiItem.IconNumber.ToString().PadLeft(6, '0'));
                    }
                }
                else
                {
                    var ret = "bgcommon/hou/";
                    if (item.SecondaryCategory == XivStrings.Furniture_Indoor)
                    {
                        ret += "indoor/";
                    }
                    else if (item.SecondaryCategory == XivStrings.Furniture_Outdoor)
                    {
                        ret += "outdoor/";
                    }

                    ret += "general/" + primaryId;
                    return(ret);
                }
            }
            else if (primaryType == XivItemType.weapon)
            {
                return("chara/weapon/w" + primaryId + "/obj/body/b" + secondaryId);
            }
            else if (primaryType == XivItemType.human)
            {
                var ret = "chara/human/c" + primaryId + "/obj/";
                if (secondaryType == XivItemType.body)
                {
                    ret += "body/b";
                }
                else if (secondaryType == XivItemType.face)
                {
                    ret += "face/f";
                }
                else if (secondaryType == XivItemType.tail)
                {
                    ret += "tail/t";
                }
                else if (secondaryType == XivItemType.hair)
                {
                    ret += "hair/h";
                }
                else if (secondaryType == XivItemType.ear)
                {
                    ret += "zear/z";
                }

                ret += secondaryId;
                return(ret);
            }
            else if (primaryType == XivItemType.decal)
            {
                if (item.SecondaryCategory == XivStrings.Face_Paint)
                {
                    return("chara/common/texture/decal_face");
                }
                if (item.SecondaryCategory == XivStrings.Equipment_Decals)
                {
                    return("chara/common/texture/decal_equip");
                }
            }
            return("");
        }
コード例 #6
0
        /// <summary>
        /// Gets the full IMC information for a given item
        /// </summary>
        /// <param name="item"></param>
        /// <param name="modelInfo"></param>
        /// <returns>The ImcData data</returns>
        public async Task <ImcData> GetFullImcInfo(IItemModel item, XivModelInfo modelInfo)
        {
            var index = new Index(_gameDirectory);
            var dat   = new Dat(_gameDirectory);

            var itemType = ItemType.GetItemType(item);
            var imcPath  = GetImcPath(modelInfo, itemType);

            var imcOffset = await index.GetDataOffset(HashGenerator.GetHash(imcPath.Folder), HashGenerator.GetHash(imcPath.File), _dataFile);

            if (imcOffset == 0)
            {
                throw new Exception($"Could not find offset for {imcPath.Folder}/{imcPath.File}");
            }

            var imcByteData = await dat.GetType2Data(imcOffset, _dataFile);

            return(await Task.Run(() =>
            {
                using (var br = new BinaryReader(new MemoryStream(imcByteData)))
                {
                    var imcData = new ImcData()
                    {
                        VariantCount = br.ReadInt16(),
                        Unknown = br.ReadInt16(),
                        GearVariantList = new List <VariantSet>()
                    };

                    //weapons and monsters do not have variant sets
                    if (itemType == XivItemType.weapon || itemType == XivItemType.monster)
                    {
                        imcData.OtherVariantList = new List <XivImc>();

                        imcData.DefaultVariant = new XivImc
                        {
                            Version = br.ReadUInt16(),
                            Mask = br.ReadUInt16(),
                            Vfx = br.ReadUInt16()
                        };

                        for (var i = 0; i < imcData.VariantCount; i++)
                        {
                            imcData.OtherVariantList.Add(new XivImc
                            {
                                Version = br.ReadUInt16(), Mask = br.ReadUInt16(), Vfx = br.ReadUInt16()
                            });
                        }
                    }
                    else
                    {
                        imcData.GearVariantList = new List <VariantSet>();

                        imcData.DefaultVariantSet = new VariantSet
                        {
                            Slot1 = new XivImc
                            {
                                Version = br.ReadUInt16(), Mask = br.ReadUInt16(), Vfx = br.ReadUInt16()
                            },
                            Slot2 = new XivImc
                            {
                                Version = br.ReadUInt16(), Mask = br.ReadUInt16(), Vfx = br.ReadUInt16()
                            },
                            Slot3 = new XivImc
                            {
                                Version = br.ReadUInt16(), Mask = br.ReadUInt16(), Vfx = br.ReadUInt16()
                            },
                            Slot4 = new XivImc
                            {
                                Version = br.ReadUInt16(), Mask = br.ReadUInt16(), Vfx = br.ReadUInt16()
                            },
                            Slot5 = new XivImc
                            {
                                Version = br.ReadUInt16(), Mask = br.ReadUInt16(), Vfx = br.ReadUInt16()
                            },
                        };

                        for (var i = 0; i < imcData.VariantCount; i++)
                        {
                            // gets the data for each slot in the current variant set
                            var imcGear = new VariantSet
                            {
                                Slot1 = new XivImc
                                {
                                    Version = br.ReadUInt16(), Mask = br.ReadUInt16(), Vfx = br.ReadUInt16()
                                },
                                Slot2 = new XivImc
                                {
                                    Version = br.ReadUInt16(), Mask = br.ReadUInt16(), Vfx = br.ReadUInt16()
                                },
                                Slot3 = new XivImc
                                {
                                    Version = br.ReadUInt16(), Mask = br.ReadUInt16(), Vfx = br.ReadUInt16()
                                },
                                Slot4 = new XivImc
                                {
                                    Version = br.ReadUInt16(), Mask = br.ReadUInt16(), Vfx = br.ReadUInt16()
                                },
                                Slot5 = new XivImc
                                {
                                    Version = br.ReadUInt16(), Mask = br.ReadUInt16(), Vfx = br.ReadUInt16()
                                },
                            };

                            imcData.GearVariantList.Add(imcGear);
                        }
                    }

                    return imcData;
                }
            }));
        }
コード例 #7
0
        /// <summary>
        /// Gets the relevant IMC information for a given item
        /// </summary>
        /// <param name="item">The item to get the version for</param>
        /// <param name="modelInfo">The model info of the item</param>
        /// <returns>The XivImc Data</returns>
        public async Task <XivImc> GetImcInfo(IItemModel item, XivModelInfo modelInfo)
        {
            var xivImc = new XivImc();

            // These are the offsets to relevant data
            // These will need to be changed if data gets added or removed with a patch
            const int headerLength     = 4;
            const int variantLength    = 6;
            const int variantSetLength = 30;

            var itemType = ItemType.GetItemType(item);
            var imcPath  = GetImcPath(modelInfo, itemType);

            var itemCategory = item.ItemCategory;

            var imcOffset = await _modding.Index.GetDataOffset(HashGenerator.GetHash(imcPath.Folder),
                                                               HashGenerator.GetHash(imcPath.File), _dataFile);

            if (imcOffset == 0)
            {
                if (item.ItemCategory == XivStrings.Two_Handed)
                {
                    itemCategory = XivStrings.Hands;
                    itemType     = XivItemType.equipment;
                    imcPath      = GetImcPath(modelInfo, itemType);

                    imcOffset = await _modding.Index.GetDataOffset(HashGenerator.GetHash(imcPath.Folder),
                                                                   HashGenerator.GetHash(imcPath.File), _dataFile);

                    if (imcOffset == 0)
                    {
                        throw new Exception($"Could not find offset for {imcPath.Folder}/{imcPath.File}");
                    }

                    ChangedType = true;
                }
                else
                {
                    throw new Exception($"Could not find offset for {imcPath.Folder}/{imcPath.File}");
                }
            }

            var imcData = await _modding.Dat.GetType2Data(imcOffset, _dataFile);

            await Task.Run(() =>
            {
                using (var br = new BinaryReader(new MemoryStream(imcData)))
                {
                    int variantOffset;

                    if (itemType == XivItemType.weapon || itemType == XivItemType.monster)
                    {
                        // weapons and monsters do not have variant sets
                        variantOffset = (modelInfo.Variant *variantLength) + headerLength;

                        // use default if offset is out of range
                        if (variantOffset >= imcData.Length)
                        {
                            variantOffset = headerLength;
                        }
                    }
                    else
                    {
                        // Variant Sets contain 5 variants for each slot
                        // These can be Head, Body, Hands, Legs, Feet  or  Ears, Neck, Wrists, LRing, RRing
                        // This skips to the correct variant set, then to the correct slot within that set for the item
                        variantOffset = (modelInfo.Variant *variantSetLength) +
                                        (_slotOffsetDictionary[itemCategory] * variantLength) + headerLength;

                        // use defalut if offset is out of range
                        if (variantOffset >= imcData.Length)
                        {
                            variantOffset = (_slotOffsetDictionary[itemCategory] * variantLength) + headerLength;
                        }
                    }

                    br.BaseStream.Seek(variantOffset, SeekOrigin.Begin);

                    // if(variantOffset)

                    xivImc.Version = br.ReadByte();
                    var unknown    = br.ReadByte();
                    xivImc.Mask    = br.ReadUInt16();
                    xivImc.Vfx     = br.ReadByte();
                    var unknown1   = br.ReadByte();
                }
            });

            return(xivImc);
        }
コード例 #8
0
        static string EnsurePath(string category, XivModelInfo modelInfo)
        {
            var modelKey = modelInfo.ModelKey.ToString().Replace(", ", "-");

            return(EnsurePath(category, modelKey));
        }
コード例 #9
0
ファイル: Gear.cs プロジェクト: ufx/xivModdingFramework
        /// <summary>
        /// A getter for available gear in the Item exd files
        /// </summary>
        /// <returns>A list containing XivGear data</returns>
        public async Task <List <XivGear> > GetGearList()
        {
            // These are the offsets to relevant data
            // These will need to be changed if data gets added or removed with a patch
            const int modelDataCheckOffset = 30;
            const int dataLength           = 160;
            const int nameDataOffset       = 14;
            const int modelDataOffset      = 24;
            const int iconDataOffset       = 136;
            const int slotDataOffset       = 154;

            var xivGearList = new List <XivGear>();

            xivGearList.AddRange(GetMissingGear());

            var ex             = new Ex(_gameDirectory, _xivLanguage);
            var itemDictionary = await ex.ReadExData(XivEx.item);

            // Loops through all the items in the item exd files
            // Item files start at 0 and increment by 500 for each new file
            // Item_0, Item_500, Item_1000, etc.
            await Task.Run(() => Parallel.ForEach(itemDictionary, (item) =>
            {
                // This checks whether there is any model data present in the current item
                if (item.Value[modelDataCheckOffset] <= 0 && item.Value[modelDataCheckOffset + 1] <= 0)
                {
                    return;
                }

                // Gear can have 2 separate models (MNK weapons for example)
                var primaryMi   = new XivModelInfo();
                var secondaryMi = new XivModelInfo();

                var xivGear = new XivGear
                {
                    Category           = XivStrings.Gear,
                    ModelInfo          = primaryMi,
                    SecondaryModelInfo = secondaryMi
                };

                /* Used to determine if the given model is a weapon
                 * This is important because the data is formatted differently
                 * The model data is a 16 byte section separated into two 8 byte parts (primary model, secondary model)
                 * Format is 8 bytes in length with 2 bytes per data point [short, short, short, short]
                 * Gear: primary model [blank, blank, variant, ID] nothing in secondary model
                 * Weapon: primary model [blank, variant, body, ID] secondary model [blank, variant, body, ID]
                 */
                var isWeapon = false;

                // Big Endian Byte Order
                using (var br = new BinaryReaderBE(new MemoryStream(item.Value)))
                {
                    br.BaseStream.Seek(nameDataOffset, SeekOrigin.Begin);
                    var nameOffset = br.ReadInt16();

                    // Model Data
                    br.BaseStream.Seek(modelDataOffset, SeekOrigin.Begin);

                    // Primary Model Key
                    primaryMi.ModelKey = Quad.Read(br.ReadBytes(8), 0);
                    br.BaseStream.Seek(-8, SeekOrigin.Current);

                    // Primary Blank
                    primaryMi.Unused = br.ReadInt16();

                    // Primary Variant for weapon, blank otherwise
                    var weaponVariant = br.ReadInt16();

                    if (weaponVariant != 0)
                    {
                        primaryMi.Variant = weaponVariant;
                        isWeapon          = true;
                    }

                    // Primary Body if weapon, Variant otherwise
                    if (isWeapon)
                    {
                        primaryMi.Body = br.ReadInt16();
                    }
                    else
                    {
                        primaryMi.Variant = br.ReadInt16();
                    }

                    // Primary Model ID
                    primaryMi.ModelID = br.ReadInt16();

                    // Secondary Model Key
                    isWeapon             = false;
                    secondaryMi.ModelKey = Quad.Read(br.ReadBytes(8), 0);
                    br.BaseStream.Seek(-8, SeekOrigin.Current);

                    // Secondary Blank
                    secondaryMi.Unused = br.ReadInt16();

                    // Secondary Variant for weapon, blank otherwise
                    weaponVariant = br.ReadInt16();

                    if (weaponVariant != 0)
                    {
                        secondaryMi.Variant = weaponVariant;
                        isWeapon            = true;
                    }

                    // Secondary Body if weapon, Variant otherwise
                    if (isWeapon)
                    {
                        secondaryMi.Body = br.ReadInt16();
                    }
                    else
                    {
                        secondaryMi.Variant = br.ReadInt16();
                    }

                    // Secondary Model ID
                    secondaryMi.ModelID = br.ReadInt16();

                    // Icon
                    br.BaseStream.Seek(iconDataOffset, SeekOrigin.Begin);
                    xivGear.IconNumber = br.ReadUInt16();

                    // Gear Slot/Category
                    br.BaseStream.Seek(slotDataOffset, SeekOrigin.Begin);
                    int slotNum = br.ReadByte();

                    // Waist items do not have texture or model data
                    if (slotNum == 6)
                    {
                        return;
                    }

                    xivGear.EquipSlotCategory = slotNum;
                    xivGear.ItemCategory      = _slotNameDictionary.ContainsKey(slotNum) ? _slotNameDictionary[slotNum] : "Unknown";

                    // Gear Name
                    var gearNameOffset = dataLength + nameOffset;
                    var gearNameLength = item.Value.Length - gearNameOffset;
                    br.BaseStream.Seek(gearNameOffset, SeekOrigin.Begin);
                    var nameString = Encoding.UTF8.GetString(br.ReadBytes(gearNameLength)).Replace("\0", "");
                    xivGear.Name   = new string(nameString.Where(c => !char.IsControl(c)).ToArray());

                    lock (_gearLock)
                    {
                        xivGearList.Add(xivGear);
                    }
                }
            }));

            xivGearList.Sort();

            return(xivGearList);
        }
コード例 #10
0
ファイル: Program.cs プロジェクト: Ariette/GarlandTools
        static void BatchExportItem(string path, IItemModel item, XivModelInfo secondaryModelInfo, Func <IEnumerable <XivRace> > getRaces)
        {
            if (File.Exists(path))
            {
                return;
            }

            WriteLine($"Exporting {item.GetType().Name} {item.Name}: {Path.GetFileNameWithoutExtension(path)}");

            var items = new List <IItemModel>();

            var metadata = new ExportMetadata();

            metadata.Name = item.Name;

            var mdl   = new Mdl(_gameDir, item.DataFile);
            var races = getRaces();

            items.Add(item);

            if (item.ModelInfo is XivMonsterModelInfo)
            {
                var info = item.ModelInfo as XivMonsterModelInfo;
                if (info.ModelType.Equals(XivItemType.demihuman) && item is XivMount)
                {
                    items.Clear();
                    var met = item.Clone() as XivMount;
                    met.TertiaryCategory = "Head";
                    var top = item.Clone() as XivMount;
                    top.TertiaryCategory = "Body";
                    var glv = item.Clone() as XivMount;
                    glv.TertiaryCategory = "Hand";
                    var dwn = item.Clone() as XivMount;
                    dwn.TertiaryCategory = "Leg";
                    var sho = item.Clone() as XivMount;
                    sho.TertiaryCategory = "Feet";

                    items.Add(met);
                    items.Add(top);
                    items.Add(glv);
                    items.Add(dwn);
                    items.Add(sho);
                }
            }

            foreach (var race in races)
            {
                var mdlDatas    = new List <XivMdl>();
                var textureSets = new List <Dictionary <string, ModelTextureData> >();

                foreach (var iItem in items)
                {
                    try
                    {
                        var mdlData = mdl.GetRawMdlData(iItem, race).Result;
                        if (mdlData != null)
                        {
                            mdlDatas.Add(mdlData);

                            textureSets.Add(TexTools.MaterialsHelper.GetMaterials(_gameDir, item, mdlData, race).Result);

                            continue;
                        }
                    }
                    catch
                    { }
                    WriteLine($"Failed to get {iItem.Name}。 Got null.");
                    if (items.Count > 1)
                    {
                        WriteLine($"{iItem.Name} has no components like {iItem.TertiaryCategory}.");
                    }
                }

                try
                {
                    var set = BatchExportSets(mdlDatas, textureSets);
                    set.Name = TexTools.XivStringRaces.ToRaceGenderName(race);
                    metadata.Sets.Add(set);
                }
                catch (NotImplementedException e)
                { }
            }
            if (metadata.Sets[0].Models.Count == 0)
            {
                WriteLine($"Empty model {item.Name}.");
                return;
            }

            var metadataJson = JsonConvert.SerializeObject(metadata);

            File.WriteAllText(path, metadataJson);

            WriteLine($"Exported {item.GetType().Name} {item.Name}: {Path.GetFileNameWithoutExtension(path)}");
        }