Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Animation and Speed control #98

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 208 additions & 20 deletions Ktisis/Interface/Components/AnimationControls.cs
Original file line number Diff line number Diff line change
@@ -1,40 +1,228 @@
using System;
using System.Collections.Generic;
using System.Linq;

using Dalamud.Interface;

using ImGuiNET;

using Dalamud.Game.ClientState.Objects.Types;
using FFXIVClientStructs.Havok;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Character;

using Lumina.Excel.GeneratedSheets;

using Ktisis.Interop.Hooks;
using Ktisis.Structs.Actor;
using Ktisis.Util;

namespace Ktisis.Interface.Components {
public static class AnimationControls {
private static int InputBaseAction;
private static bool InputInterrupt = true;
private static int InputBlendAction;
private static string SearchTerm = string.Empty;


public static unsafe void Draw(GameObject? target) {
// Animation control
private static List<ActionTimeline>? BaseSearchSelector;
private static List<ActionTimeline>? BlendSearchSelector;
public unsafe static void Draw(Actor* actor) {
if (ImGui.CollapsingHeader("Animation Control")) {
var control = PoseHooks.GetAnimationControl(target);
if (PoseHooks.PosingEnabled || !Ktisis.IsInGPose || PoseHooks.IsGamePlaybackRunning(target) || control == null) {
if (PoseHooks.PosingEnabled) {
ImGui.Text("Animation Control is available when:");
ImGui.BulletText("Game animation is paused");
ImGui.BulletText("Posing is off");
} else
AnimationSeekAndSpeed(control);
ImGui.BulletText("Posing is disabled");
return;
}

Character* character = (Character*)actor;
DrawBaseSelect(character);
DrawBlendSelect(character);
DrawReset(character);
DrawSpeedControl(character);
}
}

private unsafe static void DrawBaseSelect(Character* character) {
ImGui.SetNextItemWidth(120f);
ImGui.InputInt("###input_base", ref InputBaseAction);
ImGui.SameLine();

if (GuiHelpers.IconButtonTooltip(FontAwesomeIcon.Search, "Search", hiddenLabel: "base_search"))
OpenBaseSearchSelector();

ImGui.SameLine();
ImGui.Checkbox("###interrupt", ref InputInterrupt);
ImGui.SameLine();
if (ImGui.Button("Base")) {
character->SetMode(Character.CharacterModes.AnimLock, 0);
SetBaseOverride(character, (ushort)InputBaseAction, InputInterrupt);
}

if (BaseSearchSelector != null)
DrawBaseSearchSelector();
}

private unsafe static void DrawBlendSelect(Character* character) {
// Blend Animation
ImGui.SetNextItemWidth(120f);
ImGui.InputInt("###input_blend", ref InputBlendAction);
ImGui.SameLine();

if (GuiHelpers.IconButtonTooltip(FontAwesomeIcon.Search, "Search", hiddenLabel: "blend_search"))
OpenBlendSearchSelector();

ImGui.SameLine();
if (ImGui.Button("Blend"))
character->ActionTimelineManager.Driver.PlayTimeline((ushort)InputBlendAction);

if (BlendSearchSelector != null)
DrawBlendSearchSelector();
}

private unsafe static void DrawReset(Character* character) {
bool isOverridden = character->Mode == Character.CharacterModes.AnimLock;
if (!isOverridden) ImGui.BeginDisabled();
if (ImGui.Button(("Reset"))) {
character->SetMode(Character.CharacterModes.Normal, 0);
SetBaseOverride(character, 0, true);
}
if (!isOverridden) ImGui.EndDisabled();
}

private unsafe static void DrawSpeedControl(Character* character) {
var originalMode = (int)ActorHooks.SpeedControlMode;
var refMode = originalMode;


ImGui.SetNextItemWidth(120f);
ImGui.Combo("Speed Control Mode", ref refMode, Enum.GetNames(typeof(ActorHooks.SpeedControlModes)).ToArray(), Enum.GetValues(typeof(ActorHooks.SpeedControlModes)).Length);

if (originalMode != refMode) {
ActorHooks.SpeedControlMode = (ActorHooks.SpeedControlModes)refMode;
SetSlotSpeed(character, ActionTimelineSlots.Base, 1f); // Flush the speed state
}

if (ActorHooks.SpeedControlMode == ActorHooks.SpeedControlModes.Global) {
ImGui.Text("All Slots (Global)");
ImGui.SetNextItemWidth(190f);
ImGui.SliderFloat("Speed###global_speed", ref character->ActionTimelineManager.OverallSpeed, 0f, 5f);
ImGui.Spacing();
}

var slots = Enum.GetValues<ActionTimelineSlots>();
foreach (var slot in slots) {
var slotName = Enum.GetName(slot);
var actionid = character->ActionTimelineManager.Driver.TimelineIds[(uint)slot];
if (actionid == 0)
continue;

ImGui.Text($"{(int)slot} ({slotName}): {actionid} - {Services.DataManager.GameData.GetExcelSheet<ActionTimeline>()!.GetRow(actionid)!.Key}");

if (ActorHooks.SpeedControlMode == ActorHooks.SpeedControlModes.Slot) {
var originalSpeed = character->ActionTimelineManager.Driver.TimelineSpeeds[(uint)slot];
var refSpeed = originalSpeed;

ImGui.SetNextItemWidth(190f);
ImGui.SliderFloat($"Speed###slot_{(uint)slot}_speed", ref refSpeed, 0f, 5f);

if (originalSpeed != refSpeed)
SetSlotSpeed(character, slot, refSpeed);
}
}

for (var skeletonIdx = 0; skeletonIdx < 2; ++skeletonIdx) {
var skeleton = character->GameObject.DrawObject->Skeleton->PartialSkeletons->GetHavokAnimatedSkeleton(skeletonIdx);
for (var animControlIdx = 0; animControlIdx < skeleton->AnimationControls.Length; ++animControlIdx) {
var animControl = character->GameObject.DrawObject->Skeleton->PartialSkeletons->GetHavokAnimatedSkeleton(skeletonIdx)->AnimationControls[animControlIdx].Value;
var duration = animControl->hkaAnimationControl.Binding.ptr->Animation.ptr->Duration;

var speedEnabled = !((Actor*)character)->IsMotionEnabled;
var scrubEnabled = animControl->PlaybackSpeed == 0f;

ImGui.Text($"Skeleton {skeletonIdx} Control {animControlIdx}");

if (!speedEnabled) ImGui.BeginDisabled();
ImGui.SetNextItemWidth(190f);
ImGui.SliderFloat($"Speed###slot_{skeletonIdx}_{animControlIdx}_pbspeed", ref animControl->PlaybackSpeed, 0f, 5f);
if (!speedEnabled) ImGui.EndDisabled();


if (!scrubEnabled) ImGui.BeginDisabled();
ImGui.SetNextItemWidth(190f);
ImGui.SliderFloat($"Scrub###slot_{skeletonIdx}_{animControlIdx}_scrub", ref animControl->hkaAnimationControl.LocalTime, 0f, duration);
if (!scrubEnabled) ImGui.EndDisabled();

if ((speedEnabled || scrubEnabled) && animControl->hkaAnimationControl.LocalTime >= duration - 0.05f)
animControl->hkaAnimationControl.LocalTime = 0.0f;
}
}

ImGui.Spacing();
}
public static unsafe void AnimationSeekAndSpeed(hkaDefaultAnimationControl* control) {
var duration = control->hkaAnimationControl.Binding.ptr->Animation.ptr->Duration;
var durationLimit = duration - 0.05f;

if (control->hkaAnimationControl.LocalTime >= durationLimit)
control->hkaAnimationControl.LocalTime = 0f;
private unsafe static void SetBaseOverride(Character* character, ushort actionId, bool interrupt) {
character->ActionTimelineManager.BaseOverride = actionId;
if (InputInterrupt)
character->ActionTimelineManager.Driver.PlayTimeline(actionId);
}

ImGui.PushItemWidth(ImGui.GetContentRegionAvail().X - GuiHelpers.WidthMargin() - GuiHelpers.GetRightOffset(ImGui.CalcTextSize("Speed").X));
ImGui.SliderFloat("Seek", ref control->hkaAnimationControl.LocalTime, 0, durationLimit);
ImGui.SliderFloat("Speed", ref control->PlaybackSpeed, 0f, 0.999f);
ImGui.PopItemWidth();
private unsafe static void SetSlotSpeed(Character* character, ActionTimelineSlots slot, float speed) {
ActionTimelineDriver* driver = &character->ActionTimelineManager.Driver;
ActorHooks.SetSlotSpeedHook.Original(new IntPtr(driver), (uint)slot, speed);
}

private static void OpenBaseSearchSelector() {
BaseSearchSelector = Services.DataManager.GameData.GetExcelSheet<ActionTimeline>()!
.Where(i => !string.IsNullOrEmpty(i.Key))
.Where(i => i.Slot == (int)ActionTimelineSlots.Base)
.ToList();
}
private static void CloseBaseSearchSelector() {
BaseSearchSelector = null;
}

private static void OpenBlendSearchSelector() {
BlendSearchSelector = Services.DataManager.GameData.GetExcelSheet<ActionTimeline>()!
.Where(i => !string.IsNullOrEmpty(i.Key))
.ToList();
}

private static void CloseBlendSearchSelector() {
BlendSearchSelector = null;
}

private static void DrawBaseSearchSelector() {
PopupSelect.HoverPopupWindow(
PopupSelect.HoverPopupWindowFlags.SelectorList | PopupSelect.HoverPopupWindowFlags.SearchBar,
BaseSearchSelector!,
(e, input) => e.Where(t => $"{t.RowId} - {t.Key}".Contains(input, StringComparison.OrdinalIgnoreCase)),
(t, a) => {
var selected = ImGui.Selectable($"{t.RowId} - {t.Key}###base_{t.RowId}", a);
var focus = ImGui.IsItemFocused();
return (selected, focus);
},
t => InputBaseAction = (ushort)t.RowId,
CloseBaseSearchSelector,
ref SearchTerm,
"Action Select",
"##base_action_select",
"##base_action_search");
}

private static void DrawBlendSearchSelector() {
PopupSelect.HoverPopupWindow(
PopupSelect.HoverPopupWindowFlags.SelectorList | PopupSelect.HoverPopupWindowFlags.SearchBar,
BlendSearchSelector!,
(e, input) => e.Where(t => $"{t.RowId} - {t.Key}".Contains(input, StringComparison.OrdinalIgnoreCase)),
(t, a) => {
var selected = ImGui.Selectable($"{t.RowId} - {t.Key}###blend_{t.RowId}", a);
var focus = ImGui.IsItemFocused();
return (selected, focus);
},
t => InputBlendAction = (ushort)t.RowId,
CloseBlendSearchSelector,
ref SearchTerm,
"Action Select",
"##blend_action_select",
"##blend_action_search");
}
}
}
2 changes: 1 addition & 1 deletion Ktisis/Interface/Windows/Toolbar/AdvancedWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public unsafe static void Draw() {

if (actor->Model != null) {
// Animation Controls
AnimationControls.Draw(target);
AnimationControls.Draw(actor);

// Gaze Controls
if (ImGui.CollapsingHeader("Gaze Control")) {
Expand Down
2 changes: 1 addition & 1 deletion Ktisis/Interface/Windows/Workspace/Workspace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ private unsafe static void ActorTab(GameObject target) {
ActorsList.Draw();

// Animation control
AnimationControls.Draw(target);
AnimationControls.Draw(actor);

// Gaze control
if (ImGui.CollapsingHeader("Gaze Control")) {
Expand Down
50 changes: 50 additions & 0 deletions Ktisis/Interop/Hooks/ActorHooks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@

using Dalamud.Hooking;

using FFXIVClientStructs.FFXIV.Client.Game;

using Ktisis.Events;
using Ktisis.Structs.Actor;
using Ktisis.Interface.Windows.Workspace;
using Ktisis.Structs.Actor.State;

namespace Ktisis.Interop.Hooks {
internal static class ActorHooks {
Expand All @@ -12,6 +16,14 @@ internal static class ActorHooks {

internal delegate IntPtr ControlGazeDelegate(IntPtr a1);
internal static Hook<ControlGazeDelegate> ControlGazeHook = null!;

internal delegate IntPtr EnforceGlobalSpeedDelegate(IntPtr a1);
internal static Hook<EnforceGlobalSpeedDelegate> EnforceGlobalSpeedHook = null!;

public delegate void SetSlotSpeedDelegate(IntPtr a1, uint slot, float speed);
internal static Hook<SetSlotSpeedDelegate> SetSlotSpeedHook = null!;

public static SpeedControlModes SpeedControlMode { get; set; }

internal unsafe static IntPtr ControlGaze(IntPtr a1) {
var actor = (Actor*)(a1 - 0xC30);
Expand All @@ -25,11 +37,49 @@ internal static void Init() {
var controlGaze = Services.SigScanner.ScanText("40 53 41 54 41 55 48 81 EC ?? ?? ?? ?? 48 8B D9");
ControlGazeHook = Hook<ControlGazeDelegate>.FromAddress(controlGaze, ControlGaze);
ControlGazeHook.Enable();

var globalSpeed = Services.SigScanner.ScanText("40 53 48 83 EC 30 48 8B D9 0F 29 74 24 20 48 8B 49 08 ?? ?? ?? ?? ?? 0F 28 F0 0F 57");
EnforceGlobalSpeedHook = Hook<EnforceGlobalSpeedDelegate>.FromAddress(globalSpeed, EnforceGlobalSpeedDetour);
EnforceGlobalSpeedHook.Enable();

SetSlotSpeedHook = Hook<SetSlotSpeedDelegate>.FromAddress((nint)ActionTimelineDriver.Addresses.SetSlotSpeed.Value, SetSlotSpeedDetour);
SetSlotSpeedHook.Enable();

EventManager.OnGPoseChange += OnGPoseChange;
}

internal static void OnGPoseChange(ActorGposeState _state) {
if (_state == ActorGposeState.OFF) {
SpeedControlMode = SpeedControlModes.Manual;
}
}

internal static IntPtr EnforceGlobalSpeedDetour(IntPtr a1) {
if (SpeedControlMode != SpeedControlModes.Global)
return EnforceGlobalSpeedHook.Original(a1);

return IntPtr.Zero;
}

internal static void SetSlotSpeedDetour(IntPtr a1, uint slot, float speed) {
if (SpeedControlMode != SpeedControlModes.Slot)
SetSlotSpeedHook.Original(a1, slot, speed);
}

internal static void Dispose() {
EventManager.OnGPoseChange -= OnGPoseChange;
ControlGazeHook.Disable();
ControlGazeHook.Dispose();
EnforceGlobalSpeedHook.Disable();
EnforceGlobalSpeedHook.Dispose();
SetSlotSpeedHook.Disable();
SetSlotSpeedHook.Dispose();
}

public enum SpeedControlModes : int {
Manual = 0,
Global = 1,
Slot = 2,
}
}
}
2 changes: 2 additions & 0 deletions Ktisis/Structs/Actor/Actor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public struct Actor {

[FieldOffset(0xC40)] public ActorGaze Gaze; // Update in ActorHooks.cs as well

[FieldOffset(0x1214)] public bool IsMotionEnabled;

public unsafe string? Name => Marshal.PtrToStringAnsi((IntPtr)GameObject.GetName());

public string GetNameOr(string fallback) => ((ObjectKind)GameObject.ObjectKind == ObjectKind.Pc && !Ktisis.Configuration.DisplayCharName) || string.IsNullOrEmpty(Name)? fallback : Name;
Expand Down