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;
+ }
+ }
+}