feat: add element options and fix ctd

Prevent changing layouts while the Character Configuration window is open.

Also fix visibility to show both keyboard and gamepad checkboxes.
This commit is contained in:
Anna 2021-03-11 14:14:34 -05:00
parent ef9a4905ce
commit 9415725a44
12 changed files with 547 additions and 84 deletions

View File

@ -6,7 +6,7 @@ using Dalamud.Plugin;
namespace HUD_Manager.Configuration {
[Serializable]
public class Config : IPluginConfiguration {
public int Version { get; set; } = 2;
public int Version { get; set; } = 3;
private DalamudPluginInterface Interface { get; set; } = null!;

View File

@ -31,6 +31,43 @@ namespace HUD_Manager.Configuration {
return config;
}
private static Config MigrateV2(JObject old) {
foreach (var property in old["Layouts"].Children<JProperty>()) {
if (property.Name == "$type") {
continue;
}
var layout = (JObject) property.Value;
var elements = (JObject) layout["Elements"];
foreach (var elementProp in elements.Children<JProperty>()) {
if (elementProp.Name == "$type") {
continue;
}
var element = (JObject) elementProp.Value;
var bytes = element["Unknown4"].ToObject<byte[]>();
var options = new byte[4];
Buffer.BlockCopy(bytes, 0, options, 0, 4);
var width = BitConverter.ToUInt16(bytes, 4);
var height = BitConverter.ToUInt16(bytes, 6);
var unknown4 = bytes[8];
element.Remove("Unknown4");
element["Options"] = options;
element["Width"] = width;
element["Height"] = height;
element["Unknown4"] = unknown4;
}
}
old["Version"] = 3;
return old.ToObject<Config>();
}
private static string PluginConfig(string? pluginName = null) {
pluginName ??= Assembly.GetAssembly(typeof(Plugin)).GetName().Name;
return Path.Combine(new[] {
@ -44,27 +81,40 @@ namespace HUD_Manager.Configuration {
public static Config LoadConfig(Plugin plugin) {
var managerPath = PluginConfig();
string? text = null;
if (File.Exists(managerPath)) {
goto DefaultConfig;
text = File.ReadAllText(managerPath);
goto CheckVersion;
}
var hudSwapPath = PluginConfig("HudSwap");
if (File.Exists(hudSwapPath)) {
var oldText = File.ReadAllText(hudSwapPath);
var config = JsonConvert.DeserializeObject<JObject>(oldText);
uint version = 1;
if (config.TryGetValue("Version", out var token)) {
version = token.Value<uint>();
}
text = File.ReadAllText(hudSwapPath);
}
if (version == 1) {
CheckVersion:
if (text == null) {
goto DefaultConfig;
}
var config = JsonConvert.DeserializeObject<JObject>(text);
uint version = 1;
if (config.TryGetValue("Version", out var token)) {
version = token.Value<uint>();
}
switch (version) {
case 1: {
var v1 = config.ToObject<ConfigV1>(new JsonSerializer {
TypeNameHandling = TypeNameHandling.None,
});
return Migrate(v1);
}
case 2: {
return MigrateV2(config);
}
}
DefaultConfig:

View File

@ -11,9 +11,11 @@ namespace HUD_Manager {
public class Hud {
// Updated 5.45
public const int InMemoryLayoutElements = 81;
// Updated 5.45
// Each element is 32 bytes in ADDON.DAT, but they're 36 bytes when loaded into memory.
private const int LayoutSize = InMemoryLayoutElements * 36;
// Updated 5.4
private const int SlotOffset = 0x59e8;
@ -54,27 +56,32 @@ namespace HUD_Manager {
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;
}
unsafe {
var currentSlotPtr = (uint*) (this.GetDataPointer() + SlotOffset);
// read the current slot
var currentSlot = *currentSlotPtr;
// if the current slot is the slot we want to change to, we can force a reload by
// telling the game it's on a different slot and swapping back to the desired slot
if (currentSlot == (uint) slot) {
var backupSlot = currentSlot;
if (backupSlot < 3) {
backupSlot += 1;
} else {
backupSlot = 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;
// back up this different slot
var backup = this.ReadLayout((HudSlot) backupSlot);
// change the current slot in memory
*currentSlotPtr = backupSlot;
// 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) backupSlot, backup, false);
return;
}
}
Return:
@ -106,7 +113,7 @@ namespace HUD_Manager {
return Marshal.PtrToStructure<Layout>(slotPtr);
}
private void WriteLayout(HudSlot slot, Layout layout) {
private void WriteLayout(HudSlot slot, Layout layout, bool reloadIfNecessary = true) {
var slotPtr = this.GetLayoutPointer(slot);
var dict = layout.ToDictionary();
@ -124,6 +131,10 @@ namespace HUD_Manager {
// copy directly over
// Marshal.StructureToPtr(layout, slotPtr, false);
if (!reloadIfNecessary) {
return;
}
var currentSlot = this.GetActiveHudSlot();
if (currentSlot == slot) {
this.SelectSlot(currentSlot, true);

View File

@ -7,6 +7,7 @@ using Dalamud.Interface;
using Dalamud.Plugin;
using HUD_Manager.Configuration;
using HUD_Manager.Structs;
using HUD_Manager.Structs.Options;
using HUD_Manager.Tree;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
@ -38,19 +39,6 @@ namespace HUD_Manager {
0.6f,
};
private static readonly string[] ScaleOptionsNames = {
"200%",
"180%",
"160%",
"140%",
"120%",
"110%",
"100%",
"90%",
"80%",
"60%",
};
private Plugin Plugin { get; }
private bool _settingsVisible;
@ -64,14 +52,11 @@ namespace HUD_Manager {
private float _dragSpeed = 1.0f;
private string? _importName;
private Guid _selectedLayoutId = Guid.Empty;
private string? _newLayoutName;
private string? _renameLayout;
private Guid _selectedEditLayout = 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;
@ -205,6 +190,12 @@ namespace HUD_Manager {
goto EndTabItem;
}
var charConfig = this.Plugin.Interface.Framework.Gui.GetAddonByName("ConfigCharacter", 1);
if (charConfig != null && charConfig.Visible) {
ImGui.TextUnformatted("Please close the Character Configuration window before continuing.");
goto EndTabItem;
}
var update = false;
ImGui.TextUnformatted("Layout");
@ -357,57 +348,181 @@ namespace HUD_Manager {
if (ImGui.BeginChild("uimanager-layout-editor-elements", new Vector2(0, 0))) {
var toRemove = new List<ElementKind>();
foreach (var entry in layout.Elements) {
var name = entry.Key.LocalisedName(this.Plugin.Interface.Data);
var sortedElements = layout.Elements
.Select(entry => Tuple.Create(entry.Key, entry.Value, entry.Key.LocalisedName(this.Plugin.Interface.Data)))
.OrderBy(tuple => tuple.Item3);
foreach (var (kind, element, name) in sortedElements) {
if (this._editorSearch != null && !name.ContainsIgnoreCase(this._editorSearch)) {
continue;
}
ImGui.TextUnformatted(name);
var element = entry.Value;
var visible = element.Visibility == Visibility.Visible;
if (ImGui.Checkbox($"Visible##{entry.Key}", ref visible)) {
element.Visibility = visible ? Visibility.Visible : Visibility.Hidden;
update = true;
if (!ImGui.CollapsingHeader(name)) {
continue;
}
ImGui.SameLine();
if (IconButton(FontAwesomeIcon.TrashAlt, $"uimanager-remove-element-{entry.Key}")) {
toRemove.Add(entry.Key);
var drawVisibility = !kind.IsJobGauge();
void DrawDelete() {
ImGui.SameLine(ImGui.GetContentRegionAvail().X - 30);
if (IconButton(FontAwesomeIcon.TrashAlt, $"uimanager-remove-element-{kind}")) {
toRemove.Add(kind);
}
}
if (drawVisibility) {
ImGui.TextUnformatted("Visibility");
DrawDelete();
var keyboard = element[VisibilityFlags.Keyboard];
if (IconCheckbox(FontAwesomeIcon.Keyboard, ref keyboard, $"{kind}")) {
element[VisibilityFlags.Keyboard] = keyboard;
update = true;
}
ImGui.SameLine();
var gamepad = element[VisibilityFlags.Gamepad];
if (IconCheckbox(FontAwesomeIcon.Gamepad, ref gamepad, $"{kind}")) {
element[VisibilityFlags.Gamepad] = gamepad;
update = true;
}
}
var x = element.X;
if (ImGui.DragFloat($"X##{entry.Key}", ref x, this._dragSpeed)) {
if (ImGui.DragFloat($"X##{kind}", ref x, this._dragSpeed)) {
element.X = x;
update = true;
}
if (!drawVisibility) {
DrawDelete();
}
var y = element.Y;
if (ImGui.DragFloat($"Y##{entry.Key}", ref y, this._dragSpeed)) {
if (ImGui.DragFloat($"Y##{kind}", ref y, this._dragSpeed)) {
element.Y = y;
update = true;
}
var scaleIdx = Array.IndexOf(ScaleOptions, element.Scale);
if (scaleIdx == -1) {
scaleIdx = 6;
var currentScale = $"{element.Scale * 100}%";
if (ImGui.BeginCombo($"Scale##{kind}", currentScale)) {
foreach (var scale in ScaleOptions) {
if (!ImGui.Selectable($"{scale * 100}%")) {
continue;
}
element.Scale = scale;
update = true;
}
ImGui.EndCombo();
}
if (ImGui.Combo($"Scale##{entry.Key}", ref scaleIdx, ScaleOptionsNames, ScaleOptionsNames.Length)) {
element.Scale = ScaleOptions[scaleIdx];
update = true;
if (!kind.IsJobGauge()) {
var opacity = (int) element.Opacity;
if (ImGui.DragInt($"Opacity##{kind}", ref opacity, 1, 1, 255)) {
element.Opacity = (byte) opacity;
update = true;
}
}
var opacity = (int) element.Opacity;
if (ImGui.DragInt($"Opacity##{entry.Key}", ref opacity, 1, 1, 255)) {
element.Opacity = (byte) opacity;
update = true;
if (kind == ElementKind.TargetBar) {
var targetBarOpts = new TargetBarOptions(element.Options);
var independent = targetBarOpts.ShowIndependently;
if (ImGui.Checkbox("Display target information independently", ref independent)) {
targetBarOpts.ShowIndependently = independent;
update = true;
}
}
ImGui.Separator();
if (kind == ElementKind.StatusEffects) {
var statusOpts = new StatusOptions(element.Options);
if (ImGui.BeginCombo($"Style##{kind}", statusOpts.Style.ToString())) {
foreach (var style in (StatusStyle[]) Enum.GetValues(typeof(StatusStyle))) {
if (!ImGui.Selectable($"{style}##{kind}")) {
continue;
}
statusOpts.Style = style;
update = true;
}
ImGui.EndCombo();
}
}
if (kind == ElementKind.StatusInfoEnhancements || kind == ElementKind.StatusInfoEnfeeblements || kind == ElementKind.StatusInfoOther) {
var statusOpts = new StatusInfoOptions(kind, element.Options);
if (ImGui.BeginCombo($"Layout##{kind}", statusOpts.Layout.ToString())) {
foreach (var sLayout in (StatusLayout[]) Enum.GetValues(typeof(StatusLayout))) {
if (!ImGui.Selectable($"{sLayout}##{kind}")) {
continue;
}
statusOpts.Layout = sLayout;
update = true;
}
ImGui.EndCombo();
}
if (ImGui.BeginCombo($"Alignment##{kind}", statusOpts.Alignment.ToString())) {
foreach (var alignment in (StatusAlignment[]) Enum.GetValues(typeof(StatusAlignment))) {
if (!ImGui.Selectable($"{alignment}##{kind}")) {
continue;
}
statusOpts.Alignment = alignment;
update = true;
}
ImGui.EndCombo();
}
var focusable = statusOpts.Gamepad == StatusGamepad.Focusable;
if (ImGui.Checkbox($"Focusable by gamepad##{kind}", ref focusable)) {
statusOpts.Gamepad = focusable ? StatusGamepad.Focusable : StatusGamepad.NonFocusable;
update = true;
}
}
if (kind.IsHotbar()) {
var hotbarOpts = new HotbarOptions(element.Options);
if (kind != ElementKind.PetHotbar) {
var hotbarIndex = (int) hotbarOpts.Index;
if (ImGui.InputInt($"Hotbar number##{kind}", ref hotbarIndex)) {
hotbarOpts.Index = (byte) Math.Max(0, Math.Min(9, hotbarIndex));
update = true;
}
}
if (ImGui.BeginCombo($"Hotbar layout##{kind}", hotbarOpts.Layout.ToString())) {
foreach (var hotbarLayout in (HotbarLayout[]) Enum.GetValues(typeof(HotbarLayout))) {
if (!ImGui.Selectable($"{hotbarLayout}##{kind}")) {
continue;
}
hotbarOpts.Layout = hotbarLayout;
update = true;
}
ImGui.EndCombo();
}
}
if (kind.IsJobGauge()) {
var gaugeOpts = new GaugeOptions(element.Options);
var simple = gaugeOpts.Style == GaugeStyle.Simple;
if (ImGui.Checkbox($"Simple##{kind}", ref simple)) {
gaugeOpts.Style = simple ? GaugeStyle.Simple : GaugeStyle.Normal;
update = true;
}
}
}
foreach (var remove in toRemove) {
@ -697,6 +812,19 @@ namespace HUD_Manager {
}
}
if (ImGui.Button("Print current slot")) {
var slot = this.Plugin.Hud.GetActiveHudSlot();
this.Plugin.Interface.Framework.Gui.Chat.Print($"{slot}");
}
var current = this.Plugin.Hud.ReadLayout(this.Plugin.Hud.GetActiveHudSlot());
foreach (var element in current.elements) {
ImGui.TextUnformatted(element.id.LocalisedName(this.Plugin.Interface.Data));
ImGui.TextUnformatted($"Width: {element.width}");
ImGui.TextUnformatted($"Height: {element.height}");
ImGui.Separator();
}
ImGui.EndTabItem();
}
#endif
@ -911,6 +1039,21 @@ namespace HUD_Manager {
return result;
}
public static bool IconCheckbox(FontAwesomeIcon icon, ref bool value, string? id = null) {
ImGui.PushFont(UiBuilder.IconFont);
var text = icon.ToIconString();
if (id != null) {
text += $"##{id}";
}
var result = ImGui.Checkbox(text, ref value);
ImGui.PopFont();
return result;
}
private static void HoverTooltip(string text) {
if (!ImGui.IsItemHovered()) {
return;

View File

@ -187,5 +187,55 @@ namespace HUD_Manager.Structs {
return name;
}
public static bool IsJobGauge(this ElementKind kind) {
switch (kind) {
case ElementKind.AetherflowGaugeSch:
case ElementKind.AetherflowGaugeSmn:
case ElementKind.ArcanaGauge:
case ElementKind.BalanceGauge:
case ElementKind.BeastGauge:
case ElementKind.BloodGauge:
case ElementKind.ChakraGauge:
case ElementKind.DarksideGauge:
case ElementKind.DragonGauge:
case ElementKind.ElementalGauge:
case ElementKind.FaerieGauge:
case ElementKind.FourfoldFeathers:
case ElementKind.HealingGauge:
case ElementKind.HeatGauge:
case ElementKind.HutonGauge:
case ElementKind.KenkiGauge:
case ElementKind.NinkiGauge:
case ElementKind.OathGauge:
case ElementKind.PowderGauge:
case ElementKind.SenGauge:
case ElementKind.SongGauge:
case ElementKind.StepGauge:
case ElementKind.TranceGauge:
return true;
default:
return false;
}
}
public static bool IsHotbar(this ElementKind kind) {
switch (kind) {
case ElementKind.Hotbar1:
case ElementKind.Hotbar2:
case ElementKind.Hotbar3:
case ElementKind.Hotbar4:
case ElementKind.Hotbar5:
case ElementKind.Hotbar6:
case ElementKind.Hotbar7:
case ElementKind.Hotbar8:
case ElementKind.Hotbar9:
case ElementKind.Hotbar10:
case ElementKind.PetHotbar:
return true;
default:
return false;
}
}
}
}

View File

@ -0,0 +1,19 @@
namespace HUD_Manager.Structs.Options {
public class GaugeOptions {
private readonly byte[] _options;
public GaugeStyle Style {
get => (GaugeStyle) this._options[0];
set => this._options[0] = (byte) value;
}
public GaugeOptions(byte[] options) {
this._options = options;
}
}
public enum GaugeStyle : byte {
Normal = 0,
Simple = 1,
}
}

View File

@ -0,0 +1,28 @@
namespace HUD_Manager.Structs.Options {
public class HotbarOptions {
private readonly byte[] _options;
public byte Index {
get => this._options[0];
set => this._options[0] = value;
}
public HotbarLayout Layout {
get => (HotbarLayout) this._options[1];
set => this._options[1] = (byte) value;
}
public HotbarOptions(byte[] options) {
this._options = options;
}
}
public enum HotbarLayout : byte {
TwelveByOne = 1,
SixByTwo = 2,
FourByThree = 3,
ThreeByFour = 4,
TwoBySix = 5,
OneByTwelve = 6,
}
}

View File

@ -0,0 +1,116 @@
using System;
namespace HUD_Manager.Structs.Options {
public class StatusOptions {
private readonly byte[] _options;
public StatusStyle Style {
get => (StatusStyle) this._options[0];
set => this._options[0] = (byte) value;
}
public StatusOptions(byte[] options) {
this._options = options;
}
}
public enum StatusStyle : byte {
Normal = 1,
NormalLeftJustified1 = 11,
NormalLeftJustified2 = 21,
NormalLeftJustified3 = 31,
ThreeGroups = 0,
}
public class StatusInfoOptions {
private const int GamepadBit = 1 << 4;
private readonly ElementKind _kind;
private readonly byte[] _options;
public StatusLayout Layout {
get => this.ExtractStyle().Item1;
set => this._options[0] = this.ComputeStyle(value, this.Alignment, this.Gamepad);
}
public StatusAlignment Alignment {
get => this.ExtractStyle().Item2;
set => this._options[0] = this.ComputeStyle(this.Layout, value, this.Gamepad);
}
public StatusGamepad Gamepad {
get => this.ExtractStyle().Item3;
set => this._options[0] = this.ComputeStyle(this.Layout, this.Alignment, value);
}
public StatusInfoOptions(ElementKind kind, byte[] options) {
this._kind = kind;
this._options = options;
}
private byte ComputeStyle(StatusLayout layout, StatusAlignment alignment, StatusGamepad gamepad) {
byte result = layout switch {
StatusLayout.TenByTwo => 0,
StatusLayout.TwentyByOne => 1,
StatusLayout.SevenByThree => 2,
StatusLayout.FiveByFour => 3,
_ => throw new ArgumentOutOfRangeException(nameof(layout), layout, null),
};
if (alignment == StatusAlignment.RightJustified) {
result += 4;
}
if (this._kind != ElementKind.StatusInfoOther && gamepad == StatusGamepad.NonFocusable) {
result |= GamepadBit;
}
if (this._kind == ElementKind.StatusInfoOther && gamepad == StatusGamepad.Focusable) {
result |= GamepadBit;
}
return result;
}
private Tuple<StatusLayout, StatusAlignment, StatusGamepad> ExtractStyle() {
var gamepadBitSet = (this._options[0] & GamepadBit) > 0;
var gamepad = this._kind == ElementKind.StatusInfoOther
? gamepadBitSet
? StatusGamepad.Focusable
: StatusGamepad.NonFocusable
: gamepadBitSet
? StatusGamepad.NonFocusable
: StatusGamepad.Focusable;
var basic = this._options[0] & ~GamepadBit;
var alignment = basic < 4 ? StatusAlignment.LeftJustified : StatusAlignment.RightJustified;
var layout = (basic % 4) switch {
0 => StatusLayout.TenByTwo,
1 => StatusLayout.TwentyByOne,
2 => StatusLayout.SevenByThree,
3 => StatusLayout.FiveByFour,
_ => throw new ArgumentOutOfRangeException(),
};
return Tuple.Create(layout, alignment, gamepad);
}
}
public enum StatusLayout {
TwentyByOne,
TenByTwo,
SevenByThree,
FiveByFour,
}
public enum StatusAlignment {
LeftJustified,
RightJustified,
}
public enum StatusGamepad {
Focusable,
NonFocusable,
}
}

View File

@ -0,0 +1,14 @@
namespace HUD_Manager.Structs.Options {
public class TargetBarOptions {
private readonly byte[] _options;
public bool ShowIndependently {
get => this._options[0] == 1;
set => this._options[0] = value ? 1 : 0;
}
public TargetBarOptions(byte[] options) {
this._options = options;
}
}
}

View File

@ -11,10 +11,16 @@ namespace HUD_Manager.Structs {
public float scale;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 9)]
public byte[] unknown4;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public byte[] options;
public Visibility visibility;
public ushort width;
public ushort height;
public byte unknown4;
public VisibilityFlags visibility;
public byte unknown6;
@ -28,6 +34,9 @@ namespace HUD_Manager.Structs {
this.x = element.X;
this.y = element.Y;
this.scale = element.Scale;
this.options = element.Options;
this.width = element.Width;
this.height = element.Height;
this.unknown4 = element.Unknown4;
this.visibility = element.Visibility;
this.unknown6 = element.Unknown6;
@ -45,9 +54,15 @@ namespace HUD_Manager.Structs {
public float Scale { get; set; }
public byte[] Unknown4 { get; set; }
public byte[] Options { get; set; }
public Visibility Visibility { get; set; }
public ushort Width { get; set; }
public ushort Height { get; set; }
public byte Unknown4 { get; set; }
public VisibilityFlags Visibility { get; set; }
public byte Unknown6 { get; set; }
@ -55,11 +70,25 @@ namespace HUD_Manager.Structs {
public byte[] Unknown8 { get; set; }
public bool this[VisibilityFlags flags] {
get => (this.Visibility & flags) > 0;
set {
if (value) {
this.Visibility |= flags;
} else {
this.Visibility &= ~flags;
}
}
}
public Element(RawElement raw) {
this.Id = raw.id;
this.X = raw.x;
this.Y = raw.y;
this.Scale = raw.scale;
this.Options = raw.options;
this.Width = raw.width;
this.Height = raw.height;
this.Unknown4 = raw.unknown4;
this.Visibility = raw.visibility;
this.Unknown6 = raw.unknown6;

View File

@ -1,6 +0,0 @@
namespace HUD_Manager.Structs {
public enum Visibility : byte {
Hidden = 1,
Visible = 3,
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace HUD_Manager.Structs {
[Flags]
public enum VisibilityFlags : byte {
Keyboard = 1 << 0,
Gamepad = 1 << 1,
}
}