993 lines
34 KiB
C#
993 lines
34 KiB
C#
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using System.Linq;
|
|
|
|
using BizHawk.Common;
|
|
using BizHawk.Common.IOExtensions;
|
|
using BizHawk.Common.StringExtensions;
|
|
using BizHawk.Emulation.Common;
|
|
using BizHawk.Emulation.Cores;
|
|
using BizHawk.Emulation.Cores.Libretro;
|
|
using BizHawk.Emulation.Cores.Nintendo.Sameboy;
|
|
using BizHawk.Emulation.Cores.Nintendo.SNES;
|
|
using BizHawk.Emulation.Cores.Sony.PSX;
|
|
using BizHawk.Emulation.Cores.Arcades.MAME;
|
|
using BizHawk.Emulation.DiscSystem;
|
|
|
|
namespace BizHawk.Client.Common
|
|
{
|
|
public class RomLoader
|
|
{
|
|
private class DiscAsset : IDiscAsset
|
|
{
|
|
public Disc DiscData { get; set; }
|
|
public DiscType DiscType { get; set; }
|
|
public string DiscName { get; set; }
|
|
}
|
|
private class RomAsset : IRomAsset
|
|
{
|
|
public byte[] RomData { get; set; }
|
|
public byte[] FileData { get; set; }
|
|
public string Extension { get; set; }
|
|
public string RomPath { get; set; }
|
|
public GameInfo Game { get; set; }
|
|
}
|
|
private class CoreInventoryParameters : ICoreInventoryParameters
|
|
{
|
|
private readonly RomLoader _parent;
|
|
public CoreInventoryParameters(RomLoader parent)
|
|
{
|
|
_parent = parent;
|
|
}
|
|
public CoreComm Comm { get; set; }
|
|
|
|
public GameInfo Game { get; set; }
|
|
|
|
public List<IRomAsset> Roms { get; set; } = new List<IRomAsset>();
|
|
|
|
public List<IDiscAsset> Discs { get; set; } = new List<IDiscAsset>();
|
|
|
|
public bool DeterministicEmulationRequested => _parent.Deterministic;
|
|
|
|
public object FetchSettings(Type emulatorType, Type settingsType)
|
|
=> _parent.GetCoreSettings(emulatorType, settingsType);
|
|
|
|
public object FetchSyncSettings(Type emulatorType, Type syncSettingsType)
|
|
=> _parent.GetCoreSyncSettings(emulatorType, syncSettingsType);
|
|
}
|
|
private readonly Config _config;
|
|
|
|
public RomLoader(Config config)
|
|
{
|
|
_config = config;
|
|
}
|
|
|
|
public enum LoadErrorType
|
|
{
|
|
Unknown, MissingFirmware, Xml, DiscError
|
|
}
|
|
|
|
// helper methods for the settings events
|
|
private TSetting GetCoreSettings<TCore, TSetting>()
|
|
where TCore : IEmulator
|
|
{
|
|
return (TSetting)GetCoreSettings(typeof(TCore), typeof(TSetting));
|
|
}
|
|
|
|
private TSync GetCoreSyncSettings<TCore, TSync>()
|
|
where TCore : IEmulator
|
|
{
|
|
return (TSync)GetCoreSyncSettings(typeof(TCore), typeof(TSync));
|
|
}
|
|
|
|
private object GetCoreSettings(Type t, Type settingsType)
|
|
{
|
|
var e = new SettingsLoadArgs(t, settingsType);
|
|
if (OnLoadSettings == null)
|
|
throw new InvalidOperationException("Frontend failed to provide a settings getter");
|
|
OnLoadSettings(this, e);
|
|
if (e.Settings != null && e.Settings.GetType() != settingsType)
|
|
throw new InvalidOperationException($"Frontend did not provide the requested settings type: Expected {settingsType}, got {e.Settings.GetType()}");
|
|
return e.Settings;
|
|
}
|
|
|
|
private object GetCoreSyncSettings(Type t, Type syncSettingsType)
|
|
{
|
|
var e = new SettingsLoadArgs(t, syncSettingsType);
|
|
if (OnLoadSyncSettings == null)
|
|
throw new InvalidOperationException("Frontend failed to provide a sync settings getter");
|
|
OnLoadSyncSettings(this, e);
|
|
if (e.Settings != null && e.Settings.GetType() != syncSettingsType)
|
|
throw new InvalidOperationException($"Frontend did not provide the requested sync settings type: Expected {syncSettingsType}, got {e.Settings.GetType()}");
|
|
return e.Settings;
|
|
}
|
|
|
|
// For not throwing errors but simply outputting information to the screen
|
|
public Action<string, int?> MessageCallback { get; set; }
|
|
|
|
// TODO: reconsider the need for exposing these;
|
|
public IEmulator LoadedEmulator { get; private set; }
|
|
public GameInfo Game { get; private set; }
|
|
public RomGame Rom { get; private set; }
|
|
public string CanonicalFullPath { get; private set; }
|
|
|
|
public bool Deterministic { get; set; }
|
|
|
|
public class RomErrorArgs : EventArgs
|
|
{
|
|
// TODO: think about naming here, what to pass, a lot of potential good information about what went wrong could go here!
|
|
public RomErrorArgs(string message, string systemId, LoadErrorType type)
|
|
{
|
|
Message = message;
|
|
AttemptedCoreLoad = systemId;
|
|
Type = type;
|
|
}
|
|
|
|
public RomErrorArgs(string message, string systemId, string path, bool? det, LoadErrorType type)
|
|
: this(message, systemId, type)
|
|
{
|
|
Deterministic = det;
|
|
RomPath = path;
|
|
}
|
|
|
|
public string Message { get; }
|
|
public string AttemptedCoreLoad { get; }
|
|
public string RomPath { get; }
|
|
public bool? Deterministic { get; set; }
|
|
public bool Retry { get; set; }
|
|
public LoadErrorType Type { get; }
|
|
}
|
|
|
|
public class SettingsLoadArgs : EventArgs
|
|
{
|
|
public object Settings { get; set; }
|
|
public Type Core { get; }
|
|
public Type SettingsType { get; }
|
|
public SettingsLoadArgs(Type t, Type s)
|
|
{
|
|
Core = t;
|
|
SettingsType = s;
|
|
Settings = null;
|
|
}
|
|
}
|
|
|
|
public delegate void SettingsLoadEventHandler(object sender, SettingsLoadArgs e);
|
|
public event SettingsLoadEventHandler OnLoadSettings;
|
|
public event SettingsLoadEventHandler OnLoadSyncSettings;
|
|
|
|
public delegate void LoadErrorEventHandler(object sender, RomErrorArgs e);
|
|
public event LoadErrorEventHandler OnLoadError;
|
|
|
|
public Func<HawkFile, int?> ChooseArchive { get; set; }
|
|
|
|
public Func<RomGame, string> ChoosePlatform { get; set; }
|
|
|
|
// in case we get sent back through the picker more than once, use the same choice the second time
|
|
private int? _previousChoice;
|
|
private int? HandleArchive(HawkFile file)
|
|
{
|
|
if (_previousChoice.HasValue)
|
|
{
|
|
return _previousChoice;
|
|
}
|
|
|
|
if (ChooseArchive != null)
|
|
{
|
|
_previousChoice = ChooseArchive(file);
|
|
return _previousChoice;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// May want to phase out this method in favor of the overload with more parameters
|
|
private void DoLoadErrorCallback(string message, string systemId, LoadErrorType type = LoadErrorType.Unknown)
|
|
{
|
|
OnLoadError?.Invoke(this, new RomErrorArgs(message, systemId, type));
|
|
}
|
|
|
|
private void DoLoadErrorCallback(string message, string systemId, string path, bool det, LoadErrorType type = LoadErrorType.Unknown)
|
|
{
|
|
OnLoadError?.Invoke(this, new RomErrorArgs(message, systemId, path, det, type));
|
|
}
|
|
|
|
public IOpenAdvanced OpenAdvanced { get; set; }
|
|
|
|
private bool HandleArchiveBinding(HawkFile file)
|
|
{
|
|
// try binding normal rom extensions first
|
|
if (!file.IsBound)
|
|
{
|
|
file.BindSoleItemOf(RomFileExtensions.AutoloadFromArchive);
|
|
}
|
|
// ...including unrecognised extensions that the user has set a platform for
|
|
if (!file.IsBound)
|
|
{
|
|
var exts = _config.PreferredPlatformsForExtensions.Where(static kvp => !string.IsNullOrEmpty(kvp.Value))
|
|
.Select(static kvp => kvp.Key)
|
|
.ToList();
|
|
if (exts.Count is not 0) file.BindSoleItemOf(exts);
|
|
}
|
|
|
|
// if we have an archive and need to bind something, then pop the dialog
|
|
if (file.IsArchive && !file.IsBound)
|
|
{
|
|
int? result = HandleArchive(file);
|
|
if (result.HasValue)
|
|
{
|
|
file.BindArchiveMember(result.Value);
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
CanonicalFullPath = file.CanonicalFullPath;
|
|
|
|
return true;
|
|
}
|
|
|
|
private GameInfo MakeGameFromDisc(Disc disc, string ext, string name)
|
|
{
|
|
// TODO - use more sophisticated IDer
|
|
var discType = new DiscIdentifier(disc).DetectDiscType();
|
|
var discHasher = new DiscHasher(disc);
|
|
var discHash = discHasher.CalculateBizHash(discType);
|
|
|
|
var game = Database.CheckDatabase(discHash);
|
|
if (game is not null) return game;
|
|
// else try to use our wizard methods
|
|
game = new GameInfo { Name = name, Hash = discHash };
|
|
Exception NoCoreForSystem(string sysID)
|
|
{
|
|
// no supported emulator core for these (yet)
|
|
game.System = sysID;
|
|
return new NoAvailableCoreException(sysID);
|
|
}
|
|
switch (discType)
|
|
{
|
|
case DiscType.SegaSaturn:
|
|
game.System = VSystemID.Raw.SAT;
|
|
break;
|
|
case DiscType.MegaCD:
|
|
game.System = VSystemID.Raw.GEN;
|
|
break;
|
|
case DiscType.PCFX:
|
|
game.System = VSystemID.Raw.PCFX;
|
|
break;
|
|
|
|
case DiscType.TurboGECD:
|
|
case DiscType.TurboCD:
|
|
game.System = VSystemID.Raw.PCE;
|
|
break;
|
|
|
|
case DiscType.JaguarCD:
|
|
game.System = VSystemID.Raw.Jaguar;
|
|
break;
|
|
|
|
case DiscType.Amiga:
|
|
throw NoCoreForSystem(VSystemID.Raw.Amiga);
|
|
case DiscType.CDi:
|
|
throw NoCoreForSystem(VSystemID.Raw.PhillipsCDi);
|
|
case DiscType.Dreamcast:
|
|
throw NoCoreForSystem(VSystemID.Raw.Dreamcast);
|
|
case DiscType.GameCube:
|
|
throw NoCoreForSystem(VSystemID.Raw.GameCube);
|
|
case DiscType.NeoGeoCD:
|
|
throw NoCoreForSystem(VSystemID.Raw.NeoGeoCD);
|
|
case DiscType.Panasonic3DO:
|
|
throw NoCoreForSystem(VSystemID.Raw.Panasonic3DO);
|
|
case DiscType.Playdia:
|
|
throw NoCoreForSystem(VSystemID.Raw.Playdia);
|
|
case DiscType.SonyPS2:
|
|
throw NoCoreForSystem(VSystemID.Raw.PS2);
|
|
case DiscType.SonyPSP:
|
|
throw NoCoreForSystem(VSystemID.Raw.PSP);
|
|
case DiscType.Wii:
|
|
throw NoCoreForSystem(VSystemID.Raw.Wii);
|
|
|
|
case DiscType.AudioDisc:
|
|
case DiscType.UnknownCDFS:
|
|
case DiscType.UnknownFormat:
|
|
game.System = _config.TryGetChosenSystemForFileExt(ext, out var sysID) ? sysID : VSystemID.Raw.NULL;
|
|
break;
|
|
|
|
default: //"for an unknown disc, default to psx instead of pce-cd, since that is far more likely to be what they are attempting to open" [5e07ab3ec3b8b8de9eae71b489b55d23a3909f55, year 2015]
|
|
case DiscType.SonyPSX:
|
|
game.System = VSystemID.Raw.PSX;
|
|
break;
|
|
}
|
|
return game;
|
|
}
|
|
|
|
private bool LoadDisc(string path, CoreComm nextComm, HawkFile file, string ext, string forcedCoreName, out IEmulator nextEmulator, out GameInfo game)
|
|
{
|
|
var disc = DiscExtensions.CreateAnyType(path, str => DoLoadErrorCallback(str, "???", LoadErrorType.DiscError));
|
|
if (disc == null)
|
|
{
|
|
game = null;
|
|
nextEmulator = null;
|
|
return false;
|
|
}
|
|
|
|
game = MakeGameFromDisc(disc, ext, Path.GetFileNameWithoutExtension(file.Name));
|
|
|
|
var cip = new CoreInventoryParameters(this)
|
|
{
|
|
Comm = nextComm,
|
|
Game = game,
|
|
Discs =
|
|
{
|
|
new DiscAsset
|
|
{
|
|
DiscData = disc,
|
|
DiscType = new DiscIdentifier(disc).DetectDiscType(),
|
|
DiscName = Path.GetFileNameWithoutExtension(path)
|
|
}
|
|
},
|
|
};
|
|
nextEmulator = MakeCoreFromCoreInventory(cip, forcedCoreName);
|
|
return true;
|
|
}
|
|
|
|
private void LoadM3U(string path, CoreComm nextComm, HawkFile file, string forcedCoreName, out IEmulator nextEmulator, out GameInfo game)
|
|
{
|
|
M3U_File m3u;
|
|
using (var sr = new StreamReader(path))
|
|
m3u = M3U_File.Read(sr);
|
|
if (m3u.Entries.Count == 0)
|
|
throw new InvalidOperationException("Can't load an empty M3U");
|
|
m3u.Rebase(Path.GetDirectoryName(path));
|
|
|
|
var discs = m3u.Entries
|
|
.Select(e => e.Path)
|
|
.Where(p => Disc.IsValidExtension(Path.GetExtension(p)))
|
|
.Select(p => (p, d: DiscExtensions.CreateAnyType(p, str => DoLoadErrorCallback(str, "???", LoadErrorType.DiscError))))
|
|
.Where(a => a.d != null)
|
|
.Select(a => (IDiscAsset)new DiscAsset
|
|
{
|
|
DiscData = a.d,
|
|
DiscType = new DiscIdentifier(a.d).DetectDiscType(),
|
|
DiscName = Path.GetFileNameWithoutExtension(a.p)
|
|
})
|
|
.ToList();
|
|
if (discs.Count == 0)
|
|
throw new InvalidOperationException("Couldn't load any contents of the M3U as discs");
|
|
|
|
game = MakeGameFromDisc(discs[0].DiscData, Path.GetExtension(m3u.Entries[0].Path), discs[0].DiscName);
|
|
var cip = new CoreInventoryParameters(this)
|
|
{
|
|
Comm = nextComm,
|
|
Game = game,
|
|
Discs = discs
|
|
};
|
|
nextEmulator = MakeCoreFromCoreInventory(cip, forcedCoreName);
|
|
}
|
|
|
|
private IEmulator MakeCoreFromCoreInventory(CoreInventoryParameters cip, string forcedCoreName = null)
|
|
{
|
|
IReadOnlyCollection<CoreInventory.Core> cores;
|
|
if (forcedCoreName != null)
|
|
{
|
|
var singleCore = CoreInventory.Instance.GetCores(cip.Game.System).SingleOrDefault(c => c.Name == forcedCoreName);
|
|
cores = singleCore != null ? new[] { singleCore } : Array.Empty<CoreInventory.Core>();
|
|
}
|
|
else
|
|
{
|
|
_ = _config.PreferredCores.TryGetValue(cip.Game.System, out var preferredCore);
|
|
var dbForcedCoreName = cip.Game.ForcedCore;
|
|
cores = CoreInventory.Instance.GetCores(cip.Game.System)
|
|
.OrderBy(c =>
|
|
{
|
|
if (c.Name == preferredCore)
|
|
{
|
|
return (int)CorePriority.UserPreference;
|
|
}
|
|
|
|
if (string.Equals(c.Name, dbForcedCoreName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return (int)CorePriority.GameDbPreference;
|
|
}
|
|
|
|
return (int)c.Priority;
|
|
})
|
|
.ToList();
|
|
if (cores.Count == 0) throw new InvalidOperationException("No core was found to try on the game");
|
|
}
|
|
var exceptions = new List<Exception>();
|
|
foreach (var core in cores)
|
|
{
|
|
try
|
|
{
|
|
return core.Create(cip);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
if (_config.DontTryOtherCores || e is MissingFirmwareException || e.InnerException is MissingFirmwareException)
|
|
throw;
|
|
exceptions.Add(e);
|
|
}
|
|
}
|
|
throw new AggregateException("No core could load the game", exceptions);
|
|
}
|
|
|
|
private void LoadOther(
|
|
CoreComm nextComm,
|
|
HawkFile file,
|
|
string ext,
|
|
string forcedCoreName,
|
|
out IEmulator nextEmulator,
|
|
out RomGame rom,
|
|
out GameInfo game,
|
|
out bool cancel)
|
|
{
|
|
cancel = false;
|
|
rom = new RomGame(file);
|
|
|
|
// hacky for now
|
|
rom.GameInfo.System = ext switch
|
|
{
|
|
".exe" => VSystemID.Raw.PSX,
|
|
".nsf" => VSystemID.Raw.NES,
|
|
".gbs" => VSystemID.Raw.GB,
|
|
_ => rom.GameInfo.System,
|
|
};
|
|
|
|
Util.DebugWriteLine(rom.GameInfo.System);
|
|
|
|
if (string.IsNullOrEmpty(rom.GameInfo.System))
|
|
{
|
|
// Has the user picked a preference for this extension?
|
|
if (_config.TryGetChosenSystemForFileExt(rom.Extension.ToLowerInvariant(), out var systemID))
|
|
{
|
|
rom.GameInfo.System = systemID;
|
|
}
|
|
else if (ChoosePlatform != null)
|
|
{
|
|
var result = ChoosePlatform(rom);
|
|
if (!string.IsNullOrEmpty(result))
|
|
{
|
|
rom.GameInfo.System = result;
|
|
}
|
|
else
|
|
{
|
|
cancel = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
game = rom.GameInfo;
|
|
|
|
nextEmulator = null;
|
|
if (game.System == null)
|
|
return; // The user picked nothing in the Core picker
|
|
|
|
switch (game.System)
|
|
{
|
|
case VSystemID.Raw.GB:
|
|
case VSystemID.Raw.GBC:
|
|
if (ext == ".gbs")
|
|
{
|
|
nextEmulator = new Sameboy(
|
|
nextComm,
|
|
rom.GameInfo,
|
|
rom.FileData,
|
|
GetCoreSettings<Sameboy, Sameboy.SameboySettings>(),
|
|
GetCoreSyncSettings<Sameboy, Sameboy.SameboySyncSettings>()
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (_config.GbAsSgb)
|
|
{
|
|
game.System = VSystemID.Raw.SGB;
|
|
}
|
|
break;
|
|
}
|
|
var cip = new CoreInventoryParameters(this)
|
|
{
|
|
Comm = nextComm,
|
|
Game = game,
|
|
Roms =
|
|
{
|
|
new RomAsset
|
|
{
|
|
RomData = rom.RomData,
|
|
FileData = rom.FileData,
|
|
Extension = rom.Extension,
|
|
RomPath = file.FullPathWithoutMember,
|
|
Game = game
|
|
}
|
|
},
|
|
};
|
|
nextEmulator = MakeCoreFromCoreInventory(cip, forcedCoreName);
|
|
}
|
|
|
|
private void LoadPSF(string path, CoreComm nextComm, HawkFile file, out IEmulator nextEmulator, out RomGame rom, out GameInfo game)
|
|
{
|
|
// TODO: Why does the PSF loader need CbDeflater provided? Surely this is a matter internal to it.
|
|
static byte[] CbDeflater(Stream instream, int size)
|
|
{
|
|
return new GZipStream(instream, CompressionMode.Decompress).ReadAllBytes();
|
|
}
|
|
var psf = new PSF();
|
|
psf.Load(path, CbDeflater);
|
|
nextEmulator = new Octoshock(
|
|
nextComm,
|
|
psf,
|
|
GetCoreSettings<Octoshock, Octoshock.Settings>(),
|
|
GetCoreSyncSettings<Octoshock, Octoshock.SyncSettings>()
|
|
);
|
|
|
|
// total garbage, this
|
|
rom = new RomGame(file);
|
|
game = rom.GameInfo;
|
|
}
|
|
|
|
// HACK due to MAME wanting CHDs as hard drives / handling it on its own (bad design, I know!)
|
|
// only matters for XML, as CHDs are never the "main" rom for MAME
|
|
// (in general, this is kind of bad as CHD hard drives might be useful for other future cores?)
|
|
private static bool IsDiscForXML(string system, string path)
|
|
{
|
|
var ext = Path.GetExtension(path);
|
|
if (system == VSystemID.Raw.Arcade && ext.ToLowerInvariant() == ".chd")
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return Disc.IsValidExtension(ext);
|
|
}
|
|
|
|
private bool LoadXML(string path, CoreComm nextComm, HawkFile file, string forcedCoreName, out IEmulator nextEmulator, out RomGame rom, out GameInfo game)
|
|
{
|
|
nextEmulator = null;
|
|
rom = null;
|
|
game = null;
|
|
try
|
|
{
|
|
var xmlGame = XmlGame.Create(file); // if load fails, are we supposed to retry as a bsnes XML????????
|
|
game = xmlGame.GI;
|
|
|
|
var system = game.System;
|
|
var cip = new CoreInventoryParameters(this)
|
|
{
|
|
Comm = nextComm,
|
|
Game = game,
|
|
Roms = xmlGame.Assets
|
|
.Where(kvp => !IsDiscForXML(system, kvp.Key))
|
|
.Select(kvp => (IRomAsset)new RomAsset
|
|
{
|
|
RomData = kvp.Value,
|
|
FileData = kvp.Value, // TODO: Hope no one needed anything special here
|
|
Extension = Path.GetExtension(kvp.Key),
|
|
RomPath = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(path.SubstringBefore('|'))!, kvp.Key!)),
|
|
Game = Database.GetGameInfo(kvp.Value, Path.GetFileName(kvp.Key))
|
|
})
|
|
.ToList(),
|
|
Discs = xmlGame.AssetFullPaths
|
|
.Where(p => IsDiscForXML(system, p))
|
|
.Select(discPath => (p: discPath, d: DiscExtensions.CreateAnyType(discPath, str => DoLoadErrorCallback(str, system, LoadErrorType.DiscError))))
|
|
.Where(a => a.d != null)
|
|
.Select(a => (IDiscAsset)new DiscAsset
|
|
{
|
|
DiscData = a.d,
|
|
DiscType = new DiscIdentifier(a.d).DetectDiscType(),
|
|
DiscName = Path.GetFileNameWithoutExtension(a.p)
|
|
})
|
|
.ToList(),
|
|
};
|
|
nextEmulator = MakeCoreFromCoreInventory(cip, forcedCoreName);
|
|
return true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
try
|
|
{
|
|
// need to get rid of this hack at some point
|
|
rom = new RomGame(file);
|
|
game = rom.GameInfo;
|
|
game.System = VSystemID.Raw.SNES;
|
|
nextEmulator = new LibsnesCore(
|
|
game,
|
|
null,
|
|
rom.FileData,
|
|
Path.GetDirectoryName(path.SubstringBefore('|')),
|
|
nextComm,
|
|
GetCoreSettings<LibsnesCore, LibsnesCore.SnesSettings>(),
|
|
GetCoreSyncSettings<LibsnesCore, LibsnesCore.SnesSyncSettings>()
|
|
);
|
|
return true;
|
|
}
|
|
catch
|
|
{
|
|
DoLoadErrorCallback(ex.ToString(), VSystemID.Raw.GBL, LoadErrorType.Xml);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void LoadMAME(
|
|
string path,
|
|
CoreComm nextComm,
|
|
HawkFile file,
|
|
string ext,
|
|
out IEmulator nextEmulator,
|
|
out RomGame rom,
|
|
out GameInfo game,
|
|
out bool cancel)
|
|
{
|
|
try
|
|
{
|
|
LoadOther(nextComm, file, ext: ext, forcedCoreName: null, out nextEmulator, out rom, out game, out cancel);
|
|
}
|
|
catch (Exception mex) // ok, MAME threw, let's try another core...
|
|
{
|
|
try
|
|
{
|
|
using var f = new HawkFile(path, allowArchives: true);
|
|
if (!HandleArchiveBinding(f)) throw;
|
|
LoadOther(nextComm, f, ext: ext, forcedCoreName: null, out nextEmulator, out rom, out game, out cancel);
|
|
}
|
|
catch (Exception oex)
|
|
{
|
|
if (mex == oex) throw mex;
|
|
throw new AggregateException(mex, oex);
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool LoadRom(string path, CoreComm nextComm, string launchLibretroCore, string forcedCoreName = null, int recursiveCount = 0)
|
|
{
|
|
if (path == null) return false;
|
|
|
|
if (recursiveCount > 1) // hack to stop recursive calls from endlessly rerunning if we can't load it
|
|
{
|
|
DoLoadErrorCallback("Failed multiple attempts to load ROM.", "");
|
|
return false;
|
|
}
|
|
|
|
bool allowArchives = true;
|
|
if (OpenAdvanced is OpenAdvanced_MAME || MAMEMachineDB.IsMAMEMachine(path)) allowArchives = false;
|
|
using var file = new HawkFile(path, false, allowArchives);
|
|
if (!file.Exists && OpenAdvanced is not OpenAdvanced_LibretroNoGame) return false; // if the provided file doesn't even exist, give up! (unless libretro no game is used)
|
|
|
|
CanonicalFullPath = file.CanonicalFullPath;
|
|
|
|
IEmulator nextEmulator;
|
|
RomGame rom = null;
|
|
GameInfo game = null;
|
|
|
|
try
|
|
{
|
|
var cancel = false;
|
|
|
|
if (OpenAdvanced is OpenAdvanced_Libretro or OpenAdvanced_LibretroNoGame)
|
|
{
|
|
// must be done before LoadNoGame (which triggers retro_init and the paths to be consumed by the core)
|
|
// game name == name of core
|
|
Game = game = new GameInfo { Name = Path.GetFileNameWithoutExtension(launchLibretroCore), System = VSystemID.Raw.Libretro };
|
|
var retro = new LibretroHost(nextComm, game, launchLibretroCore);
|
|
nextEmulator = retro;
|
|
|
|
if (retro.Description.SupportsNoGame && string.IsNullOrEmpty(path))
|
|
{
|
|
// if we are allowed to run NoGame and we don't have a game, boot up the core that way
|
|
if (!retro.LoadNoGame())
|
|
{
|
|
DoLoadErrorCallback("LibretroNoGame failed to load. This is weird", VSystemID.Raw.Libretro);
|
|
retro.Dispose();
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
bool ret;
|
|
|
|
// if the core requires an archive file, then try passing the filename of the archive
|
|
// (but do we ever need to actually load the contents of the archive file into ram?)
|
|
if (retro.Description.NeedsArchives)
|
|
{
|
|
if (file.IsArchiveMember)
|
|
{
|
|
throw new InvalidOperationException("Should not have bound file member for libretro block_extract core");
|
|
}
|
|
|
|
ret = retro.LoadPath(file.FullPathWithoutMember);
|
|
}
|
|
else
|
|
{
|
|
// otherwise load the data or pass the filename, as requested. but..
|
|
if (retro.Description.NeedsRomAsPath && file.IsArchiveMember)
|
|
{
|
|
throw new InvalidOperationException("Cannot pass archive member to libretro needs_fullpath core");
|
|
}
|
|
|
|
ret = retro.Description.NeedsRomAsPath
|
|
? retro.LoadPath(file.FullPathWithoutMember)
|
|
: HandleArchiveBinding(file) && retro.LoadData(file.ReadAllBytes(), file.Name);
|
|
}
|
|
|
|
if (!ret)
|
|
{
|
|
DoLoadErrorCallback("Libretro failed to load the given file. This is probably due to a core/content mismatch. Moreover, the process is now likely to be hosed. We suggest you restart the program.", VSystemID.Raw.Libretro);
|
|
retro.Dispose();
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// do the archive binding we had to skip
|
|
if (!HandleArchiveBinding(file))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// not libretro: do extension checking
|
|
var ext = file.Extension;
|
|
switch (ext)
|
|
{
|
|
case ".m3u":
|
|
LoadM3U(path, nextComm, file, forcedCoreName, out nextEmulator, out game);
|
|
break;
|
|
case ".xml":
|
|
if (!LoadXML(path, nextComm, file, forcedCoreName, out nextEmulator, out rom, out game))
|
|
return false;
|
|
break;
|
|
case ".psf":
|
|
case ".minipsf":
|
|
LoadPSF(path, nextComm, file, out nextEmulator, out rom, out game);
|
|
break;
|
|
case ".zip" when forcedCoreName is null:
|
|
case ".7z" when forcedCoreName is null:
|
|
LoadMAME(path: path, nextComm, file, ext: ext, out nextEmulator, out rom, out game, out cancel);
|
|
break;
|
|
default:
|
|
if (Disc.IsValidExtension(ext))
|
|
{
|
|
if (file.IsArchive)
|
|
throw new InvalidOperationException("Can't load CD files from archives!");
|
|
if (!LoadDisc(path, nextComm, file, ext, forcedCoreName, out nextEmulator, out game))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
// must be called after LoadXML because of SNES hacks
|
|
LoadOther(
|
|
nextComm,
|
|
file,
|
|
ext: ext,
|
|
forcedCoreName: forcedCoreName,
|
|
out nextEmulator,
|
|
out rom,
|
|
out game,
|
|
out cancel);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (nextEmulator == null)
|
|
{
|
|
if (!cancel)
|
|
{
|
|
DoLoadErrorCallback("No core could load the rom.", null);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (game is null) throw new Exception("RomLoader returned core but no GameInfo"); // shouldn't be null if `nextEmulator` isn't? just in case
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
var system = game?.System;
|
|
|
|
DispatchErrorMessage(ex, system: system, path: path);
|
|
return false;
|
|
}
|
|
|
|
Rom = rom;
|
|
LoadedEmulator = nextEmulator;
|
|
Game = game;
|
|
return true;
|
|
}
|
|
|
|
private void DispatchErrorMessage(Exception ex, string system, string path)
|
|
{
|
|
if (ex is AggregateException agg)
|
|
{
|
|
// all cores failed to load a game, so tell the user everything that went wrong and maybe they can fix it
|
|
if (agg.InnerExceptions.Count > 1)
|
|
{
|
|
DoLoadErrorCallback("Multiple cores failed to load the rom:", system);
|
|
}
|
|
foreach (Exception e in agg.InnerExceptions)
|
|
{
|
|
DispatchErrorMessage(e, system: system, path: path);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// all of the specific exceptions we're trying to catch here aren't expected to have inner exceptions,
|
|
// so drill down in case we got a TargetInvocationException or something like that
|
|
while (ex.InnerException != null)
|
|
ex = ex.InnerException;
|
|
|
|
if (ex is MissingFirmwareException)
|
|
{
|
|
DoLoadErrorCallback(ex.Message, system, path, Deterministic, LoadErrorType.MissingFirmware);
|
|
}
|
|
else if (ex is CGBNotSupportedException)
|
|
{
|
|
// failed to load SGB bios or game does not support SGB mode.
|
|
DoLoadErrorCallback("Failed to load a GB rom in SGB mode. You might try disabling SGB Mode.", system);
|
|
}
|
|
else if (ex is NoAvailableCoreException)
|
|
{
|
|
// handle exceptions thrown by the new detected systems that BizHawk does not have cores for
|
|
DoLoadErrorCallback($"{ex.Message}\n\n{ex}", system);
|
|
}
|
|
else
|
|
{
|
|
DoLoadErrorCallback($"A core accepted the rom, but threw an exception while loading it:\n\n{ex}", system);
|
|
}
|
|
}
|
|
|
|
/// <remarks>roms ONLY; when an archive is loaded with a single file whose extension is one of these, the user prompt is skipped</remarks>
|
|
private static class RomFileExtensions
|
|
{
|
|
public static readonly IReadOnlyCollection<string> A26 = new[] { "a26" };
|
|
|
|
public static readonly IReadOnlyCollection<string> A78 = new[] { "a78" };
|
|
|
|
public static readonly IReadOnlyCollection<string> Amiga = new[] { "adf", "adz", "dms", "fdi", /*"hdf", "ipf", "lha"*/ };
|
|
|
|
public static readonly IReadOnlyCollection<string> AppleII = new[] { "dsk", "do", "po" };
|
|
|
|
public static readonly IReadOnlyCollection<string> Arcade = new[] { "zip", "7z", "chd" };
|
|
|
|
public static readonly IReadOnlyCollection<string> C64 = new[] { "prg", "d64", "g64", "crt", "tap" };
|
|
|
|
public static readonly IReadOnlyCollection<string> Coleco = new[] { "col" };
|
|
|
|
public static readonly IReadOnlyCollection<string> GB = new[] { "gb", "gbc", "sgb" };
|
|
|
|
public static readonly IReadOnlyCollection<string> GBA = new[] { "gba" };
|
|
|
|
public static readonly IReadOnlyCollection<string> GEN = new[] { "gen", "md", "smd", "32x" };
|
|
|
|
public static readonly IReadOnlyCollection<string> INTV = new[] { "int", "bin", "rom" };
|
|
|
|
public static readonly IReadOnlyCollection<string> Jaguar = new[] { "j64", "jag" };
|
|
|
|
public static readonly IReadOnlyCollection<string> Lynx = new[] { "lnx" };
|
|
|
|
public static readonly IReadOnlyCollection<string> MSX = new[] { "cas", "dsk", "mx1", "rom" };
|
|
|
|
public static readonly IReadOnlyCollection<string> N3DS = new[] { "3ds", "3dsx", "axf", "cci", "cxi", "app", "elf", "cia" };
|
|
|
|
public static readonly IReadOnlyCollection<string> N64 = new[] { "z64", "v64", "n64" };
|
|
|
|
public static readonly IReadOnlyCollection<string> N64DD = new[] { "ndd" };
|
|
|
|
public static readonly IReadOnlyCollection<string> NDS = new[] { "nds", "srl", "dsi", "ids" };
|
|
|
|
public static readonly IReadOnlyCollection<string> NES = new[] { "nes", "fds", "unf" };
|
|
|
|
public static readonly IReadOnlyCollection<string> NGP = new[] { "ngp", "ngc" };
|
|
|
|
public static readonly IReadOnlyCollection<string> O2 = new[] { "o2" };
|
|
|
|
public static readonly IReadOnlyCollection<string> PCE = new[] { "pce", "sgx" };
|
|
|
|
public static readonly IReadOnlyCollection<string> SMS = new[] { "sms", "gg", "sg" };
|
|
|
|
public static readonly IReadOnlyCollection<string> SNES = new[] { "smc", "sfc", "xml", "bs" };
|
|
|
|
public static readonly IReadOnlyCollection<string> TI83 = new[] { "83g", "83l", "83p" };
|
|
|
|
public static readonly IReadOnlyCollection<string> TIC80 = new[] { "tic" };
|
|
|
|
public static readonly IReadOnlyCollection<string> UZE = new[] { "uze" };
|
|
|
|
public static readonly IReadOnlyCollection<string> VB = new[] { "vb" };
|
|
|
|
public static readonly IReadOnlyCollection<string> VEC = new[] { "vec" };
|
|
|
|
public static readonly IReadOnlyCollection<string> WSWAN = new[] { "ws", "wsc", "pc2" };
|
|
|
|
public static readonly IReadOnlyCollection<string> ZXSpectrum = new[] { "tzx", "tap", "dsk", "pzx", "ipf" };
|
|
|
|
public static readonly IReadOnlyCollection<string> AutoloadFromArchive = Array.Empty<string>()
|
|
.Concat(A26)
|
|
.Concat(A78)
|
|
.Concat(Amiga)
|
|
.Concat(AppleII)
|
|
.Concat(C64)
|
|
.Concat(Coleco)
|
|
.Concat(GB)
|
|
.Concat(GBA)
|
|
.Concat(GEN)
|
|
.Concat(INTV)
|
|
.Concat(Jaguar)
|
|
.Concat(Lynx)
|
|
.Concat(MSX)
|
|
.Concat(N64)
|
|
.Concat(N64DD)
|
|
.Concat(NDS)
|
|
.Concat(NES)
|
|
.Concat(NGP)
|
|
.Concat(O2)
|
|
.Concat(PCE)
|
|
.Concat(SMS)
|
|
.Concat(SNES)
|
|
.Concat(TI83)
|
|
.Concat(TIC80)
|
|
.Concat(UZE)
|
|
.Concat(VB)
|
|
.Concat(VEC)
|
|
.Concat(WSWAN)
|
|
.Concat(ZXSpectrum)
|
|
.Select(static s => $".{s}") // this is what's expected at call-site
|
|
.ToArray();
|
|
}
|
|
|
|
/// <remarks>TODO add and handle <see cref="FilesystemFilter.LuaScripts"/> (you can drag-and-drop scripts and there are already non-rom things in this list, so why not?)</remarks>
|
|
public static readonly FilesystemFilterSet RomFilter = new(
|
|
new FilesystemFilter("Music Files", Array.Empty<string>(), devBuildExtraExts: new[] { "psf", "minipsf", "sid", "nsf", "gbs" }),
|
|
new FilesystemFilter("Disc Images", FilesystemFilter.DiscExtensions),
|
|
new FilesystemFilter("NES", RomFileExtensions.NES.Concat(new[] { "nsf" }).ToList(), addArchiveExts: true),
|
|
new FilesystemFilter("Super NES", RomFileExtensions.SNES, addArchiveExts: true),
|
|
new FilesystemFilter("PlayStation", FilesystemFilter.DiscExtensions),
|
|
new FilesystemFilter("PSX Executables (experimental)", Array.Empty<string>(), devBuildExtraExts: new[] { "exe" }),
|
|
new FilesystemFilter("PSF Playstation Sound File", new[] { "psf", "minipsf" }),
|
|
new FilesystemFilter("Nintendo 64", RomFileExtensions.N64),
|
|
new FilesystemFilter("Nintendo 64 Disk Drive", RomFileExtensions.N64DD),
|
|
new FilesystemFilter("Gameboy", RomFileExtensions.GB.Concat(new[] { "gbs" }).ToList(), addArchiveExts: true),
|
|
new FilesystemFilter("Gameboy Advance", RomFileExtensions.GBA, addArchiveExts: true),
|
|
new FilesystemFilter("Nintendo 3DS", RomFileExtensions.N3DS),
|
|
new FilesystemFilter("Nintendo DS", RomFileExtensions.NDS),
|
|
new FilesystemFilter("Master System", RomFileExtensions.SMS, addArchiveExts: true),
|
|
new FilesystemFilter("PC Engine", RomFileExtensions.PCE.Concat(FilesystemFilter.DiscExtensions).ToList(), addArchiveExts: true),
|
|
new FilesystemFilter("Atari 2600", RomFileExtensions.A26, devBuildExtraExts: new[] { "bin" }, addArchiveExts: true),
|
|
new FilesystemFilter("Atari 7800", RomFileExtensions.A78, devBuildExtraExts: new[] { "bin" }, addArchiveExts: true),
|
|
new FilesystemFilter("Atari Jaguar", RomFileExtensions.Jaguar, addArchiveExts: true),
|
|
new FilesystemFilter("Atari Lynx", RomFileExtensions.Lynx, addArchiveExts: true),
|
|
new FilesystemFilter("ColecoVision", RomFileExtensions.Coleco, addArchiveExts: true),
|
|
new FilesystemFilter("IntelliVision", RomFileExtensions.INTV, addArchiveExts: true),
|
|
new FilesystemFilter("TI-83", RomFileExtensions.TI83, addArchiveExts: true),
|
|
new FilesystemFilter("TIC-80", RomFileExtensions.TIC80, addArchiveExts: true),
|
|
FilesystemFilter.Archives,
|
|
new FilesystemFilter("Genesis", RomFileExtensions.GEN.Concat(FilesystemFilter.DiscExtensions).ToList(), addArchiveExts: true),
|
|
new FilesystemFilter("SID Commodore 64 Music File", Array.Empty<string>(), devBuildExtraExts: new[] { "sid" }, devBuildAddArchiveExts: true),
|
|
new FilesystemFilter("WonderSwan", RomFileExtensions.WSWAN, addArchiveExts: true),
|
|
new FilesystemFilter("Apple II", RomFileExtensions.AppleII, addArchiveExts: true),
|
|
new FilesystemFilter("Virtual Boy", RomFileExtensions.VB, addArchiveExts: true),
|
|
new FilesystemFilter("Neo Geo Pocket", RomFileExtensions.NGP, addArchiveExts: true),
|
|
new FilesystemFilter("Commodore 64", RomFileExtensions.C64, addArchiveExts: true),
|
|
new FilesystemFilter("Amstrad CPC", Array.Empty<string>(), devBuildExtraExts: new[] { "cdt", "dsk" }, devBuildAddArchiveExts: true),
|
|
new FilesystemFilter("Sinclair ZX Spectrum", RomFileExtensions.ZXSpectrum.Concat(new[] { "csw", "wav" }).ToList(), addArchiveExts: true),
|
|
new FilesystemFilter("Odyssey 2", RomFileExtensions.O2),
|
|
new FilesystemFilter("Uzebox", RomFileExtensions.UZE),
|
|
new FilesystemFilter("Vectrex", RomFileExtensions.VEC),
|
|
new FilesystemFilter("MSX", RomFileExtensions.MSX),
|
|
new FilesystemFilter("Arcade", RomFileExtensions.Arcade),
|
|
new FilesystemFilter("Amiga", RomFileExtensions.Amiga),
|
|
FilesystemFilter.EmuHawkSaveStates)
|
|
{
|
|
CombinedEntryDesc = "Everything",
|
|
};
|
|
|
|
public static readonly IReadOnlyCollection<string> KnownRomExtensions = RomFilter.Filters
|
|
.SelectMany(f => f.Extensions)
|
|
.Distinct()
|
|
.Except(FilesystemFilter.ArchiveExtensions.Concat(new[] { "State" }))
|
|
.Select(s => $".{s.ToUpperInvariant()}") // this is what's expected at call-site
|
|
.ToList();
|
|
}
|
|
}
|