diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 2925ccc..0000000 --- a/.editorconfig +++ /dev/null @@ -1,203 +0,0 @@ -# Remove the line below if you want to inherit .editorconfig settings from higher directories -root = true - -# C# files -[*.{cs,json}] - -#### Core EditorConfig Options #### - -# Indentation and spacing -indent_size = 4 -indent_style = space -tab_width = 4 - -# New line preferences -end_of_line = lf -insert_final_newline = true - -[*.cs] - -#### .NET Coding Conventions #### - -# Organize usings -dotnet_separate_import_directive_groups = false -dotnet_sort_system_directives_first = false -file_header_template = unset - -# this. and Me. preferences -dotnet_style_qualification_for_event = true:silent -dotnet_style_qualification_for_field = true:silent -dotnet_style_qualification_for_method = true:silent -dotnet_style_qualification_for_property = true:silent - -# Language keywords vs BCL types preferences -dotnet_style_predefined_type_for_locals_parameters_members = true:silent -dotnet_style_predefined_type_for_member_access = true:silent - -# Parentheses preferences -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent -dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent - -# Modifier preferences -dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent - -# Expression-level preferences -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_explicit_tuple_names = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_object_initializer = true:suggestion -dotnet_style_operator_placement_when_wrapping = beginning_of_line -dotnet_style_prefer_auto_properties = true:silent -dotnet_style_prefer_compound_assignment = true:suggestion -dotnet_style_prefer_conditional_expression_over_assignment = true:silent -dotnet_style_prefer_conditional_expression_over_return = true:silent -dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion -dotnet_style_prefer_inferred_tuple_names = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion -dotnet_style_prefer_simplified_boolean_expressions = true:suggestion -dotnet_style_prefer_simplified_interpolation = true:suggestion - -# Field preferences -dotnet_style_readonly_field = true:suggestion - -# Parameter preferences -dotnet_code_quality_unused_parameters = all:suggestion - -#### C# Coding Conventions #### - -# var preferences -csharp_style_var_elsewhere = false:silent -csharp_style_var_for_built_in_types = false:silent -csharp_style_var_when_type_is_apparent = false:silent - -# Expression-bodied members -csharp_style_expression_bodied_accessors = true:silent -csharp_style_expression_bodied_constructors = false:silent -csharp_style_expression_bodied_indexers = true:silent -csharp_style_expression_bodied_lambdas = true:silent -csharp_style_expression_bodied_local_functions = false:silent -csharp_style_expression_bodied_methods = false:silent -csharp_style_expression_bodied_operators = false:silent -csharp_style_expression_bodied_properties = true:silent - -# Pattern matching preferences -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion -csharp_style_prefer_switch_expression = true:suggestion - -# Null-checking preferences -csharp_style_conditional_delegate_call = true:suggestion - -# Modifier preferences -csharp_prefer_static_local_function = true:suggestion -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:silent - -# Code-block preferences -csharp_prefer_braces = true:error -csharp_prefer_simple_using_statement = true:suggestion - -# Expression-level preferences -csharp_prefer_simple_default_expression = true:suggestion -csharp_style_deconstructed_variable_declaration = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion -csharp_style_pattern_local_over_anonymous_function = true:suggestion -csharp_style_prefer_index_operator = true:suggestion -csharp_style_prefer_range_operator = true:suggestion -csharp_style_throw_expression = true:suggestion -csharp_style_unused_value_assignment_preference = discard_variable:suggestion -csharp_style_unused_value_expression_statement_preference = discard_variable:silent - -# 'using' directive preferences -csharp_using_directive_placement = outside_namespace:silent - -#### C# Formatting Rules #### - -# New line preferences -csharp_new_line_before_catch = false -csharp_new_line_before_else = false -csharp_new_line_before_finally = false -csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_open_brace = none -csharp_new_line_between_query_expression_clauses = true - -# Indentation preferences -csharp_indent_block_contents = true -csharp_indent_braces = false -csharp_indent_case_contents = true -csharp_indent_case_contents_when_block = true -csharp_indent_labels = one_less_than_current -csharp_indent_switch_labels = true - -# Space preferences -csharp_space_after_cast = false -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_after_comma = true -csharp_space_after_dot = false -csharp_space_after_keywords_in_control_flow_statements = true -csharp_space_after_semicolon_in_for_statement = true -csharp_space_around_binary_operators = before_and_after -csharp_space_around_declaration_statements = false -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_before_comma = false -csharp_space_before_dot = false -csharp_space_before_open_square_brackets = false -csharp_space_before_semicolon_in_for_statement = false -csharp_space_between_empty_square_brackets = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_declaration_name_and_open_parenthesis = false -csharp_space_between_method_declaration_parameter_list_parentheses = false -csharp_space_between_parentheses = false -csharp_space_between_square_brackets = false - -# Wrapping preferences -csharp_preserve_single_line_blocks = true -csharp_preserve_single_line_statements = true - -#### Naming styles #### - -# Naming rules - -dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion -dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface -dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i - -dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.types_should_be_pascal_case.symbols = types -dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case - -dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members -dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case - -# Symbol specifications - -dotnet_naming_symbols.interface.applicable_kinds = interface -dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = - -dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum -dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = - -dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method -dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = - -# Naming styles - -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case - -dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = -dotnet_naming_style.begins_with_i.capitalization = pascal_case diff --git a/HudSwap.sln b/HUD Manager.sln similarity index 87% rename from HudSwap.sln rename to HUD Manager.sln index 8654e15..708e877 100644 --- a/HudSwap.sln +++ b/HUD Manager.sln @@ -3,7 +3,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30320.27 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HudSwap", "HudSwap\HudSwap.csproj", "{CBF13A29-A5A4-4FA2-8A4F-CFBB5FB83F90}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HUD Manager", "HUD Manager\HUD Manager.csproj", "{CBF13A29-A5A4-4FA2-8A4F-CFBB5FB83F90}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/HUD Manager/Configuration/Config.cs b/HUD Manager/Configuration/Config.cs new file mode 100644 index 0000000..2a61ac8 --- /dev/null +++ b/HUD Manager/Configuration/Config.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using Dalamud.Configuration; +using Dalamud.Plugin; + +namespace HUD_Manager.Configuration { + [Serializable] + public class Config : IPluginConfiguration { + public int Version { get; set; } = 2; + + private DalamudPluginInterface Interface { get; set; } = null!; + + public bool FirstRun { get; set; } = true; + public bool UnderstandsRisks { get; set; } + + public bool ImportPositions { get; set; } + public bool SwapsEnabled { get; set; } + + public HudSlot StagingSlot { get; set; } = HudSlot.Four; + + public Dictionary Layouts { get; } = new(); + + public List HudConditionMatches { get; } = new(); + + public void Initialize(DalamudPluginInterface pluginInterface) { + this.Interface = pluginInterface; + } + + public void Save() { + this.Interface.SavePluginConfig(this); + } + } +} diff --git a/HUD Manager/Configuration/ConfigV1.cs b/HUD Manager/Configuration/ConfigV1.cs new file mode 100755 index 0000000..fc3f231 --- /dev/null +++ b/HUD Manager/Configuration/ConfigV1.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace HUD_Manager.Configuration { + [Serializable] + public class ConfigV1 { + public bool FirstRun { get; set; } + public bool UnderstandsRisks { get; set; } + public bool SwapsEnabled { get; set; } + public HudSlot StagingSlot { get; set; } + public Dictionary Layouts2 { get; set; } = null!; + } + + [Serializable] + public class ConfigV1Layout { + public string Name { get; set; } = null!; + public Dictionary> Positions { get; set; } = null!; + public byte[] Hud { get; set; } = null!; + } +} diff --git a/HUD Manager/Configuration/Migrator.cs b/HUD Manager/Configuration/Migrator.cs new file mode 100755 index 0000000..e51722a --- /dev/null +++ b/HUD Manager/Configuration/Migrator.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; +using HUD_Manager.Structs; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace HUD_Manager.Configuration { + public static class Migrator { + private static Config Migrate(ConfigV1 old) { + var config = new Config { + FirstRun = old.FirstRun, + StagingSlot = old.StagingSlot, + SwapsEnabled = old.SwapsEnabled, + UnderstandsRisks = old.UnderstandsRisks, + }; + + foreach (var entry in old.Layouts2) { + Layout layout; + unsafe { + fixed (byte* ptr = entry.Value.Hud) { + layout = Marshal.PtrToStructure((IntPtr) ptr); + } + } + + var saved = new SavedLayout(entry.Value.Name, layout, entry.Value.Positions); + config.Layouts[entry.Key] = saved; + } + + return config; + } + + private static string PluginConfig(string? pluginName = null) { + pluginName ??= Assembly.GetAssembly(typeof(Plugin)).GetName().Name; + return Path.Combine(new[] { + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "XIVLauncher", + "pluginConfigs", + $"{pluginName}.json", + }); + } + + public static Config LoadConfig(Plugin plugin) { + var managerPath = PluginConfig(); + + if (File.Exists(managerPath)) { + goto DefaultConfig; + } + + var hudSwapPath = PluginConfig("HudSwap"); + + if (File.Exists(hudSwapPath)) { + var oldText = File.ReadAllText(hudSwapPath); + var config = JsonConvert.DeserializeObject(oldText); + uint version = 1; + if (config.TryGetValue("Version", out var token)) { + version = token.Value(); + } + + if (version == 1) { + var v1 = config.ToObject(new JsonSerializer { + TypeNameHandling = TypeNameHandling.None, + }); + + return Migrate(v1); + } + } + + DefaultConfig: + return plugin.Interface.GetPluginConfig() as Config ?? new Config(); + } + } +} diff --git a/HUD Manager/Configuration/SavedLayout.cs b/HUD Manager/Configuration/SavedLayout.cs new file mode 100644 index 0000000..c3629f6 --- /dev/null +++ b/HUD Manager/Configuration/SavedLayout.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using HUD_Manager.Structs; +using Newtonsoft.Json; + +namespace HUD_Manager.Configuration { + [Serializable] + public class SavedLayout { + public Dictionary Elements { get; } + public Dictionary> Positions { get; private set; } + + public string Name { get; private set; } + + [JsonConstructor] + public SavedLayout(string name, Dictionary elements, Dictionary> positions) { + this.Name = name; + this.Elements = elements; + this.Positions = positions; + } + + public SavedLayout(string name, Layout hud, Dictionary> positions) { + this.Name = name; + this.Elements = hud.ToDictionary(); + this.Positions = positions; + } + + public Layout ToLayout() { + var elements = this.Elements.Values.ToList(); + + while (elements.Count < 81) { + elements.Add(new Element()); + } + + return new Layout { + elements = elements.ToArray(), + }; + } + } +} diff --git a/HUD Manager/DalamudPackager.targets b/HUD Manager/DalamudPackager.targets new file mode 100755 index 0000000..f22e520 --- /dev/null +++ b/HUD Manager/DalamudPackager.targets @@ -0,0 +1,10 @@ + + + + + diff --git a/HUD Manager/GameFunctions.cs b/HUD Manager/GameFunctions.cs new file mode 100644 index 0000000..6da04ee --- /dev/null +++ b/HUD Manager/GameFunctions.cs @@ -0,0 +1,60 @@ +using System; +using System.Runtime.InteropServices; + +namespace HUD_Manager { + public class GameFunctions { + private delegate IntPtr GetUiBaseDelegate(); + private delegate IntPtr GetUiWindowDelegate(IntPtr uiBase, string uiName, int index); + private delegate void MoveWindowDelegate(IntPtr windowBase, short x, short y); + + private readonly GetUiBaseDelegate _getUiBase; + private readonly GetUiWindowDelegate _getUiWindow; + private readonly MoveWindowDelegate _moveWindow; + + private Plugin Plugin { get; } + + public GameFunctions(Plugin plugin) { + this.Plugin = plugin; + + var getUiBasePtr = this.Plugin.Interface.TargetModuleScanner.ScanText("E8 ?? ?? ?? ?? 41 b8 01 00 00 00 48 8d 15 ?? ?? ?? ?? 48 8b 48 20 e8 ?? ?? ?? ?? 48 8b cf"); + var getUiWindowPtr = this.Plugin.Interface.TargetModuleScanner.ScanText("e8 ?? ?? ?? ?? 48 8b cf 48 89 87 ?? ?? 00 00 e8 ?? ?? ?? ?? 41 b8 01 00 00 00"); + var moveWindowPtr = this.Plugin.Interface.TargetModuleScanner.ScanText("E8 ?? ?? ?? ?? 48 83 BB ?? ?? ?? ?? 00 74 ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 ?? E8 ?? ?? ?? ??"); + + this._getUiBase = Marshal.GetDelegateForFunctionPointer(getUiBasePtr); + this._getUiWindow = Marshal.GetDelegateForFunctionPointer(getUiWindowPtr); + this._moveWindow = Marshal.GetDelegateForFunctionPointer(moveWindowPtr); + } + + private IntPtr GetUiBase() { + return this._getUiBase.Invoke(); + } + + private IntPtr GetUiWindow(string uiName, int index) { + var uiBase = this.GetUiBase(); + var offset = Marshal.ReadIntPtr(uiBase, 0x20); + + return this._getUiWindow.Invoke(offset, uiName, index); + } + + public void MoveWindow(string uiName, short x, short y) { + var windowBase = this.GetUiWindow(uiName, 1); + if (windowBase == IntPtr.Zero) { + return; + } + + this._moveWindow.Invoke(windowBase, x, y); + } + + public Vector2? GetWindowPosition(string uiName) { + var windowBase = this.GetUiWindow(uiName, 1); + if (windowBase == IntPtr.Zero) { + return null; + } + + var x = Marshal.ReadInt16(windowBase + 0x1bc); + var y = Marshal.ReadInt16(windowBase + 0x1bc + 2); + + return new Vector2(x, y); + } + } +} diff --git a/HUD Manager/HUD Manager.csproj b/HUD Manager/HUD Manager.csproj new file mode 100755 index 0000000..1801328 --- /dev/null +++ b/HUD Manager/HUD Manager.csproj @@ -0,0 +1,39 @@ + + + Library + net48 + 2.0.0.1 + latest + true + enable + + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll + False + + + $(AppData)\XIVLauncher\addon\Hooks\dev\Newtonsoft.Json.dll + False + + + + + + diff --git a/HUD Manager/HUD Manager.yaml b/HUD Manager/HUD Manager.yaml new file mode 100644 index 0000000..d9210a1 --- /dev/null +++ b/HUD Manager/HUD Manager.yaml @@ -0,0 +1,10 @@ +author: ascclemens +name: HUD Manager +description: | + A plugin to manage your HUD. + + - Save infinite HUD layouts + - Swap between HUD layouts based on conditions + - Edit HUD elements precisely without /hudlayout + - Export and import layouts +repo_url: https://git.sr.ht/~jkcclemens/HUDManager diff --git a/HUD Manager/Hud.cs b/HUD Manager/Hud.cs new file mode 100644 index 0000000..44d06cc --- /dev/null +++ b/HUD Manager/Hud.cs @@ -0,0 +1,145 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using HUD_Manager.Structs; + +namespace HUD_Manager { + public class Hud { + // Updated 5.45 + // This is 81 elements * 36 bytes each. Each element is 32 bytes in ADDON.DAT, + // but they're 36 bytes when loaded into memory. + private const int LayoutSize = 0xb64; + // Updated 5.4 + private const int SlotOffset = 0x59e8; + + private delegate IntPtr GetFilePointerDelegate(byte index); + + private delegate uint SetHudLayoutDelegate(IntPtr filePtr, uint hudLayout, byte unk0, byte unk1); + + private readonly GetFilePointerDelegate? _getFilePointer; + private readonly SetHudLayoutDelegate? _setHudLayout; + + private Plugin Plugin { get; } + + public Hud(Plugin plugin) { + this.Plugin = plugin; + var getFilePointerPtr = this.Plugin.Interface.TargetModuleScanner.ScanText("E8 ?? ?? ?? ?? 48 85 C0 74 14 83 7B 44 00"); + var setHudLayoutPtr = this.Plugin.Interface.TargetModuleScanner.ScanText("E8 ?? ?? ?? ?? 33 C0 EB 15"); + if (getFilePointerPtr != IntPtr.Zero) { + this._getFilePointer = Marshal.GetDelegateForFunctionPointer(getFilePointerPtr); + } + + if (setHudLayoutPtr != IntPtr.Zero) { + this._setHudLayout = Marshal.GetDelegateForFunctionPointer(setHudLayoutPtr); + } + } + + private IntPtr GetFilePointer(byte index) { + return this._getFilePointer?.Invoke(index) ?? IntPtr.Zero; + } + + public void SelectSlot(HudSlot slot, bool force = false) { + if (this._setHudLayout == null) { + return; + } + + var file = this.GetFilePointer(0); + // change the current slot so the game lets us pick one that's currently in use + if (!force) { + goto Return; + } + + var currentSlotPtr = this.GetDataPointer() + SlotOffset; + // read the current slot + var currentSlot = (uint) Marshal.ReadInt32(currentSlotPtr); + // change it to a different slot + if (currentSlot == (uint) slot) { + if (currentSlot < 3) { + currentSlot += 1; + } else { + currentSlot = 0; + } + + // back up this different slot + var backup = this.ReadLayout((HudSlot) currentSlot); + // change the current slot in memory + Marshal.WriteInt32(currentSlotPtr, (int) currentSlot); + // ask the game to change slot to our desired slot + // for some reason, this overwrites the current slot, so this is why we back up + this._setHudLayout.Invoke(file, (uint) slot, 0, 1); + // restore the backup + this.WriteLayout((HudSlot) currentSlot, backup); + return; + } + + Return: + this._setHudLayout.Invoke(file, (uint) slot, 0, 1); + } + + private IntPtr GetDataPointer() { + var dataPtr = this.GetFilePointer(0) + 0x50; + return Marshal.ReadIntPtr(dataPtr); + } + + internal IntPtr GetLayoutPointer(HudSlot slot) { + var slotNum = (int) slot; + return this.GetDataPointer() + 0x2c58 + slotNum * LayoutSize; + } + + public HudSlot GetActiveHudSlot() { + var slotVal = Marshal.ReadInt32(this.GetDataPointer() + SlotOffset); + + if (!Enum.IsDefined(typeof(HudSlot), slotVal)) { + throw new IOException($"invalid hud slot in FFXIV memory of ${slotVal}"); + } + + return (HudSlot) slotVal; + } + + public Layout ReadLayout(HudSlot slot) { + var slotPtr = this.GetLayoutPointer(slot); + return Marshal.PtrToStructure(slotPtr); + } + + public void WriteLayout(HudSlot slot, Layout layout) { + var slotPtr = this.GetLayoutPointer(slot); + + var dict = layout.ToDictionary(); + + // update existing elements with saved data instead of wholesale overwriting + var slotLayout = this.ReadLayout(slot); + for (var i = 0; i < slotLayout.elements.Length; i++) { + if (dict.TryGetValue(slotLayout.elements[i].id, out var element)) { + slotLayout.elements[i] = element; + } + } + + Marshal.StructureToPtr(slotLayout, slotPtr, false); + + // copy directly over + // Marshal.StructureToPtr(layout, slotPtr, false); + + var currentSlot = this.GetActiveHudSlot(); + if (currentSlot == slot) { + this.SelectSlot(currentSlot, true); + } + } + } + + public enum HudSlot { + One = 0, + Two = 1, + Three = 2, + Four = 3, + } + + public class Vector2 { + public T X { get; } + public T Y { get; } + + public Vector2(T x, T y) { + this.X = x; + this.Y = y; + } + } +} diff --git a/HUD Manager/Plugin.cs b/HUD Manager/Plugin.cs new file mode 100644 index 0000000..c478b13 --- /dev/null +++ b/HUD Manager/Plugin.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Game.Command; +using Dalamud.Plugin; +using HUD_Manager.Configuration; + +namespace HUD_Manager { + public class Plugin : IDalamudPlugin { + public string Name => "HUD Manager"; + + private PluginUi Ui { get; set; } = null!; + private Swapper Swapper { get; set; } = null!; + + public DalamudPluginInterface Interface { get; private set; } = null!; + public Hud Hud { get; private set; } = null!; + public Statuses Statuses { get; private set; } = null!; + public GameFunctions GameFunctions { get; private set; } = null!; + public Config Config { get; private set; } = null!; + + public void Initialize(DalamudPluginInterface pluginInterface) { + this.Interface = pluginInterface; + + this.Config = Migrator.LoadConfig(this); + this.Config.Initialize(this.Interface); + this.Config.Save(); + + this.Ui = new PluginUi(this); + this.Hud = new Hud(this); + this.Statuses = new Statuses(this); + this.GameFunctions = new GameFunctions(this); + this.Swapper = new Swapper(this); + + if (this.Config.FirstRun) { + this.Config.FirstRun = false; + if (this.Config.Layouts.Count == 0) { + foreach (HudSlot slot in Enum.GetValues(typeof(HudSlot))) { + this.Ui.ImportSlot($"Auto-import {(int) slot + 1}", slot, false); + } + } + + this.Config.Save(); + } + + this.Interface.UiBuilder.OnBuildUi += this.Ui.Draw; + this.Interface.UiBuilder.OnOpenConfigUi += this.Ui.ConfigUi; + this.Interface.Framework.OnUpdateEvent += this.Swapper.OnFrameworkUpdate; + + this.Interface.CommandManager.AddHandler("/hud", new CommandInfo(this.OnCommand) { + HelpMessage = "Open the HUD Manager settings or swap to layout name", + }); + } + + public void Dispose() { + this.Interface.UiBuilder.OnBuildUi -= this.Ui.Draw; + this.Interface.UiBuilder.OnOpenConfigUi -= this.Ui.ConfigUi; + this.Interface.Framework.OnUpdateEvent -= this.Swapper.OnFrameworkUpdate; + this.Interface.CommandManager.RemoveHandler("/hud"); + } + + private void OnCommand(string command, string args) { + if (string.IsNullOrWhiteSpace(args)) { + this.Ui.SettingsVisible = true; + return; + } + + var entry = this.Config.Layouts.FirstOrDefault(e => e.Value.Name == args); + if (entry.Equals(default(KeyValuePair))) { + return; + } + + var layout = entry.Value; + + this.Hud.WriteLayout(this.Config.StagingSlot, layout.ToLayout()); + this.Hud.SelectSlot(this.Config.StagingSlot, true); + } + } +} diff --git a/HUD Manager/PluginUi.cs b/HUD Manager/PluginUi.cs new file mode 100644 index 0000000..601ce35 --- /dev/null +++ b/HUD Manager/PluginUi.cs @@ -0,0 +1,639 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using System.Runtime.InteropServices; +using Dalamud.Interface; +using Dalamud.Plugin; +using HUD_Manager.Configuration; +using HUD_Manager.Structs; +using ImGuiNET; +using Lumina.Excel.GeneratedSheets; +using Newtonsoft.Json; +using Action = System.Action; + +// TODO: Zone swaps? + +namespace HUD_Manager { + public class PluginUi { + private static readonly string[] SavedWindows = { + "AreaMap", + "ChatLog", + "ChatLogPanel_0", + "ChatLogPanel_1", + "ChatLogPanel_2", + "ChatLogPanel_3", + }; + + private Plugin Plugin { get; } + + private bool _settingsVisible; + + public bool SettingsVisible { + get => this._settingsVisible; + set => this._settingsVisible = value; + } + + private string _importName = ""; + private string _renameName = ""; + private Guid _selectedLayoutId = Guid.Empty; + + private SavedLayout? SelectedSavedLayout => this._selectedLayoutId == Guid.Empty ? null : this.Plugin.Config.Layouts[this._selectedLayoutId]; + + private int _editingConditionIndex = -1; + private HudConditionMatch? _editingCondition; + private bool _scrollToAdd; + + public PluginUi(Plugin plugin) { + this.Plugin = plugin; + } + + public void ConfigUi(object sender, EventArgs args) { + this.SettingsVisible = true; + } + + private void DrawSettings() { + void DrawHudSlotButtons(string idSuffix, Action hudSlotAction, Action clipboardAction) { + var slotButtonSize = new Vector2(40, 0); + foreach (HudSlot slot in Enum.GetValues(typeof(HudSlot))) { + // Surround the button with parentheses if this is the current slot + var slotText = slot == this.Plugin.Hud.GetActiveHudSlot() ? $"({(int) slot + 1})" : ((int) slot + 1).ToString(); + var buttonName = $"{slotText}##${idSuffix}"; + if (ImGui.Button(buttonName, slotButtonSize)) { + PluginLog.Log("Importing outer"); + hudSlotAction(slot); + } + + ImGui.SameLine(); + } + + ImGui.SameLine(); + + if (ImGui.Button($"Clipboard##{idSuffix}")) { + clipboardAction(); + } + } + + if (!this.SettingsVisible) { + return; + } + + ImGui.SetNextWindowSize(new Vector2(500, 475), ImGuiCond.FirstUseEver); + + if (!ImGui.Begin(this.Plugin.Name, ref this._settingsVisible)) { + return; + } + + if (ImGui.BeginTabBar("##hudmanager-tabs")) { + if (!this.Plugin.Config.UnderstandsRisks) { + if (ImGui.BeginTabItem("About")) { + ImGui.TextColored(new Vector4(1f, 0f, 0f, 1f), "Read this first"); + ImGui.Separator(); + ImGui.PushTextWrapPos(ImGui.GetFontSize() * 20f); + ImGui.Text("HUD Manager will use the configured staging slot as its own slot to make changes to. This means the staging slot will be overwritten whenever any swap happens."); + ImGui.Spacing(); + ImGui.Text("Any HUD layout changes you make while HUD Manager is enabled may potentially be lost, no matter what slot. If you want to make changes to your HUD layout, TURN OFF HUD Manager first."); + ImGui.Spacing(); + ImGui.Text("When editing or making a new layout, to be completely safe, turn off swaps, set up your layout, import the layout into HUD Manager, then turn on swaps."); + ImGui.Spacing(); + ImGui.Text("If you are a new user, HUD Manager auto-imported your existing layouts on startup."); + ImGui.Spacing(); + ImGui.Text("Finally, HUD Manager is beta software. Back up your character data before using this plugin. You may lose some to all of your HUD layouts while testing this plugin."); + ImGui.Separator(); + ImGui.Text("If you have read all of the above and are okay with continuing, check the box below to enable HUD Manager. You only need to do this once."); + ImGui.PopTextWrapPos(); + var understandsRisks = this.Plugin.Config.UnderstandsRisks; + if (ImGui.Checkbox("I understand", ref understandsRisks)) { + this.Plugin.Config.UnderstandsRisks = understandsRisks; + this.Plugin.Config.Save(); + } + + ImGui.EndTabItem(); + } + + ImGui.EndTabBar(); + ImGui.End(); + return; + } + + if (ImGui.BeginTabItem("Layouts")) { + ImGui.Text("Saved layouts"); + if (this.Plugin.Config.Layouts.Count == 0) { + ImGui.Text("None saved!"); + } else { + ImGui.PushItemWidth(-1); + if (ImGui.ListBoxHeader("##saved-layouts")) { + foreach (var entry in this.Plugin.Config.Layouts) { + if (!ImGui.Selectable($"{entry.Value.Name}##{entry.Key}", this._selectedLayoutId == entry.Key)) { + continue; + } + + this._selectedLayoutId = entry.Key; + this._renameName = entry.Value.Name; + this._importName = this._renameName; + } + + ImGui.ListBoxFooter(); + } + + ImGui.PopItemWidth(); + + ImGui.PushItemWidth(200); + ImGui.InputText("##rename-input", ref this._renameName, 100); + ImGui.PopItemWidth(); + ImGui.SameLine(); + if (ImGui.Button("Rename") && this._renameName.Length != 0 && this.SelectedSavedLayout != null) { + var layout = this.Plugin.Config.Layouts[this._selectedLayoutId]; + var newLayout = new SavedLayout(this._renameName, layout.ToLayout(), layout.Positions); + this.Plugin.Config.Layouts[this._selectedLayoutId] = newLayout; + this.Plugin.Config.Save(); + } + + const int layoutActionButtonWidth = 30; + // `layoutActionButtonWidth` must be multiplied by however many action buttons there are here + ImGui.SameLine(ImGui.GetWindowContentRegionWidth() - layoutActionButtonWidth * 1); + ImGui.PushFont(UiBuilder.IconFont); + if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString(), new Vector2(layoutActionButtonWidth, 0)) && + this.SelectedSavedLayout != null) { + this.Plugin.Config.Layouts.Remove(this._selectedLayoutId); + this.Plugin.Config.HudConditionMatches.RemoveAll(m => m.LayoutId == this._selectedLayoutId); + this._selectedLayoutId = Guid.Empty; + this._renameName = ""; + this.Plugin.Config.Save(); + } + + ImGui.PopFont(); + + ImGui.Text("Copy to..."); + DrawHudSlotButtons("copy", slot => { + if (this.SelectedSavedLayout == null) { + return; + } + + this.Plugin.Hud.WriteLayout(slot, this.SelectedSavedLayout.ToLayout()); + }, () => { + if (this.SelectedSavedLayout == null) { + return; + } + + var json = JsonConvert.SerializeObject(this.SelectedSavedLayout); + ImGui.SetClipboardText(json); + }); + } + + ImGui.Separator(); + + ImGui.Text("Import"); + + ImGui.InputText("Imported layout name", ref this._importName, 100); + + var importPositions = this.Plugin.Config.ImportPositions; + if (ImGui.Checkbox("Import window positions", ref importPositions)) { + this.Plugin.Config.ImportPositions = importPositions; + this.Plugin.Config.Save(); + } + + ImGui.SameLine(); + HelpMarker("If this is checked, the position of the chat box and the map will be saved with the imported layout."); + + var isOverwriting = this.Plugin.Config.Layouts.Values.Any(layout => layout.Name == this._importName); + ImGui.Text((isOverwriting ? "Overwrite" : "Import") + " from..."); + + DrawHudSlotButtons("import", slot => { + PluginLog.Log("Importing inner"); + this.ImportSlot(this._importName, slot); + this._importName = ""; + }, () => { + SavedLayout? shared = null; + try { + shared = JsonConvert.DeserializeObject(ImGui.GetClipboardText()); + } catch (Exception) { + // ignored + } + + if (shared == null) { + return; + } + + this.Import(this._importName, shared.ToLayout(), shared.Positions); + this._importName = ""; + }); + + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Swaps")) { + var enabled = this.Plugin.Config.SwapsEnabled; + if (ImGui.Checkbox("Enable swaps", ref enabled)) { + this.Plugin.Config.SwapsEnabled = enabled; + this.Plugin.Config.Save(); + } + + ImGui.Text("Note: Disable swaps when editing your HUD."); + + ImGui.Spacing(); + var staging = ((int) this.Plugin.Config.StagingSlot + 1).ToString(); + if (ImGui.BeginCombo("Staging slot", staging)) { + foreach (HudSlot slot in Enum.GetValues(typeof(HudSlot))) { + if (!ImGui.Selectable(((int) slot + 1).ToString())) { + continue; + } + + this.Plugin.Config.StagingSlot = slot; + this.Plugin.Config.Save(); + } + + ImGui.EndCombo(); + } + + ImGui.SameLine(); + HelpMarker("The staging slot is the HUD layout slot that will be used as your HUD layout. All changes will be written to this slot when swaps are enabled."); + + ImGui.Separator(); + + if (this.Plugin.Config.Layouts.Count == 0) { + ImGui.Text("Create at least one layout to begin setting up swaps."); + } else { + ImGui.Text("Add swap conditions below.\nThe first condition that is satisfied will be the layout that is used."); + ImGui.Separator(); + this.DrawConditionsTable(); + } + + ImGui.EndTabItem(); + } + + #if DEBUG + if (ImGui.BeginTabItem("Debug")) { + ImGui.TextUnformatted("Print layout pointer address"); + + if (ImGui.Button("1")) { + var ptr = this.Plugin.Hud.GetLayoutPointer(HudSlot.One); + this.Plugin.Interface.Framework.Gui.Chat.Print($"{ptr.ToInt64():x}"); + } + + ImGui.SameLine(); + + if (ImGui.Button("2")) { + var ptr = this.Plugin.Hud.GetLayoutPointer(HudSlot.Two); + this.Plugin.Interface.Framework.Gui.Chat.Print($"{ptr.ToInt64():x}"); + } + + ImGui.SameLine(); + + if (ImGui.Button("3")) { + var ptr = this.Plugin.Hud.GetLayoutPointer(HudSlot.Three); + this.Plugin.Interface.Framework.Gui.Chat.Print($"{ptr.ToInt64():x}"); + } + + ImGui.SameLine(); + + if (ImGui.Button("4")) { + var ptr = this.Plugin.Hud.GetLayoutPointer(HudSlot.Four); + this.Plugin.Interface.Framework.Gui.Chat.Print($"{ptr.ToInt64():x}"); + } + + if (ImGui.Button("Save layout")) { + var ptr = this.Plugin.Hud.GetLayoutPointer(HudSlot.One); + var layout = Marshal.PtrToStructure(ptr); + this.PreviousLayout = layout; + } + + ImGui.SameLine(); + + if (ImGui.Button("Find difference")) { + var ptr = this.Plugin.Hud.GetLayoutPointer(HudSlot.One); + var layout = Marshal.PtrToStructure(ptr); + + foreach (var prevElem in this.PreviousLayout.elements) { + var currElem = layout.elements.FirstOrDefault(el => el.id == prevElem.id); + if (currElem.visibility == prevElem.visibility && !(Math.Abs(currElem.x - prevElem.x) > .01)) { + continue; + } + + PluginLog.Log(currElem.id.ToString()); + this.Plugin.Interface.Framework.Gui.Chat.Print(currElem.id.ToString()); + } + } + + if (ImGui.BeginChild("ui-elements", new Vector2(0, 0))) { + var ptr = this.Plugin.Hud.GetLayoutPointer(this.Plugin.Hud.GetActiveHudSlot()); + var layout = Marshal.PtrToStructure(ptr); + + var changed = false; + + var elements = (ElementKind[]) Enum.GetValues(typeof(ElementKind)); + foreach (var kind in elements.OrderBy(el => el.ToString())) { + for (var i = 0; i < layout.elements.Length; i++) { + if (layout.elements[i].id != kind) { + continue; + } + + ImGui.TextUnformatted(kind.ToString()); + + var x = layout.elements[i].x; + if (ImGui.DragFloat($"X##{kind}", ref x)) { + layout.elements[i].x = x; + changed = true; + } + + var y = layout.elements[i].y; + if (ImGui.DragFloat($"Y##{kind}", ref y)) { + layout.elements[i].y = y; + changed = true; + } + + var visible = layout.elements[i].visibility == Visibility.Visible; + if (ImGui.Checkbox($"Visible##{kind}", ref visible)) { + layout.elements[i].visibility = visible ? Visibility.Visible : Visibility.Hidden; + changed = true; + } + + var scale = layout.elements[i].scale; + if (ImGui.DragFloat($"Scale##{kind}", ref scale)) { + layout.elements[i].scale = scale; + changed = true; + } + + var opacity = (int) layout.elements[i].opacity; + if (ImGui.DragInt($"Opacity##{kind}", ref opacity, 1, 1, 255)) { + layout.elements[i].opacity = (byte) opacity; + changed = true; + } + + ImGui.Separator(); + + break; + } + } + + if (changed) { + Marshal.StructureToPtr(layout, ptr, false); + this.Plugin.Hud.SelectSlot(this.Plugin.Hud.GetActiveHudSlot(), true); + } + + ImGui.EndChild(); + } + + ImGui.EndTabItem(); + } + #endif + + ImGui.EndTabBar(); + } + + ImGui.End(); + } + + private Layout PreviousLayout { get; set; } + + private void DrawConditionsTable() { + ImGui.PushFont(UiBuilder.IconFont); + var height = ImGui.GetContentRegionAvail().Y - ImGui.CalcTextSize(FontAwesomeIcon.Plus.ToIconString()).Y - ImGui.GetStyle().ItemSpacing.Y - ImGui.GetStyle().ItemInnerSpacing.Y * 2; + ImGui.PopFont(); + if (!ImGui.BeginChild("##conditions-table", new Vector2(-1, height))) { + return; + } + + ImGui.Columns(4); + + var conditions = new List(this.Plugin.Config.HudConditionMatches); + if (this._editingConditionIndex == conditions.Count) { + conditions.Add(new HudConditionMatch()); + } + + ImGui.Text("Job"); + ImGui.NextColumn(); + + ImGui.Text("State"); + ImGui.NextColumn(); + + ImGui.Text("Layout"); + ImGui.NextColumn(); + + ImGui.Text("Options"); + ImGui.NextColumn(); + + ImGui.Separator(); + + var addCondition = false; + var actionedItemIndex = -1; + var action = 0; // 0 for delete, otherwise move. + foreach (var item in conditions.Select((cond, i) => new {cond, i})) { + if (this._editingConditionIndex == item.i) { + this._editingCondition ??= new HudConditionMatch(); + ImGui.PushItemWidth(-1); + if (ImGui.BeginCombo("##condition-edit-job", this._editingCondition.ClassJob ?? "Any")) { + if (ImGui.Selectable("Any##condition-edit-job")) { + this._editingCondition.ClassJob = null; + } + + foreach (var job in this.Plugin.Interface.Data.GetExcelSheet().Skip(1)) { + if (ImGui.Selectable($"{job.Abbreviation}##condition-edit-job")) { + this._editingCondition.ClassJob = job.Abbreviation; + } + } + + ImGui.EndCombo(); + } + + ImGui.PopItemWidth(); + ImGui.NextColumn(); + + ImGui.PushItemWidth(-1); + if (ImGui.BeginCombo("##condition-edit-status", this._editingCondition.Status?.Name() ?? "Any")) { + if (ImGui.Selectable("Any##condition-edit-status")) { + this._editingCondition.Status = null; + } + + foreach (Status status in Enum.GetValues(typeof(Status))) { + if (ImGui.Selectable($"{status.Name()}##condition-edit-status")) { + this._editingCondition.Status = status; + } + } + + ImGui.EndCombo(); + } + + ImGui.PopItemWidth(); + ImGui.NextColumn(); + + ImGui.PushItemWidth(-1); + var comboPreview = this._editingCondition.LayoutId == Guid.Empty ? string.Empty : this.Plugin.Config.Layouts[this._editingCondition.LayoutId].Name; + if (ImGui.BeginCombo("##condition-edit-layout", comboPreview)) { + foreach (var layout in this.Plugin.Config.Layouts) { + if (ImGui.Selectable($"{layout.Value.Name}##condition-edit-layout-{layout.Key}")) { + this._editingCondition.LayoutId = layout.Key; + } + } + + ImGui.EndCombo(); + } + + ImGui.PopItemWidth(); + ImGui.NextColumn(); + + if (this._editingCondition.LayoutId != Guid.Empty) { + if (IconButton(FontAwesomeIcon.Check, "condition-edit")) { + addCondition = true; + } + + ImGui.SameLine(); + } + + if (IconButton(FontAwesomeIcon.Times, "condition-stop")) { + this._editingConditionIndex = -1; + } + + if (this._scrollToAdd) { + this._scrollToAdd = false; + ImGui.SetScrollHereY(); + } + } else { + ImGui.Text(item.cond.ClassJob ?? string.Empty); + ImGui.NextColumn(); + + ImGui.Text(item.cond.Status?.Name() ?? string.Empty); + ImGui.NextColumn(); + + this.Plugin.Config.Layouts.TryGetValue(item.cond.LayoutId, out var condLayout); + ImGui.Text(condLayout?.Name ?? string.Empty); + ImGui.NextColumn(); + + if (IconButton(FontAwesomeIcon.PencilAlt, $"{item.i}")) { + this._editingConditionIndex = item.i; + this._editingCondition = item.cond; + } + + ImGui.SameLine(); + if (IconButton(FontAwesomeIcon.Trash, $"{item.i}")) { + actionedItemIndex = item.i; + } + + ImGui.SameLine(); + if (IconButton(FontAwesomeIcon.ArrowUp, $"{item.i}")) { + actionedItemIndex = item.i; + action = -1; + } + + ImGui.SameLine(); + if (IconButton(FontAwesomeIcon.ArrowDown, $"{item.i}")) { + actionedItemIndex = item.i; + action = 1; + } + } + + ImGui.NextColumn(); + } + + ImGui.Columns(1); + + ImGui.Separator(); + + ImGui.EndChild(); + + if (IconButton(FontAwesomeIcon.Plus, "condition")) { + this._editingConditionIndex = this.Plugin.Config.HudConditionMatches.Count; + this._editingCondition = new HudConditionMatch(); + this._scrollToAdd = true; + } + + var recalculate = false; + + if (addCondition) { + recalculate = true; + if (this._editingConditionIndex == this.Plugin.Config.HudConditionMatches.Count && this._editingCondition != null) { + this.Plugin.Config.HudConditionMatches.Add(this._editingCondition); + } else if (this._editingCondition != null) { + this.Plugin.Config.HudConditionMatches.RemoveAt(this._editingConditionIndex); + this.Plugin.Config.HudConditionMatches.Insert(this._editingConditionIndex, this._editingCondition); + } + + this.Plugin.Config.Save(); + this._editingConditionIndex = -1; + } + + if (actionedItemIndex >= 0) { + recalculate = true; + if (action == 0) { + this.Plugin.Config.HudConditionMatches.RemoveAt(actionedItemIndex); + } else { + if (actionedItemIndex + action >= 0 && actionedItemIndex + action < this.Plugin.Config.HudConditionMatches.Count) { + // Move the condition. + var c = this.Plugin.Config.HudConditionMatches[actionedItemIndex]; + this.Plugin.Config.HudConditionMatches.RemoveAt(actionedItemIndex); + this.Plugin.Config.HudConditionMatches.Insert(actionedItemIndex + action, c); + } + } + + this.Plugin.Config.Save(); + } + + if (!recalculate) { + return; + } + + var player = this.Plugin.Interface.ClientState.LocalPlayer; + if (player == null || !this.Plugin.Config.SwapsEnabled) { + return; + } + + this.Plugin.Statuses.Update(player); + this.Plugin.Statuses.SetHudLayout(null); + } + + private static bool IconButton(FontAwesomeIcon icon, string append = "") { + ImGui.PushFont(UiBuilder.IconFont); + var button = ImGui.Button($"{icon.ToIconString()}##{append}"); + ImGui.PopFont(); + return button; + } + + private static void HelpMarker(string text) { + ImGui.TextDisabled("(?)"); + if (!ImGui.IsItemHovered()) { + return; + } + + ImGui.BeginTooltip(); + ImGui.PushTextWrapPos(ImGui.GetFontSize() * 20f); + ImGui.TextUnformatted(text); + ImGui.PopTextWrapPos(); + ImGui.EndTooltip(); + } + + public void Draw() { + this.DrawSettings(); + } + + private Dictionary> GetPositions() { + var positions = new Dictionary>(); + + foreach (var name in SavedWindows) { + var pos = this.Plugin.GameFunctions.GetWindowPosition(name); + if (pos != null) { + positions[name] = pos; + } + } + + return positions; + } + + public void ImportSlot(string name, HudSlot slot, bool save = true) { + var positions = this.Plugin.Config.ImportPositions + ? this.GetPositions() + : new Dictionary>(); + + this.Import(name, this.Plugin.Hud.ReadLayout(slot), positions, save); + } + + private void Import(string name, Layout layout, Dictionary> positions, bool save = true) { + var guid = this.Plugin.Config.Layouts.FirstOrDefault(kv => kv.Value.Name == name).Key; + guid = guid != default ? guid : Guid.NewGuid(); + + this.Plugin.Config.Layouts[guid] = new SavedLayout(name, layout, positions); + if (save) { + this.Plugin.Config.Save(); + } + } + } +} diff --git a/HudSwap/Statuses.cs b/HUD Manager/Statuses.cs similarity index 64% rename from HudSwap/Statuses.cs rename to HUD Manager/Statuses.cs index 2543176..3d88cb8 100644 --- a/HudSwap/Statuses.cs +++ b/HUD Manager/Statuses.cs @@ -1,64 +1,62 @@ -using Dalamud.Game.ClientState; +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Actors.Types; using Dalamud.Plugin; using Lumina.Excel.GeneratedSheets; -using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; using Newtonsoft.Json; using Newtonsoft.Json.Converters; // TODO: Zone swaps? -namespace HudSwap { +namespace HUD_Manager { public class Statuses { - private readonly HudSwapPlugin plugin; - private readonly DalamudPluginInterface pi; + private Plugin Plugin { get; } - private readonly Dictionary condition = new Dictionary(); - private ClassJob job; + private readonly Dictionary _condition = new(); + private ClassJob? _job; internal static byte GetStatus(DalamudPluginInterface pi, Actor actor) { - IntPtr statusPtr = pi.TargetModuleScanner.ResolveRelativeAddress(actor.Address, 0x1980); + var statusPtr = pi.TargetModuleScanner.ResolveRelativeAddress(actor.Address, 0x1980); return Marshal.ReadByte(statusPtr); } - public Statuses(HudSwapPlugin plugin, DalamudPluginInterface pi) { - this.plugin = plugin; - this.pi = pi; + public Statuses(Plugin plugin) { + this.Plugin = plugin; } - public bool Update(PlayerCharacter player) { + public bool Update(PlayerCharacter? player) { if (player == null) { return false; } - bool anyChanged = false; + var anyChanged = false; - ClassJob currentJob = this.pi.Data.GetExcelSheet().GetRow(player.ClassJob.Id); - if (this.job != null && this.job != currentJob) { + var currentJob = this.Plugin.Interface.Data.GetExcelSheet().GetRow(player.ClassJob.Id); + if (this._job != null && this._job != currentJob) { anyChanged = true; } - this.job = currentJob; + this._job = currentJob; foreach (Status status in Enum.GetValues(typeof(Status))) { - var old = this.condition.ContainsKey(status) && this.condition[status]; - this.condition[status] = status.Active(player, this.pi); - anyChanged |= old != this.condition[status]; + var old = this._condition.ContainsKey(status) && this._condition[status]; + this._condition[status] = status.Active(player, this.Plugin.Interface); + anyChanged |= old != this._condition[status]; } return anyChanged; } - public Guid CalculateCurrentHud() { - PlayerCharacter player = this.pi.ClientState.LocalPlayer; + private Guid CalculateCurrentHud() { + var player = this.Plugin.Interface.ClientState.LocalPlayer; if (player == null) { return Guid.Empty; } - foreach (var match in this.plugin.Config.HudConditionMatches) { - if ((!match.Status.HasValue || this.condition[match.Status.Value]) && - (match.ClassJob == null || this.job.Abbreviation == match.ClassJob)) { + foreach (var match in this.Plugin.Config.HudConditionMatches) { + if ((!match.Status.HasValue || this._condition[match.Status.Value]) && + (match.ClassJob == null || this._job?.Abbreviation == match.ClassJob)) { return match.LayoutId; } } @@ -66,23 +64,23 @@ namespace HudSwap { return Guid.Empty; } - public void SetHudLayout(PlayerCharacter player, bool update = false) { + public void SetHudLayout(PlayerCharacter? player, bool update = false) { if (update && player != null) { this.Update(player); } - Guid layoutId = this.CalculateCurrentHud(); + var layoutId = this.CalculateCurrentHud(); if (layoutId == Guid.Empty) { return; // FIXME: do something better } - if (!this.plugin.Config.Layouts2.TryGetValue(layoutId, out Layout layout)) { + if (!this.Plugin.Config.Layouts.TryGetValue(layoutId, out var layout)) { return; // FIXME: do something better } - this.plugin.Hud.WriteLayout(this.plugin.Config.StagingSlot, layout.Hud); - this.plugin.Hud.SelectSlot(this.plugin.Config.StagingSlot, true); + this.Plugin.Hud.WriteLayout(this.Plugin.Config.StagingSlot, layout.ToLayout()); + this.Plugin.Hud.SelectSlot(this.Plugin.Config.StagingSlot, true); - foreach (KeyValuePair> entry in layout.Positions) { - this.plugin.GameFunctions.MoveWindow(entry.Key, entry.Value.X, entry.Value.Y); + foreach (var entry in layout.Positions) { + this.Plugin.GameFunctions.MoveWindow(entry.Key, entry.Value.X, entry.Value.Y); } } } @@ -92,7 +90,7 @@ namespace HudSwap { /// Values stored here should be the abbreviation of the class/job name (all caps). /// We do this because using results in circular dependency errors when serializing. /// - public string ClassJob { get; set; } + public string? ClassJob { get; set; } [JsonConverter(typeof(StringEnumConverter))] public Status? Status { get; set; } @@ -141,7 +139,7 @@ namespace HudSwap { throw new ArgumentNullException(nameof(pi), "DalamudPluginInterface cannot be null"); } - ConditionFlag flag = (ConditionFlag)status; + var flag = (ConditionFlag)status; if (flag != ConditionFlag.None) { return pi.ClientState.Condition[flag]; } diff --git a/HUD Manager/Structs/Element.cs b/HUD Manager/Structs/Element.cs new file mode 100755 index 0000000..87a7585 --- /dev/null +++ b/HUD Manager/Structs/Element.cs @@ -0,0 +1,29 @@ +using System.Runtime.InteropServices; + +namespace HUD_Manager.Structs { + [StructLayout(LayoutKind.Sequential)] + public struct Element { + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + // public byte[] unknown0; + + public ElementKind id; + + public float x; + + public float y; + + public float scale; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 9)] + public byte[] unknown4; + + public Visibility visibility; + + public byte unknown6; + + public byte opacity; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 8)] + public byte[] unknown8; + } +} diff --git a/HUD Manager/Structs/ElementKind.cs b/HUD Manager/Structs/ElementKind.cs new file mode 100755 index 0000000..e01867d --- /dev/null +++ b/HUD Manager/Structs/ElementKind.cs @@ -0,0 +1,79 @@ +namespace HUD_Manager.Structs { + public enum ElementKind : uint { + FocusTargetBar = 3264409695, + StatusInfoEnfeeblements = 511728259, + StatusInfoEnhancements = 524431540, + StatusInfoOther = 482796762, + TargetInfoHp = 3172107127, + TargetInfoProgressBar = 3411321583, + TargetInfoStatus = 124737899, + PartyList = 1027756089, + EnemyList = 3099420293, + ScenarioGuide = 2297324375, + ExperienceBar = 568671438, + PetHotbar = 3637610751, + Hotbar10 = 4117249958, + Hotbar9 = 4104803729, + Hotbar8 = 4294316716, + Hotbar7 = 4264851611, + Hotbar6 = 4235380418, + Hotbar5 = 4256214261, + Hotbar4 = 4177508976, + Hotbar3 = 4181577799, + Hotbar2 = 4219170334, + Hotbar1 = 3297588741, + CrossHotbar = 3129075921, + ProgressBar = 3971127313, + Minimap = 1901658651, + BloodGauge = 4031678328, // DRK + DarksideGauge = 4052544847, // DRK + OathGauge = 4022009408, // PLD + BeastGauge = 2136801802, // WAR + PowderGauge = 2931998943, // GNB + ArcanaGauge = 2509863090, // AST + AetherflowGaugeSch = 3403503819, // SCH + FaerieGauge = 2712183943, // SCH + HealingGauge = 2050434994, // WHM + DragonGauge = 3130538176, // DRG + ChakraGauge = 1939064324, // MNK + HutonGauge = 1895405704, // NIN + NinkiGauge = 1899754175, // NIN + SenGauge = 3983830498, // SAM + KenkiGauge = 3971352533, // SAM + SongGauge = 2121561139, // BRD + HeatGauge = 2557790060, // MCH + StepGauge = 2431309076, // DNC + FourfoldFeathers = 2435366691, // DNC + ElementalGauge = 3702264410, // BLM + BalanceGauge = 4010433280, // RDM + TranceGauge = 976301837, // SMN + AetherflowGaugeSmn = 1005798714, // SMN + ItemHelp = 1120659295, + ActionHelp = 1180822218, + Gil = 1125522082, + InventoryGrid = 471196175, + MainMenu = 2331597424, + Notices = 3743511396, + ParameterBar = 2552153246, + LimitGauge = 3349103882, + DutyList = 2727411922, + ServerInfo = 3450378102, + AllianceList1 = 692286106, + AllianceList2 = 721800387, + NewGamePlusGuide = 3660166250, + TargetBar = 2436811133, + StatusEffects = 1247188502, + DutyGauge = 2168013717, + DutyAction = 1421395594, + CompressedAether = 3300326260, + RivalWingsMercenaryInfo = 2045148168, + RivalWingsTeamInfo = 1180574470, + RivalWingsStationInfo = 2702651974, + RivalWingsAllianceList = 3869061330, + RivalWingsGauges = 273150177, + TheFeastEnemyInfo = 912936203, + TheFeastAllyInfo = 933766972, + TheFeastScore = 3622852831, + BattleHighGauge = 884971695, + } +} diff --git a/HUD Manager/Structs/Layout.cs b/HUD Manager/Structs/Layout.cs new file mode 100755 index 0000000..606ec20 --- /dev/null +++ b/HUD Manager/Structs/Layout.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace HUD_Manager.Structs { + [StructLayout(LayoutKind.Sequential)] + public struct Layout { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 81)] + public Element[] elements; + + public Dictionary ToDictionary() { + // NOTE: not using ToDictionary here because duplicate keys are possible with old broken layouts + var dict = new Dictionary(); + foreach (var elem in this.elements) { + if (elem.id == 0) { + continue; + } + + dict[elem.id] = elem; + } + + return dict; + } + } +} diff --git a/HUD Manager/Structs/Visibility.cs b/HUD Manager/Structs/Visibility.cs new file mode 100755 index 0000000..061c0c3 --- /dev/null +++ b/HUD Manager/Structs/Visibility.cs @@ -0,0 +1,6 @@ +namespace HUD_Manager.Structs { + public enum Visibility : byte { + Hidden = 1, + Visible = 3, + } +} diff --git a/HUD Manager/Swapper.cs b/HUD Manager/Swapper.cs new file mode 100644 index 0000000..9d5a101 --- /dev/null +++ b/HUD Manager/Swapper.cs @@ -0,0 +1,31 @@ +using System; +using Dalamud.Game.Internal; + +namespace HUD_Manager { + public class Swapper { + private Plugin Plugin { get; } + + public Swapper(Plugin plugin) { + this.Plugin = plugin; + } + + public void OnFrameworkUpdate(Framework framework) { + if (framework == null) { + throw new ArgumentNullException(nameof(framework), "Framework cannot be null"); + } + + if (!this.Plugin.Config.SwapsEnabled || !this.Plugin.Config.UnderstandsRisks) { + return; + } + + var player = this.Plugin.Interface.ClientState.LocalPlayer; + if (player == null) { + return; + } + + if (this.Plugin.Statuses.Update(player)) { + this.Plugin.Statuses.SetHudLayout(null); + } + } + } +} diff --git a/HudSwap/GameFunctions.cs b/HudSwap/GameFunctions.cs deleted file mode 100644 index 0745229..0000000 --- a/HudSwap/GameFunctions.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Dalamud.Plugin; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; - -namespace HudSwap { - public class GameFunctions { - private delegate IntPtr GetUIBaseDelegate(); - private delegate IntPtr GetUIWindowDelegate(IntPtr uiBase, string uiName, int index); - private delegate void MoveWindowDelegate(IntPtr windowBase, short x, short y); - - private readonly GetUIBaseDelegate getUIBase; - private readonly GetUIWindowDelegate getUIWindow; - private readonly MoveWindowDelegate moveWindow; - - public GameFunctions(DalamudPluginInterface pi) { - if (pi == null) { - throw new ArgumentNullException(nameof(pi), "DalamudPluginInterface cannot be null"); - } - - IntPtr getUIBasePtr = pi.TargetModuleScanner.ScanText("E8 ?? ?? ?? ?? 41 b8 01 00 00 00 48 8d 15 ?? ?? ?? ?? 48 8b 48 20 e8 ?? ?? ?? ?? 48 8b cf"); - IntPtr getUIWindowPtr = pi.TargetModuleScanner.ScanText("e8 ?? ?? ?? ?? 48 8b cf 48 89 87 ?? ?? 00 00 e8 ?? ?? ?? ?? 41 b8 01 00 00 00"); - IntPtr moveWindowPtr = pi.TargetModuleScanner.ScanText("E8 ?? ?? ?? ?? 48 83 BB ?? ?? ?? ?? 00 74 ?? 48 8B 8B ?? ?? ?? ?? 48 85 C9 74 ?? E8 ?? ?? ?? ??"); - - if (getUIBasePtr == IntPtr.Zero || getUIWindowPtr == IntPtr.Zero || moveWindowPtr == IntPtr.Zero) { - throw new ApplicationException("could not get game functions"); - } - - this.getUIBase = Marshal.GetDelegateForFunctionPointer(getUIBasePtr); - this.getUIWindow = Marshal.GetDelegateForFunctionPointer(getUIWindowPtr); - this.moveWindow = Marshal.GetDelegateForFunctionPointer(moveWindowPtr); - } - - private IntPtr GetUIBase() { - return this.getUIBase.Invoke(); - } - - private IntPtr GetUIWindow(string uiName, int index) { - IntPtr uiBase = this.GetUIBase(); - IntPtr offset = Marshal.ReadIntPtr(uiBase, 0x20); - - return this.getUIWindow.Invoke(offset, uiName, index); - } - - public void MoveWindow(string uiName, short x, short y) { - IntPtr windowBase = this.GetUIWindow(uiName, 1); - if (windowBase == IntPtr.Zero) { - return; - } - - this.moveWindow.Invoke(windowBase, x, y); - } - - public Vector2 GetWindowPosition(string uiName) { - IntPtr windowBase = this.GetUIWindow(uiName, 1); - if (windowBase == IntPtr.Zero) { - return null; - } - - short x = Marshal.ReadInt16(windowBase + 0x1bc); - short y = Marshal.ReadInt16(windowBase + 0x1bc + 2); - - return new Vector2(x, y); - } - } -} diff --git a/HudSwap/GlobalSuppressions.cs b/HudSwap/GlobalSuppressions.cs deleted file mode 100644 index 012d536..0000000 --- a/HudSwap/GlobalSuppressions.cs +++ /dev/null @@ -1,11 +0,0 @@ -// This file is used by Code Analysis to maintain SuppressMessage -// attributes that are applied to this project. -// Project-level suppressions either have no target or are given -// a specific target and scoped to a namespace, type, member, etc. - -using System.Diagnostics.CodeAnalysis; - -[assembly: SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Will be done eventually", Scope = "module")] -[assembly: SuppressMessage("Globalization", "CA1305", Scope = "module")] -[assembly: SuppressMessage("Globalization", "CA1304", Scope = "module")] -[assembly: SuppressMessage("Globalization", "CA1724", Scope = "module")] diff --git a/HudSwap/HUD.cs b/HudSwap/HUD.cs deleted file mode 100644 index ee59dee..0000000 --- a/HudSwap/HUD.cs +++ /dev/null @@ -1,194 +0,0 @@ -using Dalamud.Plugin; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; -using System.Runtime.InteropServices; - -namespace HudSwap { - public class HUD { - private const int LAYOUT_SIZE = 0xb40; // 5.4 - private const int SLOT_OFFSET = 0x59e8; // 5.4 - - private delegate IntPtr GetFilePointerDelegate(byte index); - - private delegate uint SetHudLayoutDelegate(IntPtr filePtr, uint hudLayout, byte unk0, byte unk1); - - private readonly GetFilePointerDelegate _getFilePointer; - private readonly SetHudLayoutDelegate _setHudLayout; - - private readonly DalamudPluginInterface pi; - - public HUD(DalamudPluginInterface pi) { - this.pi = pi ?? throw new ArgumentNullException(nameof(pi), "DalamudPluginInterface cannot be null"); - IntPtr getFilePointerPtr = this.pi.TargetModuleScanner.ScanText("E8 ?? ?? ?? ?? 48 85 C0 74 14 83 7B 44 00"); - IntPtr setHudLayoutPtr = this.pi.TargetModuleScanner.ScanText("E8 ?? ?? ?? ?? 33 C0 EB 15"); - if (getFilePointerPtr != IntPtr.Zero) { - this._getFilePointer = Marshal.GetDelegateForFunctionPointer(getFilePointerPtr); - } - - if (setHudLayoutPtr != IntPtr.Zero) { - this._setHudLayout = Marshal.GetDelegateForFunctionPointer(setHudLayoutPtr); - } - } - - private IntPtr GetFilePointer(byte index) { - return this._getFilePointer.Invoke(index); - } - - public uint SelectSlot(HudSlot slot, bool force = false) { - IntPtr file = this.GetFilePointer(0); - // change the current slot so the game lets us pick one that's currently in use - if (!force) { - goto Return; - } - - IntPtr currentSlotPtr = this.GetDataPointer() + SLOT_OFFSET; - // read the current slot - uint currentSlot = (uint)Marshal.ReadInt32(currentSlotPtr); - // change it to a different slot - if (currentSlot == (uint)slot) { - if (currentSlot < 3) { - currentSlot += 1; - } else { - currentSlot = 0; - } - - // back up this different slot - byte[] backup = this.ReadLayout((HudSlot)currentSlot); - // change the current slot in memory - Marshal.WriteInt32(currentSlotPtr, (int)currentSlot); - // ask the game to change slot to our desired slot - // for some reason, this overwrites the current slot, so this is why we back up - uint res = this._setHudLayout.Invoke(file, (uint)slot, 0, 1); - // restore the backup - this.WriteLayout((HudSlot)currentSlot, backup); - return res; - } - - Return: - return this._setHudLayout.Invoke(file, (uint)slot, 0, 1); - } - - private IntPtr GetDataPointer() { - IntPtr dataPtr = this.GetFilePointer(0) + 0x50; - return Marshal.ReadIntPtr(dataPtr); - } - - private IntPtr GetLayoutPointer(HudSlot slot) { - int slotNum = (int)slot; - return this.GetDataPointer() + 0x2c58 + (slotNum * LAYOUT_SIZE); - } - - public HudSlot GetActiveHudSlot() { - int slotVal = Marshal.ReadInt32(this.GetDataPointer() + SLOT_OFFSET); - - if (!Enum.IsDefined(typeof(HudSlot), slotVal)) { - throw new IOException($"invalid hud slot in FFXIV memory of ${slotVal}"); - } - - return (HudSlot)slotVal; - } - - public byte[] ReadLayout(HudSlot slot) { - IntPtr slotPtr = this.GetLayoutPointer(slot); - byte[] bytes = new byte[LAYOUT_SIZE]; - Marshal.Copy(slotPtr, bytes, 0, LAYOUT_SIZE); - return bytes; - } - - public void WriteLayout(HudSlot slot, byte[] layout) { - if (layout == null) { - throw new ArgumentNullException(nameof(layout), "layout cannot be null"); - } - - if (layout.Length != LAYOUT_SIZE) { - throw new ArgumentException($"layout must be {LAYOUT_SIZE} bytes", nameof(layout)); - } - - IntPtr slotPtr = this.GetLayoutPointer(slot); - Marshal.Copy(layout, 0, slotPtr, LAYOUT_SIZE); - - var currentSlot = this.GetActiveHudSlot(); - if (currentSlot == slot) { - this.SelectSlot(currentSlot, true); - } - } - - public void WriteLayout(byte[] layout) => WriteLayout(this.GetActiveHudSlot(), layout); - } - - public enum HudSlot { - One = 0, - Two = 1, - Three = 2, - Four = 3, - } - - public class Vector2 { - public T X { get; private set; } - public T Y { get; private set; } - - public Vector2(T x, T y) { - this.X = x; - this.Y = y; - } - } - - [Serializable] - public class SharedLayout { - [JsonProperty] - private readonly byte[] compressed; - - [NonSerialized] - private byte[] uncompressed = null; - - public Dictionary> Positions { get; private set; } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "nah")] - public byte[] Layout() { - if (this.uncompressed != null) { - return this.uncompressed; - } - - try { - using (MemoryStream compressed = new MemoryStream(this.compressed)) { - using (GZipStream gzip = new GZipStream(compressed, CompressionMode.Decompress)) { - using (MemoryStream uncompressed = new MemoryStream()) { - gzip.CopyTo(uncompressed); - this.uncompressed = uncompressed.ToArray(); - } - } - } - } catch (Exception) { - return null; - } - - return this.uncompressed; - } - - [JsonConstructor] - private SharedLayout() { - // For JSON - } - - public SharedLayout(Layout layout) { - if (layout == null) { - throw new ArgumentNullException(nameof(layout), "Layout cannot be null"); - } - - using (MemoryStream compressed = new MemoryStream()) { - using (GZipStream gzip = new GZipStream(compressed, CompressionLevel.Optimal)) { - using (MemoryStream uncompressed = new MemoryStream(layout.Hud)) { - uncompressed.CopyTo(gzip); - } - } - - this.compressed = compressed.ToArray(); - } - - this.Positions = layout.Positions; - } - } -} diff --git a/HudSwap/HudSwap.csproj b/HudSwap/HudSwap.csproj deleted file mode 100644 index 15c3e5b..0000000 --- a/HudSwap/HudSwap.csproj +++ /dev/null @@ -1,110 +0,0 @@ - - - - - - - - - - Debug - AnyCPU - {CBF13A29-A5A4-4FA2-8A4F-CFBB5FB83F90} - Library - Properties - HudSwap - HudSwap - v4.8 - 512 - true - - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - $(AppData)\XIVLauncher\addon\Hooks\Dalamud.dll - - - $(AppData)\XIVLauncher\addon\Hooks\ImGui.NET.dll - - - $(AppData)\XIVLauncher\addon\Hooks\ImGuiScene.dll - - - False - $(AppData)\XIVLauncher\addon\Hooks\Lumina.dll - - - False - $(AppData)\XIVLauncher\addon\Hooks\Lumina.Excel.dll - - - False - $(AppData)\XIVLauncher\addon\Hooks\Newtonsoft.Json.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - - \ No newline at end of file diff --git a/HudSwap/HudSwap.json b/HudSwap/HudSwap.json deleted file mode 100644 index 3085655..0000000 --- a/HudSwap/HudSwap.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "Author": "ascclemens", - "Name": "HudSwap", - "Description": "Automatically changes your HUD layout for you. /phudswap", - "InternalName": "HudSwap", - "AssemblyVersion": "1.5.1", - "RepoUrl": "https://sr.ht/~jkcclemens/HudSwap", - "ApplicableVersion": "any", - "DalamudApiLevel": 2 -} diff --git a/HudSwap/Layout.cs b/HudSwap/Layout.cs deleted file mode 100644 index 3095185..0000000 --- a/HudSwap/Layout.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace HudSwap { - [Serializable] - public class Layout { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays")] - public byte[] Hud { get; private set; } - public Dictionary> Positions { get; private set; } - - public string Name { get; private set; } - - public Layout(string name, byte[] hud, Dictionary> positions) { - this.Name = name; - this.Hud = hud; - this.Positions = positions; - } - } -} diff --git a/HudSwap/Plugin.cs b/HudSwap/Plugin.cs deleted file mode 100644 index b53e3f6..0000000 --- a/HudSwap/Plugin.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Dalamud.Game.Command; -using Dalamud.Plugin; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace HudSwap { - public class HudSwapPlugin : IDalamudPlugin { - public string Name => "HudSwap"; - - private DalamudPluginInterface pi; - private PluginUI ui; - private Swapper swapper; - - public HUD Hud { get; private set; } - public Statuses Statuses { get; private set; } - public GameFunctions GameFunctions { get; private set; } - public PluginConfig Config { get; private set; } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "nah")] - public void Initialize(DalamudPluginInterface pluginInterface) { - this.pi = pluginInterface ?? throw new ArgumentNullException(nameof(pluginInterface), "DalamudPluginInterface cannot be null"); - try { - this.Config = this.pi.GetPluginConfig() as PluginConfig ?? new PluginConfig(); - } catch (Exception) { - this.pi.UiBuilder.OnBuildUi += PluginUI.ConfigError; - return; - } - this.Config.Initialize(this.pi); - - this.ui = new PluginUI(this, this.pi); - this.Hud = new HUD(this.pi); - this.Statuses = new Statuses(this, this.pi); - this.GameFunctions = new GameFunctions(this.pi); - - this.swapper = new Swapper(this, this.pi); - - if (this.Config.FirstRun) { - this.Config.FirstRun = false; - if (this.Config.Layouts2.Count == 0) { - foreach (HudSlot slot in Enum.GetValues(typeof(HudSlot))) { - this.ui.ImportSlot($"Auto-import {(int)slot + 1}", slot, false); - } - } - this.Config.Save(); - } - - this.pi.UiBuilder.OnBuildUi += this.ui.Draw; - this.pi.UiBuilder.OnOpenConfigUi += this.ui.ConfigUI; - this.pi.Framework.OnUpdateEvent += this.swapper.OnFrameworkUpdate; - - this.pi.CommandManager.AddHandler("/phudswap", new CommandInfo(OnSettingsCommand) { - HelpMessage = "Open the HudSwap settings" - }); - this.pi.CommandManager.AddHandler("/phud", new CommandInfo(OnSwapCommand) { - HelpMessage = "/phud - Swap to HUD layout called " - }); - } - - protected virtual void Dispose(bool all) { - this.pi.UiBuilder.OnBuildUi -= this.ui.Draw; - this.pi.UiBuilder.OnOpenConfigUi -= this.ui.ConfigUI; - this.pi.Framework.OnUpdateEvent -= this.swapper.OnFrameworkUpdate; - this.pi.CommandManager.RemoveHandler("/phudswap"); - this.pi.CommandManager.RemoveHandler("/phud"); - } - - public void Dispose() { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - private void OnSettingsCommand(string command, string args) { - this.ui.SettingsVisible = true; - } - - private void OnSwapCommand(string command, string args) { - KeyValuePair entry = this.Config.Layouts2.FirstOrDefault(e => e.Value.Name == args); - if (entry.Equals(default(KeyValuePair))) { - return; - } - - Layout layout = entry.Value; - - this.Hud.WriteLayout(this.Config.StagingSlot, layout.Hud); - this.Hud.SelectSlot(this.Config.StagingSlot, true); - } - } -} diff --git a/HudSwap/PluginConfig.cs b/HudSwap/PluginConfig.cs deleted file mode 100644 index 16c267f..0000000 --- a/HudSwap/PluginConfig.cs +++ /dev/null @@ -1,144 +0,0 @@ -using Dalamud.Configuration; -using Dalamud.Plugin; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; - -namespace HudSwap { - [Serializable] - public class PluginConfig : IPluginConfiguration { - public int Version { get; set; } = 1; - - [NonSerialized] - private DalamudPluginInterface pi; - - public bool FirstRun { get; set; } = true; - public bool UnderstandsRisks { get; set; } = false; - - public bool ImportPositions { get; set; } = false; - public bool SwapsEnabled { get; set; } = false; - - public HudSlot StagingSlot { get; set; } = HudSlot.Four; - - [Obsolete("Superceded by HudConditionMatches")] - public Guid DefaultLayout { get; set; } = Guid.Empty; - -#pragma warning disable CA1051 // Do not declare visible instance fields - [Obsolete("Individual layout fields are deprecated; use StatusLayouts instead")] - public Guid combatLayout = Guid.Empty; - [Obsolete("Individual layout fields are deprecated; use StatusLayouts instead")] - public Guid weaponDrawnLayout = Guid.Empty; - [Obsolete("Individual layout fields are deprecated; use StatusLayouts instead")] - public Guid instanceLayout = Guid.Empty; - [Obsolete("Individual layout fields are deprecated; use StatusLayouts instead")] - public Guid craftingLayout = Guid.Empty; - [Obsolete("Individual layout fields are deprecated; use StatusLayouts instead")] - public Guid gatheringLayout = Guid.Empty; - [Obsolete("Individual layout fields are deprecated; use StatusLayouts instead")] - public Guid fishingLayout = Guid.Empty; - [Obsolete("Individual layout fields are deprecated; use StatusLayouts instead")] - public Guid roleplayingLayout = Guid.Empty; -#pragma warning restore CA1051 // Do not declare visible instance fields - - [Obsolete("Superceded by HudConditionMatches")] - public Dictionary StatusLayouts { get; } = new Dictionary(); - [Obsolete("Superceded by HudConditionMatches")] - public Dictionary JobLayouts { get; } = new Dictionary(); - [Obsolete("Superceded by HudConditionMatches")] - public bool HighPriorityJobs { get; set; } = false; - [Obsolete("Superceded by HudConditionMatches")] - public bool JobsCombatOnly { get; set; } = false; - - [Obsolete("Use Layouts2 instead")] - public Dictionary> Layouts { get; } = new Dictionary>(); - public Dictionary Layouts2 { get; } = new Dictionary(); - - public List HudConditionMatches { get; } = new List(); - - public void Initialize(DalamudPluginInterface pluginInterface) { - this.pi = pluginInterface; - this.Migrate(); - this.Save(); - } - - public void Save() { - this.pi.SavePluginConfig(this); - } - - private void Migrate() { -#pragma warning disable 618 - if (this.combatLayout != Guid.Empty) { - this.StatusLayouts[Status.InCombat] = this.combatLayout; - this.combatLayout = Guid.Empty; - } - - if (this.weaponDrawnLayout != Guid.Empty) { - this.StatusLayouts[Status.WeaponDrawn] = this.weaponDrawnLayout; - this.weaponDrawnLayout = Guid.Empty; - } - - if (this.instanceLayout != Guid.Empty) { - this.StatusLayouts[Status.InInstance] = this.instanceLayout; - this.instanceLayout = Guid.Empty; - } - - if (this.craftingLayout != Guid.Empty) { - this.StatusLayouts[Status.Crafting] = this.craftingLayout; - this.craftingLayout = Guid.Empty; - } - - if (this.gatheringLayout != Guid.Empty) { - this.StatusLayouts[Status.Gathering] = this.gatheringLayout; - this.gatheringLayout = Guid.Empty; - } - - if (this.fishingLayout != Guid.Empty) { - this.StatusLayouts[Status.Fishing] = this.fishingLayout; - this.fishingLayout = Guid.Empty; - } - - if (this.roleplayingLayout != Guid.Empty) { - this.StatusLayouts[Status.Roleplaying] = this.roleplayingLayout; - this.roleplayingLayout = Guid.Empty; - } - - if (this.Layouts.Count != 0) { - foreach (KeyValuePair> entry in this.Layouts) { - Layout layout = new Layout(entry.Value.Item1, entry.Value.Item2, new Dictionary>()); - this.Layouts2.Add(entry.Key, layout); - } - this.Layouts.Clear(); - } - - if (this.JobLayouts.Count != 0) { - foreach (var jobLayout in this.JobLayouts) { - this.HudConditionMatches.Add(new HudConditionMatch() { - ClassJob = jobLayout.Key, - Status = JobsCombatOnly ? Status.InCombat : default, - LayoutId = jobLayout.Value - }); - } - - this.JobLayouts.Clear(); - } - - if (this.StatusLayouts.Count != 0) { - foreach (var statusLayout in this.StatusLayouts) { - var match = new HudConditionMatch() { - Status = statusLayout.Key, - LayoutId = statusLayout.Value - }; - if (this.HighPriorityJobs) { - this.HudConditionMatches.Add(match); - } else { - this.HudConditionMatches.Insert(0, match); - } - } - - this.StatusLayouts.Clear(); - } -#pragma warning restore 618 - } - } -} diff --git a/HudSwap/PluginUI.cs b/HudSwap/PluginUI.cs deleted file mode 100644 index 9854c89..0000000 --- a/HudSwap/PluginUI.cs +++ /dev/null @@ -1,509 +0,0 @@ -using Dalamud.Game.ClientState.Actors.Types; -using Dalamud.Interface; -using Dalamud.Plugin; -using ImGuiNET; -using Lumina.Excel.GeneratedSheets; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; - -// TODO: Zone swaps? - -namespace HudSwap { - public class PluginUI { - private static readonly string[] SAVED_WINDOWS = { - "AreaMap", - "ChatLog", - "ChatLogPanel_0", - "ChatLogPanel_1", - "ChatLogPanel_2", - "ChatLogPanel_3", - }; - - private readonly HudSwapPlugin plugin; - private readonly DalamudPluginInterface pi; - - private bool _settingsVisible = false; - public bool SettingsVisible { get => this._settingsVisible; set => this._settingsVisible = value; } - - public PluginUI(HudSwapPlugin plugin, DalamudPluginInterface pi) { - this.plugin = plugin; - this.pi = pi; - } - - public void ConfigUI(object sender, EventArgs args) { - this.SettingsVisible = true; - } - - private string importName = ""; - private string renameName = ""; - private Guid selectedLayoutId = Guid.Empty; - - private Layout selectedLayout => - selectedLayoutId == Guid.Empty ? null : this.plugin.Config.Layouts2[this.selectedLayoutId]; - - private int editingConditionIndex = -1; - private HudConditionMatch editingCondition; - private bool scrollToAdd = false; - - private static bool configErrorOpen = true; - public static void ConfigError() { - if (ImGui.Begin("HudSwap error", ref configErrorOpen)) { - ImGui.Text("Could not load HudSwap configuration."); - ImGui.Spacing(); - ImGui.Text("If you are updating from a previous version, please\ndelete your configuration file and restart the game."); - - ImGui.End(); - } - } - - public void DrawSettings() { - void DrawHudSlotButtons(string idSuffix, System.Action hudSlotAction, System.Action clipboardAction) { - Vector2 slotButtonSize = new Vector2(40, 0); - foreach (HudSlot slot in Enum.GetValues(typeof(HudSlot))) { - // Surround the button with parentheses if this is the current slot - string slotText = slot == this.plugin.Hud.GetActiveHudSlot() ? $"({(int)slot + 1})" : ((int)slot + 1).ToString(); - string buttonName = $"{slotText}##${idSuffix}"; - if (ImGui.Button(buttonName, slotButtonSize)) { - PluginLog.Log("Importing outer"); - hudSlotAction(slot); - } - ImGui.SameLine(); - } - - ImGui.SameLine(); - - if (ImGui.Button($"Clipboard##{idSuffix}")) { - clipboardAction(); - } - } - - if (!this.SettingsVisible) { - return; - } - - ImGui.SetNextWindowSize(new Vector2(500, 450), ImGuiCond.FirstUseEver); - - if (!ImGui.Begin(this.plugin.Name, ref this._settingsVisible)) { - return; - } - - if (ImGui.BeginTabBar("##hudswap-tabs")) { - if (!this.plugin.Config.UnderstandsRisks) { - if (ImGui.BeginTabItem("About")) { - ImGui.TextColored(new Vector4(1f, 0f, 0f, 1f), "Read this first"); - ImGui.Separator(); - ImGui.PushTextWrapPos(ImGui.GetFontSize() * 20f); - ImGui.Text("HudSwap will use the configured staging slot as its own slot to make changes to. This means the staging slot will be overwritten whenever any swap happens."); - ImGui.Spacing(); - ImGui.Text("Any HUD layout changes you make while HudSwap is enabled may potentially be lost, no matter what slot. If you want to make changes to your HUD layout, TURN OFF HudSwap first."); - ImGui.Spacing(); - ImGui.Text("When editing or making a new layout, to be completely safe, turn off swaps, set up your layout, import the layout into HudSwap, then turn on swaps."); - ImGui.Spacing(); - ImGui.Text("If you are a new user, HudSwap auto-imported your existing layouts on startup."); - ImGui.Spacing(); - ImGui.Text("Finally, HudSwap is beta software. Back up your character data before using this plugin. You may lose some to all of your HUD layouts while testing this plugin."); - ImGui.Separator(); - ImGui.Text("If you have read all of the above and are okay with continuing, check the box below to enable HudSwap. You only need to do this once."); - ImGui.PopTextWrapPos(); - bool understandsRisks = this.plugin.Config.UnderstandsRisks; - if (ImGui.Checkbox("I understand", ref understandsRisks)) { - this.plugin.Config.UnderstandsRisks = understandsRisks; - this.plugin.Config.Save(); - } - - ImGui.EndTabItem(); - } - - ImGui.EndTabBar(); - ImGui.End(); - return; - } - - if (ImGui.BeginTabItem("Layouts")) { - ImGui.Text("Saved layouts"); - if (this.plugin.Config.Layouts2.Count == 0) { - ImGui.Text("None saved!"); - } else { - ImGui.PushItemWidth(-1); - if (ImGui.ListBoxHeader("##saved-layouts")) { - foreach (KeyValuePair entry in this.plugin.Config.Layouts2) { - if (ImGui.Selectable($"{entry.Value.Name}##{entry.Key}", this.selectedLayoutId == entry.Key)) { - this.selectedLayoutId = entry.Key; - this.renameName = entry.Value.Name; - this.importName = this.renameName; - } - } - ImGui.ListBoxFooter(); - } - ImGui.PopItemWidth(); - - ImGui.PushItemWidth(200); - ImGui.InputText("##rename-input", ref this.renameName, 100); - ImGui.PopItemWidth(); - ImGui.SameLine(); - if (ImGui.Button("Rename") && this.renameName.Length != 0 && this.selectedLayout != null) { - Layout layout = this.plugin.Config.Layouts2[this.selectedLayoutId]; - Layout newLayout = new Layout(this.renameName, layout.Hud, layout.Positions); - this.plugin.Config.Layouts2[this.selectedLayoutId] = newLayout; - this.plugin.Config.Save(); - } - - const int layoutActionButtonWidth = 30; - // `layoutActionButtonWidth` must be multiplied by however many action buttons there are here - ImGui.SameLine(ImGui.GetWindowContentRegionWidth() - layoutActionButtonWidth * 1); - ImGui.PushFont(UiBuilder.IconFont); - if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString(), new Vector2(layoutActionButtonWidth, 0)) && - this.selectedLayout != null) { - this.plugin.Config.Layouts2.Remove(this.selectedLayoutId); - this.plugin.Config.HudConditionMatches.RemoveAll(m => m.LayoutId == this.selectedLayoutId); - this.selectedLayoutId = Guid.Empty; - this.renameName = ""; - this.plugin.Config.Save(); - } - ImGui.PopFont(); - - ImGui.Text("Copy to..."); - DrawHudSlotButtons("copy", slot => { - if (this.selectedLayout == null) { - return; - } - - this.plugin.Hud.WriteLayout(slot, this.selectedLayout.Hud); - }, () => { - if (this.selectedLayout == null) { - return; - } - - SharedLayout shared = new SharedLayout(selectedLayout); - string json = JsonConvert.SerializeObject(shared); - ImGui.SetClipboardText(json); - }); - } - - ImGui.Separator(); - - ImGui.Text("Import"); - - ImGui.InputText("Imported layout name", ref this.importName, 100); - - bool importPositions = this.plugin.Config.ImportPositions; - if (ImGui.Checkbox("Import window positions", ref importPositions)) { - this.plugin.Config.ImportPositions = importPositions; - this.plugin.Config.Save(); - } - ImGui.SameLine(); - HelpMarker("If this is checked, the position of the chat box and the map will be saved with the imported layout."); - - bool isOverwriting = - this.plugin.Config.Layouts2.Values.Any(layout => layout.Name == this.importName); - ImGui.Text((isOverwriting ? "Overwrite" : "Import") + " from..."); - - DrawHudSlotButtons("import", slot => { - PluginLog.Log("Importing inner"); - this.ImportSlot(this.importName, slot); - this.importName = ""; - }, () => { - SharedLayout shared = null; - try { - shared = (SharedLayout)JsonConvert.DeserializeObject(ImGui.GetClipboardText(), typeof(SharedLayout)); -#pragma warning disable CA1031 // Do not catch general exception types - } catch (Exception) { -#pragma warning restore CA1031 // Do not catch general exception types - } - if (shared != null) { - byte[] layout = shared.Layout(); - if (layout != null) { - this.Import(this.importName, layout, shared.Positions); - this.importName = ""; - } - } - }); - - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Swaps")) { - bool enabled = this.plugin.Config.SwapsEnabled; - if (ImGui.Checkbox("Enable swaps", ref enabled)) { - this.plugin.Config.SwapsEnabled = enabled; - this.plugin.Config.Save(); - } - ImGui.Text("Note: Disable swaps when editing your HUD."); - - ImGui.Spacing(); - string staging = ((int)this.plugin.Config.StagingSlot + 1).ToString(); - if (ImGui.BeginCombo("Staging slot", staging)) { - foreach (HudSlot slot in Enum.GetValues(typeof(HudSlot))) { - if (ImGui.Selectable(((int)slot + 1).ToString())) { - this.plugin.Config.StagingSlot = slot; - this.plugin.Config.Save(); - } - } - ImGui.EndCombo(); - } - ImGui.SameLine(); - HelpMarker("The staging slot is the HUD layout slot that will be used as your HUD layout. All changes will be written to this slot when swaps are enabled."); - - ImGui.Separator(); - - if (this.plugin.Config.Layouts2.Count == 0) { - ImGui.Text("Create at least one layout to begin setting up swaps."); - } else { - ImGui.Text("Add swap conditions below.\nThe first condition that is satisfied will be the layout that is used."); - ImGui.Separator(); - this.DrawConditionsTable(); - } - - ImGui.EndTabItem(); - } - - ImGui.EndTabBar(); - } - - ImGui.End(); - } - - private void DrawConditionsTable() { - ImGui.PushFont(UiBuilder.IconFont); - float height = ImGui.GetContentRegionAvail().Y - ImGui.CalcTextSize(FontAwesomeIcon.Plus.ToIconString()).Y - ImGui.GetStyle().ItemSpacing.Y - ImGui.GetStyle().ItemInnerSpacing.Y * 2; - ImGui.PopFont(); - if (!ImGui.BeginChild("##conditions-table", new Vector2(-1, height))) { - return; - } - - ImGui.Columns(4); - - var conditions = new List(this.plugin.Config.HudConditionMatches); - if (this.editingConditionIndex == conditions.Count) { - conditions.Add(new HudConditionMatch()); - } - - ImGui.Text("Job"); - ImGui.NextColumn(); - - ImGui.Text("State"); - ImGui.NextColumn(); - - ImGui.Text("Layout"); - ImGui.NextColumn(); - - ImGui.Text("Options"); - ImGui.NextColumn(); - - ImGui.Separator(); - - bool addCondition = false; - int actionedItemIndex = -1; - int action = 0; // 0 for delete, otherwise move. - foreach (var item in conditions.Select((cond, i) => new { cond, i })) { - if (this.editingConditionIndex == item.i) { - ImGui.PushItemWidth(-1); - if (ImGui.BeginCombo("##condition-edit-job", this.editingCondition.ClassJob ?? "Any")) { - if (ImGui.Selectable("Any##condition-edit-job")) { - this.editingCondition.ClassJob = null; - } - - foreach (ClassJob job in this.pi.Data.GetExcelSheet().Skip(1)) { - if (ImGui.Selectable($"{job.Abbreviation}##condition-edit-job")) { - this.editingCondition.ClassJob = job.Abbreviation; - } - } - - ImGui.EndCombo(); - } - ImGui.PopItemWidth(); - ImGui.NextColumn(); - - ImGui.PushItemWidth(-1); - if (ImGui.BeginCombo("##condition-edit-status", this.editingCondition.Status?.Name() ?? "Any")) { - if (ImGui.Selectable("Any##condition-edit-status")) { - this.editingCondition.Status = null; - } - - foreach (Status status in Enum.GetValues(typeof(Status))) { - if (ImGui.Selectable($"{status.Name()}##condition-edit-status")) { - this.editingCondition.Status = status; - } - } - - ImGui.EndCombo(); - } - ImGui.PopItemWidth(); - ImGui.NextColumn(); - - ImGui.PushItemWidth(-1); - string comboPreview = this.editingCondition.LayoutId == Guid.Empty ? string.Empty : this.plugin.Config.Layouts2[this.editingCondition.LayoutId].Name; - if (ImGui.BeginCombo("##condition-edit-layout", comboPreview)) { - foreach (var layout in this.plugin.Config.Layouts2) { - if (ImGui.Selectable($"{layout.Value.Name}##condition-edit-layout-{layout.Key}")) { - this.editingCondition.LayoutId = layout.Key; - } - } - - ImGui.EndCombo(); - } - ImGui.PopItemWidth(); - ImGui.NextColumn(); - - if (this.editingCondition.LayoutId != Guid.Empty) { - if (IconButton(FontAwesomeIcon.Check, "condition-edit")) { - addCondition = true; - } - - ImGui.SameLine(); - } - - if (IconButton(FontAwesomeIcon.Times, "condition-stop")) { - this.editingConditionIndex = -1; - } - - if (this.scrollToAdd) { - this.scrollToAdd = false; - ImGui.SetScrollHereY(); - } - } else { - ImGui.Text(item.cond.ClassJob ?? string.Empty); - ImGui.NextColumn(); - - ImGui.Text(item.cond.Status?.Name() ?? string.Empty); - ImGui.NextColumn(); - - this.plugin.Config.Layouts2.TryGetValue(item.cond.LayoutId, out Layout condLayout); - ImGui.Text(condLayout?.Name ?? string.Empty); - ImGui.NextColumn(); - - if (IconButton(FontAwesomeIcon.PencilAlt, $"{item.i}")) { - this.editingConditionIndex = item.i; - this.editingCondition = item.cond; - } - - ImGui.SameLine(); - if (IconButton(FontAwesomeIcon.Trash, $"{item.i}")) { - actionedItemIndex = item.i; - } - - ImGui.SameLine(); - if (IconButton(FontAwesomeIcon.ArrowUp, $"{item.i}")) { - actionedItemIndex = item.i; - action = -1; - } - - ImGui.SameLine(); - if (IconButton(FontAwesomeIcon.ArrowDown, $"{item.i}")) { - actionedItemIndex = item.i; - action = 1; - } - } - - ImGui.NextColumn(); - } - - ImGui.Columns(1); - - ImGui.Separator(); - - ImGui.EndChild(); - - if (IconButton(FontAwesomeIcon.Plus, "condition")) { - this.editingConditionIndex = this.plugin.Config.HudConditionMatches.Count; - this.editingCondition = new HudConditionMatch(); - this.scrollToAdd = true; - } - - bool recalculate = false; - - if (addCondition) { - recalculate = true; - if (this.editingConditionIndex == this.plugin.Config.HudConditionMatches.Count) { - this.plugin.Config.HudConditionMatches.Add(this.editingCondition); - } else { - this.plugin.Config.HudConditionMatches.RemoveAt(this.editingConditionIndex); - this.plugin.Config.HudConditionMatches.Insert(this.editingConditionIndex, this.editingCondition); - } - this.plugin.Config.Save(); - this.editingConditionIndex = -1; - } - - if (actionedItemIndex >= 0) { - recalculate = true; - if (action == 0) { - this.plugin.Config.HudConditionMatches.RemoveAt(actionedItemIndex); - } else { - if (actionedItemIndex + action >= 0 && actionedItemIndex + action < this.plugin.Config.HudConditionMatches.Count) { - // Move the condition. - var c = this.plugin.Config.HudConditionMatches[actionedItemIndex]; - this.plugin.Config.HudConditionMatches.RemoveAt(actionedItemIndex); - this.plugin.Config.HudConditionMatches.Insert(actionedItemIndex + action, c); - } - } - this.plugin.Config.Save(); - } - - if (recalculate) { - PlayerCharacter player = this.pi.ClientState.LocalPlayer; - if (player != null && this.plugin.Config.SwapsEnabled) { - this.plugin.Statuses.Update(player); - this.plugin.Statuses.SetHudLayout(null); - } - } - } - - private static bool IconButton(FontAwesomeIcon icon, string append = "") { - ImGui.PushFont(UiBuilder.IconFont); - bool button = ImGui.Button($"{icon.ToIconString()}##{append}"); - ImGui.PopFont(); - return button; - } - - private static void HelpMarker(string text) { - ImGui.TextDisabled("(?)"); - if (ImGui.IsItemHovered()) { - ImGui.BeginTooltip(); - ImGui.PushTextWrapPos(ImGui.GetFontSize() * 20f); - ImGui.TextUnformatted(text); - ImGui.PopTextWrapPos(); - ImGui.EndTooltip(); - } - } - - public void Draw() { - this.DrawSettings(); - } - - private Dictionary> GetPositions() { - Dictionary> positions = new Dictionary>(); - - foreach (string name in SAVED_WINDOWS) { - Vector2 pos = this.plugin.GameFunctions.GetWindowPosition(name); - if (pos != null) { - positions[name] = pos; - } - } - - return positions; - } - - public void ImportSlot(string name, HudSlot slot, bool save = true) { - Dictionary> positions; - if (this.plugin.Config.ImportPositions) { - positions = this.GetPositions(); - } else { - positions = new Dictionary>(); - } - this.Import(name, this.plugin.Hud.ReadLayout(slot), positions, save); - } - - public void Import(string name, byte[] layout, Dictionary> positions, bool save = true) { - Guid guid = this.plugin.Config.Layouts2.FirstOrDefault(kv => kv.Value.Name == name).Key; - guid = guid != default ? guid : Guid.NewGuid(); - - this.plugin.Config.Layouts2[Guid.NewGuid()] = new Layout(name, layout, positions); - if (save) { - this.plugin.Config.Save(); - } - } - } -} diff --git a/HudSwap/PluginUI.cs.orig b/HudSwap/PluginUI.cs.orig deleted file mode 100644 index 968d045..0000000 --- a/HudSwap/PluginUI.cs.orig +++ /dev/null @@ -1,497 +0,0 @@ -using Dalamud.Game.ClientState.Actors.Types; -using Dalamud.Interface; -using Dalamud.Plugin; -using ImGuiNET; -using Lumina.Excel.GeneratedSheets; -using Newtonsoft.Json; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; - -// TODO: Zone swaps? - -namespace HudSwap { - public class PluginUI { - private static readonly string[] SAVED_WINDOWS = { - "AreaMap", - "ChatLog", - "ChatLogPanel_0", - "ChatLogPanel_1", - "ChatLogPanel_2", - "ChatLogPanel_3", - }; - - private readonly HudSwapPlugin plugin; - private readonly DalamudPluginInterface pi; - - private bool _settingsVisible = false; - public bool SettingsVisible { get => this._settingsVisible; set => this._settingsVisible = value; } - - public PluginUI(HudSwapPlugin plugin, DalamudPluginInterface pi) { - this.plugin = plugin; - this.pi = pi; - } - - public void ConfigUI(object sender, EventArgs args) { - this.SettingsVisible = true; - } - - private string importName = ""; - private string renameName = ""; - private Guid selectedLayoutId = Guid.Empty; - - private Layout selectedLayout => - selectedLayoutId == Guid.Empty ? null : this.plugin.Config.Layouts2[this.selectedLayoutId]; - - private int editingConditionIndex = -1; - private HudConditionMatch editingCondition; - private bool scrollToAdd = false; - - private static bool configErrorOpen = true; - public static void ConfigError() { - if (ImGui.Begin("HudSwap error", ref configErrorOpen)) { - ImGui.Text("Could not load HudSwap configuration."); - ImGui.Spacing(); - ImGui.Text("If you are updating from a previous version, please\ndelete your configuration file and restart the game."); - - ImGui.End(); - } - } - - public void DrawSettings() { - if (!this.SettingsVisible) { - return; - } - - ImGui.SetNextWindowSize(new Vector2(500, 450), ImGuiCond.FirstUseEver); - - if (!ImGui.Begin(this.plugin.Name, ref this._settingsVisible)) { - return; - } - - if (ImGui.BeginTabBar("##hudswap-tabs")) { - if (!this.plugin.Config.UnderstandsRisks) { - if (ImGui.BeginTabItem("About")) { - ImGui.TextColored(new Vector4(1f, 0f, 0f, 1f), "Read this first"); - ImGui.Separator(); - ImGui.PushTextWrapPos(ImGui.GetFontSize() * 20f); - ImGui.Text("HudSwap will use the configured staging slot as its own slot to make changes to. This means the staging slot will be overwritten whenever any swap happens."); - ImGui.Spacing(); - ImGui.Text("Any HUD layout changes you make while HudSwap is enabled may potentially be lost, no matter what slot. If you want to make changes to your HUD layout, TURN OFF HudSwap first."); - ImGui.Spacing(); - ImGui.Text("When editing or making a new layout, to be completely safe, turn off swaps, set up your layout, import the layout into HudSwap, then turn on swaps."); - ImGui.Spacing(); - ImGui.Text("If you are a new user, HudSwap auto-imported your existing layouts on startup."); - ImGui.Spacing(); - ImGui.Text("Finally, HudSwap is beta software. Back up your character data before using this plugin. You may lose some to all of your HUD layouts while testing this plugin."); - ImGui.Separator(); - ImGui.Text("If you have read all of the above and are okay with continuing, check the box below to enable HudSwap. You only need to do this once."); - ImGui.PopTextWrapPos(); - bool understandsRisks = this.plugin.Config.UnderstandsRisks; - if (ImGui.Checkbox("I understand", ref understandsRisks)) { - this.plugin.Config.UnderstandsRisks = understandsRisks; - this.plugin.Config.Save(); - } - - ImGui.EndTabItem(); - } - - ImGui.EndTabBar(); - ImGui.End(); - return; - } - - if (ImGui.BeginTabItem("Layouts")) { - ImGui.Text("Saved layouts"); - if (this.plugin.Config.Layouts2.Count == 0) { - ImGui.Text("None saved!"); - } else { - ImGui.PushItemWidth(-1); - if (ImGui.ListBoxHeader("##saved-layouts")) { - foreach (KeyValuePair entry in this.plugin.Config.Layouts2) { - if (ImGui.Selectable($"{entry.Value.Name}##{entry.Key}", this.selectedLayoutId == entry.Key)) { - this.selectedLayoutId = entry.Key; - this.renameName = entry.Value.Name; - this.importName = this.renameName; - } - } - ImGui.ListBoxFooter(); - } - ImGui.PopItemWidth(); - - ImGui.PushItemWidth(200); - ImGui.InputText("##rename-input", ref this.renameName, 100); - ImGui.PopItemWidth(); - ImGui.SameLine(); - if (ImGui.Button("Rename") && this.renameName.Length != 0 && this.selectedLayoutId != null) { - Layout layout = this.plugin.Config.Layouts2[this.selectedLayoutId]; - Layout newLayout = new Layout(this.renameName, layout.Hud, layout.Positions); - this.plugin.Config.Layouts2[this.selectedLayoutId] = newLayout; - this.plugin.Config.Save(); - } - - const int deleteButtonWidth = 30; - ImGui.SameLine(ImGui.GetWindowContentRegionWidth() - deleteButtonWidth); - ImGui.PushFont(UiBuilder.IconFont); - if (ImGui.Button(FontAwesomeIcon.Trash.ToIconString(), new Vector2(deleteButtonWidth, 0)) && - this.selectedLayoutId != null) { - this.plugin.Config.Layouts2.Remove(this.selectedLayoutId); - this.plugin.Config.HudConditionMatches.RemoveAll(m => m.LayoutId == this.selectedLayoutId); - this.selectedLayoutId = Guid.Empty; - this.renameName = ""; - this.plugin.Config.Save(); - } - ImGui.PopFont(); - - ImGui.Text("Copy to..."); - Vector2 slotButtonSize = new Vector2(40, 0); - foreach (HudSlot slot in Enum.GetValues(typeof(HudSlot))) { - // Surround the button with parentheses if this is the current slot - string slotText = slot == this.plugin.Hud.GetActiveHudSlot() ? $"({(int)slot + 1})" : ((int)slot + 1).ToString(); - string buttonName = $"{slotText}##copy"; - if (ImGui.Button(buttonName, slotButtonSize) && this.selectedLayout != null) { - this.plugin.Hud.WriteLayout(slot, this.selectedLayout.Hud); - } - ImGui.SameLine(); - } - - ImGui.SameLine(); - - if (ImGui.Button("Clipboard") && this.selectedLayoutId != null) { - if (this.plugin.Config.Layouts2.TryGetValue(this.selectedLayoutId, out Layout layout)) { - SharedLayout shared = new SharedLayout(layout); - string json = JsonConvert.SerializeObject(shared); - ImGui.SetClipboardText(json); - } - } - - } - - ImGui.Separator(); - - ImGui.Text("Import"); - - ImGui.InputText("Imported layout name", ref this.importName, 100); - - bool importPositions = this.plugin.Config.ImportPositions; - if (ImGui.Checkbox("Import window positions", ref importPositions)) { - this.plugin.Config.ImportPositions = importPositions; - this.plugin.Config.Save(); - } - ImGui.SameLine(); - HelpMarker("If this is checked, the position of the chat box and the map will be saved with the imported layout."); - - foreach (HudSlot slot in Enum.GetValues(typeof(HudSlot))) { - string buttonName = $"{(int)slot + 1}##import"; - if (ImGui.Button(buttonName) && this.importName.Length != 0) { - this.ImportSlot(this.importName, slot); - this.importName = ""; - } - ImGui.SameLine(); - } - - if (ImGui.Button("Clipboard") && this.importName.Length != 0) { - SharedLayout shared = null; - try { - shared = (SharedLayout)JsonConvert.DeserializeObject(ImGui.GetClipboardText(), typeof(SharedLayout)); -#pragma warning disable CA1031 // Do not catch general exception types - } catch (Exception) { -#pragma warning restore CA1031 // Do not catch general exception types - } - if (shared != null) { - byte[] layout = shared.Layout(); - if (layout != null) { - this.Import(this.importName, layout, shared.Positions); - this.importName = ""; - } - } - } - - ImGui.EndTabItem(); - } - - if (ImGui.BeginTabItem("Swaps")) { - bool enabled = this.plugin.Config.SwapsEnabled; - if (ImGui.Checkbox("Enable swaps", ref enabled)) { - this.plugin.Config.SwapsEnabled = enabled; - this.plugin.Config.Save(); - } - ImGui.Text("Note: Disable swaps when editing your HUD."); - - ImGui.Spacing(); - string staging = ((int)this.plugin.Config.StagingSlot + 1).ToString(); - if (ImGui.BeginCombo("Staging slot", staging)) { - foreach (HudSlot slot in Enum.GetValues(typeof(HudSlot))) { - if (ImGui.Selectable(((int)slot + 1).ToString())) { - this.plugin.Config.StagingSlot = slot; - this.plugin.Config.Save(); - } - } - ImGui.EndCombo(); - } - ImGui.SameLine(); - HelpMarker("The staging slot is the HUD layout slot that will be used as your HUD layout. All changes will be written to this slot when swaps are enabled."); - - ImGui.Separator(); - - if (this.plugin.Config.Layouts2.Count == 0) { - ImGui.Text("Create at least one layout to begin setting up swaps."); - } else { - ImGui.Text("Add swap conditions below.\nThe first condition that is satisfied will be the layout that is used."); - ImGui.Separator(); - this.DrawConditionsTable(); - } - - ImGui.EndTabItem(); - } - - ImGui.EndTabBar(); - } - - ImGui.End(); - } - - private void DrawConditionsTable() { - ImGui.PushFont(UiBuilder.IconFont); - float height = ImGui.GetContentRegionAvail().Y - ImGui.CalcTextSize(FontAwesomeIcon.Plus.ToIconString()).Y - ImGui.GetStyle().ItemSpacing.Y - ImGui.GetStyle().ItemInnerSpacing.Y * 2; - ImGui.PopFont(); - if (!ImGui.BeginChild("##conditions-table", new Vector2(-1, height))) { - return; - } - - ImGui.Columns(4); - - var conditions = new List(this.plugin.Config.HudConditionMatches); - if (this.editingConditionIndex == conditions.Count) { - conditions.Add(new HudConditionMatch()); - } - - ImGui.Text("Job"); - ImGui.NextColumn(); - - ImGui.Text("State"); - ImGui.NextColumn(); - - ImGui.Text("Layout"); - ImGui.NextColumn(); - - ImGui.Text("Options"); - ImGui.NextColumn(); - - ImGui.Separator(); - - bool addCondition = false; - int actionedItemIndex = -1; - int action = 0; // 0 for delete, otherwise move. - foreach (var item in conditions.Select((cond, i) => new { cond, i })) { - if (this.editingConditionIndex == item.i) { - ImGui.PushItemWidth(-1); - if (ImGui.BeginCombo("##condition-edit-job", this.editingCondition.ClassJob ?? "Any")) { - if (ImGui.Selectable("Any##condition-edit-job")) { - this.editingCondition.ClassJob = null; - } - - foreach (ClassJob job in this.pi.Data.GetExcelSheet().Skip(1)) { - if (ImGui.Selectable($"{job.Abbreviation}##condition-edit-job")) { - this.editingCondition.ClassJob = job.Abbreviation; - } - } - - ImGui.EndCombo(); - } - ImGui.PopItemWidth(); - ImGui.NextColumn(); - - ImGui.PushItemWidth(-1); - if (ImGui.BeginCombo("##condition-edit-status", this.editingCondition.Status?.Name() ?? "Any")) { - if (ImGui.Selectable("Any##condition-edit-status")) { - this.editingCondition.Status = null; - } - - foreach (Status status in Enum.GetValues(typeof(Status))) { - if (ImGui.Selectable($"{status.Name()}##condition-edit-status")) { - this.editingCondition.Status = status; - } - } - - ImGui.EndCombo(); - } - ImGui.PopItemWidth(); - ImGui.NextColumn(); - - ImGui.PushItemWidth(-1); - string comboPreview = this.editingCondition.LayoutId == Guid.Empty ? string.Empty : this.plugin.Config.Layouts2[this.editingCondition.LayoutId].Name; - if (ImGui.BeginCombo("##condition-edit-layout", comboPreview)) { - foreach (var layout in this.plugin.Config.Layouts2) { - if (ImGui.Selectable($"{layout.Value.Name}##condition-edit-layout-{layout.Key}")) { - this.editingCondition.LayoutId = layout.Key; - } - } - - ImGui.EndCombo(); - } - ImGui.PopItemWidth(); - ImGui.NextColumn(); - - if (this.editingCondition.LayoutId != Guid.Empty) { - if (IconButton(FontAwesomeIcon.Check, "condition-edit")) { - addCondition = true; - } - - ImGui.SameLine(); - } - - if (IconButton(FontAwesomeIcon.Times, "condition-stop")) { - this.editingConditionIndex = -1; - } - - if (this.scrollToAdd) { - this.scrollToAdd = false; - ImGui.SetScrollHereY(); - } - } else { - ImGui.Text(item.cond.ClassJob ?? string.Empty); - ImGui.NextColumn(); - - ImGui.Text(item.cond.Status?.Name() ?? string.Empty); - ImGui.NextColumn(); - - this.plugin.Config.Layouts2.TryGetValue(item.cond.LayoutId, out Layout condLayout); - ImGui.Text(condLayout?.Name ?? string.Empty); - ImGui.NextColumn(); - - if (IconButton(FontAwesomeIcon.PencilAlt, $"{item.i}")) { - this.editingConditionIndex = item.i; - this.editingCondition = item.cond; - } - - ImGui.SameLine(); - if (IconButton(FontAwesomeIcon.Trash, $"{item.i}")) { - actionedItemIndex = item.i; - } - - ImGui.SameLine(); - if (IconButton(FontAwesomeIcon.ArrowUp, $"{item.i}")) { - actionedItemIndex = item.i; - action = -1; - } - - ImGui.SameLine(); - if (IconButton(FontAwesomeIcon.ArrowDown, $"{item.i}")) { - actionedItemIndex = item.i; - action = 1; - } - } - - ImGui.NextColumn(); - } - - ImGui.Columns(1); - - ImGui.Separator(); - - ImGui.EndChild(); - - if (IconButton(FontAwesomeIcon.Plus, "condition")) { - this.editingConditionIndex = this.plugin.Config.HudConditionMatches.Count; - this.editingCondition = new HudConditionMatch(); - this.scrollToAdd = true; - } - - bool recalculate = false; - - if (addCondition) { - recalculate = true; - if (this.editingConditionIndex == this.plugin.Config.HudConditionMatches.Count) { - this.plugin.Config.HudConditionMatches.Add(this.editingCondition); - } else { - this.plugin.Config.HudConditionMatches.RemoveAt(this.editingConditionIndex); - this.plugin.Config.HudConditionMatches.Insert(this.editingConditionIndex, this.editingCondition); - } - this.plugin.Config.Save(); - this.editingConditionIndex = -1; - } - - if (actionedItemIndex >= 0) { - recalculate = true; - if (action == 0) { - this.plugin.Config.HudConditionMatches.RemoveAt(actionedItemIndex); - } else { - if (actionedItemIndex + action >= 0 && actionedItemIndex + action < this.plugin.Config.HudConditionMatches.Count) { - // Move the condition. - var c = this.plugin.Config.HudConditionMatches[actionedItemIndex]; - this.plugin.Config.HudConditionMatches.RemoveAt(actionedItemIndex); - this.plugin.Config.HudConditionMatches.Insert(actionedItemIndex + action, c); - } - } - this.plugin.Config.Save(); - } - - if (recalculate) { - PlayerCharacter player = this.pi.ClientState.LocalPlayer; - if (player != null && this.plugin.Config.SwapsEnabled) { - this.plugin.Statuses.Update(player); - this.plugin.Statuses.SetHudLayout(null); - } - } - } - - private static bool IconButton(FontAwesomeIcon icon, string append = "") { - ImGui.PushFont(UiBuilder.IconFont); - bool button = ImGui.Button($"{icon.ToIconString()}##{append}"); - ImGui.PopFont(); - return button; - } - - private static void HelpMarker(string text) { - ImGui.TextDisabled("(?)"); - if (ImGui.IsItemHovered()) { - ImGui.BeginTooltip(); - ImGui.PushTextWrapPos(ImGui.GetFontSize() * 20f); - ImGui.TextUnformatted(text); - ImGui.PopTextWrapPos(); - ImGui.EndTooltip(); - } - } - - public void Draw() { - this.DrawSettings(); - } - - private Dictionary> GetPositions() { - Dictionary> positions = new Dictionary>(); - - foreach (string name in SAVED_WINDOWS) { - Vector2 pos = this.plugin.GameFunctions.GetWindowPosition(name); - if (pos != null) { - positions[name] = pos; - } - } - - return positions; - } - - public void ImportSlot(string name, HudSlot slot, bool save = true) { - Dictionary> positions; - if (this.plugin.Config.ImportPositions) { - positions = this.GetPositions(); - } else { - positions = new Dictionary>(); - } - this.Import(name, this.plugin.Hud.ReadLayout(slot), positions, save); - } - - public void Import(string name, byte[] layout, Dictionary> positions, bool save = true) { - Guid guid = this.plugin.Config.Layouts2.FirstOrDefault(kv => kv.Value.Name == name).Key; - if (guid == default) { - guid = new Guid(); - } - - this.plugin.Config.Layouts2[Guid.NewGuid()] = new Layout(name, layout, positions); - if (save) { - this.plugin.Config.Save(); - } - } - } -} diff --git a/HudSwap/Properties/AssemblyInfo.cs b/HudSwap/Properties/AssemblyInfo.cs deleted file mode 100644 index 7d96a84..0000000 --- a/HudSwap/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("HudSwap")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("HudSwap")] -[assembly: AssemblyCopyright("Copyright © 2020")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("cbf13a29-a5a4-4fa2-8a4f-cfbb5fb83f90")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.5.1")] -[assembly: AssemblyFileVersion("1.5.1")] diff --git a/HudSwap/Swapper.cs b/HudSwap/Swapper.cs deleted file mode 100644 index fd4d8c1..0000000 --- a/HudSwap/Swapper.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Dalamud.Game.ClientState.Actors.Types; -using Dalamud.Game.Internal; -using Dalamud.Plugin; -using System; - -namespace HudSwap { - public class Swapper { - private readonly HudSwapPlugin plugin; - private readonly DalamudPluginInterface pi; - - public Swapper(HudSwapPlugin plugin, DalamudPluginInterface pi) { - this.plugin = plugin; - this.pi = pi; - } - - public void OnFrameworkUpdate(Framework framework) { - if (framework == null) { - throw new ArgumentNullException(nameof(framework), "Framework cannot be null"); - } - - if (!this.plugin.Config.SwapsEnabled || !this.plugin.Config.UnderstandsRisks) { - return; - } - - PlayerCharacter player = this.pi.ClientState.LocalPlayer; - if (player == null) { - return; - } - - if (this.plugin.Statuses.Update(player)) { - this.plugin.Statuses.SetHudLayout(null); - } - } - } -} diff --git a/HudSwap/packages.config b/HudSwap/packages.config deleted file mode 100644 index b8ca95a..0000000 --- a/HudSwap/packages.config +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file