using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.ImGuiNotification; using ImSharp; using Newtonsoft.Json.Linq; using OtterGui.Raii; using OtterGui.Text; using OtterGui.Widgets; using Penumbra.Collections.Cache; using Penumbra.GameData.Data; using Penumbra.GameData.Enums; using Penumbra.GameData.Files; using Penumbra.GameData.Files.AtchStructs; using Penumbra.Meta; using Penumbra.Meta.Manipulations; using Penumbra.Mods.Editor; using Penumbra.UI.Classes; using MouseWheelType = OtterGui.Widgets.MouseWheelType; using Notification = Luna.Notification; namespace Penumbra.UI.AdvancedWindow.Meta; public sealed class AtchMetaDrawer : MetaDrawer { public override ReadOnlySpan Label => "Attachment Points (ATCH)###ATCH"u8; public override int NumColumns => 10; public override float ColumnHeight => 2 * ImUtf8.FrameHeightSpacing; private AtchFile? _currentBaseAtchFile; private AtchPoint? _currentBaseAtchPoint; private readonly AtchPointCombo _combo; private string _fileImport = string.Empty; public AtchMetaDrawer(ModMetaEditor editor, MetaFileManager metaFiles) : base(editor, metaFiles) { _combo = new AtchPointCombo(() => _currentBaseAtchFile?.Points.Select(p => p.Type).ToList() ?? []); } private sealed class AtchPointCombo(Func> generator) : FilterComboCache(generator, MouseWheelType.Control, Penumbra.Log) { protected override string ToString(AtchType obj) => obj.ToName(); } private sealed class RaceCodeException(string filePath) : Exception($"Could not identify race code from path {filePath}."); public void ImportFile(string filePath) { try { if (filePath.Length == 0 || !File.Exists(filePath)) throw new FileNotFoundException(); var gr = Parser.ParseRaceCode(filePath); if (gr is GenderRace.Unknown) throw new RaceCodeException(filePath); var text = File.ReadAllBytes(filePath); var file = new AtchFile(text); foreach (var point in file.Points) { foreach (var (index, entry) in point.Entries.Index()) { var identifier = new AtchIdentifier(point.Type, gr, (ushort)index); var defaultValue = AtchCache.GetDefault(MetaFiles, identifier); if (defaultValue == null) continue; if (defaultValue.Value.Equals(entry)) Editor.Changes |= Editor.Remove(identifier); else Editor.Changes |= Editor.TryAdd(identifier, entry) || Editor.Update(identifier, entry); } } } catch (RaceCodeException ex) { Penumbra.Messager.AddMessage(new Notification(ex, "The imported .atch file does not contain a race code (cXXXX) in its name.", "Could not import .atch file:", NotificationType.Warning)); } catch (Exception ex) { Penumbra.Messager.AddMessage(new Notification(ex, "Unable to import .atch file.", "Could not import .atch file:", NotificationType.Warning)); } } protected override void DrawNew() { ImGui.TableNextColumn(); CopyToClipboardButton("Copy all current ATCH manipulations to clipboard."u8, new Lazy(() => MetaDictionary.SerializeTo([], Editor.Atch))); ImGui.TableNextColumn(); var canAdd = !Editor.Contains(Identifier); var tt = canAdd ? "Stage this edit."u8 : "This entry is already edited."u8; if (ImUtf8.IconButton(FontAwesomeIcon.Plus, tt, disabled: !canAdd)) Editor.Changes |= Editor.TryAdd(Identifier, Entry); if (DrawIdentifierInput(ref Identifier)) UpdateEntry(); var defaultEntry = AtchCache.GetDefault(MetaFiles, Identifier) ?? default; DrawEntry(defaultEntry, ref defaultEntry, true); } private void UpdateEntry() => Entry = _currentBaseAtchPoint!.Entries[Identifier.EntryIndex]; protected override void Initialize() { _currentBaseAtchFile = MetaFiles.AtchManager.AtchFileBase[GenderRace.MidlanderMale]; _currentBaseAtchPoint = _currentBaseAtchFile.Points.First(); Identifier = new AtchIdentifier(_currentBaseAtchPoint.Type, GenderRace.MidlanderMale, 0); Entry = _currentBaseAtchPoint.Entries[0]; } protected override void DrawEntry(AtchIdentifier identifier, AtchEntry entry) { DrawMetaButtons(identifier, entry); DrawIdentifier(identifier); var defaultEntry = AtchCache.GetDefault(MetaFiles, identifier) ?? default; if (DrawEntry(defaultEntry, ref entry, false)) Editor.Changes |= Editor.Update(identifier, entry); } protected override IEnumerable<(AtchIdentifier, AtchEntry)> Enumerate() => Editor.Atch.Select(kvp => (kvp.Key, kvp.Value)) .OrderBy(p => p.Key.GenderRace) .ThenBy(p => p.Key.Type) .ThenBy(p => p.Key.EntryIndex); protected override int Count => Editor.Atch.Count; private bool DrawIdentifierInput(ref AtchIdentifier identifier) { var changes = false; ImGui.TableNextColumn(); changes |= DrawRace(ref identifier); ImGui.TableNextColumn(); changes |= DrawGender(ref identifier, false); if (changes) UpdateFile(); ImGui.TableNextColumn(); if (DrawPointInput(ref identifier, _combo)) { _currentBaseAtchPoint = _currentBaseAtchFile?.GetPoint(identifier.Type); changes = true; } ImGui.TableNextColumn(); changes |= DrawEntryIndexInput(ref identifier, _currentBaseAtchPoint!); return changes; } private void UpdateFile() { _currentBaseAtchFile = MetaFiles.AtchManager.AtchFileBase[Identifier.GenderRace]; _currentBaseAtchPoint = _currentBaseAtchFile.GetPoint(Identifier.Type); if (_currentBaseAtchPoint == null) { _currentBaseAtchPoint = _currentBaseAtchFile.Points.First(); Identifier = Identifier with { Type = _currentBaseAtchPoint.Type }; } if (Identifier.EntryIndex >= _currentBaseAtchPoint.Entries.Length) Identifier = Identifier with { EntryIndex = 0 }; } private static void DrawIdentifier(AtchIdentifier identifier) { ImGui.TableNextColumn(); ImUtf8.TextFramed(identifier.Race.ToName(), FrameColor); Im.Tooltip.OnHover("Model Race"u8); ImGui.TableNextColumn(); DrawGender(ref identifier, true); ImGui.TableNextColumn(); ImUtf8.TextFramed(identifier.Type.ToName(), FrameColor); Im.Tooltip.OnHover("Attachment Point Type"u8); ImGui.TableNextColumn(); ImUtf8.TextFramed(identifier.EntryIndex.ToString(), FrameColor); Im.Tooltip.OnHover("State Entry Index"u8); } private static bool DrawEntry(in AtchEntry defaultEntry, ref AtchEntry entry, bool disabled) { var changes = false; using var dis = ImRaii.Disabled(disabled); if (defaultEntry.Bone.Length == 0) return false; ImGui.TableNextColumn(); ImGui.SetNextItemWidth(200 * Im.Style.GlobalScale); if (ImUtf8.InputText("##BoneName"u8, entry.FullSpan, out TerminatedByteString newBone)) { entry.SetBoneName(newBone); changes = true; } Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Bone Name"u8); ImGui.SetNextItemWidth(200 * Im.Style.GlobalScale); changes |= ImUtf8.InputScalar("##AtchScale"u8, ref entry.Scale); Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Scale"u8); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(120 * Im.Style.GlobalScale); changes |= ImUtf8.InputScalar("##AtchOffsetX"u8, ref entry.OffsetX); Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Offset X-Coordinate"u8); ImGui.SetNextItemWidth(120 * Im.Style.GlobalScale); changes |= ImUtf8.InputScalar("##AtchRotationX"u8, ref entry.RotationX); Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Rotation X-Axis"u8); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(120 * Im.Style.GlobalScale); changes |= ImUtf8.InputScalar("##AtchOffsetY"u8, ref entry.OffsetY); Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Offset Y-Coordinate"u8); ImGui.SetNextItemWidth(120 * Im.Style.GlobalScale); changes |= ImUtf8.InputScalar("##AtchRotationY"u8, ref entry.RotationY); Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Rotation Y-Axis"u8); ImGui.TableNextColumn(); ImGui.SetNextItemWidth(120 * Im.Style.GlobalScale); changes |= ImUtf8.InputScalar("##AtchOffsetZ"u8, ref entry.OffsetZ); Im.Tooltip.OnHover("Offset Z-Coordinate"u8); ImGui.SetNextItemWidth(120 * Im.Style.GlobalScale); changes |= ImUtf8.InputScalar("##AtchRotationZ"u8, ref entry.RotationZ); Im.Tooltip.OnHover(HoveredFlags.AllowWhenDisabled, "Rotation Z-Axis"u8); return changes; } private static bool DrawRace(ref AtchIdentifier identifier, float unscaledWidth = 100) { var ret = Combos.Race("##atchRace", identifier.Race, out var race, unscaledWidth); Im.Tooltip.OnHover("Model Race"u8); if (ret) identifier = identifier with { GenderRace = Names.CombinedRace(identifier.Gender, race) }; return ret; } private static bool DrawGender(ref AtchIdentifier identifier, bool disabled) { var isMale = identifier.Gender is Gender.Male; if (!ImUtf8.IconButton(isMale ? FontAwesomeIcon.Mars : FontAwesomeIcon.Venus, "Gender"u8, buttonColor: disabled ? 0x000F0000u : 0) || disabled) return false; identifier = identifier with { GenderRace = Names.CombinedRace(isMale ? Gender.Female : Gender.Male, identifier.Race) }; return true; } private static bool DrawPointInput(ref AtchIdentifier identifier, AtchPointCombo combo) { if (!combo.Draw("##AtchPoint", identifier.Type.ToName(), "Attachment Point Type", 160 * Im.Style.GlobalScale, Im.Style.TextHeightWithSpacing)) return false; identifier = identifier with { Type = combo.CurrentSelection }; return true; } private static bool DrawEntryIndexInput(ref AtchIdentifier identifier, AtchPoint currentAtchPoint) { var index = identifier.EntryIndex; ImGui.SetNextItemWidth(40 * Im.Style.GlobalScale); var ret = ImUtf8.DragScalar("##AtchEntry"u8, ref index, 0, (ushort)(currentAtchPoint.Entries.Length - 1), 0.05f, ImGuiSliderFlags.AlwaysClamp); Im.Tooltip.OnHover("State Entry Index"u8); if (!ret) return false; index = Math.Clamp(index, (ushort)0, (ushort)(currentAtchPoint.Entries.Length - 1)); identifier = identifier with { EntryIndex = index }; return true; } }