Skip to content

Commit

Permalink
[Control Hint Mirroring] Rewrite
Browse files Browse the repository at this point in the history
  • Loading branch information
Caraxi committed Oct 10, 2023
1 parent d67f759 commit 1e5618f
Showing 1 changed file with 57 additions and 281 deletions.
338 changes: 57 additions & 281 deletions Tweaks/UiAdjustment/ControlHintMirroring.cs
Original file line number Diff line number Diff line change
@@ -1,304 +1,80 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.Interop;
using SimpleTweaksPlugin.Debugging;
using SimpleTweaksPlugin.Events;
using SimpleTweaksPlugin.TweakSystem;
using SimpleTweaksPlugin.Utility;

namespace SimpleTweaksPlugin.Tweaks.UiAdjustment;

public unsafe class ControlHintMirroring : UiAdjustments.SubTweak
{

struct HotbarSlotCommand
{
public uint Id;
public HotbarSlotType Type;
}

private delegate byte ActionBarBaseUpdate(AddonActionBarBase* addonActionBarBase, NumberArrayData** numberArrayData, StringArrayData** stringArrayData);

private HookWrapper<ActionBarBaseUpdate> actionBarBaseUpdateHook;

private ActionManager* actionManager;

public override string Name => "Duplicate Keybind Hints Between Hotbars";
public override string Description => "Will display the keybind hint for any hotbar slot onto unbound slots with the same action.";
protected override string Author => "BoredDan";

private static readonly string[] allActionBars = {
"_ActionBar",
"_ActionBar01",
"_ActionBar02",
"_ActionBar03",
"_ActionBar04",
"_ActionBar05",
"_ActionBar06",
"_ActionBar07",
"_ActionBar08",
"_ActionBar09",
};

private const int StrDataIndex = 5;
private const int StrDataHotbarLength = 16;
private const int StrDataSlotLength = 3;
private const int StrDataHintIndex = 2;

private static readonly int actionBarLength = 12;
private string[][] recordedControlHints = new string[allActionBars.Length][];
private HotbarSlotCommand[][] recordedCommands = new HotbarSlotCommand[allActionBars.Length][];

protected override void Enable()
{
UpdateAll();
actionBarBaseUpdateHook ??= Common.Hook<ActionBarBaseUpdate>("E8 ?? ?? ?? ?? 83 BB ?? ?? ?? ?? ?? 75 09", ActionBarBaseUpdateDetour);
actionBarBaseUpdateHook?.Enable();
actionManager = ActionManager.Instance();
base.Enable();
}

private static AddonActionBarBase* GetActionBarAddon(string actionBar)
{
return (AddonActionBarBase*)Service.GameGui.GetAddonByName(actionBar, 1);
}

private byte ActionBarBaseUpdateDetour(AddonActionBarBase* addonActionBarBase, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
var changesFound = false;

var hotbarID = addonActionBarBase->RaptureHotbarId;

if (hotbarID < allActionBars.Length)
{
changesFound = UpdateHotbarRecordedControlHints(addonActionBarBase, stringArrayData[StrDataIndex]);
changesFound |= UpdateHotbarCommands(addonActionBarBase);
}

var ret = actionBarBaseUpdateHook.Original(addonActionBarBase, numberArrayData, stringArrayData);

if (changesFound)
{
FillControlHints();
}
return ret;
}

private void UpdateAll()
{
var changesFound = UpdateHotbarRecordedControlHints();
changesFound |= UpdateCommands();

if (changesFound)
{
FillControlHints();
}
}

private void Reset()
{
foreach (var actionBar in allActionBars)
{
AddonActionBarBase* ab = GetActionBarAddon(actionBar);
if (ab != null && ab->ActionBarSlots != null)
{
ResetHotbar(ab);
[TweakName("Duplicate Keybind Hints Between Hotbars")]
[TweakDescription("Will display the keybind hint for any hotbar slot onto unbound slots with the same action.")]
[TweakAuthor("BoredDan")]
[TweakVersion(2)]
[Changelog(UnreleasedVersion, "Rewritten & re-enabled")]
public unsafe class ControlHintMirroring : UiAdjustments.SubTweak {
private readonly Dictionary<(HotbarSlotType Type, uint Id), string> hints = new();
private readonly Dictionary<(byte BarID, byte SlotIndex), bool> barSlotHasNoHint = new();
private readonly Dictionary<(byte BarID, byte SlotIndex), bool> barSlotIsSet = new();
private readonly string[] actionBars = { "_ActionBar09", "_ActionBar", "_ActionBar01", "_ActionBar02", "_ActionBar03", "_ActionBar04", "_ActionBar05", "_ActionBar06", "_ActionBar07", "_ActionBar08" };
// const arrays please?
[AddonPreRequestedUpdate("_ActionBar09", "_ActionBar", "_ActionBar01", "_ActionBar02", "_ActionBar03", "_ActionBar04", "_ActionBar05", "_ActionBar06", "_ActionBar07", "_ActionBar08")]
private void ActionBarUpdateRequested(AddonActionBarX* addon) {
var barId = addon->AddonActionBarBase.RaptureHotbarId;
if (barId == 9) hints.Clear(); // Relies on the game updating action bars in reverse order
for (var slotIndex = (byte)(addon->AddonActionBarBase.SlotCount - 1); slotIndex < addon->AddonActionBarBase.SlotCount; slotIndex--) {
if (barSlotIsSet.TryGetValue((barId, slotIndex), out var isSet) && isSet) {
addon->AddonActionBarBase.Slot[slotIndex].ControlHintTextNode->SetText(string.Empty);
barSlotIsSet[(barId, slotIndex)] = false;
}
}
recordedControlHints = new string[allActionBars.Length][];
recordedCommands = new HotbarSlotCommand[allActionBars.Length][];
}

private void ResetHotbar(AddonActionBarBase* ab)
{
if (ab == null || ab->ActionBarSlots == null) return;

var numSlots = ab->SlotCount;
var slot = RaptureHotbarModule.Instance()->GetSlotById(barId, slotIndex);
if (slot->CommandType == HotbarSlotType.Empty) continue;

for (int i = 0; i < numSlots; i++)
{
var slot = ab->ActionBarSlots[i];
if (slot.ControlHintTextNode != null)
{
var normalControlHint = recordedControlHints?[ab->RaptureHotbarId]?[i];
if (normalControlHint != null)
{
slot.ControlHintTextNode->SetText(normalControlHint);
}
var str = Common.ReadString(slot->KeybindHint, 0x10);
if (string.IsNullOrWhiteSpace(str)) {
barSlotHasNoHint[(barId, slotIndex)] = true;
continue;
}
}
}

private bool UpdateHotbarRecordedControlHints()
{
var changeFound = false;
foreach (var actionBar in allActionBars)
{
AddonActionBarBase* ab = GetActionBarAddon(actionBar);
if (ab != null && ab->ActionBarSlots != null)
{
changeFound |= UpdateHotbarRecordedControlHints(ab);
}
var commandId = slot->CommandType == HotbarSlotType.Action ? ActionManager.Instance()->GetAdjustedActionId(slot->CommandId) : slot->CommandId;
hints[(slot->CommandType, commandId)] = str;
barSlotHasNoHint[(barId, slotIndex)] = false;
}
return changeFound;

if (barId == 0 && !Unloading) Service.Framework.RunOnTick(FrameworkUpdate);
}

private bool UpdateHotbarRecordedControlHints(AddonActionBarBase* ab, StringArrayData* strArrayData = null)
{
if (ab == null || ab->ActionBarSlots == null) return false;
if (ab->RaptureHotbarId > recordedControlHints.Length) return false;

if (strArrayData == null)
{
strArrayData = Framework.Instance()->GetUiModule()->GetRaptureAtkModule()->AtkModule.AtkArrayDataHolder.StringArrays[StrDataIndex];
}

var numSlots = ab->SlotCount;

var changeFound = false;

if (recordedControlHints[ab->RaptureHotbarId] == null || recordedControlHints[ab->RaptureHotbarId].Length != numSlots || recordedControlHints[ab->RaptureHotbarId].Length != numSlots)
{
recordedControlHints[ab->RaptureHotbarId] = new string[numSlots];
changeFound = true;
}

for (int i = 0; i < numSlots; i++)
{
var currentControlHint = Marshal.PtrToStringUTF8(new IntPtr(strArrayData->StringArray[(ab->RaptureHotbarId * StrDataHotbarLength + i) * StrDataSlotLength + StrDataHintIndex]));
if (recordedControlHints?[ab->RaptureHotbarId]?[i] != currentControlHint)
{
recordedControlHints[ab->RaptureHotbarId][i] = currentControlHint;
changeFound = true;
}
}

return changeFound;
}

private bool UpdateCommands()
{
var changeFound = false;
foreach (var actionBar in allActionBars)
{
AddonActionBarBase* ab = GetActionBarAddon(actionBar);
if (ab != null)
{
changeFound |= UpdateHotbarCommands(ab);
}
}
return changeFound;
}

private bool UpdateHotbarCommands(AddonActionBarBase* ab)
{
if (ab == null) return false;
var hotbarModule = Framework.Instance()->GetUiModule()->GetRaptureHotbarModule();
var name = Marshal.PtrToStringUTF8(new IntPtr(ab->AtkUnitBase.Name));
if (name == null) return false;
var hotbar = hotbarModule->HotBarsSpan.GetPointer(ab->RaptureHotbarId);
if (hotbar == null) return false;

var numSlots = ab->SlotCount;

var changeFound = false;

if (recordedCommands[ab->RaptureHotbarId] == null || recordedCommands[ab->RaptureHotbarId].Length != numSlots)
{
recordedCommands[ab->RaptureHotbarId] = new HotbarSlotCommand[numSlots];
changeFound = true;
}

for (int i = 0; i < numSlots; i++)
{
var command = new HotbarSlotCommand();
var slotStruct = hotbar->SlotsSpan.GetPointer(i);
if (slotStruct != null)
{
command.Type = slotStruct->CommandType;
command.Id = slotStruct->CommandType == HotbarSlotType.Action ? actionManager->GetAdjustedActionId(slotStruct->CommandId) : slotStruct->CommandId;
}

if (!command.Equals(recordedCommands[ab->RaptureHotbarId][i]))
{
recordedCommands[ab->RaptureHotbarId][i] = command;
changeFound = true;
}
}

return changeFound;
}

private void FillControlHints()
{
Dictionary<HotbarSlotCommand, string> commandControlHints = new Dictionary<HotbarSlotCommand, string>(allActionBars.Length * actionBarLength);
for (int i = 0; i < Math.Min(recordedControlHints.Length, recordedCommands.Length); i++)
{
if (recordedControlHints[i] == null || recordedCommands[i] == null) continue;
for (int j = 0; j < Math.Min(recordedControlHints[i].Length, recordedCommands[i].Length); j++)
{
if (recordedCommands[i][j].Type != HotbarSlotType.Empty && recordedControlHints[i][j] != null && recordedControlHints[i][j].Length > 0)
{
commandControlHints.TryAdd(recordedCommands[i][j], recordedControlHints[i][j]);
}
}
}

foreach (var actionBar in allActionBars)
{
AddonActionBarBase* ab = GetActionBarAddon(actionBar);
if (ab != null && ab->ActionBarSlots != null)
{
FillHotbarControlHints(ab, commandControlHints);
}
}
}

private void FillHotbarControlHints(AddonActionBarBase* ab, Dictionary<HotbarSlotCommand, string> commandControlHints)
{
if (ab == null || ab->ActionBarSlots == null) return;
var hotbarModule = Framework.Instance()->GetUiModule()->GetRaptureHotbarModule();
var name = Marshal.PtrToStringUTF8(new IntPtr(ab->AtkUnitBase.Name));
if (name == null) return;
var hotbar = hotbarModule->HotBarsSpan.GetPointer(ab->RaptureHotbarId);
if (hotbar == null) return;

for (int i = 0; i < ab->SlotCount; i++)
{
if (recordedCommands?[ab->RaptureHotbarId]?[i] == null) continue;

var slot = ab->ActionBarSlots[i];
if (slot.ControlHintTextNode != null)
{
var controlHint = recordedControlHints?[ab->RaptureHotbarId]?[i];
if (controlHint == null || controlHint.Length == 0)
{
var command = recordedCommands[ab->RaptureHotbarId][i];
if (commandControlHints.ContainsKey(command))
{
controlHint = commandControlHints[command];

private void FrameworkUpdate() {
using var _ = PerformanceMonitor.Run();
foreach (var bar in actionBars) {
if (!Common.GetUnitBase<AddonActionBarX>(out var addon, bar)) continue;
var barId = addon->AddonActionBarBase.RaptureHotbarId;
for (byte slotIndex = 0; slotIndex < addon->AddonActionBarBase.SlotCount; slotIndex++) {
var slot = RaptureHotbarModule.Instance()->GetSlotById(barId, slotIndex);
if (slot->CommandType == HotbarSlotType.Empty) continue;
if (barSlotHasNoHint.TryGetValue((barId, slotIndex), out var noHint) && noHint) {
var commandId = slot->CommandType == HotbarSlotType.Action ? ActionManager.Instance()->GetAdjustedActionId(slot->CommandId) : slot->CommandId;
if (hints.TryGetValue((slot->CommandType, commandId), out var hint)) {
addon->AddonActionBarBase.Slot[slotIndex].ControlHintTextNode->SetText(hint);
barSlotIsSet[(barId, slotIndex)] = true;
} else if (barSlotIsSet.TryGetValue((barId, slotIndex), out var isSet) && isSet) {
addon->AddonActionBarBase.Slot[slotIndex].ControlHintTextNode->SetText(string.Empty);
barSlotIsSet[(barId, slotIndex)] = false;
}
}

slot.ControlHintTextNode->SetText(controlHint);
}
}
}

protected override void Disable()
{
actionBarBaseUpdateHook?.Disable();
Reset();
base.Disable();
private void UpdateAll() {
foreach(var addonName in actionBars) {
if (Common.GetUnitBase<AddonActionBarX>(out var addon, addonName)) ActionBarUpdateRequested(addon);
}
}

public override void Dispose()
{
actionBarBaseUpdateHook?.Dispose();
Reset();
base.Dispose();
}
protected override void Enable() => UpdateAll();
protected override void Disable() => UpdateAll();
}

0 comments on commit 1e5618f

Please sign in to comment.