mirror of
https://github.com/xivdev/Penumbra.git
synced 2026-02-21 07:17:53 +01:00
Why is this so much work?
This commit is contained in:
parent
651c7410ac
commit
b92a3161b5
53 changed files with 3054 additions and 2936 deletions
|
|
@ -1,763 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Dalamud.Utility;
|
||||
using ImGuiNET;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Widgets;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.ItemSwap;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public class ItemSwapWindow : IDisposable
|
||||
{
|
||||
private enum SwapType
|
||||
{
|
||||
Hat,
|
||||
Top,
|
||||
Gloves,
|
||||
Pants,
|
||||
Shoes,
|
||||
Earrings,
|
||||
Necklace,
|
||||
Bracelet,
|
||||
Ring,
|
||||
BetweenSlots,
|
||||
Hair,
|
||||
Face,
|
||||
Ears,
|
||||
Tail,
|
||||
Weapon,
|
||||
}
|
||||
|
||||
private class ItemSelector : FilterComboCache<(string, Item)>
|
||||
{
|
||||
public ItemSelector(FullEquipType type)
|
||||
: base(() => Penumbra.ItemData[type].Select(i => (i.Name.ToDalamudString().TextValue, i)).ToArray())
|
||||
{ }
|
||||
|
||||
protected override string ToString((string, Item) obj)
|
||||
=> obj.Item1;
|
||||
}
|
||||
|
||||
private class WeaponSelector : FilterComboCache<FullEquipType>
|
||||
{
|
||||
public WeaponSelector()
|
||||
: base(FullEquipTypeExtensions.WeaponTypes.Concat(FullEquipTypeExtensions.ToolTypes))
|
||||
{ }
|
||||
|
||||
protected override string ToString(FullEquipType type)
|
||||
=> type.ToName();
|
||||
}
|
||||
|
||||
private readonly CommunicatorService _communicator;
|
||||
|
||||
public ItemSwapWindow(CommunicatorService communicator)
|
||||
{
|
||||
_communicator = communicator;
|
||||
_communicator.CollectionChange.Event += OnCollectionChange;
|
||||
Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_communicator.CollectionChange.Event -= OnCollectionChange;
|
||||
Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange;
|
||||
}
|
||||
|
||||
private readonly Dictionary<SwapType, (ItemSelector Source, ItemSelector Target, string TextFrom, string TextTo)> _selectors = new()
|
||||
{
|
||||
[SwapType.Hat] =
|
||||
(new ItemSelector(FullEquipType.Head), new ItemSelector(FullEquipType.Head), "Take this Hat", "and put it on this one"),
|
||||
[SwapType.Top] =
|
||||
(new ItemSelector(FullEquipType.Body), new ItemSelector(FullEquipType.Body), "Take this Top", "and put it on this one"),
|
||||
[SwapType.Gloves] =
|
||||
(new ItemSelector(FullEquipType.Hands), new ItemSelector(FullEquipType.Hands), "Take these Gloves", "and put them on these"),
|
||||
[SwapType.Pants] =
|
||||
(new ItemSelector(FullEquipType.Legs), new ItemSelector(FullEquipType.Legs), "Take these Pants", "and put them on these"),
|
||||
[SwapType.Shoes] =
|
||||
(new ItemSelector(FullEquipType.Feet), new ItemSelector(FullEquipType.Feet), "Take these Shoes", "and put them on these"),
|
||||
[SwapType.Earrings] =
|
||||
(new ItemSelector(FullEquipType.Ears), new ItemSelector(FullEquipType.Ears), "Take these Earrings", "and put them on these"),
|
||||
[SwapType.Necklace] =
|
||||
(new ItemSelector(FullEquipType.Neck), new ItemSelector(FullEquipType.Neck), "Take this Necklace", "and put it on this one"),
|
||||
[SwapType.Bracelet] =
|
||||
(new ItemSelector(FullEquipType.Wrists), new ItemSelector(FullEquipType.Wrists), "Take these Bracelets", "and put them on these"),
|
||||
[SwapType.Ring] = (new ItemSelector(FullEquipType.Finger), new ItemSelector(FullEquipType.Finger), "Take this Ring",
|
||||
"and put it on this one"),
|
||||
};
|
||||
|
||||
private ItemSelector? _weaponSource = null;
|
||||
private ItemSelector? _weaponTarget = null;
|
||||
private readonly WeaponSelector _slotSelector = new();
|
||||
private readonly ItemSwapContainer _swapData = new();
|
||||
|
||||
private Mod? _mod;
|
||||
private ModSettings? _modSettings;
|
||||
private bool _dirty;
|
||||
|
||||
private SwapType _lastTab = SwapType.Hair;
|
||||
private Gender _currentGender = Gender.Male;
|
||||
private ModelRace _currentRace = ModelRace.Midlander;
|
||||
private int _targetId = 0;
|
||||
private int _sourceId = 0;
|
||||
private Exception? _loadException = null;
|
||||
private EquipSlot _slotFrom = EquipSlot.Head;
|
||||
private EquipSlot _slotTo = EquipSlot.Ears;
|
||||
|
||||
private string _newModName = string.Empty;
|
||||
private string _newGroupName = "Swaps";
|
||||
private string _newOptionName = string.Empty;
|
||||
private IModGroup? _selectedGroup = null;
|
||||
private bool _subModValid = false;
|
||||
private bool _useFileSwaps = true;
|
||||
private bool _useCurrentCollection = false;
|
||||
private bool _useLeftRing = true;
|
||||
private bool _useRightRing = true;
|
||||
|
||||
private Item[]? _affectedItems;
|
||||
|
||||
public void UpdateMod(Mod mod, ModSettings? settings)
|
||||
{
|
||||
if (mod == _mod && settings == _modSettings)
|
||||
return;
|
||||
|
||||
var oldDefaultName = $"{_mod?.Name.Text ?? "Unknown"} (Swapped)";
|
||||
if (_newModName.Length == 0 || oldDefaultName == _newModName)
|
||||
_newModName = $"{mod.Name.Text} (Swapped)";
|
||||
|
||||
_mod = mod;
|
||||
_modSettings = settings;
|
||||
_swapData.LoadMod(_mod, _modSettings);
|
||||
UpdateOption();
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
private void UpdateState()
|
||||
{
|
||||
if (!_dirty)
|
||||
return;
|
||||
|
||||
_swapData.Clear();
|
||||
_loadException = null;
|
||||
_affectedItems = null;
|
||||
try
|
||||
{
|
||||
switch (_lastTab)
|
||||
{
|
||||
case SwapType.Hat:
|
||||
case SwapType.Top:
|
||||
case SwapType.Gloves:
|
||||
case SwapType.Pants:
|
||||
case SwapType.Shoes:
|
||||
case SwapType.Earrings:
|
||||
case SwapType.Necklace:
|
||||
case SwapType.Bracelet:
|
||||
case SwapType.Ring:
|
||||
var values = _selectors[ _lastTab ];
|
||||
if( values.Source.CurrentSelection.Item2 != null && values.Target.CurrentSelection.Item2 != null )
|
||||
{
|
||||
_affectedItems = _swapData.LoadEquipment( values.Target.CurrentSelection.Item2, values.Source.CurrentSelection.Item2,
|
||||
_useCurrentCollection ? Penumbra.CollectionManager.Current : null, _useRightRing, _useLeftRing );
|
||||
}
|
||||
|
||||
break;
|
||||
case SwapType.BetweenSlots:
|
||||
var (_, _, selectorFrom) = GetAccessorySelector( _slotFrom, true );
|
||||
var (_, _, selectorTo) = GetAccessorySelector( _slotTo, false );
|
||||
if( selectorFrom.CurrentSelection.Item2 != null && selectorTo.CurrentSelection.Item2 != null )
|
||||
{
|
||||
_affectedItems = _swapData.LoadTypeSwap( _slotTo, selectorTo.CurrentSelection.Item2, _slotFrom, selectorFrom.CurrentSelection.Item2,
|
||||
_useCurrentCollection ? Penumbra.CollectionManager.Current : null);
|
||||
}
|
||||
break;
|
||||
case SwapType.Hair when _targetId > 0 && _sourceId > 0:
|
||||
_swapData.LoadCustomization(BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
|
||||
(SetId)_targetId,
|
||||
_useCurrentCollection ? Penumbra.CollectionManager.Current : null);
|
||||
break;
|
||||
case SwapType.Face when _targetId > 0 && _sourceId > 0:
|
||||
_swapData.LoadCustomization(BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
|
||||
(SetId)_targetId,
|
||||
_useCurrentCollection ? Penumbra.CollectionManager.Current : null);
|
||||
break;
|
||||
case SwapType.Ears when _targetId > 0 && _sourceId > 0:
|
||||
_swapData.LoadCustomization(BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera), (SetId)_sourceId,
|
||||
(SetId)_targetId,
|
||||
_useCurrentCollection ? Penumbra.CollectionManager.Current : null);
|
||||
break;
|
||||
case SwapType.Tail when _targetId > 0 && _sourceId > 0:
|
||||
_swapData.LoadCustomization(BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
|
||||
(SetId)_targetId,
|
||||
_useCurrentCollection ? Penumbra.CollectionManager.Current : null);
|
||||
break;
|
||||
case SwapType.Weapon: break;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Could not get Customization Data container for {_lastTab}:\n{e}");
|
||||
_loadException = e;
|
||||
_affectedItems = null;
|
||||
_swapData.Clear();
|
||||
}
|
||||
|
||||
_dirty = false;
|
||||
}
|
||||
|
||||
private static string SwapToString(Swap swap)
|
||||
{
|
||||
return swap switch
|
||||
{
|
||||
MetaSwap meta => $"{meta.SwapFrom}: {meta.SwapFrom.EntryToString()} -> {meta.SwapApplied.EntryToString()}",
|
||||
FileSwap file =>
|
||||
$"{file.Type}: {file.SwapFromRequestPath} -> {file.SwapToModded.FullName}{(file.DataWasChanged ? " (EDITED)" : string.Empty)}",
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
private string CreateDescription()
|
||||
=> $"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}.";
|
||||
|
||||
private void UpdateOption()
|
||||
{
|
||||
_selectedGroup = _mod?.Groups.FirstOrDefault(g => g.Name == _newGroupName);
|
||||
_subModValid = _mod != null
|
||||
&& _newGroupName.Length > 0
|
||||
&& _newOptionName.Length > 0
|
||||
&& (_selectedGroup?.All(o => o.Name != _newOptionName) ?? true);
|
||||
}
|
||||
|
||||
private void CreateMod()
|
||||
{
|
||||
var newDir = Mod.Creator.CreateModFolder(Penumbra.ModManager.BasePath, _newModName);
|
||||
Mod.Creator.CreateMeta(newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty);
|
||||
Mod.Creator.CreateDefaultFiles(newDir);
|
||||
Penumbra.ModManager.AddMod(newDir);
|
||||
if (!_swapData.WriteMod(Penumbra.ModManager.Last(),
|
||||
_useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps))
|
||||
Penumbra.ModManager.DeleteMod(Penumbra.ModManager.Count - 1);
|
||||
}
|
||||
|
||||
private void CreateOption()
|
||||
{
|
||||
if (_mod == null || !_subModValid)
|
||||
return;
|
||||
|
||||
var groupCreated = false;
|
||||
var dirCreated = false;
|
||||
var optionCreated = false;
|
||||
DirectoryInfo? optionFolderName = null;
|
||||
try
|
||||
{
|
||||
optionFolderName =
|
||||
Mod.Creator.NewSubFolderName(new DirectoryInfo(Path.Combine(_mod.ModPath.FullName, _selectedGroup?.Name ?? _newGroupName)),
|
||||
_newOptionName);
|
||||
if (optionFolderName?.Exists == true)
|
||||
throw new Exception($"The folder {optionFolderName.FullName} for the option already exists.");
|
||||
|
||||
if (optionFolderName != null)
|
||||
{
|
||||
if (_selectedGroup == null)
|
||||
{
|
||||
Penumbra.ModManager.AddModGroup(_mod, GroupType.Multi, _newGroupName);
|
||||
_selectedGroup = _mod.Groups.Last();
|
||||
groupCreated = true;
|
||||
}
|
||||
|
||||
Penumbra.ModManager.AddOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _newOptionName);
|
||||
optionCreated = true;
|
||||
optionFolderName = Directory.CreateDirectory(optionFolderName.FullName);
|
||||
dirCreated = true;
|
||||
if (!_swapData.WriteMod(_mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps,
|
||||
optionFolderName,
|
||||
_mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1))
|
||||
throw new Exception("Failure writing files for mod swap.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.ChatService.NotificationMessage($"Could not create new Swap Option:\n{e}", "Error", NotificationType.Error);
|
||||
try
|
||||
{
|
||||
if (optionCreated && _selectedGroup != null)
|
||||
Penumbra.ModManager.DeleteOption(_mod, _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1);
|
||||
|
||||
if (groupCreated)
|
||||
{
|
||||
Penumbra.ModManager.DeleteModGroup(_mod, _mod.Groups.IndexOf(_selectedGroup!));
|
||||
_selectedGroup = null;
|
||||
}
|
||||
|
||||
if (dirCreated && optionFolderName != null)
|
||||
Directory.Delete(optionFolderName.FullName, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
UpdateOption();
|
||||
}
|
||||
|
||||
private void DrawHeaderLine(float width)
|
||||
{
|
||||
var newModAvailable = _loadException == null && _swapData.Loaded;
|
||||
|
||||
ImGui.SetNextItemWidth(width);
|
||||
if (ImGui.InputTextWithHint("##newModName", "New Mod Name...", ref _newModName, 64))
|
||||
{ }
|
||||
|
||||
ImGui.SameLine();
|
||||
var tt = !newModAvailable
|
||||
? "No swap is currently loaded."
|
||||
: _newModName.Length == 0
|
||||
? "Please enter a name for your mod."
|
||||
: "Create a new mod of the given name containing only the swap.";
|
||||
if (ImGuiUtil.DrawDisabledButton("Create New Mod", new Vector2(width / 2, 0), tt, !newModAvailable || _newModName.Length == 0))
|
||||
CreateMod();
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * UiHelpers.Scale);
|
||||
ImGui.Checkbox("Use File Swaps", ref _useFileSwaps);
|
||||
ImGuiUtil.HoverTooltip("Instead of writing every single non-default file to the newly created mod or option,\n"
|
||||
+ "even those available from game files, use File Swaps to default game files where possible.");
|
||||
|
||||
ImGui.SetNextItemWidth((width - ImGui.GetStyle().ItemSpacing.X) / 2);
|
||||
if (ImGui.InputTextWithHint("##groupName", "Group Name...", ref _newGroupName, 32))
|
||||
UpdateOption();
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth((width - ImGui.GetStyle().ItemSpacing.X) / 2);
|
||||
if (ImGui.InputTextWithHint("##optionName", "New Option Name...", ref _newOptionName, 32))
|
||||
UpdateOption();
|
||||
|
||||
ImGui.SameLine();
|
||||
tt = !_subModValid
|
||||
? "An option with that name already exists in that group, or no name is specified."
|
||||
: !newModAvailable
|
||||
? "Create a new option inside this mod containing only the swap."
|
||||
: "Create a new option (and possibly Multi-Group) inside the currently selected mod containing the swap.";
|
||||
if (ImGuiUtil.DrawDisabledButton("Create New Option", new Vector2(width / 2, 0), tt, !newModAvailable || !_subModValid))
|
||||
CreateOption();
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + 20 * UiHelpers.Scale);
|
||||
_dirty |= ImGui.Checkbox("Use Entire Collection", ref _useCurrentCollection);
|
||||
ImGuiUtil.HoverTooltip(
|
||||
"Use all applied mods from the Selected Collection with their current settings and respecting the enabled state of mods and inheritance,\n"
|
||||
+ "instead of using only the selected mod with its current settings in the Selected collection or the default settings, ignoring the enabled state and inheritance.");
|
||||
}
|
||||
|
||||
private void DrawSwapBar()
|
||||
{
|
||||
using var bar = ImRaii.TabBar( "##swapBar", ImGuiTabBarFlags.None );
|
||||
|
||||
DrawEquipmentSwap( SwapType.Hat );
|
||||
DrawEquipmentSwap( SwapType.Top );
|
||||
DrawEquipmentSwap( SwapType.Gloves );
|
||||
DrawEquipmentSwap( SwapType.Pants );
|
||||
DrawEquipmentSwap( SwapType.Shoes );
|
||||
DrawEquipmentSwap( SwapType.Earrings );
|
||||
DrawEquipmentSwap( SwapType.Necklace );
|
||||
DrawEquipmentSwap( SwapType.Bracelet );
|
||||
DrawEquipmentSwap( SwapType.Ring );
|
||||
DrawAccessorySwap();
|
||||
DrawHairSwap();
|
||||
DrawFaceSwap();
|
||||
DrawEarSwap();
|
||||
DrawTailSwap();
|
||||
DrawWeaponSwap();
|
||||
}
|
||||
|
||||
private ImRaii.IEndObject DrawTab( SwapType newTab )
|
||||
{
|
||||
using var tab = ImRaii.TabItem( newTab is SwapType.BetweenSlots ? "Between Slots" : newTab.ToString() );
|
||||
if( tab )
|
||||
{
|
||||
_dirty |= _lastTab != newTab;
|
||||
_lastTab = newTab;
|
||||
}
|
||||
|
||||
UpdateState();
|
||||
|
||||
return tab;
|
||||
}
|
||||
|
||||
private void DrawAccessorySwap()
|
||||
{
|
||||
using var tab = DrawTab( SwapType.BetweenSlots );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( "##settings", 3, ImGuiTableFlags.SizingFixedFit );
|
||||
ImGui.TableSetupColumn( "##text", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize( "and put them on these" ).X );
|
||||
|
||||
var (article1, article2, selector) = GetAccessorySelector( _slotFrom, true );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted( $"Take {article1}" );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth( 100 * UiHelpers.Scale );
|
||||
using( var combo = ImRaii.Combo( "##fromType", _slotFrom is EquipSlot.Head ? "Hat" : _slotFrom.ToName() ) )
|
||||
{
|
||||
if( combo )
|
||||
{
|
||||
foreach( var slot in EquipSlotExtensions.AccessorySlots.Prepend(EquipSlot.Head) )
|
||||
{
|
||||
if( ImGui.Selectable( slot is EquipSlot.Head ? "Hat" : slot.ToName(), slot == _slotFrom ) && slot != _slotFrom )
|
||||
{
|
||||
_dirty = true;
|
||||
_slotFrom = slot;
|
||||
if( slot == _slotTo )
|
||||
{
|
||||
_slotTo = EquipSlotExtensions.AccessorySlots.First( s => slot != s );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
_dirty |= selector.Draw( "##itemSource", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() );
|
||||
|
||||
(article1, _, selector) = GetAccessorySelector( _slotTo, false );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted( $"and put {article2} on {article1}" );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth( 100 * UiHelpers.Scale );
|
||||
using( var combo = ImRaii.Combo( "##toType", _slotTo.ToName() ) )
|
||||
{
|
||||
if( combo )
|
||||
{
|
||||
foreach( var slot in EquipSlotExtensions.AccessorySlots.Where( s => s != _slotFrom ) )
|
||||
{
|
||||
if( ImGui.Selectable( slot.ToName(), slot == _slotTo ) && slot != _slotTo )
|
||||
{
|
||||
_dirty = true;
|
||||
_slotTo = slot;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
_dirty |= selector.Draw( "##itemTarget", selector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2, ImGui.GetTextLineHeightWithSpacing() );
|
||||
if( _affectedItems is { Length: > 1 } )
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGuiUtil.DrawTextButton( $"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero, Colors.PressEnterWarningBg );
|
||||
if( ImGui.IsItemHovered() )
|
||||
{
|
||||
ImGui.SetTooltip( string.Join( '\n', _affectedItems.Where( i => !ReferenceEquals( i, selector.CurrentSelection.Item2 ) )
|
||||
.Select( i => i.Name.ToDalamudString().TextValue ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private (string, string, ItemSelector) GetAccessorySelector( EquipSlot slot, bool source )
|
||||
{
|
||||
var (type, article1, article2) = slot switch
|
||||
{
|
||||
EquipSlot.Head => (SwapType.Hat, "this", "it"),
|
||||
EquipSlot.Ears => (SwapType.Earrings, "these", "them"),
|
||||
EquipSlot.Neck => (SwapType.Necklace, "this", "it"),
|
||||
EquipSlot.Wrists => (SwapType.Bracelet, "these", "them"),
|
||||
EquipSlot.RFinger => (SwapType.Ring, "this", "it"),
|
||||
EquipSlot.LFinger => (SwapType.Ring, "this", "it"),
|
||||
_ => (SwapType.Ring, "this", "it"),
|
||||
};
|
||||
var tuple = _selectors[ type ];
|
||||
return (article1, article2, source ? tuple.Source : tuple.Target);
|
||||
}
|
||||
|
||||
private void DrawEquipmentSwap(SwapType type)
|
||||
{
|
||||
using var tab = DrawTab(type);
|
||||
if (!tab)
|
||||
return;
|
||||
|
||||
var (sourceSelector, targetSelector, text1, text2) = _selectors[type];
|
||||
using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(text1);
|
||||
ImGui.TableNextColumn();
|
||||
_dirty |= sourceSelector.Draw("##itemSource", sourceSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2,
|
||||
ImGui.GetTextLineHeightWithSpacing());
|
||||
|
||||
if (type == SwapType.Ring)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
_dirty |= ImGui.Checkbox("Swap Right Ring", ref _useRightRing);
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(text2);
|
||||
ImGui.TableNextColumn();
|
||||
_dirty |= targetSelector.Draw("##itemTarget", targetSelector.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2,
|
||||
ImGui.GetTextLineHeightWithSpacing());
|
||||
if (type == SwapType.Ring)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
_dirty |= ImGui.Checkbox("Swap Left Ring", ref _useLeftRing);
|
||||
}
|
||||
|
||||
if (_affectedItems is { Length: > 1 })
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGuiUtil.DrawTextButton($"which will also affect {_affectedItems.Length - 1} other Items.", Vector2.Zero,
|
||||
Colors.PressEnterWarningBg);
|
||||
if (ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(string.Join('\n', _affectedItems.Where(i => !ReferenceEquals(i, targetSelector.CurrentSelection.Item2))
|
||||
.Select(i => i.Name.ToDalamudString().TextValue)));
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawHairSwap()
|
||||
{
|
||||
using var tab = DrawTab(SwapType.Hair);
|
||||
if (!tab)
|
||||
return;
|
||||
|
||||
using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
|
||||
DrawTargetIdInput("Take this Hairstyle");
|
||||
DrawSourceIdInput();
|
||||
DrawGenderInput();
|
||||
}
|
||||
|
||||
private void DrawFaceSwap()
|
||||
{
|
||||
using var disabled = ImRaii.Disabled();
|
||||
using var tab = DrawTab(SwapType.Face);
|
||||
if (!tab)
|
||||
return;
|
||||
|
||||
using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
|
||||
DrawTargetIdInput("Take this Face Type");
|
||||
DrawSourceIdInput();
|
||||
DrawGenderInput();
|
||||
}
|
||||
|
||||
private void DrawTailSwap()
|
||||
{
|
||||
using var tab = DrawTab(SwapType.Tail);
|
||||
if (!tab)
|
||||
return;
|
||||
|
||||
using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
|
||||
DrawTargetIdInput("Take this Tail Type");
|
||||
DrawSourceIdInput();
|
||||
DrawGenderInput("for all", 2);
|
||||
}
|
||||
|
||||
|
||||
private void DrawEarSwap()
|
||||
{
|
||||
using var tab = DrawTab(SwapType.Ears);
|
||||
if (!tab)
|
||||
return;
|
||||
|
||||
using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
|
||||
DrawTargetIdInput("Take this Ear Type");
|
||||
DrawSourceIdInput();
|
||||
DrawGenderInput("for all Viera", 0);
|
||||
}
|
||||
|
||||
|
||||
private void DrawWeaponSwap()
|
||||
{
|
||||
using var disabled = ImRaii.Disabled();
|
||||
using var tab = DrawTab(SwapType.Weapon);
|
||||
if (!tab)
|
||||
return;
|
||||
|
||||
using var table = ImRaii.Table("##settings", 2, ImGuiTableFlags.SizingFixedFit);
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted("Select the weapon or tool you want");
|
||||
ImGui.TableNextColumn();
|
||||
if (_slotSelector.Draw("##weaponSlot", _slotSelector.CurrentSelection.ToName(), string.Empty, InputWidth * 2,
|
||||
ImGui.GetTextLineHeightWithSpacing()))
|
||||
{
|
||||
_dirty = true;
|
||||
_weaponSource = new ItemSelector(_slotSelector.CurrentSelection);
|
||||
_weaponTarget = new ItemSelector(_slotSelector.CurrentSelection);
|
||||
}
|
||||
else
|
||||
{
|
||||
_dirty = _weaponSource == null || _weaponTarget == null;
|
||||
_weaponSource ??= new ItemSelector(_slotSelector.CurrentSelection);
|
||||
_weaponTarget ??= new ItemSelector(_slotSelector.CurrentSelection);
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted("and put this variant of it");
|
||||
ImGui.TableNextColumn();
|
||||
_dirty |= _weaponSource.Draw("##weaponSource", _weaponSource.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2,
|
||||
ImGui.GetTextLineHeightWithSpacing());
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted("onto this one");
|
||||
ImGui.TableNextColumn();
|
||||
_dirty |= _weaponTarget.Draw("##weaponTarget", _weaponTarget.CurrentSelection.Item1 ?? string.Empty, string.Empty, InputWidth * 2,
|
||||
ImGui.GetTextLineHeightWithSpacing());
|
||||
}
|
||||
|
||||
private const float InputWidth = 120;
|
||||
|
||||
private void DrawTargetIdInput(string text = "Take this ID")
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(text);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth(InputWidth * UiHelpers.Scale);
|
||||
if (ImGui.InputInt("##targetId", ref _targetId, 0, 0))
|
||||
_targetId = Math.Clamp(_targetId, 0, byte.MaxValue);
|
||||
|
||||
_dirty |= ImGui.IsItemDeactivatedAfterEdit();
|
||||
}
|
||||
|
||||
private void DrawSourceIdInput(string text = "and put it on this one")
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(text);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth(InputWidth * UiHelpers.Scale);
|
||||
if (ImGui.InputInt("##sourceId", ref _sourceId, 0, 0))
|
||||
_sourceId = Math.Clamp(_sourceId, 0, byte.MaxValue);
|
||||
|
||||
_dirty |= ImGui.IsItemDeactivatedAfterEdit();
|
||||
}
|
||||
|
||||
private void DrawGenderInput(string text = "for all", int drawRace = 1)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted(text);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
_dirty |= Combos.Gender("##Gender", InputWidth, _currentGender, out _currentGender);
|
||||
if (drawRace == 1)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
_dirty |= Combos.Race("##Race", InputWidth, _currentRace, out _currentRace);
|
||||
}
|
||||
else if (drawRace == 2)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
if (_currentRace is not ModelRace.Miqote and not ModelRace.AuRa and not ModelRace.Hrothgar)
|
||||
_currentRace = ModelRace.Miqote;
|
||||
|
||||
_dirty |= ImGuiUtil.GenericEnumCombo("##Race", InputWidth, _currentRace, out _currentRace, new[]
|
||||
{
|
||||
ModelRace.Miqote,
|
||||
ModelRace.AuRa,
|
||||
ModelRace.Hrothgar,
|
||||
},
|
||||
RaceEnumExtensions.ToName);
|
||||
}
|
||||
}
|
||||
|
||||
private string NonExistentText()
|
||||
=> _lastTab switch
|
||||
{
|
||||
SwapType.Hat => "One of the selected hats does not seem to exist.",
|
||||
SwapType.Top => "One of the selected tops does not seem to exist.",
|
||||
SwapType.Gloves => "One of the selected pairs of gloves does not seem to exist.",
|
||||
SwapType.Pants => "One of the selected pants does not seem to exist.",
|
||||
SwapType.Shoes => "One of the selected pairs of shoes does not seem to exist.",
|
||||
SwapType.Earrings => "One of the selected earrings does not seem to exist.",
|
||||
SwapType.Necklace => "One of the selected necklaces does not seem to exist.",
|
||||
SwapType.Bracelet => "One of the selected bracelets does not seem to exist.",
|
||||
SwapType.Ring => "One of the selected rings does not seem to exist.",
|
||||
SwapType.Hair => "One of the selected hairstyles does not seem to exist for this gender and race combo.",
|
||||
SwapType.Face => "One of the selected faces does not seem to exist for this gender and race combo.",
|
||||
SwapType.Ears => "One of the selected ear types does not seem to exist for this gender and race combo.",
|
||||
SwapType.Tail => "One of the selected tails does not seem to exist for this gender and race combo.",
|
||||
SwapType.Weapon => "One of the selected weapons or tools does not seem to exist.",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
|
||||
public void DrawItemSwapPanel()
|
||||
{
|
||||
using var tab = ImRaii.TabItem("Item Swap (WIP)");
|
||||
if (!tab)
|
||||
return;
|
||||
|
||||
ImGui.NewLine();
|
||||
DrawHeaderLine(300 * UiHelpers.Scale);
|
||||
ImGui.NewLine();
|
||||
|
||||
DrawSwapBar();
|
||||
|
||||
using var table = ImRaii.ListBox("##swaps", -Vector2.One);
|
||||
if (_loadException != null)
|
||||
ImGuiUtil.TextWrapped($"Could not load Customization Swap:\n{_loadException}");
|
||||
else if (_swapData.Loaded)
|
||||
foreach (var swap in _swapData.Swaps)
|
||||
DrawSwap(swap);
|
||||
else
|
||||
ImGui.TextUnformatted(NonExistentText());
|
||||
}
|
||||
|
||||
private static void DrawSwap(Swap swap)
|
||||
{
|
||||
var flags = swap.ChildSwaps.Count == 0 ? ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.DefaultOpen;
|
||||
using var tree = ImRaii.TreeNode(SwapToString(swap), flags);
|
||||
if (!tree)
|
||||
return;
|
||||
|
||||
foreach (var child in swap.ChildSwaps)
|
||||
DrawSwap(child);
|
||||
}
|
||||
|
||||
private void OnCollectionChange(CollectionType collectionType, ModCollection? oldCollection,
|
||||
ModCollection? newCollection, string _)
|
||||
{
|
||||
if (collectionType != CollectionType.Current || _mod == null || newCollection == null)
|
||||
return;
|
||||
|
||||
UpdateMod(_mod, _mod.Index < newCollection.Settings.Count ? newCollection.Settings[_mod.Index] : null);
|
||||
newCollection.ModSettingChanged += OnSettingChange;
|
||||
if (oldCollection != null)
|
||||
oldCollection.ModSettingChanged -= OnSettingChange;
|
||||
}
|
||||
|
||||
private void OnSettingChange(ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited)
|
||||
{
|
||||
if (modIdx == _mod?.Index)
|
||||
{
|
||||
_swapData.LoadMod(_mod, _modSettings);
|
||||
_dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,267 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private class FileEditor<T> where T : class, IWritable
|
||||
{
|
||||
private readonly string _tabName;
|
||||
private readonly string _fileType;
|
||||
private readonly Func<IReadOnlyList<Mod.Editor.FileRegistry>> _getFiles;
|
||||
private readonly Func<T, bool, bool> _drawEdit;
|
||||
private readonly Func<string> _getInitialPath;
|
||||
private readonly Func<byte[], T?> _parseFile;
|
||||
|
||||
private Mod.Editor.FileRegistry? _currentPath;
|
||||
private T? _currentFile;
|
||||
private Exception? _currentException;
|
||||
private bool _changed;
|
||||
|
||||
private string _defaultPath = string.Empty;
|
||||
private bool _inInput;
|
||||
private T? _defaultFile;
|
||||
private Exception? _defaultException;
|
||||
|
||||
private IReadOnlyList<Mod.Editor.FileRegistry> _list = null!;
|
||||
|
||||
private readonly FileDialogService _fileDialog;
|
||||
|
||||
public FileEditor(string tabName, string fileType, FileDialogService fileDialog, Func<IReadOnlyList<Mod.Editor.FileRegistry>> getFiles,
|
||||
Func<T, bool, bool> drawEdit, Func<string> getInitialPath, Func<byte[], T?>? parseFile)
|
||||
{
|
||||
_tabName = tabName;
|
||||
_fileType = fileType;
|
||||
_getFiles = getFiles;
|
||||
_drawEdit = drawEdit;
|
||||
_getInitialPath = getInitialPath;
|
||||
_fileDialog = fileDialog;
|
||||
_parseFile = parseFile ?? DefaultParseFile;
|
||||
}
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
_list = _getFiles();
|
||||
using var tab = ImRaii.TabItem(_tabName);
|
||||
if (!tab)
|
||||
return;
|
||||
|
||||
ImGui.NewLine();
|
||||
DrawFileSelectCombo();
|
||||
SaveButton();
|
||||
ImGui.SameLine();
|
||||
ResetButton();
|
||||
ImGui.SameLine();
|
||||
DefaultInput();
|
||||
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
|
||||
|
||||
DrawFilePanel();
|
||||
}
|
||||
|
||||
private void DefaultInput()
|
||||
{
|
||||
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 3 * UiHelpers.Scale });
|
||||
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X - 3 * UiHelpers.Scale - ImGui.GetFrameHeight());
|
||||
ImGui.InputTextWithHint("##defaultInput", "Input game path to compare...", ref _defaultPath, Utf8GamePath.MaxGamePathLength);
|
||||
_inInput = ImGui.IsItemActive();
|
||||
if (ImGui.IsItemDeactivatedAfterEdit() && _defaultPath.Length > 0)
|
||||
{
|
||||
_fileDialog.Reset();
|
||||
try
|
||||
{
|
||||
var file = DalamudServices.SGameData.GetFile(_defaultPath);
|
||||
if (file != null)
|
||||
{
|
||||
_defaultException = null;
|
||||
_defaultFile = _parseFile(file.Data);
|
||||
}
|
||||
else
|
||||
{
|
||||
_defaultFile = null;
|
||||
_defaultException = new Exception("File does not exist.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_defaultFile = null;
|
||||
_defaultException = e;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Save.ToIconString(), new Vector2(ImGui.GetFrameHeight()), "Export this file.",
|
||||
_defaultFile == null, true))
|
||||
_fileDialog.OpenSavePicker($"Export {_defaultPath} to...", _fileType, Path.GetFileNameWithoutExtension(_defaultPath), _fileType,
|
||||
(success, name) =>
|
||||
{
|
||||
if (!success)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes(name, _defaultFile?.Write() ?? throw new Exception("File invalid."));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.ChatService.NotificationMessage($"Could not export {_defaultPath}:\n{e}", "Error", NotificationType.Error);
|
||||
}
|
||||
}, _getInitialPath(), false);
|
||||
|
||||
_fileDialog.Draw();
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_currentException = null;
|
||||
_currentPath = null;
|
||||
_currentFile = null;
|
||||
_changed = false;
|
||||
}
|
||||
|
||||
private void DrawFileSelectCombo()
|
||||
{
|
||||
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
|
||||
using var combo = ImRaii.Combo("##fileSelect", _currentPath?.RelPath.ToString() ?? $"Select {_fileType} File...");
|
||||
if (!combo)
|
||||
return;
|
||||
|
||||
foreach (var file in _list)
|
||||
{
|
||||
if (ImGui.Selectable(file.RelPath.ToString(), ReferenceEquals(file, _currentPath)))
|
||||
UpdateCurrentFile(file);
|
||||
|
||||
if (ImGui.IsItemHovered())
|
||||
{
|
||||
using var tt = ImRaii.Tooltip();
|
||||
ImGui.TextUnformatted("All Game Paths");
|
||||
ImGui.Separator();
|
||||
using var t = ImRaii.Table("##Tooltip", 2, ImGuiTableFlags.SizingFixedFit);
|
||||
foreach (var (option, gamePath) in file.SubModUsage)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
UiHelpers.Text(gamePath.Path);
|
||||
ImGui.TableNextColumn();
|
||||
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(Penumbra.Config));
|
||||
ImGui.TextUnformatted(option.FullName);
|
||||
}
|
||||
}
|
||||
|
||||
if (file.SubModUsage.Count > 0)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
using var color = ImRaii.PushColor(ImGuiCol.Text, ColorId.ItemId.Value(Penumbra.Config));
|
||||
ImGuiUtil.RightAlign(file.SubModUsage[0].Item2.Path.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static T? DefaultParseFile(byte[] bytes)
|
||||
=> Activator.CreateInstance(typeof(T), bytes) as T;
|
||||
|
||||
private void UpdateCurrentFile(Mod.Editor.FileRegistry path)
|
||||
{
|
||||
if (ReferenceEquals(_currentPath, path))
|
||||
return;
|
||||
|
||||
_changed = false;
|
||||
_currentPath = path;
|
||||
_currentException = null;
|
||||
try
|
||||
{
|
||||
var bytes = File.ReadAllBytes(_currentPath.File.FullName);
|
||||
_currentFile = _parseFile(bytes);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_currentFile = null;
|
||||
_currentException = e;
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveButton()
|
||||
{
|
||||
if (ImGuiUtil.DrawDisabledButton("Save to File", Vector2.Zero,
|
||||
$"Save the selected {_fileType} file with all changes applied. This is not revertible.", !_changed))
|
||||
{
|
||||
File.WriteAllBytes(_currentPath!.File.FullName, _currentFile!.Write());
|
||||
_changed = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetButton()
|
||||
{
|
||||
if (ImGuiUtil.DrawDisabledButton("Reset Changes", Vector2.Zero,
|
||||
$"Reset all changes made to the {_fileType} file.", !_changed))
|
||||
{
|
||||
var tmp = _currentPath;
|
||||
_currentPath = null;
|
||||
UpdateCurrentFile(tmp!);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawFilePanel()
|
||||
{
|
||||
using var child = ImRaii.Child("##filePanel", -Vector2.One, true);
|
||||
if (!child)
|
||||
return;
|
||||
|
||||
if (_currentPath != null)
|
||||
{
|
||||
if (_currentFile == null)
|
||||
{
|
||||
ImGui.TextUnformatted($"Could not parse selected {_fileType} file.");
|
||||
if (_currentException != null)
|
||||
{
|
||||
using var tab = ImRaii.PushIndent();
|
||||
ImGuiUtil.TextWrapped(_currentException.ToString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using var id = ImRaii.PushId(0);
|
||||
_changed |= _drawEdit(_currentFile, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!_inInput && _defaultPath.Length > 0)
|
||||
{
|
||||
if (_currentPath != null)
|
||||
{
|
||||
ImGui.NewLine();
|
||||
ImGui.NewLine();
|
||||
ImGui.TextUnformatted($"Preview of {_defaultPath}:");
|
||||
ImGui.Separator();
|
||||
}
|
||||
|
||||
if (_defaultFile == null)
|
||||
{
|
||||
ImGui.TextUnformatted($"Could not parse provided {_fileType} game file:\n");
|
||||
if (_defaultException != null)
|
||||
{
|
||||
using var tab = ImRaii.PushIndent();
|
||||
ImGuiUtil.TextWrapped(_defaultException.ToString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
using var id = ImRaii.PushId(1);
|
||||
_drawEdit(_defaultFile, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,360 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private readonly HashSet<Mod.Editor.FileRegistry> _selectedFiles = new(256);
|
||||
private LowerString _fileFilter = LowerString.Empty;
|
||||
private bool _showGamePaths = true;
|
||||
private string _gamePathEdit = string.Empty;
|
||||
private int _fileIdx = -1;
|
||||
private int _pathIdx = -1;
|
||||
private int _folderSkip = 0;
|
||||
private bool _overviewMode = false;
|
||||
private LowerString _fileOverviewFilter1 = LowerString.Empty;
|
||||
private LowerString _fileOverviewFilter2 = LowerString.Empty;
|
||||
private LowerString _fileOverviewFilter3 = LowerString.Empty;
|
||||
|
||||
private bool CheckFilter(Mod.Editor.FileRegistry registry)
|
||||
=> _fileFilter.IsEmpty || registry.File.FullName.Contains(_fileFilter.Lower, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private bool CheckFilter((Mod.Editor.FileRegistry, int) p)
|
||||
=> CheckFilter(p.Item1);
|
||||
|
||||
private void DrawFileTab()
|
||||
{
|
||||
using var tab = ImRaii.TabItem("File Redirections");
|
||||
if (!tab)
|
||||
return;
|
||||
|
||||
DrawOptionSelectHeader();
|
||||
DrawButtonHeader();
|
||||
|
||||
if (_overviewMode)
|
||||
DrawFileManagementOverview();
|
||||
else
|
||||
DrawFileManagementNormal();
|
||||
|
||||
using var child = ImRaii.Child("##files", -Vector2.One, true);
|
||||
if (!child)
|
||||
return;
|
||||
|
||||
if (_overviewMode)
|
||||
DrawFilesOverviewMode();
|
||||
else
|
||||
DrawFilesNormalMode();
|
||||
}
|
||||
|
||||
private void DrawFilesOverviewMode()
|
||||
{
|
||||
var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y;
|
||||
var skips = ImGuiClip.GetNecessarySkips(height);
|
||||
|
||||
using var list = ImRaii.Table("##table", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV, -Vector2.One);
|
||||
|
||||
if (!list)
|
||||
return;
|
||||
|
||||
var width = ImGui.GetContentRegionAvail().X / 8;
|
||||
|
||||
ImGui.TableSetupColumn("##file", ImGuiTableColumnFlags.WidthFixed, width * 3);
|
||||
ImGui.TableSetupColumn("##path", ImGuiTableColumnFlags.WidthFixed, width * 3 + ImGui.GetStyle().FrameBorderSize);
|
||||
ImGui.TableSetupColumn("##option", ImGuiTableColumnFlags.WidthFixed, width * 2);
|
||||
|
||||
var idx = 0;
|
||||
|
||||
var files = _editor!.AvailableFiles.SelectMany(f =>
|
||||
{
|
||||
var file = f.RelPath.ToString();
|
||||
return f.SubModUsage.Count == 0
|
||||
? Enumerable.Repeat((file, "Unused", string.Empty, 0x40000080u), 1)
|
||||
: f.SubModUsage.Select(s => (file, s.Item2.ToString(), s.Item1.FullName,
|
||||
_editor.CurrentOption == s.Item1 && _mod!.HasOptions ? 0x40008000u : 0u));
|
||||
});
|
||||
|
||||
void DrawLine((string, string, string, uint) data)
|
||||
{
|
||||
using var id = ImRaii.PushId(idx++);
|
||||
ImGui.TableNextColumn();
|
||||
if (data.Item4 != 0)
|
||||
ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, data.Item4);
|
||||
|
||||
ImGuiUtil.CopyOnClickSelectable(data.Item1);
|
||||
ImGui.TableNextColumn();
|
||||
if (data.Item4 != 0)
|
||||
ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, data.Item4);
|
||||
|
||||
ImGuiUtil.CopyOnClickSelectable(data.Item2);
|
||||
ImGui.TableNextColumn();
|
||||
if (data.Item4 != 0)
|
||||
ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, data.Item4);
|
||||
|
||||
ImGuiUtil.CopyOnClickSelectable(data.Item3);
|
||||
}
|
||||
|
||||
bool Filter((string, string, string, uint) data)
|
||||
=> _fileOverviewFilter1.IsContained(data.Item1)
|
||||
&& _fileOverviewFilter2.IsContained(data.Item2)
|
||||
&& _fileOverviewFilter3.IsContained(data.Item3);
|
||||
|
||||
var end = ImGuiClip.FilteredClippedDraw(files, skips, Filter, DrawLine);
|
||||
ImGuiClip.DrawEndDummy(end, height);
|
||||
}
|
||||
|
||||
private void DrawFilesNormalMode()
|
||||
{
|
||||
using var list = ImRaii.Table("##table", 1);
|
||||
|
||||
if (!list)
|
||||
return;
|
||||
|
||||
foreach (var (registry, i) in _editor!.AvailableFiles.WithIndex().Where(CheckFilter))
|
||||
{
|
||||
using var id = ImRaii.PushId(i);
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
DrawSelectable(registry);
|
||||
|
||||
if (!_showGamePaths)
|
||||
continue;
|
||||
|
||||
using var indent = ImRaii.PushIndent(50f);
|
||||
for (var j = 0; j < registry.SubModUsage.Count; ++j)
|
||||
{
|
||||
var (subMod, gamePath) = registry.SubModUsage[j];
|
||||
if (subMod != _editor.CurrentOption)
|
||||
continue;
|
||||
|
||||
PrintGamePath(i, j, registry, subMod, gamePath);
|
||||
}
|
||||
|
||||
PrintNewGamePath(i, registry, _editor.CurrentOption);
|
||||
}
|
||||
}
|
||||
|
||||
private static string DrawFileTooltip(Mod.Editor.FileRegistry registry, ColorId color)
|
||||
{
|
||||
(string, int) GetMulti()
|
||||
{
|
||||
var groups = registry.SubModUsage.GroupBy(s => s.Item1).ToArray();
|
||||
return (string.Join("\n", groups.Select(g => g.Key.Name)), groups.Length);
|
||||
}
|
||||
|
||||
var (text, groupCount) = color switch
|
||||
{
|
||||
ColorId.ConflictingMod => (string.Empty, 0),
|
||||
ColorId.NewMod => (registry.SubModUsage[0].Item1.Name, 1),
|
||||
ColorId.InheritedMod => GetMulti(),
|
||||
_ => (string.Empty, 0),
|
||||
};
|
||||
|
||||
if (text.Length > 0 && ImGui.IsItemHovered())
|
||||
ImGui.SetTooltip(text);
|
||||
|
||||
|
||||
return (groupCount, registry.SubModUsage.Count) switch
|
||||
{
|
||||
(0, 0) => "(unused)",
|
||||
(1, 1) => "(used 1 time)",
|
||||
(1, > 1) => $"(used {registry.SubModUsage.Count} times in 1 group)",
|
||||
_ => $"(used {registry.SubModUsage.Count} times over {groupCount} groups)",
|
||||
};
|
||||
}
|
||||
|
||||
private void DrawSelectable(Mod.Editor.FileRegistry registry)
|
||||
{
|
||||
var selected = _selectedFiles.Contains(registry);
|
||||
var color = registry.SubModUsage.Count == 0 ? ColorId.ConflictingMod :
|
||||
registry.CurrentUsage == registry.SubModUsage.Count ? ColorId.NewMod : ColorId.InheritedMod;
|
||||
using var c = ImRaii.PushColor(ImGuiCol.Text, color.Value(Penumbra.Config));
|
||||
if (UiHelpers.Selectable(registry.RelPath.Path, selected))
|
||||
{
|
||||
if (selected)
|
||||
_selectedFiles.Remove(registry);
|
||||
else
|
||||
_selectedFiles.Add(registry);
|
||||
}
|
||||
|
||||
var rightText = DrawFileTooltip(registry, color);
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGuiUtil.RightAlign(rightText);
|
||||
}
|
||||
|
||||
private void PrintGamePath(int i, int j, Mod.Editor.FileRegistry registry, ISubMod subMod, Utf8GamePath gamePath)
|
||||
{
|
||||
using var id = ImRaii.PushId(j);
|
||||
ImGui.TableNextColumn();
|
||||
var tmp = _fileIdx == i && _pathIdx == j ? _gamePathEdit : gamePath.ToString();
|
||||
var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight();
|
||||
ImGui.SetNextItemWidth(-1);
|
||||
if (ImGui.InputText(string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength))
|
||||
{
|
||||
_fileIdx = i;
|
||||
_pathIdx = j;
|
||||
_gamePathEdit = tmp;
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip("Clear completely to remove the path from this mod.");
|
||||
|
||||
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
if (Utf8GamePath.FromString(_gamePathEdit, out var path, false))
|
||||
_editor!.SetGamePath(_fileIdx, _pathIdx, path);
|
||||
|
||||
_fileIdx = -1;
|
||||
_pathIdx = -1;
|
||||
}
|
||||
else if (_fileIdx == i
|
||||
&& _pathIdx == j
|
||||
&& (!Utf8GamePath.FromString(_gamePathEdit, out var path, false)
|
||||
|| !path.IsEmpty && !path.Equals(gamePath) && !_editor!.CanAddGamePath(path)))
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosX(pos);
|
||||
using var font = ImRaii.PushFont(UiBuilder.IconFont);
|
||||
ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString());
|
||||
}
|
||||
}
|
||||
|
||||
private void PrintNewGamePath(int i, Mod.Editor.FileRegistry registry, ISubMod subMod)
|
||||
{
|
||||
var tmp = _fileIdx == i && _pathIdx == -1 ? _gamePathEdit : string.Empty;
|
||||
var pos = ImGui.GetCursorPosX() - ImGui.GetFrameHeight();
|
||||
ImGui.SetNextItemWidth(-1);
|
||||
if (ImGui.InputTextWithHint("##new", "Add New Path...", ref tmp, Utf8GamePath.MaxGamePathLength))
|
||||
{
|
||||
_fileIdx = i;
|
||||
_pathIdx = -1;
|
||||
_gamePathEdit = tmp;
|
||||
}
|
||||
|
||||
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||
{
|
||||
if (Utf8GamePath.FromString(_gamePathEdit, out var path, false) && !path.IsEmpty)
|
||||
_editor!.SetGamePath(_fileIdx, _pathIdx, path);
|
||||
|
||||
_fileIdx = -1;
|
||||
_pathIdx = -1;
|
||||
}
|
||||
else if (_fileIdx == i
|
||||
&& _pathIdx == -1
|
||||
&& (!Utf8GamePath.FromString(_gamePathEdit, out var path, false)
|
||||
|| !path.IsEmpty && !_editor!.CanAddGamePath(path)))
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.SetCursorPosX(pos);
|
||||
using var font = ImRaii.PushFont(UiBuilder.IconFont);
|
||||
ImGuiUtil.TextColored(0xFF0000FF, FontAwesomeIcon.TimesCircle.ToIconString());
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawButtonHeader()
|
||||
{
|
||||
ImGui.NewLine();
|
||||
|
||||
using var spacing = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(3 * UiHelpers.Scale, 0));
|
||||
ImGui.SetNextItemWidth(30 * UiHelpers.Scale);
|
||||
ImGui.DragInt("##skippedFolders", ref _folderSkip, 0.01f, 0, 10);
|
||||
ImGuiUtil.HoverTooltip("Skip the first N folders when automatically constructing the game path from the file path.");
|
||||
ImGui.SameLine();
|
||||
spacing.Pop();
|
||||
if (ImGui.Button("Add Paths"))
|
||||
_editor!.AddPathsToSelected(_editor!.AvailableFiles.Where(_selectedFiles.Contains), _folderSkip);
|
||||
|
||||
ImGuiUtil.HoverTooltip(
|
||||
"Add the file path converted to a game path to all selected files for the current option, optionally skipping the first N folders.");
|
||||
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Remove Paths"))
|
||||
_editor!.RemovePathsFromSelected(_editor!.AvailableFiles.Where(_selectedFiles.Contains));
|
||||
|
||||
ImGuiUtil.HoverTooltip("Remove all game paths associated with the selected files in the current option.");
|
||||
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Delete Selected Files"))
|
||||
_editor!.DeleteFiles(_editor!.AvailableFiles.Where(_selectedFiles.Contains));
|
||||
|
||||
ImGuiUtil.HoverTooltip(
|
||||
"Delete all selected files entirely from your filesystem, but not their file associations in the mod, if there are any.\n!!!This can not be reverted!!!");
|
||||
ImGui.SameLine();
|
||||
var changes = _editor!.FileChanges;
|
||||
var tt = changes ? "Apply the current file setup to the currently selected option." : "No changes made.";
|
||||
if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, !changes))
|
||||
{
|
||||
var failedFiles = _editor!.ApplyFiles();
|
||||
if (failedFiles > 0)
|
||||
Penumbra.Log.Information($"Failed to apply {failedFiles} file redirections to {_editor.CurrentOption.FullName}.");
|
||||
}
|
||||
|
||||
|
||||
ImGui.SameLine();
|
||||
var label = changes ? "Revert Changes" : "Reload Files";
|
||||
var length = new Vector2(ImGui.CalcTextSize("Revert Changes").X, 0);
|
||||
if (ImGui.Button(label, length))
|
||||
_editor!.RevertFiles();
|
||||
|
||||
ImGuiUtil.HoverTooltip("Revert all revertible changes since the last file or option reload or data refresh.");
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.Checkbox("Overview Mode", ref _overviewMode);
|
||||
}
|
||||
|
||||
private void DrawFileManagementNormal()
|
||||
{
|
||||
ImGui.SetNextItemWidth(250 * UiHelpers.Scale);
|
||||
LowerString.InputWithHint("##filter", "Filter paths...", ref _fileFilter, Utf8GamePath.MaxGamePathLength);
|
||||
ImGui.SameLine();
|
||||
ImGui.Checkbox("Show Game Paths", ref _showGamePaths);
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Unselect All"))
|
||||
_selectedFiles.Clear();
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Select Visible"))
|
||||
_selectedFiles.UnionWith(_editor!.AvailableFiles.Where(CheckFilter));
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Select Unused"))
|
||||
_selectedFiles.UnionWith(_editor!.AvailableFiles.Where(f => f.SubModUsage.Count == 0));
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Select Used Here"))
|
||||
_selectedFiles.UnionWith(_editor!.AvailableFiles.Where(f => f.CurrentUsage > 0));
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
ImGuiUtil.RightAlign($"{_selectedFiles.Count} / {_editor!.AvailableFiles.Count} Files Selected");
|
||||
}
|
||||
|
||||
private void DrawFileManagementOverview()
|
||||
{
|
||||
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 0)
|
||||
.Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
|
||||
.Push(ImGuiStyleVar.FrameBorderSize, ImGui.GetStyle().ChildBorderSize);
|
||||
|
||||
var width = ImGui.GetContentRegionAvail().X / 8;
|
||||
|
||||
ImGui.SetNextItemWidth(width * 3);
|
||||
LowerString.InputWithHint("##fileFilter", "Filter file...", ref _fileOverviewFilter1, Utf8GamePath.MaxGamePathLength);
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth(width * 3);
|
||||
LowerString.InputWithHint("##pathFilter", "Filter path...", ref _fileOverviewFilter2, Utf8GamePath.MaxGamePathLength);
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth(width * 2);
|
||||
LowerString.InputWithHint("##optionFilter", "Filter option...", ref _fileOverviewFilter3, Utf8GamePath.MaxGamePathLength);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,436 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Runtime.InteropServices;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.String.Functions;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private static bool DrawMaterialColorSetChange( MtrlFile file, bool disabled )
|
||||
{
|
||||
if( !file.ColorSets.Any( c => c.HasRows ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ColorSetCopyAllClipboardButton( file, 0 );
|
||||
ImGui.SameLine();
|
||||
var ret = ColorSetPasteAllClipboardButton( file, 0 );
|
||||
ImGui.SameLine();
|
||||
ImGui.Dummy( ImGuiHelpers.ScaledVector2( 20, 0 ) );
|
||||
ImGui.SameLine();
|
||||
ret |= DrawPreviewDye( file, disabled );
|
||||
|
||||
using var table = ImRaii.Table( "##ColorSets", 11,
|
||||
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInnerV );
|
||||
if( !table )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader( string.Empty );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader( "Row" );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader( "Diffuse" );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader( "Specular" );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader( "Emissive" );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader( "Gloss" );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader( "Tile" );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader( "Repeat" );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader( "Skew" );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader( "Dye" );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader( "Dye Preview" );
|
||||
|
||||
for( var j = 0; j < file.ColorSets.Length; ++j )
|
||||
{
|
||||
using var _ = ImRaii.PushId( j );
|
||||
for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i )
|
||||
{
|
||||
ret |= DrawColorSetRow( file, j, i, disabled );
|
||||
ImGui.TableNextRow();
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
private static void ColorSetCopyAllClipboardButton( MtrlFile file, int colorSetIdx )
|
||||
{
|
||||
if( !ImGui.Button( "Export All Rows to Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var data1 = file.ColorSets[ colorSetIdx ].Rows.AsBytes();
|
||||
var data2 = file.ColorDyeSets.Length > colorSetIdx ? file.ColorDyeSets[ colorSetIdx ].Rows.AsBytes() : ReadOnlySpan< byte >.Empty;
|
||||
var array = new byte[data1.Length + data2.Length];
|
||||
data1.TryCopyTo( array );
|
||||
data2.TryCopyTo( array.AsSpan( data1.Length ) );
|
||||
var text = Convert.ToBase64String( array );
|
||||
ImGui.SetClipboardText( text );
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
private static bool DrawPreviewDye( MtrlFile file, bool disabled )
|
||||
{
|
||||
var (dyeId, (name, dyeColor, _)) = Penumbra.StainService.StainCombo.CurrentSelection;
|
||||
var tt = dyeId == 0 ? "Select a preview dye first." : "Apply all preview values corresponding to the dye template and chosen dye where dyeing is enabled.";
|
||||
if( ImGuiUtil.DrawDisabledButton( "Apply Preview Dye", Vector2.Zero, tt, disabled || dyeId == 0 ) )
|
||||
{
|
||||
var ret = false;
|
||||
for( var j = 0; j < file.ColorDyeSets.Length; ++j )
|
||||
{
|
||||
for( var i = 0; i < MtrlFile.ColorSet.RowArray.NumRows; ++i )
|
||||
{
|
||||
ret |= file.ApplyDyeTemplate( Penumbra.StainService.StmFile, j, i, dyeId );
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
var label = dyeId == 0 ? "Preview Dye###previewDye" : $"{name} (Preview)###previewDye";
|
||||
Penumbra.StainService.StainCombo.Draw( label, dyeColor, string.Empty, true );
|
||||
return false;
|
||||
}
|
||||
|
||||
private static unsafe bool ColorSetPasteAllClipboardButton( MtrlFile file, int colorSetIdx )
|
||||
{
|
||||
if( !ImGui.Button( "Import All Rows from Clipboard", ImGuiHelpers.ScaledVector2( 200, 0 ) ) || file.ColorSets.Length <= colorSetIdx )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var text = ImGui.GetClipboardText();
|
||||
var data = Convert.FromBase64String( text );
|
||||
if( data.Length < Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ref var rows = ref file.ColorSets[ colorSetIdx ].Rows;
|
||||
fixed( void* ptr = data, output = &rows )
|
||||
{
|
||||
MemoryUtility.MemCpyUnchecked( output, ptr, Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() );
|
||||
if( data.Length >= Marshal.SizeOf< MtrlFile.ColorSet.RowArray >() + Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >()
|
||||
&& file.ColorDyeSets.Length > colorSetIdx )
|
||||
{
|
||||
ref var dyeRows = ref file.ColorDyeSets[ colorSetIdx ].Rows;
|
||||
fixed( void* output2 = &dyeRows )
|
||||
{
|
||||
MemoryUtility.MemCpyUnchecked( output2, ( byte* )ptr + Marshal.SizeOf< MtrlFile.ColorSet.RowArray >(), Marshal.SizeOf< MtrlFile.ColorDyeSet.RowArray >() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void ColorSetCopyClipboardButton( MtrlFile.ColorSet.Row row, MtrlFile.ColorDyeSet.Row dye )
|
||||
{
|
||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
|
||||
"Export this row to your clipboard.", false, true ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
var data = new byte[MtrlFile.ColorSet.Row.Size + 2];
|
||||
fixed( byte* ptr = data )
|
||||
{
|
||||
MemoryUtility.MemCpyUnchecked( ptr, &row, MtrlFile.ColorSet.Row.Size );
|
||||
MemoryUtility.MemCpyUnchecked( ptr + MtrlFile.ColorSet.Row.Size, &dye, 2 );
|
||||
}
|
||||
|
||||
var text = Convert.ToBase64String( data );
|
||||
ImGui.SetClipboardText( text );
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe bool ColorSetPasteFromClipboardButton( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled )
|
||||
{
|
||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Paste.ToIconString(), ImGui.GetFrameHeight() * Vector2.One,
|
||||
"Import an exported row from your clipboard onto this row.", disabled, true ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
var text = ImGui.GetClipboardText();
|
||||
var data = Convert.FromBase64String( text );
|
||||
if( data.Length != MtrlFile.ColorSet.Row.Size + 2
|
||||
|| file.ColorSets.Length <= colorSetIdx )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
fixed( byte* ptr = data )
|
||||
{
|
||||
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorSet.Row* )ptr;
|
||||
if( colorSetIdx < file.ColorDyeSets.Length )
|
||||
{
|
||||
file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] = *( MtrlFile.ColorDyeSet.Row* )( ptr + MtrlFile.ColorSet.Row.Size );
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool DrawColorSetRow( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled )
|
||||
{
|
||||
static bool FixFloat( ref float val, float current )
|
||||
{
|
||||
val = ( float )( Half )val;
|
||||
return val != current;
|
||||
}
|
||||
|
||||
using var id = ImRaii.PushId( rowIdx );
|
||||
var row = file.ColorSets[ colorSetIdx ].Rows[ rowIdx ];
|
||||
var hasDye = file.ColorDyeSets.Length > colorSetIdx;
|
||||
var dye = hasDye ? file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ] : new MtrlFile.ColorDyeSet.Row();
|
||||
var floatSize = 70 * UiHelpers.Scale;
|
||||
var intSize = 45 * UiHelpers.Scale;
|
||||
ImGui.TableNextColumn();
|
||||
ColorSetCopyClipboardButton( row, dye );
|
||||
ImGui.SameLine();
|
||||
var ret = ColorSetPasteFromClipboardButton( file, colorSetIdx, rowIdx, disabled );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( $"#{rowIdx + 1:D2}" );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
using var dis = ImRaii.Disabled( disabled );
|
||||
ret |= ColorPicker( "##Diffuse", "Diffuse Color", row.Diffuse, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = c );
|
||||
if( hasDye )
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ret |= ImGuiUtil.Checkbox( "##dyeDiffuse", "Apply Diffuse Color on Dye", dye.Diffuse,
|
||||
b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Diffuse = b, ImGuiHoveredFlags.AllowWhenDisabled );
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ret |= ColorPicker( "##Specular", "Specular Color", row.Specular, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Specular = c );
|
||||
ImGui.SameLine();
|
||||
var tmpFloat = row.SpecularStrength;
|
||||
ImGui.SetNextItemWidth( floatSize );
|
||||
if( ImGui.DragFloat( "##SpecularStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.SpecularStrength ) )
|
||||
{
|
||||
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = tmpFloat;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( "Specular Strength", ImGuiHoveredFlags.AllowWhenDisabled );
|
||||
|
||||
if( hasDye )
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ret |= ImGuiUtil.Checkbox( "##dyeSpecular", "Apply Specular Color on Dye", dye.Specular,
|
||||
b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Specular = b, ImGuiHoveredFlags.AllowWhenDisabled );
|
||||
ImGui.SameLine();
|
||||
ret |= ImGuiUtil.Checkbox( "##dyeSpecularStrength", "Apply Specular Strength on Dye", dye.SpecularStrength,
|
||||
b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].SpecularStrength = b, ImGuiHoveredFlags.AllowWhenDisabled );
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ret |= ColorPicker( "##Emissive", "Emissive Color", row.Emissive, c => file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = c );
|
||||
if( hasDye )
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ret |= ImGuiUtil.Checkbox( "##dyeEmissive", "Apply Emissive Color on Dye", dye.Emissive,
|
||||
b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Emissive = b, ImGuiHoveredFlags.AllowWhenDisabled );
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
tmpFloat = row.GlossStrength;
|
||||
ImGui.SetNextItemWidth( floatSize );
|
||||
if( ImGui.DragFloat( "##GlossStrength", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.GlossStrength ) )
|
||||
{
|
||||
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].GlossStrength = tmpFloat;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( "Gloss Strength", ImGuiHoveredFlags.AllowWhenDisabled );
|
||||
if( hasDye )
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ret |= ImGuiUtil.Checkbox( "##dyeGloss", "Apply Gloss Strength on Dye", dye.Gloss,
|
||||
b => file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Gloss = b, ImGuiHoveredFlags.AllowWhenDisabled );
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
int tmpInt = row.TileSet;
|
||||
ImGui.SetNextItemWidth( intSize );
|
||||
if( ImGui.InputInt( "##TileSet", ref tmpInt, 0, 0 ) && tmpInt != row.TileSet && tmpInt is >= 0 and <= ushort.MaxValue )
|
||||
{
|
||||
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].TileSet = ( ushort )tmpInt;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( "Tile Set", ImGuiHoveredFlags.AllowWhenDisabled );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
tmpFloat = row.MaterialRepeat.X;
|
||||
ImGui.SetNextItemWidth( floatSize );
|
||||
if( ImGui.DragFloat( "##RepeatX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.X ) )
|
||||
{
|
||||
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { X = tmpFloat };
|
||||
ret = true;
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( "Repeat X", ImGuiHoveredFlags.AllowWhenDisabled );
|
||||
ImGui.SameLine();
|
||||
tmpFloat = row.MaterialRepeat.Y;
|
||||
ImGui.SetNextItemWidth( floatSize );
|
||||
if( ImGui.DragFloat( "##RepeatY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialRepeat.Y ) )
|
||||
{
|
||||
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialRepeat = row.MaterialRepeat with { Y = tmpFloat };
|
||||
ret = true;
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( "Repeat Y", ImGuiHoveredFlags.AllowWhenDisabled );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
tmpFloat = row.MaterialSkew.X;
|
||||
ImGui.SetNextItemWidth( floatSize );
|
||||
if( ImGui.DragFloat( "##SkewX", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.X ) )
|
||||
{
|
||||
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { X = tmpFloat };
|
||||
ret = true;
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( "Skew X", ImGuiHoveredFlags.AllowWhenDisabled );
|
||||
|
||||
ImGui.SameLine();
|
||||
tmpFloat = row.MaterialSkew.Y;
|
||||
ImGui.SetNextItemWidth( floatSize );
|
||||
if( ImGui.DragFloat( "##SkewY", ref tmpFloat, 0.1f, 0f ) && FixFloat( ref tmpFloat, row.MaterialSkew.Y ) )
|
||||
{
|
||||
file.ColorSets[ colorSetIdx ].Rows[ rowIdx ].MaterialSkew = row.MaterialSkew with { Y = tmpFloat };
|
||||
ret = true;
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( "Skew Y", ImGuiHoveredFlags.AllowWhenDisabled );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( hasDye )
|
||||
{
|
||||
if( Penumbra.StainService.TemplateCombo.Draw( "##dyeTemplate", dye.Template.ToString(), string.Empty, intSize
|
||||
+ ImGui.GetStyle().ScrollbarSize / 2, ImGui.GetTextLineHeightWithSpacing(), ImGuiComboFlags.NoArrowButton ) )
|
||||
{
|
||||
file.ColorDyeSets[ colorSetIdx ].Rows[ rowIdx ].Template = Penumbra.StainService.TemplateCombo.CurrentSelection;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( "Dye Template", ImGuiHoveredFlags.AllowWhenDisabled );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ret |= DrawDyePreview( file, colorSetIdx, rowIdx, disabled, dye, floatSize );
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
}
|
||||
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool DrawDyePreview( MtrlFile file, int colorSetIdx, int rowIdx, bool disabled, MtrlFile.ColorDyeSet.Row dye, float floatSize )
|
||||
{
|
||||
var stain = Penumbra.StainService.StainCombo.CurrentSelection.Key;
|
||||
if( stain == 0 || !Penumbra.StainService.StmFile.Entries.TryGetValue( dye.Template, out var entry ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var values = entry[ ( int )stain ];
|
||||
using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing / 2 );
|
||||
|
||||
var ret = ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.PaintBrush.ToIconString(), new Vector2( ImGui.GetFrameHeight() ),
|
||||
"Apply the selected dye to this row.", disabled, true );
|
||||
|
||||
ret = ret && file.ApplyDyeTemplate( Penumbra.StainService.StmFile, colorSetIdx, rowIdx, stain );
|
||||
|
||||
ImGui.SameLine();
|
||||
ColorPicker( "##diffusePreview", string.Empty, values.Diffuse, _ => { }, "D" );
|
||||
ImGui.SameLine();
|
||||
ColorPicker( "##specularPreview", string.Empty, values.Specular, _ => { }, "S" );
|
||||
ImGui.SameLine();
|
||||
ColorPicker( "##emissivePreview", string.Empty, values.Emissive, _ => { }, "E" );
|
||||
ImGui.SameLine();
|
||||
using var dis = ImRaii.Disabled();
|
||||
ImGui.SetNextItemWidth( floatSize );
|
||||
ImGui.DragFloat( "##gloss", ref values.Gloss, 0, 0, 0, "%.2f G" );
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth( floatSize );
|
||||
ImGui.DragFloat( "##specularStrength", ref values.SpecularPower, 0, 0, 0, "%.2f S" );
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool ColorPicker( string label, string tooltip, Vector3 input, Action< Vector3 > setter, string letter = "" )
|
||||
{
|
||||
var ret = false;
|
||||
var tmp = input;
|
||||
if( ImGui.ColorEdit3( label, ref tmp,
|
||||
ImGuiColorEditFlags.NoInputs | ImGuiColorEditFlags.DisplayRGB | ImGuiColorEditFlags.InputRGB | ImGuiColorEditFlags.NoTooltip )
|
||||
&& tmp != input )
|
||||
{
|
||||
setter( tmp );
|
||||
ret = true;
|
||||
}
|
||||
|
||||
if( letter.Length > 0 && ImGui.IsItemVisible() )
|
||||
{
|
||||
var textSize = ImGui.CalcTextSize( letter );
|
||||
var center = ImGui.GetItemRectMin() + ( ImGui.GetItemRectSize() - textSize ) / 2;
|
||||
var textColor = input.LengthSquared() < 0.25f ? 0x80FFFFFFu : 0x80000000u;
|
||||
ImGui.GetWindowDrawList().AddText( center, textColor, letter );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled );
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,293 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.Util;
|
||||
using static Penumbra.GameData.Files.ShpkFile;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private sealed class MtrlTab : IWritable
|
||||
{
|
||||
private readonly ModEditWindow _edit;
|
||||
public readonly MtrlFile Mtrl;
|
||||
|
||||
public uint NewKeyId;
|
||||
public uint NewKeyDefault;
|
||||
public uint NewConstantId;
|
||||
public int NewConstantIdx;
|
||||
public uint NewSamplerId;
|
||||
public int NewSamplerIdx;
|
||||
|
||||
|
||||
public ShpkFile? AssociatedShpk;
|
||||
public readonly List< string > TextureLabels = new(4);
|
||||
public FullPath LoadedShpkPath = FullPath.Empty;
|
||||
public string LoadedShpkPathName = string.Empty;
|
||||
public float TextureLabelWidth;
|
||||
|
||||
// Shader Key State
|
||||
public readonly List< string > ShaderKeyLabels = new(16);
|
||||
public readonly Dictionary< uint, uint > DefinedShaderKeys = new(16);
|
||||
public readonly List< int > MissingShaderKeyIndices = new(16);
|
||||
public readonly List< uint > AvailableKeyValues = new(16);
|
||||
public string VertexShaders = "Vertex Shaders: ???";
|
||||
public string PixelShaders = "Pixel Shaders: ???";
|
||||
|
||||
// Material Constants
|
||||
public readonly List< (string Name, bool ComponentOnly, int ParamValueOffset) > MaterialConstants = new(16);
|
||||
public readonly List< (string Name, uint Id, ushort ByteSize) > MissingMaterialConstants = new(16);
|
||||
public readonly HashSet< uint > DefinedMaterialConstants = new(16);
|
||||
|
||||
public string MaterialConstantLabel = "Constants###Constants";
|
||||
public IndexSet OrphanedMaterialValues = new(0, false);
|
||||
public int AliasedMaterialValueCount;
|
||||
public bool HasMalformedMaterialConstants;
|
||||
|
||||
// Samplers
|
||||
public readonly List< (string Label, string FileName) > Samplers = new(4);
|
||||
public readonly List< (string Name, uint Id) > MissingSamplers = new(4);
|
||||
public readonly HashSet< uint > DefinedSamplers = new(4);
|
||||
public IndexSet OrphanedSamplers = new(0, false);
|
||||
public int AliasedSamplerCount;
|
||||
|
||||
public FullPath FindAssociatedShpk( out string defaultPath, out Utf8GamePath defaultGamePath )
|
||||
{
|
||||
defaultPath = GamePaths.Shader.ShpkPath( Mtrl.ShaderPackage.Name );
|
||||
if( !Utf8GamePath.FromString( defaultPath, out defaultGamePath, true ) )
|
||||
{
|
||||
return FullPath.Empty;
|
||||
}
|
||||
|
||||
return _edit.FindBestMatch( defaultGamePath );
|
||||
}
|
||||
|
||||
public void LoadShpk( FullPath path )
|
||||
{
|
||||
try
|
||||
{
|
||||
LoadedShpkPath = path;
|
||||
var data = LoadedShpkPath.IsRooted
|
||||
? File.ReadAllBytes( LoadedShpkPath.FullName )
|
||||
: DalamudServices.SGameData.GetFile( LoadedShpkPath.InternalName.ToString() )?.Data;
|
||||
AssociatedShpk = data?.Length > 0 ? new ShpkFile( data ) : throw new Exception( "Failure to load file data." );
|
||||
LoadedShpkPathName = path.ToPath();
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
LoadedShpkPath = FullPath.Empty;
|
||||
LoadedShpkPathName = string.Empty;
|
||||
AssociatedShpk = null;
|
||||
Penumbra.ChatService.NotificationMessage( $"Could not load {LoadedShpkPath.ToPath()}:\n{e}", "Penumbra Advanced Editing", NotificationType.Error );
|
||||
}
|
||||
|
||||
Update();
|
||||
}
|
||||
|
||||
public void UpdateTextureLabels()
|
||||
{
|
||||
var samplers = Mtrl.GetSamplersByTexture( AssociatedShpk );
|
||||
TextureLabels.Clear();
|
||||
TextureLabelWidth = 50f * UiHelpers.Scale;
|
||||
using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) )
|
||||
{
|
||||
for( var i = 0; i < Mtrl.Textures.Length; ++i )
|
||||
{
|
||||
var (sampler, shpkSampler) = samplers[ i ];
|
||||
var name = shpkSampler.HasValue ? shpkSampler.Value.Name : sampler.HasValue ? $"0x{sampler.Value.SamplerId:X8}" : $"#{i}";
|
||||
TextureLabels.Add( name );
|
||||
TextureLabelWidth = Math.Max( TextureLabelWidth, ImGui.CalcTextSize( name ).X );
|
||||
}
|
||||
}
|
||||
|
||||
TextureLabelWidth = TextureLabelWidth / UiHelpers.Scale + 4;
|
||||
}
|
||||
|
||||
public void UpdateShaderKeyLabels()
|
||||
{
|
||||
ShaderKeyLabels.Clear();
|
||||
DefinedShaderKeys.Clear();
|
||||
foreach( var (key, idx) in Mtrl.ShaderPackage.ShaderKeys.WithIndex() )
|
||||
{
|
||||
ShaderKeyLabels.Add( $"#{idx}: 0x{key.Category:X8} = 0x{key.Value:X8}###{idx}: 0x{key.Category:X8}" );
|
||||
DefinedShaderKeys.Add( key.Category, key.Value );
|
||||
}
|
||||
|
||||
MissingShaderKeyIndices.Clear();
|
||||
AvailableKeyValues.Clear();
|
||||
var vertexShaders = new IndexSet( AssociatedShpk?.VertexShaders.Length ?? 0, false );
|
||||
var pixelShaders = new IndexSet( AssociatedShpk?.PixelShaders.Length ?? 0, false );
|
||||
if( AssociatedShpk != null )
|
||||
{
|
||||
MissingShaderKeyIndices.AddRange( AssociatedShpk.MaterialKeys.WithIndex().Where( k => !DefinedShaderKeys.ContainsKey( k.Value.Id ) ).WithoutValue() );
|
||||
|
||||
if( MissingShaderKeyIndices.Count > 0 && MissingShaderKeyIndices.All( i => AssociatedShpk.MaterialKeys[ i ].Id != NewKeyId ) )
|
||||
{
|
||||
var key = AssociatedShpk.MaterialKeys[ MissingShaderKeyIndices[ 0 ] ];
|
||||
NewKeyId = key.Id;
|
||||
NewKeyDefault = key.DefaultValue;
|
||||
}
|
||||
|
||||
AvailableKeyValues.AddRange( AssociatedShpk.MaterialKeys.Select( k => DefinedShaderKeys.TryGetValue( k.Id, out var value ) ? value : k.DefaultValue ) );
|
||||
foreach( var node in AssociatedShpk.Nodes )
|
||||
{
|
||||
if( node.MaterialKeys.WithIndex().All( key => key.Value == AvailableKeyValues[ key.Index ] ) )
|
||||
{
|
||||
foreach( var pass in node.Passes )
|
||||
{
|
||||
vertexShaders.Add( ( int )pass.VertexShader );
|
||||
pixelShaders.Add( ( int )pass.PixelShader );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VertexShaders = $"Vertex Shaders: {( vertexShaders.Count > 0 ? string.Join( ", ", vertexShaders.Select( i => $"#{i}" ) ) : "???" )}";
|
||||
PixelShaders = $"Pixel Shaders: {( pixelShaders.Count > 0 ? string.Join( ", ", pixelShaders.Select( i => $"#{i}" ) ) : "???" )}";
|
||||
}
|
||||
|
||||
public void UpdateConstantLabels()
|
||||
{
|
||||
var prefix = AssociatedShpk?.GetConstantById( MaterialParamsConstantId )?.Name ?? string.Empty;
|
||||
MaterialConstantLabel = prefix.Length == 0 ? "Constants###Constants" : prefix + "###Constants";
|
||||
|
||||
DefinedMaterialConstants.Clear();
|
||||
MaterialConstants.Clear();
|
||||
HasMalformedMaterialConstants = false;
|
||||
AliasedMaterialValueCount = 0;
|
||||
OrphanedMaterialValues = new IndexSet( Mtrl.ShaderPackage.ShaderValues.Length, true );
|
||||
foreach( var (constant, idx) in Mtrl.ShaderPackage.Constants.WithIndex() )
|
||||
{
|
||||
DefinedMaterialConstants.Add( constant.Id );
|
||||
var values = Mtrl.GetConstantValues( constant );
|
||||
var paramValueOffset = -values.Length;
|
||||
if( values.Length > 0 )
|
||||
{
|
||||
var shpkParam = AssociatedShpk?.GetMaterialParamById( constant.Id );
|
||||
var paramByteOffset = shpkParam?.ByteOffset ?? -1;
|
||||
if( ( paramByteOffset & 0x3 ) == 0 )
|
||||
{
|
||||
paramValueOffset = paramByteOffset >> 2;
|
||||
}
|
||||
|
||||
var unique = OrphanedMaterialValues.RemoveRange( constant.ByteOffset >> 2, values.Length );
|
||||
AliasedMaterialValueCount += values.Length - unique;
|
||||
}
|
||||
else
|
||||
{
|
||||
HasMalformedMaterialConstants = true;
|
||||
}
|
||||
|
||||
var (name, componentOnly) = MaterialParamRangeName( prefix, paramValueOffset, values.Length );
|
||||
var label = name == null
|
||||
? $"#{idx:D2} (ID: 0x{constant.Id:X8})###{constant.Id}"
|
||||
: $"#{idx:D2}: {name} (ID: 0x{constant.Id:X8})###{constant.Id}";
|
||||
|
||||
MaterialConstants.Add( ( label, componentOnly, paramValueOffset ) );
|
||||
}
|
||||
|
||||
MissingMaterialConstants.Clear();
|
||||
if( AssociatedShpk != null )
|
||||
{
|
||||
var setIdx = false;
|
||||
foreach( var param in AssociatedShpk.MaterialParams.Where( m => !DefinedMaterialConstants.Contains( m.Id ) ) )
|
||||
{
|
||||
var (name, _) = MaterialParamRangeName( prefix, param.ByteOffset >> 2, param.ByteSize >> 2 );
|
||||
var label = name == null
|
||||
? $"(ID: 0x{param.Id:X8})"
|
||||
: $"{name} (ID: 0x{param.Id:X8})";
|
||||
if( NewConstantId == param.Id )
|
||||
{
|
||||
setIdx = true;
|
||||
NewConstantIdx = MissingMaterialConstants.Count;
|
||||
}
|
||||
|
||||
MissingMaterialConstants.Add( ( label, param.Id, param.ByteSize ) );
|
||||
}
|
||||
|
||||
if( !setIdx && MissingMaterialConstants.Count > 0 )
|
||||
{
|
||||
NewConstantIdx = 0;
|
||||
NewConstantId = MissingMaterialConstants[ 0 ].Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateSamplers()
|
||||
{
|
||||
Samplers.Clear();
|
||||
DefinedSamplers.Clear();
|
||||
OrphanedSamplers = new IndexSet( Mtrl.Textures.Length, true );
|
||||
foreach( var (sampler, idx) in Mtrl.ShaderPackage.Samplers.WithIndex() )
|
||||
{
|
||||
DefinedSamplers.Add( sampler.SamplerId );
|
||||
if( !OrphanedSamplers.Remove( sampler.TextureIndex ) )
|
||||
{
|
||||
++AliasedSamplerCount;
|
||||
}
|
||||
|
||||
var shpk = AssociatedShpk?.GetSamplerById( sampler.SamplerId );
|
||||
var label = shpk.HasValue
|
||||
? $"#{idx}: {shpk.Value.Name} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}"
|
||||
: $"#{idx} (ID: 0x{sampler.SamplerId:X8})##{sampler.SamplerId}";
|
||||
var fileName = $"Texture #{sampler.TextureIndex} - {Path.GetFileName( Mtrl.Textures[ sampler.TextureIndex ].Path )}";
|
||||
Samplers.Add( ( label, fileName ) );
|
||||
}
|
||||
|
||||
MissingSamplers.Clear();
|
||||
if( AssociatedShpk != null )
|
||||
{
|
||||
var setSampler = false;
|
||||
foreach( var sampler in AssociatedShpk.Samplers.Where( s => s.Slot == 2 && !DefinedSamplers.Contains( s.Id ) ) )
|
||||
{
|
||||
if( sampler.Id == NewSamplerId )
|
||||
{
|
||||
setSampler = true;
|
||||
NewSamplerIdx = MissingSamplers.Count;
|
||||
}
|
||||
|
||||
MissingSamplers.Add( ( sampler.Name, sampler.Id ) );
|
||||
}
|
||||
|
||||
if( !setSampler && MissingSamplers.Count > 0 )
|
||||
{
|
||||
NewSamplerIdx = 0;
|
||||
NewSamplerId = MissingSamplers[ 0 ].Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
UpdateTextureLabels();
|
||||
UpdateShaderKeyLabels();
|
||||
UpdateConstantLabels();
|
||||
UpdateSamplers();
|
||||
}
|
||||
|
||||
public MtrlTab( ModEditWindow edit, MtrlFile file )
|
||||
{
|
||||
_edit = edit;
|
||||
Mtrl = file;
|
||||
LoadShpk( FindAssociatedShpk( out _, out _ ) );
|
||||
}
|
||||
|
||||
public bool Valid
|
||||
=> Mtrl.Valid;
|
||||
|
||||
public byte[] Write()
|
||||
=> Mtrl.Write();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,535 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiFileDialog;
|
||||
using ImGuiNET;
|
||||
using Lumina.Data.Parsing;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private readonly FileDialogService _fileDialog;
|
||||
|
||||
private bool DrawPackageNameInput(MtrlTab tab, bool disabled)
|
||||
{
|
||||
var ret = false;
|
||||
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
|
||||
if (ImGui.InputText("Shader Package Name", ref tab.Mtrl.ShaderPackage.Name, 63,
|
||||
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))
|
||||
{
|
||||
ret = true;
|
||||
tab.AssociatedShpk = null;
|
||||
tab.LoadedShpkPath = FullPath.Empty;
|
||||
}
|
||||
|
||||
if (ImGui.IsItemDeactivatedAfterEdit())
|
||||
tab.LoadShpk(tab.FindAssociatedShpk(out _, out _));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool DrawShaderFlagsInput(MtrlFile file, bool disabled)
|
||||
{
|
||||
var ret = false;
|
||||
var shpkFlags = (int)file.ShaderPackage.Flags;
|
||||
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
|
||||
if (ImGui.InputInt("Shader Package Flags", ref shpkFlags, 0, 0,
|
||||
ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)))
|
||||
{
|
||||
file.ShaderPackage.Flags = (uint)shpkFlags;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Show the currently associated shpk file, if any, and the buttons to associate
|
||||
/// a specific shpk from your drive, the modded shpk by path or the default shpk.
|
||||
/// </summary>
|
||||
private void DrawCustomAssociations(MtrlTab tab)
|
||||
{
|
||||
var text = tab.AssociatedShpk == null
|
||||
? "Associated .shpk file: None"
|
||||
: $"Associated .shpk file: {tab.LoadedShpkPathName}";
|
||||
|
||||
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
|
||||
|
||||
if (ImGui.Selectable(text))
|
||||
ImGui.SetClipboardText(tab.LoadedShpkPathName);
|
||||
|
||||
ImGuiUtil.HoverTooltip("Click to copy file path to clipboard.");
|
||||
|
||||
if (ImGui.Button("Associate Custom .shpk File"))
|
||||
_fileDialog.OpenFilePicker("Associate Custom .shpk File...", ".shpk", (success, name) =>
|
||||
{
|
||||
if (success)
|
||||
tab.LoadShpk(new FullPath(name[0]));
|
||||
}, 1, _mod!.ModPath.FullName, false);
|
||||
|
||||
var moddedPath = tab.FindAssociatedShpk(out var defaultPath, out var gamePath);
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.DrawDisabledButton("Associate Default .shpk File", Vector2.Zero, moddedPath.ToPath(),
|
||||
moddedPath.Equals(tab.LoadedShpkPath)))
|
||||
tab.LoadShpk(moddedPath);
|
||||
|
||||
if (!gamePath.Path.Equals(moddedPath.InternalName))
|
||||
{
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.DrawDisabledButton("Associate Unmodded .shpk File", Vector2.Zero, defaultPath,
|
||||
gamePath.Path.Equals(tab.LoadedShpkPath.InternalName)))
|
||||
tab.LoadShpk(new FullPath(gamePath));
|
||||
}
|
||||
|
||||
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
|
||||
}
|
||||
|
||||
|
||||
private static bool DrawShaderKey(MtrlTab tab, bool disabled, ref int idx)
|
||||
{
|
||||
var ret = false;
|
||||
using var t2 = ImRaii.TreeNode(tab.ShaderKeyLabels[idx], disabled ? ImGuiTreeNodeFlags.Leaf : 0);
|
||||
if (!t2 || disabled)
|
||||
return ret;
|
||||
|
||||
var key = tab.Mtrl.ShaderPackage.ShaderKeys[idx];
|
||||
var shpkKey = tab.AssociatedShpk?.GetMaterialKeyById(key.Category);
|
||||
if (shpkKey.HasValue)
|
||||
{
|
||||
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
|
||||
using var c = ImRaii.Combo("Value", $"0x{key.Value:X8}");
|
||||
if (c)
|
||||
foreach (var value in shpkKey.Value.Values)
|
||||
{
|
||||
if (ImGui.Selectable($"0x{value:X8}", value == key.Value))
|
||||
{
|
||||
tab.Mtrl.ShaderPackage.ShaderKeys[idx].Value = value;
|
||||
ret = true;
|
||||
tab.UpdateShaderKeyLabels();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui.Button("Remove Key"))
|
||||
{
|
||||
tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.RemoveItems(idx--);
|
||||
ret = true;
|
||||
tab.UpdateShaderKeyLabels();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool DrawNewShaderKey(MtrlTab tab)
|
||||
{
|
||||
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
|
||||
var ret = false;
|
||||
using (var c = ImRaii.Combo("##NewConstantId", $"ID: 0x{tab.NewKeyId:X8}"))
|
||||
{
|
||||
if (c)
|
||||
foreach (var idx in tab.MissingShaderKeyIndices)
|
||||
{
|
||||
var key = tab.AssociatedShpk!.MaterialKeys[idx];
|
||||
|
||||
if (ImGui.Selectable($"ID: 0x{key.Id:X8}", key.Id == tab.NewKeyId))
|
||||
{
|
||||
tab.NewKeyDefault = key.DefaultValue;
|
||||
tab.NewKeyId = key.Id;
|
||||
ret = true;
|
||||
tab.UpdateShaderKeyLabels();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Add Key"))
|
||||
{
|
||||
tab.Mtrl.ShaderPackage.ShaderKeys = tab.Mtrl.ShaderPackage.ShaderKeys.AddItem(new ShaderKey
|
||||
{
|
||||
Category = tab.NewKeyId,
|
||||
Value = tab.NewKeyDefault,
|
||||
});
|
||||
ret = true;
|
||||
tab.UpdateShaderKeyLabels();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool DrawMaterialShaderKeys(MtrlTab tab, bool disabled)
|
||||
{
|
||||
if (tab.Mtrl.ShaderPackage.ShaderKeys.Length <= 0
|
||||
&& (disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialKeys.Length <= 0))
|
||||
return false;
|
||||
|
||||
using var t = ImRaii.TreeNode("Shader Keys");
|
||||
if (!t)
|
||||
return false;
|
||||
|
||||
var ret = false;
|
||||
for (var idx = 0; idx < tab.Mtrl.ShaderPackage.ShaderKeys.Length; ++idx)
|
||||
ret |= DrawShaderKey(tab, disabled, ref idx);
|
||||
|
||||
if (!disabled && tab.AssociatedShpk != null && tab.MissingShaderKeyIndices.Count != 0)
|
||||
ret |= DrawNewShaderKey(tab);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static void DrawMaterialShaders(MtrlTab tab)
|
||||
{
|
||||
if (tab.AssociatedShpk == null)
|
||||
return;
|
||||
|
||||
ImRaii.TreeNode(tab.VertexShaders, ImGuiTreeNodeFlags.Leaf).Dispose();
|
||||
ImRaii.TreeNode(tab.PixelShaders, ImGuiTreeNodeFlags.Leaf).Dispose();
|
||||
}
|
||||
|
||||
|
||||
private static bool DrawMaterialConstantValues(MtrlTab tab, bool disabled, ref int idx)
|
||||
{
|
||||
var (name, componentOnly, paramValueOffset) = tab.MaterialConstants[idx];
|
||||
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
|
||||
using var t2 = ImRaii.TreeNode(name);
|
||||
if (!t2)
|
||||
return false;
|
||||
|
||||
font.Dispose();
|
||||
|
||||
var constant = tab.Mtrl.ShaderPackage.Constants[idx];
|
||||
var ret = false;
|
||||
var values = tab.Mtrl.GetConstantValues(constant);
|
||||
if (values.Length > 0)
|
||||
{
|
||||
var valueOffset = constant.ByteOffset >> 2;
|
||||
|
||||
for (var valueIdx = 0; valueIdx < values.Length; ++valueIdx)
|
||||
{
|
||||
var paramName = MaterialParamName(componentOnly, paramValueOffset + valueIdx) ?? $"#{valueIdx}";
|
||||
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
|
||||
if (ImGui.InputFloat($"{paramName} (at 0x{(valueOffset + valueIdx) << 2:X4})", ref values[valueIdx], 0.0f, 0.0f, "%.3f",
|
||||
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))
|
||||
{
|
||||
ret = true;
|
||||
tab.UpdateConstantLabels();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImRaii.TreeNode($"Offset: 0x{constant.ByteOffset:X4}", ImGuiTreeNodeFlags.Leaf).Dispose();
|
||||
ImRaii.TreeNode($"Size: 0x{constant.ByteSize:X4}", ImGuiTreeNodeFlags.Leaf).Dispose();
|
||||
}
|
||||
|
||||
if (!disabled
|
||||
&& !tab.HasMalformedMaterialConstants
|
||||
&& tab.OrphanedMaterialValues.Count == 0
|
||||
&& tab.AliasedMaterialValueCount == 0
|
||||
&& ImGui.Button("Remove Constant"))
|
||||
{
|
||||
tab.Mtrl.ShaderPackage.ShaderValues =
|
||||
tab.Mtrl.ShaderPackage.ShaderValues.RemoveItems(constant.ByteOffset >> 2, constant.ByteSize >> 2);
|
||||
tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.RemoveItems(idx--);
|
||||
for (var i = 0; i < tab.Mtrl.ShaderPackage.Constants.Length; ++i)
|
||||
{
|
||||
if (tab.Mtrl.ShaderPackage.Constants[i].ByteOffset >= constant.ByteOffset)
|
||||
tab.Mtrl.ShaderPackage.Constants[i].ByteOffset -= constant.ByteSize;
|
||||
}
|
||||
|
||||
ret = true;
|
||||
tab.UpdateConstantLabels();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool DrawMaterialOrphans(MtrlTab tab, bool disabled)
|
||||
{
|
||||
using var t2 = ImRaii.TreeNode($"Orphan Values ({tab.OrphanedMaterialValues.Count})");
|
||||
if (!t2)
|
||||
return false;
|
||||
|
||||
var ret = false;
|
||||
foreach (var idx in tab.OrphanedMaterialValues)
|
||||
{
|
||||
ImGui.SetNextItemWidth(ImGui.GetFontSize() * 10.0f);
|
||||
if (ImGui.InputFloat($"#{idx} (at 0x{idx << 2:X4})",
|
||||
ref tab.Mtrl.ShaderPackage.ShaderValues[idx], 0.0f, 0.0f, "%.3f",
|
||||
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))
|
||||
{
|
||||
ret = true;
|
||||
tab.UpdateConstantLabels();
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool DrawNewMaterialParam(MtrlTab tab)
|
||||
{
|
||||
ImGui.SetNextItemWidth(UiHelpers.Scale * 450.0f);
|
||||
using (var font = ImRaii.PushFont(UiBuilder.MonoFont))
|
||||
{
|
||||
using var c = ImRaii.Combo("##NewConstantId", tab.MissingMaterialConstants[tab.NewConstantIdx].Name);
|
||||
if (c)
|
||||
foreach (var (constant, idx) in tab.MissingMaterialConstants.WithIndex())
|
||||
{
|
||||
if (ImGui.Selectable(constant.Name, constant.Id == tab.NewConstantId))
|
||||
{
|
||||
tab.NewConstantIdx = idx;
|
||||
tab.NewConstantId = constant.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Add Constant"))
|
||||
{
|
||||
var (_, _, byteSize) = tab.MissingMaterialConstants[tab.NewConstantIdx];
|
||||
tab.Mtrl.ShaderPackage.Constants = tab.Mtrl.ShaderPackage.Constants.AddItem(new MtrlFile.Constant
|
||||
{
|
||||
Id = tab.NewConstantId,
|
||||
ByteOffset = (ushort)(tab.Mtrl.ShaderPackage.ShaderValues.Length << 2),
|
||||
ByteSize = byteSize,
|
||||
});
|
||||
tab.Mtrl.ShaderPackage.ShaderValues = tab.Mtrl.ShaderPackage.ShaderValues.AddItem(0.0f, byteSize >> 2);
|
||||
tab.UpdateConstantLabels();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool DrawMaterialConstants(MtrlTab tab, bool disabled)
|
||||
{
|
||||
if (tab.Mtrl.ShaderPackage.Constants.Length == 0
|
||||
&& tab.Mtrl.ShaderPackage.ShaderValues.Length == 0
|
||||
&& (disabled || tab.AssociatedShpk == null || tab.AssociatedShpk.MaterialParams.Length == 0))
|
||||
return false;
|
||||
|
||||
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
|
||||
using var t = ImRaii.TreeNode(tab.MaterialConstantLabel);
|
||||
if (!t)
|
||||
return false;
|
||||
|
||||
font.Dispose();
|
||||
var ret = false;
|
||||
for (var idx = 0; idx < tab.Mtrl.ShaderPackage.Constants.Length; ++idx)
|
||||
ret |= DrawMaterialConstantValues(tab, disabled, ref idx);
|
||||
|
||||
if (tab.OrphanedMaterialValues.Count > 0)
|
||||
ret |= DrawMaterialOrphans(tab, disabled);
|
||||
else if (!disabled && !tab.HasMalformedMaterialConstants && tab.MissingMaterialConstants.Count > 0)
|
||||
ret |= DrawNewMaterialParam(tab);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool DrawMaterialSampler(MtrlTab tab, bool disabled, ref int idx)
|
||||
{
|
||||
var (label, filename) = tab.Samplers[idx];
|
||||
using var tree = ImRaii.TreeNode(label);
|
||||
if (!tree)
|
||||
return false;
|
||||
|
||||
ImRaii.TreeNode(filename, ImGuiTreeNodeFlags.Leaf).Dispose();
|
||||
var ret = false;
|
||||
var sampler = tab.Mtrl.ShaderPackage.Samplers[idx];
|
||||
|
||||
// FIXME this probably doesn't belong here
|
||||
static unsafe bool InputHexUInt16(string label, ref ushort v, ImGuiInputTextFlags flags)
|
||||
{
|
||||
fixed (ushort* v2 = &v)
|
||||
{
|
||||
return ImGui.InputScalar(label, ImGuiDataType.U16, (nint)v2, IntPtr.Zero, IntPtr.Zero, "%04X", flags);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
|
||||
if (InputHexUInt16("Texture Flags", ref tab.Mtrl.Textures[sampler.TextureIndex].Flags,
|
||||
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None))
|
||||
ret = true;
|
||||
|
||||
var samplerFlags = (int)sampler.Flags;
|
||||
ImGui.SetNextItemWidth(UiHelpers.Scale * 150.0f);
|
||||
if (ImGui.InputInt("Sampler Flags", ref samplerFlags, 0, 0,
|
||||
ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)))
|
||||
{
|
||||
tab.Mtrl.ShaderPackage.Samplers[idx].Flags = (uint)samplerFlags;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
if (!disabled
|
||||
&& tab.OrphanedSamplers.Count == 0
|
||||
&& tab.AliasedSamplerCount == 0
|
||||
&& ImGui.Button("Remove Sampler"))
|
||||
{
|
||||
tab.Mtrl.Textures = tab.Mtrl.Textures.RemoveItems(sampler.TextureIndex);
|
||||
tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.RemoveItems(idx--);
|
||||
for (var i = 0; i < tab.Mtrl.ShaderPackage.Samplers.Length; ++i)
|
||||
{
|
||||
if (tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex >= sampler.TextureIndex)
|
||||
--tab.Mtrl.ShaderPackage.Samplers[i].TextureIndex;
|
||||
}
|
||||
|
||||
ret = true;
|
||||
tab.UpdateSamplers();
|
||||
tab.UpdateTextureLabels();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool DrawMaterialNewSampler(MtrlTab tab)
|
||||
{
|
||||
var (name, id) = tab.MissingSamplers[tab.NewSamplerIdx];
|
||||
ImGui.SetNextItemWidth(UiHelpers.Scale * 450.0f);
|
||||
using (var c = ImRaii.Combo("##NewSamplerId", $"{name} (ID: 0x{id:X8})"))
|
||||
{
|
||||
if (c)
|
||||
foreach (var (sampler, idx) in tab.MissingSamplers.WithIndex())
|
||||
{
|
||||
if (ImGui.Selectable($"{sampler.Name} (ID: 0x{sampler.Id:X8})", sampler.Id == tab.NewSamplerId))
|
||||
{
|
||||
tab.NewSamplerIdx = idx;
|
||||
tab.NewSamplerId = sampler.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (!ImGui.Button("Add Sampler"))
|
||||
return false;
|
||||
|
||||
tab.Mtrl.ShaderPackage.Samplers = tab.Mtrl.ShaderPackage.Samplers.AddItem(new Sampler
|
||||
{
|
||||
SamplerId = tab.NewSamplerId,
|
||||
TextureIndex = (byte)tab.Mtrl.Textures.Length,
|
||||
Flags = 0,
|
||||
});
|
||||
tab.Mtrl.Textures = tab.Mtrl.Textures.AddItem(new MtrlFile.Texture
|
||||
{
|
||||
Path = string.Empty,
|
||||
Flags = 0,
|
||||
});
|
||||
tab.UpdateSamplers();
|
||||
tab.UpdateTextureLabels();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool DrawMaterialSamplers(MtrlTab tab, bool disabled)
|
||||
{
|
||||
if (tab.Mtrl.ShaderPackage.Samplers.Length == 0
|
||||
&& tab.Mtrl.Textures.Length == 0
|
||||
&& (disabled || (tab.AssociatedShpk?.Samplers.All(sampler => sampler.Slot != 2) ?? false)))
|
||||
return false;
|
||||
|
||||
using var t = ImRaii.TreeNode("Samplers");
|
||||
if (!t)
|
||||
return false;
|
||||
|
||||
var ret = false;
|
||||
for (var idx = 0; idx < tab.Mtrl.ShaderPackage.Samplers.Length; ++idx)
|
||||
ret |= DrawMaterialSampler(tab, disabled, ref idx);
|
||||
|
||||
if (tab.OrphanedSamplers.Count > 0)
|
||||
{
|
||||
using var t2 = ImRaii.TreeNode($"Orphan Textures ({tab.OrphanedSamplers.Count})");
|
||||
if (t2)
|
||||
foreach (var idx in tab.OrphanedSamplers)
|
||||
{
|
||||
ImRaii.TreeNode($"#{idx}: {Path.GetFileName(tab.Mtrl.Textures[idx].Path)} - {tab.Mtrl.Textures[idx].Flags:X4}",
|
||||
ImGuiTreeNodeFlags.Leaf)
|
||||
.Dispose();
|
||||
}
|
||||
}
|
||||
else if (!disabled && tab.MissingSamplers.Count > 0 && tab.AliasedSamplerCount == 0 && tab.Mtrl.Textures.Length < 255)
|
||||
{
|
||||
ret |= DrawMaterialNewSampler(tab);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private bool DrawMaterialShaderResources(MtrlTab tab, bool disabled)
|
||||
{
|
||||
var ret = false;
|
||||
if (!ImGui.CollapsingHeader("Advanced Shader Resources"))
|
||||
return ret;
|
||||
|
||||
ret |= DrawPackageNameInput(tab, disabled);
|
||||
ret |= DrawShaderFlagsInput(tab.Mtrl, disabled);
|
||||
DrawCustomAssociations(tab);
|
||||
ret |= DrawMaterialShaderKeys(tab, disabled);
|
||||
DrawMaterialShaders(tab);
|
||||
ret |= DrawMaterialConstants(tab, disabled);
|
||||
ret |= DrawMaterialSamplers(tab, disabled);
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static string? MaterialParamName(bool componentOnly, int offset)
|
||||
{
|
||||
if (offset < 0)
|
||||
return null;
|
||||
|
||||
return (componentOnly, offset & 0x3) switch
|
||||
{
|
||||
(true, 0) => "x",
|
||||
(true, 1) => "y",
|
||||
(true, 2) => "z",
|
||||
(true, 3) => "w",
|
||||
(false, 0) => $"[{offset >> 2:D2}].x",
|
||||
(false, 1) => $"[{offset >> 2:D2}].y",
|
||||
(false, 2) => $"[{offset >> 2:D2}].z",
|
||||
(false, 3) => $"[{offset >> 2:D2}].w",
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
private static (string? Name, bool ComponentOnly) MaterialParamRangeName(string prefix, int valueOffset, int valueLength)
|
||||
{
|
||||
static string VectorSwizzle(int firstComponent, int lastComponent)
|
||||
=> (firstComponent, lastComponent) switch
|
||||
{
|
||||
(0, 4) => " ",
|
||||
(0, 0) => ".x ",
|
||||
(0, 1) => ".xy ",
|
||||
(0, 2) => ".xyz ",
|
||||
(0, 3) => " ",
|
||||
(1, 1) => ".y ",
|
||||
(1, 2) => ".yz ",
|
||||
(1, 3) => ".yzw ",
|
||||
(2, 2) => ".z ",
|
||||
(2, 3) => ".zw ",
|
||||
(3, 3) => ".w ",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
if (valueLength == 0 || valueOffset < 0)
|
||||
return (null, false);
|
||||
|
||||
var firstVector = valueOffset >> 2;
|
||||
var lastVector = (valueOffset + valueLength - 1) >> 2;
|
||||
var firstComponent = valueOffset & 0x3;
|
||||
var lastComponent = (valueOffset + valueLength - 1) & 0x3;
|
||||
if (firstVector == lastVector)
|
||||
return ($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, lastComponent)}", true);
|
||||
|
||||
var sb = new StringBuilder(128);
|
||||
sb.Append($"{prefix}[{firstVector}]{VectorSwizzle(firstComponent, 3).TrimEnd()}");
|
||||
for (var i = firstVector + 1; i < lastVector; ++i)
|
||||
sb.Append($", [{i}]");
|
||||
|
||||
sb.Append($", [{lastVector}]{VectorSwizzle(0, lastComponent)}");
|
||||
return (sb.ToString(), false);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,194 +0,0 @@
|
|||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private readonly FileEditor< MtrlTab > _materialTab;
|
||||
|
||||
private bool DrawMaterialPanel( MtrlTab tab, bool disabled )
|
||||
{
|
||||
var ret = DrawMaterialTextureChange( tab, disabled );
|
||||
|
||||
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
|
||||
ret |= DrawBackFaceAndTransparency( tab.Mtrl, disabled );
|
||||
|
||||
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
|
||||
ret |= DrawMaterialColorSetChange( tab.Mtrl, disabled );
|
||||
|
||||
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
|
||||
ret |= DrawMaterialShaderResources( tab, disabled );
|
||||
|
||||
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
|
||||
DrawOtherMaterialDetails( tab.Mtrl, disabled );
|
||||
|
||||
return !disabled && ret;
|
||||
}
|
||||
|
||||
private static bool DrawMaterialTextureChange( MtrlTab tab, bool disabled )
|
||||
{
|
||||
var ret = false;
|
||||
using var table = ImRaii.Table( "##Textures", 2 );
|
||||
ImGui.TableSetupColumn( "Path", ImGuiTableColumnFlags.WidthStretch );
|
||||
ImGui.TableSetupColumn( "Name", ImGuiTableColumnFlags.WidthFixed, tab.TextureLabelWidth * UiHelpers.Scale );
|
||||
for( var i = 0; i < tab.Mtrl.Textures.Length; ++i )
|
||||
{
|
||||
using var _ = ImRaii.PushId( i );
|
||||
var tmp = tab.Mtrl.Textures[ i ].Path;
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth( ImGui.GetContentRegionAvail().X );
|
||||
if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength,
|
||||
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None )
|
||||
&& tmp.Length > 0
|
||||
&& tmp != tab.Mtrl.Textures[ i ].Path )
|
||||
{
|
||||
ret = true;
|
||||
tab.Mtrl.Textures[ i ].Path = tmp;
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
using var font = ImRaii.PushFont( UiBuilder.MonoFont );
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted( tab.TextureLabels[ i ] );
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool DrawBackFaceAndTransparency( MtrlFile file, bool disabled )
|
||||
{
|
||||
const uint transparencyBit = 0x10;
|
||||
const uint backfaceBit = 0x01;
|
||||
|
||||
var ret = false;
|
||||
|
||||
using var dis = ImRaii.Disabled( disabled );
|
||||
|
||||
var tmp = ( file.ShaderPackage.Flags & transparencyBit ) != 0;
|
||||
if( ImGui.Checkbox( "Enable Transparency", ref tmp ) )
|
||||
{
|
||||
file.ShaderPackage.Flags = tmp ? file.ShaderPackage.Flags | transparencyBit : file.ShaderPackage.Flags & ~transparencyBit;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
ImGui.SameLine( 200 * UiHelpers.Scale + ImGui.GetStyle().ItemSpacing.X + ImGui.GetStyle().WindowPadding.X );
|
||||
tmp = ( file.ShaderPackage.Flags & backfaceBit ) != 0;
|
||||
if( ImGui.Checkbox( "Hide Backfaces", ref tmp ) )
|
||||
{
|
||||
file.ShaderPackage.Flags = tmp ? file.ShaderPackage.Flags | backfaceBit : file.ShaderPackage.Flags & ~backfaceBit;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static void DrawOtherMaterialDetails( MtrlFile file, bool _ )
|
||||
{
|
||||
if( !ImGui.CollapsingHeader( "Further Content" ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using( var sets = ImRaii.TreeNode( "UV Sets", ImGuiTreeNodeFlags.DefaultOpen ) )
|
||||
{
|
||||
if( sets )
|
||||
{
|
||||
foreach( var set in file.UvSets )
|
||||
{
|
||||
ImRaii.TreeNode( $"#{set.Index:D2} - {set.Name}", ImGuiTreeNodeFlags.Leaf ).Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( file.AdditionalData.Length <= 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var t = ImRaii.TreeNode( $"Additional Data (Size: {file.AdditionalData.Length})###AdditionalData" );
|
||||
if( t )
|
||||
{
|
||||
ImGuiUtil.TextWrapped( string.Join( ' ', file.AdditionalData.Select( c => $"{c:X2}" ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawMaterialReassignmentTab()
|
||||
{
|
||||
if( _editor!.ModelFiles.Count == 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var tab = ImRaii.TabItem( "Material Reassignment" );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.NewLine();
|
||||
MaterialSuffix.Draw( _editor, ImGuiHelpers.ScaledVector2( 175, 0 ) );
|
||||
|
||||
ImGui.NewLine();
|
||||
using var child = ImRaii.Child( "##mdlFiles", -Vector2.One, true );
|
||||
if( !child )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( "##files", 4, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One );
|
||||
if( !table )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var iconSize = ImGui.GetFrameHeight() * Vector2.One;
|
||||
foreach( var (info, idx) in _editor.ModelFiles.WithIndex() )
|
||||
{
|
||||
using var id = ImRaii.PushId( idx );
|
||||
ImGui.TableNextColumn();
|
||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Save.ToIconString(), iconSize,
|
||||
"Save the changed mdl file.\nUse at own risk!", !info.Changed, true ) )
|
||||
{
|
||||
info.Save();
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Recycle.ToIconString(), iconSize,
|
||||
"Restore current changes to default.", !info.Changed, true ) )
|
||||
{
|
||||
info.Restore();
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted( info.Path.FullName[ ( _mod!.ModPath.FullName.Length + 1 ).. ] );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth( 400 * UiHelpers.Scale );
|
||||
var tmp = info.CurrentMaterials[ 0 ];
|
||||
if( ImGui.InputText( "##0", ref tmp, 64 ) )
|
||||
{
|
||||
info.SetMaterial( tmp, 0 );
|
||||
}
|
||||
|
||||
for( var i = 1; i < info.Count; ++i )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth( 400 * UiHelpers.Scale );
|
||||
tmp = info.CurrentMaterials[ i ];
|
||||
if( ImGui.InputText( $"##{i}", ref tmp, 64 ) )
|
||||
{
|
||||
info.SetMaterial( tmp, i );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,975 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Interop.Structs;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private const string ModelSetIdTooltip =
|
||||
"Model Set ID - You can usually find this as the 'e####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that.";
|
||||
|
||||
private const string PrimaryIdTooltip =
|
||||
"Primary ID - You can usually find this as the 'x####' part of an item path.\nThis should generally not be left <= 1 unless you explicitly want that.";
|
||||
|
||||
private const string ModelSetIdTooltipShort = "Model Set ID";
|
||||
private const string EquipSlotTooltip = "Equip Slot";
|
||||
private const string ModelRaceTooltip = "Model Race";
|
||||
private const string GenderTooltip = "Gender";
|
||||
private const string ObjectTypeTooltip = "Object Type";
|
||||
private const string SecondaryIdTooltip = "Secondary ID";
|
||||
private const string VariantIdTooltip = "Variant ID";
|
||||
private const string EstTypeTooltip = "EST Type";
|
||||
private const string RacialTribeTooltip = "Racial Tribe";
|
||||
private const string ScalingTypeTooltip = "Scaling Type";
|
||||
|
||||
private void DrawMetaTab()
|
||||
{
|
||||
using var tab = ImRaii.TabItem( "Meta Manipulations" );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DrawOptionSelectHeader();
|
||||
|
||||
var setsEqual = !_editor!.Meta.Changes;
|
||||
var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option.";
|
||||
ImGui.NewLine();
|
||||
if( ImGuiUtil.DrawDisabledButton( "Apply Changes", Vector2.Zero, tt, setsEqual ) )
|
||||
{
|
||||
_editor.ApplyManipulations();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
tt = setsEqual ? "No changes staged." : "Revert all currently staged changes.";
|
||||
if( ImGuiUtil.DrawDisabledButton( "Revert Changes", Vector2.Zero, tt, setsEqual ) )
|
||||
{
|
||||
_editor.RevertManipulations();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
AddFromClipboardButton();
|
||||
ImGui.SameLine();
|
||||
SetFromClipboardButton();
|
||||
ImGui.SameLine();
|
||||
CopyToClipboardButton( "Copy all current manipulations to clipboard.", _iconSize, _editor.Meta.Recombine() );
|
||||
ImGui.SameLine();
|
||||
if( ImGui.Button( "Write as TexTools Files" ) )
|
||||
{
|
||||
_mod!.WriteAllTexToolsMeta();
|
||||
}
|
||||
|
||||
using var child = ImRaii.Child( "##meta", -Vector2.One, true );
|
||||
if( !child )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DrawEditHeader( _editor.Meta.Eqp, "Equipment Parameter Edits (EQP)###EQP", 5, EqpRow.Draw, EqpRow.DrawNew );
|
||||
DrawEditHeader( _editor.Meta.Eqdp, "Racial Model Edits (EQDP)###EQDP", 7, EqdpRow.Draw, EqdpRow.DrawNew );
|
||||
DrawEditHeader( _editor.Meta.Imc, "Variant Edits (IMC)###IMC", 10, ImcRow.Draw, ImcRow.DrawNew );
|
||||
DrawEditHeader( _editor.Meta.Est, "Extra Skeleton Parameters (EST)###EST", 7, EstRow.Draw, EstRow.DrawNew );
|
||||
DrawEditHeader( _editor.Meta.Gmp, "Visor/Gimmick Edits (GMP)###GMP", 7, GmpRow.Draw, GmpRow.DrawNew );
|
||||
DrawEditHeader( _editor.Meta.Rsp, "Racial Scaling Edits (RSP)###RSP", 5, RspRow.Draw, RspRow.DrawNew );
|
||||
}
|
||||
|
||||
|
||||
// The headers for the different meta changes all have basically the same structure for different types.
|
||||
private void DrawEditHeader< T >( IReadOnlyCollection< T > items, string label, int numColumns, Action< T, Mod.Editor, Vector2 > draw,
|
||||
Action< Mod.Editor, Vector2 > drawNew )
|
||||
{
|
||||
const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV;
|
||||
if( !ImGui.CollapsingHeader( $"{items.Count} {label}" ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using( var table = ImRaii.Table( label, numColumns, flags ) )
|
||||
{
|
||||
if( table )
|
||||
{
|
||||
drawNew( _editor!, _iconSize );
|
||||
foreach( var (item, index) in items.ToArray().WithIndex() )
|
||||
{
|
||||
using var id = ImRaii.PushId( index );
|
||||
draw( item, _editor!, _iconSize );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.NewLine();
|
||||
}
|
||||
|
||||
private static class EqpRow
|
||||
{
|
||||
private static EqpManipulation _new = new(Eqp.DefaultEntry, EquipSlot.Head, 1);
|
||||
|
||||
private static float IdWidth
|
||||
=> 100 * UiHelpers.Scale;
|
||||
|
||||
public static void DrawNew( Mod.Editor editor, Vector2 iconSize )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
CopyToClipboardButton( "Copy all current EQP manipulations to clipboard.", iconSize,
|
||||
editor.Meta.Eqp.Select( m => ( MetaManipulation )m ) );
|
||||
ImGui.TableNextColumn();
|
||||
var canAdd = editor.Meta.CanAdd( _new );
|
||||
var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
|
||||
var defaultEntry = ExpandedEqpFile.GetDefault( _new.SetId );
|
||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) )
|
||||
{
|
||||
editor.Meta.Add( _new.Copy( defaultEntry ) );
|
||||
}
|
||||
|
||||
// Identifier
|
||||
ImGui.TableNextColumn();
|
||||
if( IdInput( "##eqpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1 ) )
|
||||
{
|
||||
_new = new EqpManipulation( ExpandedEqpFile.GetDefault( setId ), _new.Slot, setId );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( ModelSetIdTooltip );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( Combos.EqpEquipSlot( "##eqpSlot", 100, _new.Slot, out var slot ) )
|
||||
{
|
||||
_new = new EqpManipulation( ExpandedEqpFile.GetDefault( setId ), slot, _new.SetId );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( EquipSlotTooltip );
|
||||
|
||||
// Values
|
||||
using var disabled = ImRaii.Disabled();
|
||||
ImGui.TableNextColumn();
|
||||
using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing,
|
||||
new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) );
|
||||
foreach( var flag in Eqp.EqpAttributes[ _new.Slot ] )
|
||||
{
|
||||
var value = defaultEntry.HasFlag( flag );
|
||||
Checkmark( "##eqp", flag.ToLocalName(), value, value, out _ );
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
ImGui.NewLine();
|
||||
}
|
||||
|
||||
public static void Draw( EqpManipulation meta, Mod.Editor editor, Vector2 iconSize )
|
||||
{
|
||||
DrawMetaButtons( meta, editor, iconSize );
|
||||
|
||||
// Identifier
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
|
||||
ImGui.TextUnformatted( meta.SetId.ToString() );
|
||||
ImGuiUtil.HoverTooltip( ModelSetIdTooltipShort );
|
||||
var defaultEntry = ExpandedEqpFile.GetDefault( meta.SetId );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
|
||||
ImGui.TextUnformatted( meta.Slot.ToName() );
|
||||
ImGuiUtil.HoverTooltip( EquipSlotTooltip );
|
||||
|
||||
// Values
|
||||
ImGui.TableNextColumn();
|
||||
using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing,
|
||||
new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) );
|
||||
var idx = 0;
|
||||
foreach( var flag in Eqp.EqpAttributes[ meta.Slot ] )
|
||||
{
|
||||
using var id = ImRaii.PushId( idx++ );
|
||||
var defaultValue = defaultEntry.HasFlag( flag );
|
||||
var currentValue = meta.Entry.HasFlag( flag );
|
||||
if( Checkmark( "##eqp", flag.ToLocalName(), currentValue, defaultValue, out var value ) )
|
||||
{
|
||||
editor.Meta.Change( meta.Copy( value ? meta.Entry | flag : meta.Entry & ~flag ) );
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
ImGui.NewLine();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class EqdpRow
|
||||
{
|
||||
private static EqdpManipulation _new = new(EqdpEntry.Invalid, EquipSlot.Head, Gender.Male, ModelRace.Midlander, 1);
|
||||
|
||||
private static float IdWidth
|
||||
=> 100 * UiHelpers.Scale;
|
||||
|
||||
public static void DrawNew( Mod.Editor editor, Vector2 iconSize )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
CopyToClipboardButton( "Copy all current EQDP manipulations to clipboard.", iconSize,
|
||||
editor.Meta.Eqdp.Select( m => ( MetaManipulation )m ) );
|
||||
ImGui.TableNextColumn();
|
||||
var raceCode = Names.CombinedRace( _new.Gender, _new.Race );
|
||||
var validRaceCode = CharacterUtility.EqdpIdx( raceCode, false ) >= 0;
|
||||
var canAdd = validRaceCode && editor.Meta.CanAdd( _new );
|
||||
var tt = canAdd ? "Stage this edit." :
|
||||
validRaceCode ? "This entry is already edited." : "This combination of race and gender can not be used.";
|
||||
var defaultEntry = validRaceCode
|
||||
? ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, _new.Race ), _new.Slot.IsAccessory(), _new.SetId )
|
||||
: 0;
|
||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) )
|
||||
{
|
||||
editor.Meta.Add( _new.Copy( defaultEntry ) );
|
||||
}
|
||||
|
||||
// Identifier
|
||||
ImGui.TableNextColumn();
|
||||
if( IdInput( "##eqdpId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1 ) )
|
||||
{
|
||||
var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, _new.Race ), _new.Slot.IsAccessory(), setId );
|
||||
_new = new EqdpManipulation( newDefaultEntry, _new.Slot, _new.Gender, _new.Race, setId );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( ModelSetIdTooltip );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( Combos.Race( "##eqdpRace", _new.Race, out var race ) )
|
||||
{
|
||||
var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, race ), _new.Slot.IsAccessory(), _new.SetId );
|
||||
_new = new EqdpManipulation( newDefaultEntry, _new.Slot, _new.Gender, race, _new.SetId );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( ModelRaceTooltip );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( Combos.Gender( "##eqdpGender", _new.Gender, out var gender ) )
|
||||
{
|
||||
var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( gender, _new.Race ), _new.Slot.IsAccessory(), _new.SetId );
|
||||
_new = new EqdpManipulation( newDefaultEntry, _new.Slot, gender, _new.Race, _new.SetId );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( GenderTooltip );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( Combos.EqdpEquipSlot( "##eqdpSlot", _new.Slot, out var slot ) )
|
||||
{
|
||||
var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, _new.Race ), slot.IsAccessory(), _new.SetId );
|
||||
_new = new EqdpManipulation( newDefaultEntry, slot, _new.Gender, _new.Race, _new.SetId );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( EquipSlotTooltip );
|
||||
|
||||
// Values
|
||||
using var disabled = ImRaii.Disabled();
|
||||
ImGui.TableNextColumn();
|
||||
var (bit1, bit2) = defaultEntry.ToBits( _new.Slot );
|
||||
Checkmark( "Material##eqdpCheck1", string.Empty, bit1, bit1, out _ );
|
||||
ImGui.SameLine();
|
||||
Checkmark( "Model##eqdpCheck2", string.Empty, bit2, bit2, out _ );
|
||||
}
|
||||
|
||||
public static void Draw( EqdpManipulation meta, Mod.Editor editor, Vector2 iconSize )
|
||||
{
|
||||
DrawMetaButtons( meta, editor, iconSize );
|
||||
|
||||
// Identifier
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
|
||||
ImGui.TextUnformatted( meta.SetId.ToString() );
|
||||
ImGuiUtil.HoverTooltip( ModelSetIdTooltipShort );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
|
||||
ImGui.TextUnformatted( meta.Race.ToName() );
|
||||
ImGuiUtil.HoverTooltip( ModelRaceTooltip );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
|
||||
ImGui.TextUnformatted( meta.Gender.ToName() );
|
||||
ImGuiUtil.HoverTooltip( GenderTooltip );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
|
||||
ImGui.TextUnformatted( meta.Slot.ToName() );
|
||||
ImGuiUtil.HoverTooltip( EquipSlotTooltip );
|
||||
|
||||
// Values
|
||||
var defaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( meta.Gender, meta.Race ), meta.Slot.IsAccessory(), meta.SetId );
|
||||
var (defaultBit1, defaultBit2) = defaultEntry.ToBits( meta.Slot );
|
||||
var (bit1, bit2) = meta.Entry.ToBits( meta.Slot );
|
||||
ImGui.TableNextColumn();
|
||||
if( Checkmark( "Material##eqdpCheck1", string.Empty, bit1, defaultBit1, out var newBit1 ) )
|
||||
{
|
||||
editor.Meta.Change( meta.Copy( Eqdp.FromSlotAndBits( meta.Slot, newBit1, bit2 ) ) );
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if( Checkmark( "Model##eqdpCheck2", string.Empty, bit2, defaultBit2, out var newBit2 ) )
|
||||
{
|
||||
editor.Meta.Change( meta.Copy( Eqdp.FromSlotAndBits( meta.Slot, bit1, newBit2 ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class ImcRow
|
||||
{
|
||||
private static ImcManipulation _new = new(EquipSlot.Head, 1, 1, new ImcEntry());
|
||||
|
||||
private static float IdWidth
|
||||
=> 80 * UiHelpers.Scale;
|
||||
|
||||
private static float SmallIdWidth
|
||||
=> 45 * UiHelpers.Scale;
|
||||
|
||||
// Convert throwing to null-return if the file does not exist.
|
||||
private static ImcEntry? GetDefault( ImcManipulation imc )
|
||||
{
|
||||
try
|
||||
{
|
||||
return ImcFile.GetDefault( imc.GamePath(), imc.EquipSlot, imc.Variant, out _ );
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void DrawNew( Mod.Editor editor, Vector2 iconSize )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
CopyToClipboardButton( "Copy all current IMC manipulations to clipboard.", iconSize,
|
||||
editor.Meta.Imc.Select( m => ( MetaManipulation )m ) );
|
||||
ImGui.TableNextColumn();
|
||||
var defaultEntry = GetDefault( _new );
|
||||
var canAdd = defaultEntry != null && editor.Meta.CanAdd( _new );
|
||||
var tt = canAdd ? "Stage this edit." : defaultEntry == null ? "This IMC file does not exist." : "This entry is already edited.";
|
||||
defaultEntry ??= new ImcEntry();
|
||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) )
|
||||
{
|
||||
editor.Meta.Add( _new.Copy( defaultEntry.Value ) );
|
||||
}
|
||||
|
||||
// Identifier
|
||||
ImGui.TableNextColumn();
|
||||
if( Combos.ImcType( "##imcType", _new.ObjectType, out var type ) )
|
||||
{
|
||||
var equipSlot = type switch
|
||||
{
|
||||
ObjectType.Equipment => _new.EquipSlot.IsEquipment() ? _new.EquipSlot : EquipSlot.Head,
|
||||
ObjectType.DemiHuman => _new.EquipSlot.IsEquipment() ? _new.EquipSlot : EquipSlot.Head,
|
||||
ObjectType.Accessory => _new.EquipSlot.IsAccessory() ? _new.EquipSlot : EquipSlot.Ears,
|
||||
_ => EquipSlot.Unknown,
|
||||
};
|
||||
_new = new ImcManipulation( type, _new.BodySlot, _new.PrimaryId, _new.SecondaryId == 0 ? ( ushort )1 : _new.SecondaryId, _new.Variant, equipSlot, _new.Entry );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( ObjectTypeTooltip );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( IdInput( "##imcId", IdWidth, _new.PrimaryId, out var setId, 0, ushort.MaxValue, _new.PrimaryId <= 1 ) )
|
||||
{
|
||||
_new = new ImcManipulation( _new.ObjectType, _new.BodySlot, setId, _new.SecondaryId, _new.Variant, _new.EquipSlot, _new.Entry ).Copy( GetDefault( _new )
|
||||
?? new ImcEntry() );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( PrimaryIdTooltip );
|
||||
|
||||
using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing,
|
||||
new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
// Equipment and accessories are slightly different imcs than other types.
|
||||
if( _new.ObjectType is ObjectType.Equipment )
|
||||
{
|
||||
if( Combos.EqpEquipSlot( "##imcSlot", 100, _new.EquipSlot, out var slot ) )
|
||||
{
|
||||
_new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry ).Copy( GetDefault( _new )
|
||||
?? new ImcEntry() );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( EquipSlotTooltip );
|
||||
}
|
||||
else if( _new.ObjectType is ObjectType.Accessory )
|
||||
{
|
||||
if( Combos.AccessorySlot( "##imcSlot", _new.EquipSlot, out var slot ) )
|
||||
{
|
||||
_new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry ).Copy( GetDefault( _new )
|
||||
?? new ImcEntry() );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( EquipSlotTooltip );
|
||||
}
|
||||
else
|
||||
{
|
||||
if( IdInput( "##imcId2", 100 * UiHelpers.Scale, _new.SecondaryId, out var setId2, 0, ushort.MaxValue, false ) )
|
||||
{
|
||||
_new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, setId2, _new.Variant, _new.EquipSlot, _new.Entry ).Copy( GetDefault( _new )
|
||||
?? new ImcEntry() );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( SecondaryIdTooltip );
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( IdInput( "##imcVariant", SmallIdWidth, _new.Variant, out var variant, 0, byte.MaxValue, false ) )
|
||||
{
|
||||
_new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, variant, _new.EquipSlot, _new.Entry ).Copy( GetDefault( _new )
|
||||
?? new ImcEntry() );
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( _new.ObjectType is ObjectType.DemiHuman )
|
||||
{
|
||||
if( Combos.EqpEquipSlot( "##imcSlot", 70, _new.EquipSlot, out var slot ) )
|
||||
{
|
||||
_new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry ).Copy( GetDefault( _new )
|
||||
?? new ImcEntry() );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( EquipSlotTooltip );
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.Dummy( new Vector2( 70 * UiHelpers.Scale, 0 ) );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( VariantIdTooltip );
|
||||
|
||||
// Values
|
||||
using var disabled = ImRaii.Disabled();
|
||||
ImGui.TableNextColumn();
|
||||
IntDragInput( "##imcMaterialId", "Material ID", SmallIdWidth, defaultEntry.Value.MaterialId, defaultEntry.Value.MaterialId, out _,
|
||||
1, byte.MaxValue, 0f );
|
||||
ImGui.SameLine();
|
||||
IntDragInput( "##imcMaterialAnimId", "Material Animation ID", SmallIdWidth, defaultEntry.Value.MaterialAnimationId,
|
||||
defaultEntry.Value.MaterialAnimationId, out _, 0, byte.MaxValue, 0.01f );
|
||||
ImGui.TableNextColumn();
|
||||
IntDragInput( "##imcDecalId", "Decal ID", SmallIdWidth, defaultEntry.Value.DecalId, defaultEntry.Value.DecalId, out _, 0,
|
||||
byte.MaxValue, 0f );
|
||||
ImGui.SameLine();
|
||||
IntDragInput( "##imcVfxId", "VFX ID", SmallIdWidth, defaultEntry.Value.VfxId, defaultEntry.Value.VfxId, out _, 0, byte.MaxValue,
|
||||
0f );
|
||||
ImGui.SameLine();
|
||||
IntDragInput( "##imcSoundId", "Sound ID", SmallIdWidth, defaultEntry.Value.SoundId, defaultEntry.Value.SoundId, out _, 0, 0b111111,
|
||||
0f );
|
||||
ImGui.TableNextColumn();
|
||||
for( var i = 0; i < 10; ++i )
|
||||
{
|
||||
using var id = ImRaii.PushId( i );
|
||||
var flag = 1 << i;
|
||||
Checkmark( "##attribute", $"{( char )( 'A' + i )}", ( defaultEntry.Value.AttributeMask & flag ) != 0,
|
||||
( defaultEntry.Value.AttributeMask & flag ) != 0, out _ );
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
ImGui.NewLine();
|
||||
}
|
||||
|
||||
public static void Draw( ImcManipulation meta, Mod.Editor editor, Vector2 iconSize )
|
||||
{
|
||||
DrawMetaButtons( meta, editor, iconSize );
|
||||
|
||||
// Identifier
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
|
||||
ImGui.TextUnformatted( meta.ObjectType.ToName() );
|
||||
ImGuiUtil.HoverTooltip( ObjectTypeTooltip );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
|
||||
ImGui.TextUnformatted( meta.PrimaryId.ToString() );
|
||||
ImGuiUtil.HoverTooltip( "Primary ID" );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
|
||||
if( meta.ObjectType is ObjectType.Equipment or ObjectType.Accessory )
|
||||
{
|
||||
ImGui.TextUnformatted( meta.EquipSlot.ToName() );
|
||||
ImGuiUtil.HoverTooltip( EquipSlotTooltip );
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted( meta.SecondaryId.ToString() );
|
||||
ImGuiUtil.HoverTooltip( SecondaryIdTooltip );
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
|
||||
ImGui.TextUnformatted( meta.Variant.ToString() );
|
||||
ImGuiUtil.HoverTooltip( VariantIdTooltip );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
|
||||
if( meta.ObjectType is ObjectType.DemiHuman )
|
||||
{
|
||||
ImGui.TextUnformatted( meta.EquipSlot.ToName() );
|
||||
}
|
||||
|
||||
// Values
|
||||
using var style = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing,
|
||||
new Vector2( 3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y ) );
|
||||
ImGui.TableNextColumn();
|
||||
var defaultEntry = GetDefault( meta ) ?? new ImcEntry();
|
||||
if( IntDragInput( "##imcMaterialId", $"Material ID\nDefault Value: {defaultEntry.MaterialId}", SmallIdWidth, meta.Entry.MaterialId,
|
||||
defaultEntry.MaterialId, out var materialId, 1, byte.MaxValue, 0.01f ) )
|
||||
{
|
||||
editor.Meta.Change( meta.Copy( meta.Entry with { MaterialId = ( byte )materialId } ) );
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if( IntDragInput( "##imcMaterialAnimId", $"Material Animation ID\nDefault Value: {defaultEntry.MaterialAnimationId}", SmallIdWidth,
|
||||
meta.Entry.MaterialAnimationId, defaultEntry.MaterialAnimationId, out var materialAnimId, 0, byte.MaxValue, 0.01f ) )
|
||||
{
|
||||
editor.Meta.Change( meta.Copy( meta.Entry with { MaterialAnimationId = ( byte )materialAnimId } ) );
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( IntDragInput( "##imcDecalId", $"Decal ID\nDefault Value: {defaultEntry.DecalId}", SmallIdWidth, meta.Entry.DecalId,
|
||||
defaultEntry.DecalId, out var decalId, 0, byte.MaxValue, 0.01f ) )
|
||||
{
|
||||
editor.Meta.Change( meta.Copy( meta.Entry with { DecalId = ( byte )decalId } ) );
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if( IntDragInput( "##imcVfxId", $"VFX ID\nDefault Value: {defaultEntry.VfxId}", SmallIdWidth, meta.Entry.VfxId, defaultEntry.VfxId,
|
||||
out var vfxId, 0, byte.MaxValue, 0.01f ) )
|
||||
{
|
||||
editor.Meta.Change( meta.Copy( meta.Entry with { VfxId = ( byte )vfxId } ) );
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if( IntDragInput( "##imcSoundId", $"Sound ID\nDefault Value: {defaultEntry.SoundId}", SmallIdWidth, meta.Entry.SoundId,
|
||||
defaultEntry.SoundId, out var soundId, 0, 0b111111, 0.01f ) )
|
||||
{
|
||||
editor.Meta.Change( meta.Copy( meta.Entry with { SoundId = ( byte )soundId } ) );
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
for( var i = 0; i < 10; ++i )
|
||||
{
|
||||
using var id = ImRaii.PushId( i );
|
||||
var flag = 1 << i;
|
||||
if( Checkmark( "##attribute", $"{( char )( 'A' + i )}", ( meta.Entry.AttributeMask & flag ) != 0,
|
||||
( defaultEntry.AttributeMask & flag ) != 0, out var val ) )
|
||||
{
|
||||
var attributes = val ? meta.Entry.AttributeMask | flag : meta.Entry.AttributeMask & ~flag;
|
||||
editor.Meta.Change( meta.Copy( meta.Entry with { AttributeMask = ( ushort )attributes } ) );
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
}
|
||||
|
||||
ImGui.NewLine();
|
||||
}
|
||||
}
|
||||
|
||||
private static class EstRow
|
||||
{
|
||||
private static EstManipulation _new = new(Gender.Male, ModelRace.Midlander, EstManipulation.EstType.Body, 1, 0);
|
||||
|
||||
private static float IdWidth
|
||||
=> 100 * UiHelpers.Scale;
|
||||
|
||||
public static void DrawNew( Mod.Editor editor, Vector2 iconSize )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
CopyToClipboardButton( "Copy all current EST manipulations to clipboard.", iconSize,
|
||||
editor.Meta.Est.Select( m => ( MetaManipulation )m ) );
|
||||
ImGui.TableNextColumn();
|
||||
var canAdd = editor.Meta.CanAdd( _new );
|
||||
var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
|
||||
var defaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( _new.Gender, _new.Race ), _new.SetId );
|
||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) )
|
||||
{
|
||||
editor.Meta.Add( _new.Copy( defaultEntry ) );
|
||||
}
|
||||
|
||||
// Identifier
|
||||
ImGui.TableNextColumn();
|
||||
if( IdInput( "##estId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1 ) )
|
||||
{
|
||||
var newDefaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( _new.Gender, _new.Race ), setId );
|
||||
_new = new EstManipulation( _new.Gender, _new.Race, _new.Slot, setId, newDefaultEntry );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( ModelSetIdTooltip );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( Combos.Race( "##estRace", _new.Race, out var race ) )
|
||||
{
|
||||
var newDefaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( _new.Gender, race ), _new.SetId );
|
||||
_new = new EstManipulation( _new.Gender, race, _new.Slot, _new.SetId, newDefaultEntry );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( ModelRaceTooltip );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( Combos.Gender( "##estGender", _new.Gender, out var gender ) )
|
||||
{
|
||||
var newDefaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( gender, _new.Race ), _new.SetId );
|
||||
_new = new EstManipulation( gender, _new.Race, _new.Slot, _new.SetId, newDefaultEntry );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( GenderTooltip );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( Combos.EstSlot( "##estSlot", _new.Slot, out var slot ) )
|
||||
{
|
||||
var newDefaultEntry = EstFile.GetDefault( slot, Names.CombinedRace( _new.Gender, _new.Race ), _new.SetId );
|
||||
_new = new EstManipulation( _new.Gender, _new.Race, slot, _new.SetId, newDefaultEntry );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( EstTypeTooltip );
|
||||
|
||||
// Values
|
||||
using var disabled = ImRaii.Disabled();
|
||||
ImGui.TableNextColumn();
|
||||
IntDragInput( "##estSkeleton", "Skeleton Index", IdWidth, _new.Entry, defaultEntry, out _, 0, ushort.MaxValue, 0.05f );
|
||||
}
|
||||
|
||||
public static void Draw( EstManipulation meta, Mod.Editor editor, Vector2 iconSize )
|
||||
{
|
||||
DrawMetaButtons( meta, editor, iconSize );
|
||||
|
||||
// Identifier
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
|
||||
ImGui.TextUnformatted( meta.SetId.ToString() );
|
||||
ImGuiUtil.HoverTooltip( ModelSetIdTooltipShort );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
|
||||
ImGui.TextUnformatted( meta.Race.ToName() );
|
||||
ImGuiUtil.HoverTooltip( ModelRaceTooltip );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
|
||||
ImGui.TextUnformatted( meta.Gender.ToName() );
|
||||
ImGuiUtil.HoverTooltip( GenderTooltip );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
|
||||
ImGui.TextUnformatted( meta.Slot.ToString() );
|
||||
ImGuiUtil.HoverTooltip( EstTypeTooltip );
|
||||
|
||||
// Values
|
||||
var defaultEntry = EstFile.GetDefault( meta.Slot, Names.CombinedRace( meta.Gender, meta.Race ), meta.SetId );
|
||||
ImGui.TableNextColumn();
|
||||
if( IntDragInput( "##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry, defaultEntry,
|
||||
out var entry, 0, ushort.MaxValue, 0.05f ) )
|
||||
{
|
||||
editor.Meta.Change( meta.Copy( ( ushort )entry ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class GmpRow
|
||||
{
|
||||
private static GmpManipulation _new = new(GmpEntry.Default, 1);
|
||||
|
||||
private static float RotationWidth
|
||||
=> 75 * UiHelpers.Scale;
|
||||
|
||||
private static float UnkWidth
|
||||
=> 50 * UiHelpers.Scale;
|
||||
|
||||
private static float IdWidth
|
||||
=> 100 * UiHelpers.Scale;
|
||||
|
||||
public static void DrawNew( Mod.Editor editor, Vector2 iconSize )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
CopyToClipboardButton( "Copy all current GMP manipulations to clipboard.", iconSize,
|
||||
editor.Meta.Gmp.Select( m => ( MetaManipulation )m ) );
|
||||
ImGui.TableNextColumn();
|
||||
var canAdd = editor.Meta.CanAdd( _new );
|
||||
var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
|
||||
var defaultEntry = ExpandedGmpFile.GetDefault( _new.SetId );
|
||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) )
|
||||
{
|
||||
editor.Meta.Add( _new.Copy( defaultEntry ) );
|
||||
}
|
||||
|
||||
// Identifier
|
||||
ImGui.TableNextColumn();
|
||||
if( IdInput( "##gmpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1 ) )
|
||||
{
|
||||
_new = new GmpManipulation( ExpandedGmpFile.GetDefault( setId ), setId );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( ModelSetIdTooltip );
|
||||
|
||||
// Values
|
||||
using var disabled = ImRaii.Disabled();
|
||||
ImGui.TableNextColumn();
|
||||
Checkmark( "##gmpEnabled", "Gimmick Enabled", defaultEntry.Enabled, defaultEntry.Enabled, out _ );
|
||||
ImGui.TableNextColumn();
|
||||
Checkmark( "##gmpAnimated", "Gimmick Animated", defaultEntry.Animated, defaultEntry.Animated, out _ );
|
||||
ImGui.TableNextColumn();
|
||||
IntDragInput( "##gmpRotationA", "Rotation A in Degrees", RotationWidth, defaultEntry.RotationA, defaultEntry.RotationA, out _, 0,
|
||||
360, 0f );
|
||||
ImGui.SameLine();
|
||||
IntDragInput( "##gmpRotationB", "Rotation B in Degrees", RotationWidth, defaultEntry.RotationB, defaultEntry.RotationB, out _, 0,
|
||||
360, 0f );
|
||||
ImGui.SameLine();
|
||||
IntDragInput( "##gmpRotationC", "Rotation C in Degrees", RotationWidth, defaultEntry.RotationC, defaultEntry.RotationC, out _, 0,
|
||||
360, 0f );
|
||||
ImGui.TableNextColumn();
|
||||
IntDragInput( "##gmpUnkA", "Animation Type A?", UnkWidth, defaultEntry.UnknownA, defaultEntry.UnknownA, out _, 0, 15, 0f );
|
||||
ImGui.SameLine();
|
||||
IntDragInput( "##gmpUnkB", "Animation Type B?", UnkWidth, defaultEntry.UnknownB, defaultEntry.UnknownB, out _, 0, 15, 0f );
|
||||
}
|
||||
|
||||
public static void Draw( GmpManipulation meta, Mod.Editor editor, Vector2 iconSize )
|
||||
{
|
||||
DrawMetaButtons( meta, editor, iconSize );
|
||||
|
||||
// Identifier
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
|
||||
ImGui.TextUnformatted( meta.SetId.ToString() );
|
||||
ImGuiUtil.HoverTooltip( ModelSetIdTooltipShort );
|
||||
|
||||
// Values
|
||||
var defaultEntry = ExpandedGmpFile.GetDefault( meta.SetId );
|
||||
ImGui.TableNextColumn();
|
||||
if( Checkmark( "##gmpEnabled", "Gimmick Enabled", meta.Entry.Enabled, defaultEntry.Enabled, out var enabled ) )
|
||||
{
|
||||
editor.Meta.Change( meta.Copy( meta.Entry with { Enabled = enabled } ) );
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( Checkmark( "##gmpAnimated", "Gimmick Animated", meta.Entry.Animated, defaultEntry.Animated, out var animated ) )
|
||||
{
|
||||
editor.Meta.Change( meta.Copy( meta.Entry with { Animated = animated } ) );
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( IntDragInput( "##gmpRotationA", $"Rotation A in Degrees\nDefault Value: {defaultEntry.RotationA}", RotationWidth,
|
||||
meta.Entry.RotationA, defaultEntry.RotationA, out var rotationA, 0, 360, 0.05f ) )
|
||||
{
|
||||
editor.Meta.Change( meta.Copy( meta.Entry with { RotationA = ( ushort )rotationA } ) );
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if( IntDragInput( "##gmpRotationB", $"Rotation B in Degrees\nDefault Value: {defaultEntry.RotationB}", RotationWidth,
|
||||
meta.Entry.RotationB, defaultEntry.RotationB, out var rotationB, 0, 360, 0.05f ) )
|
||||
{
|
||||
editor.Meta.Change( meta.Copy( meta.Entry with { RotationB = ( ushort )rotationB } ) );
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if( IntDragInput( "##gmpRotationC", $"Rotation C in Degrees\nDefault Value: {defaultEntry.RotationC}", RotationWidth,
|
||||
meta.Entry.RotationC, defaultEntry.RotationC, out var rotationC, 0, 360, 0.05f ) )
|
||||
{
|
||||
editor.Meta.Change( meta.Copy( meta.Entry with { RotationC = ( ushort )rotationC } ) );
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( IntDragInput( "##gmpUnkA", $"Animation Type A?\nDefault Value: {defaultEntry.UnknownA}", UnkWidth, meta.Entry.UnknownA,
|
||||
defaultEntry.UnknownA, out var unkA, 0, 15, 0.01f ) )
|
||||
{
|
||||
editor.Meta.Change( meta.Copy( meta.Entry with { UnknownA = ( byte )unkA } ) );
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if( IntDragInput( "##gmpUnkB", $"Animation Type B?\nDefault Value: {defaultEntry.UnknownB}", UnkWidth, meta.Entry.UnknownB,
|
||||
defaultEntry.UnknownB, out var unkB, 0, 15, 0.01f ) )
|
||||
{
|
||||
editor.Meta.Change( meta.Copy( meta.Entry with { UnknownA = ( byte )unkB } ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class RspRow
|
||||
{
|
||||
private static RspManipulation _new = new(SubRace.Midlander, RspAttribute.MaleMinSize, 1f);
|
||||
|
||||
private static float FloatWidth
|
||||
=> 150 * UiHelpers.Scale;
|
||||
|
||||
public static void DrawNew( Mod.Editor editor, Vector2 iconSize )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
CopyToClipboardButton( "Copy all current RSP manipulations to clipboard.", iconSize,
|
||||
editor.Meta.Rsp.Select( m => ( MetaManipulation )m ) );
|
||||
ImGui.TableNextColumn();
|
||||
var canAdd = editor.Meta.CanAdd( _new );
|
||||
var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
|
||||
var defaultEntry = CmpFile.GetDefault( _new.SubRace, _new.Attribute );
|
||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true ) )
|
||||
{
|
||||
editor.Meta.Add( _new.Copy( defaultEntry ) );
|
||||
}
|
||||
|
||||
// Identifier
|
||||
ImGui.TableNextColumn();
|
||||
if( Combos.SubRace( "##rspSubRace", _new.SubRace, out var subRace ) )
|
||||
{
|
||||
_new = new RspManipulation( subRace, _new.Attribute, CmpFile.GetDefault( subRace, _new.Attribute ) );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( RacialTribeTooltip );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( Combos.RspAttribute( "##rspAttribute", _new.Attribute, out var attribute ) )
|
||||
{
|
||||
_new = new RspManipulation( _new.SubRace, attribute, CmpFile.GetDefault( subRace, attribute ) );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( ScalingTypeTooltip );
|
||||
|
||||
// Values
|
||||
using var disabled = ImRaii.Disabled();
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth( FloatWidth );
|
||||
ImGui.DragFloat( "##rspValue", ref defaultEntry, 0f );
|
||||
}
|
||||
|
||||
public static void Draw( RspManipulation meta, Mod.Editor editor, Vector2 iconSize )
|
||||
{
|
||||
DrawMetaButtons( meta, editor, iconSize );
|
||||
|
||||
// Identifier
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
|
||||
ImGui.TextUnformatted( meta.SubRace.ToName() );
|
||||
ImGuiUtil.HoverTooltip( RacialTribeTooltip );
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetCursorPosX( ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X );
|
||||
ImGui.TextUnformatted( meta.Attribute.ToFullString() );
|
||||
ImGuiUtil.HoverTooltip( ScalingTypeTooltip );
|
||||
ImGui.TableNextColumn();
|
||||
|
||||
// Values
|
||||
var def = CmpFile.GetDefault( meta.SubRace, meta.Attribute );
|
||||
var value = meta.Entry;
|
||||
ImGui.SetNextItemWidth( FloatWidth );
|
||||
using var color = ImRaii.PushColor( ImGuiCol.FrameBg,
|
||||
def < value ? ColorId.IncreasedMetaValue.Value(Penumbra.Config) : ColorId.DecreasedMetaValue.Value(Penumbra.Config),
|
||||
def != value );
|
||||
if( ImGui.DragFloat( "##rspValue", ref value, 0.001f, 0.01f, 8f ) && value is >= 0.01f and <= 8f )
|
||||
{
|
||||
editor.Meta.Change( meta.Copy( value ) );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( $"Default Value: {def:0.###}" );
|
||||
}
|
||||
}
|
||||
|
||||
// A number input for ids with a optional max id of given width.
|
||||
// Returns true if newId changed against currentId.
|
||||
private static bool IdInput( string label, float width, ushort currentId, out ushort newId, int minId, int maxId, bool border )
|
||||
{
|
||||
int tmp = currentId;
|
||||
ImGui.SetNextItemWidth( width );
|
||||
using var style = ImRaii.PushStyle( ImGuiStyleVar.FrameBorderSize, UiHelpers.Scale, border );
|
||||
using var color = ImRaii.PushColor( ImGuiCol.Border, Colors.RegexWarningBorder, border );
|
||||
if( ImGui.InputInt( label, ref tmp, 0 ) )
|
||||
{
|
||||
tmp = Math.Clamp( tmp, minId, maxId );
|
||||
}
|
||||
|
||||
newId = ( ushort )tmp;
|
||||
return newId != currentId;
|
||||
}
|
||||
|
||||
// A checkmark that compares against a default value and shows a tooltip.
|
||||
// Returns true if newValue is changed against currentValue.
|
||||
private static bool Checkmark( string label, string tooltip, bool currentValue, bool defaultValue, out bool newValue )
|
||||
{
|
||||
using var color = ImRaii.PushColor( ImGuiCol.FrameBg,
|
||||
defaultValue ? ColorId.DecreasedMetaValue.Value(Penumbra.Config) : ColorId.IncreasedMetaValue.Value(Penumbra.Config), defaultValue != currentValue );
|
||||
newValue = currentValue;
|
||||
ImGui.Checkbox( label, ref newValue );
|
||||
ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled );
|
||||
return newValue != currentValue;
|
||||
}
|
||||
|
||||
// A dragging int input of given width that compares against a default value, shows a tooltip and clamps against min and max.
|
||||
// Returns true if newValue changed against currentValue.
|
||||
private static bool IntDragInput( string label, string tooltip, float width, int currentValue, int defaultValue, out int newValue,
|
||||
int minValue, int maxValue, float speed )
|
||||
{
|
||||
newValue = currentValue;
|
||||
using var color = ImRaii.PushColor( ImGuiCol.FrameBg,
|
||||
defaultValue > currentValue ? ColorId.DecreasedMetaValue.Value(Penumbra.Config) : ColorId.IncreasedMetaValue.Value(Penumbra.Config),
|
||||
defaultValue != currentValue );
|
||||
ImGui.SetNextItemWidth( width );
|
||||
if( ImGui.DragInt( label, ref newValue, speed, minValue, maxValue ) )
|
||||
{
|
||||
newValue = Math.Clamp( newValue, minValue, maxValue );
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( tooltip, ImGuiHoveredFlags.AllowWhenDisabled );
|
||||
|
||||
return newValue != currentValue;
|
||||
}
|
||||
|
||||
private static void CopyToClipboardButton( string tooltip, Vector2 iconSize, IEnumerable< MetaManipulation > manipulations )
|
||||
{
|
||||
if( !ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Clipboard.ToIconString(), iconSize, tooltip, false, true ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var text = Functions.ToCompressedBase64( manipulations, MetaManipulation.CurrentVersion );
|
||||
if( text.Length > 0 )
|
||||
{
|
||||
ImGui.SetClipboardText( text );
|
||||
}
|
||||
}
|
||||
|
||||
private void AddFromClipboardButton()
|
||||
{
|
||||
if( ImGui.Button( "Add from Clipboard" ) )
|
||||
{
|
||||
var clipboard = ImGuiUtil.GetClipboardText();
|
||||
|
||||
var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips );
|
||||
if( version == MetaManipulation.CurrentVersion && manips != null )
|
||||
{
|
||||
foreach( var manip in manips.Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) )
|
||||
{
|
||||
_editor!.Meta.Set( manip );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip(
|
||||
"Try to add meta manipulations currently stored in the clipboard to the current manipulations.\nOverwrites already existing manipulations." );
|
||||
}
|
||||
|
||||
private void SetFromClipboardButton()
|
||||
{
|
||||
if( ImGui.Button( "Set from Clipboard" ) )
|
||||
{
|
||||
var clipboard = ImGuiUtil.GetClipboardText();
|
||||
var version = Functions.FromCompressedBase64< MetaManipulation[] >( clipboard, out var manips );
|
||||
if( version == MetaManipulation.CurrentVersion && manips != null )
|
||||
{
|
||||
_editor!.Meta.Clear();
|
||||
foreach( var manip in manips.Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) )
|
||||
{
|
||||
_editor!.Meta.Set( manip );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip(
|
||||
"Try to set the current meta manipulations to the set currently stored in the clipboard.\nRemoves all other manipulations." );
|
||||
}
|
||||
|
||||
private static void DrawMetaButtons( MetaManipulation meta, Mod.Editor editor, Vector2 iconSize )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
CopyToClipboardButton( "Copy this manipulation to clipboard.", iconSize, Array.Empty< MetaManipulation >().Append( meta ) );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( ImGuiUtil.DrawDisabledButton( FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this meta manipulation.", false, true ) )
|
||||
{
|
||||
editor.Meta.Delete( meta );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,134 +0,0 @@
|
|||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.String.Classes;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private readonly FileEditor< MdlFile > _modelTab;
|
||||
|
||||
private static bool DrawModelPanel( MdlFile file, bool disabled )
|
||||
{
|
||||
var ret = false;
|
||||
for( var i = 0; i < file.Materials.Length; ++i )
|
||||
{
|
||||
using var id = ImRaii.PushId( i );
|
||||
var tmp = file.Materials[ i ];
|
||||
if( ImGui.InputText( string.Empty, ref tmp, Utf8GamePath.MaxGamePathLength,
|
||||
disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None )
|
||||
&& tmp.Length > 0
|
||||
&& tmp != file.Materials[ i ] )
|
||||
{
|
||||
file.Materials[ i ] = tmp;
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
|
||||
ret |= DrawOtherModelDetails( file, disabled );
|
||||
|
||||
return !disabled && ret;
|
||||
}
|
||||
|
||||
private static bool DrawOtherModelDetails( MdlFile file, bool _ )
|
||||
{
|
||||
if( !ImGui.CollapsingHeader( "Further Content" ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using( var table = ImRaii.Table( "##data", 2, ImGuiTableFlags.SizingFixedFit ) )
|
||||
{
|
||||
if( table )
|
||||
{
|
||||
ImGuiUtil.DrawTableColumn( "Version" );
|
||||
ImGuiUtil.DrawTableColumn( file.Version.ToString() );
|
||||
ImGuiUtil.DrawTableColumn( "Radius" );
|
||||
ImGuiUtil.DrawTableColumn( file.Radius.ToString( CultureInfo.InvariantCulture ) );
|
||||
ImGuiUtil.DrawTableColumn( "Model Clip Out Distance" );
|
||||
ImGuiUtil.DrawTableColumn( file.ModelClipOutDistance.ToString( CultureInfo.InvariantCulture ) );
|
||||
ImGuiUtil.DrawTableColumn( "Shadow Clip Out Distance" );
|
||||
ImGuiUtil.DrawTableColumn( file.ShadowClipOutDistance.ToString( CultureInfo.InvariantCulture ) );
|
||||
ImGuiUtil.DrawTableColumn( "LOD Count" );
|
||||
ImGuiUtil.DrawTableColumn( file.LodCount.ToString() );
|
||||
ImGuiUtil.DrawTableColumn( "Enable Index Buffer Streaming" );
|
||||
ImGuiUtil.DrawTableColumn( file.EnableIndexBufferStreaming.ToString() );
|
||||
ImGuiUtil.DrawTableColumn( "Enable Edge Geometry" );
|
||||
ImGuiUtil.DrawTableColumn( file.EnableEdgeGeometry.ToString() );
|
||||
ImGuiUtil.DrawTableColumn( "Flags 1" );
|
||||
ImGuiUtil.DrawTableColumn( file.Flags1.ToString() );
|
||||
ImGuiUtil.DrawTableColumn( "Flags 2" );
|
||||
ImGuiUtil.DrawTableColumn( file.Flags2.ToString() );
|
||||
ImGuiUtil.DrawTableColumn( "Vertex Declarations" );
|
||||
ImGuiUtil.DrawTableColumn( file.VertexDeclarations.Length.ToString() );
|
||||
ImGuiUtil.DrawTableColumn( "Bone Bounding Boxes" );
|
||||
ImGuiUtil.DrawTableColumn( file.BoneBoundingBoxes.Length.ToString() );
|
||||
ImGuiUtil.DrawTableColumn( "Bone Tables" );
|
||||
ImGuiUtil.DrawTableColumn( file.BoneTables.Length.ToString() );
|
||||
ImGuiUtil.DrawTableColumn( "Element IDs" );
|
||||
ImGuiUtil.DrawTableColumn( file.ElementIds.Length.ToString() );
|
||||
ImGuiUtil.DrawTableColumn( "Extra LoDs" );
|
||||
ImGuiUtil.DrawTableColumn( file.ExtraLods.Length.ToString() );
|
||||
ImGuiUtil.DrawTableColumn( "Meshes" );
|
||||
ImGuiUtil.DrawTableColumn( file.Meshes.Length.ToString() );
|
||||
ImGuiUtil.DrawTableColumn( "Shape Meshes" );
|
||||
ImGuiUtil.DrawTableColumn( file.ShapeMeshes.Length.ToString() );
|
||||
ImGuiUtil.DrawTableColumn( "LoDs" );
|
||||
ImGuiUtil.DrawTableColumn( file.Lods.Length.ToString() );
|
||||
ImGuiUtil.DrawTableColumn( "Vertex Declarations" );
|
||||
ImGuiUtil.DrawTableColumn( file.VertexDeclarations.Length.ToString() );
|
||||
ImGuiUtil.DrawTableColumn( "Stack Size" );
|
||||
ImGuiUtil.DrawTableColumn( file.StackSize.ToString() );
|
||||
}
|
||||
}
|
||||
|
||||
using( var attributes = ImRaii.TreeNode( "Attributes", ImGuiTreeNodeFlags.DefaultOpen ) )
|
||||
{
|
||||
if( attributes )
|
||||
{
|
||||
foreach( var attribute in file.Attributes )
|
||||
{
|
||||
ImRaii.TreeNode( attribute, ImGuiTreeNodeFlags.Leaf ).Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using( var bones = ImRaii.TreeNode( "Bones", ImGuiTreeNodeFlags.DefaultOpen ) )
|
||||
{
|
||||
if( bones )
|
||||
{
|
||||
foreach( var bone in file.Bones )
|
||||
{
|
||||
ImRaii.TreeNode( bone, ImGuiTreeNodeFlags.Leaf ).Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using( var shapes = ImRaii.TreeNode( "Shapes", ImGuiTreeNodeFlags.DefaultOpen ) )
|
||||
{
|
||||
if( shapes )
|
||||
{
|
||||
foreach( var shape in file.Shapes )
|
||||
{
|
||||
ImRaii.TreeNode( shape.ShapeName, ImGuiTreeNodeFlags.Leaf ).Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( file.RemainingData.Length > 0 )
|
||||
{
|
||||
using var t = ImRaii.TreeNode( $"Additional Data (Size: {file.RemainingData.Length})###AdditionalData" );
|
||||
if( t )
|
||||
{
|
||||
ImGuiUtil.TextWrapped( string.Join( ' ', file.RemainingData.Select( c => $"{c:X2}" ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,666 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using Dalamud.Interface.Internal.Notifications;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using Lumina.Misc;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.String;
|
||||
using Penumbra.Util;
|
||||
using static Penumbra.GameData.Files.ShpkFile;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private static readonly ByteString DisassemblyLabel = ByteString.FromSpanUnsafe( "##disassembly"u8, true, true, true );
|
||||
|
||||
private readonly FileEditor< ShpkTab > _shaderPackageTab;
|
||||
|
||||
private static bool DrawShaderPackagePanel( ShpkTab file, bool disabled )
|
||||
{
|
||||
DrawShaderPackageSummary( file );
|
||||
|
||||
var ret = false;
|
||||
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
|
||||
ret |= DrawShaderPackageShaderArray( file, "Vertex Shader", file.Shpk.VertexShaders, disabled );
|
||||
|
||||
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
|
||||
ret |= DrawShaderPackageShaderArray( file, "Pixel Shader", file.Shpk.PixelShaders, disabled );
|
||||
|
||||
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
|
||||
ret |= DrawShaderPackageMaterialParamLayout( file, disabled );
|
||||
|
||||
ImGui.Dummy( new Vector2( ImGui.GetTextLineHeight() / 2 ) );
|
||||
ret |= DrawOtherShaderPackageDetails( file, disabled );
|
||||
|
||||
file.FileDialog.Draw();
|
||||
|
||||
ret |= file.Shpk.IsChanged();
|
||||
|
||||
return !disabled && ret;
|
||||
}
|
||||
|
||||
private static void DrawShaderPackageSummary( ShpkTab tab )
|
||||
=> ImGui.TextUnformatted( tab.Header );
|
||||
|
||||
private static void DrawShaderExportButton( ShpkTab tab, string objectName, Shader shader, int idx )
|
||||
{
|
||||
if( !ImGui.Button( $"Export Shader Program Blob ({shader.Blob.Length} bytes)" ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var defaultName = objectName[ 0 ] switch
|
||||
{
|
||||
'V' => $"vs{idx}",
|
||||
'P' => $"ps{idx}",
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
|
||||
var blob = shader.Blob;
|
||||
tab.FileDialog.OpenSavePicker( $"Export {objectName} #{idx} Program Blob to...", tab.Extension, defaultName, tab.Extension, ( success, name ) =>
|
||||
{
|
||||
if( !success )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllBytes( name, blob );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Penumbra.ChatService.NotificationMessage( $"Could not export {defaultName}{tab.Extension} to {name}:\n{e.Message}", "Penumbra Advanced Editing",
|
||||
NotificationType.Error );
|
||||
return;
|
||||
}
|
||||
|
||||
Penumbra.ChatService.NotificationMessage( $"Shader Program Blob {defaultName}{tab.Extension} exported successfully to {Path.GetFileName( name )}",
|
||||
"Penumbra Advanced Editing", NotificationType.Success );
|
||||
}, null, false );
|
||||
}
|
||||
|
||||
private static void DrawShaderImportButton( ShpkTab tab, string objectName, Shader[] shaders, int idx )
|
||||
{
|
||||
if( !ImGui.Button( "Replace Shader Program Blob" ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tab.FileDialog.OpenFilePicker( $"Replace {objectName} #{idx} Program Blob...", "Shader Program Blobs{.o,.cso,.dxbc,.dxil}", ( success, name ) =>
|
||||
{
|
||||
if( !success )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
shaders[ idx ].Blob = File.ReadAllBytes(name[0] );
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Penumbra.ChatService.NotificationMessage( $"Could not import {name}:\n{e.Message}", "Penumbra Advanced Editing", NotificationType.Error );
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
shaders[ idx ].UpdateResources( tab.Shpk );
|
||||
tab.Shpk.UpdateResources();
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
tab.Shpk.SetInvalid();
|
||||
Penumbra.ChatService.NotificationMessage( $"Failed to update resources after importing {name}:\n{e.Message}", "Penumbra Advanced Editing",
|
||||
NotificationType.Error );
|
||||
return;
|
||||
}
|
||||
|
||||
tab.Shpk.SetChanged();
|
||||
}, 1, null, false );
|
||||
}
|
||||
|
||||
private static unsafe void DrawRawDisassembly( Shader shader )
|
||||
{
|
||||
using var t2 = ImRaii.TreeNode( "Raw Program Disassembly" );
|
||||
if( !t2 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var font = ImRaii.PushFont( UiBuilder.MonoFont );
|
||||
var size = new Vector2( ImGui.GetContentRegionAvail().X, ImGui.GetTextLineHeight() * 20 );
|
||||
ImGuiNative.igInputTextMultiline( DisassemblyLabel.Path, shader.Disassembly!.RawDisassembly.Path, ( uint )shader.Disassembly!.RawDisassembly.Length + 1, size,
|
||||
ImGuiInputTextFlags.ReadOnly, null, null );
|
||||
}
|
||||
|
||||
private static bool DrawShaderPackageShaderArray( ShpkTab tab, string objectName, Shader[] shaders, bool disabled )
|
||||
{
|
||||
if( shaders.Length == 0 || !ImGui.CollapsingHeader( $"{objectName}s" ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ret = false;
|
||||
for( var idx = 0; idx < shaders.Length; ++idx )
|
||||
{
|
||||
var shader = shaders[ idx ];
|
||||
using var t = ImRaii.TreeNode( $"{objectName} #{idx}" );
|
||||
if( !t )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DrawShaderExportButton( tab, objectName, shader, idx );
|
||||
if( !disabled )
|
||||
{
|
||||
ImGui.SameLine();
|
||||
DrawShaderImportButton( tab, objectName, shaders, idx );
|
||||
}
|
||||
|
||||
ret |= DrawShaderPackageResourceArray( "Constant Buffers", "slot", true, shader.Constants, true );
|
||||
ret |= DrawShaderPackageResourceArray( "Samplers", "slot", false, shader.Samplers, true );
|
||||
ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "slot", true, shader.Uavs, true );
|
||||
|
||||
if( shader.AdditionalHeader.Length > 0 )
|
||||
{
|
||||
using var t2 = ImRaii.TreeNode( $"Additional Header (Size: {shader.AdditionalHeader.Length})###AdditionalHeader" );
|
||||
if( t2 )
|
||||
{
|
||||
ImGuiUtil.TextWrapped( string.Join( ' ', shader.AdditionalHeader.Select( c => $"{c:X2}" ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
DrawRawDisassembly( shader );
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool DrawShaderPackageResource( string slotLabel, bool withSize, ref Resource resource, bool disabled )
|
||||
{
|
||||
var ret = false;
|
||||
if( !disabled )
|
||||
{
|
||||
ImGui.SetNextItemWidth( UiHelpers.Scale * 150.0f );
|
||||
if( ImGuiUtil.InputUInt16( $"{char.ToUpper( slotLabel[ 0 ] )}{slotLabel[ 1.. ].ToLower()}", ref resource.Slot, ImGuiInputTextFlags.None ) )
|
||||
{
|
||||
ret = true;
|
||||
}
|
||||
}
|
||||
|
||||
if( resource.Used == null )
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
var usedString = UsedComponentString( withSize, resource );
|
||||
if( usedString.Length > 0 )
|
||||
{
|
||||
ImRaii.TreeNode( $"Used: {usedString}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
|
||||
}
|
||||
else
|
||||
{
|
||||
ImRaii.TreeNode( "Unused", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool DrawShaderPackageResourceArray( string arrayName, string slotLabel, bool withSize, Resource[] resources, bool disabled )
|
||||
{
|
||||
if( resources.Length == 0 )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using var t = ImRaii.TreeNode( arrayName );
|
||||
if( !t )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var ret = false;
|
||||
for( var idx = 0; idx < resources.Length; ++idx )
|
||||
{
|
||||
ref var buf = ref resources[ idx ];
|
||||
var name = $"#{idx}: {buf.Name} (ID: 0x{buf.Id:X8}), {slotLabel}: {buf.Slot}"
|
||||
+ ( withSize ? $", size: {buf.Size} registers###{idx}: {buf.Name} (ID: 0x{buf.Id:X8})" : string.Empty );
|
||||
using var font = ImRaii.PushFont( UiBuilder.MonoFont );
|
||||
using var t2 = ImRaii.TreeNode( name, !disabled || buf.Used != null ? 0 : ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet );
|
||||
font.Dispose();
|
||||
if( t2 )
|
||||
{
|
||||
ret |= DrawShaderPackageResource( slotLabel, withSize, ref buf, disabled );
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool DrawMaterialParamLayoutHeader( string label )
|
||||
{
|
||||
using var font = ImRaii.PushFont( UiBuilder.MonoFont );
|
||||
var pos = ImGui.GetCursorScreenPos()
|
||||
+ new Vector2( ImGui.CalcTextSize( label ).X + 3 * ImGui.GetStyle().ItemInnerSpacing.X + ImGui.GetFrameHeight(), ImGui.GetStyle().FramePadding.Y );
|
||||
|
||||
var ret = ImGui.CollapsingHeader( label );
|
||||
ImGui.GetWindowDrawList().AddText( UiBuilder.DefaultFont, UiBuilder.DefaultFont.FontSize, pos, ImGui.GetColorU32( ImGuiCol.Text ), "Layout" );
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static bool DrawMaterialParamLayoutBufferSize( ShpkFile file, Resource? materialParams )
|
||||
{
|
||||
var isSizeWellDefined = ( file.MaterialParamsSize & 0xF ) == 0 && ( !materialParams.HasValue || file.MaterialParamsSize == materialParams.Value.Size << 4 );
|
||||
if( isSizeWellDefined )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
ImGui.TextUnformatted( materialParams.HasValue
|
||||
? $"Buffer size mismatch: {file.MaterialParamsSize} bytes ≠ {materialParams.Value.Size} registers ({materialParams.Value.Size << 4} bytes)"
|
||||
: $"Buffer size mismatch: {file.MaterialParamsSize} bytes, not a multiple of 16" );
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool DrawShaderPackageMaterialMatrix( ShpkTab tab, bool disabled )
|
||||
{
|
||||
ImGui.TextUnformatted( "Parameter positions (continuations are grayed out, unused values are red):" );
|
||||
|
||||
using var table = ImRaii.Table( "##MaterialParamLayout", 5,
|
||||
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg );
|
||||
if( !table )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ImGui.TableSetupColumn( string.Empty, ImGuiTableColumnFlags.WidthFixed, 25 * UiHelpers.Scale );
|
||||
ImGui.TableSetupColumn( "x", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale );
|
||||
ImGui.TableSetupColumn( "y", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale );
|
||||
ImGui.TableSetupColumn( "z", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale );
|
||||
ImGui.TableSetupColumn( "w", ImGuiTableColumnFlags.WidthFixed, 100 * UiHelpers.Scale );
|
||||
ImGui.TableHeadersRow();
|
||||
|
||||
var textColorStart = ImGui.GetColorU32( ImGuiCol.Text );
|
||||
var textColorCont = ( textColorStart & 0x00FFFFFFu ) | ( ( textColorStart & 0xFE000000u ) >> 1 ); // Half opacity
|
||||
var textColorUnusedStart = ( textColorStart & 0xFF000000u ) | ( ( textColorStart & 0x00FEFEFE ) >> 1 ) | 0x80u; // Half red
|
||||
var textColorUnusedCont = ( textColorUnusedStart & 0x00FFFFFFu ) | ( ( textColorUnusedStart & 0xFE000000u ) >> 1 );
|
||||
|
||||
var ret = false;
|
||||
for( var i = 0; i < tab.Matrix.GetLength( 0 ); ++i )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableHeader( $" [{i}]" );
|
||||
for( var j = 0; j < 4; ++j )
|
||||
{
|
||||
var (name, tooltip, idx, colorType) = tab.Matrix[ i, j ];
|
||||
var color = colorType switch
|
||||
{
|
||||
ShpkTab.ColorType.Unused => textColorUnusedStart,
|
||||
ShpkTab.ColorType.Used => textColorStart,
|
||||
ShpkTab.ColorType.Continuation => textColorUnusedCont,
|
||||
ShpkTab.ColorType.Continuation | ShpkTab.ColorType.Used => textColorCont,
|
||||
_ => textColorStart,
|
||||
};
|
||||
using var _ = ImRaii.PushId( i * 4 + j );
|
||||
var deletable = !disabled && idx >= 0;
|
||||
using( var font = ImRaii.PushFont( UiBuilder.MonoFont, tooltip.Length > 0 ) )
|
||||
{
|
||||
using( var c = ImRaii.PushColor( ImGuiCol.Text, color ) )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.Selectable( name );
|
||||
if( deletable && ImGui.IsItemClicked( ImGuiMouseButton.Right ) && ImGui.GetIO().KeyCtrl )
|
||||
{
|
||||
tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.RemoveItems( idx );
|
||||
ret = true;
|
||||
tab.Update();
|
||||
}
|
||||
}
|
||||
|
||||
ImGuiUtil.HoverTooltip( tooltip );
|
||||
}
|
||||
|
||||
if( deletable )
|
||||
{
|
||||
ImGuiUtil.HoverTooltip( "\nControl + Right-Click to remove." );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static void DrawShaderPackageMisalignedParameters( ShpkTab tab )
|
||||
{
|
||||
using var t = ImRaii.TreeNode( "Misaligned / Overflowing Parameters" );
|
||||
if( !t )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var _ = ImRaii.PushFont( UiBuilder.MonoFont );
|
||||
foreach( var name in tab.MalformedParameters )
|
||||
{
|
||||
ImRaii.TreeNode( name, ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawShaderPackageStartCombo( ShpkTab tab )
|
||||
{
|
||||
using var s = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing );
|
||||
using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) )
|
||||
{
|
||||
ImGui.SetNextItemWidth( UiHelpers.Scale * 400 );
|
||||
using var c = ImRaii.Combo( "##Start", tab.Orphans[ tab.NewMaterialParamStart ].Name );
|
||||
if( c )
|
||||
{
|
||||
foreach( var (start, idx) in tab.Orphans.WithIndex() )
|
||||
{
|
||||
if( ImGui.Selectable( start.Name, idx == tab.NewMaterialParamStart ) )
|
||||
{
|
||||
tab.UpdateOrphanStart( idx );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted( "Start" );
|
||||
}
|
||||
|
||||
private static void DrawShaderPackageEndCombo( ShpkTab tab )
|
||||
{
|
||||
using var s = ImRaii.PushStyle( ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing );
|
||||
using( var _ = ImRaii.PushFont( UiBuilder.MonoFont ) )
|
||||
{
|
||||
ImGui.SetNextItemWidth( UiHelpers.Scale * 400 );
|
||||
using var c = ImRaii.Combo( "##End", tab.Orphans[ tab.NewMaterialParamEnd ].Name );
|
||||
if( c )
|
||||
{
|
||||
var current = tab.Orphans[ tab.NewMaterialParamStart ].Index;
|
||||
for( var i = tab.NewMaterialParamStart; i < tab.Orphans.Count; ++i )
|
||||
{
|
||||
var next = tab.Orphans[ i ];
|
||||
if( current++ != next.Index )
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if( ImGui.Selectable( next.Name, i == tab.NewMaterialParamEnd ) )
|
||||
{
|
||||
tab.NewMaterialParamEnd = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted( "End" );
|
||||
}
|
||||
|
||||
private static bool DrawShaderPackageNewParameter( ShpkTab tab )
|
||||
{
|
||||
if( tab.Orphans.Count == 0 )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
DrawShaderPackageStartCombo( tab );
|
||||
DrawShaderPackageEndCombo( tab );
|
||||
|
||||
ImGui.SetNextItemWidth( UiHelpers.Scale * 400 );
|
||||
if( ImGui.InputText( "Name", ref tab.NewMaterialParamName, 63 ) )
|
||||
{
|
||||
tab.NewMaterialParamId = Crc32.Get( tab.NewMaterialParamName, 0xFFFFFFFFu );
|
||||
}
|
||||
|
||||
var tooltip = tab.UsedIds.Contains( tab.NewMaterialParamId )
|
||||
? "The ID is already in use. Please choose a different name."
|
||||
: string.Empty;
|
||||
if( !ImGuiUtil.DrawDisabledButton( $"Add ID 0x{tab.NewMaterialParamId:X8}", new Vector2( 400 * UiHelpers.Scale, ImGui.GetFrameHeight() ), tooltip,
|
||||
tooltip.Length > 0 ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
tab.Shpk.MaterialParams = tab.Shpk.MaterialParams.AddItem( new MaterialParam
|
||||
{
|
||||
Id = tab.NewMaterialParamId,
|
||||
ByteOffset = ( ushort )( tab.Orphans[ tab.NewMaterialParamStart ].Index << 2 ),
|
||||
ByteSize = ( ushort )( ( tab.NewMaterialParamEnd - tab.NewMaterialParamStart + 1 ) << 2 ),
|
||||
} );
|
||||
tab.Update();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool DrawShaderPackageMaterialParamLayout( ShpkTab tab, bool disabled )
|
||||
{
|
||||
var ret = false;
|
||||
|
||||
var materialParams = tab.Shpk.GetConstantById( MaterialParamsConstantId );
|
||||
if( !DrawMaterialParamLayoutHeader( materialParams?.Name ?? "Material Parameter" ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var sizeWellDefined = DrawMaterialParamLayoutBufferSize( tab.Shpk, materialParams );
|
||||
|
||||
ret |= DrawShaderPackageMaterialMatrix( tab, disabled );
|
||||
|
||||
if( tab.MalformedParameters.Count > 0 )
|
||||
{
|
||||
DrawShaderPackageMisalignedParameters( tab );
|
||||
}
|
||||
else if( !disabled && sizeWellDefined )
|
||||
{
|
||||
ret |= DrawShaderPackageNewParameter( tab );
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static void DrawKeyArray( string arrayName, bool withId, IReadOnlyCollection< Key > keys )
|
||||
{
|
||||
if( keys.Count == 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var t = ImRaii.TreeNode( arrayName );
|
||||
if( !t )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var font = ImRaii.PushFont( UiBuilder.MonoFont );
|
||||
foreach( var (key, idx) in keys.WithIndex() )
|
||||
{
|
||||
using var t2 = ImRaii.TreeNode( withId ? $"#{idx}: ID: 0x{key.Id:X8}" : $"#{idx}" );
|
||||
if( t2 )
|
||||
{
|
||||
ImRaii.TreeNode( $"Default Value: 0x{key.DefaultValue:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
|
||||
ImRaii.TreeNode( $"Known Values: {string.Join( ", ", Array.ConvertAll( key.Values, value => $"0x{value:X8}" ) )}",
|
||||
ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawShaderPackageNodes( ShpkTab tab )
|
||||
{
|
||||
if( tab.Shpk.Nodes.Length <= 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var t = ImRaii.TreeNode( $"Nodes ({tab.Shpk.Nodes.Length})###Nodes" );
|
||||
if( !t )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var (node, idx) in tab.Shpk.Nodes.WithIndex() )
|
||||
{
|
||||
using var font = ImRaii.PushFont( UiBuilder.MonoFont );
|
||||
using var t2 = ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{node.Id:X8}" );
|
||||
if( !t2 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach( var (key, keyIdx) in node.SystemKeys.WithIndex() )
|
||||
{
|
||||
ImRaii.TreeNode( $"System Key 0x{tab.Shpk.SystemKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
|
||||
}
|
||||
|
||||
foreach( var (key, keyIdx) in node.SceneKeys.WithIndex() )
|
||||
{
|
||||
ImRaii.TreeNode( $"Scene Key 0x{tab.Shpk.SceneKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
|
||||
}
|
||||
|
||||
foreach( var (key, keyIdx) in node.MaterialKeys.WithIndex() )
|
||||
{
|
||||
ImRaii.TreeNode( $"Material Key 0x{tab.Shpk.MaterialKeys[ keyIdx ].Id:X8} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
|
||||
}
|
||||
|
||||
foreach( var (key, keyIdx) in node.SubViewKeys.WithIndex() )
|
||||
{
|
||||
ImRaii.TreeNode( $"Sub-View Key #{keyIdx} = 0x{key:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
|
||||
}
|
||||
|
||||
ImRaii.TreeNode( $"Pass Indices: {string.Join( ' ', node.PassIndices.Select( c => $"{c:X2}" ) )}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
|
||||
foreach( var (pass, passIdx) in node.Passes.WithIndex() )
|
||||
{
|
||||
ImRaii.TreeNode( $"Pass #{passIdx}: ID: 0x{pass.Id:X8}, Vertex Shader #{pass.VertexShader}, Pixel Shader #{pass.PixelShader}",
|
||||
ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet )
|
||||
.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool DrawOtherShaderPackageDetails( ShpkTab tab, bool disabled )
|
||||
{
|
||||
var ret = false;
|
||||
|
||||
if( !ImGui.CollapsingHeader( "Further Content" ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ImRaii.TreeNode( $"Version: 0x{tab.Shpk.Version:X8}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
|
||||
|
||||
ret |= DrawShaderPackageResourceArray( "Constant Buffers", "type", true, tab.Shpk.Constants, disabled );
|
||||
ret |= DrawShaderPackageResourceArray( "Samplers", "type", false, tab.Shpk.Samplers, disabled );
|
||||
ret |= DrawShaderPackageResourceArray( "Unordered Access Views", "type", false, tab.Shpk.Uavs, disabled );
|
||||
|
||||
DrawKeyArray( "System Keys", true, tab.Shpk.SystemKeys );
|
||||
DrawKeyArray( "Scene Keys", true, tab.Shpk.SceneKeys );
|
||||
DrawKeyArray( "Material Keys", true, tab.Shpk.MaterialKeys );
|
||||
DrawKeyArray( "Sub-View Keys", false, tab.Shpk.SubViewKeys );
|
||||
|
||||
DrawShaderPackageNodes( tab );
|
||||
if( tab.Shpk.Items.Length > 0 )
|
||||
{
|
||||
using var t = ImRaii.TreeNode( $"Items ({tab.Shpk.Items.Length})###Items" );
|
||||
if( t )
|
||||
{
|
||||
using var font = ImRaii.PushFont( UiBuilder.MonoFont );
|
||||
foreach( var (item, idx) in tab.Shpk.Items.WithIndex() )
|
||||
{
|
||||
ImRaii.TreeNode( $"#{idx:D4}: ID: 0x{item.Id:X8}, node: {item.Node}", ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.Bullet ).Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( tab.Shpk.AdditionalData.Length > 0 )
|
||||
{
|
||||
using var t = ImRaii.TreeNode( $"Additional Data (Size: {tab.Shpk.AdditionalData.Length})###AdditionalData" );
|
||||
if( t )
|
||||
{
|
||||
ImGuiUtil.TextWrapped( string.Join( ' ', tab.Shpk.AdditionalData.Select( c => $"{c:X2}" ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static string UsedComponentString( bool withSize, in Resource resource )
|
||||
{
|
||||
var sb = new StringBuilder( 256 );
|
||||
if( withSize )
|
||||
{
|
||||
foreach( var (components, i) in ( resource.Used ?? Array.Empty< DisassembledShader.VectorComponents >() ).WithIndex() )
|
||||
{
|
||||
switch( components )
|
||||
{
|
||||
case 0: break;
|
||||
case DisassembledShader.VectorComponents.All:
|
||||
sb.Append( $"[{i}], " );
|
||||
break;
|
||||
default:
|
||||
sb.Append( $"[{i}]." );
|
||||
foreach( var c in components.ToString().Where( char.IsUpper ) )
|
||||
{
|
||||
sb.Append( char.ToLower( c ) );
|
||||
}
|
||||
|
||||
sb.Append( ", " );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
switch( resource.UsedDynamically ?? 0 )
|
||||
{
|
||||
case 0: break;
|
||||
case DisassembledShader.VectorComponents.All:
|
||||
sb.Append( "[*], " );
|
||||
break;
|
||||
default:
|
||||
sb.Append( "[*]." );
|
||||
foreach( var c in resource.UsedDynamically!.Value.ToString().Where( char.IsUpper ) )
|
||||
{
|
||||
sb.Append( char.ToLower( c ) );
|
||||
}
|
||||
|
||||
sb.Append( ", " );
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var components = ( resource.Used is { Length: > 0 } ? resource.Used[ 0 ] : 0 ) | ( resource.UsedDynamically ?? 0 );
|
||||
if( ( components & DisassembledShader.VectorComponents.X ) != 0 )
|
||||
{
|
||||
sb.Append( "Red, " );
|
||||
}
|
||||
|
||||
if( ( components & DisassembledShader.VectorComponents.Y ) != 0 )
|
||||
{
|
||||
sb.Append( "Green, " );
|
||||
}
|
||||
|
||||
if( ( components & DisassembledShader.VectorComponents.Z ) != 0 )
|
||||
{
|
||||
sb.Append( "Blue, " );
|
||||
}
|
||||
|
||||
if( ( components & DisassembledShader.VectorComponents.W ) != 0 )
|
||||
{
|
||||
sb.Append( "Alpha, " );
|
||||
}
|
||||
}
|
||||
|
||||
return sb.Length == 0 ? string.Empty : sb.ToString( 0, sb.Length - 2 );
|
||||
}
|
||||
}
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Dalamud.Utility;
|
||||
using Lumina.Misc;
|
||||
using OtterGui;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Files;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private class ShpkTab : IWritable
|
||||
{
|
||||
public readonly ShpkFile Shpk;
|
||||
|
||||
public string NewMaterialParamName = string.Empty;
|
||||
public uint NewMaterialParamId = Crc32.Get(string.Empty, 0xFFFFFFFFu);
|
||||
public short NewMaterialParamStart;
|
||||
public short NewMaterialParamEnd;
|
||||
|
||||
public readonly FileDialogService FileDialog;
|
||||
|
||||
public readonly string Header;
|
||||
public readonly string Extension;
|
||||
|
||||
public ShpkTab(FileDialogService fileDialog, byte[] bytes)
|
||||
{
|
||||
FileDialog = fileDialog;
|
||||
Shpk = new ShpkFile(bytes, true);
|
||||
Header = $"Shader Package for DirectX {(int)Shpk.DirectXVersion}";
|
||||
Extension = Shpk.DirectXVersion switch
|
||||
{
|
||||
ShpkFile.DxVersion.DirectX9 => ".cso",
|
||||
ShpkFile.DxVersion.DirectX11 => ".dxbc",
|
||||
_ => throw new NotImplementedException(),
|
||||
};
|
||||
Update();
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum ColorType : byte
|
||||
{
|
||||
Unused = 0,
|
||||
Used = 1,
|
||||
Continuation = 2,
|
||||
}
|
||||
|
||||
public (string Name, string Tooltip, short Index, ColorType Color)[,] Matrix = null!;
|
||||
public readonly List<string> MalformedParameters = new();
|
||||
public readonly HashSet<uint> UsedIds = new(16);
|
||||
public readonly List<(string Name, short Index)> Orphans = new(16);
|
||||
|
||||
public void Update()
|
||||
{
|
||||
var materialParams = Shpk.GetConstantById(ShpkFile.MaterialParamsConstantId);
|
||||
var numParameters = ((Shpk.MaterialParamsSize + 0xFu) & ~0xFu) >> 4;
|
||||
Matrix = new (string Name, string Tooltip, short Index, ColorType Color)[numParameters, 4];
|
||||
|
||||
MalformedParameters.Clear();
|
||||
UsedIds.Clear();
|
||||
foreach (var (param, idx) in Shpk.MaterialParams.WithIndex())
|
||||
{
|
||||
UsedIds.Add(param.Id);
|
||||
var iStart = param.ByteOffset >> 4;
|
||||
var jStart = (param.ByteOffset >> 2) & 3;
|
||||
var iEnd = (param.ByteOffset + param.ByteSize - 1) >> 4;
|
||||
var jEnd = ((param.ByteOffset + param.ByteSize - 1) >> 2) & 3;
|
||||
if ((param.ByteOffset & 0x3) != 0 || (param.ByteSize & 0x3) != 0)
|
||||
{
|
||||
MalformedParameters.Add($"ID: 0x{param.Id:X8}, offset: 0x{param.ByteOffset:X4}, size: 0x{param.ByteSize:X4}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (iEnd >= numParameters)
|
||||
{
|
||||
MalformedParameters.Add(
|
||||
$"{MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2)} (ID: 0x{param.Id:X8})");
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var i = iStart; i <= iEnd; ++i)
|
||||
{
|
||||
var end = i == iEnd ? jEnd : 3;
|
||||
for (var j = i == iStart ? jStart : 0; j <= end; ++j)
|
||||
{
|
||||
var tt =
|
||||
$"{MaterialParamRangeName(materialParams?.Name ?? string.Empty, param.ByteOffset >> 2, param.ByteSize >> 2).Item1} (ID: 0x{param.Id:X8})";
|
||||
Matrix[i, j] = ($"0x{param.Id:X8}", tt, (short)idx, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UpdateOrphans(materialParams);
|
||||
UpdateColors(materialParams);
|
||||
}
|
||||
|
||||
public void UpdateOrphanStart(int orphanStart)
|
||||
{
|
||||
var oldEnd = Orphans.Count > 0 ? Orphans[NewMaterialParamEnd].Index : -1;
|
||||
UpdateOrphanStart(orphanStart, oldEnd);
|
||||
}
|
||||
|
||||
private void UpdateOrphanStart(int orphanStart, int oldEnd)
|
||||
{
|
||||
var count = Math.Min(NewMaterialParamEnd - NewMaterialParamStart + orphanStart + 1, Orphans.Count);
|
||||
NewMaterialParamStart = (short)orphanStart;
|
||||
var current = Orphans[NewMaterialParamStart].Index;
|
||||
for (var i = NewMaterialParamStart; i < count; ++i)
|
||||
{
|
||||
var next = Orphans[i].Index;
|
||||
if (current++ != next)
|
||||
{
|
||||
NewMaterialParamEnd = (short)(i - 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (next == oldEnd)
|
||||
{
|
||||
NewMaterialParamEnd = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
NewMaterialParamEnd = (short)(count - 1);
|
||||
}
|
||||
|
||||
private void UpdateOrphans(ShpkFile.Resource? materialParams)
|
||||
{
|
||||
var oldStart = Orphans.Count > 0 ? Orphans[NewMaterialParamStart].Index : -1;
|
||||
var oldEnd = Orphans.Count > 0 ? Orphans[NewMaterialParamEnd].Index : -1;
|
||||
|
||||
Orphans.Clear();
|
||||
short newMaterialParamStart = 0;
|
||||
for (var i = 0; i < Matrix.GetLength(0); ++i)
|
||||
{
|
||||
for (var j = 0; j < 4; ++j)
|
||||
{
|
||||
if (!Matrix[i, j].Name.IsNullOrEmpty())
|
||||
continue;
|
||||
|
||||
Matrix[i, j] = ("(none)", string.Empty, -1, 0);
|
||||
var linear = (short)(4 * i + j);
|
||||
if (oldStart == linear)
|
||||
newMaterialParamStart = (short)Orphans.Count;
|
||||
|
||||
Orphans.Add(($"{materialParams?.Name ?? string.Empty}{MaterialParamName(false, linear)}", linear));
|
||||
}
|
||||
}
|
||||
|
||||
if (Orphans.Count == 0)
|
||||
return;
|
||||
|
||||
UpdateOrphanStart(newMaterialParamStart, oldEnd);
|
||||
}
|
||||
|
||||
private void UpdateColors(ShpkFile.Resource? materialParams)
|
||||
{
|
||||
var lastIndex = -1;
|
||||
for (var i = 0; i < Matrix.GetLength(0); ++i)
|
||||
{
|
||||
var usedComponents = (materialParams?.Used?[i] ?? DisassembledShader.VectorComponents.All)
|
||||
| (materialParams?.UsedDynamically ?? 0);
|
||||
for (var j = 0; j < 4; ++j)
|
||||
{
|
||||
var color = ((byte)usedComponents & (1 << j)) != 0
|
||||
? ColorType.Used
|
||||
: 0;
|
||||
if (Matrix[i, j].Index == lastIndex || Matrix[i, j].Index < 0)
|
||||
color |= ColorType.Continuation;
|
||||
|
||||
lastIndex = Matrix[i, j].Index;
|
||||
Matrix[i, j].Color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Valid
|
||||
=> Shpk.Valid;
|
||||
|
||||
public byte[] Write()
|
||||
=> Shpk.Write();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.Import.Textures;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public partial class ModEditWindow
|
||||
{
|
||||
private readonly Texture _left = new();
|
||||
private readonly Texture _right = new();
|
||||
private readonly CombinedTexture _center;
|
||||
|
||||
private bool _overlayCollapsed = true;
|
||||
|
||||
private bool _addMipMaps = true;
|
||||
private int _currentSaveAs;
|
||||
|
||||
private static readonly (string, string)[] SaveAsStrings =
|
||||
{
|
||||
("As Is", "Save the current texture with its own format without additional conversion or compression, if possible."),
|
||||
("RGBA (Uncompressed)",
|
||||
"Save the current texture as an uncompressed BGRA bitmap. This requires the most space but technically offers the best quality."),
|
||||
("BC3 (Simple Compression)",
|
||||
"Save the current texture compressed via BC3/DXT5 compression. This offers a 4:1 compression ratio and is quick with acceptable quality."),
|
||||
("BC7 (Complex Compression)",
|
||||
"Save the current texture compressed via BC7 compression. This offers a 4:1 compression ratio and has almost indistinguishable quality, but may take a while."),
|
||||
};
|
||||
|
||||
private void DrawInputChild(string label, Texture tex, Vector2 size, Vector2 imageSize)
|
||||
{
|
||||
using var child = ImRaii.Child(label, size, true);
|
||||
if (!child)
|
||||
return;
|
||||
|
||||
using var id = ImRaii.PushId(label);
|
||||
ImGuiUtil.DrawTextButton(label, new Vector2(-1, 0), ImGui.GetColorU32(ImGuiCol.FrameBg));
|
||||
ImGui.NewLine();
|
||||
|
||||
tex.PathInputBox("##input", "Import Image...", "Can import game paths as well as your own files.", _mod!.ModPath.FullName,
|
||||
_fileDialog);
|
||||
var files = _editor!.TexFiles.SelectMany(f => f.SubModUsage.Select(p => (p.Item2.ToString(), true))
|
||||
.Prepend((f.File.FullName, false)));
|
||||
tex.PathSelectBox("##combo", "Select the textures included in this mod on your drive or the ones they replace from the game files.",
|
||||
files, _mod.ModPath.FullName.Length + 1);
|
||||
|
||||
if (tex == _left)
|
||||
_center.DrawMatrixInputLeft(size.X);
|
||||
else
|
||||
_center.DrawMatrixInputRight(size.X);
|
||||
|
||||
ImGui.NewLine();
|
||||
using var child2 = ImRaii.Child("image");
|
||||
if (child2)
|
||||
tex.Draw(imageSize);
|
||||
}
|
||||
|
||||
private void SaveAsCombo()
|
||||
{
|
||||
var (text, desc) = SaveAsStrings[_currentSaveAs];
|
||||
ImGui.SetNextItemWidth(-ImGui.GetFrameHeight() - ImGui.GetStyle().ItemSpacing.X);
|
||||
using var combo = ImRaii.Combo("##format", text);
|
||||
ImGuiUtil.HoverTooltip(desc);
|
||||
if (!combo)
|
||||
return;
|
||||
|
||||
foreach (var ((newText, newDesc), idx) in SaveAsStrings.WithIndex())
|
||||
{
|
||||
if (ImGui.Selectable(newText, idx == _currentSaveAs))
|
||||
_currentSaveAs = idx;
|
||||
|
||||
ImGuiUtil.HoverTooltip(newDesc);
|
||||
}
|
||||
}
|
||||
|
||||
private void MipMapInput()
|
||||
{
|
||||
ImGui.Checkbox("##mipMaps", ref _addMipMaps);
|
||||
ImGuiUtil.HoverTooltip(
|
||||
"Add the appropriate number of MipMaps to the file.");
|
||||
}
|
||||
|
||||
private void DrawOutputChild(Vector2 size, Vector2 imageSize)
|
||||
{
|
||||
using var child = ImRaii.Child("Output", size, true);
|
||||
if (!child)
|
||||
return;
|
||||
|
||||
if (_center.IsLoaded)
|
||||
{
|
||||
SaveAsCombo();
|
||||
ImGui.SameLine();
|
||||
MipMapInput();
|
||||
if (ImGui.Button("Save as TEX", -Vector2.UnitX))
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(_left.Path.Length > 0 ? _left.Path : _right.Path);
|
||||
_fileDialog.OpenSavePicker("Save Texture as TEX...", ".tex", fileName, ".tex", (a, b) =>
|
||||
{
|
||||
if (a)
|
||||
_center.SaveAsTex(b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps);
|
||||
}, _mod!.ModPath.FullName, false);
|
||||
}
|
||||
|
||||
if (ImGui.Button("Save as DDS", -Vector2.UnitX))
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(_right.Path.Length > 0 ? _right.Path : _left.Path);
|
||||
_fileDialog.OpenSavePicker("Save Texture as DDS...", ".dds", fileName, ".dds", (a, b) =>
|
||||
{
|
||||
if (a)
|
||||
_center.SaveAsDds(b, (CombinedTexture.TextureSaveType)_currentSaveAs, _addMipMaps);
|
||||
}, _mod!.ModPath.FullName, false);
|
||||
}
|
||||
|
||||
ImGui.NewLine();
|
||||
|
||||
if (ImGui.Button("Save as PNG", -Vector2.UnitX))
|
||||
{
|
||||
var fileName = Path.GetFileNameWithoutExtension(_right.Path.Length > 0 ? _right.Path : _left.Path);
|
||||
_fileDialog.OpenSavePicker("Save Texture as PNG...", ".png", fileName, ".png", (a, b) =>
|
||||
{
|
||||
if (a)
|
||||
_center.SaveAsPng(b);
|
||||
}, _mod!.ModPath.FullName, false);
|
||||
}
|
||||
|
||||
ImGui.NewLine();
|
||||
}
|
||||
|
||||
if (_center.SaveException != null)
|
||||
{
|
||||
ImGui.TextUnformatted("Could not save file:");
|
||||
using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF0000FF);
|
||||
ImGuiUtil.TextWrapped(_center.SaveException.ToString());
|
||||
}
|
||||
|
||||
using var child2 = ImRaii.Child("image");
|
||||
if (child2)
|
||||
_center.Draw(imageSize);
|
||||
}
|
||||
|
||||
private Vector2 GetChildWidth()
|
||||
{
|
||||
var windowWidth = ImGui.GetWindowContentRegionMax().X - ImGui.GetWindowContentRegionMin().X - ImGui.GetTextLineHeight();
|
||||
if (_overlayCollapsed)
|
||||
{
|
||||
var width = windowWidth - ImGui.GetStyle().FramePadding.X * 3;
|
||||
return new Vector2(width / 2, -1);
|
||||
}
|
||||
|
||||
return new Vector2((windowWidth - ImGui.GetStyle().FramePadding.X * 5) / 3, -1);
|
||||
}
|
||||
|
||||
private void DrawTextureTab()
|
||||
{
|
||||
using var tab = ImRaii.TabItem("Texture Import/Export");
|
||||
if (!tab)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var childWidth = GetChildWidth();
|
||||
var imageSize = new Vector2(childWidth.X - ImGui.GetStyle().FramePadding.X * 2);
|
||||
DrawInputChild("Input Texture", _left, childWidth, imageSize);
|
||||
ImGui.SameLine();
|
||||
DrawOutputChild(childWidth, imageSize);
|
||||
if (!_overlayCollapsed)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
DrawInputChild("Overlay Texture", _right, childWidth, imageSize);
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
DrawOverlayCollapseButton();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Penumbra.Log.Error($"Unknown Error while drawing textures:\n{e}");
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawOverlayCollapseButton()
|
||||
{
|
||||
var (label, tooltip) = _overlayCollapsed
|
||||
? (">", "Show a third panel in which you can import an additional texture as an overlay for the primary texture.")
|
||||
: ("<", "Hide the overlay texture panel and clear the currently loaded overlay texture, if any.");
|
||||
if (ImGui.Button(label, new Vector2(ImGui.GetTextLineHeight(), ImGui.GetContentRegionAvail().Y)))
|
||||
_overlayCollapsed = !_overlayCollapsed;
|
||||
|
||||
ImGuiUtil.HoverTooltip(tooltip);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,511 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Components;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.Import.Textures;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Services;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.Util;
|
||||
using static Penumbra.Mods.Mod;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public partial class ModEditWindow : Window, IDisposable
|
||||
{
|
||||
private const string WindowBaseLabel = "###SubModEdit";
|
||||
internal readonly ItemSwapWindow _swapWindow;
|
||||
|
||||
private Editor? _editor;
|
||||
private Mod? _mod;
|
||||
private Vector2 _iconSize = Vector2.Zero;
|
||||
private bool _allowReduplicate = false;
|
||||
|
||||
public void ChangeMod(Mod mod)
|
||||
{
|
||||
if (mod == _mod)
|
||||
return;
|
||||
|
||||
_editor?.Dispose();
|
||||
_editor = new Editor(mod, mod.Default);
|
||||
_mod = mod;
|
||||
|
||||
SizeConstraints = new WindowSizeConstraints
|
||||
{
|
||||
MinimumSize = new Vector2(1240, 600),
|
||||
MaximumSize = 4000 * Vector2.One,
|
||||
};
|
||||
_selectedFiles.Clear();
|
||||
_modelTab.Reset();
|
||||
_materialTab.Reset();
|
||||
_shaderPackageTab.Reset();
|
||||
_swapWindow.UpdateMod(mod, Penumbra.CollectionManager.Current[mod.Index].Settings);
|
||||
}
|
||||
|
||||
public void ChangeOption(ISubMod? subMod)
|
||||
=> _editor?.SetSubMod(subMod);
|
||||
|
||||
public void UpdateModels()
|
||||
=> _editor?.ScanModels();
|
||||
|
||||
public override bool DrawConditions()
|
||||
=> _editor != null;
|
||||
|
||||
public override void PreDraw()
|
||||
{
|
||||
using var performance = Penumbra.Performance.Measure(PerformanceType.UiAdvancedWindow);
|
||||
|
||||
var sb = new StringBuilder(256);
|
||||
|
||||
var redirections = 0;
|
||||
var unused = 0;
|
||||
var size = _editor!.AvailableFiles.Sum(f =>
|
||||
{
|
||||
if (f.SubModUsage.Count > 0)
|
||||
redirections += f.SubModUsage.Count;
|
||||
else
|
||||
++unused;
|
||||
|
||||
return f.FileSize;
|
||||
});
|
||||
var manipulations = 0;
|
||||
var subMods = 0;
|
||||
var swaps = _mod!.AllSubMods.Sum(m =>
|
||||
{
|
||||
++subMods;
|
||||
manipulations += m.Manipulations.Count;
|
||||
return m.FileSwaps.Count;
|
||||
});
|
||||
sb.Append(_mod!.Name);
|
||||
if (subMods > 1)
|
||||
sb.Append($" | {subMods} Options");
|
||||
|
||||
if (size > 0)
|
||||
sb.Append($" | {_editor.AvailableFiles.Count} Files ({Functions.HumanReadableSize(size)})");
|
||||
|
||||
if (unused > 0)
|
||||
sb.Append($" | {unused} Unused Files");
|
||||
|
||||
if (_editor.MissingFiles.Count > 0)
|
||||
sb.Append($" | {_editor.MissingFiles.Count} Missing Files");
|
||||
|
||||
if (redirections > 0)
|
||||
sb.Append($" | {redirections} Redirections");
|
||||
|
||||
if (manipulations > 0)
|
||||
sb.Append($" | {manipulations} Manipulations");
|
||||
|
||||
if (swaps > 0)
|
||||
sb.Append($" | {swaps} Swaps");
|
||||
|
||||
_allowReduplicate = redirections != _editor.AvailableFiles.Count || _editor.MissingFiles.Count > 0;
|
||||
sb.Append(WindowBaseLabel);
|
||||
WindowName = sb.ToString();
|
||||
}
|
||||
|
||||
public override void OnClose()
|
||||
{
|
||||
_left.Dispose();
|
||||
_right.Dispose();
|
||||
}
|
||||
|
||||
public override void Draw()
|
||||
{
|
||||
using var performance = Penumbra.Performance.Measure(PerformanceType.UiAdvancedWindow);
|
||||
|
||||
using var tabBar = ImRaii.TabBar("##tabs");
|
||||
if (!tabBar)
|
||||
return;
|
||||
|
||||
_iconSize = new Vector2(ImGui.GetFrameHeight());
|
||||
DrawFileTab();
|
||||
DrawMetaTab();
|
||||
DrawSwapTab();
|
||||
DrawMissingFilesTab();
|
||||
DrawDuplicatesTab();
|
||||
DrawMaterialReassignmentTab();
|
||||
_modelTab.Draw();
|
||||
_materialTab.Draw();
|
||||
DrawTextureTab();
|
||||
_shaderPackageTab.Draw();
|
||||
_swapWindow.DrawItemSwapPanel();
|
||||
}
|
||||
|
||||
// A row of three buttonSizes and a help marker that can be used for material suffix changing.
|
||||
private static class MaterialSuffix
|
||||
{
|
||||
private static string _materialSuffixFrom = string.Empty;
|
||||
private static string _materialSuffixTo = string.Empty;
|
||||
private static GenderRace _raceCode = GenderRace.Unknown;
|
||||
|
||||
private static string RaceCodeName(GenderRace raceCode)
|
||||
{
|
||||
if (raceCode == GenderRace.Unknown)
|
||||
return "All Races and Genders";
|
||||
|
||||
var (gender, race) = raceCode.Split();
|
||||
return $"({raceCode.ToRaceCode()}) {race.ToName()} {gender.ToName()} ";
|
||||
}
|
||||
|
||||
private static void DrawRaceCodeCombo(Vector2 buttonSize)
|
||||
{
|
||||
ImGui.SetNextItemWidth(buttonSize.X);
|
||||
using var combo = ImRaii.Combo("##RaceCode", RaceCodeName(_raceCode));
|
||||
if (!combo)
|
||||
return;
|
||||
|
||||
foreach (var raceCode in Enum.GetValues<GenderRace>())
|
||||
{
|
||||
if (ImGui.Selectable(RaceCodeName(raceCode), _raceCode == raceCode))
|
||||
_raceCode = raceCode;
|
||||
}
|
||||
}
|
||||
|
||||
public static void Draw(Editor editor, Vector2 buttonSize)
|
||||
{
|
||||
DrawRaceCodeCombo(buttonSize);
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth(buttonSize.X);
|
||||
ImGui.InputTextWithHint("##suffixFrom", "From...", ref _materialSuffixFrom, 32);
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth(buttonSize.X);
|
||||
ImGui.InputTextWithHint("##suffixTo", "To...", ref _materialSuffixTo, 32);
|
||||
ImGui.SameLine();
|
||||
var disabled = !Editor.ValidString(_materialSuffixTo);
|
||||
var tt = _materialSuffixTo.Length == 0
|
||||
? "Please enter a target suffix."
|
||||
: _materialSuffixFrom == _materialSuffixTo
|
||||
? "The source and target are identical."
|
||||
: disabled
|
||||
? "The suffix is invalid."
|
||||
: _materialSuffixFrom.Length == 0
|
||||
? _raceCode == GenderRace.Unknown
|
||||
? "Convert all skin material suffices to the target."
|
||||
: "Convert all skin material suffices for the given race code to the target."
|
||||
: _raceCode == GenderRace.Unknown
|
||||
? $"Convert all skin material suffices that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'."
|
||||
: $"Convert all skin material suffices for the given race code that are currently '{_materialSuffixFrom}' to '{_materialSuffixTo}'.";
|
||||
if (ImGuiUtil.DrawDisabledButton("Change Material Suffix", buttonSize, tt, disabled))
|
||||
editor.ReplaceAllMaterials(_materialSuffixTo, _materialSuffixFrom, _raceCode);
|
||||
|
||||
var anyChanges = editor.ModelFiles.Any(m => m.Changed);
|
||||
if (ImGuiUtil.DrawDisabledButton("Save All Changes", buttonSize,
|
||||
anyChanges ? "Irreversibly rewrites all currently applied changes to model files." : "No changes made yet.", !anyChanges))
|
||||
editor.SaveAllModels();
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.DrawDisabledButton("Revert All Changes", buttonSize,
|
||||
anyChanges ? "Revert all currently made and unsaved changes." : "No changes made yet.", !anyChanges))
|
||||
editor.RestoreAllModels();
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGuiComponents.HelpMarker(
|
||||
"Model files refer to the skin material they should use. This skin material is always the same, but modders have started using different suffices to differentiate between body types.\n"
|
||||
+ "This option allows you to switch the suffix of all model files to another. This changes the files, so you do this on your own risk.\n"
|
||||
+ "If you do not know what the currently used suffix of this mod is, you can leave 'From' blank and it will replace all suffices with 'To', instead of only the matching ones.");
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawMissingFilesTab()
|
||||
{
|
||||
if (_editor!.MissingFiles.Count == 0)
|
||||
return;
|
||||
|
||||
using var tab = ImRaii.TabItem("Missing Files");
|
||||
if (!tab)
|
||||
return;
|
||||
|
||||
ImGui.NewLine();
|
||||
if (ImGui.Button("Remove Missing Files from Mod"))
|
||||
_editor.RemoveMissingPaths();
|
||||
|
||||
using var child = ImRaii.Child("##unusedFiles", -Vector2.One, true);
|
||||
if (!child)
|
||||
return;
|
||||
|
||||
using var table = ImRaii.Table("##missingFiles", 1, ImGuiTableFlags.RowBg, -Vector2.One);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
foreach (var path in _editor.MissingFiles)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(path.FullName);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawDuplicatesTab()
|
||||
{
|
||||
using var tab = ImRaii.TabItem("Duplicates");
|
||||
if (!tab)
|
||||
return;
|
||||
|
||||
var buttonText = _editor!.DuplicatesFinished ? "Scan for Duplicates###ScanButton" : "Scanning for Duplicates...###ScanButton";
|
||||
if (ImGuiUtil.DrawDisabledButton(buttonText, Vector2.Zero, "Search for identical files in this mod. This may take a while.",
|
||||
!_editor.DuplicatesFinished))
|
||||
_editor.StartDuplicateCheck();
|
||||
|
||||
const string desc =
|
||||
"Tries to create a unique copy of a file for every game path manipulated and put them in [Groupname]/[Optionname]/[GamePath] order.\n"
|
||||
+ "This will also delete all unused files and directories if it succeeds.\n"
|
||||
+ "Care was taken that a failure should not destroy the mod but revert to its original state, but you use this at your own risk anyway.";
|
||||
|
||||
var modifier = Penumbra.Config.DeleteModModifier.IsActive();
|
||||
|
||||
var tt = _allowReduplicate ? desc :
|
||||
modifier ? desc : desc + $"\n\nNo duplicates detected! Hold {Penumbra.Config.DeleteModModifier} to force normalization anyway.";
|
||||
|
||||
if (ImGuiUtil.DrawDisabledButton("Re-Duplicate and Normalize Mod", Vector2.Zero, tt, !_allowReduplicate && !modifier))
|
||||
{
|
||||
_mod!.Normalize(Penumbra.ModManager);
|
||||
_editor.RevertFiles();
|
||||
}
|
||||
|
||||
if (!_editor.DuplicatesFinished)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
if (ImGui.Button("Cancel"))
|
||||
_editor.Cancel();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_editor.Duplicates.Count == 0)
|
||||
{
|
||||
ImGui.NewLine();
|
||||
ImGui.TextUnformatted("No duplicates found.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui.Button("Delete and Redirect Duplicates"))
|
||||
_editor.DeleteDuplicates();
|
||||
|
||||
if (_editor.SavedSpace > 0)
|
||||
{
|
||||
ImGui.SameLine();
|
||||
ImGui.TextUnformatted($"Frees up {Functions.HumanReadableSize(_editor.SavedSpace)} from your hard drive.");
|
||||
}
|
||||
|
||||
using var child = ImRaii.Child("##duptable", -Vector2.One, true);
|
||||
if (!child)
|
||||
return;
|
||||
|
||||
using var table = ImRaii.Table("##duplicates", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit, -Vector2.One);
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
var width = ImGui.CalcTextSize("NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN ").X;
|
||||
ImGui.TableSetupColumn("file", ImGuiTableColumnFlags.WidthStretch);
|
||||
ImGui.TableSetupColumn("size", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("NNN.NNN ").X);
|
||||
ImGui.TableSetupColumn("hash", ImGuiTableColumnFlags.WidthFixed,
|
||||
ImGui.GetWindowWidth() > 2 * width ? width : ImGui.CalcTextSize("NNNNNNNN... ").X);
|
||||
foreach (var (set, size, hash) in _editor.Duplicates.Where(s => s.Paths.Length > 1))
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
using var tree = ImRaii.TreeNode(set[0].FullName[(_mod!.ModPath.FullName.Length + 1)..],
|
||||
ImGuiTreeNodeFlags.NoTreePushOnOpen);
|
||||
ImGui.TableNextColumn();
|
||||
ImGuiUtil.RightAlign(Functions.HumanReadableSize(size));
|
||||
ImGui.TableNextColumn();
|
||||
using (var _ = ImRaii.PushFont(UiBuilder.MonoFont))
|
||||
{
|
||||
if (ImGui.GetWindowWidth() > 2 * width)
|
||||
ImGuiUtil.RightAlign(string.Concat(hash.Select(b => b.ToString("X2"))));
|
||||
else
|
||||
ImGuiUtil.RightAlign(string.Concat(hash.Take(4).Select(b => b.ToString("X2"))) + "...");
|
||||
}
|
||||
|
||||
if (!tree)
|
||||
continue;
|
||||
|
||||
using var indent = ImRaii.PushIndent();
|
||||
foreach (var duplicate in set.Skip(1))
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint);
|
||||
using var node = ImRaii.TreeNode(duplicate.FullName[(_mod!.ModPath.FullName.Length + 1)..], ImGuiTreeNodeFlags.Leaf);
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint);
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TableSetBgColor(ImGuiTableBgTarget.CellBg, Colors.RedTableBgTint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawOptionSelectHeader()
|
||||
{
|
||||
const string defaultOption = "Default Option";
|
||||
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero).Push(ImGuiStyleVar.FrameRounding, 0);
|
||||
var width = new Vector2(ImGui.GetWindowWidth() / 3, 0);
|
||||
if (ImGuiUtil.DrawDisabledButton(defaultOption, width, "Switch to the default option for the mod.\nThis resets unsaved changes.",
|
||||
_editor!.CurrentOption.IsDefault))
|
||||
_editor.SetSubMod(_mod!.Default);
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImGuiUtil.DrawDisabledButton("Refresh Data", width, "Refresh data for the current option.\nThis resets unsaved changes.", false))
|
||||
_editor.SetSubMod(_editor.CurrentOption);
|
||||
|
||||
ImGui.SameLine();
|
||||
|
||||
using var combo = ImRaii.Combo("##optionSelector", _editor.CurrentOption.FullName, ImGuiComboFlags.NoArrowButton);
|
||||
if (!combo)
|
||||
return;
|
||||
|
||||
foreach (var option in _mod!.AllSubMods)
|
||||
{
|
||||
if (ImGui.Selectable(option.FullName, option == _editor.CurrentOption))
|
||||
_editor.SetSubMod(option);
|
||||
}
|
||||
}
|
||||
|
||||
private string _newSwapKey = string.Empty;
|
||||
private string _newSwapValue = string.Empty;
|
||||
|
||||
private void DrawSwapTab()
|
||||
{
|
||||
using var tab = ImRaii.TabItem("File Swaps");
|
||||
if (!tab)
|
||||
return;
|
||||
|
||||
DrawOptionSelectHeader();
|
||||
|
||||
var setsEqual = _editor!.CurrentSwaps.SetEquals(_editor.CurrentOption.FileSwaps);
|
||||
var tt = setsEqual ? "No changes staged." : "Apply the currently staged changes to the option.";
|
||||
ImGui.NewLine();
|
||||
if (ImGuiUtil.DrawDisabledButton("Apply Changes", Vector2.Zero, tt, setsEqual))
|
||||
_editor.ApplySwaps();
|
||||
|
||||
ImGui.SameLine();
|
||||
tt = setsEqual ? "No changes staged." : "Revert all currently staged changes.";
|
||||
if (ImGuiUtil.DrawDisabledButton("Revert Changes", Vector2.Zero, tt, setsEqual))
|
||||
_editor.RevertSwaps();
|
||||
|
||||
using var child = ImRaii.Child("##swaps", -Vector2.One, true);
|
||||
if (!child)
|
||||
return;
|
||||
|
||||
using var list = ImRaii.Table("##table", 3, ImGuiTableFlags.RowBg, -Vector2.One);
|
||||
if (!list)
|
||||
return;
|
||||
|
||||
var idx = 0;
|
||||
var iconSize = ImGui.GetFrameHeight() * Vector2.One;
|
||||
var pathSize = ImGui.GetContentRegionAvail().X / 2 - iconSize.X;
|
||||
ImGui.TableSetupColumn("button", ImGuiTableColumnFlags.WidthFixed, iconSize.X);
|
||||
ImGui.TableSetupColumn("source", ImGuiTableColumnFlags.WidthFixed, pathSize);
|
||||
ImGui.TableSetupColumn("value", ImGuiTableColumnFlags.WidthFixed, pathSize);
|
||||
|
||||
foreach (var (gamePath, file) in _editor!.CurrentSwaps.ToList())
|
||||
{
|
||||
using var id = ImRaii.PushId(idx++);
|
||||
ImGui.TableNextColumn();
|
||||
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), iconSize, "Delete this swap.", false, true))
|
||||
_editor.CurrentSwaps.Remove(gamePath);
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
var tmp = gamePath.Path.ToString();
|
||||
ImGui.SetNextItemWidth(-1);
|
||||
if (ImGui.InputText("##key", ref tmp, Utf8GamePath.MaxGamePathLength)
|
||||
&& Utf8GamePath.FromString(tmp, out var path)
|
||||
&& !_editor.CurrentSwaps.ContainsKey(path))
|
||||
{
|
||||
_editor.CurrentSwaps.Remove(gamePath);
|
||||
if (path.Length > 0)
|
||||
_editor.CurrentSwaps[path] = file;
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
tmp = file.FullName;
|
||||
ImGui.SetNextItemWidth(-1);
|
||||
if (ImGui.InputText("##value", ref tmp, Utf8GamePath.MaxGamePathLength) && tmp.Length > 0)
|
||||
_editor.CurrentSwaps[gamePath] = new FullPath(tmp);
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
var addable = Utf8GamePath.FromString(_newSwapKey, out var newPath)
|
||||
&& newPath.Length > 0
|
||||
&& _newSwapValue.Length > 0
|
||||
&& _newSwapValue != _newSwapKey
|
||||
&& !_editor.CurrentSwaps.ContainsKey(newPath);
|
||||
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, "Add a new file swap to this option.", !addable,
|
||||
true))
|
||||
{
|
||||
_editor.CurrentSwaps[newPath] = new FullPath(_newSwapValue);
|
||||
_newSwapKey = string.Empty;
|
||||
_newSwapValue = string.Empty;
|
||||
}
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth(-1);
|
||||
ImGui.InputTextWithHint("##swapKey", "Load this file...", ref _newSwapValue, Utf8GamePath.MaxGamePathLength);
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth(-1);
|
||||
ImGui.InputTextWithHint("##swapValue", "... instead of this file.", ref _newSwapKey, Utf8GamePath.MaxGamePathLength);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the best matching associated file for a given path.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Tries to resolve from the current collection first and chooses the currently resolved file if any exists.
|
||||
/// If none exists, goes through all options in the currently selected mod (if any) in order of priority and resolves in them.
|
||||
/// If no redirection is found in either of those options, returns the original path.
|
||||
/// </remarks>
|
||||
private FullPath FindBestMatch(Utf8GamePath path)
|
||||
{
|
||||
var currentFile = Penumbra.CollectionManager.Current.ResolvePath(path);
|
||||
if (currentFile != null)
|
||||
return currentFile.Value;
|
||||
|
||||
if (_mod != null)
|
||||
foreach (var option in _mod.Groups.OrderByDescending(g => g.Priority)
|
||||
.SelectMany(g => g.WithIndex().OrderByDescending(o => g.OptionPriority(o.Index)).Select(g => g.Value))
|
||||
.Append(_mod.Default))
|
||||
{
|
||||
if (option.Files.TryGetValue(path, out var value) || option.FileSwaps.TryGetValue(path, out value))
|
||||
return value;
|
||||
}
|
||||
|
||||
return new FullPath(path);
|
||||
}
|
||||
|
||||
public ModEditWindow(CommunicatorService communicator, FileDialogService fileDialog)
|
||||
: base(WindowBaseLabel)
|
||||
{
|
||||
_fileDialog = fileDialog;
|
||||
_swapWindow = new ItemSwapWindow(communicator);
|
||||
_materialTab = new FileEditor<MtrlTab>("Materials", ".mtrl", _fileDialog,
|
||||
() => _editor?.MtrlFiles ?? Array.Empty<Editor.FileRegistry>(),
|
||||
DrawMaterialPanel,
|
||||
() => _mod?.ModPath.FullName ?? string.Empty,
|
||||
bytes => new MtrlTab(this, new MtrlFile(bytes)));
|
||||
_modelTab = new FileEditor<MdlFile>("Models", ".mdl", _fileDialog,
|
||||
() => _editor?.MdlFiles ?? Array.Empty<Editor.FileRegistry>(),
|
||||
DrawModelPanel,
|
||||
() => _mod?.ModPath.FullName ?? string.Empty,
|
||||
null);
|
||||
_shaderPackageTab = new FileEditor<ShpkTab>("Shader Packages", ".shpk", _fileDialog,
|
||||
() => _editor?.ShpkFiles ?? Array.Empty<Editor.FileRegistry>(),
|
||||
DrawShaderPackagePanel,
|
||||
() => _mod?.ModPath.FullName ?? string.Empty,
|
||||
null);
|
||||
_center = new CombinedTexture(_left, _right);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_editor?.Dispose();
|
||||
_left.Dispose();
|
||||
_right.Dispose();
|
||||
_center.Dispose();
|
||||
_swapWindow.Dispose();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue