BizHawk/BizHawk.MultiClient/config/FirmwaresConfig.cs

339 lines
12 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;
//notes: eventually, we intend to have a "firmware acquisition interface" exposed to the emulator cores.
//it will be implemented by the multiclient, and use firmware keys to fetch the firmware content.
//however, for now, the cores are using strings from the config class. so we have the `configMember` which is
//used by reflection to set the configuration for firmwares which were found
//TODO - we may eventually need to add a progress dialog for this. we should have one for other reasons.
//I started making one in Bizhawk.Util as QuickProgressPopup but ran out of time
//IDEA: show current path in tooltip
namespace BizHawk.MultiClient
{
public partial class FirmwaresConfig : Form
{
//master firmware DB.. for now. might need to go somewhere else later
FDR[] dbItems = new[] {
new FDR("E4E41472C454F928E53EB10E0509BF7D1146ECC1", "NES", "disksys.rom", "FDS Bios", "FilenameFDSBios"),
new FDR("973E10840DB683CF3FAF61BD443090786B3A9F04", "SNES", "sgb.sfc", "Super GameBoy Rom"),
new FDR("A002F4EFBA42775A31185D443F3ED1790B0E949A", "SNES", "cx4.rom", "CX4 Rom"),
new FDR("188D471FEFEA71EB53F0EE7064697FF0971B1014", "SNES", "dsp1.rom", "DSP1 Rom"),
new FDR("78B724811F5F18D8C67669D9390397EB1A47A5E2", "SNES", "dsp1b.rom", "DSP1b Rom"),
new FDR("198C4B1C3BFC6D69E734C5957AF3DBFA26238DFB", "SNES", "dsp2.rom", "DSP2 Rom"),
new FDR("558DA7CB3BD3876A6CA693661FFC6C110E948CF9", "SNES", "dsp3.rom", "DSP3 Rom"),
new FDR("AF6478AECB6F1B67177E79C82CA04C56250A8C72", "SNES", "dsp4.rom", "DSP4 Rom"),
new FDR("6472828403DE3589433A906E2C3F3D274C0FF008", "SNES", "st010.rom", "ST010 Rom"),
new FDR("FECBAE2CEC76C710422486BAA186FFA7CA1CF925", "SNES", "st011.rom", "ST011 Rom"),
new FDR("91383B92745CC7CC4F15409AC5BC2C2F699A43F1", "SNES", "st018.rom", "ST018 Rom"),
new FDR("79F5FF55DD10187C7FD7B8DAAB0B3FFBD1F56A2C", "PCECD", "pcecd-3.0-J.pce", "Super CD-ROM System v3.0 (J)","FilenamePCEBios"),
new FDR("2B8CB4F87580683EB4D760E4ED210813D667F0A2", "SAT", "saturn-1.00-NTSC.bin", "Sega Saturn Bios v1.00 (NTSC)"),
new FDR("FAA8EA183A6D7BBE5D4E03BB1332519800D3FBC3", "SAT", "saturn-1.00-PAL.bin", "Sega Saturn Bios v1.00 (PAL)"),
new FDR("DF94C5B4D47EB3CC404D88B33A8FDA237EAF4720", "SAT", "saturn-1.01-J.bin", "Sega Saturn Bios v1.01 (J)", "FilenameSaturnBios"),
new FDR("D9D134BB6B36907C615A594CC7688F7BFCEF5B43", "A78", "7800NTSCBIOS.bin", "Atari 7800 NTSC Bios", "FilenameA78NTSCBios"),
new FDR("5A140136A16D1D83E4FF32A19409CA376A8DF874", "A78", "7800PALBIOS.bin", "Atari 7800 PAL Bios", "FilenameA78PALBios"),
new FDR("A3AF676991391A6DD716C79022D4947206B78164", "A78", "7800highscore.bin", "Atari 7800 Highscore Bios", "FilenameA78HSCBios"),
new FDR("45BEDC4CBDEAC66C7DF59E9E599195C778D86A92", "Coleco", "ColecoBios.bin", "Colecovision Bios", "FilenameCOLBios"),
new FDR("300C20DF6731A33952DED8C436F7F186D25D3492", "GBA", "gbabios.rom", "GBA Bios", "FilenameGBABIOS"),
new FDR("EF66DAD3E7B2B6A86F326765E7DFD7D1A308AD8F", "TI83", "ti83_1.rom", "TI-83 Rom"),
new FDR("5A65B922B562CB1F57DAB51B73151283F0E20C7A", "INTV", "erom.bin", "Intellivision Executive Rom", "FilenameINTVEROM"),
new FDR("F9608BB4AD1CFE3640D02844C7AD8E0BCD974917", "INTV", "grom.bin", "Intellivision Graphics Rom", "FilenameINTVGROM"),
new FDR("1D503E56DF85A62FEE696E7618DC5B4E781DF1BB", "C64", "c64-kernal.bin", "C64 Kernal Rom"),
new FDR("79015323128650C742A3694C9429AA91F355905E", "C64", "c64-basic.bin", "C64 Basic Rom"),
new FDR("ADC7C31E18C7C7413D54802EF2F4193DA14711AA", "C64", "c64-chargen.bin", "C64 Chargen Rom")
};
//friendly names than the system Ids
static readonly Dictionary<string, string> systemGroupNames = new Dictionary<string, string>()
{
{ "NES", "NES" },
{ "SNES", "SNES" },
{ "PCECD", "PCE-CD" },
{ "SAT", "Saturn" },
{ "A78", "Atari 7800" },
{ "Coleco", "Colecovision" },
{ "GBA", "GBA" },
{ "TI83", "TI-83" },
{ "INTV", "Intellivision" },
{ "C64", "C64" },
};
class FDR
{
public FDR(string hash, string systemId, string recommendedName, string descr, string configMember = null)
{
this.hash = hash;
this.systemId = systemId;
this.recommendedName = recommendedName;
this.descr = descr;
this.configMember = configMember;
}
public string hash;
public string systemId;
public string recommendedName;
public string descr;
public string configMember;
//sort of sloppy to store this here..
public FileInfo userPath;
}
private const int idUnsure = 0;
private const int idMissing = 1;
private const int idOk = 2;
Font fixedFont;
class ListViewSorter : IComparer
{
public FirmwaresConfig dialog;
public int column;
public int sign;
public ListViewSorter(FirmwaresConfig dialog, int column)
{
this.dialog = dialog;
this.column = column;
}
public int Compare(object a, object b)
{
var lva = (ListViewItem)a;
var lvb = (ListViewItem)b;
return sign*string.Compare(lva.SubItems[column].Text, lvb.SubItems[column].Text);
}
}
ListViewSorter listviewSorter;
public FirmwaresConfig()
{
InitializeComponent();
//prep imagelist for listview with 3 item states for {idUnsure, idMissing, idOk}
imageList1.Images.AddRange(new[] { MultiClient.Properties.Resources.RetroQuestion, MultiClient.Properties.Resources.ExclamationRed, MultiClient.Properties.Resources.GreenCheck });
listviewSorter = new ListViewSorter(this, -1);
}
private void FirmwaresConfig_Load(object sender, EventArgs e)
{
//we'll use this font for displaying the hash, so they dont look all jagged in a long list
fixedFont = new Font(new FontFamily("Courier New"), 8);
//populate listview from firmware DB
var groups = new Dictionary<string, ListViewGroup>();
foreach (var fdr in dbItems)
{
var lvi = new ListViewItem();
lvi.Tag = fdr;
lvi.UseItemStyleForSubItems = false;
lvi.ImageIndex = idUnsure;
lvi.SubItems.Add(fdr.systemId);
lvi.SubItems.Add("sha1:" + fdr.hash);
lvi.SubItems.Add(fdr.recommendedName);
lvi.SubItems.Add(fdr.descr);
lvi.SubItems[2].Font = fixedFont;
lvFirmwares.Items.Add(lvi);
//build the groups in the listview as we go:
if (!groups.ContainsKey(fdr.systemId))
{
lvFirmwares.Groups.Add(fdr.systemId, systemGroupNames[fdr.systemId]);
var lvg = lvFirmwares.Groups[lvFirmwares.Groups.Count - 1];
groups[fdr.systemId] = lvg;
}
lvi.Group = groups[fdr.systemId];
}
//now that we have some items in the listview, we can size the hash column to something sensible. why not the others, too?
lvFirmwares.AutoResizeColumn(0, ColumnHeaderAutoResizeStyle.ColumnContent);
lvFirmwares.AutoResizeColumn(1, ColumnHeaderAutoResizeStyle.ColumnContent);
lvFirmwares.AutoResizeColumn(2, ColumnHeaderAutoResizeStyle.ColumnContent);
DoScan();
}
private void FirmwaresConfig_FormClosed(object sender, FormClosedEventArgs e)
{
fixedFont.Dispose();
}
private void tbbGroup_Click(object sender, EventArgs e)
{
//toggle the grouping state
lvFirmwares.ShowGroups = !lvFirmwares.ShowGroups;
}
private void lvFirmwares_ColumnClick(object sender, ColumnClickEventArgs e)
{
if (listviewSorter.column != e.Column)
{
listviewSorter.column = e.Column;
listviewSorter.sign = 1;
}
else listviewSorter.sign *= -1;
lvFirmwares.ListViewItemSorter = listviewSorter;
lvFirmwares.SetSortIcon(e.Column, listviewSorter.sign == 1 ? SortOrder.Descending : SortOrder.Ascending);
lvFirmwares.Sort();
}
private void tbbScan_Click(object sender, EventArgs e)
{
//user-initiated scan
DoScan();
}
//represents a file found on disk in the user's firmware directory.
class RealFirmwareFile
{
public FileInfo fi;
public string hash;
}
private void DoScan()
{
//build a list of files under the global firmwares path, and build a hash for each of them while we're at it
var todo = new Queue<DirectoryInfo>(new[] { new DirectoryInfo(Global.Config.FirmwaresPath) });
var files = new List<RealFirmwareFile>();
while (todo.Count != 0)
{
var di = todo.Dequeue();
foreach (var disub in di.GetDirectories()) todo.Enqueue(disub);
byte[] buffer = new byte[0];
foreach (var fi in di.GetFiles())
{
var rff = new RealFirmwareFile();
rff.fi = fi;
long len = fi.Length;
if (len > buffer.Length)
buffer = new byte[len];
using (var fs = fi.OpenRead()) fs.Read(buffer, 0, (int)len);
rff.hash = Util.Hash_SHA1(buffer, 0, (int)len);
files.Add(rff);
}
}
//clean out our runtime state to get ready for a new scan result
foreach (var fdr in dbItems)
fdr.userPath = null;
foreach (ListViewItem lvi in lvFirmwares.Items)
lvi.ImageIndex = idUnsure;
//now, contemplate each file and see if it matches a known firmware (this algorithm is slow)
//if it matches, make a note for later use
foreach (var f in files)
{
foreach (var fdr in dbItems)
{
if (fdr.hash == f.hash)
{
foreach (ListViewItem lvi in lvFirmwares.Items)
{
if (lvi.Tag == fdr)
{
lvi.ImageIndex = idOk;
fdr.userPath = f.fi;
}
}
}
}
}
//set unfound firmwares to missing icon (theres no good reason for this, but its a reminder that if we thread this later we may want to start it that way, or something like that)
foreach (ListViewItem lvi in lvFirmwares.Items)
if(lvi.ImageIndex == idUnsure)
lvi.ImageIndex = idMissing;
//set entries in the Global.Config class for firmwares that have been bound to emulator cores.
//this system is due to be replaced with something else
foreach (var fdr in dbItems)
{
if(fdr.configMember == null) continue;
if (fdr.userPath == null) continue;
//ehhh... this wont be working if this file was outside of the configured firmwares directory.
//maybe we should check that: http://stackoverflow.com/questions/5617320/given-full-path-check-if-path-is-subdirectory-of-some-other-path-or-otherwise
string path = Path.GetFileName(fdr.userPath.FullName);
typeof(Config).GetField(fdr.configMember).SetValue(Global.Config, path);
}
}
private void tbbOrganize_Click(object sender, EventArgs e)
{
if (System.Windows.Forms.MessageBox.Show(this, "This is going to move/rename files under your configured firmwares directory to match our recommended organizational scheme (which is not super great right now). Proceed?", "Firmwares Organization Confirm", MessageBoxButtons.OKCancel) == System.Windows.Forms.DialogResult.Cancel)
return;
foreach (var fdr in dbItems)
{
if (fdr.userPath == null)
continue;
string fpTarget = Path.Combine(Global.Config.FirmwaresPath, fdr.recommendedName);
string fpSource = fdr.userPath.FullName;
try
{
File.Move(fpSource, fpTarget);
}
catch
{
//sometimes moves fail. especially in newer versions of windows with explorers more fragile than your great-grandma.
//I am embarassed that I know that.
}
}
//to be safe, better do this. we want the db to track the state of the files after theyre moved.
DoScan();
}
private void lvFirmwares_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.C && e.Control && !e.Alt && !e.Shift)
{
ListviewCopy();
}
}
void ListviewCopy()
{
ListView.SelectedIndexCollection indexes = lvFirmwares.SelectedIndices;
if (indexes.Count <= 0)
return;
StringBuilder sb = new StringBuilder();
//walk over each selected item and subitem within it to generate a string from it
foreach (int index in indexes)
{
foreach (ListViewItem.ListViewSubItem item in lvFirmwares.Items[index].SubItems)
{
if (!String.IsNullOrWhiteSpace(item.Text))
sb.Append(item.Text).Append('\t');
}
//remove the last tab
sb.Remove(sb.Length - 1, 1);
sb.Append("\r\n");
}
//remove last newline
sb.Length -= 2;
if (sb.Length > 0) Clipboard.SetDataObject(sb.ToString());
}
}
}