diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index ac2c028f1..0797ccb62 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -11,6 +11,7 @@ using Dalamud.Game.ClientState.Actors.Types; using Dalamud.Game.ClientState.Actors.Types.NonPlayer; using Dalamud.Game.Command; using Dalamud.Game.Internal; +using Dalamud.Game.Internal.Gui; using Dalamud.Game.Network; using Dalamud.Plugin; using Dalamud.Settings; @@ -39,6 +40,10 @@ namespace Dalamud { public readonly DalamudStartInfo StartInfo; + public readonly IconReplacer IconReplacer; + + public readonly IconReplaceChecker IconReplaceChecker; + public Dalamud(DalamudStartInfo info) { this.StartInfo = info; @@ -66,6 +71,10 @@ namespace Dalamud { this.PluginManager = new PluginManager(this, info.PluginDirectory, info.DefaultPluginDirectory); + this.IconReplaceChecker = new IconReplaceChecker(this.targetModule, this.sigScanner); + + this.IconReplacer = new IconReplacer(this, this.targetModule, this.sigScanner); + try { this.PluginManager.LoadPlugins(); } catch (Exception ex) { @@ -79,6 +88,10 @@ namespace Dalamud { Framework.Enable(); this.BotManager.Start(); + + this.IconReplaceChecker.Enable(); + + this.IconReplacer.Enable(); } public void Unload() { @@ -95,6 +108,10 @@ namespace Dalamud { this.BotManager.Dispose(); this.unloadSignal.Dispose(); + + this.IconReplaceChecker.Dispose(); + + this.IconReplacer.Dispose(); } private void SetupCommands() { diff --git a/Dalamud/Game/Internal/Gui/IconReplaceChecker.cs b/Dalamud/Game/Internal/Gui/IconReplaceChecker.cs new file mode 100644 index 000000000..48eb7f553 --- /dev/null +++ b/Dalamud/Game/Internal/Gui/IconReplaceChecker.cs @@ -0,0 +1,38 @@ +using System; +using System.Diagnostics; +using Dalamud.Hooking; + +namespace Dalamud.Game.Internal.Gui { + public class IconReplaceChecker { + private IconReplaceCheckerAddressResolver address; + private Hook checkerHook; + + public IconReplaceChecker(ProcessModule module, SigScanner scanner) { + this.address = new IconReplaceCheckerAddressResolver(); + this.address.Setup(scanner); + hookChecker(); + } + + private void hookChecker() { + this.checkerHook = new Hook(this.address.BaseAddress, (Delegate)new OnCheckDetour(this.HandleChecker), (object)this); + } + + public void Enable() { + this.checkerHook.Enable(); + } + + public void Dispose() { + this.checkerHook.Dispose(); + } + + // I hate this function. This is the dumbest function to exist in the game. Just return 1. + // Determines which abilities are allowed to have their icons updated. + private ulong HandleChecker(int actionID) { + return 1; + } + + private delegate ulong OnCheckDetour(int actionID); + + public delegate ulong OnCheckDelegate(int actionID); + } +} diff --git a/Dalamud/Game/Internal/Gui/IconReplaceCheckerAddressResolver.cs b/Dalamud/Game/Internal/Gui/IconReplaceCheckerAddressResolver.cs new file mode 100644 index 000000000..28ce04bc3 --- /dev/null +++ b/Dalamud/Game/Internal/Gui/IconReplaceCheckerAddressResolver.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dalamud.Game.Internal.Gui { + class IconReplaceCheckerAddressResolver : BaseAddressResolver { + public IntPtr BaseAddress { get; private set; } + protected bool IsResolved { get; set; } + + protected override void Setup64Bit(SigScanner sig) + { + this.BaseAddress = sig.ScanText("81 f9 d4 08 00 00 7f 33 0f 84 fa 01 00 00 83 c1 eb 81 f9 a3 00 00 00"); + } + } +} diff --git a/Dalamud/Game/Internal/Gui/IconReplacer.cs b/Dalamud/Game/Internal/Gui/IconReplacer.cs new file mode 100644 index 000000000..bd40830b1 --- /dev/null +++ b/Dalamud/Game/Internal/Gui/IconReplacer.cs @@ -0,0 +1,461 @@ +using Dalamud.Game.ClientState.Actors.Types; +using Dalamud.Hooking; +using Serilog; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace Dalamud.Game.Internal.Gui { + public class IconReplacer { + private IconReplacerAddressResolver address; + private Hook iconHook; + private IntPtr comboTimer; + private IntPtr lastComboMove; + private IntPtr activeBuffArray = IntPtr.Zero; + private IntPtr jobInfo; + private IntPtr byteBase; + private Dalamud dalamud; + private PlayerCharacter localCharacter = null; + + public unsafe IconReplacer(Dalamud dalamud, ProcessModule module, SigScanner scanner) { + this.dalamud = dalamud; + this.address = new IconReplacerAddressResolver(); + this.address.Setup(scanner); + + this.byteBase = scanner.Module.BaseAddress; + this.jobInfo = byteBase + 0x1b2d4b4; + this.comboTimer = byteBase + 0x1AE1B10; + this.lastComboMove = byteBase + 0x1AE1B14; + + this.iconHook = new Hook(this.address.BaseAddress, (Delegate)new OnIconDetour(this.HandleIconUpdate), (object)this); + + } + + public void Enable() { + this.iconHook.Enable(); + } + + public void Dispose() { + this.iconHook.Dispose(); + } + + + /// + /// Replace an ability with another ability + /// actionID is the original ability to be "used" + /// Return either actionID (itself) or a new Action table ID as the + /// ability to take its place. + /// I tend to make the "combo chain" button be the last move in the combo + /// For example, Souleater combo on DRK happens by dragging Souleater + /// onto your bar and mashing it. + /// + private unsafe ulong HandleIconUpdate(byte self, uint actionID) { + + // TODO: BRD, RDM, level checking for everything. + + // Check if player is loaded in by trying to get their buffs. + // If not, skip everything until we are (game will crash cause I'm lazy). + if (activeBuffArray == IntPtr.Zero) { + try { + activeBuffArray = FindBuffAddress(); + localCharacter = dalamud.ClientState.LocalPlayer; + } + catch (Exception e) { + activeBuffArray = IntPtr.Zero; + return this.iconHook.Original(self, actionID); + } + } + + // Don't clutter the spaghetti any worse than it already is. + int lastMove = Marshal.ReadInt32(lastComboMove); + float comboTime = (float)Marshal.ReadInt32(comboTimer); + byte level = localCharacter.Level; + + // DRAGOON + // TODO: Jump/High Jump into Mirage Dive + + // Replace Coerthan Torment with Coerthan Torment combo chain + if (actionID == 16477) { + if (comboTime > 0) { + if (Marshal.ReadInt32(lastComboMove) == 86) return 7397; + if (Marshal.ReadInt32(lastComboMove) == 7397) return 16477; + } + return 86; + } + + // Replace Chaos Thrust with the Chaos Thrust combo chain + if (actionID == 88) { + if (comboTime > 0) { + if (lastMove == 75 || lastMove == 16479) return 87; + if (lastMove == 87) return 88; + + } + if (activeBuffArray != IntPtr.Zero) { + if (SearchBuffArray(802)) return 3554; + if (SearchBuffArray(803)) return 3556; + if (SearchBuffArray(1863)) return 16479; + } + return 75; + } + + // Replace Full Thrust with the Full Thrust combo chain + if (actionID == 84) { + if (comboTime > 0) { + if (lastMove == 75 || lastMove == 16479) return 78; + if (lastMove == 78) return 84; + + } + if (activeBuffArray != IntPtr.Zero) { + if (SearchBuffArray(802)) return 3554; + if (SearchBuffArray(803)) return 3556; + if (SearchBuffArray(1863)) return 16479; + } + return 75; + } + + // DARK KNIGHT + + // Replace Souleater with Souleater combo chain + if (actionID == 3632) { + if (comboTime > 0) { + if (lastMove == 3617) return 3623; + if (lastMove == 3623) return 3632; + } + return 3617; + } + + // Replace Stalwart Soul with Stalwart Soul combo chain + if (actionID == 16468) { + if (comboTime > 0) { + if (lastMove == 3621) return 16468; + } + return 3621; + } + + // PALADIN + + // Replace Goring Blade with Goring Blade combo + if (actionID == 3538) { + if (comboTime > 0) { + if (lastMove == 9) return 15; + if (lastMove == 15) return 3538; + } + return 9; + } + + // Replace Royal Authority with Royal Authority combo + if (actionID == 3539) { + if (comboTime > 0) { + if (lastMove == 9) return 15; + if (lastMove == 15) return 3539; + } + return 9; + } + + // Replace Prominence with Prominence combo + if (actionID == 16457) { + if (comboTime > 0) { + if (lastMove == 7381) return 16457; + } + return 7381; + } + + // WARRIOR + + // Replace Storm's Path with Storm's Path combo + if (actionID == 42) { + if (comboTime > 0) { + if (lastMove == 31) return 37; + if (lastMove == 37) return 42; + } + return 31; + } + + // Replace Storm's Eye with Storm's Eye combo + if (actionID == 45) { + if (comboTime > 0) { + if (lastMove == 31) return 37; + if (lastMove == 37) return 45; + } + return 31; + } + + // Replace Mythril Tempest with Mythril Tempest combo + if (actionID == 16462) { + if (comboTime > 0) { + if (lastMove == 41) return 16462; + } + return 41; + } + + // SAMURAI + + // Replace Yukikaze with Yukikaze combo + if (actionID == 7480) { + if (activeBuffArray != IntPtr.Zero) { + if (SearchBuffArray(1233)) return 7480; + } + if (comboTime > 0) { + if (lastMove == 7477) return 7480; + } + return 7477; + } + + // Replace Gekko with Gekko combo + if (actionID == 7481) { + if (activeBuffArray != IntPtr.Zero) { + if (SearchBuffArray(1233)) return 7481; + } + if (comboTime > 0) { + if (lastMove == 7477) return 7478; + if (lastMove == 7478) return 7481; + } + return 7477; + } + + // Replace Kasha with Kasha combo + if (actionID == 7482) { + if (activeBuffArray != null) { + if (SearchBuffArray(1233)) return 7482; + } + if (comboTime > 0) { + if (lastMove == 7477) return 7479; + if (lastMove == 7479) return 7482; + } + return 7477; + } + + // Replace Mangetsu with Mangetsu combo + if (actionID == 7484) { + if (comboTime > 0) { + if (lastMove == 7483) return 7484; + } + return 7483; + } + + // Replace Yukikaze with Yukikaze combo + if (actionID == 7485) { + if (comboTime > 0) { + if (lastMove == 7483) return 7485; + } + return 7483; + } + + // NINJA + + // Replace Shadow Fang with Shadow Fang combo + if (actionID == 2257) { + if (comboTime > 0) { + if (lastMove == 2240) return 2257; + } + return 2240; + } + + // Replace Armor Crush with Armor Crush combo + if (actionID == 2257) { + if (comboTime > 0) { + if (lastMove == 2240) return 2242; + if (lastMove == 2242) return 3563; + } + return 2240; + } + + // Replace Aeolian Edge with Aeolian Edge combo + if (actionID == 2257) { + if (comboTime > 0) { + if (lastMove == 2240) return 2242; + if (lastMove == 2242) return 2255; + } + return 2240; + } + + // Replace Hakke Mujinsatsu with Hakke Mujinsatsu combo + if (actionID == 16488) { + if (comboTime > 0) { + if (lastMove == 2254) return 16488; + } + return 2254; + } + + // GUNBREAKER + + // Replace Solid Barrel with Solid Barrel combo + if (actionID == 16145) { + if (comboTime > 0) { + if (lastMove == 16137) return 16139; + if (lastMove == 16139) return 16145; + } + return 16137; + } + + // Replace Gnashing Fang with Gnashing Fang combo + // TODO: Potentially add Contuation moves as well? + if (actionID == 16146) { + byte ammoComboState = Marshal.ReadByte(jobInfo, 0x10); + if (ammoComboState == 1) return 16147; + if (ammoComboState == 2) return 16150; + return 16146; + } + + // Replace Demon Slaughter with Demon Slaughter combo + if (actionID == 16149) { + if (comboTime > 0) { + if (lastMove == 16141) return 16149; + } + return 16141; + } + + // MACHINIST + + // Replace Heated Clean Shot with Heated Clean Shot combo + // Or with Heat Blast when overheated. + // For some reason the shots use their unheated IDs as combo moves + if (actionID == 7413) { + if (Marshal.ReadInt16(jobInfo, 0xc) > 0) return 7410; + if (comboTime > 0) { + if (lastMove == 2866) return 7412; + if (lastMove == 2868) return 7413; + } + return 7411; + } + + // Replace Spread Shot with Auto Crossbow when overheated. + if (actionID == 2870) { + if (Marshal.ReadInt16(jobInfo, 0xc) > 0) return 16497; + return 2870; + } + + // BLACK MAGE + + // Enochian changes to B4 or F4 depending on stance. + if (actionID == 3575) { + if (Marshal.ReadByte(jobInfo, 0x13) == 1) { + if (Marshal.ReadByte(jobInfo, 0x10) > 3) return 3576; + if (Marshal.ReadByte(jobInfo, 0x10) > 0) return 3577; + } + return 3575; + } + + // Umbral Soul and Transpose + if (actionID == 16506) { + if (Marshal.ReadByte(jobInfo, 0x10) > 3) return 16506; + return 149; + } + + // ASTROLOGIAN + + // Make cards on the same button as draw + if (actionID == 17055) { + byte x = Marshal.ReadByte(jobInfo, 0x10); + switch (x) { + case 1: + return 4401; + case 2: + return 4404; + case 3: + return 4402; + case 4: + return 4403; + case 5: + return 4405; + case 6: + return 4406; + case 0x70: + return 7444; + case 0x80: + return 7445; + default: + return 3590; + } + } + + // SUMMONER + + // DWT changes. + // Now contains DWT, Deathflare, Summon Bahamut, Enkindle Bahamut, FBT, and Enkindle Phoenix. + // What a monster of a button. + if (actionID == 3581) { + byte stackState = Marshal.ReadByte(jobInfo, 0x10); + if (Marshal.ReadInt16(jobInfo, 0xc) > 0) { + if (Marshal.ReadInt16(jobInfo, 0xe) > 0) { + if (stackState > 0) return 16516; + return 7429; + } + return 3582; + } + else { + if (stackState == 0) return 3581; + if (stackState == 8) return 7427; + if (stackState == 0x10) return 16513; + return 3581; + } + } + + // SCHOLAR + + // Change Fey Blessing into Consolation when Seraph is out. + if (actionID == 16543) { + if (Marshal.ReadInt16(jobInfo, 0x10) > 0) return 16546; + return 16543; + } + + // DANCER + // TODO: Single-target. This needs to be done alongside 1-button dances. + + // Handle AoE GCDs on one button. Procs take priority over combo. + + if (actionID == 15994) { + if (activeBuffArray != null) { + if (SearchBuffArray(1816)) return 15995; + if (SearchBuffArray(1817)) return 15996; + } + if (comboTime > 0) { + if (lastMove == 15993) return 15994; + } + return 15993; + } + + // Fan Dance changes into Fan Dance 3 while flourishing. + if (actionID == 16007) { + if (activeBuffArray != null) { + if (SearchBuffArray(1820)) return 16009; + } + + return 16007; + } + + // Fan Dance 2 changes into Fan Dance 3 while flourishing. + if (actionID == 16008) { + if (activeBuffArray != null) { + if (SearchBuffArray(1820)) return 16009; + } + return 16008; + } + + + return this.iconHook.Original(self, actionID); + } + + private unsafe bool SearchBuffArray(short needle) { + for (int i = 0; i < 60; i++) { + if (Marshal.ReadInt16(activeBuffArray + 4 * i) == needle) return true; + } + return false; + } + + private delegate ulong OnIconDetour(byte param1, uint param2); + + public delegate ulong OnIconDelegate(byte param1, uint param2); + + private unsafe delegate int* getArray(long* address); + + private unsafe IntPtr FindBuffAddress() { + IntPtr randomAddress = byteBase + 0x1b2c970; + IntPtr num = Marshal.ReadIntPtr(randomAddress); + IntPtr step2 = (IntPtr)(Marshal.ReadInt64(num) + 0x248); + IntPtr step3 = Marshal.ReadIntPtr(step2); + var callback = Marshal.GetDelegateForFunctionPointer(step3); + return (IntPtr)callback((long*)num); + } + } +} diff --git a/Dalamud/Game/Internal/Gui/IconReplacerAddressResolver.cs b/Dalamud/Game/Internal/Gui/IconReplacerAddressResolver.cs new file mode 100644 index 000000000..e8c37533d --- /dev/null +++ b/Dalamud/Game/Internal/Gui/IconReplacerAddressResolver.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Dalamud.Game.Internal.Gui { + class IconReplacerAddressResolver : BaseAddressResolver { + public IntPtr BaseAddress { get; private set; } + protected bool IsResolved { get; set; } + + protected override void Setup64Bit(SigScanner sig) { + this.BaseAddress = sig.ScanText("81 fa d4 08 00 00 7f 4b 74 44 8d 42 eb 3d a3 00 00 00"); + } + } +}