From 2ec880c5a6e94b1695647c9ad56e7c34a62dc291 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Wed, 12 Nov 2025 15:58:49 +0800 Subject: [PATCH] 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 --- BallanceTasEditor/BallanceTasEditor.csproj | 2 + BallanceTasEditor/Utils/FpsConverter.cs | 31 +++ BallanceTasEditor/Utils/TasFrame.cs | 76 +++++- BallanceTasEditor/Utils/TasMemory.cs | 281 +++++++++++++++++++++ 4 files changed, 384 insertions(+), 6 deletions(-) create mode 100644 BallanceTasEditor/Utils/FpsConverter.cs create mode 100644 BallanceTasEditor/Utils/TasMemory.cs diff --git a/BallanceTasEditor/BallanceTasEditor.csproj b/BallanceTasEditor/BallanceTasEditor.csproj index cc6e093..9b868b0 100644 --- a/BallanceTasEditor/BallanceTasEditor.csproj +++ b/BallanceTasEditor/BallanceTasEditor.csproj @@ -97,7 +97,9 @@ App.xaml Code + + MainWindow.xaml Code diff --git a/BallanceTasEditor/Utils/FpsConverter.cs b/BallanceTasEditor/Utils/FpsConverter.cs new file mode 100644 index 0000000..d779032 --- /dev/null +++ b/BallanceTasEditor/Utils/FpsConverter.cs @@ -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; + } + } +} diff --git a/BallanceTasEditor/Utils/TasFrame.cs b/BallanceTasEditor/Utils/TasFrame.cs index ff56f21..bfb54e4 100644 --- a/BallanceTasEditor/Utils/TasFrame.cs +++ b/BallanceTasEditor/Utils/TasFrame.cs @@ -8,10 +8,10 @@ using System.Threading.Tasks; namespace BallanceTasEditor.Utils { /// - /// 描述TAS文件中一帧的结构。 + /// 原始的TAS帧结构,与二进制结构保持一致。 /// [StructLayout(LayoutKind.Sequential)] - public struct TasFrame { + public struct RawTasFrame { /// /// 该帧的持续时间(以秒为单位)。 /// @@ -20,6 +20,70 @@ namespace BallanceTasEditor.Utils { /// 该帧的按键组合。 /// public uint KeyFlags; + } + + /// + /// 描述TAS文件中一帧的结构。 + /// + public class TasFrame { + /// + /// 以指定的FPS,无任何按键初始化当前帧。 + /// + public TasFrame(uint fps = 60) { + m_TimeDelta = FpsConverter.ToDelta(fps); + m_KeyFlags = 0; + } + + /// + /// 从原始TAS数据初始化。 + /// + /// 要用来初始化的原始数据。 + public TasFrame(RawTasFrame raw) { + m_TimeDelta = raw.TimeDelta; + m_KeyFlags = raw.KeyFlags; + } + + /// + /// 转换为原始TAS数据。 + /// + /// 转换后的原始TAS数据。 + public RawTasFrame ToRaw() { + return new RawTasFrame() { TimeDelta = m_TimeDelta, KeyFlags = m_KeyFlags }; + } + + /// + /// 原位转换为原始TAS数据。 + /// + /// 以引用传递的原始TAS数据。 + public void ToImplaceRaw(ref RawTasFrame raw) { + raw.TimeDelta = m_TimeDelta; + raw.KeyFlags = m_KeyFlags; + } + + /// + /// 该帧的持续时间(以秒为单位)。 + /// + private float m_TimeDelta; + /// + /// 该帧的按键组合。 + /// + private uint m_KeyFlags; + + /// + /// 获取帧时间Delta。 + /// + /// 获取到的帧时间Delta。 + public float GetTimeDelta() { + return m_TimeDelta; + } + + /// + /// 设置帧时间Delta。 + /// + /// 要设置的帧时间Delta。 + public void SetTimeDelta(float delta) { + m_TimeDelta = delta; + } /// /// 判断按键是否被按下。 @@ -27,7 +91,7 @@ namespace BallanceTasEditor.Utils { /// 要检查的按键。 /// true表示被按下,否则为false。 public bool IsKeyPressed(TasKey key) { - return (KeyFlags & (1u << (int)key)) != 0; + return (m_KeyFlags & (1u << (int)key)) != 0; } /// @@ -36,8 +100,8 @@ namespace BallanceTasEditor.Utils { /// 要设置的按键。 /// true表示设置为按下,否则为松开。 public void SetKeyPressed(TasKey key, bool pressed = true) { - if (pressed) KeyFlags |= (1u << (int)key); - else KeyFlags &= ~(1u << (int)key); + if (pressed) m_KeyFlags |= (1u << (int)key); + else m_KeyFlags &= ~(1u << (int)key); } /// @@ -45,7 +109,7 @@ namespace BallanceTasEditor.Utils { /// /// 要反转的按键。 public void FlipKeyPressed(TasKey key) { - KeyFlags ^= (1u << (int)key); + m_KeyFlags ^= (1u << (int)key); } /// diff --git a/BallanceTasEditor/Utils/TasMemory.cs b/BallanceTasEditor/Utils/TasMemory.cs new file mode 100644 index 0000000..16da0e2 --- /dev/null +++ b/BallanceTasEditor/Utils/TasMemory.cs @@ -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 { + + /// + /// 所有用于在内存中存储TAS帧的结构都必须实现此interface。 + /// + public interface ITasMemory where T : class { + /// + /// 访问给定索引的值。 + /// + /// 要访问的单元的索引。 + /// 被访问的单元。 + /// 给定的索引超出范围。 + T Visit(int index); + /// + /// 在给定的索引之前插入给定的项目。 + /// + /// + /// 按照此函数约定,如果要在头部插入数据,则可以通过指定0来实现。 + /// 然而对于在尾部插入数据,或在空的存储中插入数据,可以指定存储结构的长度来实现。 + /// 即指定最大Index + 1的值来实现。 + /// 实现此函数时需要格外注意。 + /// + /// 要在前方插入数据的元素的索引。 + /// 要插入的元素的迭代器。 + /// 给定的索引超出范围。 + void Insert(int index, IEnumerable items); + /// + /// 从给定单元开始,移除给定个数的元素。 + /// + /// 要开始移除的单元的索引。 + /// 要移除的元素的个数。 + /// 给定的索引超出范围。 + void Remove(int index, int count); + + /// + /// 清空存储结构。 + /// + void Clear(); + /// + /// 获取当前存储的TAS帧的个数。 + /// + /// 存储的TAS帧的个数。 + int GetCount(); + /// + /// 获取当前存储结构是不是空的。 + /// + /// 如果是空的就返回true,否则返回false。 + bool IsEmpty(); + } + + /// + /// 基于Gap Buffer思想的TAS存储器。 + /// + /// + /// 其实就是把List的InsertRange的复杂度从O(n*m)修正为O(n)。 + /// + public class GapBufferTasMemory : ITasMemory where T : class { + public GapBufferTasMemory() { + + } + + public T Visit(int index) { + throw new NotImplementedException(); + } + + public void Insert(int index, IEnumerable 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(); + } + } + + /// + /// 基于简单的List的TAS存储器。 + /// + /// + /// 由于List的InsertRange的复杂度是O(n*m),可能不符合要求。 + /// + public class ListTasMemory : ITasMemory where T : class { + public ListTasMemory() { + m_Container = new List(); + } + + private List m_Container; + + public T Visit(int index) { + return m_Container[index]; + } + + public void Insert(int index, IEnumerable 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; + } + } + + /// + /// 传统的基于LinkedList的TAS存储器。 + /// + public class LegacyTasMemory : ITasMemory where T : class { + public LegacyTasMemory() { + m_Container = new LinkedList(); + m_Cursor = null; + m_CursorIndex = null; + } + + private LinkedList m_Container; + private LinkedListNode m_Cursor; + private int? m_CursorIndex; + + private enum NodeSeekOrigin { + Head, + Cursor, + Tail, + } + + private struct NodeSeekInfo : IComparable { + public NodeSeekOrigin Origin; + public int Offset; + + public int CompareTo(NodeSeekInfo other) { + return this.Offset.CompareTo(other.Offset); + } + } + + /// + /// 快速将内部游标移动到指定Index,并更新与之匹配的Index。 + /// + /// + /// + 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 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 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为当前Cursor,Index--为对应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; + } + } +}