refactor(utils): rename TasFrame to RawTasFrame and add new TasFrame class

- Renamed original TasFrame struct to RawTasFrame to reflect its role as raw binary data
- Added new TasFrame class with encapsulated fields and helper methods
- Added FpsConverter usage for time delta calculation
- Added conversion methods between RawTasFrame and TasFrame
- Added getter and setter methods for frame properties
- Updated key flag operations to use private field instead of public one
- Added new utility files FpsConverter.cs and TasMemory.cs to project
This commit is contained in:
2025-11-12 15:58:49 +08:00
parent c57108536a
commit 2ec880c5a6
4 changed files with 384 additions and 6 deletions

View File

@ -97,7 +97,9 @@
<DependentUpon>App.xaml</DependentUpon> <DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType> <SubType>Code</SubType>
</Compile> </Compile>
<Compile Include="Utils\FpsConverter.cs" />
<Compile Include="Utils\TasFrame.cs" /> <Compile Include="Utils\TasFrame.cs" />
<Compile Include="Utils\TasMemory.cs" />
<Compile Include="Views\MainWindow.xaml.cs"> <Compile Include="Views\MainWindow.xaml.cs">
<DependentUpon>MainWindow.xaml</DependentUpon> <DependentUpon>MainWindow.xaml</DependentUpon>
<SubType>Code</SubType> <SubType>Code</SubType>

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BallanceTasEditor.Utils {
public static class FpsConverter {
public static float ToFps(float delta) {
if (delta <= 0f)
throw new ArgumentOutOfRangeException("invalid time delta (not positive)");
return 1f / delta;
}
public static uint ToFloorFps(float delta) {
return (uint)Math.Floor(ToFps(delta));
}
public static float ToDelta(uint fps) {
return ToDelta((float)fps);
}
public static float ToDelta(float fps) {
if (fps <= 0f)
throw new ArgumentOutOfRangeException("invalid fps (not positive)");
return 1f / fps;
}
}
}

View File

