// Spludlow Software // Copyright © Samuel P. Ludlow 2020 All Rights Reserved // Distributed under the terms of the GNU General Public License version 3 // Distributed WITHOUT ANY WARRANTY; without implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE // https://www.spludlow.co.uk/LICENCE.TXT // The Spludlow logo is a registered trademark of Samuel P. Ludlow and may not be used without permission // v1.14 using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO; using System.Data; namespace Spludlow.Data { /// /// Some Methods for performing various MAME ROM Related tasks /// /// Put the official XML into a DB the "machine" and "rom" tables are used here /// /// Index & Import (2 Steps) ROM collections into a "SHA1 Store". All ROMS are hashed and stored with the SHA1 as the filename /// /// Create machine ROM set zip files from the DB and SHA1 store. /// /// Create a .bat file to run through specified games (so you can look for the good ones) /// /// !!! use the temp path with syntax \\?\C:\TEMP to overcome long filname problems /// /// TempDirectory & Ram DISK /// ======================== /// If TempDirectory is null the C: drive will be used /// Used to extract/create archives. /// Using a RAM disk or seperate fast drive may improve performance. /// Using 4.5GB seems to cover it. /// You can run on a smaller RAM disk then run a 2nd pass without it to get the larger stuff /// Any failures indexing (including disk full) will go into ".bad.txt" /// public class Mame { /// /// Convert MAME full driver information XML into releational database /// public static void RelationizeXmlToDatabase(string xmlFilename, string connectionString, string databaseName, string tempDirectory) { string topElementname = "machine"; DataSet dataSet = Spludlow.Data.XML.ConvertRelational(xmlFilename, topElementname); DataSet schema = Spludlow.Data.ADO.Schema(dataSet, true); Spludlow.Data.Database.AutoAddForeignKeys(schema); Spludlow.Log.Report("MAME Relationize Xml Schema: " + topElementname, schema); Spludlow.Data.Schemas.ImportDatabase(schema, dataSet, connectionString, databaseName, tempDirectory); } /// /// Some rom collections have directories and not zips, fix with this method /// public static void ZipRomDirectory(string directory) { foreach (string romDirectory in Directory.GetDirectories(directory)) { string archiveName = romDirectory + ".zip"; if (File.Exists(archiveName) == true) continue; Spludlow.Archive.Create(archiveName, romDirectory + @"\*"); Directory.Delete(romDirectory, true); } } public static void FullImportSoftwareDirectory(string softwareRootDirectory, string storeDirectory, string tempDirectory) { IndexSoftwareDirectory(softwareRootDirectory, tempDirectory); foreach (string directory in Directory.GetDirectories(softwareRootDirectory)) { string indexFilename = directory + ".txt"; if (File.Exists(indexFilename) == true) { LoadRoms(indexFilename, storeDirectory, tempDirectory); DeleteImportedRoms(directory); } } } /// /// /// public static void IndexSoftwareDirectory(string directory, string tempDirectory) { foreach (string listDirectory in Directory.GetDirectories(directory)) { string softwareListName = Path.GetFileName(listDirectory); string[] softwareFilenames = Directory.GetFiles(listDirectory); string[] softwareSubDirs = Directory.GetDirectories(listDirectory); int zipCount = 0; int nonZipCount = 0; foreach (string filename in softwareFilenames) { if (Path.GetExtension(filename).ToLower() == ".zip") ++zipCount; else ++nonZipCount; } bool error = false; try { IndexRomDirectory(listDirectory, tempDirectory); } catch (Exception ee) { Spludlow.Log.Warning("IndexSoftwareDirectory: " + directory, ee); error = true; } if (error == true || zipCount == 0 || nonZipCount > 0 || softwareSubDirs.Length > 0) File.AppendAllText(listDirectory + ".bad.txt", listDirectory + Environment.NewLine); } } public static void ReportSoftwareListHashes(string hashStoreDirectory) { HashSet storeHashes = ReadExistingHashes(hashStoreDirectory); Spludlow.Data.IDAL database = Spludlow.Data.DAL.Create("SPLCAL-MAIN@MameSoftware"); HashSet databaseHashes = new HashSet(); foreach (string tableName in new string[] { "disk", "rom" }) { foreach (DataRow row in database.Select("SELECT sha1 FROM " + tableName).Rows) { if (row.IsNull("sha1") == true) continue; string sha1 = ((string)row["sha1"]).ToUpper(); if (databaseHashes.Contains(sha1) == false) databaseHashes.Add(sha1); } } ReportDiff(databaseHashes, storeHashes, "Database", "FileStore", false); } public static void LoadSoftware(string directory, string storeDirectory, string tempDirectory) { //int count = 10; foreach (string listDirectory in Directory.GetDirectories(directory)) { string softwareListName = Path.GetFileName(listDirectory); string indexName = listDirectory + ".txt"; LoadRoms(indexName, storeDirectory, tempDirectory); Spludlow.Log.Info("MAME LoadSoftware: " + listDirectory); //if (++count == 10) // break; } Spludlow.Log.Finish("MAME LoadSoftware"); } public static void TestExtract(string zipFilename, string tempDirectory) { string parentMachineName = Path.GetFileNameWithoutExtension(zipFilename); string archiveDirectory; archiveDirectory = tempDirectory + @"\" + parentMachineName; Directory.CreateDirectory(archiveDirectory); Spludlow.Archive.Extract(zipFilename, archiveDirectory); Directory.Delete(archiveDirectory, true); //using (Spludlow.TempDirectory tempDir = new Spludlow.TempDirectory(tempDirectory)) //{ // archiveDirectory = tempDir.Path + @"\" + parentMachineName; // Directory.CreateDirectory(archiveDirectory); // Spludlow.Archive.Extract(zipFilename, archiveDirectory); // //foreach (string filename in Directory.GetFiles(archiveDirectory, "*", SearchOption.AllDirectories)) // // File.SetAttributes(filename, FileAttributes.Normal); //} } /// /// STEP 1 /// Create a text table index for the contents all rom zips, result filename is directory + ".txt" /// Use null for the temp dir or a RAM disk for speed /// public static void IndexRomDirectory(string directory, string tempDirectory) { DataTable resultsTable = Spludlow.Data.TextTable.ReadText(new string[] { "RomFileId ArchiveName MachineName RomName SHA1 Length", "Int32 String String String String Int64", }); int id = 0; List badList = new List(); foreach (string zipFilename in Directory.GetFiles(directory)) { string parentMachineName = Path.GetFileNameWithoutExtension(zipFilename); string archiveDirectory; using (Spludlow.TempDirectory tempDir = new Spludlow.TempDirectory(tempDirectory)) { List romFilenames = new List(); try { archiveDirectory = tempDir.Path + @"\" + parentMachineName; Directory.CreateDirectory(archiveDirectory); Spludlow.Archive.Extract(zipFilename, archiveDirectory); foreach (string filename in Directory.GetFiles(archiveDirectory, "*", SearchOption.AllDirectories)) File.SetAttributes(filename, FileAttributes.Normal); string[] filenames; string[] subDirs; filenames = Directory.GetFiles(archiveDirectory); if (filenames.Length == 0) throw new ApplicationException("No filenames in archive root: " + zipFilename); foreach (string filename in filenames) romFilenames.Add(filename); subDirs = Directory.GetDirectories(archiveDirectory); foreach (string subDirectory in subDirs) { string childMachine = Path.GetFileName(subDirectory); if (childMachine == parentMachineName) throw new ApplicationException("Parent name in child: " + zipFilename); if (Directory.GetDirectories(subDirectory).Length > 0) throw new ApplicationException("Sub directories in child: " + zipFilename); filenames = Directory.GetFiles(subDirectory); if (filenames.Length == 0) throw new ApplicationException("No filenames in child: " + zipFilename); foreach (string filename in filenames) romFilenames.Add(filename); } } catch { badList.Add(zipFilename); continue; } foreach (string filename in romFilenames) { string machineName = Path.GetFileName(Path.GetDirectoryName(filename)); string romName = filename.Substring(archiveDirectory.Length + 1); string sha1 = Spludlow.Hashing.SHA1HexFile(filename); long length = Spludlow.Io.Files.FileLength(filename); resultsTable.Rows.Add(++id, parentMachineName, machineName, romName, sha1, length); } } } Spludlow.Data.TextTable.Write(directory + ".txt", resultsTable, Encoding.UTF8); if (badList.Count > 0) { File.WriteAllLines(directory + ".bad.txt", badList.ToArray()); } } /// /// STEP 2 /// Import roms that the SHA1 store does not already contain /// Using the Index from STEP 1 only the zips required are extracted /// Use null for the temp dir or a RAM disk for speed /// public static void LoadRoms(string romIndexFilename, string storeDirectory, string tempDirectory) { string sourceDirectory = romIndexFilename.Substring(0, romIndexFilename.Length - 4); DataTable table = Spludlow.Data.TextTable.ReadFile(romIndexFilename, Encoding.UTF8); HashSet hashes = ReadExistingHashes(storeDirectory); HashSet wantedArchives = new HashSet(); foreach (DataRow row in table.Rows) { string sha1 = (string)row["SHA1"]; if (hashes.Contains(sha1) == true) continue; string archive = (string)row["ArchiveName"]; if (wantedArchives.Contains(archive) == false) wantedArchives.Add(archive); } int startCount = hashes.Count; foreach (string archive in wantedArchives) { string sourceZipFilename = sourceDirectory + @"\" + archive + ".zip"; if (File.Exists(sourceZipFilename) == false) throw new ApplicationException("sourceZipFilename: " + sourceZipFilename); using (Spludlow.TempDirectory tempDir = new Spludlow.TempDirectory(tempDirectory)) { Spludlow.Archive.Extract(sourceZipFilename, tempDir.Path); foreach (string filename in Directory.GetFiles(tempDir.Path, "*", SearchOption.AllDirectories)) File.SetAttributes(filename, FileAttributes.Normal); foreach (DataRow row in table.Select("ArchiveName = '" + archive + "'")) { string sha1 = (string)row["SHA1"]; if (hashes.Contains(sha1) == true) continue; string romName = (string)row["RomName"]; string sourceFilename = tempDir + @"\" + romName; if (File.Exists(sourceFilename) == false) throw new ApplicationException("sourceFilename: " + sourceFilename); string targetFilename = HashStoreFilename(sha1, storeDirectory, true); if (File.Exists(targetFilename) == true) throw new ApplicationException("targetFilename already there: " + targetFilename); File.Copy(sourceFilename, targetFilename); hashes.Add(sha1); } } } int addCount = hashes.Count - startCount; Spludlow.Log.Report("LoadRoms: " + romIndexFilename + ", submit:" + table.Rows.Count + ", add:" + addCount + ", stored:" + hashes.Count); } /// /// STEP 3 (Optional) /// After importing from a rom set source you can use this to remove everything that was taken from the source /// Will leave what's left from ".bad.txt" (Step 1) so you can go back and fix any rom zips that failed and run the import again /// /// NOTE: if .bad.txt is not found the passed directory will simpily be deleted /// /// NOTE: .bad.txt conatins full paths (so don't move things around) /// /// NOTE: Rename the fixed rom directory before running step 1 on it again or the previous index file will be wiped out /// public static void DeleteImportedRoms(string directory) { string badFilename = directory + ".bad.txt"; if (File.Exists(badFilename) == false) { Directory.Delete(directory, true); } else { HashSet badFilenames = new HashSet(File.ReadAllLines(badFilename)); foreach (string filename in Directory.GetFiles(directory)) { if (badFilenames.Contains(filename) == false) File.Delete(filename); } } } /// /// Perform steps 1 to 3 on all sub directories /// So you can do a big batch and go to the pub /// /// NOTE: The temp directory should be big engough to hold the largest extacted zip /// /// NOTE: If a rom zip fails due to the RAM disk not being big enough it will end up in .bat.txt so you can run it again without a RAM disk /// public static void FullImportSubDirectoryRomSets(string rootDirectory, string storeDirectory, string tempDirectory) { foreach (string directory in Directory.GetDirectories(rootDirectory)) { IndexRomDirectory(directory, tempDirectory); string indexFilename = directory + ".txt"; LoadRoms(indexFilename, storeDirectory, tempDirectory); DeleteImportedRoms(directory); } } public static void CreateMachineRomSets(string targetDirectory, bool reportMode) { string connectionString = "@MameMachines"; string storeDirectory = Spludlow.Config.Get("Spludlow.Mame.HashStore.MachineRom"); string tempDirectory = ""; CreateMachineRomSets(connectionString, targetDirectory, storeDirectory, tempDirectory, reportMode); } public static void CreateMachineRomSets(string targetDirectory, bool reportMode, string tempDirectory) { string connectionString = "@MameMachines"; string storeDirectory = Spludlow.Config.Get("Spludlow.Mame.HashStore.MachineRom"); CreateMachineRomSets(connectionString, targetDirectory, storeDirectory, tempDirectory, reportMode); } /// /// Make all MAME machine rom set zips posible from the SAH1 store roms /// The target zip will be created even if roms are missing (see report) /// A text table report is saved in the target directory /// Use null for the temp dir or a RAM disk for speed /// public static void CreateMachineRomSets(string connectionString, string targetDirectory, string storeDirectory, string tempDirectory, bool reportMode) { Spludlow.Data.IDAL database = Spludlow.Data.DAL.Create(connectionString); HashSet storeHashes = ReadExistingHashes(storeDirectory); DataTable machineTable = database.Select("SELECT * FROM machine"); DataTable romTable = database.Select("SELECT * FROM rom"); DataTable resultsTable = Spludlow.Data.TextTable.ReadText(new string[] { "Machine Description Year Parent ParentCount RomRowCount ParentRomCount ZipRomCount MissingRomCount", "String String String String Int32 Int32 Int32 Int32 Int32", }); DataTable missingRomTable = Spludlow.Data.TextTable.ReadText(new string[] { "Machine Parent SHA1", "String String String", }); foreach (DataRow machineRow in machineTable.Rows) { int machine_id = (int)machineRow["machine_id"]; DataRow[] romRows = romTable.Select("machine_id = " + machine_id); if (romRows.Length == 0) continue; string machineName = (string)machineRow["name"]; string description = ""; if (machineRow.IsNull("description") == false) description = (string)machineRow["description"]; string year = ""; if (machineRow.IsNull("year") == false) year = (string)machineRow["year"]; string parent = ""; if (machineRow.IsNull("romof") == false) parent = (string)machineRow["romof"]; HashSet parentHashes = new HashSet(); int parentCount = CheckParents(0, machineRow, parentHashes, romTable); int parentRomCount = 0; int missingRomCount = 0; List useRomRows = new List(); foreach (DataRow romRow in romRows) { if (romRow.IsNull("sha1") == true) continue; string sha1 = ((string)romRow["sha1"]).ToUpper(); if (storeHashes.Contains(sha1) == false) { missingRomTable.Rows.Add(machineName, parent, sha1); ++missingRomCount; continue; } if (parentHashes.Contains(sha1) == true) ++parentRomCount; else useRomRows.Add(romRow); } if (reportMode == false && useRomRows.Count > 0) { string targetZipFilename = targetDirectory + @"\" + machineName + ".zip"; if (File.Exists(targetZipFilename) == false) { using (Spludlow.TempDirectory tempDir = new TempDirectory(tempDirectory)) { foreach (DataRow romRow in useRomRows) { string sha1 = ((string)romRow["sha1"]).ToUpper(); string name = (string)romRow["name"]; long size = Int64.Parse((string)romRow["size"]); string sourceFilename = HashStoreFilename(sha1, storeDirectory, false); long fileSize = Spludlow.Io.Files.FileLength(sourceFilename); if (size != fileSize) throw new ApplicationException("Bad length: " + sha1 + ", actual:" + fileSize + ", record:" + size); string targetFilename = tempDir.Path + @"\" + name; if (File.Exists(targetFilename) == true) { // need to check sha1 same !!! not sure why this is haperning? File.Delete(targetFilename); Spludlow.Log.Warning("Rom name already exists: " + machineName + ", " + name); } File.Copy(sourceFilename, targetFilename); } Spludlow.Archive.Create(targetZipFilename, tempDir.Path + @"\*"); } } } resultsTable.Rows.Add(machineName, description, year, parent, parentCount, romRows.Length, parentRomCount, useRomRows.Count, missingRomCount); } string reportName = "_" + Spludlow.Text.TimeStamp(); Spludlow.Data.TextTable.Write(targetDirectory + @"\" + reportName + ".txt", resultsTable); Spludlow.Data.TextTable.Write(targetDirectory + @"\" + reportName + ".NoRoms.txt", missingRomTable); int missingCount = 0; int missingTotal = 0; int numParents = 0; int missingCountParents = 0; int missingTotalParents = 0; foreach (DataRow row in resultsTable.Rows) { int missing = (int)row["MissingRomCount"]; if (missing > 0) ++missingCount; missingTotal += missing; int parentCount = (int)row["ParentCount"]; if (parentCount == 0) { if (missing > 0) ++missingCountParents; missingTotalParents += missing; ++numParents; } } DataView viewParents = new DataView(resultsTable); viewParents.RowFilter = "ParentCount = 0 AND MissingRomCount > 0"; viewParents.Sort = "Machine"; DataView viewChildren = new DataView(resultsTable); viewChildren.RowFilter = "ParentCount > 0 AND MissingRomCount > 0"; viewChildren.Sort = "Parent, Machine"; StringBuilder text = new StringBuilder(); text.AppendLine("Total Machines: " + resultsTable.Rows.Count); text.AppendLine("All Machines missing ROMS: " + missingCount); text.AppendLine("All machines complete ROMS: " + (resultsTable.Rows.Count - missingCount)); text.AppendLine("All missing ROMS: " + missingTotal); text.AppendLine(); text.AppendLine("Total Machines: " + numParents); text.AppendLine("Parent Machines missing ROMS: " + missingCountParents); text.AppendLine("Parent machines complete ROMS: " + (numParents - missingCountParents)); text.AppendLine("Parent missing ROMS: " + missingTotalParents); Spludlow.Log.Report("CreateMachineRomSets", text.ToString(), viewParents, viewChildren, missingRomTable); } /// /// Recursively traverse ancestors to add sha1s to the HashSet /// Used to see what roms don't need including in a machine's set as parent(s) already have them /// private static int CheckParents(int count, DataRow machineRow, HashSet parentHashes, DataTable romTable) { if (machineRow.IsNull("romof") == true) return count; string name = (string)machineRow["name"]; string parent = (string)machineRow["romof"]; DataTable table = machineRow.Table; DataRow[] rows = table.Select("name = '" + parent + "'"); if (rows.Length != 1) throw new ApplicationException("CheckParentrs: " + name + ", parent:" + parent); DataRow[] romRows = romTable.Select("machine_id = " + rows[0]["machine_id"]); foreach (DataRow romRow in romRows) { if (romRow.IsNull("sha1") == true) continue; string sha1 = ((string)romRow["sha1"]).ToUpper(); if (parentHashes.Contains(sha1) == false) parentHashes.Add(sha1); } return CheckParents(++count, rows[0], parentHashes, romTable); } /// /// Get a hash set of existing roms in the store /// public static HashSet ReadExistingHashes(string storeDirectory) { HashSet hashes = new HashSet(); foreach (string filename in Directory.GetFiles(storeDirectory, "*", SearchOption.AllDirectories)) hashes.Add(Path.GetFileName(filename)); return hashes; } /// /// Get SHA1 Store Filename /// Using first 2 chars for directory balancing (prevent too many files in single directory) /// public static string HashStoreFilename(string sha1, string storeDirectory, bool writeMode) { if (sha1.Length != 40) throw new ApplicationException("Bad sha1: " + sha1); StringBuilder path = new StringBuilder(); path.Append(storeDirectory); path.Append(@"\"); path.Append(sha1.Substring(0, 2)); path.Append(@"\"); path.Append(sha1); string filename = path.ToString(); if (writeMode == true) { string directory = Path.GetDirectoryName(filename); if (Directory.Exists(directory) == false) Directory.CreateDirectory(directory); } return filename; } /// /// Example command text. Always do "SELECT * FROM machine" you can change the WHERE and ORDER BY to suit your needs /// Go for all machines in the 1980s that have no parents (the master that all derivatives reference, otherwise you get loads of variations of the same game) /// Order by manufacturer then year /// public static void MameBatFile1980sParents(string targetFilename, string connectionString) { string commandText = "SELECT * FROM machine WHERE ((machine.year LIKE '198%') AND (machine.romof IS NULL)) ORDER BY machine.manufacturer, machine.year, machine.name"; MameBatFile(targetFilename, connectionString, commandText); } /// /// Create a BAT file to sequentially run a bunch of MAME games /// When in MAME press ESCAPE to quit to next game. /// If ESCAPE fails do ALT + TAB then right click on MAME in Windows TaskBar to close /// To stop do ALT + TAB then make sure the command prompt has focus and do CRTL + C to terminate the BAT /// Edit the BAT in notepad to chop out what you dont want /// public static void MameBatFile(string targetFilename, string connectionString, string commandText) { Spludlow.Data.IDAL database = Spludlow.Data.DAL.Create(connectionString); DataTable table = database.Select(commandText); StringBuilder result = new StringBuilder(); foreach (DataRow row in table.Rows) { string name = (string)row["name"]; string manufacturer = (string)row["manufacturer"]; string year = (string)row["year"]; string description = (string)row["description"]; result.AppendLine("REM manufacturer:" + manufacturer + " year:" + year + " description:" + description + " name:" + name); result.AppendLine("mame64.exe " + name); } File.WriteAllText(targetFilename, result.ToString()); } /// /// Merge hash store, add to master what haven't got from source /// public static void MergeHashStore(string masterDirectory, string sourceDirectory) { HashSet masterHashes = ReadExistingHashes(masterDirectory); HashSet sourceHashes = ReadExistingHashes(sourceDirectory); int count = 0; foreach (string sha1 in sourceHashes) { if (masterHashes.Contains(sha1) == true) continue; File.Copy(HashStoreFilename(sha1, sourceDirectory, false), HashStoreFilename(sha1, masterDirectory, true)); ++count; } Spludlow.Log.Report("MergeHashStore, master:" + masterHashes.Count + ", source:" + sourceHashes.Count + ", copy:" + count); } public static void ReportDiff(HashSet listA, HashSet listB, string nameA, string nameB, bool reportData) { HashSet notInA = new HashSet(); HashSet notInB = new HashSet(); HashSet inBoth = new HashSet(); HashSet[] lists = new HashSet[] { notInA, notInB, inBoth }; string[] lables = new string[] { "In @A, missing from @B", "In @B, missing from @A", "In both" }; for (int index = 0; index < lables.Length; ++index) { lables[index] = lables[index].Replace("@A", nameA).Replace("@B", nameB); } foreach (string a in listA) { if (listB.Contains(a) == true) inBoth.Add(a); else notInB.Add(a); } foreach (string b in listB) { if (listA.Contains(b) == false) notInA.Add(b); } DataSet dataSet = new DataSet(); if (reportData == true) { for (int index = 0; index < 3; ++index) { DataTable table = new DataTable(); table.Columns.Add("Value", typeof(string)); table.TableName = lables[index]; foreach (string item in lists[index]) table.Rows.Add(item); dataSet.Tables.Add(table); } } StringBuilder info = new StringBuilder(); info.AppendLine(nameA + " Total " + listA.Count); info.AppendLine(nameB + " Total " + listB.Count); for (int index = 0; index < 3; ++index) info.AppendLine(lables[index] + " " + lists[index].Count); Spludlow.Log.Report("ReportDiff", info.ToString(), dataSet); } } }