Skip to content

Commit

Permalink
Merge pull request #384 from xanunderscore/move
Browse files Browse the repository at this point in the history
use navmesh movement code
  • Loading branch information
awgil authored Jul 16, 2024
2 parents 73a8322 + 87dbf50 commit 9b5832a
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 93 deletions.
110 changes: 18 additions & 92 deletions BossMod/AI/AIController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.ClientState.Keys;

namespace BossMod.AI;

Expand All @@ -8,60 +7,6 @@ namespace BossMod.AI;
// - using actions safely (without spamming, not in cutscenes, etc)
sealed class AIController(ActionManagerEx amex)
{
private sealed class NaviAxis(InputOverride io, VirtualKey keyFwd, VirtualKey keyBack)
{
private int _curDirection;

public int CurDirection
{
get => _curDirection;
set
{
if (_curDirection == value)
{
if (value != 0 && !Service.KeyState[value > 0 ? keyFwd : keyBack])
io.SimulatePress(value > 0 ? keyFwd : keyBack);
return;
}

if (_curDirection > 0)
io.SimulateRelease(keyFwd);
else if (_curDirection < 0)
io.SimulateRelease(keyBack);
_curDirection = value;
if (value > 0)
io.SimulatePress(keyFwd);
else if (value < 0)
io.SimulatePress(keyBack);
}
}
}

private sealed class NaviInput(InputOverride io, VirtualKey key)
{
private bool _held;

public bool Held
{
get => _held;
set
{
if (_held == value)
{
if (value && !Service.KeyState[key])
io.SimulatePress(key);
return;
}

if (_held)
io.SimulateRelease(key);
_held = value;
if (value)
io.SimulatePress(key);
}
}
}

public WPos? NaviTargetPos;
public WDir? NaviTargetRot;
public float? NaviTargetVertical;
Expand All @@ -71,11 +16,7 @@ public bool Held
public bool WantJump;

private readonly ActionManagerEx _amex = amex;
private readonly NaviAxis _axisForward = new(amex.InputOverride, VirtualKey.W, VirtualKey.S);
private readonly NaviAxis _axisStrafe = new(amex.InputOverride, VirtualKey.D, VirtualKey.A);
private readonly NaviAxis _axisRotate = new(amex.InputOverride, VirtualKey.LEFT, VirtualKey.RIGHT);
private readonly NaviAxis _axisVertical = new(amex.InputOverride, VirtualKey.UP, VirtualKey.DOWN);
private readonly NaviInput _keyJump = new(amex.InputOverride, VirtualKey.SPACE);
private DateTime _nextJump;

public bool InCutscene => Service.Condition[ConditionFlag.OccupiedInCutSceneEvent] || Service.Condition[ConditionFlag.WatchingCutscene78] || Service.Condition[ConditionFlag.Occupied33] || Service.Condition[ConditionFlag.BetweenAreas] || Service.Condition[ConditionFlag.OccupiedInQuestEvent];
public bool IsMounted => Service.Condition[ConditionFlag.Mounted];
Expand All @@ -102,13 +43,11 @@ public void SetFocusTarget(Actor? actor)

public void Update(Actor? player)
{
var movement = AIMove.Instance!;

if (player == null || player.IsDead || InCutscene)
{
_axisForward.CurDirection = 0;
_axisStrafe.CurDirection = 0;
_axisRotate.CurDirection = 0;
_axisVertical.CurDirection = 0;
_keyJump.Held = false;
movement.DesiredPosition = null;
_amex.InputOverride.GamepadOverridesEnabled = false;
return;
}
Expand All @@ -118,47 +57,34 @@ public void Update(Actor? player)
_amex.FaceDirection(NaviTargetRot.Value);
}

// TODO this checks whether movement keys are pressed, we need a better solution
bool moveRequested = _amex.InputOverride.IsMoveRequested();
var cameraFacing = CameraFacing;
var cameraFacingDir = cameraFacing.ToDirection();
if (!moveRequested && NaviTargetRot != null && NaviTargetRot.Value.Dot(cameraFacingDir) < 0.996f) // ~5 degrees
{
_axisRotate.CurDirection = cameraFacingDir.OrthoL().Dot(NaviTargetRot.Value) > 0 ? 1 : -1;
}
else
{
_axisRotate.CurDirection = 0;
}

bool castInProgress = player.CastInfo != null && !player.CastInfo.EventHappened;
bool forbidMovement = moveRequested || !AllowInterruptingCastByMovement && _amex.MoveMightInterruptCast;
if (NaviTargetPos != null && !forbidMovement && (NaviTargetPos.Value - player.Position).LengthSq() > 0.01f)
{
var dir = cameraFacing - Angle.FromDirection(NaviTargetPos.Value - player.Position);
_amex.InputOverride.GamepadOverridesEnabled = true;
_amex.InputOverride.GamepadOverrides[3] = (int)(100 * dir.Sin());
_amex.InputOverride.GamepadOverrides[4] = (int)(100 * dir.Cos());
_axisForward.CurDirection = _amex.InputOverride.GamepadOverrides[4] > 10 ? 1 : _amex.InputOverride.GamepadOverrides[4] < 10 ? -1 : 0; // this is a hack, needed to prevent afk :( this will be ignored anyway due to gamepad inputs
_keyJump.Held = !_keyJump.Held && WantJump;
movement.DesiredPosition = NaviTargetPos;
if (WantJump)
ExecuteJump();
}
else
{
_amex.InputOverride.GamepadOverridesEnabled = false;
_axisForward.CurDirection = 0;
_keyJump.Held = false;
movement.DesiredPosition = null;
_amex.ForceCancelCastNextFrame |= ForceCancelCast && castInProgress;
}

if (NaviTargetVertical != null && IsVerticalAllowed && NaviTargetPos != null)
{
var deltaY = NaviTargetVertical.Value - player.PosRot.Y;
var deltaXZ = (NaviTargetPos.Value - player.Position).Length();
var deltaAltitude = (CameraAltitude - Angle.FromDirection(new(_amex.InputOverride.GamepadOverrides[4] < 0 ? deltaY : -deltaY, deltaXZ))).Deg;
_amex.InputOverride.GamepadOverrides[6] = Math.Clamp((int)(deltaAltitude * 5), -100, 100);
}
movement.DesiredY = NaviTargetVertical;
else
movement.DesiredY = null;
}

