diff --git a/Glamourer/Interop/CharaFile/CmaFile.cs b/Glamourer/Interop/CharaFile/CmaFile.cs new file mode 100644 index 0000000..15b8af1 --- /dev/null +++ b/Glamourer/Interop/CharaFile/CmaFile.cs @@ -0,0 +1,111 @@ +using System; +using Glamourer.Designs; +using Glamourer.Services; +using Newtonsoft.Json.Linq; +using Penumbra.GameData.Enums; +using Penumbra.GameData.Structs; + +namespace Glamourer.Interop.CharaFile; + +public sealed class CmaFile +{ + public string Name = string.Empty; + public DesignData Data = new(); + + public static CmaFile? ParseData(ItemManager items, string data, string? name = null) + { + try + { + var jObj = JObject.Parse(data); + var ret = new CmaFile(); + ret.Data.SetDefaultEquipment(items); + ParseMainHand(items, jObj, ref ret.Data); + ParseOffHand(items, jObj, ref ret.Data); + ret.Name = jObj["Description"]?.ToObject() ?? name ?? "New Design"; + ParseEquipment(items, jObj, ref ret.Data); + ParseCustomization(jObj, ref ret.Data); + return ret; + } + catch + { + return null; + } + } + + private static unsafe void ParseCustomization(JObject jObj, ref DesignData data) + { + var bytes = jObj["CharacterBytes"]?.ToObject() ?? string.Empty; + if (bytes.Length is not 26 * 3 - 1) + return; + + bytes = bytes.Replace(" ", string.Empty); + var byteData = Convert.FromHexString(bytes); + fixed (byte* ptr = byteData) + { + data.Customize.Data.Read(ptr); + } + } + + private static unsafe void ParseEquipment(ItemManager items, JObject jObj, ref DesignData data) + { + var bytes = jObj["EquipmentBytes"]?.ToObject() ?? string.Empty; + bytes = bytes.Replace(" ", string.Empty); + var byteData = Convert.FromHexString(bytes); + fixed (byte* ptr = byteData) + { + foreach (var slot in EquipSlotExtensions.EqdpSlots) + { + var idx = slot.ToIndex(); + if (idx * 4 + 3 >= byteData.Length) + continue; + var armor = ((CharacterArmor*)ptr)[idx]; + var item = items.Identify(slot, armor.Set, armor.Variant); + data.SetItem(slot, item); + data.SetStain(slot, armor.Stain); + } + + data.Customize.Data.Read(ptr); + } + } + + private static void ParseMainHand(ItemManager items, JObject jObj, ref DesignData data) + { + var mainhand = jObj["MainHand"]; + if (mainhand == null) + { + data.SetItem(EquipSlot.MainHand, items.DefaultSword); + data.SetStain(EquipSlot.MainHand, 0); + return; + } + + var set = mainhand["Item1"]?.ToObject() ?? items.DefaultSword.ModelId; + var type = mainhand["Item2"]?.ToObject() ?? items.DefaultSword.WeaponType; + var variant = mainhand["Item3"]?.ToObject() ?? items.DefaultSword.Variant; + var stain = mainhand["Item4"]?.ToObject() ?? 0; + var item = items.Identify(EquipSlot.MainHand, set, type, variant); + + data.SetItem(EquipSlot.MainHand, item.Valid ? item : items.DefaultSword); + data.SetStain(EquipSlot.MainHand, stain); + } + + private static void ParseOffHand(ItemManager items, JObject jObj, ref DesignData data) + { + var offhand = jObj["OffHand"]; + var defaultOffhand = items.GetDefaultOffhand(data.Item(EquipSlot.MainHand)); + if (offhand == null) + { + data.SetItem(EquipSlot.MainHand, defaultOffhand); + data.SetStain(EquipSlot.MainHand, defaultOffhand.ModelId.Id == 0 ? 0 : data.Stain(EquipSlot.MainHand)); + return; + } + + var set = offhand["Item1"]?.ToObject() ?? items.DefaultSword.ModelId; + var type = offhand["Item2"]?.ToObject() ?? items.DefaultSword.WeaponType; + var variant = offhand["Item3"]?.ToObject() ?? items.DefaultSword.Variant; + var stain = offhand["Item4"]?.ToObject() ?? 0; + var item = items.Identify(EquipSlot.OffHand, set, type, variant, data.MainhandType); + + data.SetItem(EquipSlot.OffHand, item.Valid ? item : defaultOffhand); + data.SetStain(EquipSlot.OffHand, defaultOffhand.ModelId.Id == 0 ? 0 : (StainId)stain); + } +} diff --git a/Glamourer/Interop/ImportService.cs b/Glamourer/Interop/ImportService.cs index 2681abb..217b5fd 100644 --- a/Glamourer/Interop/ImportService.cs +++ b/Glamourer/Interop/ImportService.cs @@ -6,7 +6,9 @@ using Dalamud.Interface.DragDrop; using Dalamud.Interface.Internal.Notifications; using Glamourer.Customization; using Glamourer.Designs; +using Glamourer.Interop.CharaFile; using Glamourer.Services; +using Glamourer.Structs; using ImGuiNET; using OtterGui.Classes; @@ -22,9 +24,9 @@ public class ImportService(CustomizationService _customizations, IDragDropManage }); public void CreateCharaSource() - => _dragDropManager.CreateImGuiSource("CharaDragger", m => m.Files.Count == 1 && m.Extensions.Contains(".chara"), m => + => _dragDropManager.CreateImGuiSource("CharaDragger", m => m.Files.Count == 1 && m.Extensions.Contains(".chara") || m.Extensions.Contains(".cma"), m => { - ImGui.TextUnformatted($"Dragging {Path.GetFileName(m.Files[0])} to import Anamnesis data for Glamourer..."); + ImGui.TextUnformatted($"Dragging {Path.GetFileName(m.Files[0])} to import Anamnesis/CMTool data for Glamourer..."); return true; }); @@ -47,8 +49,8 @@ public class ImportService(CustomizationService _customizations, IDragDropManage name = string.Empty; return false; } - - return LoadChara(files[0], out design, out name); + + return Path.GetExtension(files[0]) is ".chara" ? LoadChara(files[0], out design, out name) : LoadCma(files[0], out design, out name); } public bool LoadChara(string path, [NotNullWhen(true)] out DesignBase? design, out string name) @@ -81,6 +83,36 @@ public class ImportService(CustomizationService _customizations, IDragDropManage return true; } + public bool LoadCma(string path, [NotNullWhen(true)] out DesignBase? design, out string name) + { + if (!File.Exists(path)) + { + design = null; + name = string.Empty; + return false; + } + + try + { + var text = File.ReadAllText(path); + var file = CmaFile.ParseData(_items, text, Path.GetFileNameWithoutExtension(path)); + if (file == null) + throw new Exception(); + + name = file.Name; + design = new DesignBase(_customizations, file.Data, EquipFlagExtensions.All, CustomizeFlagExtensions.AllRelevant); + } + catch (Exception ex) + { + Glamourer.Messager.NotificationMessage(ex, $"Could not read .cma file {path}.", NotificationType.Error); + design = null; + name = string.Empty; + return false; + } + + return true; + } + public bool LoadDat(string path, out DatCharacterFile file) { if (!File.Exists(path))