using Dalamud.Interface.Components; using Dalamud.Interface; using ImGuiNET; using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using Dalamud.Interface.Utility; using OtterGui; using OtterGui.Raii; using CustomizePlus.Core.Data; using CustomizePlus.Armatures.Data; using CustomizePlus.Configuration.Data; using CustomizePlus.Core.Helpers; using CustomizePlus.Templates; using CustomizePlus.Game.Services; using CustomizePlus.Templates.Data; namespace CustomizePlus.UI.Windows.MainWindow.Tabs.Templates; public class BoneEditorPanel { private readonly TemplateFileSystemSelector _templateFileSystemSelector; private readonly TemplateEditorManager _editorManager; private readonly PluginConfiguration _configuration; private readonly GameObjectService _gameObjectService; private BoneAttribute _editingAttribute; private int _precision; private bool _isShowLiveBones; private bool _isMirrorModeEnabled; private string? _newCharacterName; private Dictionary _groupExpandedState = new(); private bool _openSavePopup; private bool _isUnlocked = false; public bool HasChanges => _editorManager.HasChanges; public bool IsEditorActive => _editorManager.IsEditorActive; public bool IsEditorPaused => _editorManager.IsEditorPaused; public bool IsCharacterFound => _editorManager.IsCharacterFound; public BoneEditorPanel( TemplateFileSystemSelector templateFileSystemSelector, TemplateEditorManager editorManager, PluginConfiguration configuration, GameObjectService gameObjectService) { _templateFileSystemSelector = templateFileSystemSelector; _editorManager = editorManager; _configuration = configuration; _gameObjectService = gameObjectService; _isShowLiveBones = configuration.EditorConfiguration.ShowLiveBones; _isMirrorModeEnabled = configuration.EditorConfiguration.BoneMirroringEnabled; _precision = configuration.EditorConfiguration.EditorValuesPrecision; _editingAttribute = configuration.EditorConfiguration.EditorMode; } public bool EnableEditor(Template template) { if (_editorManager.EnableEditor(template)) { _editorManager.SetLimitLookupToOwned(_configuration.EditorConfiguration.LimitLookupToOwnedObjects); return true; } return false; } public bool DisableEditor() { if (!_editorManager.HasChanges) return _editorManager.DisableEditor(); if (_editorManager.HasChanges && !IsEditorActive) throw new Exception("Invalid state in BoneEditorPanel: has changes but editor is not active"); _openSavePopup = true; return false; } public void Draw() { _isUnlocked = IsCharacterFound && IsEditorActive && !IsEditorPaused; DrawEditorConfirmationPopup(); ImGui.Separator(); using (var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f))) { using (var table = ImRaii.Table("BasicSettings", 2)) { ImGui.TableSetupColumn("BasicCol1", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Show editor preview on").X); ImGui.TableSetupColumn("BasicCol2", ImGuiTableColumnFlags.WidthStretch); ImGui.TableNextRow(); ImGuiUtil.DrawFrameColumn("Show editor preview on"); ImGui.TableNextColumn(); var width = new Vector2(ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize("Limit to my creatures").X - 68, 0); var name = _newCharacterName ?? _editorManager.CharacterName; ImGui.SetNextItemWidth(width.X); using (var disabled = ImRaii.Disabled(!IsEditorActive || IsEditorPaused)) { if (!_templateFileSystemSelector.IncognitoMode) { if (ImGui.InputText("##PreviewCharacterName", ref name, 128)) { _newCharacterName = name; } if (ImGui.IsItemDeactivatedAfterEdit()) { if (string.IsNullOrWhiteSpace(_newCharacterName)) _newCharacterName = _gameObjectService.GetCurrentPlayerName(); _editorManager.ChangeEditorCharacter(_newCharacterName); _newCharacterName = null; } } else ImGui.TextUnformatted("Incognito active"); ImGui.SameLine(); var enabled = _editorManager.EditorProfile.LimitLookupToOwnedObjects; if (ImGui.Checkbox("##LimitLookupToOwnedObjects", ref enabled)) { _editorManager.SetLimitLookupToOwned(enabled); _configuration.EditorConfiguration.LimitLookupToOwnedObjects = enabled; _configuration.Save(); } ImGuiUtil.LabeledHelpMarker("Limit to my creatures", "When enabled limits the character search to only your own summons, mounts and minions.\nUseful when there is possibility there will be another character with that name owned by another player.\n* For battle chocobo use \"Chocobo\" as character name."); } } using (var table = ImRaii.Table("BoneEditorMenu", 2)) { ImGui.TableSetupColumn("Attributes", ImGuiTableColumnFlags.WidthFixed); ImGui.TableSetupColumn("Space", ImGuiTableColumnFlags.WidthStretch); ImGui.TableNextRow(); ImGui.TableNextColumn(); var modeChanged = false; if (ImGui.RadioButton("Position", _editingAttribute == BoneAttribute.Position)) { _editingAttribute = BoneAttribute.Position; modeChanged = true; } CtrlHelper.AddHoverText($"May have unintended effects. Edit at your own risk!"); ImGui.SameLine(); if (ImGui.RadioButton("Rotation", _editingAttribute == BoneAttribute.Rotation)) { _editingAttribute = BoneAttribute.Rotation; modeChanged = true; } CtrlHelper.AddHoverText($"May have unintended effects. Edit at your own risk!"); ImGui.SameLine(); if (ImGui.RadioButton("Scale", _editingAttribute == BoneAttribute.Scale)) { _editingAttribute = BoneAttribute.Scale; modeChanged = true; } if (modeChanged) { _configuration.EditorConfiguration.EditorMode = _editingAttribute; _configuration.Save(); } using (var disabled = ImRaii.Disabled(!_isUnlocked)) { ImGui.SameLine(); if (CtrlHelper.Checkbox("Show Live Bones", ref _isShowLiveBones)) { _configuration.EditorConfiguration.ShowLiveBones = _isShowLiveBones; _configuration.Save(); } CtrlHelper.AddHoverText($"If selected, present for editing all bones found in the game data,\nelse show only bones for which the profile already contains edits."); ImGui.SameLine(); ImGui.BeginDisabled(!_isShowLiveBones); if (CtrlHelper.Checkbox("Mirror Mode", ref _isMirrorModeEnabled)) { _configuration.EditorConfiguration.BoneMirroringEnabled = _isMirrorModeEnabled; _configuration.Save(); } CtrlHelper.AddHoverText($"Bone changes will be reflected from left to right and vice versa"); ImGui.EndDisabled(); } ImGui.TableNextColumn(); if (ImGui.SliderInt("##Precision", ref _precision, 0, 6, $"{_precision} Place{(_precision == 1 ? "" : "s")}")) { _configuration.EditorConfiguration.EditorValuesPrecision = _precision; _configuration.Save(); } CtrlHelper.AddHoverText("Level of precision to display while editing values"); } ImGui.Separator(); using (var table = ImRaii.Table("BoneEditorContents", 6, ImGuiTableFlags.BordersOuterH | ImGuiTableFlags.BordersV | ImGuiTableFlags.ScrollY)) { if (!table) return; var col1Label = _editingAttribute == BoneAttribute.Rotation ? "Roll" : "X"; var col2Label = _editingAttribute == BoneAttribute.Rotation ? "Pitch" : "Y"; var col3Label = _editingAttribute == BoneAttribute.Rotation ? "Yaw" : "Z"; var col4Label = _editingAttribute == BoneAttribute.Scale ? "All" : "N/A"; ImGui.TableSetupColumn("Bones", ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthFixed, 3 * CtrlHelper.IconButtonWidth); ImGui.TableSetupColumn($"{col1Label}", ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn($"{col2Label}", ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn($"{col3Label}", ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn($"{col4Label}", ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetColumnEnabled(4, _editingAttribute == BoneAttribute.Scale); ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.NoReorder | ImGuiTableColumnFlags.WidthStretch); ImGui.TableHeadersRow(); IEnumerable relevantModelBones = null!; if (_editorManager.IsEditorActive && _editorManager.EditorProfile != null && _editorManager.EditorProfile.Armatures.Count > 0) relevantModelBones = _isShowLiveBones && _editorManager.EditorProfile.Armatures.Count > 0 ? _editorManager.EditorProfile.Armatures[0].GetAllBones().DistinctBy(x => x.BoneName).Select(x => new EditRowParams(x)) : _editorManager.EditorProfile.Armatures[0].BoneTemplateBinding.Where(x => x.Value.Bones.ContainsKey(x.Key)) .Select(x => new EditRowParams(x.Key, x.Value.Bones[x.Key])); //todo: this is awful else relevantModelBones = _templateFileSystemSelector.Selected!.Bones.Select(x => new EditRowParams(x.Key, x.Value)); var groupedBones = relevantModelBones.GroupBy(x => BoneData.GetBoneFamily(x.BoneCodeName)); foreach (var boneGroup in groupedBones.OrderBy(x => (int)x.Key)) { //Hide root bone if it's not enabled in settings or if we are in rotation mode if (boneGroup.Key == BoneData.BoneFamily.Root && (!_configuration.EditorConfiguration.RootPositionEditingEnabled || _editingAttribute == BoneAttribute.Rotation)) continue; //create a dropdown entry for the family if one doesn't already exist //mind that it'll only be rendered if bones exist to fill it if (!_groupExpandedState.TryGetValue(boneGroup.Key, out var expanded)) { _groupExpandedState[boneGroup.Key] = false; expanded = false; } if (expanded) { //paint the row in header colors if it's expanded ImGui.TableNextRow(ImGuiTableRowFlags.Headers); } else { ImGui.TableNextRow(); } using var id = ImRaii.PushId(boneGroup.Key.ToString()); ImGui.TableNextColumn(); CtrlHelper.ArrowToggle($"##{boneGroup.Key}", ref expanded); ImGui.SameLine(); CtrlHelper.StaticLabel(boneGroup.Key.ToString()); if (BoneData.DisplayableFamilies.TryGetValue(boneGroup.Key, out var tip) && tip != null) CtrlHelper.AddHoverText(tip); if (expanded) { ImGui.TableNextRow(); foreach (var erp in boneGroup.OrderBy(x => BoneData.GetBoneRanking(x.BoneCodeName))) { CompleteBoneEditor(erp); } } _groupExpandedState[boneGroup.Key] = expanded; } } } } private void DrawEditorConfirmationPopup() { if (_openSavePopup) { ImGui.OpenPopup("SavePopup"); _openSavePopup = false; } var viewportSize = ImGui.GetWindowViewport().Size; ImGui.SetNextWindowSize(new Vector2(viewportSize.X / 4, viewportSize.Y / 12)); ImGui.SetNextWindowPos(viewportSize / 2, ImGuiCond.Always, new Vector2(0.5f)); using var popup = ImRaii.Popup("SavePopup", ImGuiWindowFlags.Modal); if (!popup) return; ImGui.SetCursorPos(new Vector2(ImGui.GetWindowWidth() / 4 - 40, ImGui.GetWindowHeight() / 4)); ImGuiUtil.TextWrapped("You have unsaved changes in current template, what would you like to do?"); var buttonWidth = new Vector2(150 * ImGuiHelpers.GlobalScale, 0); var yPos = ImGui.GetWindowHeight() - 2 * ImGui.GetFrameHeight(); var xPos = (ImGui.GetWindowWidth() - ImGui.GetStyle().ItemSpacing.X) / 4 - buttonWidth.X; ImGui.SetCursorPos(new Vector2(xPos, yPos)); if (ImGui.Button("Save", buttonWidth)) { _editorManager.SaveChanges(); _editorManager.DisableEditor(); ImGui.CloseCurrentPopup(); } ImGui.SameLine(); if (ImGui.Button("Save as a copy", buttonWidth)) { _editorManager.SaveChanges(true); _editorManager.DisableEditor(); ImGui.CloseCurrentPopup(); } ImGui.SameLine(); if (ImGui.Button("Do not save", buttonWidth)) { _editorManager.DisableEditor(); ImGui.CloseCurrentPopup(); } ImGui.SameLine(); if (ImGui.Button("Keep editing", buttonWidth)) { ImGui.CloseCurrentPopup(); } } #region ImGui helper functions private bool ResetBoneButton(EditRowParams bone) { var output = ImGuiComponents.IconButton(bone.BoneCodeName, FontAwesomeIcon.Recycle); CtrlHelper.AddHoverText( $"Reset '{BoneData.GetBoneDisplayName(bone.BoneCodeName)}' to default {_editingAttribute} values"); if (output) { _editorManager.ResetBoneAttributeChanges(bone.BoneCodeName, _editingAttribute); if (_isMirrorModeEnabled && bone.Basis?.TwinBone != null) //todo: put it inside manager _editorManager.ResetBoneAttributeChanges(bone.Basis.TwinBone.BoneName, _editingAttribute); } return output; } private bool RevertBoneButton(EditRowParams bone) { var output = ImGuiComponents.IconButton(bone.BoneCodeName, FontAwesomeIcon.ArrowCircleLeft); CtrlHelper.AddHoverText( $"Revert '{BoneData.GetBoneDisplayName(bone.BoneCodeName)}' to last saved {_editingAttribute} values"); if (output) { _editorManager.RevertBoneAttributeChanges(bone.BoneCodeName, _editingAttribute); if (_isMirrorModeEnabled && bone.Basis?.TwinBone != null) //todo: put it inside manager _editorManager.RevertBoneAttributeChanges(bone.Basis.TwinBone.BoneName, _editingAttribute); } return output; } private bool FullBoneSlider(string label, ref Vector3 value) { var velocity = _editingAttribute == BoneAttribute.Rotation ? 0.1f : 0.001f; var minValue = _editingAttribute == BoneAttribute.Rotation ? -360.0f : -10.0f; var maxValue = _editingAttribute == BoneAttribute.Rotation ? 360.0f : 10.0f; var temp = _editingAttribute switch { BoneAttribute.Position => 0.0f, BoneAttribute.Rotation => 0.0f, _ => value.X == value.Y && value.Y == value.Z ? value.X : 1.0f }; ImGui.PushItemWidth(ImGui.GetColumnWidth()); if (ImGui.DragFloat(label, ref temp, velocity, minValue, maxValue, $"%.{_precision}f")) { value = new Vector3(temp, temp, temp); return true; } return false; } private bool SingleValueSlider(string label, ref float value) { var velocity = _editingAttribute == BoneAttribute.Rotation ? 0.1f : 0.001f; var minValue = _editingAttribute == BoneAttribute.Rotation ? -360.0f : -10.0f; var maxValue = _editingAttribute == BoneAttribute.Rotation ? 360.0f : 10.0f; ImGui.PushItemWidth(ImGui.GetColumnWidth()); var temp = value; if (ImGui.DragFloat(label, ref temp, velocity, minValue, maxValue, $"%.{_precision}f")) { value = temp; return true; } return false; } private void CompleteBoneEditor(EditRowParams bone) { var codename = bone.BoneCodeName; var displayName = bone.BoneDisplayName; var transform = new BoneTransform(bone.Transform); var flagUpdate = false; var newVector = _editingAttribute switch { BoneAttribute.Position => transform.Translation, BoneAttribute.Rotation => transform.Rotation, _ => transform.Scaling }; using var id = ImRaii.PushId(codename); ImGui.TableNextColumn(); using (var disabled = ImRaii.Disabled(!_isUnlocked)) { //---------------------------------- ImGui.Dummy(new Vector2(CtrlHelper.IconButtonWidth * 0.75f, 0)); ImGui.SameLine(); ResetBoneButton(bone); ImGui.SameLine(); RevertBoneButton(bone); //---------------------------------- ImGui.TableNextColumn(); flagUpdate |= SingleValueSlider($"##{displayName}-X", ref newVector.X); //---------------------------------- ImGui.TableNextColumn(); flagUpdate |= SingleValueSlider($"##{displayName}-Y", ref newVector.Y); //----------------------------------- ImGui.TableNextColumn(); flagUpdate |= SingleValueSlider($"##{displayName}-Z", ref newVector.Z); //---------------------------------- if (_editingAttribute != BoneAttribute.Scale) ImGui.BeginDisabled(); ImGui.TableNextColumn(); var tempVec = new Vector3(newVector.X, newVector.Y, newVector.Z); flagUpdate |= FullBoneSlider($"##{displayName}-All", ref newVector); if (_editingAttribute != BoneAttribute.Scale) ImGui.EndDisabled(); } //---------------------------------- ImGui.TableNextColumn(); CtrlHelper.StaticLabel(displayName, CtrlHelper.TextAlignment.Left, BoneData.IsIVCSBone(codename) ? $"(IVCS) {codename}" : codename); if (flagUpdate) { transform.UpdateAttribute(_editingAttribute, newVector); _editorManager.ModifyBoneTransform(codename, transform); if (_isMirrorModeEnabled && bone.Basis?.TwinBone != null) //todo: put it inside manager _editorManager.ModifyBoneTransform(bone.Basis.TwinBone.BoneName, BoneData.IsIVCSBone(codename) ? transform.GetSpecialReflection() : transform.GetStandardReflection()); } ImGui.TableNextRow(); } #endregion } /// /// Simple structure for representing arguments to the editor table. /// Can be constructed with or without access to a live armature. /// internal struct EditRowParams { public string BoneCodeName; public string BoneDisplayName => BoneData.GetBoneDisplayName(BoneCodeName); public BoneTransform Transform; public ModelBone? Basis = null; public EditRowParams(ModelBone mb) { BoneCodeName = mb.BoneName; Transform = mb.CustomizedTransform ?? new BoneTransform(); Basis = mb; } public EditRowParams(string codename, BoneTransform tr) { BoneCodeName = codename; Transform = tr; Basis = null; } }