private unsafe void ExecuteJump()
{
if (DateTime.Now >= _nextJump)
{
_amex.InputOverride.GamepadOverrides[6] = 0;
FFXIVClientStructs.FFXIV.Client.Game.ActionManager.Instance()->UseAction(FFXIVClientStructs.FFXIV.Client.Game.ActionType.GeneralAction, 2);
_nextJump = DateTime.Now.AddMilliseconds(100);
}
}
}
126 changes: 126 additions & 0 deletions BossMod/AI/AIMove.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using Dalamud.Game.Config;
using Dalamud.Hooking;
using Dalamud.Utility.Signatures;
using System.Runtime.InteropServices;

namespace BossMod.AI;

[StructLayout(LayoutKind.Explicit, Size = 0x18)]
public unsafe struct PlayerMoveControllerFlyInput
{
[FieldOffset(0x0)] public float Forward;
[FieldOffset(0x4)] public float Left;
[FieldOffset(0x8)] public float Up;
[FieldOffset(0xC)] public float Turn;
[FieldOffset(0x10)] public float u10;
[FieldOffset(0x14)] public byte DirMode;
[FieldOffset(0x15)] public byte HaveBackwardOrStrafe;
}

public sealed unsafe class AIMove : IDisposable
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2211:Non-constant fields should not be visible", Justification = "Camera already does this")]
public static AIMove? Instance;

public WPos? DesiredPosition;
public float? DesiredY;
public float Precision = 0.01f;

private bool _legacyMode;

private delegate bool RMIWalkIsInputEnabled(void* self);
private readonly RMIWalkIsInputEnabled _rmiWalkIsInputEnabled1;
private readonly RMIWalkIsInputEnabled _rmiWalkIsInputEnabled2;

private delegate void RMIWalkDelegate(void* self, float* sumLeft, float* sumForward, float* sumTurnLeft, byte* haveBackwardOrStrafe, byte* a6, byte bAdditiveUnk);
[Signature("E8 ?? ?? ?? ?? 80 7B 3E 00 48 8D 3D")]
private readonly Hook<RMIWalkDelegate> _rmiWalkHook = null!;

private delegate void RMIFlyDelegate(void* self, PlayerMoveControllerFlyInput* result);
[Signature("E8 ?? ?? ?? ?? 0F B6 0D ?? ?? ?? ?? B8")]
private readonly Hook<RMIFlyDelegate> _rmiFlyHook = null!;

public AIMove()
{
var rmiWalkIsInputEnabled1Addr = Service.SigScanner.ScanText("E8 ?? ?? ?? ?? 84 C0 75 10 38 43 3C");
var rmiWalkIsInputEnabled2Addr = Service.SigScanner.ScanText("E8 ?? ?? ?? ?? 84 C0 75 03 88 47 3F");
Service.Log($"RMIWalkIsInputEnabled1 address: 0x{rmiWalkIsInputEnabled1Addr:X}");
Service.Log($"RMIWalkIsInputEnabled2 address: 0x{rmiWalkIsInputEnabled2Addr:X}");
_rmiWalkIsInputEnabled1 = Marshal.GetDelegateForFunctionPointer<RMIWalkIsInputEnabled>(rmiWalkIsInputEnabled1Addr);
_rmiWalkIsInputEnabled2 = Marshal.GetDelegateForFunctionPointer<RMIWalkIsInputEnabled>(rmiWalkIsInputEnabled2Addr);

Service.Hook.InitializeFromAttributes(this);
Service.Log($"RMIWalk address: 0x{_rmiWalkHook.Address:X}");
Service.GameConfig.UiControlChanged += OnConfigChanged;
UpdateLegacyMode();

_rmiWalkHook.Enable();
_rmiFlyHook.Enable();
}

