Removed "Limit to my creatures", the code now automatically detects this for all owned actors. If you liked to apply edits to minions and stuff of other players... too bad. Implemented UI for setting profiles to NPC, minions and mounts (still WIP, will probably have to implement multiple characters per profile)
519 lines
20 KiB
C#
519 lines
20 KiB
C#
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<BoneData.BoneFamily, bool> _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);
|
|
|
|
var isShouldDraw = ImGui.CollapsingHeader("Preview settings");
|
|
|
|
if (isShouldDraw)
|
|
{
|
|
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.Separator();
|
|
|
|
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<EditRowParams> 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.IsIVCSCompatibleBone(codename) ? $"(IVCS Compatible) {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.IsIVCSCompatibleBone(codename) ? transform.GetSpecialReflection() : transform.GetStandardReflection());
|
|
}
|
|
|
|
ImGui.TableNextRow();
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>
|
|
/// Simple structure for representing arguments to the editor table.
|
|
/// Can be constructed with or without access to a live armature.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
} |