652 lines
16 KiB
C#
652 lines
16 KiB
C#
using System;
|
|
using System.Threading.Tasks;
|
|
using System.Collections.Generic;
|
|
using System.Drawing;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Windows.Forms;
|
|
|
|
using BizHawk.Client.Common;
|
|
using BizHawk.Common;
|
|
using BizHawk.Common.CollectionExtensions;
|
|
using BizHawk.Emulation.Common;
|
|
|
|
namespace BizHawk.Client.EmuHawk
|
|
{
|
|
public partial class PlayMovie : Form
|
|
{
|
|
private readonly MainForm _mainForm;
|
|
private readonly Config _config;
|
|
private readonly GameInfo _game;
|
|
private readonly IEmulator _emulator;
|
|
private readonly IMovieSession _movieSession;
|
|
private readonly PlatformFrameRates _platformFrameRates = new PlatformFrameRates();
|
|
|
|
private List<IMovie> _movieList = new List<IMovie>();
|
|
private bool _sortReverse;
|
|
private string _sortedCol;
|
|
|
|
private bool _sortDetailsReverse;
|
|
private string _sortedDetailsCol;
|
|
|
|
public PlayMovie(
|
|
MainForm mainForm,
|
|
Config config,
|
|
GameInfo game,
|
|
IEmulator emulator,
|
|
IMovieSession movieSession)
|
|
{
|
|
_mainForm = mainForm;
|
|
_config = config;
|
|
_game = game;
|
|
_emulator = emulator;
|
|
_movieSession = movieSession;
|
|
InitializeComponent();
|
|
MovieView.RetrieveVirtualItem += MovieView_QueryItemText;
|
|
MovieView.VirtualMode = true;
|
|
_sortReverse = false;
|
|
_sortedCol = "";
|
|
|
|
_sortDetailsReverse = false;
|
|
_sortedDetailsCol = "";
|
|
}
|
|
|
|
private void PlayMovie_Load(object sender, EventArgs e)
|
|
{
|
|
IncludeSubDirectories.Checked = _config.PlayMovieIncludeSubDir;
|
|
MatchHashCheckBox.Checked = _config.PlayMovieMatchHash;
|
|
ScanFiles();
|
|
PreHighlightMovie();
|
|
TurboCheckbox.Checked = _config.TurboSeek;
|
|
}
|
|
|
|
private void MovieView_QueryItemText(object sender, RetrieveVirtualItemEventArgs e)
|
|
{
|
|
var entry = _movieList[e.ItemIndex];
|
|
e.Item = new ListViewItem(entry.Filename);
|
|
e.Item.SubItems.Add(entry.SystemID);
|
|
e.Item.SubItems.Add(entry.GameName);
|
|
e.Item.SubItems.Add(_platformFrameRates.MovieTime(entry).ToString(@"hh\:mm\:ss\.fff"));
|
|
}
|
|
|
|
private void Run()
|
|
{
|
|
var indices = MovieView.SelectedIndices;
|
|
if (indices.Count > 0) // Import file if necessary
|
|
{
|
|
_mainForm.StartNewMovie(_movieList[MovieView.SelectedIndices[0]], false);
|
|
}
|
|
}
|
|
|
|
private int? AddMovieToList(string filename, bool force)
|
|
{
|
|
using var file = new HawkFile(filename);
|
|
if (!file.Exists)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var movie = PreLoadMovieFile(file, force);
|
|
if (movie == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
int? index;
|
|
lock (_movieList)
|
|
{
|
|
// need to check IsDuplicateOf within the lock
|
|
index = IsDuplicateOf(filename);
|
|
if (index.HasValue)
|
|
{
|
|
return index;
|
|
}
|
|
|
|
_movieList.Add(movie);
|
|
index = _movieList.Count - 1;
|
|
}
|
|
|
|
_sortReverse = false;
|
|
_sortedCol = "";
|
|
|
|
return index;
|
|
}
|
|
|
|
private int? IsDuplicateOf(string filename)
|
|
{
|
|
for (var i = 0; i < _movieList.Count; i++)
|
|
{
|
|
if (_movieList[i].Filename == filename)
|
|
{
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private IMovie PreLoadMovieFile(HawkFile hf, bool force)
|
|
{
|
|
var movie = MovieService.Get(hf.CanonicalFullPath);
|
|
|
|
try
|
|
{
|
|
movie.PreLoadHeaderAndLength(hf);
|
|
|
|
// Don't do this from browse
|
|
if (movie.Hash == _game.Hash
|
|
|| _config.PlayMovieMatchHash == false || force)
|
|
{
|
|
return movie;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// TODO: inform the user that a movie failed to parse in some way
|
|
Console.WriteLine(ex.Message);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void UpdateList()
|
|
{
|
|
MovieView.Refresh();
|
|
MovieCount.Text = $"{_movieList.Count} {(_movieList.Count == 1 ? "movie" : "movies")}";
|
|
}
|
|
|
|
private void PreHighlightMovie()
|
|
{
|
|
if (_game.IsNullInstance())
|
|
{
|
|
return;
|
|
}
|
|
|
|
var indices = new List<int>();
|
|
|
|
// Pull out matching names
|
|
for (var i = 0; i < _movieList.Count; i++)
|
|
{
|
|
if (_game.FilesystemSafeName() == _movieList[i].GameName)
|
|
{
|
|
indices.Add(i);
|
|
}
|
|
}
|
|
|
|
if (indices.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (indices.Count == 1)
|
|
{
|
|
HighlightMovie(indices[0]);
|
|
return;
|
|
}
|
|
|
|
// Prefer tas files
|
|
var tas = new List<int>();
|
|
for (var i = 0; i < indices.Count; i++)
|
|
{
|
|
foreach (var ext in MovieService.MovieExtensions)
|
|
{
|
|
if (Path.GetExtension(_movieList[indices[i]].Filename)?.ToUpper() == $".{ext}")
|
|
{
|
|
tas.Add(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (tas.Count == 1)
|
|
{
|
|
HighlightMovie(tas[0]);
|
|
return;
|
|
}
|
|
|
|
if (tas.Count > 1)
|
|
{
|
|
indices = new List<int>(tas);
|
|
}
|
|
|
|
// Final tie breaker - Last used file
|
|
var file = new FileInfo(_movieList[indices[0]].Filename);
|
|
var time = file.LastAccessTime;
|
|
var mostRecent = indices.First();
|
|
for (var i = 1; i < indices.Count; i++)
|
|
{
|
|
file = new FileInfo(_movieList[indices[0]].Filename);
|
|
if (file.LastAccessTime > time)
|
|
{
|
|
time = file.LastAccessTime;
|
|
mostRecent = indices[i];
|
|
}
|
|
}
|
|
|
|
HighlightMovie(mostRecent);
|
|
}
|
|
|
|
private void HighlightMovie(int index)
|
|
{
|
|
MovieView.SelectedIndices.Clear();
|
|
MovieView.Items[index].Selected = true;
|
|
}
|
|
|
|
private void ScanFiles()
|
|
{
|
|
_movieList.Clear();
|
|
MovieView.VirtualListSize = 0;
|
|
MovieView.Update();
|
|
|
|
var directory = _config.PathEntries.MovieAbsolutePath();
|
|
if (!Directory.Exists(directory))
|
|
{
|
|
Directory.CreateDirectory(directory);
|
|
}
|
|
|
|
var dpTodo = new Queue<string>();
|
|
var fpTodo = new List<string>();
|
|
dpTodo.Enqueue(directory);
|
|
var ordinals = new Dictionary<string, int>();
|
|
|
|
while (dpTodo.Count > 0)
|
|
{
|
|
string dp = dpTodo.Dequeue();
|
|
|
|
// enqueue subdirectories if appropriate
|
|
if (_config.PlayMovieIncludeSubDir)
|
|
{
|
|
foreach (var subDir in Directory.GetDirectories(dp))
|
|
{
|
|
dpTodo.Enqueue(subDir);
|
|
}
|
|
}
|
|
|
|
// add movies
|
|
foreach (var extension in MovieService.MovieExtensions)
|
|
{
|
|
fpTodo.AddRange(Directory.GetFiles(dp, $"*.{extension}"));
|
|
}
|
|
}
|
|
|
|
// in parallel, scan each movie
|
|
Parallel.For(0, fpTodo.Count, i =>
|
|
{
|
|
var file = fpTodo[i];
|
|
lock (ordinals)
|
|
{
|
|
ordinals[file] = i;
|
|
}
|
|
|
|
AddMovieToList(file, force: false);
|
|
});
|
|
|
|
// sort by the ordinal key to maintain relatively stable results when rescanning
|
|
_movieList.Sort((a, b) => ordinals[a.Filename].CompareTo(ordinals[b.Filename]));
|
|
|
|
RefreshMovieList();
|
|
}
|
|
|
|
#region Events
|
|
|
|
#region Movie List
|
|
|
|
private void RefreshMovieList()
|
|
{
|
|
MovieView.VirtualListSize = _movieList.Count;
|
|
UpdateList();
|
|
}
|
|
|
|
private void MovieView_DragEnter(object sender, DragEventArgs e)
|
|
{
|
|
e.Set(DragDropEffects.Copy);
|
|
}
|
|
|
|
private void MovieView_DragDrop(object sender, DragEventArgs e)
|
|
{
|
|
var filePaths = (string[])e.Data.GetData(DataFormats.FileDrop);
|
|
|
|
foreach (var path in filePaths.Where(path => MovieService.MovieExtensions.Contains(Path.GetExtension(path)?.Replace(".", ""))))
|
|
{
|
|
AddMovieToList(path, force: true);
|
|
}
|
|
|
|
RefreshMovieList();
|
|
}
|
|
|
|
private void MovieView_KeyDown(object sender, KeyEventArgs e)
|
|
{
|
|
if (e.Control && e.KeyCode == Keys.C)
|
|
{
|
|
var indexes = MovieView.SelectedIndices;
|
|
if (indexes.Count > 0)
|
|
{
|
|
var copyStr = new StringBuilder();
|
|
foreach (int index in indexes)
|
|
{
|
|
copyStr
|
|
.Append(_movieList[index].Filename).Append('\t')
|
|
.Append(_movieList[index].SystemID).Append('\t')
|
|
.Append(_movieList[index].GameName).Append('\t')
|
|
.Append(_platformFrameRates.MovieTime(_movieList[index]).ToString(@"hh\:mm\:ss\.fff"))
|
|
.AppendLine();
|
|
}
|
|
|
|
Clipboard.SetDataObject(copyStr.ToString());
|
|
}
|
|
}
|
|
}
|
|
|
|
private void MovieView_DoubleClick(object sender, EventArgs e)
|
|
{
|
|
Run();
|
|
Close();
|
|
}
|
|
|
|
private void MovieView_ColumnClick(object sender, ColumnClickEventArgs e)
|
|
{
|
|
var columnName = MovieView.Columns[e.Column].Text;
|
|
switch (columnName)
|
|
{
|
|
case "File":
|
|
default:
|
|
_movieList = _movieList.OrderBy(x => Path.GetFileName(x.Filename))
|
|
.ThenBy(x => x.SystemID)
|
|
.ThenBy(x => x.GameName)
|
|
.ThenBy(x => x.FrameCount)
|
|
.ToList();
|
|
break;
|
|
case "SysID":
|
|
_movieList = _movieList.OrderBy(x => x.SystemID)
|
|
.ThenBy(x => Path.GetFileName(x.Filename))
|
|
.ThenBy(x => x.GameName)
|
|
.ThenBy(x => x.FrameCount)
|
|
.ToList();
|
|
break;
|
|
case "Game":
|
|
_movieList = _movieList.OrderBy(x => x.GameName)
|
|
.ThenBy(x => Path.GetFileName(x.Filename))
|
|
.ThenBy(x => x.SystemID)
|
|
.ThenBy(x => x.FrameCount)
|
|
.ToList();
|
|
break;
|
|
case "Length (est.)":
|
|
_movieList = _movieList.OrderBy(x => x.FrameCount)
|
|
.ThenBy(x => Path.GetFileName(x.Filename))
|
|
.ThenBy(x => x.SystemID)
|
|
.ThenBy(x => x.GameName)
|
|
.ToList();
|
|
break;
|
|
}
|
|
if (_sortedCol == columnName && _sortReverse)
|
|
{
|
|
_movieList.Reverse();
|
|
_sortReverse = false;
|
|
}
|
|
else
|
|
{
|
|
_sortReverse = true;
|
|
_sortedCol = columnName;
|
|
}
|
|
MovieView.Refresh();
|
|
}
|
|
|
|
private void MovieView_SelectedIndexChanged(object sender, EventArgs e)
|
|
{
|
|
toolTip1.SetToolTip(DetailsView, "");
|
|
DetailsView.Items.Clear();
|
|
if (MovieView.SelectedIndices.Count < 1)
|
|
{
|
|
OK.Enabled = false;
|
|
return;
|
|
}
|
|
|
|
OK.Enabled = true;
|
|
|
|
var firstIndex = MovieView.SelectedIndices[0];
|
|
MovieView.EnsureVisible(firstIndex);
|
|
|
|
foreach (var kvp in _movieList[firstIndex].HeaderEntries)
|
|
{
|
|
var item = new ListViewItem(kvp.Key);
|
|
item.SubItems.Add(kvp.Value);
|
|
|
|
switch (kvp.Key)
|
|
{
|
|
case HeaderKeys.Sha1:
|
|
if (kvp.Value != _game.Hash)
|
|
{
|
|
item.BackColor = Color.Pink;
|
|
toolTip1.SetToolTip(DetailsView, $"Current SHA1: {_game.Hash}");
|
|
}
|
|
break;
|
|
case HeaderKeys.EmulationVersion:
|
|
if (kvp.Value != VersionInfo.GetEmuVersion())
|
|
{
|
|
item.BackColor = Color.Yellow;
|
|
}
|
|
break;
|
|
case HeaderKeys.Platform:
|
|
// feos: previously it was compared against _game.System, but when the movie is created
|
|
// its platform is copied from _emulator.SystemId, see PopulateWithDefaultHeaderValues()
|
|
// the problem is that for GameGear and SG100, those mismatch, resulting in false positive here
|
|
// I have a patch to make GG and SG appear as platforms in movie header (issue #1246)
|
|
// but even with it, for all the old movies, this false positive would have to be worked around anyway
|
|
// TODO: actually check header flags like "IsGGMode" and "IsSegaCDMode" (those are never parsed by BizHawk)
|
|
if (kvp.Value != _emulator.SystemId)
|
|
{
|
|
item.BackColor = Color.Pink;
|
|
}
|
|
break;
|
|
}
|
|
|
|
DetailsView.Items.Add(item);
|
|
}
|
|
|
|
var fpsItem = new ListViewItem("Fps");
|
|
fpsItem.SubItems.Add($"{Fps(_movieList[firstIndex]):0.#######}");
|
|
DetailsView.Items.Add(fpsItem);
|
|
|
|
var framesItem = new ListViewItem("Frames");
|
|
framesItem.SubItems.Add(_movieList[firstIndex].FrameCount.ToString());
|
|
DetailsView.Items.Add(framesItem);
|
|
CommentsBtn.Enabled = _movieList[firstIndex].Comments.Any();
|
|
SubtitlesBtn.Enabled = _movieList[firstIndex].Subtitles.Any();
|
|
}
|
|
|
|
public double Fps(IMovie movie)
|
|
{
|
|
var system = movie.HeaderEntries[HeaderKeys.Platform];
|
|
var pal = movie.HeaderEntries.ContainsKey(HeaderKeys.Pal)
|
|
&& movie.HeaderEntries[HeaderKeys.Pal] == "1";
|
|
|
|
return new PlatformFrameRates()[system, pal];
|
|
|
|
}
|
|
|
|
private void EditMenuItem_Click(object sender, EventArgs e)
|
|
{
|
|
foreach (var movie in MovieView.SelectedIndices.Cast<int>()
|
|
.Select(index => _movieList[index]))
|
|
{
|
|
System.Diagnostics.Process.Start(movie.Filename);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Details
|
|
|
|
private void DetailsView_ColumnClick(object sender, ColumnClickEventArgs e)
|
|
{
|
|
var detailsList = new List<MovieDetails>();
|
|
for (var i = 0; i < DetailsView.Items.Count; i++)
|
|
{
|
|
detailsList.Add(new MovieDetails
|
|
{
|
|
Keys = DetailsView.Items[i].Text,
|
|
Values = DetailsView.Items[i].SubItems[1].Text,
|
|
BackgroundColor = DetailsView.Items[i].BackColor
|
|
});
|
|
}
|
|
|
|
var columnName = DetailsView.Columns[e.Column].Text;
|
|
if (_sortedDetailsCol != columnName)
|
|
{
|
|
_sortDetailsReverse = false;
|
|
}
|
|
|
|
detailsList = columnName switch
|
|
{
|
|
"Header" => detailsList
|
|
.OrderBy(x => x.Keys, _sortDetailsReverse)
|
|
.ThenBy(x => x.Values)
|
|
.ToList(),
|
|
"Value" => detailsList
|
|
.OrderBy(x => x.Values, _sortDetailsReverse)
|
|
.ThenBy(x => x.Keys)
|
|
.ToList(),
|
|
_ => detailsList
|
|
};
|
|
|
|
DetailsView.Items.Clear();
|
|
foreach (var detail in detailsList)
|
|
{
|
|
var item = new ListViewItem { Text = detail.Keys, BackColor = detail.BackgroundColor };
|
|
item.SubItems.Add(detail.Values);
|
|
DetailsView.Items.Add(item);
|
|
}
|
|
|
|
_sortedDetailsCol = columnName;
|
|
_sortDetailsReverse = !_sortDetailsReverse;
|
|
}
|
|
|
|
private void CommentsBtn_Click(object sender, EventArgs e)
|
|
{
|
|
var indices = MovieView.SelectedIndices;
|
|
if (indices.Count > 0)
|
|
{
|
|
var form = new EditCommentsForm();
|
|
form.GetMovie(_movieList[MovieView.SelectedIndices[0]]);
|
|
form.Show();
|
|
}
|
|
}
|
|
|
|
private void SubtitlesBtn_Click(object sender, EventArgs e)
|
|
{
|
|
var indices = MovieView.SelectedIndices;
|
|
if (indices.Count > 0)
|
|
{
|
|
var s = new EditSubtitlesForm { ReadOnly = true };
|
|
s.GetMovie(_movieList[MovieView.SelectedIndices[0]]);
|
|
s.Show();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Misc Widgets
|
|
|
|
private void BrowseMovies_Click(object sender, EventArgs e)
|
|
{
|
|
using var ofd = new OpenFileDialog
|
|
{
|
|
Filter = new FilesystemFilterSet(FilesystemFilter.BizHawkMovies, FilesystemFilter.TAStudioProjects).ToString(),
|
|
InitialDirectory = _config.PathEntries.MovieAbsolutePath()
|
|
};
|
|
|
|
var result = ofd.ShowHawkDialog();
|
|
if (result == DialogResult.OK)
|
|
{
|
|
var file = new FileInfo(ofd.FileName);
|
|
if (!file.Exists)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int? index = AddMovieToList(ofd.FileName, true);
|
|
RefreshMovieList();
|
|
if (index.HasValue)
|
|
{
|
|
MovieView.SelectedIndices.Clear();
|
|
MovieView.Items[index.Value].Selected = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void Scan_Click(object sender, EventArgs e)
|
|
{
|
|
ScanFiles();
|
|
PreHighlightMovie();
|
|
}
|
|
|
|
private void IncludeSubDirectories_CheckedChanged(object sender, EventArgs e)
|
|
{
|
|
_config.PlayMovieIncludeSubDir = IncludeSubDirectories.Checked;
|
|
ScanFiles();
|
|
PreHighlightMovie();
|
|
}
|
|
|
|
private void MatchHashCheckBox_CheckedChanged(object sender, EventArgs e)
|
|
{
|
|
_config.PlayMovieMatchHash = MatchHashCheckBox.Checked;
|
|
ScanFiles();
|
|
PreHighlightMovie();
|
|
}
|
|
|
|
private void Ok_Click(object sender, EventArgs e)
|
|
{
|
|
_config.TurboSeek = TurboCheckbox.Checked;
|
|
Run();
|
|
_movieSession.ReadOnly = ReadOnlyCheckBox.Checked;
|
|
|
|
if (StopOnFrameCheckbox.Checked &&
|
|
(StopOnFrameTextBox.ToRawInt().HasValue || LastFrameCheckbox.Checked))
|
|
{
|
|
_mainForm.PauseOnFrame = LastFrameCheckbox.Checked
|
|
? _movieSession.Movie.InputLogLength
|
|
: StopOnFrameTextBox.ToRawInt();
|
|
}
|
|
|
|
Close();
|
|
}
|
|
|
|
private void Cancel_Click(object sender, EventArgs e)
|
|
{
|
|
Close();
|
|
}
|
|
|
|
#endregion
|
|
|
|
private bool _programmaticallyChangingStopFrameCheckbox;
|
|
private void StopOnFrameCheckbox_CheckedChanged(object sender, EventArgs e)
|
|
{
|
|
if (!_programmaticallyChangingStopFrameCheckbox)
|
|
{
|
|
StopOnFrameTextBox.Focus();
|
|
}
|
|
}
|
|
|
|
private void StopOnFrameTextBox_TextChanged_1(object sender, EventArgs e)
|
|
{
|
|
_programmaticallyChangingStopFrameCheckbox = true;
|
|
StopOnFrameCheckbox.Checked = !string.IsNullOrWhiteSpace(StopOnFrameTextBox.Text);
|
|
_programmaticallyChangingStopFrameCheckbox = false;
|
|
}
|
|
|
|
private void LastFrameCheckbox_CheckedChanged(object sender, EventArgs e)
|
|
{
|
|
if (LastFrameCheckbox.Checked)
|
|
{
|
|
_programmaticallyChangingStopFrameCheckbox = true;
|
|
StopOnFrameCheckbox.Checked = true;
|
|
_programmaticallyChangingStopFrameCheckbox = false;
|
|
}
|
|
|
|
StopOnFrameTextBox.Enabled = !LastFrameCheckbox.Checked;
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|