1169 lines
32 KiB
C#
1169 lines
32 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Drawing;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Windows.Forms;
|
|
using System.ComponentModel;
|
|
|
|
using BizHawk.Emulation.Common;
|
|
using BizHawk.Emulation.Common.IEmulatorExtensions;
|
|
using BizHawk.Emulation.Cores.Nintendo.SNES9X;
|
|
|
|
using BizHawk.Client.Common;
|
|
using BizHawk.Client.Common.MovieConversionExtensions;
|
|
|
|
using BizHawk.Client.EmuHawk.WinFormExtensions;
|
|
using BizHawk.Client.EmuHawk.ToolExtensions;
|
|
|
|
namespace BizHawk.Client.EmuHawk
|
|
{
|
|
public partial class TAStudio : ToolFormBase, IToolFormAutoConfig, IControlMainform
|
|
{
|
|
// TODO: UI flow that conveniently allows to start from savestate
|
|
public TasMovie CurrentTasMovie => Global.MovieSession.Movie as TasMovie;
|
|
private MainForm Mainform => GlobalWin.MainForm;
|
|
|
|
public bool IsInMenuLoop { get; private set; }
|
|
public string StatesPath => PathManager.MakeAbsolutePath(Global.Config.PathEntries["Global", "TAStudio states"].Path, null);
|
|
|
|
private readonly List<TasClipboardEntry> _tasClipboard = new List<TasClipboardEntry>();
|
|
private const string CursorColumnName = "CursorColumn";
|
|
private const string FrameColumnName = "FrameColumn";
|
|
private BackgroundWorker _seekBackgroundWorker;
|
|
private MovieEndAction _originalEndAction; // The movie end behavior selected by the user (that is overridden by TAStudio)
|
|
private UndoHistoryForm _undoForm;
|
|
private Timer _autosaveTimer;
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value that separates "restore last position" logic from seeking caused by navigation.
|
|
/// TASEditor never kills LastPositionFrame, and it only pauses on it, if it hasn't been greenzoned beforehand and middle mouse button was pressed.
|
|
/// </summary>
|
|
public int LastPositionFrame { get; private set; }
|
|
|
|
private Dictionary<string, string> GenerateColumnNames()
|
|
{
|
|
var lg = Global.MovieSession.LogGeneratorInstance();
|
|
lg.SetSource(Global.MovieSession.MovieControllerAdapter);
|
|
return ((Bk2LogEntryGenerator)lg).Map();
|
|
}
|
|
|
|
[ConfigPersist]
|
|
public TAStudioSettings Settings { get; set; }
|
|
|
|
public class TAStudioSettings
|
|
{
|
|
public TAStudioSettings()
|
|
{
|
|
RecentTas = new RecentFiles(8);
|
|
DrawInput = true;
|
|
AutoPause = true;
|
|
FollowCursor = true;
|
|
ScrollSpeed = 6;
|
|
FollowCursorAlwaysScroll = false;
|
|
FollowCursorScrollMethod = "near";
|
|
BranchCellHoverInterval = 1;
|
|
SeekingCutoffInterval = 2; // unused, relying on VisibleRows is smarter
|
|
AutoRestoreOnMouseUpOnly = false; // default to taseditor way, must be harmless since we suspend rerecord counting while drawing
|
|
AutosaveInterval = 120000;
|
|
AutosaveAsBk2 = false;
|
|
AutosaveAsBackupFile = false;
|
|
BackupPerFileSave = false;
|
|
SingleClickFloatEdit = false;
|
|
|
|
// default to taseditor fashion
|
|
DenoteStatesWithIcons = false;
|
|
DenoteStatesWithBGColor = true;
|
|
DenoteMarkersWithIcons = false;
|
|
DenoteMarkersWithBGColor = true;
|
|
}
|
|
|
|
public RecentFiles RecentTas { get; set; }
|
|
public bool DrawInput { get; set; }
|
|
public bool AutoPause { get; set; }
|
|
public bool AutoRestoreLastPosition { get; set; }
|
|
public bool FollowCursor { get; set; }
|
|
public bool EmptyMarkers { get; set; }
|
|
public int ScrollSpeed { get; set; }
|
|
public bool FollowCursorAlwaysScroll { get; set; }
|
|
public string FollowCursorScrollMethod { get; set; }
|
|
public int BranchCellHoverInterval { get; set; }
|
|
public int SeekingCutoffInterval { get; set; }
|
|
public bool AutoRestoreOnMouseUpOnly { get; set; }
|
|
public uint AutosaveInterval { get; set; }
|
|
public bool AutosaveAsBk2 { get; set; }
|
|
public bool AutosaveAsBackupFile { get; set; }
|
|
public bool BackupPerFileSave { get; set; }
|
|
public bool SingleClickFloatEdit { get; set; }
|
|
public bool DenoteStatesWithIcons { get; set; }
|
|
public bool DenoteStatesWithBGColor { get; set; }
|
|
public bool DenoteMarkersWithIcons { get; set; }
|
|
public bool DenoteMarkersWithBGColor { get; set; }
|
|
public int MainVerticalSplitDistance { get; set; }
|
|
public int BranchMarkerSplitDistance { get; set; }
|
|
}
|
|
|
|
#region Initializing
|
|
|
|
public TAStudio()
|
|
{
|
|
Settings = new TAStudioSettings();
|
|
InitializeComponent();
|
|
InitializeSeekWorker();
|
|
|
|
// TODO: show this at all times or hide it when saving is done?
|
|
SavingProgressBar.Visible = false;
|
|
|
|
WantsToControlStopMovie = true;
|
|
WantsToControlRestartMovie = true;
|
|
TasPlaybackBox.Tastudio = this;
|
|
MarkerControl.Tastudio = this;
|
|
BookMarkControl.Tastudio = this;
|
|
TasView.QueryItemText += TasView_QueryItemText;
|
|
TasView.QueryItemBkColor += TasView_QueryItemBkColor;
|
|
TasView.QueryRowBkColor += TasView_QueryRowBkColor;
|
|
TasView.QueryItemIcon += TasView_QueryItemIcon;
|
|
TasView.QueryFrameLag += TasView_QueryFrameLag;
|
|
TasView.PointedCellChanged += TasView_PointedCellChanged;
|
|
TasView.MultiSelect = true;
|
|
TasView.MaxCharactersInHorizontal = 1;
|
|
LastPositionFrame = -1;
|
|
}
|
|
|
|
private void AutosaveTimerEventProcessor(object sender, EventArgs e)
|
|
{
|
|
if (CurrentTasMovie == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!CurrentTasMovie.Changes || Settings.AutosaveInterval == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Settings.AutosaveAsBackupFile)
|
|
{
|
|
if (Settings.AutosaveAsBk2)
|
|
{
|
|
SaveBk2BackupMenuItem_Click(sender, e);
|
|
}
|
|
else
|
|
{
|
|
SaveBackupMenuItem_Click(sender, e);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Settings.AutosaveAsBk2)
|
|
{
|
|
ToBk2MenuItem_Click(sender, e);
|
|
}
|
|
else
|
|
{
|
|
SaveTas(sender, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void InitializeSeekWorker()
|
|
{
|
|
if (_seekBackgroundWorker != null)
|
|
{
|
|
_seekBackgroundWorker.Dispose();
|
|
_seekBackgroundWorker = null; // Idk if this line is even useful.
|
|
}
|
|
|
|
_seekBackgroundWorker = new BackgroundWorker
|
|
{
|
|
WorkerReportsProgress = true,
|
|
WorkerSupportsCancellation = true
|
|
};
|
|
|
|
_seekBackgroundWorker.DoWork += (s, e) =>
|
|
{
|
|
this.Invoke(() => MessageStatusLabel.Text = "Seeking...");
|
|
this.Invoke(() => SavingProgressBar.Visible = true);
|
|
for (;;)
|
|
{
|
|
if (_seekBackgroundWorker.CancellationPending || !this.IsHandleCreated)
|
|
{
|
|
e.Cancel = true;
|
|
break;
|
|
}
|
|
|
|
int diff = Emulator.Frame - _seekStartFrame.Value;
|
|
int unit = Mainform.PauseOnFrame.Value - _seekStartFrame.Value;
|
|
double progress = 0;
|
|
|
|
if (diff != 0 && unit != 0)
|
|
{
|
|
progress = (double)100d / unit * diff;
|
|
}
|
|
|
|
if (progress < 0)
|
|
{
|
|
progress = 0;
|
|
}
|
|
|
|
_seekBackgroundWorker.ReportProgress((int)progress);
|
|
System.Threading.Thread.Sleep(1);
|
|
}
|
|
};
|
|
|
|
_seekBackgroundWorker.ProgressChanged += (s, e) =>
|
|
{
|
|
this.Invoke(() => SavingProgressBar.Value = e.ProgressPercentage);
|
|
};
|
|
|
|
_seekBackgroundWorker.RunWorkerCompleted += (s, e) =>
|
|
{
|
|
this.Invoke(() => SavingProgressBar.Visible = false);
|
|
this.Invoke(() => MessageStatusLabel.Text = "");
|
|
InitializeSeekWorker(); // Required, or it will error when trying to report progress again.
|
|
};
|
|
}
|
|
|
|
private bool _initialized;
|
|
private void Tastudio_Load(object sender, EventArgs e)
|
|
{
|
|
if (!InitializeOnLoad())
|
|
{
|
|
Close();
|
|
DialogResult = DialogResult.Cancel;
|
|
return;
|
|
}
|
|
|
|
SetColumnsFromCurrentStickies();
|
|
|
|
if (VersionInfo.DeveloperBuild)
|
|
{
|
|
RightClickMenu.Items.AddRange(TasView.GenerateContextMenuItems().ToArray());
|
|
|
|
RightClickMenu.Items
|
|
.OfType<ToolStripMenuItem>()
|
|
.First(t => t.Name == "RotateMenuItem")
|
|
.Click += (o, ov) =>
|
|
{
|
|
CurrentTasMovie.FlagChanges();
|
|
};
|
|
}
|
|
|
|
TasView.InputPaintingMode = Settings.DrawInput;
|
|
TasView.ScrollSpeed = Settings.ScrollSpeed;
|
|
TasView.AlwaysScroll = Settings.FollowCursorAlwaysScroll;
|
|
TasView.ScrollMethod = Settings.FollowCursorScrollMethod;
|
|
TasView.SeekingCutoffInterval = Settings.SeekingCutoffInterval;
|
|
BookMarkControl.HoverInterval = Settings.BranchCellHoverInterval;
|
|
|
|
_autosaveTimer = new Timer(components);
|
|
_autosaveTimer.Tick += AutosaveTimerEventProcessor;
|
|
if (Settings.AutosaveInterval > 0)
|
|
{
|
|
_autosaveTimer.Interval = (int)Settings.AutosaveInterval;
|
|
_autosaveTimer.Start();
|
|
}
|
|
|
|
// Remembering Split container logic
|
|
int defaultMainSplitDistance = MainVertialSplit.SplitterDistance;
|
|
int defaultBranchMarkerSplitDistance = BranchesMarkersSplit.SplitterDistance;
|
|
|
|
ToolStripMenuItem restoreDefaults = TASMenu.Items
|
|
.OfType<ToolStripMenuItem>()
|
|
.Single(t => t.Name == "SettingsSubMenu")
|
|
.DropDownItems
|
|
.OfType<ToolStripMenuItem>()
|
|
.Single(t => t.Text == "Restore &Defaults");
|
|
|
|
restoreDefaults.Click += (o, ev) =>
|
|
{
|
|
MainVertialSplit.SplitterDistance = defaultMainSplitDistance;
|
|
BranchesMarkersSplit.SplitterDistance = defaultBranchMarkerSplitDistance;
|
|
};
|
|
|
|
if (Settings.MainVerticalSplitDistance > 0)
|
|
{
|
|
MainVertialSplit.SplitterDistance = Settings.MainVerticalSplitDistance;
|
|
}
|
|
|
|
if (Settings.BranchMarkerSplitDistance > 0)
|
|
{
|
|
BranchesMarkersSplit.SplitterDistance = Settings.BranchMarkerSplitDistance;
|
|
}
|
|
|
|
RefreshDialog();
|
|
_initialized = true;
|
|
}
|
|
|
|
private bool InitializeOnLoad()
|
|
{
|
|
Mainform.PauseOnFrame = null;
|
|
Mainform.PauseEmulator();
|
|
|
|
// Start Scenario 0: snes9x needs a nag (copied from RecordMovieMenuItem_Click())
|
|
if (Emulator is Snes9x)
|
|
{
|
|
var box = new CustomControls.MsgBox(
|
|
"While the Snes9x core is faster, it is not nearly as accurate as bsnes. \nIt is recommended that you switch to the bsnes core for movie recording\nSwitch to bsnes?",
|
|
"Accuracy Warning",
|
|
MessageBoxIcon.Warning);
|
|
|
|
box.SetButtons(
|
|
new[] { "Switch", "Cancel" },
|
|
new[] { DialogResult.Yes, DialogResult.Cancel });
|
|
|
|
box.MaximumSize = new Size(475, 350);
|
|
box.SetMessageToAutoSize();
|
|
var result = box.ShowDialog();
|
|
|
|
if (result == DialogResult.Yes)
|
|
{
|
|
Global.Config.SNES_InSnes9x = false;
|
|
Mainform.RebootCore();
|
|
}
|
|
else if (result == DialogResult.Cancel)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Start Scenario 1: A regular movie is active
|
|
if (Global.MovieSession.Movie.IsActive && !(Global.MovieSession.Movie is TasMovie))
|
|
{
|
|
var result = MessageBox.Show("In order to use Tastudio, a new project must be created from the current movie\nThe current movie will be saved and closed, and a new project file will be created\nProceed?", "Convert movie", MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
|
|
if (result == DialogResult.OK)
|
|
{
|
|
ConvertCurrentMovieToTasproj();
|
|
StartNewMovieWrapper(false);
|
|
SetUpColumns();
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Start Scenario 2: A tasproj is already active
|
|
else if (Global.MovieSession.Movie.IsActive && Global.MovieSession.Movie is TasMovie)
|
|
{
|
|
bool result = LoadFile(new FileInfo(CurrentTasMovie.Filename), gotoFrame: Emulator.Frame);
|
|
if (!result)
|
|
{
|
|
TasView.AllColumns.Clear();
|
|
StartNewTasMovie();
|
|
}
|
|
}
|
|
|
|
// Start Scenario 3: No movie, but user wants to autload their last project
|
|
else if (Settings.RecentTas.AutoLoad && !string.IsNullOrEmpty(Settings.RecentTas.MostRecent))
|
|
{
|
|
bool result = LoadFile(new FileInfo(Settings.RecentTas.MostRecent));
|
|
if (!result)
|
|
{
|
|
TasView.AllColumns.Clear();
|
|
StartNewTasMovie();
|
|
}
|
|
}
|
|
|
|
// Start Scenario 4: No movie, default behavior of engaging tastudio with a new default project
|
|
else
|
|
{
|
|
StartNewTasMovie();
|
|
}
|
|
|
|
EngageTastudio();
|
|
return true;
|
|
}
|
|
|
|
private void SetTasMovieCallbacks(TasMovie movie = null)
|
|
{
|
|
if (movie == null)
|
|
{
|
|
movie = CurrentTasMovie;
|
|
}
|
|
|
|
movie.ClientSettingsForSave = ClientSettingsForSave;
|
|
movie.GetClientSettingsOnLoad = GetClientSettingsOnLoad;
|
|
}
|
|
|
|
private string ClientSettingsForSave()
|
|
{
|
|
return TasView.UserSettingsSerialized();
|
|
}
|
|
|
|
private void GetClientSettingsOnLoad(string settingsJson)
|
|
{
|
|
TasView.LoadSettingsSerialized(settingsJson);
|
|
}
|
|
|
|
private void SetUpColumns()
|
|
{
|
|
TasView.AllColumns.Clear();
|
|
AddColumn(CursorColumnName, "", 18);
|
|
AddColumn(FrameColumnName, "Frame#", 68);
|
|
|
|
var columnNames = GenerateColumnNames();
|
|
foreach (var kvp in columnNames)
|
|
{
|
|
InputRoll.RollColumn.InputType type;
|
|
int digits;
|
|
if (Global.MovieSession.MovieControllerAdapter.Definition.FloatControls.Contains(kvp.Key))
|
|
{
|
|
ControllerDefinition.FloatRange range = Global.MovieSession.MovieControllerAdapter.Definition.FloatRanges
|
|
[Global.MovieSession.MovieControllerAdapter.Definition.FloatControls.IndexOf(kvp.Key)];
|
|
type = InputRoll.RollColumn.InputType.Float;
|
|
digits = Math.Max(kvp.Value.Length, range.MaxDigits());
|
|
}
|
|
else
|
|
{
|
|
type = InputRoll.RollColumn.InputType.Boolean;
|
|
digits = kvp.Value.Length;
|
|
}
|
|
|
|
AddColumn(kvp.Key, kvp.Value, (digits * 6) + 14, type); // magic numbers reused in EditBranchTextPopUp()
|
|
}
|
|
|
|
var columnsToHide = TasView.AllColumns
|
|
.Where(c =>
|
|
// todo: make a proper user editable list?
|
|
c.Name == "Power"
|
|
|| c.Name == "Reset"
|
|
|| c.Name == "Light Sensor"
|
|
|| c.Name == "Open"
|
|
|| c.Name == "Close"
|
|
|| c.Name == "Disc Select"
|
|
|| c.Name.StartsWith("Tilt")
|
|
|| c.Name.StartsWith("Key "));
|
|
|
|
foreach (var column in columnsToHide)
|
|
{
|
|
column.Visible = false;
|
|
}
|
|
|
|
TasView.AllColumns.ColumnsChanged();
|
|
|
|
// Patterns
|
|
int bStart = 0;
|
|
int fStart = 0;
|
|
if (BoolPatterns == null)
|
|
{
|
|
BoolPatterns = new AutoPatternBool[ControllerType.BoolButtons.Count + 2];
|
|
FloatPatterns = new AutoPatternFloat[ControllerType.FloatControls.Count + 2];
|
|
}
|
|
else
|
|
{
|
|
bStart = BoolPatterns.Length - 2;
|
|
fStart = FloatPatterns.Length - 2;
|
|
Array.Resize(ref BoolPatterns, ControllerType.BoolButtons.Count + 2);
|
|
Array.Resize(ref FloatPatterns, ControllerType.FloatControls.Count + 2);
|
|
}
|
|
|
|
for (int i = bStart; i < BoolPatterns.Length - 2; i++)
|
|
{
|
|
BoolPatterns[i] = new AutoPatternBool(1, 1);
|
|
}
|
|
|
|
BoolPatterns[BoolPatterns.Length - 2] = new AutoPatternBool(1, 0);
|
|
BoolPatterns[BoolPatterns.Length - 1] = new AutoPatternBool(
|
|
Global.Config.AutofireOn, Global.Config.AutofireOff);
|
|
|
|
for (int i = fStart; i < FloatPatterns.Length - 2; i++)
|
|
{
|
|
FloatPatterns[i] = new AutoPatternFloat(new[] { 1f });
|
|
}
|
|
|
|
FloatPatterns[FloatPatterns.Length - 2] = new AutoPatternFloat(new[] { 1f });
|
|
FloatPatterns[FloatPatterns.Length - 1] = new AutoPatternFloat(
|
|
1f, Global.Config.AutofireOn, 0f, Global.Config.AutofireOff);
|
|
|
|
SetUpToolStripColumns();
|
|
}
|
|
|
|
private void AddColumn(string columnName, string columnText, int columnWidth, InputRoll.RollColumn.InputType columnType = InputRoll.RollColumn.InputType.Boolean)
|
|
{
|
|
if (TasView.AllColumns[columnName] == null)
|
|
{
|
|
var column = new InputRoll.RollColumn
|
|
{
|
|
Name = columnName,
|
|
Text = columnText,
|
|
Width = columnWidth,
|
|
Type = columnType
|
|
};
|
|
|
|
TasView.AllColumns.Add(column);
|
|
}
|
|
}
|
|
|
|
private void EngageTastudio()
|
|
{
|
|
GlobalWin.OSD.AddMessage("TAStudio engaged");
|
|
SetTasMovieCallbacks();
|
|
SetTextProperty();
|
|
Mainform.RelinquishControl(this);
|
|
_originalEndAction = Global.Config.MovieEndAction;
|
|
Mainform.ClearRewindData();
|
|
Global.Config.MovieEndAction = MovieEndAction.Record;
|
|
Mainform.SetMainformMovieInfo();
|
|
Global.MovieSession.ReadOnly = true;
|
|
SetSplicer();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Loading
|
|
|
|
private void ConvertCurrentMovieToTasproj()
|
|
{
|
|
Global.MovieSession.Movie.Save();
|
|
Global.MovieSession.Movie = Global.MovieSession.Movie.ToTasMovie();
|
|
Global.MovieSession.Movie.Save();
|
|
Global.MovieSession.Movie.SwitchToPlay();
|
|
Settings.RecentTas.Add(Global.MovieSession.Movie.Filename);
|
|
}
|
|
|
|
private bool LoadFile(FileInfo file, bool startsFromSavestate = false, int gotoFrame = 0)
|
|
{
|
|
if (!file.Exists)
|
|
{
|
|
Settings.RecentTas.HandleLoadError(file.FullName);
|
|
return false;
|
|
}
|
|
|
|
TasMovie newMovie = new TasMovie(startsFromSavestate, _seekBackgroundWorker);
|
|
newMovie.TasStateManager.InvalidateCallback = GreenzoneInvalidated;
|
|
newMovie.Filename = file.FullName;
|
|
|
|
if (!HandleMovieLoadStuff(newMovie))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Settings.RecentTas.Add(newMovie.Filename); // only add if it did load
|
|
|
|
if (startsFromSavestate)
|
|
{
|
|
GoToFrame(0);
|
|
}
|
|
else if (gotoFrame > 0)
|
|
{
|
|
GoToFrame(gotoFrame);
|
|
}
|
|
else
|
|
{
|
|
GoToFrame(CurrentTasMovie.Session.CurrentFrame);
|
|
}
|
|
|
|
if (TasView.AllColumns.Count == 0 || file.Extension != "." + TasMovie.Extension)
|
|
{
|
|
SetUpColumns();
|
|
}
|
|
else
|
|
{
|
|
SetUpToolStripColumns();
|
|
}
|
|
|
|
CurrentTasMovie.PropertyChanged += TasMovie_OnPropertyChanged;
|
|
CurrentTasMovie.CurrentBranch = CurrentTasMovie.Session.CurrentBranch;
|
|
BookMarkControl.UpdateTextColumnWidth();
|
|
|
|
// clear all selections
|
|
TasView.DeselectAll();
|
|
BookMarkControl.Restart();
|
|
MarkerControl.Restart();
|
|
|
|
RefreshDialog();
|
|
return true;
|
|
}
|
|
|
|
private void StartNewTasMovie()
|
|
{
|
|
if (AskSaveChanges())
|
|
{
|
|
Global.MovieSession.Movie = new TasMovie(false, _seekBackgroundWorker);
|
|
var stateManager = ((TasMovie)Global.MovieSession.Movie).TasStateManager;
|
|
stateManager.MountWriteAccess();
|
|
stateManager.InvalidateCallback = GreenzoneInvalidated;
|
|
CurrentTasMovie.PropertyChanged += TasMovie_OnPropertyChanged;
|
|
CurrentTasMovie.Filename = DefaultTasProjName(); // TODO don't do this, take over any mainform actions that can crash without a filename
|
|
CurrentTasMovie.PopulateWithDefaultHeaderValues();
|
|
SetTasMovieCallbacks();
|
|
CurrentTasMovie.ClearChanges(); // Don't ask to save changes here.
|
|
HandleMovieLoadStuff();
|
|
CurrentTasMovie.TasStateManager.Capture(); // Capture frame 0 always.
|
|
|
|
// clear all selections
|
|
TasView.DeselectAll();
|
|
BookMarkControl.Restart();
|
|
MarkerControl.Restart();
|
|
SetUpColumns();
|
|
RefreshDialog();
|
|
}
|
|
}
|
|
|
|
private bool HandleMovieLoadStuff(TasMovie movie = null)
|
|
{
|
|
bool result;
|
|
WantsToControlStopMovie = false;
|
|
|
|
if (movie == null)
|
|
{
|
|
movie = CurrentTasMovie;
|
|
result = StartNewMovieWrapper(movie.InputLogLength == 0, movie);
|
|
}
|
|
else
|
|
{
|
|
result = StartNewMovieWrapper(false, movie);
|
|
}
|
|
|
|
if (!result)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
WantsToControlStopMovie = true;
|
|
|
|
CurrentTasMovie.ChangeLog.ClearLog();
|
|
CurrentTasMovie.ClearChanges();
|
|
|
|
SetTextProperty();
|
|
MessageStatusLabel.Text = Path.GetFileName(CurrentTasMovie.Filename) + " loaded.";
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool StartNewMovieWrapper(bool record, IMovie movie = null)
|
|
{
|
|
_initializing = true;
|
|
|
|
if (movie == null)
|
|
{
|
|
movie = CurrentTasMovie;
|
|
}
|
|
|
|
SetTasMovieCallbacks(movie as TasMovie);
|
|
|
|
bool result = Mainform.StartNewMovie(movie, record);
|
|
if (result)
|
|
{
|
|
CurrentTasMovie.TasStateManager.Capture(); // Capture frame 0 always.
|
|
BookMarkControl.UpdateTextColumnWidth();
|
|
}
|
|
|
|
TastudioPlayMode();
|
|
|
|
_initializing = false;
|
|
|
|
return result;
|
|
}
|
|
|
|
private void DummyLoadProject(string path)
|
|
{
|
|
if (AskSaveChanges())
|
|
{
|
|
LoadFile(new FileInfo(path));
|
|
}
|
|
}
|
|
|
|
private void DummyLoadMacro(string path)
|
|
{
|
|
if (!TasView.AnyRowsSelected)
|
|
{
|
|
return;
|
|
}
|
|
|
|
MovieZone loadZone = new MovieZone(path)
|
|
{
|
|
Start = TasView.FirstSelectedIndex.Value
|
|
};
|
|
loadZone.PlaceZone(CurrentTasMovie);
|
|
}
|
|
|
|
private void SetColumnsFromCurrentStickies()
|
|
{
|
|
foreach (var column in TasView.VisibleColumns)
|
|
{
|
|
if (Global.StickyXORAdapter.IsSticky(column.Name))
|
|
{
|
|
column.Emphasis = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
private void TastudioPlayMode()
|
|
{
|
|
TasPlaybackBox.RecordingMode = false;
|
|
}
|
|
|
|
private void TastudioRecordMode()
|
|
{
|
|
TasPlaybackBox.RecordingMode = true;
|
|
}
|
|
|
|
private void TastudioStopMovie()
|
|
{
|
|
Global.MovieSession.StopMovie(false);
|
|
Mainform.SetMainformMovieInfo();
|
|
}
|
|
|
|
private void DisengageTastudio()
|
|
{
|
|
Mainform.PauseOnFrame = null;
|
|
GlobalWin.OSD.AddMessage("TAStudio disengaged");
|
|
Global.MovieSession.Movie = MovieService.DefaultInstance;
|
|
Mainform.TakeBackControl();
|
|
Global.Config.MovieEndAction = _originalEndAction;
|
|
Mainform.SetMainformMovieInfo();
|
|
|
|
// Do not keep TAStudio's disk save states.
|
|
// if (Directory.Exists(statesPath)) Directory.Delete(statesPath, true);
|
|
// TODO - do we need to dispose something here instead?
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used when starting a new project
|
|
/// </summary>
|
|
private static string DefaultTasProjName()
|
|
{
|
|
return Path.Combine(
|
|
PathManager.MakeAbsolutePath(Global.Config.PathEntries.MoviesPathFragment, null),
|
|
TasMovie.DefaultProjectName + "." + TasMovie.Extension);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used for things like SaveFile dialogs to suggest a name to the user
|
|
/// </summary>
|
|
private static string SuggestedTasProjName()
|
|
{
|
|
return Path.Combine(
|
|
PathManager.MakeAbsolutePath(Global.Config.PathEntries.MoviesPathFragment, null),
|
|
PathManager.FilesystemSafeName(Global.Game) + "." + TasMovie.Extension);
|
|
}
|
|
|
|
private void SetTextProperty()
|
|
{
|
|
var text = "TAStudio";
|
|
if (CurrentTasMovie != null)
|
|
{
|
|
text += " - " + CurrentTasMovie.Name + (CurrentTasMovie.Changes ? "*" : "");
|
|
}
|
|
|
|
if (InvokeRequired)
|
|
{
|
|
this.Invoke(() => Text = text);
|
|
}
|
|
else
|
|
{
|
|
Text = text;
|
|
}
|
|
}
|
|
|
|
public IEnumerable<int> GetSelection()
|
|
{
|
|
return TasView.SelectedRows;
|
|
}
|
|
|
|
public void RefreshDialog(bool refreshTasView = true)
|
|
{
|
|
if (_exiting)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (refreshTasView)
|
|
{
|
|
RefreshTasView();
|
|
}
|
|
|
|
MarkerControl?.UpdateValues();
|
|
|
|
BookMarkControl?.UpdateValues();
|
|
|
|
if (_undoForm != null && !_undoForm.IsDisposed)
|
|
{
|
|
_undoForm.UpdateValues();
|
|
}
|
|
}
|
|
|
|
private void RefreshTasView()
|
|
{
|
|
CurrentTasMovie.UseInputCache = true;
|
|
if (TasView.RowCount != CurrentTasMovie.InputLogLength + 1)
|
|
{
|
|
TasView.RowCount = CurrentTasMovie.InputLogLength + 1;
|
|
}
|
|
|
|
TasView.Refresh();
|
|
|
|
CurrentTasMovie.FlushInputCache();
|
|
CurrentTasMovie.UseInputCache = false;
|
|
|
|
_lastRefresh = Emulator.Frame;
|
|
}
|
|
|
|
private void DoAutoRestore()
|
|
{
|
|
if (Settings.AutoRestoreLastPosition && LastPositionFrame != -1)
|
|
{
|
|
if (LastPositionFrame > Emulator.Frame) // Don't unpause if we are already on the desired frame, else runaway seek
|
|
{
|
|
StartSeeking(LastPositionFrame);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (_autoRestorePaused.HasValue && !_autoRestorePaused.Value)
|
|
{
|
|
// this happens when we're holding the left button while unpaused - view scrolls down, new input gets drawn, seek pauses
|
|
Mainform.UnpauseEmulator();
|
|
}
|
|
|
|
_autoRestorePaused = null;
|
|
}
|
|
}
|
|
|
|
private void StartAtNearestFrameAndEmulate(int frame, bool fromLua, bool fromRewinding)
|
|
{
|
|
if (frame == Emulator.Frame)
|
|
return;
|
|
|
|
_unpauseAfterSeeking = (fromRewinding || WasRecording) && !Mainform.EmulatorPaused;
|
|
TastudioPlayMode();
|
|
KeyValuePair<int, byte[]> closestState = CurrentTasMovie.TasStateManager.GetStateClosestToFrame(frame);
|
|
if (closestState.Value != null && (frame < Emulator.Frame || closestState.Key > Emulator.Frame))
|
|
{
|
|
LoadState(closestState);
|
|
}
|
|
|
|
if (fromLua)
|
|
{
|
|
bool wasPaused = Mainform.EmulatorPaused;
|
|
|
|
// why not use this? because I'm not letting the form freely run. it all has to be under this loop.
|
|
// i could use this and then poll StepRunLoop_Core() repeatedly, but.. that's basically what I'm doing
|
|
// PauseOnFrame = frame;
|
|
|
|
// can't re-enter lua while doing this
|
|
Mainform.SuppressLua = true;
|
|
while (Emulator.Frame != frame)
|
|
{
|
|
Mainform.SeekFrameAdvance();
|
|
}
|
|
|
|
Mainform.SuppressLua = false;
|
|
|
|
if (!wasPaused)
|
|
{
|
|
Mainform.UnpauseEmulator();
|
|
}
|
|
|
|
// lua botting users will want to re-activate record mode automatically -- it should be like nothing ever happened
|
|
if (WasRecording)
|
|
{
|
|
TastudioRecordMode();
|
|
}
|
|
|
|
// now the next section won't happen since we're at the right spot
|
|
}
|
|
|
|
// frame == Emulator.Frame when frame == 0
|
|
if (frame > Emulator.Frame)
|
|
{
|
|
// make seek frame keep up with emulation on fast scrolls
|
|
if (Mainform.EmulatorPaused || Mainform.IsSeeking || fromRewinding || WasRecording)
|
|
{
|
|
StartSeeking(frame);
|
|
}
|
|
else
|
|
{
|
|
// GUI users may want to be protected from clobbering their video when skipping around...
|
|
// well, users who are rewinding arent. (that gets done through the seeking system in the call above)
|
|
// users who are clicking around.. I dont know.
|
|
}
|
|
}
|
|
}
|
|
|
|
public void LoadState(KeyValuePair<int, byte[]> state)
|
|
{
|
|
StatableEmulator.LoadStateBinary(new BinaryReader(new MemoryStream(state.Value.ToArray())));
|
|
|
|
if (state.Key == 0 && CurrentTasMovie.StartsFromSavestate)
|
|
{
|
|
Emulator.ResetCounters();
|
|
}
|
|
|
|
_hackyDontUpdate = true;
|
|
GlobalWin.Tools.UpdateBefore();
|
|
GlobalWin.Tools.UpdateAfter();
|
|
_hackyDontUpdate = false;
|
|
}
|
|
|
|
public void AddBranchExternal()
|
|
{
|
|
BookMarkControl.AddBranchExternal();
|
|
}
|
|
|
|
public void RemoveBranchExtrenal()
|
|
{
|
|
BookMarkControl.RemoveBranchExtrenal();
|
|
}
|
|
|
|
private void UpdateOtherTools() // a hack probably, surely there is a better way to do this
|
|
{
|
|
_hackyDontUpdate = true;
|
|
GlobalWin.Tools.UpdateBefore();
|
|
GlobalWin.Tools.UpdateAfter();
|
|
_hackyDontUpdate = false;
|
|
}
|
|
|
|
public void TogglePause()
|
|
{
|
|
Mainform.TogglePause();
|
|
}
|
|
|
|
private void SetSplicer()
|
|
{
|
|
// TODO: columns selected?
|
|
SplicerStatusLabel.Text =
|
|
"Selected: " + TasView.SelectedRows.Count() + " frame" +
|
|
(TasView.SelectedRows.Count() == 1 ? "" : "s") +
|
|
", Clipboard: " + (_tasClipboard.Any() ? _tasClipboard.Count + " frame" +
|
|
(_tasClipboard.Count == 1 ? "" : "s") : "empty");
|
|
}
|
|
|
|
private void UpdateChangesIndicator()
|
|
{
|
|
// TODO
|
|
}
|
|
|
|
private void DoTriggeredAutoRestoreIfNeeded()
|
|
{
|
|
if (_triggerAutoRestore)
|
|
{
|
|
DoAutoRestore();
|
|
|
|
_triggerAutoRestore = false;
|
|
_autoRestorePaused = null;
|
|
}
|
|
}
|
|
|
|
public void InsertNumFrames(int insertionFrame, int numberOfFrames)
|
|
{
|
|
if (insertionFrame < CurrentTasMovie.InputLogLength)
|
|
{
|
|
bool needsToRollback = TasView.FirstSelectedIndex < Emulator.Frame;
|
|
|
|
CurrentTasMovie.InsertEmptyFrame(insertionFrame, numberOfFrames);
|
|
|
|
if (needsToRollback)
|
|
{
|
|
GoToLastEmulatedFrameIfNecessary(insertionFrame);
|
|
DoAutoRestore();
|
|
}
|
|
else
|
|
{
|
|
RefreshDialog();
|
|
}
|
|
}
|
|
}
|
|
|
|
public void DeleteFrames(int beginningFrame, int numberofFrames)
|
|
{
|
|
if (beginningFrame < CurrentTasMovie.InputLogLength)
|
|
{
|
|
int[] framesToRemove = Enumerable.Range(beginningFrame, numberofFrames).ToArray();
|
|
CurrentTasMovie.RemoveFrames(framesToRemove);
|
|
SetSplicer();
|
|
|
|
var needsToRollback = beginningFrame < Emulator.Frame;
|
|
if (needsToRollback)
|
|
{
|
|
GoToLastEmulatedFrameIfNecessary(beginningFrame);
|
|
DoAutoRestore();
|
|
}
|
|
else
|
|
{
|
|
RefreshDialog();
|
|
}
|
|
}
|
|
}
|
|
|
|
#region Dialog Events
|
|
|
|
private void Tastudio_Closing(object sender, FormClosingEventArgs e)
|
|
{
|
|
if (!_initialized)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_exiting = true;
|
|
|
|
if (AskSaveChanges())
|
|
{
|
|
WantsToControlStopMovie = false;
|
|
TastudioStopMovie();
|
|
DisengageTastudio();
|
|
}
|
|
else
|
|
{
|
|
e.Cancel = true;
|
|
_exiting = false;
|
|
}
|
|
|
|
_undoForm?.Close();
|
|
}
|
|
|
|
/// <summary>
|
|
/// This method is called every time the Changes property is toggled on a <see cref="TasMovie"/> instance.
|
|
/// </summary>
|
|
private void TasMovie_OnPropertyChanged(object sender, PropertyChangedEventArgs e)
|
|
{
|
|
SetTextProperty();
|
|
}
|
|
|
|
private void LuaConsole_DragEnter(object sender, DragEventArgs e)
|
|
{
|
|
e.Effect = e.Data.GetDataPresent(DataFormats.FileDrop) ? DragDropEffects.Copy : DragDropEffects.None;
|
|
}
|
|
|
|
private void TAStudio_DragDrop(object sender, DragEventArgs e)
|
|
{
|
|
if (!AskSaveChanges())
|
|
{
|
|
return;
|
|
}
|
|
|
|
var filePaths = (string[])e.Data.GetData(DataFormats.FileDrop);
|
|
if (Path.GetExtension(filePaths[0]) == "." + TasMovie.Extension)
|
|
{
|
|
FileInfo file = new FileInfo(filePaths[0]);
|
|
if (file.Exists)
|
|
{
|
|
LoadFile(file);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TAStudio_MouseLeave(object sender, EventArgs e)
|
|
{
|
|
DoTriggeredAutoRestoreIfNeeded();
|
|
}
|
|
|
|
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
|
|
{
|
|
if (keyData == Keys.Tab
|
|
|| keyData == (Keys.Shift | Keys.Tab)
|
|
|| keyData == Keys.Space)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return base.ProcessCmdKey(ref msg, keyData);
|
|
}
|
|
|
|
#endregion
|
|
|
|
private bool AutoAdjustInput()
|
|
{
|
|
TasMovieRecord lagLog = CurrentTasMovie[Emulator.Frame - 1]; // Minus one because get frame is +1;
|
|
bool isLag = Emulator.AsInputPollable().IsLagFrame;
|
|
|
|
if (lagLog.WasLagged.HasValue)
|
|
{
|
|
if (lagLog.WasLagged.Value && !isLag)
|
|
{
|
|
// Deleting this frame requires rewinding a frame.
|
|
CurrentTasMovie.ChangeLog.AddInputBind(Emulator.Frame - 1, true, "Bind Input; Delete " + (Emulator.Frame - 1));
|
|
bool wasRecording = CurrentTasMovie.ChangeLog.IsRecording;
|
|
CurrentTasMovie.ChangeLog.IsRecording = false;
|
|
|
|
CurrentTasMovie.RemoveFrame(Emulator.Frame - 1);
|
|
CurrentTasMovie.RemoveLagHistory(Emulator.Frame); // Removes from WasLag
|
|
|
|
CurrentTasMovie.ChangeLog.IsRecording = wasRecording;
|
|
GoToFrame(Emulator.Frame - 1);
|
|
return true;
|
|
}
|
|
else if (!lagLog.WasLagged.Value && isLag)
|
|
{ // (it shouldn't need to rewind, since the inserted input wasn't polled)
|
|
CurrentTasMovie.ChangeLog.AddInputBind(Emulator.Frame - 1, false, "Bind Input; Insert " + (Emulator.Frame - 1));
|
|
bool wasRecording = CurrentTasMovie.ChangeLog.IsRecording;
|
|
CurrentTasMovie.ChangeLog.IsRecording = false;
|
|
|
|
CurrentTasMovie.InsertInput(Emulator.Frame - 1, CurrentTasMovie.GetInputLogEntry(Emulator.Frame - 2));
|
|
CurrentTasMovie.InsertLagHistory(Emulator.Frame, true);
|
|
|
|
CurrentTasMovie.ChangeLog.IsRecording = wasRecording;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void TAStudio_KeyDown(object sender, KeyEventArgs e)
|
|
{
|
|
//if (e.KeyCode == Keys.F)
|
|
// TasPlaybackBox.FollowCursor ^= true;
|
|
}
|
|
|
|
private void MainVertialSplit_SplitterMoved(object sender, SplitterEventArgs e)
|
|
{
|
|
Settings.MainVerticalSplitDistance = MainVertialSplit.SplitterDistance;
|
|
}
|
|
|
|
private void BranchesMarkersSplit_SplitterMoved(object sender, SplitterEventArgs e)
|
|
{
|
|
Settings.BranchMarkerSplitDistance = BranchesMarkersSplit.SplitterDistance;
|
|
}
|
|
|
|
private void TasView_CellDropped(object sender, InputRoll.CellEventArgs e)
|
|
{
|
|
if (e.NewCell?.RowIndex != null && !CurrentTasMovie.Markers.IsMarker(e.NewCell.RowIndex.Value))
|
|
{
|
|
var currentMarker = CurrentTasMovie.Markers.Single(m => m.Frame == e.OldCell.RowIndex.Value);
|
|
int newFrame = e.NewCell.RowIndex.Value;
|
|
var newMarker = new TasMovieMarker(newFrame, currentMarker.Message);
|
|
CurrentTasMovie.Markers.Remove(currentMarker);
|
|
CurrentTasMovie.Markers.Add(newMarker);
|
|
RefreshDialog();
|
|
}
|
|
}
|
|
|
|
private void NewFromSubMenu_DropDownOpened(object sender, EventArgs e)
|
|
{
|
|
NewFromNowMenuItem.Enabled =
|
|
CurrentTasMovie.InputLogLength > 0
|
|
&& !CurrentTasMovie.StartsFromSaveRam;
|
|
|
|
NewFromCurrentSaveRamMenuItem.Enabled =
|
|
CurrentTasMovie.InputLogLength > 0
|
|
&& SaveRamEmulator != null;
|
|
}
|
|
|
|
private void TASMenu_MenuActivate(object sender, EventArgs e)
|
|
{
|
|
IsInMenuLoop = true;
|
|
}
|
|
|
|
private void TASMenu_MenuDeactivate(object sender, EventArgs e)
|
|
{
|
|
IsInMenuLoop = false;
|
|
}
|
|
|
|
// Stupid designer
|
|
protected void DragEnterWrapper(object sender, DragEventArgs e)
|
|
{
|
|
base.GenericDragEnter(sender, e);
|
|
}
|
|
|
|
private void TasPlaybackBox_Load(object sender, EventArgs e)
|
|
{
|
|
|
|
}
|
|
}
|
|
}
|