public void Dispose()
{
Service.GameConfig.UiControlChanged -= OnConfigChanged;
_rmiWalkHook.Dispose();
_rmiFlyHook.Dispose();
}

private void RMIWalkDetour(void* self, float* sumLeft, float* sumForward, float* sumTurnLeft, byte* haveBackwardOrStrafe, byte* a6, byte bAdditiveUnk)
{
_rmiWalkHook.Original(self, sumLeft, sumForward, sumTurnLeft, haveBackwardOrStrafe, a6, bAdditiveUnk);
// TODO: we really need to introduce some extra checks that PlayerMoveController::readInput does - sometimes it skips reading input, and returning something non-zero breaks stuff...
bool movementAllowed = bAdditiveUnk == 0 && _rmiWalkIsInputEnabled1(self) && _rmiWalkIsInputEnabled2(self); //&& !Service.Condition[Dalamud.Game.ClientState.Conditions.ConditionFlag.BeingMoved];
if (movementAllowed && (*sumLeft == 0 && *sumForward == 0) && DirectionToDestination(false) is var relDir && relDir != null)
{
var dir = relDir.Value.h.ToDirection();
*sumLeft = dir.X;
*sumForward = dir.Z;
}
}

private void RMIFlyDetour(void* self, PlayerMoveControllerFlyInput* result)
{
_rmiFlyHook.Original(self, result);
// TODO: we really need to introduce some extra checks that PlayerMoveController::readInput does - sometimes it skips reading input, and returning something non-zero breaks stuff...
if (result->Forward == 0 && result->Left == 0 && result->Up == 0 && DirectionToDestination(true) is var relDir && relDir != null)
{
var dir = relDir.Value.h.ToDirection();
result->Forward = dir.Z;
result->Left = dir.X;
result->Up = relDir.Value.v.Rad;
}
}

private (Angle h, Angle v)? DirectionToDestination(bool allowVertical)
{
if (DesiredPosition == null)
return null;

var player = Service.ClientState.LocalPlayer;
if (player == null)
return null;

var playerXZ = new WPos(player.Position.X, player.Position.Z);

var distV3 = DesiredPosition.Value - playerXZ;
if (distV3.Length() <= Precision)
return null;

var dist = new WDir(distV3.X, distV3.Z);

var dirH = Angle.FromDirection(dist);
var dirV = allowVertical && DesiredY != null ? Angle.FromDirection(new(DesiredY.Value, dist.Length())) : default;

var refDir = _legacyMode
? Camera.Instance!.CameraAzimuth.Radians() + 180.Degrees()
: player.Rotation.Radians();
return (dirH - refDir, dirV);
}

private void OnConfigChanged(object? sender, ConfigChangeEvent evt) => UpdateLegacyMode();
private void UpdateLegacyMode()
{
_legacyMode = Service.GameConfig.UiControl.TryGetUInt("MoveMode", out var mode) && mode == 1;
Service.Log($"Legacy mode is now {(_legacyMode ? "enabled" : "disabled")}");
}
}
4 changes: 3 additions & 1 deletion BossMod/Framework/Plugin.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using BossMod.Autorotation;
using BossMod.AI;
using BossMod.Autorotation;
using Dalamud.Common;
using Dalamud.Game;
using Dalamud.Game.ClientState.Conditions;
Expand Down Expand Up @@ -58,6 +59,7 @@ public unsafe Plugin(IDalamudPluginInterface dalamud, ICommandManager commandMan
MultiboxUnlock.Exec();
Network.IDScramble.Initialize();
Camera.Instance = new();
AIMove.Instance = new();

Service.Config.Initialize();
Service.Config.LoadFromFile(dalamud.ConfigFile);
Expand Down
1 change: 1 addition & 0 deletions BossMod/Framework/Service.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public sealed class Service
[PluginService] public static IPluginLog Logger { get; private set; }
[PluginService] public static IChatGui ChatGui { get; private set; }
[PluginService] public static IGameGui GameGui { get; private set; }
[PluginService] public static IGameConfig GameConfig { get; private set; }
[PluginService] public static IGameInteropProvider Hook { get; private set; }
[PluginService] public static ISigScanner SigScanner { get; private set; }
[PluginService] public static ICondition Condition { get; private set; }
Expand Down

0 comments on commit 9b5832a

Please sign in to comment.