@ -8,10 +8,10 @@ using System.Threading.Tasks;
namespace BallanceTasEditor.Utils { namespace BallanceTasEditor.Utils {
/// <summary> /// <summary>
/// 描述TAS文件中一帧的结构 /// 原始的TAS帧结构与二进制结构保持一致
/// </summary> /// </summary>
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
public struct TasFrame { public struct RawTasFrame {
/// <summary> /// <summary>
/// 该帧的持续时间(以秒为单位)。 /// 该帧的持续时间(以秒为单位)。
/// </summary> /// </summary>
@ -20,6 +20,70 @@ namespace BallanceTasEditor.Utils {
/// 该帧的按键组合。 /// 该帧的按键组合。
/// </summary> /// </summary>
public uint KeyFlags; public uint KeyFlags;
}
/// <summary>
/// 描述TAS文件中一帧的结构。
/// </summary>
public class TasFrame {
/// <summary>
/// 以指定的FPS无任何按键初始化当前帧。
/// </summary>
public TasFrame(uint fps = 60) {
m_TimeDelta = FpsConverter.ToDelta(fps);
m_KeyFlags = 0;
}
/// <summary>
/// 从原始TAS数据初始化。
/// </summary>
/// <param name="raw">要用来初始化的原始数据。</param>
public TasFrame(RawTasFrame raw) {
m_TimeDelta = raw.TimeDelta;
m_KeyFlags = raw.KeyFlags;
}
/// <summary>
/// 转换为原始TAS数据。
/// </summary>
/// <returns>转换后的原始TAS数据。</returns>
public RawTasFrame ToRaw() {
return new RawTasFrame() { TimeDelta = m_TimeDelta, KeyFlags = m_KeyFlags };
}
/// <summary>
/// 原位转换为原始TAS数据。
/// </summary>
/// <param name="raw">以引用传递的原始TAS数据。</param>
public void ToImplaceRaw(ref RawTasFrame raw) {
raw.TimeDelta = m_TimeDelta;
raw.KeyFlags = m_KeyFlags;
}
/// <summary>
/// 该帧的持续时间(以秒为单位)。
/// </summary>
private float m_TimeDelta;
/// <summary>
/// 该帧的按键组合。
/// </summary>
private uint m_KeyFlags;
/// <summary>
/// 获取帧时间Delta。
/// </summary>
/// <returns>获取到的帧时间Delta。</returns>
public float GetTimeDelta() {
return m_TimeDelta;
}
/// <summary>
/// 设置帧时间Delta。
/// </summary>
/// <param name="delta">要设置的帧时间Delta。</param>
public void SetTimeDelta(float delta) {
m_TimeDelta = delta;
}
/// <summary> /// <summary>
/// 判断按键是否被按下。 /// 判断按键是否被按下。
@ -27,7 +91,7 @@ namespace BallanceTasEditor.Utils {
/// <param name="key">要检查的按键。</param> /// <param name="key">要检查的按键。</param>
/// <returns>true表示被按下否则为false。</returns> /// <returns>true表示被按下否则为false。</returns>
public bool IsKeyPressed(TasKey key) { public bool IsKeyPressed(TasKey key) {
return (KeyFlags & (1u << (int)key)) != 0; return (m_KeyFlags & (1u << (int)key)) != 0;
} }
/// <summary> /// <summary>
@ -36,8 +100,8 @@ namespace BallanceTasEditor.Utils {
/// <param name="key">要设置的按键。</param> /// <param name="key">要设置的按键。</param>
/// <param name="pressed">true表示设置为按下否则为松开。</param> /// <param name="pressed">true表示设置为按下否则为松开。</param>
public void SetKeyPressed(TasKey key, bool pressed = true) { public void SetKeyPressed(TasKey key, bool pressed = true) {
if (pressed) KeyFlags |= (1u << (int)key); if (pressed) m_KeyFlags |= (1u << (int)key);
else KeyFlags &= ~(1u << (int)key); else m_KeyFlags &= ~(1u << (int)key);
} }
/// <summary> /// <summary>
@ -45,7 +109,7 @@ namespace BallanceTasEditor.Utils {
/// </summary> /// </summary>
/// <param name="key">要反转的按键。</param> /// <param name="key">要反转的按键。</param>
public void FlipKeyPressed(TasKey key) { public void FlipKeyPressed(TasKey key) {
KeyFlags ^= (1u << (int)key); m_KeyFlags ^= (1u << (int)key);
} }
/// <summary> /// <summary>

View File

@ -0,0 +1,281 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace BallanceTasEditor.Utils {
/// <summary>
/// 所有用于在内存中存储TAS帧的结构都必须实现此interface。
/// </summary>
public interface ITasMemory<T> where T : class {
/// <summary>
/// 访问给定索引的值。
/// </summary>
/// <param name="index">要访问的单元的索引。</param>
/// <returns>被访问的单元。</returns>
/// <exception cref="ArgumentOutOfRangeException">给定的索引超出范围。</exception>
T Visit(int index);
/// <summary>
/// 在给定的索引<b>之前</b>插入给定的项目。
/// </summary>
/// <remarks>
/// 按照此函数约定如果要在头部插入数据则可以通过指定0来实现。
/// 然而对于在尾部插入数据,或在空的存储中插入数据,可以指定存储结构的长度来实现。
/// 即指定最大Index + 1的值来实现。
/// 实现此函数时需要格外注意。
/// </remarks>
/// <param name="index">要在前方插入数据的元素的索引。</param>
/// <param name="items">要插入的元素的迭代器。</param>
/// <exception cref="ArgumentOutOfRangeException">给定的索引超出范围。</exception>
void Insert(int index, IEnumerable<T> items);
/// <summary>
/// 从给定单元开始,移除给定个数的元素。
/// </summary>
/// <param name="index">要开始移除的单元的索引。</param>
/// <param name="count">要移除的元素的个数。</param>
/// <exception cref="ArgumentOutOfRangeException">给定的索引超出范围。</exception>
void Remove(int index, int count);
/// <summary>
/// 清空存储结构。
/// </summary>
void Clear();
/// <summary>
/// 获取当前存储的TAS帧的个数。
/// </summary>
/// <returns>存储的TAS帧的个数。</returns>
int GetCount();
/// <summary>
/// 获取当前存储结构是不是空的。
/// </summary>
/// <returns>如果是空的就返回true否则返回false。</returns>
bool IsEmpty();
}
/// <summary>
/// 基于Gap Buffer思想的TAS存储器。
/// </summary>
/// <remarks>
/// 其实就是把List的InsertRange的复杂度从O(n*m)修正为O(n)。
/// </remarks>
public class GapBufferTasMemory<T> : ITasMemory<T> where T : class {
public GapBufferTasMemory() {
}
public T Visit(int index) {
throw new NotImplementedException();
}
public void Insert(int index, IEnumerable<T> items) {
throw new NotImplementedException();
}
public void Remove(int index, int count) {
throw new NotImplementedException();
}
public void Clear() {
throw new NotImplementedException();
}
public int GetCount() {
throw new NotImplementedException();
}
public bool IsEmpty() {
throw new NotImplementedException();
}
}
/// <summary>
/// 基于简单的List的TAS存储器。
/// </summary>
/// <remarks>
/// 由于List的InsertRange的复杂度是O(n*m),可能不符合要求。
/// </remarks>
public class ListTasMemory<T> : ITasMemory<T> where T : class {
public ListTasMemory() {
m_Container = new List<T>();
}
private List<T> m_Container;
public T Visit(int index) {
return m_Container[index];
}
public void Insert(int index, IEnumerable<T> items) {
m_Container.InsertRange(index, items);
}
public void Remove(int index, int count) {
m_Container.RemoveRange(index, count);
}
public void Clear() {
m_Container.Clear();
}
public int GetCount() {
return m_Container.Count;
}
public bool IsEmpty() {
return GetCount() == 0;
}
}
/// <summary>
/// 传统的基于LinkedList的TAS存储器。
/// </summary>
public class LegacyTasMemory<T> : ITasMemory<T> where T : class {
public LegacyTasMemory() {
m_Container = new LinkedList<T>();
m_Cursor = null;
m_CursorIndex = null;
}
private LinkedList<T> m_Container;
private LinkedListNode<T> m_Cursor;
private int? m_CursorIndex;
private enum NodeSeekOrigin {
Head,
Cursor,
Tail,
}
private struct NodeSeekInfo : IComparable<NodeSeekInfo> {
public NodeSeekOrigin Origin;
public int Offset;
public int CompareTo(NodeSeekInfo other) {
return this.Offset.CompareTo(other.Offset);
}
}
/// <summary>
/// 快速将内部游标移动到指定Index并更新与之匹配的Index。
/// </summary>
/// <param name="desiredIndex"></param>
/// <exception cref="Exception"></exception>
private void MoveToIndex(int desiredIndex) {
// 检查基本环境
if (desiredIndex >= GetCount())
throw new ArgumentOutOfRangeException("Index out of range");
if (m_Cursor is null || !m_CursorIndex.HasValue || IsEmpty())
throw new InvalidOperationException("Can not move cursor when container is empty.");
// 创建三个候选方案。
var candidates = new NodeSeekInfo[3] {
new NodeSeekInfo() { Origin = NodeSeekOrigin.Head, Offset = desiredIndex },
new NodeSeekInfo() { Origin = NodeSeekOrigin.Cursor, Offset = desiredIndex - (GetCount() - 1) },
new NodeSeekInfo() { Origin = NodeSeekOrigin.Tail, Offset = desiredIndex - m_CursorIndex.Value },
};
// 确定哪个候选方案最短。
var bestCandidate = candidates.Min();
// 用最短候选方案移动。
int pickedOffset = bestCandidate.Offset;
LinkedListNode<T> pickedNode = null;
switch (bestCandidate.Origin) {
case NodeSeekOrigin.Head:
pickedNode = m_Container.First;
break;
case NodeSeekOrigin.Cursor:
pickedNode = m_Cursor;
break;
case NodeSeekOrigin.Tail:
pickedNode = m_Container.Last;
break;
}
long alreadyMoved = 0;
if (pickedOffset < 0) {
while (alreadyMoved != pickedOffset) {
pickedNode = pickedNode.Previous;
alreadyMoved--;
}
} else if (pickedOffset > 0) {
while (alreadyMoved != pickedOffset) {
pickedNode = pickedNode.Next;
alreadyMoved++;
}
}
// 设置Cursor和Index
m_Cursor = pickedNode;
m_CursorIndex = desiredIndex;
}
public T Visit(int index) {
MoveToIndex(index);
return m_Cursor.Value;
}
public void Insert(int index, IEnumerable<T> items) {
if (index == GetCount()) {
foreach (T item in items) {
m_Container.AddLast(item);
}
} else {
MoveToIndex(index);
int count = 0;
foreach (T item in items) {
m_Container.AddBefore(m_Cursor, item);
++count;
}
m_CursorIndex += count;
}
}
public void Remove(int index, int count) {
if (index + count >= GetCount())
throw new ArgumentOutOfRangeException("Expected removed items out of range.");
MoveToIndex(index);
// 我们总是获取要删除的项目的前一项来作为参照。
// 如果获取到的是null则说明是正在删第一项从m_Container里获取First来删除就行
// 否则就继续用这个Node的Next来删除。
var prevNode = m_Cursor.Previous;
if (prevNode is null) {
for (int i = 0; i < count; ++i) {
m_Container.RemoveFirst();
}
} else {
for (int i = 0; i < count; ++i) {
m_Container.Remove(prevNode.Next);
}
}
// 然后设置Cursor和Index
// 如果全部删完了,就清除这两个的设置。
// 否则就以prevNode为当前CursorIndex--为对应Index。
if (IsEmpty()) {
m_Cursor = null;
m_CursorIndex = null;
} else {
m_Cursor = prevNode;
--m_CursorIndex;
}
}
public void Clear() {
m_Container.Clear();
m_Cursor = null;
m_CursorIndex = null;
}
public int GetCount() {
return m_Container.Count();
}
public bool IsEmpty() {
return GetCount() == 0;
}
}
}