Files
LightlessClient/LightlessSync/UI/DataAnalysisUi.cs
defnotken 7672f147f5
Some checks failed
Tag and Release Lightless / tag-and-release (push) Failing after 32s
1.12.0 - LightFinder + Bugfixes + Other fixes from 1.11.X (#39)
![image.png](/attachments/c984e826-254e-4ce3-af92-4e795ea717ab)

Lightless 1.12.0 is HERE! In this major update, we are introducing something we've been working on and testing for the last couple of weeks. In this update we are introducing a new (**OPTIONAL**) feature called **LightFinder**! We took inspiration from FFXIV's very own Party Finder and decided to implement something that allows users to not only look for fellow Lightless users, but also put up their Syncshell for others looking to join a sync community!

When you enable LightFinder, you will be visible to other LightFinder users for **3 hours** or when you want to disabled it. When the 3 hours are up, you can either leave it disabled or enable it again for another 3 hours. The tag shown above will show above your nameplate, and you will be able to receive pair requests in your UI from other users with LightFinder enabled without having to input their uid!

![image.png](/attachments/91a7ce60-7771-49d7-bae8-6d7a67e46fa3)

Are you at a Venue? In Limsa? Partying in the streets of Uldah? If you're looking for fellow Lightless users you can now enable LightFinder and you will be shown to others who also have LightFinder enabled!

Looking for a Syncshell to join? Enable LightFinder to see what SyncShells are available to join!

Want to advertise your Syncshell? Choose the syncshell you want to put up in LightFinder and enable LightFinder.

**IMPORTANT: We want to stress the fact that, if you just want to sync with just your friends and no one else, this does not take that away. No one will know you use Lightless unless you choose to tell them, or use this **OPTIONAL** feature. Your privacy is still maintained if you don't want to use the feature.**

# Major Changes

## LightFinder - **OPTIONAL FEATURE** - **DOES NOT AFFECT YOU IF YOU DON'T WANT TO USE IT**

![image.png](/attachments/9a1e7455-f9b3-4e3a-9ec5-5bc74ed43c26)

* New **OPTIONAL** syncing feature where one can enable something like a Party Finder so that other Lightless users in the area can see you are looking for people to sync with. Enable it by clicking the compass button and then the `Enable LightFinder` button.

![image.png](/attachments/7be5c1fc-439f-43fc-bb1e-8ae32ab5e52e)

* [L] Send Pair Request has been added to player context menus. You should still be able to send a request without Lightfinder on BUT you will need to know the other player is using Lightless and have them send a pair request back.

![image.png](/attachments/75e0d600-baf9-43d5-af37-e86b6a1e209b)

* When in LightFinder mode, for X mins you will be visible to all Lightless Users WHO ALSO HAVE LIGHTFINDER ON and will receive notifications of people wanting to pair with you. If you are the person using LightFinder, you understand the risks of pairing with someone you don't know. If you are the person sending a request to someone with LightFinder on, you also understand the risks of pairing with someone you don't know. **AGAIN, THIS IS OPTIONAL.**

* When in LightFinder mode, you can also put up a Syncshell you own on the Syncshell Finder so that others can easily find it and join. This has to be done prior to enabling LightFinder.

![image.png](/attachments/1929d8e0-778d-4ced-9bf0-f350887980db)

* Syncshell Finder allows you to join Syncshells that are indexed by LightFinder

![image.png](/attachments/fef7b786-b1a0-4967-a4a9-886cdddc1a61)

# Minor Changes

* Vanity addition to our supporters: On top of vanity ids you can find a fun addition under Your User Settings -> Edit Lightless Profile -> Vanity Settings to change the colour of your name in the lightless ui. This will be shown to all users.
![image.png](/attachments/9b950c07-6b27-4bd1-a0d4-5346396b23e2)

* Pairing nameplate colour override can also now override FC tag (new option_

* Bunch of UI fixes, updates, and changes

* kdb is now a whitelisted filetype that can upload

Co-authored-by: CakeAndBanana <admin@cakeandbanana.nl>
Co-authored-by: azyges <aaaaaa@aaa.aaa>
Co-authored-by: choco <thijmenhogenkamp@gmail.com>
Co-authored-by: choco <choco@noreply.git.lightless-sync.org>
Co-authored-by: defnotken <itsdefnotken@gmail.com>
Reviewed-on: #39
2025-10-05 07:48:32 +02:00

918 lines
48 KiB
C#

using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.API.Data.Enum;
using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services;
using LightlessSync.Services.Mediator;
using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using System.Numerics;
namespace LightlessSync.UI;
public class DataAnalysisUi : WindowMediatorSubscriberBase
{
private readonly CharacterAnalyzer _characterAnalyzer;
private readonly Progress<(string, int)> _conversionProgress = new();
private readonly IpcManager _ipcManager;
private readonly UiSharedService _uiSharedService;
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
private readonly TransientResourceManager _transientResourceManager;
private readonly TransientConfigService _transientConfigService;
private readonly Dictionary<string, string[]> _texturesToConvert = new(StringComparer.Ordinal);
private Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>? _cachedAnalysis;
private CancellationTokenSource _conversionCancellationTokenSource = new();
private string _conversionCurrentFileName = string.Empty;
private int _conversionCurrentFileProgress = 0;
private Task? _conversionTask;
private bool _enableBc7ConversionMode = false;
private bool _hasUpdate = false;
private bool _modalOpen = false;
private string _selectedFileTypeTab = string.Empty;
private string _selectedHash = string.Empty;
private ObjectKind _selectedObjectTab;
private bool _showModal = false;
private CancellationTokenSource _transientRecordCts = new();
public DataAnalysisUi(ILogger<DataAnalysisUi> logger, LightlessMediator mediator,
CharacterAnalyzer characterAnalyzer, IpcManager ipcManager,
PerformanceCollectorService performanceCollectorService, UiSharedService uiSharedService,
PlayerPerformanceConfigService playerPerformanceConfig, TransientResourceManager transientResourceManager,
TransientConfigService transientConfigService)
: base(logger, mediator, "Lightless Character Data Analysis", performanceCollectorService)
{
_characterAnalyzer = characterAnalyzer;
_ipcManager = ipcManager;
_uiSharedService = uiSharedService;
_playerPerformanceConfig = playerPerformanceConfig;
_transientResourceManager = transientResourceManager;
_transientConfigService = transientConfigService;
Mediator.Subscribe<CharacterDataAnalyzedMessage>(this, (_) =>
{
_hasUpdate = true;
});
SizeConstraints = new()
{
MinimumSize = new()
{
X = 800,
Y = 600
},
MaximumSize = new()
{
X = 3840,
Y = 2160
}
};
_conversionProgress.ProgressChanged += ConversionProgress_ProgressChanged;
}
protected override void DrawInternal()
{
if (_conversionTask != null && !_conversionTask.IsCompleted)
{
_showModal = true;
if (ImGui.BeginPopupModal("BC7 Conversion in Progress"))
{
ImGui.TextUnformatted("BC7 Conversion in progress: " + _conversionCurrentFileProgress + "/" + _texturesToConvert.Count);
UiSharedService.TextWrapped("Current file: " + _conversionCurrentFileName);
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel conversion"))
{
_conversionCancellationTokenSource.Cancel();
}
UiSharedService.SetScaledWindowSize(500);
ImGui.EndPopup();
}
else
{
_modalOpen = false;
}
}
else if (_conversionTask != null && _conversionTask.IsCompleted && _texturesToConvert.Count > 0)
{
_conversionTask = null;
_texturesToConvert.Clear();
_showModal = false;
_modalOpen = false;
_enableBc7ConversionMode = false;
}
if (_showModal && !_modalOpen)
{
ImGui.OpenPopup("BC7 Conversion in Progress");
_modalOpen = true;
}
if (_hasUpdate)
{
_cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone();
_hasUpdate = false;
}
using var tabBar = ImRaii.TabBar("analysisRecordingTabBar");
using (var tabItem = ImRaii.TabItem("Analysis"))
{
if (tabItem)
{
using var id = ImRaii.PushId("analysis");
DrawAnalysis();
}
}
using (var tabItem = ImRaii.TabItem("Transient Files"))
{
if (tabItem)
{
using var tabbar = ImRaii.TabBar("transientData");
using (var transientData = ImRaii.TabItem("Stored Transient File Data"))
{
using var id = ImRaii.PushId("data");
if (transientData)
{
DrawStoredData();
}
}
using (var transientRecord = ImRaii.TabItem("Record Transient Data"))
{
using var id = ImRaii.PushId("recording");
if (transientRecord)
{
DrawRecording();
}
}
}
}
}
private bool _showAlreadyAddedTransients = false;
private bool _acknowledgeReview = false;
private string _selectedStoredCharacter = string.Empty;
private string _selectedJobEntry = string.Empty;
private readonly List<string> _storedPathsToRemove = [];
private readonly Dictionary<string, string> _filePathResolve = [];
private string _filterGamePath = string.Empty;
private string _filterFilePath = string.Empty;
private void DrawStoredData()
{
UiSharedService.DrawTree("What is this? (Explanation / Help)", () =>
{
UiSharedService.TextWrapped("This tab allows you to see which transient files are attached to your character.");
UiSharedService.TextWrapped("Transient files are files that cannot be resolved to your character permanently. Lightless gathers these files in the background while you execute animations, VFX, sound effects, etc.");
UiSharedService.TextWrapped("When sending your character data to others, Lightless will combine the files listed in \"All Jobs\" and the corresponding currently used job.");
UiSharedService.TextWrapped("The purpose of this tab is primarily informational for you to see which files you are carrying with you. You can remove added game paths, however if you are using the animations etc. again, "
+ "Lightless will automatically attach these after using them. If you disable associated mods in Penumbra, the associated entries here will also be deleted automatically.");
});
ImGuiHelpers.ScaledDummy(5);
var config = _transientConfigService.Current.TransientConfigs;
Vector2 availableContentRegion = Vector2.Zero;
using (ImRaii.Group())
{
ImGui.TextUnformatted("Character");
ImGui.Separator();
ImGuiHelpers.ScaledDummy(3);
availableContentRegion = ImGui.GetContentRegionAvail();
using (ImRaii.ListBox("##characters", new Vector2(200, availableContentRegion.Y)))
{
foreach (var entry in config)
{
var name = entry.Key.Split("_");
if (!_uiSharedService.WorldData.TryGetValue(ushort.Parse(name[1]), out var worldname))
{
continue;
}
if (ImGui.Selectable(name[0] + " (" + worldname + ")", string.Equals(_selectedStoredCharacter, entry.Key, StringComparison.Ordinal)))
{
_selectedStoredCharacter = entry.Key;
_selectedJobEntry = string.Empty;
_storedPathsToRemove.Clear();
_filePathResolve.Clear();
_filterFilePath = string.Empty;
_filterGamePath = string.Empty;
}
}
}
}
ImGui.SameLine();
bool selectedData = config.TryGetValue(_selectedStoredCharacter, out var transientStorage) && transientStorage != null;
using (ImRaii.Group())
{
ImGui.TextUnformatted("Job");
ImGui.Separator();
ImGuiHelpers.ScaledDummy(3);
using (ImRaii.ListBox("##data", new Vector2(150, availableContentRegion.Y)))
{
if (selectedData)
{
if (ImGui.Selectable("All Jobs", string.Equals(_selectedJobEntry, "alljobs", StringComparison.Ordinal)))
{
_selectedJobEntry = "alljobs";
}
foreach (var job in transientStorage!.JobSpecificCache)
{
if (!_uiSharedService.JobData.TryGetValue(job.Key, out var jobName)) continue;
if (ImGui.Selectable(jobName, string.Equals(_selectedJobEntry, job.Key.ToString(), StringComparison.Ordinal)))
{
_selectedJobEntry = job.Key.ToString();
_storedPathsToRemove.Clear();
_filePathResolve.Clear();
_filterFilePath = string.Empty;
_filterGamePath = string.Empty;
}
}
}
}
}
ImGui.SameLine();
using (ImRaii.Group())
{
var selectedList = string.Equals(_selectedJobEntry, "alljobs", StringComparison.Ordinal)
? config[_selectedStoredCharacter].GlobalPersistentCache
: (string.IsNullOrEmpty(_selectedJobEntry) ? [] : config[_selectedStoredCharacter].JobSpecificCache[uint.Parse(_selectedJobEntry)]);
ImGui.TextUnformatted($"Attached Files (Total Files: {selectedList.Count})");
ImGui.Separator();
ImGuiHelpers.ScaledDummy(3);
using (ImRaii.Disabled(string.IsNullOrEmpty(_selectedJobEntry)))
{
var restContent = availableContentRegion.X - ImGui.GetCursorPosX();
using var group = ImRaii.Group();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.ArrowRight, "Resolve Game Paths to used File Paths"))
{
_ = Task.Run(async () =>
{
var paths = selectedList.ToArray();
var resolved = await _ipcManager.Penumbra.ResolvePathsAsync(paths, []).ConfigureAwait(false);
_filePathResolve.Clear();
for (int i = 0; i < resolved.forward.Length; i++)
{
_filePathResolve[paths[i]] = resolved.forward[i];
}
});
}
ImGui.SameLine();
ImGuiHelpers.ScaledDummy(20, 1);
ImGui.SameLine();
using (ImRaii.Disabled(!_storedPathsToRemove.Any()))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Remove selected Game Paths"))
{
foreach (var item in _storedPathsToRemove)
{
selectedList.Remove(item);
}
_transientConfigService.Save();
_transientResourceManager.RebuildSemiTransientResources();
_filterFilePath = string.Empty;
_filterGamePath = string.Empty;
}
}
ImGui.SameLine();
using (ImRaii.Disabled(!UiSharedService.CtrlPressed()))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Trash, "Clear ALL Game Paths"))
{
selectedList.Clear();
_transientConfigService.Save();
_transientResourceManager.RebuildSemiTransientResources();
_filterFilePath = string.Empty;
_filterGamePath = string.Empty;
}
}
UiSharedService.AttachToolTip("Hold CTRL to delete all game paths from the displayed list"
+ UiSharedService.TooltipSeparator + "You usually do not need to do this. All animation and VFX data will be automatically handled through Lightless.");
ImGuiHelpers.ScaledDummy(5);
ImGuiHelpers.ScaledDummy(30);
ImGui.SameLine();
ImGui.SetNextItemWidth((restContent - 30) / 2f);
ImGui.InputTextWithHint("##filterGamePath", "Filter by Game Path", ref _filterGamePath, 255);
ImGui.SameLine();
ImGui.SetNextItemWidth((restContent - 30) / 2f);
ImGui.InputTextWithHint("##filterFilePath", "Filter by File Path", ref _filterFilePath, 255);
using (var dataTable = ImRaii.Table("##table", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.ScrollY | ImGuiTableFlags.RowBg))
{
if (dataTable)
{
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 30);
ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthFixed, (restContent - 30) / 2f);
ImGui.TableSetupColumn("File Path", ImGuiTableColumnFlags.WidthFixed, (restContent - 30) / 2f);
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableHeadersRow();
int id = 0;
foreach (var entry in selectedList)
{
if (!string.IsNullOrWhiteSpace(_filterGamePath) && !entry.Contains(_filterGamePath, StringComparison.OrdinalIgnoreCase))
continue;
bool hasFileResolve = _filePathResolve.TryGetValue(entry, out var filePath);
if (hasFileResolve && !string.IsNullOrEmpty(_filterFilePath) && !filePath!.Contains(_filterFilePath, StringComparison.OrdinalIgnoreCase))
continue;
using var imguiid = ImRaii.PushId(id++);
ImGui.TableNextColumn();
bool isSelected = _storedPathsToRemove.Contains(entry, StringComparer.Ordinal);
if (ImGui.Checkbox("##", ref isSelected))
{
if (isSelected)
_storedPathsToRemove.Add(entry);
else
_storedPathsToRemove.Remove(entry);
}
ImGui.TableNextColumn();
ImGui.TextUnformatted(entry);
UiSharedService.AttachToolTip(entry + UiSharedService.TooltipSeparator + "Click to copy to clipboard");
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
ImGui.SetClipboardText(entry);
}
ImGui.TableNextColumn();
if (hasFileResolve)
{
ImGui.TextUnformatted(filePath ?? "Unk");
UiSharedService.AttachToolTip(filePath ?? "Unk" + UiSharedService.TooltipSeparator + "Click to copy to clipboard");
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
ImGui.SetClipboardText(filePath);
}
}
else
{
ImGui.TextUnformatted("-");
UiSharedService.AttachToolTip("Resolve Game Paths to used File Paths to display the associated file paths.");
}
}
}
}
}
}
}
private void DrawRecording()
{
UiSharedService.DrawTree("What is this? (Explanation / Help)", () =>
{
UiSharedService.TextWrapped("This tab allows you to attempt to fix mods that do not sync correctly, especially those with modded models and animations." + Environment.NewLine + Environment.NewLine
+ "To use this, start the recording, execute one or multiple emotes/animations you want to attempt to fix and check if new data appears in the table below." + Environment.NewLine
+ "If it doesn't, Lightless is not able to catch the data or already has recorded the animation files (check 'Show previously added transient files' to see if not all is already present)." + Environment.NewLine + Environment.NewLine
+ "For most animations, vfx, etc. it is enough to just run them once unless they have random variations. Longer animations do not require to play out in their entirety to be captured.");
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("Important Note: If you need to fix an animation that should apply across multiple jobs, you need to repeat this process with at least one additional job, " +
"otherwise the animation will only be fixed for the currently active job. This goes primarily for emotes that are used across multiple jobs.",
UIColors.Get("LightlessYellow"), 800);
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("WARNING: WHILE RECORDING TRANSIENT DATA, DO NOT CHANGE YOUR APPEARANCE, ENABLED MODS OR ANYTHING. JUST DO THE ANIMATION(S) OR WHATEVER YOU NEED DOING AND STOP THE RECORDING.",
ImGuiColors.DalamudRed, 800);
ImGuiHelpers.ScaledDummy(5);
});
using (ImRaii.Disabled(_transientResourceManager.IsTransientRecording))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Play, "Start Transient Recording"))
{
_transientRecordCts.Cancel();
_transientRecordCts.Dispose();
_transientRecordCts = new();
_transientResourceManager.StartRecording(_transientRecordCts.Token);
_acknowledgeReview = false;
}
}
ImGui.SameLine();
using (ImRaii.Disabled(!_transientResourceManager.IsTransientRecording))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Stop, "Stop Transient Recording"))
{
_transientRecordCts.Cancel();
}
}
if (_transientResourceManager.IsTransientRecording)
{
ImGui.SameLine();
UiSharedService.ColorText($"RECORDING - Time Remaining: {_transientResourceManager.RecordTimeRemaining.Value}", UIColors.Get("LightlessYellow"));
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("DO NOT CHANGE YOUR APPEARANCE OR MODS WHILE RECORDING, YOU CAN ACCIDENTALLY MAKE SOME OF YOUR APPEARANCE RELATED MODS PERMANENT.", ImGuiColors.DalamudRed, 800);
}
ImGuiHelpers.ScaledDummy(5);
ImGui.Checkbox("Show previously added transient files in the recording", ref _showAlreadyAddedTransients);
_uiSharedService.DrawHelpText("Use this only if you want to see what was previously already caught by Lightless");
ImGuiHelpers.ScaledDummy(5);
using (ImRaii.Disabled(_transientResourceManager.IsTransientRecording || _transientResourceManager.RecordedTransients.All(k => !k.AddTransient) || !_acknowledgeReview))
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Save, "Save Recorded Transient Data"))
{
_transientResourceManager.SaveRecording();
_acknowledgeReview = false;
}
}
ImGui.SameLine();
ImGui.Checkbox("I acknowledge I have reviewed the recorded data", ref _acknowledgeReview);
if (_transientResourceManager.RecordedTransients.Any(k => !k.AlreadyTransient))
{
ImGuiHelpers.ScaledDummy(5);
UiSharedService.DrawGroupedCenteredColorText("Please review the recorded mod files before saving and deselect files that got into the recording on accident.", UIColors.Get("LightlessYellow"));
ImGuiHelpers.ScaledDummy(5);
}
ImGuiHelpers.ScaledDummy(5);
var width = ImGui.GetContentRegionAvail();
using var table = ImRaii.Table("Recorded Transients", 4, ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (table)
{
int id = 0;
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 30);
ImGui.TableSetupColumn("Owner", ImGuiTableColumnFlags.WidthFixed, 100);
ImGui.TableSetupColumn("Game Path", ImGuiTableColumnFlags.WidthFixed, (width.X - 30 - 100) / 2f);
ImGui.TableSetupColumn("File Path", ImGuiTableColumnFlags.WidthFixed, (width.X - 30 - 100) / 2f);
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableHeadersRow();
var transients = _transientResourceManager.RecordedTransients.ToList();
transients.Reverse();
foreach (var value in transients)
{
if (value.AlreadyTransient && !_showAlreadyAddedTransients)
continue;
using var imguiid = ImRaii.PushId(id++);
if (value.AlreadyTransient)
{
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudGrey);
}
ImGui.TableNextColumn();
bool addTransient = value.AddTransient;
if (ImGui.Checkbox("##add", ref addTransient))
{
value.AddTransient = addTransient;
}
ImGui.TableNextColumn();
ImGui.TextUnformatted(value.Owner.Name);
ImGui.TableNextColumn();
ImGui.TextUnformatted(value.GamePath);
UiSharedService.AttachToolTip(value.GamePath);
ImGui.TableNextColumn();
ImGui.TextUnformatted(value.FilePath);
UiSharedService.AttachToolTip(value.FilePath);
if (value.AlreadyTransient)
{
ImGui.PopStyleColor();
}
}
}
}
private void DrawAnalysis()
{
UiSharedService.DrawTree("What is this? (Explanation / Help)", () =>
{
UiSharedService.TextWrapped("This tab shows you all files and their sizes that are currently in use through your character and associated entities in Lightless");
});
if (_cachedAnalysis!.Count == 0) return;
bool isAnalyzing = _characterAnalyzer.IsAnalysisRunning;
if (isAnalyzing)
{
UiSharedService.ColorTextWrapped($"Analyzing {_characterAnalyzer.CurrentFile}/{_characterAnalyzer.TotalFiles}",
UIColors.Get("LightlessYellow"));
if (_uiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis"))
{
_characterAnalyzer.CancelAnalyze();
}
}
else
{
if (_cachedAnalysis!.Any(c => c.Value.Any(f => !f.Value.IsComputed)))
{
UiSharedService.ColorTextWrapped("Some entries in the analysis have file size not determined yet, press the button below to analyze your current data",
UIColors.Get("LightlessYellow"));
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (missing entries)"))
{
_ = _characterAnalyzer.ComputeAnalysis(print: false);
}
}
else
{
if (_uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (recalculate all entries)"))
{
_ = _characterAnalyzer.ComputeAnalysis(print: false, recalculate: true);
}
}
}
ImGui.Separator();
ImGui.TextUnformatted("Total files:");
ImGui.SameLine();
ImGui.TextUnformatted(_cachedAnalysis!.Values.Sum(c => c.Values.Count).ToString());
ImGui.SameLine();
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString());
}
if (ImGui.IsItemHovered())
{
string text = "";
var groupedfiles = _cachedAnalysis.Values.SelectMany(f => f.Values).GroupBy(f => f.FileType, StringComparer.Ordinal);
text = string.Join(Environment.NewLine, groupedfiles.OrderBy(f => f.Key, StringComparer.Ordinal)
.Select(f => f.Key + ": " + f.Count() + " files, size: " + UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))
+ ", compressed: " + UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))));
ImGui.SetTooltip(text);
}
ImGui.TextUnformatted("Total size (actual):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.OriginalSize))));
ImGui.TextUnformatted("Total size (compressed for up/download only):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(_cachedAnalysis!.Sum(c => c.Value.Sum(c => c.Value.CompressedSize))));
ImGui.TextUnformatted($"Total modded model triangles: {_cachedAnalysis.Sum(c => c.Value.Sum(f => f.Value.Triangles))}");
ImGui.Separator();
using var tabbar = ImRaii.TabBar("objectSelection");
foreach (var kvp in _cachedAnalysis)
{
using var id = ImRaii.PushId(kvp.Key.ToString());
string tabText = kvp.Key.ToString();
if (kvp.Value.Any(f => !f.Value.IsComputed)) tabText += " (!)";
using var tab = ImRaii.TabItem(tabText + "###" + kvp.Key.ToString());
if (tab.Success)
{
var groupedfiles = kvp.Value.Select(v => v.Value).GroupBy(f => f.FileType, StringComparer.Ordinal).OrderBy(k => k.Key, StringComparer.Ordinal).ToList();
ImGui.PushStyleVar(ImGuiStyleVar.CellPadding, new Vector2(1f, 1f));
ImGui.PushStyleVar(ImGuiStyleVar.ItemSpacing, new Vector2(1f, 1f));
if (ImGui.BeginTable($"##fileStats_{kvp.Key}", 3,
ImGuiTableFlags.BordersInnerV | ImGuiTableFlags.SizingFixedFit))
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted($"Files for {kvp.Key}");
ImGui.TableNextColumn();
ImGui.TextUnformatted(kvp.Value.Count.ToString());
ImGui.SameLine();
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
ImGui.TextUnformatted(FontAwesomeIcon.InfoCircle.ToIconString());
if (ImGui.IsItemHovered())
{
string text = string.Join(Environment.NewLine, groupedfiles.Select(f =>
$"{f.Key}: {f.Count()} files, size: {UiSharedService.ByteToString(f.Sum(v => v.OriginalSize))}, compressed: {UiSharedService.ByteToString(f.Sum(v => v.CompressedSize))}"));
ImGui.SetTooltip(text);
}
ImGui.TableNextColumn();
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{kvp.Key} size (actual):");
ImGui.TableNextColumn();
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.OriginalSize)));
ImGui.TableNextColumn();
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{kvp.Key} size (compressed for up/download only):");
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TableNextColumn();
ImGui.TextUnformatted(UiSharedService.ByteToString(kvp.Value.Sum(c => c.Value.CompressedSize)));
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurpleDefault"), 1.5f);
ImGui.TableNextColumn();
var vramUsage = groupedfiles.SingleOrDefault(v => string.Equals(v.Key, "tex", StringComparison.Ordinal));
if (vramUsage != null)
{
var actualVramUsage = vramUsage.Sum(f => f.OriginalSize);
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{kvp.Key} VRAM usage:");
ImGui.TableNextColumn();
ImGui.TextUnformatted(UiSharedService.ByteToString(actualVramUsage));
ImGui.TableNextColumn();
if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds
|| _playerPerformanceConfig.Current.ShowPerformanceIndicator)
{
var currentVramWarning = _playerPerformanceConfig.Current.VRAMSizeWarningThresholdMiB;
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted("Configured VRAM threshold:");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{currentVramWarning} MiB.");
ImGui.TableNextColumn();
if (currentVramWarning * 1024 * 1024 < actualVramUsage)
{
UiSharedService.ColorText(
$"You exceed your own threshold by {UiSharedService.ByteToString(actualVramUsage - (currentVramWarning * 1024 * 1024))}",
UIColors.Get("LightlessYellow"));
}
}
}
var actualTriCount = kvp.Value.Sum(f => f.Value.Triangles);
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{kvp.Key} modded model triangles:");
ImGui.TableNextColumn();
ImGui.TextUnformatted(actualTriCount.ToString());
ImGui.TableNextColumn();
if (_playerPerformanceConfig.Current.WarnOnExceedingThresholds
|| _playerPerformanceConfig.Current.ShowPerformanceIndicator)
{
var currentTriWarning = _playerPerformanceConfig.Current.TrisWarningThresholdThousands;
ImGui.TableNextRow();
ImGui.TableNextColumn();
ImGui.TextUnformatted("Configured triangle threshold:");
ImGui.TableNextColumn();
ImGui.TextUnformatted($"{currentTriWarning * 1000} triangles.");
ImGui.TableNextColumn();
if (currentTriWarning * 1000 < actualTriCount)
{
UiSharedService.ColorText(
$"You exceed your own threshold by {actualTriCount - (currentTriWarning * 1000)}",
UIColors.Get("LightlessYellow"));
}
}
ImGui.EndTable();
}
ImGui.PopStyleVar(2);
_uiSharedService.ColoredSeparator(UIColors.Get("LightlessPurple"), 2f);
_uiSharedService.MediumText("Selected file:", UIColors.Get("LightlessBlue"));
ImGui.SameLine();
_uiSharedService.MediumText(_selectedHash, UIColors.Get("LightlessYellow"));
if (_cachedAnalysis[_selectedObjectTab].TryGetValue(_selectedHash, out CharacterAnalyzer.FileDataEntry? item))
{
var filePaths = item.FilePaths;
UiSharedService.ColorText("Local file path:", UIColors.Get("LightlessBlue"));
ImGui.SameLine();
UiSharedService.TextWrapped(filePaths[0]);
if (filePaths.Count > 1)
{
ImGui.SameLine();
ImGui.TextUnformatted($"(and {filePaths.Count - 1} more)");
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, filePaths.Skip(1)));
}
var gamepaths = item.GamePaths;
UiSharedService.ColorText("Used by game path:", UIColors.Get("LightlessBlue"));
ImGui.SameLine();
UiSharedService.TextWrapped(gamepaths[0]);
if (gamepaths.Count > 1)
{
ImGui.SameLine();
ImGui.TextUnformatted($"(and {gamepaths.Count - 1} more)");
ImGui.SameLine();
_uiSharedService.IconText(FontAwesomeIcon.InfoCircle);
UiSharedService.AttachToolTip(string.Join(Environment.NewLine, gamepaths.Skip(1)));
}
}
ImGui.Separator();
if (_selectedObjectTab != kvp.Key)
{
_selectedHash = string.Empty;
_selectedObjectTab = kvp.Key;
_selectedFileTypeTab = string.Empty;
_enableBc7ConversionMode = false;
_texturesToConvert.Clear();
}
using var fileTabBar = ImRaii.TabBar("fileTabs");
foreach (IGrouping<string, CharacterAnalyzer.FileDataEntry>? fileGroup in groupedfiles)
{
string fileGroupText = fileGroup.Key + " [" + fileGroup.Count() + "]";
var requiresCompute = fileGroup.Any(k => !k.IsComputed);
using var tabcol = ImRaii.PushColor(ImGuiCol.Tab, UiSharedService.Color(UIColors.Get("LightlessYellow")), requiresCompute);
if (requiresCompute)
{
fileGroupText += " (!)";
}
ImRaii.IEndObject fileTab;
using (var textcol = ImRaii.PushColor(ImGuiCol.Text, UiSharedService.Color(new(0, 0, 0, 1)),
requiresCompute && !string.Equals(_selectedFileTypeTab, fileGroup.Key, StringComparison.Ordinal)))
{
fileTab = ImRaii.TabItem(fileGroupText + "###" + fileGroup.Key);
}
if (!fileTab) { fileTab.Dispose(); continue; }
if (!string.Equals(fileGroup.Key, _selectedFileTypeTab, StringComparison.Ordinal))
{
_selectedFileTypeTab = fileGroup.Key;
_selectedHash = string.Empty;
_enableBc7ConversionMode = false;
_texturesToConvert.Clear();
}
ImGui.TextUnformatted($"{fileGroup.Key} files");
ImGui.SameLine();
ImGui.TextUnformatted(fileGroup.Count().ToString());
ImGui.TextUnformatted($"{fileGroup.Key} files size (actual):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.OriginalSize)));
ImGui.TextUnformatted($"{fileGroup.Key} files size (compressed for up/download only):");
ImGui.SameLine();
ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.CompressedSize)));
if (string.Equals(_selectedFileTypeTab, "tex", StringComparison.Ordinal))
{
ImGui.Checkbox("Enable BC7 Conversion Mode", ref _enableBc7ConversionMode);
if (_enableBc7ConversionMode)
{
UiSharedService.ColorText("WARNING BC7 CONVERSION:", UIColors.Get("LightlessYellow"));
ImGui.SameLine();
UiSharedService.ColorText("Converting textures to BC7 is irreversible!", ImGuiColors.DalamudRed);
UiSharedService.ColorTextWrapped("- Converting textures to BC7 will reduce their size (compressed and uncompressed) drastically. It is recommended to be used for large (4k+) textures." +
Environment.NewLine + "- Some textures, especially ones utilizing colorsets, might not be suited for BC7 conversion and might produce visual artifacts." +
Environment.NewLine + "- Before converting textures, make sure to have the original files of the mod you are converting so you can reimport it in case of issues." +
Environment.NewLine + "- Conversion will convert all found texture duplicates (entries with more than 1 file path) automatically." +
Environment.NewLine + "- Converting textures to BC7 is a very expensive operation and, depending on the amount of textures to convert, will take a while to complete."
, UIColors.Get("LightlessYellow"));
if (_texturesToConvert.Count > 0 && _uiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start conversion of " + _texturesToConvert.Count + " texture(s)"))
{
_conversionCancellationTokenSource = _conversionCancellationTokenSource.CancelRecreate();
_conversionTask = _ipcManager.Penumbra.ConvertTextureFiles(_logger, _texturesToConvert, _conversionProgress, _conversionCancellationTokenSource.Token);
}
}
}
ImGui.Separator();
DrawTable(fileGroup);
fileTab.Dispose();
}
}
}
}
public override void OnOpen()
{
_hasUpdate = true;
_selectedHash = string.Empty;
_enableBc7ConversionMode = false;
_texturesToConvert.Clear();
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_conversionProgress.ProgressChanged -= ConversionProgress_ProgressChanged;
}
private void ConversionProgress_ProgressChanged(object? sender, (string, int) e)
{
_conversionCurrentFileName = e.Item1;
_conversionCurrentFileProgress = e.Item2;
}
private void DrawTable(IGrouping<string, CharacterAnalyzer.FileDataEntry> fileGroup)
{
var tableColumns = string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal)
? (_enableBc7ConversionMode ? 7 : 6)
: (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) ? 6 : 5);
using var table = ImRaii.Table("Analysis", tableColumns, ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit,
new Vector2(0, 300));
if (!table.Success) return;
ImGui.TableSetupColumn("Hash");
ImGui.TableSetupColumn("Filepaths");
ImGui.TableSetupColumn("Gamepaths");
ImGui.TableSetupColumn("Original Size");
ImGui.TableSetupColumn("Compressed Size");
if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal))
{
ImGui.TableSetupColumn("Format");
if (_enableBc7ConversionMode) ImGui.TableSetupColumn("Convert to BC7");
}
if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal))
{
ImGui.TableSetupColumn("Triangles");
}
ImGui.TableSetupScrollFreeze(0, 1);
ImGui.TableHeadersRow();
var sortSpecs = ImGui.TableGetSortSpecs();
if (sortSpecs.SpecsDirty)
{
var idx = sortSpecs.Specs.ColumnIndex;
if (idx == 0 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Key, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 0 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Key, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 1 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.FilePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 1 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.FilePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 2 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.GamePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 2 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.GamePaths.Count).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 3 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.OriginalSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 3 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.OriginalSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 4 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.CompressedSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (idx == 4 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.CompressedSize).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.Triangles).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal) && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.Triangles).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal) && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Ascending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderBy(k => k.Value.Format.Value, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal) && idx == 5 && sortSpecs.Specs.SortDirection == ImGuiSortDirection.Descending)
_cachedAnalysis![_selectedObjectTab] = _cachedAnalysis[_selectedObjectTab].OrderByDescending(k => k.Value.Format.Value, StringComparer.Ordinal).ToDictionary(d => d.Key, d => d.Value, StringComparer.Ordinal);
sortSpecs.SpecsDirty = false;
}
foreach (var item in fileGroup)
{
using var text = ImRaii.PushColor(ImGuiCol.Text, new Vector4(0, 0, 0, 1), string.Equals(item.Hash, _selectedHash, StringComparison.Ordinal));
using var text2 = ImRaii.PushColor(ImGuiCol.Text, new Vector4(1, 1, 1, 1), !item.IsComputed);
ImGui.TableNextColumn();
if (!item.IsComputed)
{
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, UiSharedService.Color(ImGuiColors.DalamudRed));
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, UiSharedService.Color(ImGuiColors.DalamudRed));
}
if (string.Equals(_selectedHash, item.Hash, StringComparison.Ordinal))
{
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg1, UiSharedService.Color(UIColors.Get("LightlessYellow")));
ImGui.TableSetBgColor(ImGuiTableBgTarget.RowBg0, UiSharedService.Color(UIColors.Get("LightlessYellow")));
}
ImGui.TextUnformatted(item.Hash);
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
ImGui.TableNextColumn();
ImGui.TextUnformatted(item.FilePaths.Count.ToString());
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
ImGui.TableNextColumn();
ImGui.TextUnformatted(item.GamePaths.Count.ToString());
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
ImGui.TableNextColumn();
ImGui.TextUnformatted(UiSharedService.ByteToString(item.OriginalSize));
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
ImGui.TableNextColumn();
ImGui.TextUnformatted(UiSharedService.ByteToString(item.CompressedSize));
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
if (string.Equals(fileGroup.Key, "tex", StringComparison.Ordinal))
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(item.Format.Value);
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
if (_enableBc7ConversionMode)
{
ImGui.TableNextColumn();
if (string.Equals(item.Format.Value, "BC7", StringComparison.Ordinal))
{
ImGui.TextUnformatted("");
continue;
}
var filePath = item.FilePaths[0];
bool toConvert = _texturesToConvert.ContainsKey(filePath);
if (UiSharedService.CheckboxWithBorder("###convert" + item.Hash, ref toConvert, UIColors.Get("LightlessPurple"), 1.5f))
{
if (toConvert && !_texturesToConvert.ContainsKey(filePath))
{
_texturesToConvert[filePath] = item.FilePaths.Skip(1).ToArray();
}
else if (!toConvert && _texturesToConvert.ContainsKey(filePath))
{
_texturesToConvert.Remove(filePath);
}
}
}
}
if (string.Equals(fileGroup.Key, "mdl", StringComparison.Ordinal))
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(item.Triangles.ToString());
if (ImGui.IsItemClicked()) _selectedHash = item.Hash;
}
}
}
}