Initial commit: AetherBags + KamiToolKit for FC Gitea
Debug Build and Test / Build against Latest Dalamud (push) Has been cancelled
Debug Build and Test / Build against Staging Dalamud (push) Has been cancelled

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-08 14:46:31 -05:00
commit 8db4ce6094
375 changed files with 34124 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Timelines;
public class FrameSetBuilder(TimelineBuilder parent, int startFrameId, int endFrameId) {
private readonly List<TimelineKeyFrame> animationKeyFrames = [];
private readonly List<TimelineKeyFrame> labelKeyFrames = [];
public FrameSetBuilder AddFrame(params TimelineKeyFrame[] keyFrame) {
foreach (var frame in keyFrame) {
switch (frame.GroupType) {
case AtkTimelineKeyGroupType.Label:
labelKeyFrames.Add(frame);
break;
case AtkTimelineKeyGroupType.Float2:
case AtkTimelineKeyGroupType.Float:
case AtkTimelineKeyGroupType.Byte:
case AtkTimelineKeyGroupType.NodeTint:
case AtkTimelineKeyGroupType.UShort:
case AtkTimelineKeyGroupType.RGB:
case AtkTimelineKeyGroupType.Short:
case AtkTimelineKeyGroupType.None:
default:
animationKeyFrames.Add(frame);
break;
}
}
return this;
}
public FrameSetBuilder AddEmptyFrame(int frameId) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frameId, GroupType = AtkTimelineKeyGroupType.None,
});
return this;
}
public FrameSetBuilder AddFrame(
int frameId, Vector2? position = null, byte? alpha = null, Vector3? addColor = null, Vector3? multiplyColor = null,
float? rotation = null, Vector2? scale = null, Vector3? textColor = null, Vector3? textOutlineColor = null, uint? partId = null, AtkTimelineInterpolation? interpolation = null,
float? rotationDegrees = null) {
if (position is not null) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frameId, Position = position.Value, Interpolation = interpolation ?? AtkTimelineInterpolation.Linear,
});
}
if (alpha is not null) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frameId, Alpha = alpha.Value, Interpolation = interpolation ?? AtkTimelineInterpolation.Linear,
});
}
if (addColor is not null || multiplyColor is not null) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frameId, AddColor = addColor ?? new Vector3(0.0f, 0.0f, 0.0f), MultiplyColor = multiplyColor ?? new Vector3(100.0f, 100.0f, 100.0f), Interpolation = interpolation ?? AtkTimelineInterpolation.Linear,
});
}
if (rotation is not null) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frameId, Rotation = rotation.Value, Interpolation = interpolation ?? AtkTimelineInterpolation.Linear,
});
}
if (rotationDegrees is not null) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frameId, Rotation = rotationDegrees.Value * MathF.PI / 180.0f, Interpolation = interpolation ?? AtkTimelineInterpolation.Linear,
});
}
if (scale is not null) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frameId, Scale = scale.Value, Interpolation = interpolation ?? AtkTimelineInterpolation.Linear,
});
}
if (textColor is not null) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frameId, TextColor = textColor.Value, Interpolation = interpolation ?? AtkTimelineInterpolation.Linear,
});
}
if (textOutlineColor is not null) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frameId, TextEdgeColor = textOutlineColor.Value, Interpolation = interpolation ?? AtkTimelineInterpolation.Linear,
});
}
if (partId is not null) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frameId, PartId = partId.Value, Interpolation = interpolation ?? AtkTimelineInterpolation.Linear,
});
}
return this;
}
public FrameSetBuilder AddLabel(int frameId, int labelId, AtkTimelineJumpBehavior jumpBehavior, int labelTarget) {
labelKeyFrames.Add(new TimelineLabelSetKeyFrame {
FrameIndex = frameId,
GroupType = AtkTimelineKeyGroupType.Label,
JumpBehavior = jumpBehavior,
LabelId = labelId,
JumpLabelId = labelTarget,
});
return this;
}
public FrameSetBuilder AddLabelPair(int frameStart, int frameStop, int labelId) {
labelKeyFrames.Add(new TimelineLabelSetKeyFrame {
FrameIndex = frameStart,
GroupType = AtkTimelineKeyGroupType.Label,
JumpBehavior = AtkTimelineJumpBehavior.Start,
LabelId = labelId,
});
labelKeyFrames.Add(new TimelineLabelSetKeyFrame {
FrameIndex = frameStop,
GroupType = AtkTimelineKeyGroupType.Label,
JumpBehavior = AtkTimelineJumpBehavior.PlayOnce,
LabelId = 0,
JumpLabelId = 0,
});
return this;
}
public KeyFrameBuilder BeginFrameBuilder(int frame)
=> new(this, frame);
public TimelineBuilder EndFrameSet() {
if (labelKeyFrames.Count != 0) {
parent.LabelSets.Add(new TimelineLabelSet {
StartFrameId = startFrameId, EndFrameId = endFrameId, Labels = labelKeyFrames,
});
}
if (animationKeyFrames.Count != 0) {
parent.Animations.Add(new TimelineAnimation {
StartFrameId = startFrameId, EndFrameId = endFrameId, KeyFrames = animationKeyFrames,
});
}
return parent;
}
}
+102
View File
@@ -0,0 +1,102 @@
using System.Collections.Generic;
using FFXIVClientStructs.FFXIV.Common.Math;
namespace KamiToolKit.Timelines;
public class KeyFrameBuilder(FrameSetBuilder parent, int frame) {
private readonly List<TimelineKeyFrame> animationKeyFrames = [];
public KeyFrameBuilder Position(Vector2 position) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frame, Position = position,
});
return this;
}
public KeyFrameBuilder Alpha(byte alpha) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frame, Alpha = alpha,
});
return this;
}
public KeyFrameBuilder AddColor(Vector3 color) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frame, AddColor = color,
});
return this;
}
public KeyFrameBuilder MultiplyColor(Vector3 color) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frame, MultiplyColor = color,
});
return this;
}
public KeyFrameBuilder MultiplyColor(float color) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frame, MultiplyColor = new Vector3(color, color, color),
});
return this;
}
public KeyFrameBuilder Rotation(float rotation) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frame, Rotation = rotation,
});
return this;
}
public KeyFrameBuilder Scale(Vector2 scale) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frame, Scale = scale,
});
return this;
}
public KeyFrameBuilder Scale(float scale) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frame, Scale = new Vector2(scale, scale),
});
return this;
}
public KeyFrameBuilder TextColor(Vector3 textColor) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frame, TextColor = textColor,
});
return this;
}
public KeyFrameBuilder TextOutlineColor(Vector3 textColor) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frame, TextEdgeColor = textColor,
});
return this;
}
public KeyFrameBuilder Part(uint partId) {
animationKeyFrames.Add(new TimelineAnimationKeyFrame {
FrameIndex = frame, PartId = partId,
});
return this;
}
public FrameSetBuilder EndFrameBuilder() {
parent.AddFrame(animationKeyFrames.ToArray());
return parent;
}
}
+31
View File
@@ -0,0 +1,31 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Client.Graphics;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Timelines;
public class NodeTint {
public Vector3 AddColor;
public Vector3 MultiplyColor;
public static implicit operator AtkTimelineNodeTint(NodeTint tint) => new() {
MultiplyRGB = new ByteColor {
R = (byte)tint.MultiplyColor.X, G = (byte)tint.MultiplyColor.Y, B = (byte)tint.MultiplyColor.Z,
},
AddRGBBitfield = Convert(tint.AddColor),
};
public static implicit operator NodeTint(AtkTimelineNodeTint tint) => new() {
AddColor = new Vector3(tint.AddR, tint.AddG, tint.AddB), MultiplyColor = tint.MultiplyRGB.ToVector4().AsVector3(),
};
private static uint Convert(Vector3 color) {
var red = (short)(color.X + 255);
var green = (short)(color.Y + 255);
var blue = (short)(color.Z + 255);
return (uint)(red & 0x3FF | (green & 0xFFF) << 10 | (blue & 0x3FF) << 22);
}
}
+188
View File
@@ -0,0 +1,188 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using FFXIVClientStructs.Interop;
using KamiToolKit.Classes;
namespace KamiToolKit.Timelines;
public unsafe class Timeline : IDisposable {
private readonly TimelineResource internalTimelineResource;
internal AtkTimeline* InternalTimeline;
public Timeline() {
InternalTimeline = NativeMemoryHelper.UiAlloc<AtkTimeline>();
internalTimelineResource = new TimelineResource();
InternalTimeline->Resource = internalTimelineResource.InternalResource;
InternalTimeline->LabelResource = null;
InternalTimeline->ActiveAnimation = null;
InternalTimeline->OwnerNode = null;
}
internal AtkResNode* OwnerNode {
get => InternalTimeline->OwnerNode;
set => InternalTimeline->OwnerNode = value;
}
public float FrameTime {
get => InternalTimeline->FrameTime;
set => InternalTimeline->FrameTime = value;
}
public float ParentFrameTime {
get => InternalTimeline->ParentFrameTime;
set => InternalTimeline->ParentFrameTime = value;
}
public int LabelFrameIdxDuration {
get => InternalTimeline->LabelFrameIdxDuration;
set => InternalTimeline->LabelFrameIdxDuration = (ushort)value;
}
public int LabelEndFrameIdx {
get => InternalTimeline->LabelEndFrameIdx;
set => InternalTimeline->LabelEndFrameIdx = (ushort)value;
}
public int ActiveLabelId {
get => InternalTimeline->ActiveLabelId;
set => InternalTimeline->ActiveLabelId = (ushort)value;
}
public AtkTimelineMask Mask {
get => InternalTimeline->Mask;
set => InternalTimeline->Mask = value;
}
public AtkTimelineFlags Flags {
get => InternalTimeline->Flags;
set => InternalTimeline->Flags = value;
}
public List<TimelineAnimation> Animations {
set => internalTimelineResource.Animations = value;
}
public List<TimelineLabelSet> LabelSets {
set => internalTimelineResource.LabelSets = value;
}
public void Dispose() {
internalTimelineResource.Dispose();
NativeMemoryHelper.UiFree(InternalTimeline);
InternalTimeline = null;
}
/// <summary>
/// Plays the specified animation via label ID
/// </summary>
/// <param name="labelId">The label ID to play</param>
/// <param name="force">Force the animation to restart even if it was already playing</param>
public void PlayAnimation(int labelId, bool force = false)
=> PlayAnimation(AtkTimelineJumpBehavior.Start, labelId, force);
public void PlayAnimation(AtkTimelineJumpBehavior behavior, int labelId, bool force = false) {
if (InternalTimeline is null) return;
if (InternalTimeline->ActiveLabelId != labelId || force) {
InternalTimeline->PlayAnimation(behavior, (ushort)labelId);
}
}
public void StopAnimation() {
if (InternalTimeline is null) return;
InternalTimeline->PlayAnimation(AtkTimelineJumpBehavior.Start, 0);
}
public void UpdateKeyFrame(
int frameId, KeyFrameGroupType groupType, Vector2? position = null, byte? alpha = null, Vector3? addColor = null, Vector3? multiplyColor = null,
float? rotation = null, Vector2? scale = null, Vector3? textColor = null, Vector3? textOutlineColor = null, uint? partId = null, AtkTimelineInterpolation? interpolation = null) {
var keyFrame = GetKeyFrame(groupType, frameId);
if (keyFrame is null) return;
if (position is not null) {
*keyFrame = new TimelineAnimationKeyFrame {
FrameIndex = frameId, Position = position.Value, Interpolation = interpolation ?? AtkTimelineInterpolation.Linear,
};
}
if (alpha is not null) {
*keyFrame = new TimelineAnimationKeyFrame {
FrameIndex = frameId, Alpha = alpha.Value, Interpolation = interpolation ?? AtkTimelineInterpolation.Linear,
};
}
if (addColor is not null || multiplyColor is not null) {
*keyFrame = new TimelineAnimationKeyFrame {
FrameIndex = frameId, AddColor = addColor ?? new Vector3(0.0f, 0.0f, 0.0f), MultiplyColor = multiplyColor ?? new Vector3(100.0f, 100.0f, 100.0f), Interpolation = interpolation ?? AtkTimelineInterpolation.Linear,
};
}
if (rotation is not null) {
*keyFrame = new TimelineAnimationKeyFrame {
FrameIndex = frameId, Rotation = rotation.Value, Interpolation = interpolation ?? AtkTimelineInterpolation.Linear,
};
}
if (scale is not null) {
*keyFrame = new TimelineAnimationKeyFrame {
FrameIndex = frameId, Scale = scale.Value, Interpolation = interpolation ?? AtkTimelineInterpolation.Linear,
};
}
if (textColor is not null) {
*keyFrame = new TimelineAnimationKeyFrame {
FrameIndex = frameId, TextColor = textColor.Value, Interpolation = interpolation ?? AtkTimelineInterpolation.Linear,
};
}
if (textOutlineColor is not null) {
*keyFrame = new TimelineAnimationKeyFrame {
FrameIndex = frameId, TextEdgeColor = textOutlineColor.Value, Interpolation = interpolation ?? AtkTimelineInterpolation.Linear,
};
}
if (partId is not null) {
*keyFrame = new TimelineAnimationKeyFrame {
FrameIndex = frameId, PartId = partId.Value, Interpolation = interpolation ?? AtkTimelineInterpolation.Linear,
};
}
}
private AtkTimelineKeyFrame* GetKeyFrame(KeyFrameGroupType type, int frameIndex) {
var animation = GetAnimationForFrameId(frameIndex);
if (animation is null) return null;
var keyGroup = animation->KeyGroups.GetPointer((int)type);
for (var i = 0; i < keyGroup->KeyFrameCount; i++) {
var keyFrame = &keyGroup->KeyFrames[i];
if (keyFrame->FrameIdx == frameIndex) {
return keyFrame;
}
}
return null;
}
private AtkTimelineAnimation* GetAnimationForFrameId(int frameId) {
if (InternalTimeline is null) return null;
if (InternalTimeline->Resource is null) return null;
for (var index = 0; index < InternalTimeline->Resource->AnimationCount; index++) {
var animation = &InternalTimeline->Resource->Animations[index];
if (animation->StartFrameIdx <= frameId && frameId <= animation->EndFrameIdx)
return animation;
}
return null;
}
}
@@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit.Timelines;
public unsafe class TimelineAnimation : IDisposable {
internal AtkTimelineAnimation* InternalAnimation;
private List<TimelineKeyFrame> internalKeyFrames = [];
public TimelineAnimation() {
InternalAnimation = NativeMemoryHelper.UiAlloc<AtkTimelineAnimation>();
InternalAnimation->StartFrameIdx = 0;
InternalAnimation->EndFrameIdx = 0;
foreach (ref var value in InternalAnimation->KeyGroups) {
value.Type = AtkTimelineKeyGroupType.None;
}
}
public int StartFrameId {
get => InternalAnimation->StartFrameIdx;
set => InternalAnimation->StartFrameIdx = (ushort)value;
}
public int EndFrameId {
get => InternalAnimation->EndFrameIdx;
set => InternalAnimation->EndFrameIdx = (ushort)value;
}
public List<TimelineKeyFrame> KeyFrames {
get => internalKeyFrames;
set {
internalKeyFrames = value;
Resync();
}
}
public void Dispose() {
if (InternalAnimation is null) return;
foreach (ref var spanGroup in InternalAnimation->KeyGroups) {
NativeMemoryHelper.UiFree(spanGroup.KeyFrames);
spanGroup.KeyFrames = null;
spanGroup.KeyFrameCount = 0;
}
NativeMemoryHelper.UiFree(InternalAnimation);
InternalAnimation = null;
}
private void Resync() {
foreach (var keyFrameSet in internalKeyFrames.GroupBy(frame => frame.GroupSelector)) {
ref var keyFrameGroup = ref InternalAnimation->KeyGroups[(int)keyFrameSet.Key];
keyFrameGroup.Type = keyFrameSet.First().GroupType;
if (keyFrameGroup.KeyFrames is not null) {
NativeMemoryHelper.UiFree(keyFrameGroup.KeyFrames, keyFrameGroup.KeyFrameCount);
keyFrameGroup.KeyFrames = null;
}
keyFrameGroup.KeyFrames = NativeMemoryHelper.UiAlloc<AtkTimelineKeyFrame>(keyFrameSet.Count());
var index = 0;
foreach (var keyframe in keyFrameSet) {
keyFrameGroup.KeyFrames[index] = keyframe;
index++;
}
keyFrameGroup.KeyFrameCount = (ushort)keyFrameSet.Count();
}
}
}
public enum KeyFrameGroupType {
Position = 0,
Rotation = 1,
Scale = 2,
Alpha = 3,
Tint = 4,
PartId = 5,
TextColor = 5,
TextEdge = 6,
TextLabel = 7,
}
@@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit.Timelines;
public unsafe class TimelineAnimationArray : IDisposable {
internal AtkTimelineAnimation* InternalTimelineArray = null;
private List<TimelineAnimation> timelineAnimations = [];
public uint Count { get; private set; }
public List<TimelineAnimation> Animations {
get => timelineAnimations;
set {
timelineAnimations = value;
Resync();
}
}
public void Dispose() {
foreach (var animation in timelineAnimations) {
animation.Dispose();
}
NativeMemoryHelper.UiFree(InternalTimelineArray, Count);
InternalTimelineArray = null;
}
private void Resync() {
// Free existing array, we will completely rebuild it
if (InternalTimelineArray is not null) {
NativeMemoryHelper.UiFree(InternalTimelineArray, Count);
InternalTimelineArray = null;
}
// Allocate new array
InternalTimelineArray = NativeMemoryHelper.UiAlloc<AtkTimelineAnimation>(timelineAnimations.Count);
// Copy all Animations into it
foreach (var index in Enumerable.Range(0, timelineAnimations.Count)) {
InternalTimelineArray[index] = *timelineAnimations[index].InternalAnimation;
}
Count = (uint)timelineAnimations.Count;
}
}
@@ -0,0 +1,116 @@
using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using FFXIVClientStructs.STD;
namespace KamiToolKit.Timelines;
public class TimelineAnimationKeyFrame : TimelineKeyFrame {
private readonly NodeTint nodeTint = new();
public Vector2 Position {
get => new(Value.Float2.Item1, Value.Float2.Item2);
set {
Value = new AtkTimelineKeyValue {
Float2 = new StdPair<float, float>(value.X, value.Y),
};
GroupSelector = KeyFrameGroupType.Position;
GroupType = AtkTimelineKeyGroupType.Float2;
}
}
public byte Alpha {
get => Value.Byte;
set {
Value = new AtkTimelineKeyValue {
Byte = value,
};
GroupType = AtkTimelineKeyGroupType.Byte;
GroupSelector = KeyFrameGroupType.Alpha;
}
}
public Vector3 AddColor {
set {
nodeTint.AddColor = value;
UpdateNodeTint();
}
}
public Vector3 MultiplyColor {
set {
nodeTint.MultiplyColor = value;
UpdateNodeTint();
}
}
public float Rotation {
get => Value.Float;
set {
Value = new AtkTimelineKeyValue {
Float = value,
};
GroupType = AtkTimelineKeyGroupType.Float;
GroupSelector = KeyFrameGroupType.Rotation;
}
}
public Vector2 Scale {
get => new(Value.Float2.Item1, Value.Float2.Item2);
set {
Value = new AtkTimelineKeyValue {
Float2 = new StdPair<float, float>(value.X, value.Y),
};
GroupType = AtkTimelineKeyGroupType.Float2;
GroupSelector = KeyFrameGroupType.Scale;
}
}
public Vector3 TextColor {
get => new Vector3(Value.RGB.R, Value.RGB.G, Value.RGB.B) * 255.0f;
set {
Value = new AtkTimelineKeyValue {
RGB = value.AsVector4().ToByteColor(),
};
GroupType = AtkTimelineKeyGroupType.RGB;
GroupSelector = KeyFrameGroupType.TextColor;
}
}
public Vector3 TextEdgeColor {
get => new Vector3(Value.RGB.R, Value.RGB.G, Value.RGB.B) * 255.0f;
set {
Value = new AtkTimelineKeyValue {
RGB = value.AsVector4().ToByteColor(),
};
GroupType = AtkTimelineKeyGroupType.RGB;
GroupSelector = KeyFrameGroupType.TextEdge;
}
}
public uint PartId {
set {
Value = new AtkTimelineKeyValue {
UShort = (ushort)value,
};
GroupType = AtkTimelineKeyGroupType.UShort;
GroupSelector = KeyFrameGroupType.PartId;
}
}
private void UpdateNodeTint() {
Value = new AtkTimelineKeyValue {
NodeTint = nodeTint,
};
GroupType = AtkTimelineKeyGroupType.NodeTint;
GroupSelector = KeyFrameGroupType.Tint;
}
}
+44
View File
@@ -0,0 +1,44 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace KamiToolKit.Timelines;
public class TimelineBuilder {
internal List<TimelineAnimation> Animations = [];
internal List<TimelineLabelSet> LabelSets = [];
public FrameSetBuilder BeginFrameSet(int startFrameId, int endFrameId)
=> new(this, startFrameId, endFrameId);
public TimelineBuilder AddFrameSetWithFrame(
int startFrameId, int endFrameId, int frameId, Vector2? position = null, byte? alpha = null, Vector3? addColor = null, Vector3? multiplyColor = null,
float? rotation = null, Vector2? scale = null, Vector3? textColor = null, Vector3? textOutlineColor = null, uint? partId = null) {
new FrameSetBuilder(this, startFrameId, endFrameId)
.AddFrame(frameId, position, alpha, addColor, multiplyColor, rotation, scale, textColor, textOutlineColor, partId)
.EndFrameSet();
return this;
}
public KeyFrameBuilder AddFrame(int frameSetStart, int frameSetEnd, int frameIndex)
=> new(new FrameSetBuilder(this, frameSetStart, frameSetEnd), frameIndex);
public Timeline Build() {
var newTimeline = new Timeline();
if (LabelSets.Count != 0) {
newTimeline.LabelSets = LabelSets;
newTimeline.LabelFrameIdxDuration = LabelSets.Max(label => label.EndFrameId) - 1;
newTimeline.LabelEndFrameIdx = LabelSets.Max(label => label.EndFrameId);
}
if (Animations.Count != 0) {
newTimeline.Animations = Animations;
}
return newTimeline;
}
}
+23
View File
@@ -0,0 +1,23 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Timelines;
public abstract class TimelineKeyFrame {
public KeyFrameGroupType GroupSelector { get; set; }
public AtkTimelineKeyGroupType GroupType { get; set; }
public float SpeedStart { get; set; } = 0.0f;
public float SpeedEnd { get; set; } = 1.0f;
public required int FrameIndex { get; set; }
public AtkTimelineInterpolation Interpolation { get; set; } = AtkTimelineInterpolation.Linear;
public AtkTimelineKeyValue Value { get; set; }
public static implicit operator AtkTimelineKeyFrame(TimelineKeyFrame frame) => new() {
Interpolation = frame.Interpolation,
SpeedCoefficient1 = frame.SpeedStart,
SpeedCoefficient2 = frame.SpeedEnd,
FrameIdx = (ushort)frame.FrameIndex,
Value = frame.Value,
};
}
+65
View File
@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit.Timelines;
public unsafe class TimelineLabelSet : IDisposable {
private List<TimelineKeyFrame> internalKeyFrames = [];
internal AtkTimelineLabelSet* InternalLabelSet;
public TimelineLabelSet() {
InternalLabelSet = NativeMemoryHelper.UiAlloc<AtkTimelineLabelSet>();
InternalLabelSet->StartFrameIdx = 0;
InternalLabelSet->EndFrameIdx = 0;
InternalLabelSet->LabelKeyGroup.Type = AtkTimelineKeyGroupType.Label;
}
public int StartFrameId {
get => InternalLabelSet->StartFrameIdx;
set => InternalLabelSet->StartFrameIdx = (ushort)value;
}
public int EndFrameId {
get => InternalLabelSet->EndFrameIdx;
set => InternalLabelSet->EndFrameIdx = (ushort)value;
}
public List<TimelineKeyFrame> Labels {
get => internalKeyFrames;
set {
internalKeyFrames = value;
Resync();
}
}
public void Dispose() {
NativeMemoryHelper.UiFree(InternalLabelSet);
InternalLabelSet = null;
}
private void Resync() {
ref var keyGroup = ref InternalLabelSet->LabelKeyGroup;
// Free existing array, we will completely rebuild it
if (keyGroup.KeyFrames is null) {
NativeMemoryHelper.UiFree(keyGroup.KeyFrames, keyGroup.KeyFrameCount);
keyGroup.KeyFrames = null;
}
// Allocate new array
keyGroup.KeyFrames = NativeMemoryHelper.UiAlloc<AtkTimelineKeyFrame>(internalKeyFrames.Count);
var index = 0;
foreach (var keyFrame in internalKeyFrames) {
keyGroup.KeyFrames[index] = keyFrame;
index++;
}
keyGroup.KeyFrameCount = (ushort)internalKeyFrames.Count;
}
}
@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.Linq;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit.Timelines;
public unsafe class TimelineLabelSetArray : IDisposable {
internal AtkTimelineLabelSet* InternalLabelSetArray = null;
private List<TimelineLabelSet> labelSets = [];
public uint Count { get; private set; }
public List<TimelineLabelSet> LabelSets {
get => labelSets;
set {
labelSets = value;
Resync();
}
}
public void Dispose() {
foreach (var labelSet in labelSets) {
labelSet.Dispose();
}
NativeMemoryHelper.UiFree(InternalLabelSetArray, Count);
InternalLabelSetArray = null;
}
private void Resync() {
// Free existing array, we will completely rebuild it
if (InternalLabelSetArray is not null) {
NativeMemoryHelper.UiFree(InternalLabelSetArray, Count);
InternalLabelSetArray = null;
}
// Allocate new array
InternalLabelSetArray = NativeMemoryHelper.UiAlloc<AtkTimelineLabelSet>(labelSets.Count);
// Copy all Animations into it
foreach (var index in Enumerable.Range(0, labelSets.Count)) {
InternalLabelSetArray[index] = *labelSets[index].InternalLabelSet;
}
Count = (uint)labelSets.Count;
}
}
@@ -0,0 +1,43 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace KamiToolKit.Timelines;
public class TimelineLabelSetKeyFrame : TimelineKeyFrame {
private AtkTimelineLabel data;
public AtkTimelineJumpBehavior JumpBehavior {
get => data.JumpBehavior;
set {
data.JumpBehavior = value;
UpdateValue();
}
}
public int LabelId {
get => data.LabelId;
set {
data.LabelId = (ushort)value;
UpdateValue();
}
}
public int JumpLabelId {
get => data.JumpLabelId;
set {
data.JumpLabelId = (byte)value;
UpdateValue();
}
}
private void UpdateValue() {
Value = new AtkTimelineKeyValue {
Label = data,
};
GroupType = AtkTimelineKeyGroupType.Label;
SpeedEnd = 0.0f;
Interpolation = AtkTimelineInterpolation.None;
GroupSelector = KeyFrameGroupType.TextLabel;
}
}
+59
View File
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
namespace KamiToolKit.Timelines;
public unsafe class TimelineResource : IDisposable {
private readonly TimelineAnimationArray animationArray;
private readonly TimelineLabelSetArray labelsArray;
internal AtkTimelineResource* InternalResource;
public TimelineResource() {
InternalResource = NativeMemoryHelper.UiAlloc<AtkTimelineResource>();
InternalResource->Id = 2;
InternalResource->AnimationCount = 0;
InternalResource->LabelSetCount = 0;
animationArray = new TimelineAnimationArray();
InternalResource->Animations = animationArray.InternalTimelineArray;
labelsArray = new TimelineLabelSetArray();
InternalResource->LabelSets = labelsArray.InternalLabelSetArray;
}
public List<TimelineAnimation> Animations {
get => animationArray.Animations;
set {
animationArray.Animations = value;
InternalResource->Animations = animationArray.InternalTimelineArray;
InternalResource->AnimationCount = (ushort)animationArray.Count;
}
}
public List<TimelineLabelSet> LabelSets {
get => labelsArray.LabelSets;
set {
labelsArray.LabelSets = value;
InternalResource->LabelSets = labelsArray.InternalLabelSetArray;
InternalResource->LabelSetCount = (ushort)labelsArray.Count;
}
}
public int Id {
get => (int)InternalResource->Id;
set => InternalResource->Id = (uint)value;
}
public void Dispose() {
animationArray.Dispose();
labelsArray.Dispose();
NativeMemoryHelper.UiFree(InternalResource);
InternalResource = null;
}
}