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); }
/// <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); }
/// <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); }
/// <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); }
/// <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(""); }
/// <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; } })); }
/// <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); }
static string EnsurePath(string category, XivModelInfo modelInfo) { var modelKey = modelInfo.ModelKey.ToString().Replace(", ", "-"); return(EnsurePath(category, modelKey)); }
/// <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); }
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)}"); }