1
0
Files
BallanceTasToolbox/BallanceTasEditor/BallanceTasEditor/Backend/TasOperation.cs

526 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BallanceTasEditor.Backend {
/// <summary>
/// TAS操作接口。所有TAS操作均需要支持此接口。
/// </summary>
public interface ITasOperation {
/// <summary>
/// 执行对应的TAS操作。
/// </summary>
/// <param name="seq">所要操作的TAS存储容器。</param>
void Execute(ITasSequence seq);
/// <summary>
/// 检查该操作是否已经被执行过。
/// </summary>
/// <remarks>
/// 所有Tas操作类创建后只能执行一次或者不执行
/// 因此有此函数用于获取是否已经执行过。
/// </remarks>
/// <returns>如果已经执行过返回true否则返回false。</returns>
bool IsExecuted();
}
/// <summary>
/// 可撤销的TAS操作接口所有可撤销的TAS操作均需支持此接口。
/// </summary>
public interface ITasRevocableOperation : ITasOperation {
/// <summary>
/// 撤销对应TAS操作。
/// </summary>
/// <param name="seq">所要撤销操作的TAS存储容器。</param>
void Revoke(ITasSequence seq);
/// <summary>
/// 返回该TAS操作占用的内存大小。
/// </summary>
/// <remarks>
/// 可撤销的TAS操作会在内存中存储一定数据用于撤销对应操作。
/// 该函数返回的占用用于衡量该操作的开销。
/// 我们应当基于大小,而非写死的个数决定撤销栈中的最大操作次数,
/// 例如对于小型操作我们可以存储100个对于大型操作则只能存储5个等。
/// 用于解决编辑者目前认为撤销栈大小不足的情况。
/// <para/>
/// 该函数返回的大小可以不是特别精确,但要准确反映空间复杂度。
/// </remarks>
/// <returns>占用的内存大小以byte为单位。</returns>
int Occupation();
}
internal static class OperationUtils {
internal const int SIZEOF_DELTA_TIME = sizeof(float);
internal const int SIZEOF_KEYS = sizeof(uint);
internal const int SIZEOF_FRAME = SIZEOF_DELTA_TIME + SIZEOF_KEYS;
internal static readonly InvalidOperationException ExecutionEnvironment = new InvalidOperationException("Can not execute one TAS operation multiple times.");
internal static readonly InvalidOperationException RevokeEnvironment = new InvalidOperationException("Can not revoke an not executed TAS operation.");
}
public enum CellKeysOperationKind {
Set, Unset, Flip
}
public class CellKeysOperation : ITasRevocableOperation {
public static CellKeysOperation FromSingleCell(CellKeysOperationKind kind, int index, TasKey key) {
return new CellKeysOperation(kind, index, index, key, key);
}
public static CellKeysOperation FromCellRange(CellKeysOperationKind kind, int startIndex, int endIndex, TasKey startKey, TasKey endKey) {
return new CellKeysOperation(kind, startIndex, endIndex, startKey, endKey);
}
private CellKeysOperation(CellKeysOperationKind kind, int startIndex, int endIndex, TasKey startKey, TasKey endKey) {
// Check arguments.
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
ArgumentOutOfRangeException.ThrowIfGreaterThan(startKey.ToIndex(), endKey.ToIndex());
// Setup members.
m_Kind = kind;
m_StartIndex = startIndex;
m_EndIndex = endIndex;
m_StartKey = startKey;
m_EndKey = endKey;
m_FramesBackup = null;
}
private CellKeysOperationKind m_Kind;
private int m_StartIndex, m_EndIndex;
private TasKey m_StartKey, m_EndKey;
private RawTasFrame[]? m_FramesBackup;
[MemberNotNullWhen(true, nameof(m_FramesBackup))]
public bool IsExecuted() {
return m_FramesBackup is not null;
}
public void Execute(ITasSequence seq) {
if (IsExecuted()) {
throw OperationUtils.ExecutionEnvironment;
}
// Check index range.
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(m_EndIndex, seq.GetCount());
ArgumentOutOfRangeException.ThrowIfLessThan(m_StartIndex, 0);
// Do backup and set values at the same time
var backups = new RawTasFrame[m_EndIndex - m_StartIndex + 1];
// Pre-build key list for fast fetching.
var keys = Enumerable.Range(m_StartKey.ToIndex(), m_EndKey.ToIndex() - m_StartKey.ToIndex() + 1).Select((i) => TasKey.FromIndex(i)).ToArray();
for (int index = m_StartIndex; index <= m_EndIndex; index++) {
// Fetch frame
var frame = seq.Visit(index);
// Do backup
frame.ToRawImplace(ref backups[index - m_StartIndex]);
// Modify keys
foreach (var key in keys) {
switch (m_Kind) {
case CellKeysOperationKind.Set:
frame.SetKeyPressed(key, true);
break;
case CellKeysOperationKind.Unset:
frame.SetKeyPressed(key, false);
break;
case CellKeysOperationKind.Flip:
frame.FlipKeyPressed(key);
break;
}
}
}
// Assign backups
m_FramesBackup = backups;
}
public void Revoke(ITasSequence seq) {
if (!IsExecuted()) {
throw OperationUtils.RevokeEnvironment;
}
// Index range is checked,
// so we directly restore backup.
for (int index = m_StartIndex; index <= m_EndIndex; index++) {
seq.Visit(index).FromRawImplace(m_FramesBackup[index - m_StartIndex]);
}
// Clear backups
m_FramesBackup = null;
}
public int Occupation() {
return (m_EndIndex - m_StartIndex) * OperationUtils.SIZEOF_FRAME;
}
}
public class FrameFpsOperation : ITasRevocableOperation {
public static FrameFpsOperation FromSingleFrame(int index, uint fps) {
return new FrameFpsOperation(index, index, fps);
}
public static FrameFpsOperation FromFrameRange(int startIndex, int endIndex, uint fps) {
return new FrameFpsOperation(startIndex, endIndex, fps);
}
private FrameFpsOperation(int startIndex, int endIndex, uint fps) {
// Check arguments
if (!FpsConverter.IsValidFps(fps)) {
throw new ArgumentOutOfRangeException(nameof(fps));
}
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
// Assign arguments
m_StartIndex = startIndex;
m_EndIndex = endIndex;
m_DeltaTime = FpsConverter.ToDelta(fps);
}
private int m_StartIndex, m_EndIndex;
private float m_DeltaTime;
private float[]? m_DeltaTimesBackup;
[MemberNotNullWhen(true, nameof(m_DeltaTimesBackup))]
public bool IsExecuted() {
return m_DeltaTimesBackup is not null;
}
public void Execute(ITasSequence seq) {
if (IsExecuted()) {
throw OperationUtils.ExecutionEnvironment;
}
// Check index range
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(m_EndIndex, seq.GetCount());
ArgumentOutOfRangeException.ThrowIfLessThan(m_StartIndex, 0);
// Do backup and set values at the same time
var backups = new float[m_EndIndex - m_StartIndex + 1];
for (int index = m_StartIndex; index <= m_EndIndex; index++) {
// Fetch frame
var frame = seq.Visit(index);
// Do backup
backups[index - m_StartIndex] = frame.GetTimeDelta();
// Modify delta time
frame.SetTimeDelta(m_DeltaTime);
}
// Assign backups
m_DeltaTimesBackup = backups;
}
public void Revoke(ITasSequence seq) {
if (!IsExecuted()) {
throw OperationUtils.RevokeEnvironment;
}
// Index range is checked,
// so we directly restore backup.
for (int index = m_StartIndex; index <= m_EndIndex; index++) {
seq.Visit(index).SetTimeDelta(m_DeltaTimesBackup[index - m_StartIndex]);
}
// Clear backups
m_DeltaTimesBackup = null;
}
public int Occupation() {
return (m_EndIndex - m_StartIndex) * OperationUtils.SIZEOF_DELTA_TIME;
}
}
public class RemoveFrameOperation : ITasRevocableOperation {
public RemoveFrameOperation(int startIndex, int endIndex) {
// Check arguments
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
// Assign arguments
m_StartIndex = startIndex;
m_EndIndex = endIndex;
}
private int m_StartIndex, m_EndIndex;
private RawTasFrame[]? m_FramesBackup;
[MemberNotNullWhen(true, nameof(m_FramesBackup))]
public bool IsExecuted() {
return m_FramesBackup is not null;
}
public void Execute(ITasSequence seq) {
if (IsExecuted()) {
throw OperationUtils.ExecutionEnvironment;
}
// Check index range
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(m_EndIndex, seq.GetCount());
ArgumentOutOfRangeException.ThrowIfLessThan(m_StartIndex, 0);
// Do backups
var backups = new RawTasFrame[m_EndIndex - m_StartIndex + 1];
for (int index = m_StartIndex; index <= m_EndIndex; index++) {
seq.Visit(index).ToRawImplace(ref backups[index - m_StartIndex]);
}
// Do remove
seq.Remove(m_StartIndex, m_EndIndex);
// Assign backups
m_FramesBackup = backups;
}
public void Revoke(ITasSequence seq) {
if (!IsExecuted()) {
throw OperationUtils.RevokeEnvironment;
}
// Index range is checked,
// so we directly restore backup.
// Build iterator first
var iter = m_FramesBackup.Select((frame) => TasFrame.FromRaw(frame));
var exactSizedIter = new ExactSizeEnumerableAdapter<TasFrame>(iter, m_EndIndex - m_StartIndex + 1);
// Insert at start index
seq.Insert(m_StartIndex, exactSizedIter);
// Clear backups
m_FramesBackup = null;
}
public int Occupation() {
return (m_EndIndex - m_StartIndex) * OperationUtils.SIZEOF_FRAME;
}
}
public enum AddFrameOperationKind {
Before, After
}
public class AddFrameOperation : ITasRevocableOperation {
public AddFrameOperation(AddFrameOperationKind kind, int index, uint fps, int count) {
// Check argument
if (!FpsConverter.IsValidFps(fps)) {
throw new ArgumentOutOfRangeException(nameof(fps));
}
ArgumentOutOfRangeException.ThrowIfNegative(count);
// Assign argument
m_Kind = kind;
m_Index = index;
m_Fps = fps;
m_Count = count;
m_IsExecuted = false;
}
private AddFrameOperationKind m_Kind;
private int m_Index;
private uint m_Fps;
private int m_Count;
private bool m_IsExecuted;
public bool IsExecuted() {
return m_IsExecuted;
}
public void Execute(ITasSequence seq) {
if (IsExecuted()) {
throw OperationUtils.ExecutionEnvironment;
}
// Check arguments
// If we add before some frame, the valid index can be [0, count],
// however, if we add after some frame, the valid index is [0, count),
switch (m_Kind) {
case AddFrameOperationKind.Before:
ArgumentOutOfRangeException.ThrowIfGreaterThan(m_Index, seq.GetCount());
break;
case AddFrameOperationKind.After:
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(m_Index, seq.GetCount());
break;
default:
throw new UnreachableException("Unknown AddFrameOperationKind");
}
// Skip if count is zero.
if (m_Count != 0) {
// Prepare data builder.
var iter = Enumerable.Range(0, m_Count).Select((_) => TasFrame.FromFps(m_Fps));
var exactSizedIter = new ExactSizeEnumerableAdapter<TasFrame>(iter, m_Count);
// Compute the insert index
var index = m_Kind switch {
AddFrameOperationKind.Before => m_Index,
AddFrameOperationKind.After => m_Index + 1,
_ => throw new UnreachableException("Unknown AddFrameOperationKind"),
};
seq.Insert(index, exactSizedIter);
}
// Set status
m_IsExecuted = true;
}
public void Revoke(ITasSequence seq) {
if (!IsExecuted()) {
throw OperationUtils.RevokeEnvironment;
}
// Arguments were checked so we directly resotre them.
// If we inserted count is not zero, remove inserted frames, otherwise do nothing.
if (m_Count != 0) {
// Compute the index for removing
var index = m_Kind switch {
AddFrameOperationKind.Before => m_Index,
AddFrameOperationKind.After => m_Index + 1,
_ => throw new UnreachableException("Unknown AddFrameOperationKind"),
};
// Execute removing.
seq.Remove(index, index + m_Count - 1);
}
// Modify execution status
m_IsExecuted = false;
}
public int Occupation() {
return OperationUtils.SIZEOF_FRAME;
}
}
public enum InsertFrameOperationKind {
Before, After
}
public class InsertFrameOperation : ITasRevocableOperation {
public InsertFrameOperation(InsertFrameOperationKind kind, int index, IExactSizeEnumerable<TasFrame> frames) {
m_Kind = kind;
m_Index = index;
m_InsertedFrames = frames.Select((frame) => frame.ToRaw()).ToArray();
m_IsExecuted = false;
}
private InsertFrameOperationKind m_Kind;
private int m_Index;
private RawTasFrame[] m_InsertedFrames;
private bool m_IsExecuted;
public bool IsExecuted() {
return m_IsExecuted;
}
public void Execute(ITasSequence seq) {
if (IsExecuted()) {
throw OperationUtils.ExecutionEnvironment;
}
// Check arguments
// If we insert before some frame, the valid index can be [0, count],
// however, if we insert after some frame, the valid index is [0, count),
switch (m_Kind) {
case InsertFrameOperationKind.Before:
ArgumentOutOfRangeException.ThrowIfGreaterThan(m_Index, seq.GetCount());
break;
case InsertFrameOperationKind.After:
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(m_Index, seq.GetCount());
break;
default:
throw new UnreachableException("Unknown InsertFrameOperationKind");
}
// Skip if count is zero
var count = m_InsertedFrames.Length;
if (count != 0) {
// Prepare iterator
var iter = m_InsertedFrames.Select((frame) => TasFrame.FromRaw(frame));
var exactSizedIter = new ExactSizeEnumerableAdapter<TasFrame>(iter, count);
// Compute the insert index
var index = m_Kind switch {
InsertFrameOperationKind.Before => m_Index,
InsertFrameOperationKind.After => m_Index + 1,
_ => throw new UnreachableException("Unknown InsertFrameOperationKind"),
};
// Execute inserting.
seq.Insert(index, exactSizedIter);
}
// Set execution status
m_IsExecuted = true;
}
public void Revoke(ITasSequence seq) {
if (!IsExecuted()) {
throw OperationUtils.RevokeEnvironment;
}
// Arguments were checked so we directly restore them.
var count = m_InsertedFrames.Length;
if (count != 0) {
// Compute the index for removing
var index = m_Kind switch {
InsertFrameOperationKind.Before => m_Index,
InsertFrameOperationKind.After => m_Index + 1,
_ => throw new UnreachableException("Unknown InsertFrameOperationKind"),
};
// Execute removing.
seq.Remove(index, index + count - 1);
}
// Modify execution status
m_IsExecuted = false;
}
public int Occupation() {
return m_InsertedFrames.Length * OperationUtils.SIZEOF_FRAME;
}
}
public class ClearKeysOperation : ITasOperation {
public ClearKeysOperation() {
m_IsExecuted = false;
}
private bool m_IsExecuted;
public void Execute(ITasSequence seq) {
// Check execution status first.
if (IsExecuted()) {
throw OperationUtils.ExecutionEnvironment;
}
// Execute operation
foreach (var frame in seq) {
frame.ClearKeyPressed();
}
m_IsExecuted = true;
}
public bool IsExecuted() {
return m_IsExecuted;
}
}
public class UniformFpsOperation : ITasOperation {
public UniformFpsOperation(float deltaTime) {
m_DeltaTime = deltaTime;
m_IsExecuted = false;
}
private float m_DeltaTime;
private bool m_IsExecuted;
public void Execute(ITasSequence seq) {
// Check execution status first.
if (IsExecuted()) {
throw OperationUtils.ExecutionEnvironment;
}
// Execute operation
foreach (var frame in seq) {
frame.SetTimeDelta(m_DeltaTime);
}
m_IsExecuted = true;
}
public bool IsExecuted() {
return m_IsExecuted;
}
}
}