From 64bcfaccace7a31093a04098cba0381780cba335 Mon Sep 17 00:00:00 2001 From: Detective Squirrel <2137769+DetectiveSquirrel@users.noreply.github.com> Date: Fri, 20 Mar 2026 15:49:49 +1300 Subject: [PATCH 1/2] split atlas from character tree and included needed menu updates --- ESkillTreeType.cs | 2 +- ImGuiExtension.cs | 7 +- PassiveSkillTreePlanter.cs | 615 +++++++++---------------- PassiveSkillTreePlanter.csproj | 6 +- PassiveSkillTreePlanterSettings.cs | 20 +- PassiveSkillTreePlanterSettingsMenu.cs | 485 +++++++++++++++++++ SkillNode.cs | 48 +- TreeConfig.cs | 13 +- TreeEncoder.cs | 9 +- TreeGraph/DisjointSet.cs | 2 +- UrlDecoders/PathOfExileUrlDecoder.cs | 2 +- UrlDecoders/PoePlannerUrlDecoder.cs | 2 +- UrlImporters/BaseUrlImporter.cs | 3 +- UrlImporters/PobCodeImporter.cs | 2 +- UrlImporters/PobHelpers.cs | 2 +- 15 files changed, 752 insertions(+), 466 deletions(-) create mode 100644 PassiveSkillTreePlanterSettingsMenu.cs diff --git a/ESkillTreeType.cs b/ESkillTreeType.cs index 57287ae..a6d5815 100644 --- a/ESkillTreeType.cs +++ b/ESkillTreeType.cs @@ -4,5 +4,5 @@ public enum ESkillTreeType { Unknown, Character, - Atlas, + Atlas } \ No newline at end of file diff --git a/ImGuiExtension.cs b/ImGuiExtension.cs index 590cd1e..90d21ef 100644 --- a/ImGuiExtension.cs +++ b/ImGuiExtension.cs @@ -1,12 +1,11 @@ -using System.Collections.Generic; -using ImGuiNET; +using ImGuiNET; +using System.Collections.Generic; namespace PassiveSkillTreePlanter; public class ImGuiExtension { - public static string ComboBox(string sideLabel, string currentSelectedItem, List objectList, out bool didChange, - ImGuiComboFlags comboFlags = ImGuiComboFlags.HeightRegular) + public static string ComboBox(string sideLabel, string currentSelectedItem, List objectList, out bool didChange, ImGuiComboFlags comboFlags = ImGuiComboFlags.HeightRegular) { if (ImGui.BeginCombo(sideLabel, currentSelectedItem, comboFlags)) { diff --git a/PassiveSkillTreePlanter.cs b/PassiveSkillTreePlanter.cs index 8610189..bf91ea3 100644 --- a/PassiveSkillTreePlanter.cs +++ b/PassiveSkillTreePlanter.cs @@ -1,24 +1,21 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using System.Windows.Forms; -using System.Xml.Linq; using ExileCore; using ExileCore.PoEMemory.Elements; using ExileCore.Shared; using ExileCore.Shared.AtlasHelper; -using ExileCore.Shared.Enums; using ExileCore.Shared.Helpers; using ImGuiNET; using PassiveSkillTreePlanter.SkillTreeJson; using PassiveSkillTreePlanter.TreeGraph; -using PassiveSkillTreePlanter.UrlDecoders; using PassiveSkillTreePlanter.UrlImporters; using SharpDX; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; using Vector2 = System.Numerics.Vector2; namespace PassiveSkillTreePlanter; @@ -27,35 +24,53 @@ public class PassiveSkillTreePlanter : BaseSettingsPlugin _importers = - [ - new MaxrollTreeImporter(), - new PobbinTreeImporter(), - new PobCodeImporter(), - ]; - - //List of nodes decoded from URL - private HashSet _characterUrlNodeIds = new HashSet(); - private HashSet _atlasUrlNodeIds = new HashSet(); - - private int _selectedSettingsTab; - private string _addNewBuildFile = ""; - private string _buildNameEditorValue; - private AtlasTexture _ringImage; + private readonly PoESkillTreeJsonDecoder _atlasTreeData = new(); + + private readonly List _importers = [new MaxrollTreeImporter(), new PobbinTreeImporter(), new PobCodeImporter()]; + + private readonly Dictionary _nodeMap = new(); + + private readonly PoESkillTreeJsonDecoder _skillTreeData = new(); + private HashSet _atlasUrlNodeIds = new(); + + private HashSet _characterUrlNodeIds = new(); private SyncTask _currentTask; + private bool _editorShown; - private List BuildFiles { get; set; } = new List(); + private int _officialDownloadBusy; + private Task> _pathingNodes; + private volatile bool _pendingReloadGameTreeData; + private AtlasTexture _ringImage; - public string SkillTreeUrlFilesDir => Directory.CreateDirectory(Path.Join(ConfigDirectory, SkillTreeDir)).FullName; + private PassiveSkillTreePlanterSettingsMenu _settingsMenu; + internal string BuildNameEditorValue = ""; + + public PassiveSkillTreePlanter() + { + Name = "Passive Skill Tree Planner"; + } - private TreeConfig.SkillTreeData _selectedBuildData = new TreeConfig.SkillTreeData(); + internal List BuildFiles { get; private set; } = new(); + + internal TreeConfig.SkillTreeData SelectedBuildData { get; private set; } = new(); + + internal TreeConfig.SkillTreeData AtlasBuildData { get; private set; } = new(); + + internal TreeConfig.SkillTreeData EditBuildData { get; private set; } = new(); + + internal IReadOnlyList UrlImporters => _importers; + + public string SkillTreeUrlFilesDir => Directory.CreateDirectory(Path.Join(ConfigDirectory, SkillTreeDir)).FullName; public override void OnLoad() { + _settingsMenu = new PassiveSkillTreePlanterSettingsMenu(this); _ringImage = GetAtlasTexture("AtlasMapCircle"); Graphics.InitImage("Icons.png"); ReloadGameTreeData(); @@ -66,25 +81,76 @@ public override void OnLoad() } LoadBuild(Settings.SelectedBuild); + if (string.IsNullOrWhiteSpace(Settings.SelectedAtlasBuild)) + { + Settings.SelectedAtlasBuild = Settings.SelectedBuild; + } + + LoadAtlasBuild(Settings.SelectedAtlasBuild); + if (string.IsNullOrWhiteSpace(Settings.SelectedEditBuild)) + { + Settings.SelectedEditBuild = Settings.SelectedBuild; + } + + LoadEditBuild(Settings.SelectedEditBuild); LoadUrl(Settings.LastSelectedAtlasUrl); LoadUrl(Settings.LastSelectedCharacterUrl); } - private void ReloadBuildList() + internal void ReloadBuildList() { BuildFiles = TreeConfig.GetBuilds(SkillTreeUrlFilesDir); } - private void LoadBuild(string buildName) + internal void LoadBuild(string buildName) { Settings.SelectedBuild = buildName; - _selectedBuildData = TreeConfig.LoadBuild(SkillTreeUrlFilesDir, Settings.SelectedBuild) ?? new TreeConfig.SkillTreeData(); + SelectedBuildData = TreeConfig.LoadBuild(SkillTreeUrlFilesDir, Settings.SelectedBuild) ?? new TreeConfig.SkillTreeData(); _characterUrlNodeIds = new HashSet(); + SyncAtlasBuildDataIfSameFile(); + } + + internal void LoadEditBuild(string buildName) + { + if (BuildFiles.Count == 0) + { + ReloadBuildList(); + } + + if (BuildFiles.Count > 0 && (string.IsNullOrWhiteSpace(buildName) || !BuildFiles.Contains(buildName))) + { + buildName = BuildFiles.Contains(Settings.SelectedBuild) ? Settings.SelectedBuild : BuildFiles[0]; + } + + Settings.SelectedEditBuild = buildName ?? string.Empty; + EditBuildData = string.IsNullOrWhiteSpace(buildName) ? new TreeConfig.SkillTreeData() : TreeConfig.LoadBuild(SkillTreeUrlFilesDir, buildName) ?? new TreeConfig.SkillTreeData(); + BuildNameEditorValue = Settings.SelectedEditBuild; + } + + internal void LoadAtlasBuild(string buildName) + { + Settings.SelectedAtlasBuild = buildName; + if (string.Equals(Settings.SelectedAtlasBuild, Settings.SelectedBuild, StringComparison.Ordinal)) + { + AtlasBuildData = SelectedBuildData; + } + else + { + AtlasBuildData = TreeConfig.LoadBuild(SkillTreeUrlFilesDir, Settings.SelectedAtlasBuild) ?? new TreeConfig.SkillTreeData(); + } + _atlasUrlNodeIds = new HashSet(); - _buildNameEditorValue = buildName; } - private void LoadUrl(string url) + private void SyncAtlasBuildDataIfSameFile() + { + if (string.Equals(Settings.SelectedAtlasBuild, Settings.SelectedBuild, StringComparison.Ordinal)) + { + AtlasBuildData = SelectedBuildData; + } + } + + internal void LoadUrl(string url) { if (string.IsNullOrWhiteSpace(url)) return; var cleanedUrl = RemoveAccName(url).Trim(); @@ -110,9 +176,55 @@ private void LoadUrl(string url) } } - public override bool Initialise() + public override bool Initialise() => true; + + internal void QueueRedownloadOfficialTreeData(bool isAtlas) { - return true; + if (Interlocked.CompareExchange(ref _officialDownloadBusy, 1, 0) != 0) return; + + var destFile = isAtlas ? AtlasTreeDataFile : SkillTreeDataFile; + var url = isAtlas ? OfficialAtlasTreeDataJsonRawUrl : OfficialSkillTreeDataJsonRawUrl; + + _ = Task.Run(async () => + { + try + { + using var http = CreateRawGithubHttpClient(); + var json = await http.GetStringAsync(url).ConfigureAwait(false); + + var testDecoder = new PoESkillTreeJsonDecoder(); + testDecoder.Decode(json); + if (testDecoder.SkillNodes.Count == 0) throw new InvalidOperationException("JSON did not decode to any skill nodes."); + + var path = Path.Join(DirectoryFullName, destFile); + await Task.Run(() => File.WriteAllText(path, json)).ConfigureAwait(false); + + _pendingReloadGameTreeData = true; + } + catch (Exception ex) + { + LogError($"Official tree JSON download: {ex.Message}", 10); + } + finally + { + Interlocked.Exchange(ref _officialDownloadBusy, 0); + } + }); + } + + private static HttpClient CreateRawGithubHttpClient() + { + var http = new HttpClient {Timeout = TimeSpan.FromMinutes(10)}; + http.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "PassiveSkillTreePlanter (ExileCore plugin)"); + return http; + } + + internal void ProcessPendingOfficialTreeReload() + { + if (!_pendingReloadGameTreeData) return; + + _pendingReloadGameTreeData = false; + ReloadGameTreeData(); } private void ReloadGameTreeData() @@ -141,36 +253,34 @@ private void ReloadGameTreeData() public override void Render() { - DrawTreeOverlay(GameController.Game.IngameState.IngameUi.TreePanel.AsObject(), - _skillTreeData, _characterUrlNodeIds, - () => GameController.Game.IngameState.ServerData.PassiveSkillIds.ToHashSet(), - ESkillTreeType.Character); - DrawTreeOverlay(GameController.Game.IngameState.IngameUi.AtlasTreePanel.AsObject(), - _atlasTreeData, _atlasUrlNodeIds, - () => GameController.Game.IngameState.ServerData.AtlasPassiveSkillIds.ToHashSet(), - ESkillTreeType.Atlas); + DrawTreeOverlay(GameController.Game.IngameState.IngameUi.TreePanel.AsObject(), _skillTreeData, _characterUrlNodeIds, + () => GameController.Game.IngameState.ServerData.PassiveSkillIds.ToHashSet(), ESkillTreeType.Character); + + DrawTreeOverlay(GameController.Game.IngameState.IngameUi.AtlasTreePanel.AsObject(), _atlasTreeData, _atlasUrlNodeIds, + () => GameController.Game.IngameState.ServerData.AtlasPassiveSkillIds.ToHashSet(), ESkillTreeType.Atlas); TaskUtils.RunOrRestart(ref _currentTask, () => null); } private void DrawControlPanel(ESkillTreeType skillTreeType, TreePanel treePanel, IReadOnlySet allocatedNodeIds, IReadOnlySet targetNodeIds) { - if (!Settings.ShowControlPanel) - return; + if (!Settings.ShowControlPanel) return; var isOpen = true; ImGui.SetNextWindowPos(new Vector2(0, 0), ImGuiCond.FirstUseEver); if (ImGui.Begin("#treeSwitcher", ref isOpen, ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoTitleBar)) { - var trees = _selectedBuildData.Trees.Where(x => x.Type == skillTreeType).ToList(); + var source = skillTreeType == ESkillTreeType.Character ? SelectedBuildData : AtlasBuildData; + var trees = source.Trees.Where(x => x.Type == skillTreeType).ToList(); foreach (var tree in trees) { var lastSelectedUrl = skillTreeType switch { ESkillTreeType.Character => Settings.LastSelectedCharacterUrl, - ESkillTreeType.Atlas => Settings.LastSelectedAtlasUrl, + ESkillTreeType.Atlas => Settings.LastSelectedAtlasUrl }; + ImGui.BeginDisabled(lastSelectedUrl == tree.SkillTreeUrl); if (ImGui.Button($"Load {tree.Tag}")) { @@ -192,29 +302,44 @@ private void DrawControlPanel(ESkillTreeType skillTreeType, TreePanel treePanel, _pathingNodes = null; } - ImGui.EndMenu(); + ImGui.End(); } } private static string CleanFileName(string fileName) { - return Path.GetInvalidFileNameChars() - .Aggregate(fileName, (current, c) => current.Replace(c.ToString(), string.Empty)); + return Path.GetInvalidFileNameChars().Aggregate(fileName, (current, c) => current.Replace(c.ToString(), string.Empty)); } - private void RenameFile(string fileName, string oldFileName) + internal void RenameFile(string fileName, string oldFileName) { fileName = CleanFileName(fileName); var oldFilePath = Path.Combine(SkillTreeUrlFilesDir, $"{oldFileName}.json"); var newFilePath = Path.Combine(SkillTreeUrlFilesDir, $"{fileName}.json"); File.Move(oldFilePath, newFilePath); - Settings.SelectedBuild = fileName; + if (string.Equals(Settings.SelectedAtlasBuild, oldFileName, StringComparison.Ordinal)) + { + Settings.SelectedAtlasBuild = fileName; + } + + if (string.Equals(Settings.SelectedBuild, oldFileName, StringComparison.Ordinal)) + { + Settings.SelectedBuild = fileName; + } + + if (string.Equals(Settings.SelectedEditBuild, oldFileName, StringComparison.Ordinal)) + { + Settings.SelectedEditBuild = fileName; + } + ReloadBuildList(); LoadBuild(Settings.SelectedBuild); + LoadAtlasBuild(Settings.SelectedAtlasBuild); + LoadEditBuild(Settings.SelectedEditBuild); } - private bool CanRename(string fileName) + internal bool CanRename(string fileName) { if (string.IsNullOrWhiteSpace(fileName) || fileName.Intersect(Path.GetInvalidFileNameChars()).Any()) { @@ -235,323 +360,8 @@ private static string RemoveAccName(string url) public override void DrawSettings() { - string[] settingName = - { - "Build Selection", - "Build Edit", - "Settings", - }; - if (ImGui.BeginChild("LeftSettings", new Vector2(150, ImGui.GetContentRegionAvail().Y), ImGuiChildFlags.Border, ImGuiWindowFlags.None)) - for (var i = 0; i < settingName.Length; i++) - if (ImGui.Selectable(settingName[i], _selectedSettingsTab == i)) - _selectedSettingsTab = i; - - ImGui.EndChild(); - ImGui.SameLine(); - ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 5.0f); - var contentRegionArea = ImGui.GetContentRegionAvail(); - if (ImGui.BeginChild("RightSettings", contentRegionArea, ImGuiChildFlags.Border, ImGuiWindowFlags.None)) - { - var trees = _selectedBuildData.Trees; - switch (settingName[_selectedSettingsTab]) - { - case "Build Selection": - if (ImGui.Button("Open Build Folder")) - Process.Start("explorer.exe", Path.Join(ConfigDirectory, "Builds")); - - ImGui.SameLine(); - if (ImGui.Button("(Re)Load List")) - ReloadBuildList(); - - if (!string.IsNullOrEmpty(_selectedBuildData.BuildLink)) - { - ImGui.SameLine(); - - if (ImGui.Button("Open Forum Thread")) - { - Process.Start( - new ProcessStartInfo(_selectedBuildData.BuildLink) - { - UseShellExecute = true // required for urls - } - ); - } - } - - - var newBuildName = ImGuiExtension.ComboBox("Builds", Settings.SelectedBuild, - BuildFiles, out var buildSelected, ImGuiComboFlags.HeightLarge); - if (buildSelected) - { - LoadBuild(newBuildName); - } - - ImGui.Separator(); - ImGui.Text($"Currently Selected: {Settings.SelectedBuild}"); - ImGui.InputText("##CreationLabel", ref _addNewBuildFile, 1024, ImGuiInputTextFlags.EnterReturnsTrue); - ImGui.BeginDisabled(!CanRename(_addNewBuildFile)); - if (ImGui.Button($"Add new build {_addNewBuildFile}")) - { - TreeConfig.SaveSettingFile(Path.Join(SkillTreeUrlFilesDir, _addNewBuildFile), new TreeConfig.SkillTreeData()); - _addNewBuildFile = string.Empty; - ReloadBuildList(); - } - - ImGui.EndDisabled(); - - ImGui.Separator(); - ImGui.Columns(3, "LoadColums", true); - ImGui.SetColumnWidth(0, 51f); - ImGui.SetColumnWidth(1, 38f); - ImGui.Text(""); - ImGui.NextColumn(); - ImGui.Text("Type"); - ImGui.NextColumn(); - ImGui.Text("Tree Name"); - ImGui.NextColumn(); - if (trees.Count != 0) - ImGui.Separator(); - - for (var j = 0; j < trees.Count; j++) - { - if (ImGui.Button($"LOAD##LOADRULE{j}")) - { - LoadUrl(trees[j].SkillTreeUrl); - } - - ImGui.NextColumn(); - var iconsIndex = trees[j].Type switch - { - ESkillTreeType.Unknown => MapIconsIndex.QuestObject, - ESkillTreeType.Character => MapIconsIndex.MyPlayer, - ESkillTreeType.Atlas => MapIconsIndex.TangleAltar, - }; - var rect = SpriteHelper.GetUV(iconsIndex); - ImGui.Image(Graphics.GetTextureId("Icons.png"), new Vector2(ImGui.CalcTextSize("A").Y), rect.TopLeft.ToVector2Num(), - rect.BottomRight.ToVector2Num()); - ImGui.NextColumn(); - ImGui.Text(trees[j].Tag); - ImGui.NextColumn(); - ImGui.PopItemWidth(); - } - - ImGui.Columns(1, "", false); - ImGui.Separator(); - - ImGui.Text("NOTES:"); - - // only way to wrap text with imgui.net without a limit on TextWrap function - ImGuiNative.igPushTextWrapPos(0.0f); - ImGui.TextUnformatted(_selectedBuildData.Notes); - ImGuiNative.igPopTextWrapPos(); - break; - case "Build Edit": - DrawBuildEdit(trees, contentRegionArea); - break; - case "Settings": - base.DrawSettings(); - break; - } - } - - ImGui.PopStyleVar(); - ImGui.EndChild(); - } - - private void DrawBuildEdit(List trees, Vector2 contentRegionArea) - { - if (trees.Count > 0) - { - ImGui.Separator(); - var buildLink = _selectedBuildData.BuildLink; - if (ImGui.InputText("Forum Thread", ref buildLink, 1024, ImGuiInputTextFlags.None)) - { - _selectedBuildData.BuildLink = buildLink.Replace("\u0000", null); - _selectedBuildData.Modified = true; - } - - ImGui.Text("Notes"); - // Keep at max 4k byte size not sure why it crashes when upped, not going to bother dealing with this either. - var notes = _selectedBuildData.Notes; - if (ImGui.InputTextMultiline("##Notes", ref notes, 150000, new Vector2(contentRegionArea.X - 20, 200))) - { - _selectedBuildData.Notes = notes.Replace("\u0000", null); - _selectedBuildData.Modified = true; - } - - ImGui.Separator(); - ImGui.Columns(5, "EditColums", true); - ImGui.SetColumnWidth(0, 30f); - ImGui.SetColumnWidth(1, 50f); - ImGui.SetColumnWidth(3, 38f); - ImGui.Text(""); - ImGui.NextColumn(); - ImGui.Text("Move"); - ImGui.NextColumn(); - ImGui.Text("Tree Name"); - ImGui.NextColumn(); - ImGui.Text("Type"); - ImGui.NextColumn(); - ImGui.Text("Skill Tree"); - ImGui.NextColumn(); - if (trees.Count != 0) - ImGui.Separator(); - for (var j = 0; j < trees.Count; j++) - { - ImGui.PushID($"{j}"); - DrawTreeEdit(trees, j); - ImGui.PopID(); - } - - ImGui.Separator(); - ImGui.Columns(1, "", false); - } - else - { - ImGui.Text("No Data Selected"); - } - - if (ImGui.Button("+##AN")) - { - trees.Add(new TreeConfig.Tree()); - _selectedBuildData.Modified = true; - } - - ImGui.Text("Export current build"); - ImGui.SameLine(); - var rectMyPlayer = SpriteHelper.GetUV(MapIconsIndex.MyPlayer); - if (ImGui.ImageButton("charBtn", Graphics.GetTextureId("Icons.png"), new Vector2(ImGui.CalcTextSize("A").Y), - rectMyPlayer.TopLeft.ToVector2Num(), rectMyPlayer.BottomRight.ToVector2Num())) - { - trees.Add(new TreeConfig.Tree - { - Tag = "Current character tree", - SkillTreeUrl = PathOfExileUrlDecoder.Encode(GameController.Game.IngameState.ServerData.PassiveSkillIds.ToHashSet(), ESkillTreeType.Character) - }); - _selectedBuildData.Modified = true; - } - - ImGui.SameLine(); - var rectTangle = SpriteHelper.GetUV(MapIconsIndex.TangleAltar); - if (ImGui.ImageButton("atlasBtn", Graphics.GetTextureId("Icons.png"), new Vector2(ImGui.CalcTextSize("A").Y), - rectTangle.TopLeft.ToVector2Num(), rectTangle.BottomRight.ToVector2Num())) - { - trees.Add(new TreeConfig.Tree - { - Tag = "Current atlas tree", - SkillTreeUrl = PathOfExileUrlDecoder.Encode(GameController.Game.IngameState.ServerData.AtlasPassiveSkillIds.ToHashSet(), ESkillTreeType.Atlas) - }); - _selectedBuildData.Modified = true; - } - - foreach (var importer in _importers) - { - if (importer.DrawAddInterface() is { } newTree) - { - trees.Add(newTree); - _selectedBuildData.Modified = true; - } - } - - ImGui.Separator(); - - ImGui.InputText("##RenameLabel", ref _buildNameEditorValue, 200, ImGuiInputTextFlags.None); - ImGui.SameLine(); - ImGui.BeginDisabled(!CanRename(_buildNameEditorValue)); - if (ImGui.Button("Rename Build")) - { - RenameFile(_buildNameEditorValue, Settings.SelectedBuild); - } - - ImGui.EndDisabled(); - - if (ImGui.Button($"Save Build to File: {Settings.SelectedBuild}") || - _selectedBuildData.Modified && Settings.SaveChangesAutomatically) - { - _selectedBuildData.Modified = false; - TreeConfig.SaveSettingFile(Path.Join(SkillTreeUrlFilesDir, Settings.SelectedBuild), _selectedBuildData); - ReloadBuildList(); - } - - if (_selectedBuildData.Modified) - { - ImGui.TextColored(Color.Red.ToImguiVec4(), "Unsaved changes detected"); - } - } - - private void DrawTreeEdit(List trees, int treeIndex) - { - if (ImGui.Button("X##REMOVERULE")) - { - trees.RemoveAt(treeIndex); - _selectedBuildData.Modified = true; - return; - } - - ImGui.NextColumn(); - - ImGui.BeginDisabled(treeIndex == 0); - if (ImGui.Button("^##MOVERULEUPEDIT")) - { - MoveElement(trees, treeIndex, true); - _selectedBuildData.Modified = true; - } - - ImGui.EndDisabled(); - ImGui.SameLine(); - ImGui.BeginDisabled(treeIndex == trees.Count - 1); - if (ImGui.Button("v##MOVERULEDOWNEDIT")) - { - MoveElement(trees, treeIndex, false); - _selectedBuildData.Modified = true; - } - - ImGui.EndDisabled(); - ImGui.NextColumn(); - ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); - ImGui.InputText("##TAG", ref trees[treeIndex].Tag, 1024, ImGuiInputTextFlags.AutoSelectAll); - ImGui.PopItemWidth(); - //ImGui.SameLine(); - ImGui.NextColumn(); - var iconsIndex = trees[treeIndex].Type switch - { - ESkillTreeType.Unknown => MapIconsIndex.QuestObject, - ESkillTreeType.Character => MapIconsIndex.MyPlayer, - ESkillTreeType.Atlas => MapIconsIndex.TangleAltar, - }; - var rect = SpriteHelper.GetUV(iconsIndex); - ImGui.Image(Graphics.GetTextureId("Icons.png"), new Vector2(ImGui.CalcTextSize("A").Y), rect.TopLeft.ToVector2Num(), - rect.BottomRight.ToVector2Num()); - ImGui.NextColumn(); - ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); - if (ImGui.InputText("##GN", ref trees[treeIndex].SkillTreeUrl, 1024, ImGuiInputTextFlags.AutoSelectAll)) - { - trees[treeIndex].ResetType(); - _selectedBuildData.Modified = true; - } - - ImGui.PopItemWidth(); - ImGui.NextColumn(); - } - - private static void MoveElement(List list, int changeIndex, bool moveUp) - { - if (moveUp) - { - // Move Up - if (changeIndex > 0) - { - (list[changeIndex], list[changeIndex - 1]) = (list[changeIndex - 1], list[changeIndex]); - } - } - else - { - // Move Down - if (changeIndex < list.Count - 1) - { - (list[changeIndex], list[changeIndex + 1]) = (list[changeIndex + 1], list[changeIndex]); - } - } + _settingsMenu ??= new PassiveSkillTreePlanterSettingsMenu(this); + _settingsMenu.Draw(() => base.DrawSettings()); } private void ValidateNodes(HashSet currentNodes, Dictionary nodeDict) @@ -588,7 +398,7 @@ private void ValidateNodes(HashSet currentNodes, Dictionary targetNodeIds, Func> allocatedNodeIdsFunc, ESkillTreeType type) { - if (targetNodeIds is not { Count: > 0 }) + if (targetNodeIds is not {Count: > 0}) { return; } @@ -607,13 +417,6 @@ private void DrawTreeOverlay(TreePanel treePanel, PoESkillTreeJsonDecoder treeDa DrawControlPanel(type, treePanel, allocatedNodeIds, targetNodeIds); } - private enum ConnectionType - { - Deallocate, - Allocate, - Allocated, - } - private async SyncTask ChangeTree(IReadOnlySet allocatedNodeIds, IReadOnlySet targetNodeIds, TreePanel panel) { var passivesById = panel.Passives.DistinctBy(x => x.PassiveSkill.PassiveId).ToDictionary(x => x.PassiveSkill.PassiveId); @@ -621,7 +424,7 @@ private async SyncTask ChangeTree(IReadOnlySet allocatedNodeIds, I var nodesToTake = targetNodeIds.Except(allocatedNodeIds).ToHashSet(); while (panel.IsVisible) { - var nodeToRemove = wrongNodes.Select(arg => passivesById.GetValueOrDefault(arg)).FirstOrDefault(x => x is { IsAllocatedForPlan: true, CanDeallocate: true }); + var nodeToRemove = wrongNodes.Select(arg => passivesById.GetValueOrDefault(arg)).FirstOrDefault(x => x is {IsAllocatedForPlan: true, CanDeallocate: true}); if (nodeToRemove != null) { var windowRect = GameController.Window.GetWindowRectangleTimeCache.TopLeft.ToVector2Num(); @@ -657,7 +460,7 @@ private async SyncTask ChangeTree(IReadOnlySet allocatedNodeIds, I } else if (panel.RefundButton.IsVisible) { - var nodeToTake = nodesToTake.Select(arg => passivesById.GetValueOrDefault(arg)).FirstOrDefault(x => x is { IsAllocatedForPlan: false, CanAllocate: true }); + var nodeToTake = nodesToTake.Select(arg => passivesById.GetValueOrDefault(arg)).FirstOrDefault(x => x is {IsAllocatedForPlan: false, CanAllocate: true}); if (nodeToTake != null) { var windowRect = GameController.Window.GetWindowRectangleTimeCache.TopLeft.ToVector2Num(); @@ -689,10 +492,6 @@ private async SyncTask ChangeTree(IReadOnlySet allocatedNodeIds, I return true; } - private Dictionary _nodeMap = new(); - private Task> _pathingNodes; - private bool _editorShown; - private void DrawTreeEditOverlay(PoESkillTreeJsonDecoder treeData, float scale, Vector2 baseOffset) { if (!_editorShown) @@ -701,7 +500,7 @@ private void DrawTreeEditOverlay(PoESkillTreeJsonDecoder treeData, float scale, } var nodes = treeData.SkillNodes.Where(x => x.Value.linkedNodes != null).Select(x => x.Value).ToList(); - var pathingNodes = _pathingNodes is { IsCompletedSuccessfully: true } ? _pathingNodes.Result : []; + var pathingNodes = _pathingNodes is {IsCompletedSuccessfully: true} ? _pathingNodes.Result : []; DebugWindow.LogMsg($"Solved optimization in {pathingNodes.Count} nodes"); foreach (var node in nodes) { @@ -713,8 +512,7 @@ private void DrawTreeEditOverlay(PoESkillTreeJsonDecoder treeData, float scale, ImGui.SetNextWindowPos(new Vector2(posX, posY) - new Vector2(drawSize / 2)); ImGui.SetNextWindowSize(new Vector2(drawSize)); ImGui.PushStyleColor(ImGuiCol.WindowBg, Color.Transparent.ToImgui()); - ImGui.PushStyleColor(ImGuiCol.Border, - _nodeMap.GetValueOrDefault(node.Id, false) ? Color.Green.ToImgui() : pathingNodes.Contains(node.Id) ? Color.Blue.ToImgui() : Color.Red.ToImgui()); + ImGui.PushStyleColor(ImGuiCol.Border, _nodeMap.GetValueOrDefault(node.Id, false) ? Color.Green.ToImgui() : pathingNodes.Contains(node.Id) ? Color.Blue.ToImgui() : Color.Red.ToImgui()); foreach (var linkedNode in node.linkedNodes) { if (linkedNode < node.Id) @@ -724,9 +522,10 @@ private void DrawTreeEditOverlay(PoESkillTreeJsonDecoder treeData, float scale, if (pathingNodes.Contains(linkedNode) && pathingNodes.Contains(node.Id)) { - Graphics.DrawLine(treeData.SkillNodes[linkedNode].DrawPosition*scale+baseOffset, new Vector2(posX, posY), 5, Color.Blue); + Graphics.DrawLine(treeData.SkillNodes[linkedNode].DrawPosition * scale + baseOffset, new Vector2(posX, posY), 5, Color.Blue); } } + if (ImGui.Begin($"planter_node_{node.Id}", ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.NoSavedSettings | ImGuiWindowFlags.NoTitleBar)) { ImGui.SetCursorPos(Vector2.Zero); @@ -758,22 +557,19 @@ private void DrawTreeOverlay(IReadOnlySet allocatedNodeIds, IReadOnlySet { return; } + var wrongNodes = allocatedNodeIds.Except(targetNodeIds).ToHashSet(); var missingNodes = targetNodeIds.Except(allocatedNodeIds).ToHashSet(); var allNodes = targetNodeIds.Union(allocatedNodeIds).Select(x => treeData.SkillNodes.GetValueOrDefault(x)).Where(x => x?.linkedNodes != null).ToList(); var allConnections = allNodes - .SelectMany(node => node.linkedNodes - .Where(treeData.SkillNodes.ContainsKey) - .Where(id => targetNodeIds.Contains(id) || allocatedNodeIds.Contains(id)) - .Select(linkedNode => (Math.Min(node.Id, linkedNode), Math.Max(node.Id, linkedNode)))) - .Distinct() - .Select(pair => (ids: pair, type: pair switch + .SelectMany(node => node.linkedNodes.Where(treeData.SkillNodes.ContainsKey).Where(id => targetNodeIds.Contains(id) || allocatedNodeIds.Contains(id)) + .Select(linkedNode => (Math.Min(node.Id, linkedNode), Math.Max(node.Id, linkedNode)))).Distinct().Select(pair => (ids: pair, type: pair switch { var (a, b) when wrongNodes.Contains(a) || wrongNodes.Contains(b) => ConnectionType.Deallocate, var (a, b) when missingNodes.Contains(a) || missingNodes.Contains(b) => ConnectionType.Allocate, - _ => ConnectionType.Allocated, - })) - .ToList(); + _ => ConnectionType.Allocated + })).ToList(); + foreach (var node in allNodes) { var drawSize = node.DrawSize * scale; @@ -785,7 +581,7 @@ private void DrawTreeOverlay(IReadOnlySet allocatedNodeIds, IReadOnlySet (true, true) => Settings.PickedBorderColor.Value, (true, false) => Settings.WrongPickedBorderColor.Value, (false, true) => Settings.UnpickedBorderColor.Value, - (false, false) => Color.Orange, + (false, false) => Color.Orange }; Graphics.DrawImage(_ringImage, new RectangleF(posX - drawSize / 2, posY - drawSize / 2, drawSize, drawSize), color); @@ -813,7 +609,7 @@ private void DrawTreeOverlay(IReadOnlySet allocatedNodeIds, IReadOnlySet ConnectionType.Deallocate => Settings.WrongPickedBorderColor, ConnectionType.Allocate => Settings.UnpickedBorderColor, ConnectionType.Allocated => Settings.PickedBorderColor, - _ => Color.Orange, + _ => Color.Orange }); bool NodeNameEndsWithGateway(ushort nodeId) @@ -824,10 +620,17 @@ bool NodeNameEndsWithGateway(ushort nodeId) } var textPos = new Vector2(50, 300); - Graphics.DrawText($"Total Tree Nodes: {targetNodeIds.Count}", textPos, Color.White, 15); + Graphics.DrawText($"Total Tree Nodes: {targetNodeIds.Count}", textPos, Color.White); textPos.Y += 20; - Graphics.DrawText($"Picked Nodes: {allocatedNodeIds.Count}", textPos, Color.Green, 15); + Graphics.DrawText($"Picked Nodes: {allocatedNodeIds.Count}", textPos, Color.Green); textPos.Y += 20; - Graphics.DrawText($"Wrong Picked Nodes: {wrongNodes.Count}", textPos, Color.Red, 15); + Graphics.DrawText($"Wrong Picked Nodes: {wrongNodes.Count}", textPos, Color.Red); + } + + private enum ConnectionType + { + Deallocate, + Allocate, + Allocated } } \ No newline at end of file diff --git a/PassiveSkillTreePlanter.csproj b/PassiveSkillTreePlanter.csproj index 58da447..143e8e2 100644 --- a/PassiveSkillTreePlanter.csproj +++ b/PassiveSkillTreePlanter.csproj @@ -1,4 +1,4 @@ - + net10.0-windows Library @@ -9,7 +9,7 @@ $(MSBuildProjectDirectory)=$(MSBuildProjectName) true $(ExApiPluginOutputPath)$(MSBuildProjectName) - + @@ -33,7 +33,7 @@ - + \ No newline at end of file diff --git a/PassiveSkillTreePlanterSettings.cs b/PassiveSkillTreePlanterSettings.cs index 5d20e78..1c09f84 100644 --- a/PassiveSkillTreePlanterSettings.cs +++ b/PassiveSkillTreePlanterSettings.cs @@ -1,4 +1,4 @@ -using ExileCore.Shared.Attributes; +using ExileCore.Shared.Attributes; using ExileCore.Shared.Interfaces; using ExileCore.Shared.Nodes; using SharpDX; @@ -10,6 +10,12 @@ public class PassiveSkillTreePlanterSettings : ISettings [IgnoreMenu] public string SelectedBuild { get; set; } = string.Empty; + [IgnoreMenu] + public string SelectedAtlasBuild { get; set; } = string.Empty; + + [IgnoreMenu] + public string SelectedEditBuild { get; set; } = string.Empty; + [IgnoreMenu] public string LastSelectedCharacterUrl { get; set; } @@ -18,11 +24,11 @@ public class PassiveSkillTreePlanterSettings : ISettings public RangeNode LineWidth { get; set; } = new(3, 0, 5); - public ColorNode PickedBorderColor { get; set; } = new ColorNode(); - public ColorNode UnpickedBorderColor { get; set; } = new ColorNode(Color.Green); - public ColorNode WrongPickedBorderColor { get; set; } = new ColorNode(Color.Red); + public ColorNode PickedBorderColor { get; set; } = new(); + public ColorNode UnpickedBorderColor { get; set; } = new(Color.Green); + public ColorNode WrongPickedBorderColor { get; set; } = new(Color.Red); - public ToggleNode ShowControlPanel { get; set; } = new ToggleNode(true); - public ToggleNode SaveChangesAutomatically { get; set; } = new ToggleNode(true); - public ToggleNode Enable { get; set; } = new ToggleNode(false); + public ToggleNode ShowControlPanel { get; set; } = new(true); + public ToggleNode SaveChangesAutomatically { get; set; } = new(true); + public ToggleNode Enable { get; set; } = new(false); } \ No newline at end of file diff --git a/PassiveSkillTreePlanterSettingsMenu.cs b/PassiveSkillTreePlanterSettingsMenu.cs new file mode 100644 index 0000000..5db4d03 --- /dev/null +++ b/PassiveSkillTreePlanterSettingsMenu.cs @@ -0,0 +1,485 @@ +using ExileCore; +using ExileCore.Shared.Enums; +using ExileCore.Shared.Helpers; +using ImGuiNET; +using PassiveSkillTreePlanter.UrlDecoders; +using SharpDX; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Vector2 = System.Numerics.Vector2; +using Vector4 = System.Numerics.Vector4; + +namespace PassiveSkillTreePlanter; + +internal sealed class PassiveSkillTreePlanterSettingsMenu +{ + private const string TreeRowDragPayloadId = "PstpBuildEditTreeIndex"; + + private readonly PassiveSkillTreePlanter _plugin; + + private string _addNewBuildFile = ""; + private bool _showAtlasBuildNotes = true; + private bool _showCharacterBuildNotes = true; + + public PassiveSkillTreePlanterSettingsMenu(PassiveSkillTreePlanter plugin) + { + _plugin = plugin; + } + + private static void CenterCellContentHorizontally(float itemWidth) + { + var cellAvail = ImGui.GetContentRegionAvail().X; + var pad = Math.Max(0f, (cellAvail - itemWidth) * 0.5f); + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + pad); + } + + private static bool FullCellCenteredButton(string labelId) + { + var avail = ImGui.GetContentRegionAvail(); + var lineH = ImGui.GetFrameHeight(); + var size = new Vector2(avail.X, lineH); + ImGui.PushStyleVar(ImGuiStyleVar.ButtonTextAlign, new Vector2(0.5f, 0.5f)); + var pressed = ImGui.Button(labelId, size); + ImGui.PopStyleVar(); + return pressed; + } + + public void Draw(Action drawCorePluginSettings) + { + _plugin.ProcessPendingOfficialTreeReload(); + + ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 5.0f); + var contentRegionArea = ImGui.GetContentRegionAvail(); + if (ImGui.BeginChild("PluginSettingsRoot", contentRegionArea, ImGuiChildFlags.Border, ImGuiWindowFlags.None)) + { + if (ImGui.BeginTabBar("PassiveSkillTreePlanter_MainTabs", ImGuiTabBarFlags.None)) + { + if (ImGui.BeginTabItem("Passive Tree")) + { + DrawBuildSelectionSharedMenu(ESkillTreeType.Character); + var characterTrees = _plugin.SelectedBuildData.Trees.Where(t => t.Type == ESkillTreeType.Character).ToList(); + DrawTreeLoadTableSection(characterTrees, "CharLoad"); + DrawBuildNotesSubsection(_plugin.SelectedBuildData.Notes, ref _showCharacterBuildNotes, "CharSelNotes"); + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Atlas Tree")) + { + DrawBuildSelectionSharedMenu(ESkillTreeType.Atlas); + var atlasTrees = _plugin.AtlasBuildData.Trees.Where(t => t.Type == ESkillTreeType.Atlas).ToList(); + DrawTreeLoadTableSection(atlasTrees, "AtlasLoad"); + DrawBuildNotesSubsection(_plugin.AtlasBuildData.Notes, ref _showAtlasBuildNotes, "AtlasSelNotes"); + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Build Edit")) + { + DrawBuildEdit(contentRegionArea); + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("GGG tree JSON")) + { + DrawOfficialTreeExportTab(); + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Settings")) + { + drawCorePluginSettings(); + ImGui.EndTabItem(); + } + + ImGui.EndTabBar(); + } + } + + ImGui.PopStyleVar(); + ImGui.EndChild(); + } + + private void DrawOfficialTreeExportTab() + { + if (ImGui.Button("Open skilltree-export on GitHub##ggg")) + { + Process.Start(new ProcessStartInfo("https://github.com/grindinggear/skilltree-export") + { + UseShellExecute = true + }); + } + + if (ImGui.Button("Open atlastree-export on GitHub##ggg")) + { + Process.Start(new ProcessStartInfo("https://github.com/grindinggear/atlastree-export") + { + UseShellExecute = true + }); + } + + ImGui.Spacing(); + if (ImGui.Button("Redownload SkillTreeData.json##gggPassive")) _plugin.QueueRedownloadOfficialTreeData(false); + + if (ImGui.Button("Redownload AtlasTreeData.json##gggAtlas")) _plugin.QueueRedownloadOfficialTreeData(true); + } + + private void DrawBuildSelectionSharedMenu(ESkillTreeType sectionTreeKind) + { + if (ImGui.Button("Open Build Folder")) Process.Start("explorer.exe", Path.Join(_plugin.ConfigDirectory, "Builds")); + + ImGui.SameLine(); + if (ImGui.Button("(Re)Load List")) _plugin.ReloadBuildList(); + + ImGui.Separator(); + + switch (sectionTreeKind) + { + case ESkillTreeType.Character: + { + var newCharacterBuild = ImGuiExtension.ComboBox("Character build", _plugin.Settings.SelectedBuild, _plugin.BuildFiles, out var characterBuildSelected, ImGuiComboFlags.HeightLarge); + if (characterBuildSelected) + { + _plugin.LoadBuild(newCharacterBuild); + } + + break; + } + case ESkillTreeType.Atlas: + { + var newAtlasBuild = ImGuiExtension.ComboBox("Atlas build", _plugin.Settings.SelectedAtlasBuild, _plugin.BuildFiles, out var atlasBuildSelected, ImGuiComboFlags.HeightLarge); + if (atlasBuildSelected) + { + _plugin.LoadAtlasBuild(newAtlasBuild); + } + + break; + } + case ESkillTreeType.Unknown: + default: + break; + } + + var forumLinkForSection = sectionTreeKind switch + { + ESkillTreeType.Character => _plugin.SelectedBuildData.BuildLink, + ESkillTreeType.Atlas => _plugin.AtlasBuildData.BuildLink, + _ => "" + }; + + var forumIdSuffix = sectionTreeKind switch + { + ESkillTreeType.Character => "char", + ESkillTreeType.Atlas => "atlas", + _ => "na" + }; + + ImGui.BeginDisabled(string.IsNullOrEmpty(forumLinkForSection)); + if (ImGui.Button($"Open forum thread##forumBtn_{forumIdSuffix}")) + { + Process.Start(new ProcessStartInfo(forumLinkForSection) + { + UseShellExecute = true + }); + } + + ImGui.EndDisabled(); + + ImGui.InputText("##CreationLabel", ref _addNewBuildFile, 1024, ImGuiInputTextFlags.EnterReturnsTrue); + ImGui.BeginDisabled(!_plugin.CanRename(_addNewBuildFile)); + if (ImGui.Button($"Add new build {_addNewBuildFile}")) + { + TreeConfig.SaveSettingFile(Path.Join(_plugin.SkillTreeUrlFilesDir, _addNewBuildFile), new TreeConfig.SkillTreeData()); + _addNewBuildFile = string.Empty; + _plugin.ReloadBuildList(); + } + + ImGui.EndDisabled(); + } + + private static void DrawBuildNotesSubsection(string notesText, ref bool showNotes, string subsectionId) + { + ImGui.Spacing(); + ImGui.Checkbox($"Show notes##{subsectionId}", ref showNotes); + ImGui.Separator(); + if (!showNotes) + { + return; + } + + ImGui.BeginChild("#notes"); + ImGuiNative.igPushTextWrapPos(0.0f); + ImGui.TextUnformatted(string.IsNullOrEmpty(notesText) ? "" : notesText); + ImGuiNative.igPopTextWrapPos(); + ImGui.EndChild(); + } + + private void DrawTreeLoadTableSection(List trees, string idPrefix) + { + if (!ImGui.BeginTable($"LoadTable_{idPrefix}", 3, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.Resizable | ImGuiTableFlags.SizingStretchProp)) + { + return; + } + + ImGui.TableSetupColumn("Load", ImGuiTableColumnFlags.WidthFixed, 56f); + ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, 42f); + ImGui.TableSetupColumn("Tree Name", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableHeadersRow(); + + var iconSize = new Vector2(ImGui.CalcTextSize("A").Y); + for (var j = 0; j < trees.Count; j++) + { + ImGui.TableNextRow(); + ImGui.PushID($"{idPrefix}_row_{j}"); + + ImGui.TableSetColumnIndex(0); + if (FullCellCenteredButton("LOAD")) + { + _plugin.LoadUrl(trees[j].SkillTreeUrl); + } + + ImGui.TableSetColumnIndex(1); + var iconsIndex = trees[j].Type switch + { + ESkillTreeType.Unknown => MapIconsIndex.QuestObject, + ESkillTreeType.Character => MapIconsIndex.MyPlayer, + ESkillTreeType.Atlas => MapIconsIndex.TangleAltar + }; + + var rect = SpriteHelper.GetUV(iconsIndex); + CenterCellContentHorizontally(iconSize.X); + ImGui.Image(_plugin.Graphics.GetTextureId("Icons.png"), iconSize, rect.TopLeft.ToVector2Num(), rect.BottomRight.ToVector2Num()); + + ImGui.TableSetColumnIndex(2); + ImGui.TextUnformatted(string.IsNullOrEmpty(trees[j].Tag) ? " " : $" {trees[j].Tag}"); + + ImGui.PopID(); + } + + ImGui.EndTable(); + } + + private void DrawBuildEdit(Vector2 contentRegionArea) + { + if (ImGui.Button("Open Build Folder")) Process.Start("explorer.exe", Path.Join(_plugin.ConfigDirectory, "Builds")); + + ImGui.SameLine(); + if (ImGui.Button("(Re)Load List")) _plugin.ReloadBuildList(); + + ImGui.Separator(); + + var newEditBuild = ImGuiExtension.ComboBox("Build to edit", _plugin.Settings.SelectedEditBuild, _plugin.BuildFiles, out var editBuildSelected, ImGuiComboFlags.HeightLarge); + if (editBuildSelected) + { + _plugin.LoadEditBuild(newEditBuild); + } + + var renameValue = _plugin.BuildNameEditorValue; + ImGui.InputText("##RenameLabel", ref renameValue, 200, ImGuiInputTextFlags.None); + _plugin.BuildNameEditorValue = renameValue; + ImGui.SameLine(); + ImGui.BeginDisabled(!_plugin.CanRename(_plugin.BuildNameEditorValue)); + if (ImGui.Button("Rename Build")) + { + _plugin.RenameFile(_plugin.BuildNameEditorValue, _plugin.Settings.SelectedEditBuild); + } + + ImGui.EndDisabled(); + + if (ImGui.Button("Save Build") || _plugin.EditBuildData.Modified && _plugin.Settings.SaveChangesAutomatically) + { + _plugin.EditBuildData.Modified = false; + TreeConfig.SaveSettingFile(Path.Join(_plugin.SkillTreeUrlFilesDir, _plugin.Settings.SelectedEditBuild), _plugin.EditBuildData); + _plugin.ReloadBuildList(); + if (string.Equals(_plugin.Settings.SelectedEditBuild, _plugin.Settings.SelectedBuild, StringComparison.Ordinal)) + { + _plugin.LoadBuild(_plugin.Settings.SelectedBuild); + } + + if (string.Equals(_plugin.Settings.SelectedEditBuild, _plugin.Settings.SelectedAtlasBuild, StringComparison.Ordinal)) + { + _plugin.LoadAtlasBuild(_plugin.Settings.SelectedAtlasBuild); + } + } + + if (_plugin.EditBuildData.Modified) + { + ImGui.TextColored(Color.Red.ToImguiVec4(), "Unsaved changes detected"); + } + + ImGui.Separator(); + + var trees = _plugin.EditBuildData.Trees; + if (trees.Count > 0) + { + var buildLink = _plugin.EditBuildData.BuildLink; + if (ImGui.InputText("Forum Thread", ref buildLink, 1024, ImGuiInputTextFlags.None)) + { + _plugin.EditBuildData.BuildLink = buildLink.Replace("\u0000", null); + _plugin.EditBuildData.Modified = true; + } + + ImGui.Text("Notes"); + var notes = _plugin.EditBuildData.Notes; + if (ImGui.InputTextMultiline("##Notes", ref notes, 150000, new Vector2(contentRegionArea.X - 20, 200))) + { + _plugin.EditBuildData.Notes = notes.Replace("\u0000", null); + _plugin.EditBuildData.Modified = true; + } + + ImGui.Separator(); + if (ImGui.BeginTable("EditTreesTable", 5, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.Resizable | ImGuiTableFlags.SizingStretchProp)) + { + ImGui.TableSetupColumn("Drag", ImGuiTableColumnFlags.WidthFixed, 40f); + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 38f); + ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, 42f); + ImGui.TableSetupColumn("Tree Name", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableSetupColumn("Skill Tree", ImGuiTableColumnFlags.WidthStretch); + ImGui.TableHeadersRow(); + + for (var j = 0; j < trees.Count; j++) + { + ImGui.TableNextRow(); + ImGui.PushID($"{j}"); + DrawTreeEdit(trees, j); + ImGui.PopID(); + } + + ImGui.EndTable(); + } + + ImGui.Separator(); + } + else + { + ImGui.Text("No Data Selected"); + } + + if (ImGui.Button("+##AN")) + { + trees.Add(new TreeConfig.Tree()); + _plugin.EditBuildData.Modified = true; + } + + ImGui.Text("Export current build"); + ImGui.SameLine(); + var rectMyPlayer = SpriteHelper.GetUV(MapIconsIndex.MyPlayer); + if (ImGui.ImageButton("charBtn", _plugin.Graphics.GetTextureId("Icons.png"), new Vector2(ImGui.CalcTextSize("A").Y), rectMyPlayer.TopLeft.ToVector2Num(), + rectMyPlayer.BottomRight.ToVector2Num())) + { + trees.Add(new TreeConfig.Tree + { + Tag = "Current character tree", + SkillTreeUrl = PathOfExileUrlDecoder.Encode(_plugin.GameController.Game.IngameState.ServerData.PassiveSkillIds.ToHashSet(), ESkillTreeType.Character) + }); + + _plugin.EditBuildData.Modified = true; + } + + ImGui.SameLine(); + var rectTangle = SpriteHelper.GetUV(MapIconsIndex.TangleAltar); + if (ImGui.ImageButton("atlasBtn", _plugin.Graphics.GetTextureId("Icons.png"), new Vector2(ImGui.CalcTextSize("A").Y), rectTangle.TopLeft.ToVector2Num(), rectTangle.BottomRight.ToVector2Num())) + { + trees.Add(new TreeConfig.Tree + { + Tag = "Current atlas tree", + SkillTreeUrl = PathOfExileUrlDecoder.Encode(_plugin.GameController.Game.IngameState.ServerData.AtlasPassiveSkillIds.ToHashSet(), ESkillTreeType.Atlas) + }); + + _plugin.EditBuildData.Modified = true; + } + + foreach (var importer in _plugin.UrlImporters) + { + if (importer.DrawAddInterface() is { } newTree) + { + trees.Add(newTree); + _plugin.EditBuildData.Modified = true; + } + } + } + + private void DrawTreeEdit(List trees, int treeIndex) + { + ImGui.TableSetColumnIndex(0); + var rowLabel = string.IsNullOrEmpty(trees[treeIndex].Tag) ? $"row_{treeIndex}" : trees[treeIndex].Tag; + ImGui.PushID($"drag_{treeIndex}_{rowLabel.GetHashCode()}"); + + var dropTargetStart = ImGui.GetCursorScreenPos(); + + ImGui.PushStyleColor(ImGuiCol.Button, new Vector4(0, 0, 0, 0)); + ImGui.Button("=", new Vector2(30, 20)); + ImGui.PopStyleColor(); + + if (ImGui.BeginDragDropSource()) + { + ImGuiHelpers.SetDragDropPayload(TreeRowDragPayloadId, treeIndex); + ImGui.TextUnformatted(string.IsNullOrEmpty(trees[treeIndex].Tag) ? $"(tree {treeIndex})" : trees[treeIndex].Tag); + ImGui.EndDragDropSource(); + } + else if (ImGui.IsItemHovered()) + { + ImGui.SetTooltip("Drag to reorder"); + } + + ImGui.SetCursorScreenPos(dropTargetStart); + ImGui.InvisibleButton($"dropTreeRow##{treeIndex}", new Vector2(30, 20)); + + if (ImGui.BeginDragDropTarget()) + { + var payload = ImGuiHelpers.AcceptDragDropPayload(TreeRowDragPayloadId); + if (payload != null && ImGui.IsMouseReleased(ImGuiMouseButton.Left)) + { + var from = payload.Value; + if (from >= 0 && from < trees.Count && treeIndex >= 0 && treeIndex < trees.Count) + { + var moved = trees[from]; + trees.RemoveAt(from); + trees.Insert(treeIndex, moved); + _plugin.EditBuildData.Modified = true; + } + } + + ImGui.EndDragDropTarget(); + } + + ImGui.PopID(); + + ImGui.TableSetColumnIndex(1); + if (FullCellCenteredButton("X##REMOVERULE")) + { + trees.RemoveAt(treeIndex); + _plugin.EditBuildData.Modified = true; + return; + } + + ImGui.TableSetColumnIndex(2); + var iconsIndex = trees[treeIndex].Type switch + { + ESkillTreeType.Unknown => MapIconsIndex.QuestObject, + ESkillTreeType.Character => MapIconsIndex.MyPlayer, + ESkillTreeType.Atlas => MapIconsIndex.TangleAltar + }; + + var rect = SpriteHelper.GetUV(iconsIndex); + var iconSize = new Vector2(ImGui.CalcTextSize("A").Y); + CenterCellContentHorizontally(iconSize.X); + ImGui.Image(_plugin.Graphics.GetTextureId("Icons.png"), iconSize, rect.TopLeft.ToVector2Num(), rect.BottomRight.ToVector2Num()); + + ImGui.TableSetColumnIndex(3); + ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); + ImGui.InputText("##TAG", ref trees[treeIndex].Tag, 1024, ImGuiInputTextFlags.AutoSelectAll); + ImGui.PopItemWidth(); + + ImGui.TableSetColumnIndex(4); + ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.InputText("##GN", ref trees[treeIndex].SkillTreeUrl, 1024, ImGuiInputTextFlags.AutoSelectAll)) + { + trees[treeIndex].ResetType(); + _plugin.EditBuildData.Modified = true; + } + + ImGui.PopItemWidth(); + } +} \ No newline at end of file diff --git a/SkillNode.cs b/SkillNode.cs index 2a76299..fd0e939 100644 --- a/SkillNode.cs +++ b/SkillNode.cs @@ -1,15 +1,17 @@ -using System; +using PassiveSkillTreePlanter.SkillTreeJson; +using System; using System.Collections.Generic; using System.Numerics; -using PassiveSkillTreePlanter.SkillTreeJson; namespace PassiveSkillTreePlanter; public class SkillNode { - public Constants Constants { private get; init; } - public List OrbitRadii => Constants.OrbitRadii; - public List SkillsPerOrbit => Constants.SkillsPerOrbit; + private static readonly int[] Angles16 = {0, 30, 45, 60, 90, 120, 135, 150, 180, 210, 225, 240, 270, 300, 315, 330}; + + private static readonly int[] Angles40 = + {0, 10, 20, 30, 40, 45, 50, 60, 70, 80, 90, 100, 110, 120, 130, 135, 140, 150, 160, 170, 180, 190, 200, 210, 220, 225, 230, 240, 250, 260, 270, 280, 290, 300, 310, 315, 320, 330, 340, 350}; + public bool bJevel; public bool bKeyStone; public bool bMastery; @@ -20,41 +22,38 @@ public class SkillNode //Cached for drawing public float DrawSize = 100; public ushort Id; // "id": -28194677, - public List linkedNodes = new List(); - public string Name; //"dn": "Block Recovery", - public int Orbit; // "o": 1, + public List linkedNodes = new(); + public string Name; //"dn": "Block Recovery", + public int Orbit; // "o": 1, public long OrbitIndex; // "oidx": 3, public SkillNodeGroup SkillNodeGroup; + public Constants Constants { private get; init; } + public List OrbitRadii => Constants.OrbitRadii; + public List SkillsPerOrbit => Constants.SkillsPerOrbit; public Vector2 Position => GetPositionAtAngle(Arc); + public int OrbitRadius => OrbitRadii[Orbit]; + + public float Arc => GetOrbitAngle(OrbitIndex, SkillsPerOrbit[Orbit]); + public Vector2 GetPositionAtAngle(float angle) { if (SkillNodeGroup == null) return new Vector2(); return SkillNodeGroup.Position + OrbitRadius * GetAngleVector(angle); } - public int OrbitRadius => OrbitRadii[Orbit]; - - public float Arc => GetOrbitAngle(OrbitIndex, SkillsPerOrbit[Orbit]); - public void Init() { DrawPosition = Position; - if (bJevel) - DrawSize = 160; + if (bJevel) DrawSize = 160; - if (bNotable) - DrawSize = 170; + if (bNotable) DrawSize = 170; - if (bKeyStone) - DrawSize = 250; + if (bKeyStone) DrawSize = 250; } - private static readonly int[] Angles16 = { 0, 30, 45, 60, 90, 120, 135, 150, 180, 210, 225, 240, 270, 300, 315, 330 }; - private static readonly int[] Angles40 = { 0, 10, 20, 30, 40, 45, 50, 60, 70, 80, 90, 100, 110, 120, 130, 135, 140, 150, 160, 170, 180, 190, 200, 210, 220, 225, 230, 240, 250, 260, 270, 280, 290, 300, 310, 315, 320, 330, 340, 350 }; - private static float GetOrbitAngle(long orbitIndex, long maxNodePositions) { return maxNodePositions switch @@ -65,14 +64,11 @@ private static float GetOrbitAngle(long orbitIndex, long maxNodePositions) }; } - public static Vector2 GetAngleVector(float angle) - { - return new Vector2(MathF.Sin(angle), -MathF.Cos(angle)); - } + public static Vector2 GetAngleVector(float angle) => new(MathF.Sin(angle), -MathF.Cos(angle)); } public class SkillNodeGroup { - public List Nodes = new List(); + public List Nodes = new(); public Vector2 Position; } \ No newline at end of file diff --git a/TreeConfig.cs b/TreeConfig.cs index 4505f47..17bfc9c 100644 --- a/TreeConfig.cs +++ b/TreeConfig.cs @@ -1,7 +1,7 @@ -using System.Collections.Generic; +using Newtonsoft.Json; +using System.Collections.Generic; using System.IO; using System.Linq; -using Newtonsoft.Json; namespace PassiveSkillTreePlanter; @@ -22,8 +22,7 @@ public static SkillTreeData LoadBuild(string buildDirectory, string buildName) public static TSettingType LoadSettingFile(string fileName) { - if (!File.Exists(fileName)) - return default(TSettingType); + if (!File.Exists(fileName)) return default; return JsonConvert.DeserializeObject(File.ReadAllText(fileName)); } @@ -38,9 +37,9 @@ public static void SaveSettingFile(string fileName, TSettingType s public class Tree { - public string Tag = ""; - public string SkillTreeUrl = ""; private ESkillTreeType? _type; + public string SkillTreeUrl = ""; + public string Tag = ""; [JsonIgnore] public ESkillTreeType Type => _type ??= TreeEncoder.DecodeUrl(SkillTreeUrl) switch @@ -55,7 +54,7 @@ public class Tree public class SkillTreeData { public string Notes { get; set; } = ""; - public List Trees { get; set; } = new List(); + public List Trees { get; set; } = new(); public string BuildLink { get; set; } = ""; internal bool Modified { get; set; } } diff --git a/TreeEncoder.cs b/TreeEncoder.cs index 6d4025a..e935826 100644 --- a/TreeEncoder.cs +++ b/TreeEncoder.cs @@ -1,7 +1,7 @@ -using System; -using System.Collections.Generic; -using ExileCore; +using ExileCore; using PassiveSkillTreePlanter.UrlDecoders; +using System; +using System.Collections.Generic; namespace PassiveSkillTreePlanter; @@ -11,8 +11,7 @@ public static (HashSet Nodes, ESkillTreeType Type) DecodeUrl(string url) { try { - if (PoePlannerUrlDecoder.TryMatch(url, out var type, out var passiveIds) || - PathOfExileUrlDecoder.TryMatch(url, out type, out passiveIds)) + if (PoePlannerUrlDecoder.TryMatch(url, out var type, out var passiveIds) || PathOfExileUrlDecoder.TryMatch(url, out type, out passiveIds)) { return (passiveIds, type); } diff --git a/TreeGraph/DisjointSet.cs b/TreeGraph/DisjointSet.cs index 3dc9046..b877aee 100644 --- a/TreeGraph/DisjointSet.cs +++ b/TreeGraph/DisjointSet.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace PassiveSkillTreePlanter.TreeGraph; diff --git a/UrlDecoders/PathOfExileUrlDecoder.cs b/UrlDecoders/PathOfExileUrlDecoder.cs index a3532ca..8093d73 100644 --- a/UrlDecoders/PathOfExileUrlDecoder.cs +++ b/UrlDecoders/PathOfExileUrlDecoder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; diff --git a/UrlDecoders/PoePlannerUrlDecoder.cs b/UrlDecoders/PoePlannerUrlDecoder.cs index 4226e54..d443612 100644 --- a/UrlDecoders/PoePlannerUrlDecoder.cs +++ b/UrlDecoders/PoePlannerUrlDecoder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; diff --git a/UrlImporters/BaseUrlImporter.cs b/UrlImporters/BaseUrlImporter.cs index ac54631..138b2c9 100644 --- a/UrlImporters/BaseUrlImporter.cs +++ b/UrlImporters/BaseUrlImporter.cs @@ -1,4 +1,4 @@ -using ImGuiNET; +using ImGuiNET; using PassiveSkillTreePlanter.UrlDecoders; using System.Collections.Generic; using System.Linq; @@ -113,7 +113,6 @@ public TreeConfig.Tree DrawAddInterface() if (data.Count != 1) { - //select next tree after import _selectedVariant = Math.Min(_selectedVariant + 1, data.Count - 1); _selectedProgress = data[_selectedVariant]?.Passives?.Count ?? 0; } diff --git a/UrlImporters/PobCodeImporter.cs b/UrlImporters/PobCodeImporter.cs index f64870e..813036d 100644 --- a/UrlImporters/PobCodeImporter.cs +++ b/UrlImporters/PobCodeImporter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; diff --git a/UrlImporters/PobHelpers.cs b/UrlImporters/PobHelpers.cs index 7af1e8b..8619e98 100644 --- a/UrlImporters/PobHelpers.cs +++ b/UrlImporters/PobHelpers.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.IO.Compression; using System.Text; From ff6b3c8fb39cd21b2dc58a7fb22d3b915a93d279 Mon Sep 17 00:00:00 2001 From: Detective Squirrel <2137769+DetectiveSquirrel@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:00:35 +1300 Subject: [PATCH 2/2] Remove old unneeded menu separator --- PassiveSkillTreePlanterSettingsMenu.cs | 72 ++++++++++++-------------- 1 file changed, 32 insertions(+), 40 deletions(-) diff --git a/PassiveSkillTreePlanterSettingsMenu.cs b/PassiveSkillTreePlanterSettingsMenu.cs index 5db4d03..7323be2 100644 --- a/PassiveSkillTreePlanterSettingsMenu.cs +++ b/PassiveSkillTreePlanterSettingsMenu.cs @@ -51,54 +51,46 @@ public void Draw(Action drawCorePluginSettings) { _plugin.ProcessPendingOfficialTreeReload(); - ImGui.PushStyleVar(ImGuiStyleVar.ChildRounding, 5.0f); var contentRegionArea = ImGui.GetContentRegionAvail(); - if (ImGui.BeginChild("PluginSettingsRoot", contentRegionArea, ImGuiChildFlags.Border, ImGuiWindowFlags.None)) - { - if (ImGui.BeginTabBar("PassiveSkillTreePlanter_MainTabs", ImGuiTabBarFlags.None)) - { - if (ImGui.BeginTabItem("Passive Tree")) - { - DrawBuildSelectionSharedMenu(ESkillTreeType.Character); - var characterTrees = _plugin.SelectedBuildData.Trees.Where(t => t.Type == ESkillTreeType.Character).ToList(); - DrawTreeLoadTableSection(characterTrees, "CharLoad"); - DrawBuildNotesSubsection(_plugin.SelectedBuildData.Notes, ref _showCharacterBuildNotes, "CharSelNotes"); - ImGui.EndTabItem(); - } + if (!ImGui.BeginTabBar("PassiveSkillTreePlanter_MainTabs", ImGuiTabBarFlags.None)) return; - if (ImGui.BeginTabItem("Atlas Tree")) - { - DrawBuildSelectionSharedMenu(ESkillTreeType.Atlas); - var atlasTrees = _plugin.AtlasBuildData.Trees.Where(t => t.Type == ESkillTreeType.Atlas).ToList(); - DrawTreeLoadTableSection(atlasTrees, "AtlasLoad"); - DrawBuildNotesSubsection(_plugin.AtlasBuildData.Notes, ref _showAtlasBuildNotes, "AtlasSelNotes"); - ImGui.EndTabItem(); - } + if (ImGui.BeginTabItem("Passive Tree")) + { + DrawBuildSelectionSharedMenu(ESkillTreeType.Character); + var characterTrees = _plugin.SelectedBuildData.Trees.Where(t => t.Type == ESkillTreeType.Character).ToList(); + DrawTreeLoadTableSection(characterTrees, "CharLoad"); + DrawBuildNotesSubsection(_plugin.SelectedBuildData.Notes, ref _showCharacterBuildNotes, "CharSelNotes"); + ImGui.EndTabItem(); + } - if (ImGui.BeginTabItem("Build Edit")) - { - DrawBuildEdit(contentRegionArea); - ImGui.EndTabItem(); - } + if (ImGui.BeginTabItem("Atlas Tree")) + { + DrawBuildSelectionSharedMenu(ESkillTreeType.Atlas); + var atlasTrees = _plugin.AtlasBuildData.Trees.Where(t => t.Type == ESkillTreeType.Atlas).ToList(); + DrawTreeLoadTableSection(atlasTrees, "AtlasLoad"); + DrawBuildNotesSubsection(_plugin.AtlasBuildData.Notes, ref _showAtlasBuildNotes, "AtlasSelNotes"); + ImGui.EndTabItem(); + } - if (ImGui.BeginTabItem("GGG tree JSON")) - { - DrawOfficialTreeExportTab(); - ImGui.EndTabItem(); - } + if (ImGui.BeginTabItem("Build Edit")) + { + DrawBuildEdit(contentRegionArea); + ImGui.EndTabItem(); + } - if (ImGui.BeginTabItem("Settings")) - { - drawCorePluginSettings(); - ImGui.EndTabItem(); - } + if (ImGui.BeginTabItem("GGG tree JSON")) + { + DrawOfficialTreeExportTab(); + ImGui.EndTabItem(); + } - ImGui.EndTabBar(); - } + if (ImGui.BeginTabItem("Settings")) + { + drawCorePluginSettings(); + ImGui.EndTabItem(); } - ImGui.PopStyleVar(); - ImGui.EndChild(); + ImGui.EndTabBar(); } private void DrawOfficialTreeExportTab()