commit C# parts for Citra port
not committing the dll yet until some more things are ready
This commit is contained in:
parent
cdfb8d67e8
commit
e87536ea8f
|
@ -36,6 +36,22 @@ namespace BizHawk.Client.Common
|
|||
public string GetRetroSystemPath(IGameInfo game)
|
||||
=> _pathEntries.RetroSystemAbsolutePath(game);
|
||||
|
||||
public string GetUserPath(string sysID, bool temp)
|
||||
{
|
||||
if (temp)
|
||||
{
|
||||
var tempUserPath = Path.Combine(Path.GetTempPath(), $"biz-temp{sysID}user");
|
||||
if (Directory.Exists(tempUserPath))
|
||||
{
|
||||
Directory.Delete(tempUserPath, true);
|
||||
}
|
||||
|
||||
return tempUserPath;
|
||||
}
|
||||
|
||||
return _pathEntries.UserAbsolutePathFor(sysID);
|
||||
}
|
||||
|
||||
private (byte[] FW, string Path)? GetFirmwareWithPath(FirmwareID id)
|
||||
{
|
||||
var path = _firmwareManager.Request(_pathEntries, _firmwareUserSpecifications, id);
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
using BizHawk.Common;
|
||||
using BizHawk.Common.IOExtensions;
|
||||
|
@ -21,6 +23,12 @@ namespace BizHawk.Client.Common
|
|||
|
||||
private const int BankSize = 1024;
|
||||
|
||||
// 3DS roms typically exceed 2GiB, so we don't want to load them into memory
|
||||
// TODO: Don't rely only on extension if this is actually a 3DS ROM (validate in some way)
|
||||
// TODO: ELF is another 3DS extension, but it's too generic / might be used for other systems...
|
||||
private static bool Is3DSRom(string ext)
|
||||
=> ext is ".3DS" or ".3DSX" or ".AXF" or ".CCI" or ".CXI" or ".APP" or ".CIA";
|
||||
|
||||
public RomGame(HawkFile file)
|
||||
: this(file, null)
|
||||
{
|
||||
|
@ -36,6 +44,33 @@ namespace BizHawk.Client.Common
|
|||
|
||||
Extension = file.Extension.ToUpperInvariant();
|
||||
|
||||
if (Is3DSRom(Extension))
|
||||
{
|
||||
if (file.IsArchive)
|
||||
{
|
||||
throw new InvalidOperationException("3DS ROMs cannot be in archives.");
|
||||
}
|
||||
|
||||
Console.WriteLine($"3DS ROM detected, skipping hash checks...");
|
||||
|
||||
FileData = RomData = Array.Empty<byte>();
|
||||
GameInfo = new()
|
||||
{
|
||||
Name = Path.GetFileNameWithoutExtension(file.Name).Replace('_', ' '),
|
||||
System = VSystemID.Raw._3DS,
|
||||
Hash = "N/A",
|
||||
Status = RomStatus.NotInDatabase,
|
||||
NotInDatabase = true
|
||||
};
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(GameInfo.Name) && GameInfo.Name == GameInfo.Name.ToUpperInvariant())
|
||||
{
|
||||
GameInfo.Name = Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(GameInfo.Name.ToLower());
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var stream = file.GetStream();
|
||||
int fileLength = (int)stream.Length;
|
||||
|
||||
|
|
|
@ -52,7 +52,11 @@ namespace BizHawk.Client.Common
|
|||
// some cores
|
||||
"Palettes" => 0x30,
|
||||
|
||||
_ => 0x40
|
||||
// currently Citra only
|
||||
// potentially applicable for future cores (Dolphin?)
|
||||
"User" => 0x40,
|
||||
|
||||
_ => 0x50
|
||||
};
|
||||
Path = path;
|
||||
System = system;
|
||||
|
|
|
@ -63,7 +63,8 @@ namespace BizHawk.Client.Common
|
|||
[VSystemID.Raw.NDS] = "NDS",
|
||||
[VSystemID.Raw.Sega32X] = "Sega 32X",
|
||||
[VSystemID.Raw.GGL] = "Dual Game Gear",
|
||||
[VSystemID.Raw.Satellaview] = "Satellaview"
|
||||
[VSystemID.Raw.Satellaview] = "Satellaview",
|
||||
[VSystemID.Raw._3DS] = "3DS"
|
||||
};
|
||||
|
||||
private static PathEntry BaseEntryFor(string sysID, string path)
|
||||
|
@ -103,6 +104,9 @@ namespace BizHawk.Client.Common
|
|||
private static PathEntry ScreenshotsEntryFor(string sysID)
|
||||
=> new(sysID, "Screenshots", Path.Combine(".", "Screenshots"));
|
||||
|
||||
private static PathEntry UserEntryFor(string sysID)
|
||||
=> new(sysID, "User", Path.Combine(".", "User"));
|
||||
|
||||
public List<PathEntry> Paths { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
|
@ -187,6 +191,11 @@ namespace BizHawk.Client.Common
|
|||
new(GLOBAL, "Temp Files", ""),
|
||||
},
|
||||
|
||||
CommonEntriesFor(VSystemID.Raw._3DS, basePath: Path.Combine(".", "3DS"), omitSaveRAM: true),
|
||||
new[] {
|
||||
UserEntryFor(VSystemID.Raw._3DS),
|
||||
},
|
||||
|
||||
CommonEntriesFor(VSystemID.Raw.Sega32X, basePath: Path.Combine(".", "32X")),
|
||||
|
||||
CommonEntriesFor(VSystemID.Raw.A26, basePath: Path.Combine(".", "Atari 2600"), omitSaveRAM: true),
|
||||
|
|
|
@ -294,6 +294,11 @@ namespace BizHawk.Client.Common
|
|||
return collection.AbsolutePathFor(collection[systemId, "Palettes"].Path, systemId);
|
||||
}
|
||||
|
||||
public static string UserAbsolutePathFor(this PathEntryCollection collection, string systemId)
|
||||
{
|
||||
return collection.AbsolutePathFor(collection[systemId, "User"].Path, systemId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Takes an absolute path and attempts to convert it to a relative, based on the system,
|
||||
/// or global base if no system is supplied, if it is not a subfolder of the base, it will return the path unaltered
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
using BizHawk.Common;
|
||||
|
||||
namespace BizHawk.Client.Common
|
||||
{
|
||||
public class ZwinderStateManagerSettings
|
||||
|
@ -38,13 +40,13 @@ namespace BizHawk.Client.Common
|
|||
[DisplayName("Current - Buffer Size")]
|
||||
[Description("Max amount of buffer space to use in MB.\n\nThe Current buffer is the primary buffer used near the last edited frame. This should be the largest buffer to ensure minimal gaps during editing.")]
|
||||
[Range(64, 32768)]
|
||||
[TypeConverter(typeof(IntConverter))]
|
||||
[TypeConverter(typeof(ConstrainedIntConverter))]
|
||||
public int CurrentBufferSize { get; set; } = 256;
|
||||
|
||||
[DisplayName("Current - Target Frame Length")]
|
||||
[Description("Desired frame length (number of emulated frames you can go back before running out of buffer)\n\nThe Current buffer is the primary buffer used near the last edited frame. This should be the largest buffer to ensure minimal gaps during editing.")]
|
||||
[Range(1, int.MaxValue)]
|
||||
[TypeConverter(typeof(IntConverter))]
|
||||
[TypeConverter(typeof(ConstrainedIntConverter))]
|
||||
public int CurrentTargetFrameLength { get; set; } = 500;
|
||||
|
||||
[DisplayName("Current - Storage Type")]
|
||||
|
@ -61,13 +63,13 @@ namespace BizHawk.Client.Common
|
|||
[DisplayName("Recent - Buffer Size")]
|
||||
[Description("Max amount of buffer space to use in MB.\n\nThe Recent buffer is where the current frames decay as the buffer fills up. The goal of this buffer is to maximize the amount of movie that can be fairly quickly navigated to. Therefore, a high target frame length is ideal here.")]
|
||||
[Range(64, 32768)]
|
||||
[TypeConverter(typeof(IntConverter))]
|
||||
[TypeConverter(typeof(ConstrainedIntConverter))]
|
||||
public int RecentBufferSize { get; set; } = 128;
|
||||
|
||||
[DisplayName("Recent - Target Frame Length")]
|
||||
[Description("Desired frame length (number of emulated frames you can go back before running out of buffer).\n\nThe Recent buffer is where the current frames decay as the buffer fills up. The goal of this buffer is to maximize the amount of movie that can be fairly quickly navigated to. Therefore, a high target frame length is ideal here.")]
|
||||
[Range(1, int.MaxValue)]
|
||||
[TypeConverter(typeof(IntConverter))]
|
||||
[TypeConverter(typeof(ConstrainedIntConverter))]
|
||||
public int RecentTargetFrameLength { get; set; } = 2000;
|
||||
|
||||
[DisplayName("Recent - Storage Type")]
|
||||
|
@ -84,13 +86,13 @@ namespace BizHawk.Client.Common
|
|||
[DisplayName("Gaps - Buffer Size")]
|
||||
[Description("Max amount of buffer space to use in MB\n\nThe Gap buffer is used for temporary storage when replaying older segment of the run without editing. It is used to 're-greenzone' large gaps while navigating around in an older area of the movie. This buffer can be small, and a similar size to target frame length ratio as current is ideal.")]
|
||||
[Range(64, 32768)]
|
||||
[TypeConverter(typeof(IntConverter))]
|
||||
[TypeConverter(typeof(ConstrainedIntConverter))]
|
||||
public int GapsBufferSize { get; set; } = 64;
|
||||
|
||||
[DisplayName("Gaps - Target Frame Length")]
|
||||
[Description("Desired frame length (number of emulated frames you can go back before running out of buffer)\n\nThe Gap buffer is used for temporary storage when replaying older segment of the run without editing. It is used to 're-greenzone' large gaps while navigating around in an older area of the movie. This buffer can be small, and a similar size to target frame length ratio as current is ideal.")]
|
||||
[Range(1, int.MaxValue)]
|
||||
[TypeConverter(typeof(IntConverter))]
|
||||
[TypeConverter(typeof(ConstrainedIntConverter))]
|
||||
public int GapsTargetFrameLength { get; set; } = 125;
|
||||
|
||||
[DisplayName("Gaps - Storage Type")]
|
||||
|
@ -100,7 +102,7 @@ namespace BizHawk.Client.Common
|
|||
[DisplayName("Ancient State Interval")]
|
||||
[Description("Once both the Current and Recent buffers have filled, some states are put into reserved to ensure there is always a state somewhat near a desired frame to navigate to. These states never decay but are invalidated. This number should be as high as possible without being overly cumbersome to replay this many frames.")]
|
||||
[Range(1, int.MaxValue)]
|
||||
[TypeConverter(typeof(IntConverter))]
|
||||
[TypeConverter(typeof(ConstrainedIntConverter))]
|
||||
public int AncientStateInterval { get; set; } = 5000;
|
||||
|
||||
[DisplayName("Ancient - Storage Type")]
|
||||
|
|
|
@ -71,6 +71,7 @@ namespace BizHawk.Client.EmuHawk
|
|||
["Coleco"] = "Colecovision",
|
||||
["GBA"] = "GBA",
|
||||
["NDS"] = "Nintendo DS",
|
||||
["3DS"] = "Nintendo 3DS",
|
||||
["TI83"] = "TI-83",
|
||||
["INTV"] = "Intellivision",
|
||||
["C64"] = "C64",
|
||||
|
|
|
@ -6,7 +6,7 @@ namespace BizHawk.Common
|
|||
{
|
||||
public class BizDateTimeConverter : DateTimeConverter
|
||||
{
|
||||
public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
|
||||
public override object ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type destinationType)
|
||||
{
|
||||
if (value is not DateTime d || destinationType != typeof(string)) throw new NotSupportedException("can only do DateTime --> string");
|
||||
return d.ToString();
|
|
@ -0,0 +1,59 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
|
||||
namespace BizHawk.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Used in conjunction with the <see cref="RangeAttribute" /> will perform range validation against a float value using PropertyGrid
|
||||
/// </summary>
|
||||
public class ConstrainedFloatConverter : TypeConverter
|
||||
{
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
||||
{
|
||||
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
|
||||
}
|
||||
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
||||
{
|
||||
var range = (RangeAttribute)context.Instance
|
||||
.GetType()
|
||||
.GetProperty(context.PropertyDescriptor!.Name)!
|
||||
.GetCustomAttributes()
|
||||
.First(i => i.GetType() == typeof(RangeAttribute));
|
||||
|
||||
range.Validate(value, context.PropertyDescriptor.Name);
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
throw new FormatException($"{context.PropertyDescriptor.Name} can not be null");
|
||||
}
|
||||
|
||||
if (float.TryParse(value.ToString(), out var floatVal))
|
||||
{
|
||||
return floatVal;
|
||||
}
|
||||
|
||||
throw new FormatException($"Invalid value: {value}, {context.PropertyDescriptor.Name} must be a float.");
|
||||
}
|
||||
|
||||
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
|
||||
{
|
||||
if (destinationType == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(destinationType));
|
||||
}
|
||||
|
||||
if (destinationType == typeof(string))
|
||||
{
|
||||
var num = Convert.ToSingle(value);
|
||||
return num.ToString();
|
||||
}
|
||||
|
||||
return base.ConvertTo(context, culture, value, destinationType)!;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,12 +5,12 @@ using System.ComponentModel.DataAnnotations;
|
|||
using System.Reflection;
|
||||
using System.Linq;
|
||||
|
||||
namespace BizHawk.Client.Common
|
||||
namespace BizHawk.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Used in conjunction with the <see cref="RangeAttribute" /> will perform range validation against an int value using PropertyGrid
|
||||
/// </summary>
|
||||
public class IntConverter : TypeConverter
|
||||
public class ConstrainedIntConverter : TypeConverter
|
||||
{
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
||||
{
|
||||
|
@ -19,11 +19,11 @@ namespace BizHawk.Client.Common
|
|||
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
||||
{
|
||||
var range = context.Instance
|
||||
var range = (RangeAttribute)context.Instance
|
||||
.GetType()
|
||||
.GetProperty(context.PropertyDescriptor.Name)
|
||||
.GetProperty(context.PropertyDescriptor!.Name)!
|
||||
.GetCustomAttributes()
|
||||
.FirstOrDefault(i => i.GetType() == typeof(RangeAttribute)) as RangeAttribute;
|
||||
.First(i => i.GetType() == typeof(RangeAttribute));
|
||||
|
||||
range.Validate(value, context.PropertyDescriptor.Name);
|
||||
|
||||
|
@ -32,7 +32,7 @@ namespace BizHawk.Client.Common
|
|||
throw new FormatException($"{context.PropertyDescriptor.Name} can not be null");
|
||||
}
|
||||
|
||||
if (int.TryParse(value.ToString(), out int intVal))
|
||||
if (int.TryParse(value.ToString(), out var intVal))
|
||||
{
|
||||
return intVal;
|
||||
}
|
||||
|
@ -49,11 +49,11 @@ namespace BizHawk.Client.Common
|
|||
|
||||
if (destinationType == typeof(string))
|
||||
{
|
||||
int num = Convert.ToInt32(value);
|
||||
var num = Convert.ToInt32(value);
|
||||
return num.ToString();
|
||||
}
|
||||
|
||||
return base.ConvertTo(context, culture, value, destinationType);
|
||||
return base.ConvertTo(context, culture, value, destinationType)!;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
|
||||
namespace BizHawk.Common
|
||||
{
|
||||
/// <summary>
|
||||
/// Used in conjunction with the <see cref="MaxLengthAttribute" /> will perform max length validation against a string value using PropertyGrid
|
||||
/// </summary>
|
||||
public class ConstrainedStringConverter : TypeConverter
|
||||
{
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
||||
{
|
||||
return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
|
||||
}
|
||||
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
||||
{
|
||||
var maxLength = (MaxLengthAttribute)context.Instance
|
||||
.GetType()
|
||||
.GetProperty(context.PropertyDescriptor!.Name)!
|
||||
.GetCustomAttributes()
|
||||
.First(i => i.GetType() == typeof(MaxLengthAttribute));
|
||||
|
||||
maxLength.Validate(value, context.PropertyDescriptor.Name);
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
throw new FormatException($"{context.PropertyDescriptor.Name} can not be null");
|
||||
}
|
||||
|
||||
return value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -43,12 +43,11 @@ namespace BizHawk.Common
|
|||
if (found is DisplayAttribute da) return da.Name;
|
||||
}
|
||||
}
|
||||
#pragma warning disable CS8603 // value can't be null here, it would've thrown already
|
||||
|
||||
return value.ToString();
|
||||
#pragma warning restore CS8603
|
||||
}
|
||||
|
||||
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) => new StandardValuesCollection(
|
||||
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context) => new(
|
||||
enumType.GetFields(BindingFlags.Public | BindingFlags.Static)
|
||||
.Select(fi => fi.GetValue(null))
|
||||
.ToList()
|
|
@ -165,6 +165,9 @@ namespace BizHawk.Emulation.Common
|
|||
FirmwareAndOption(SHA1Checksum.Dummy, 251658240 + 64, "NDS", "NAND (CHN)", "DSi_Nand_CHN.bin", "DSi NAND (China)");
|
||||
FirmwareAndOption(SHA1Checksum.Dummy, 251658240 + 64, "NDS", "NAND (KOR)", "DSi_Nand_KOR.bin", "DSi NAND (Korea)");
|
||||
|
||||
FirmwareAndOption("5A3D3D6DF4743E6B50AFE0FC717FA8A12BC888E6", 65536, "3DS", "boot9", "3DS_Boot9.bin", "ARM9 BIOS");
|
||||
// TODO: need to add more 3DS crap...
|
||||
|
||||
FirmwareAndOption("E4ED47FAE31693E016B081C6BDA48DA5B70D7CCB", 512, "Lynx", "Boot", "LYNX_boot.img", "Boot Rom");
|
||||
|
||||
FirmwareAndOption("5A65B922B562CB1F57DAB51B73151283F0E20C7A", 8192, "INTV", "EROM", "INTV_EROM.bin", "Executive Rom");
|
||||
|
|
|
@ -20,6 +20,12 @@
|
|||
/// </summary>
|
||||
string GetRetroSystemPath(IGameInfo game);
|
||||
|
||||
/// <summary>
|
||||
/// produces a 'user' path for a given system id
|
||||
/// can produce an empty temp folder, suitable for movies
|
||||
/// </summary>
|
||||
string GetUserPath(string sysID, bool temp);
|
||||
|
||||
/// <param name="msg">warning message to show on failure</param>
|
||||
/// <returns><see langword="null"/> iff failed</returns>
|
||||
byte[]? GetFirmware(FirmwareID id, string? msg = null);
|
||||
|
|
|
@ -14,6 +14,7 @@ namespace BizHawk.Emulation.Common
|
|||
{
|
||||
public static class Raw
|
||||
{
|
||||
public const string _3DS = "3DS";
|
||||
public const string A26 = "A26";
|
||||
public const string A78 = "A78";
|
||||
public const string Amiga = "Amiga";
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace BizHawk.Emulation.Cores.Consoles.Nintendo._3DS
|
||||
{
|
||||
public class _3DSMotionEmu
|
||||
{
|
||||
// update per frame
|
||||
private const float UPDATE_PERIOD = (float)(4481136.0 / 268111856.0);
|
||||
|
||||
// todo: make these adjustable
|
||||
private const float SENSITIVITY = 0.01f;
|
||||
private const float TILT_CLAMP = 90.0f;
|
||||
|
||||
public void Update(bool tilting, int x, int y)
|
||||
{
|
||||
if (!IsTilting && tilting)
|
||||
{
|
||||
TiltOrigin = new(x, y);
|
||||
}
|
||||
|
||||
IsTilting = tilting;
|
||||
|
||||
if (tilting)
|
||||
{
|
||||
var tiltMove = new Vector2(x, y) - TiltOrigin;
|
||||
if (tiltMove == default)
|
||||
{
|
||||
TiltAngle = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
TiltDirection = tiltMove;
|
||||
var tiltMoveNormalized = tiltMove.Length();
|
||||
TiltDirection /= tiltMoveNormalized;
|
||||
TiltAngle = tiltMoveNormalized * SENSITIVITY;
|
||||
TiltAngle = Math.Max(TiltAngle, 0.0f);
|
||||
TiltAngle = Math.Min(TiltAngle, (float)Math.PI * TILT_CLAMP / 180.0f);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
TiltAngle = 0;
|
||||
}
|
||||
|
||||
var tiltQ = Quaternion.CreateFromAxisAngle(new(-TiltDirection.Y, 0.0f, TiltDirection.X), TiltAngle);
|
||||
var conTiltQ = Quaternion.Conjugate(tiltQ);
|
||||
|
||||
var angularRateQ = (tiltQ - PrevTiltQuaternion) * conTiltQ;
|
||||
var angularRate = new Vector3(angularRateQ.X, angularRateQ.Y, angularRateQ.Z) * 2;
|
||||
angularRate *= UPDATE_PERIOD / (float)Math.PI * 180;
|
||||
|
||||
Gravity = Vector3.Transform(new(0, -1, 0), conTiltQ);
|
||||
AngularRate = Vector3.Transform(angularRate, conTiltQ);
|
||||
|
||||
PrevTiltQuaternion = tiltQ;
|
||||
}
|
||||
|
||||
public bool IsTilting;
|
||||
public Vector2 TiltOrigin;
|
||||
public Vector2 TiltDirection;
|
||||
public float TiltAngle;
|
||||
public Quaternion PrevTiltQuaternion;
|
||||
public Vector3 Gravity;
|
||||
public Vector3 AngularRate;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
using BizHawk.Emulation.Common;
|
||||
|
||||
namespace BizHawk.Emulation.Cores.Consoles.Nintendo._3DS
|
||||
{
|
||||
public partial class Citra : IEmulator
|
||||
{
|
||||
private readonly BasicServiceProvider _serviceProvider;
|
||||
|
||||
public IEmulatorServiceProvider ServiceProvider => _serviceProvider;
|
||||
|
||||
public ControllerDefinition ControllerDefinition => _3DSController;
|
||||
|
||||
public int Frame { get; set; }
|
||||
|
||||
public string SystemId => VSystemID.Raw._3DS;
|
||||
|
||||
public bool DeterministicEmulation { get; }
|
||||
|
||||
public bool FrameAdvance(IController controller, bool render, bool renderSound = true)
|
||||
{
|
||||
_controller = controller;
|
||||
IsLagFrame = true;
|
||||
|
||||
_motionEmu.Update(
|
||||
controller.IsPressed("Tilt"),
|
||||
controller.AxisValue("Tilt X"),
|
||||
controller.AxisValue("Tilt Y"));
|
||||
|
||||
if (_controller.IsPressed("Power"))
|
||||
{
|
||||
_core.Citra_Reset(_context);
|
||||
}
|
||||
|
||||
_core.Citra_RunFrame(_context);
|
||||
|
||||
_core.Citra_GetVideoDimensions(_context, out _citraVideoProvider.Width, out _citraVideoProvider.Height);
|
||||
_citraVideoProvider.VideoDirty = true;
|
||||
|
||||
if (renderSound)
|
||||
{
|
||||
ProcessSound();
|
||||
}
|
||||
|
||||
Frame++;
|
||||
if (IsLagFrame)
|
||||
{
|
||||
LagCount++;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ResetCounters()
|
||||
{
|
||||
Frame = 0;
|
||||
LagCount = 0;
|
||||
IsLagFrame = false;
|
||||
}
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_core.Citra_DestroyContext(_context);
|
||||
|
||||
foreach (var glContext in _glContexts)
|
||||
{
|
||||
_openGLProvider.ReleaseGLContext(glContext);
|
||||
}
|
||||
|
||||
_glContexts.Clear();
|
||||
|
||||
CurrentCore = null;
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
using System;
|
||||
|
||||
using BizHawk.Common;
|
||||
using BizHawk.Emulation.Common;
|
||||
|
||||
namespace BizHawk.Emulation.Cores.Consoles.Nintendo._3DS
|
||||
{
|
||||
public partial class Citra : IInputPollable
|
||||
{
|
||||
public int LagCount { get; set; }
|
||||
public bool IsLagFrame { get; set; }
|
||||
public IInputCallbackSystem InputCallbacks { get; } = new InputCallbackSystem();
|
||||
|
||||
private IController _controller = NullController.Instance;
|
||||
|
||||
private readonly _3DSMotionEmu _motionEmu = new();
|
||||
|
||||
private static readonly ControllerDefinition _3DSController = new ControllerDefinition("3DS Controller")
|
||||
{
|
||||
BoolButtons =
|
||||
{
|
||||
"A", "B", "X", "Y", "Up", "Down", "Left", "Right", "L", "R", "Start", "Select", "Debug", "GPIO14", "ZL", "ZR", "Home", "Touch", "Tilt", "Power"
|
||||
}
|
||||
}.AddXYPair("Circle Pad {0}", AxisPairOrientation.RightAndUp, (-128).RangeTo(127), 0)
|
||||
.AddXYPair("C-Stick {0}", AxisPairOrientation.RightAndUp, (-128).RangeTo(127), 0)
|
||||
.AddXYPair("Touch {0}", AxisPairOrientation.RightAndUp, 0.RangeTo(320), 160, 0.RangeTo(240), 120)
|
||||
.AddXYPair("Tilt {0}", AxisPairOrientation.RightAndUp, 0.RangeTo(320), 160, 0.RangeTo(240), 120)
|
||||
.MakeImmutable();
|
||||
|
||||
private bool GetButtonCallback(LibCitra.Buttons button) => button switch
|
||||
{
|
||||
LibCitra.Buttons.A => _controller.IsPressed("A"),
|
||||
LibCitra.Buttons.B => _controller.IsPressed("B"),
|
||||
LibCitra.Buttons.X => _controller.IsPressed("X"),
|
||||
LibCitra.Buttons.Y => _controller.IsPressed("Y"),
|
||||
LibCitra.Buttons.Up => _controller.IsPressed("Up"),
|
||||
LibCitra.Buttons.Down => _controller.IsPressed("Down"),
|
||||
LibCitra.Buttons.Left => _controller.IsPressed("Left"),
|
||||
LibCitra.Buttons.Right => _controller.IsPressed("Right"),
|
||||
LibCitra.Buttons.L => _controller.IsPressed("L"),
|
||||
LibCitra.Buttons.R => _controller.IsPressed("R"),
|
||||
LibCitra.Buttons.Start => _controller.IsPressed("Start"),
|
||||
LibCitra.Buttons.Select => _controller.IsPressed("Select"),
|
||||
LibCitra.Buttons.Debug => _controller.IsPressed("Debug"),
|
||||
LibCitra.Buttons.Gpio14 => _controller.IsPressed("GPIO14"),
|
||||
LibCitra.Buttons.ZL => _controller.IsPressed("ZL"),
|
||||
LibCitra.Buttons.ZR => _controller.IsPressed("ZR"),
|
||||
LibCitra.Buttons.Home => _controller.IsPressed("Home"),
|
||||
_ => throw new InvalidOperationException(),
|
||||
};
|
||||
|
||||
private void GetAxisCallback(LibCitra.AnalogSticks stick, out float x, out float y)
|
||||
{
|
||||
switch (stick)
|
||||
{
|
||||
case LibCitra.AnalogSticks.CirclePad:
|
||||
x = _controller.AxisValue("Circle Pad X") / 128.0f;
|
||||
y = _controller.AxisValue("Circle Pad Y") / 128.0f;
|
||||
break;
|
||||
case LibCitra.AnalogSticks.CStick:
|
||||
x = _controller.AxisValue("C-Stick X") / 128.0f;
|
||||
y = _controller.AxisValue("C-Stick Y") / 128.0f;
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
private bool GetTouchCallback(out float x, out float y)
|
||||
{
|
||||
x = _controller.AxisValue("Touch X") / 320.0f;
|
||||
y = _controller.AxisValue("Touch Y") / 240.0f;
|
||||
return _controller.IsPressed("Touch");
|
||||
}
|
||||
|
||||
private void GetMotionCallback(
|
||||
out float accelX,
|
||||
out float accelY,
|
||||
out float accelZ,
|
||||
out float gyroX,
|
||||
out float gyroY,
|
||||
out float gyroZ)
|
||||
{
|
||||
accelX = _motionEmu.Gravity.X;
|
||||
accelY = _motionEmu.Gravity.Y;
|
||||
accelZ = _motionEmu.Gravity.Z;
|
||||
gyroX = _motionEmu.AngularRate.X;
|
||||
gyroY = _motionEmu.AngularRate.Y;
|
||||
gyroZ = _motionEmu.AngularRate.Z;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,415 @@
|
|||
using BizHawk.Common;
|
||||
using BizHawk.Emulation.Common;
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Drawing;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace BizHawk.Emulation.Cores.Consoles.Nintendo._3DS
|
||||
{
|
||||
public partial class Citra : ISettable<Citra.CitraSettings, Citra.CitraSyncSettings>
|
||||
{
|
||||
private CitraSettings _settings;
|
||||
private CitraSyncSettings _syncSettings;
|
||||
private string _userPath;
|
||||
|
||||
private bool GetBooleanSettingCallback(string label) => label switch
|
||||
{
|
||||
"use_cpu_jit" => _syncSettings.UseCpuJit,
|
||||
"use_hw_shader" => _syncSettings.UseHwShader,
|
||||
"shaders_accurate_mul" => _syncSettings.ShadersAccurateMul,
|
||||
"use_shader_jit" => _syncSettings.UseShaderJit,
|
||||
"use_virtual_sd" => _syncSettings.UseVirtualSd,
|
||||
"is_new_3ds" => _syncSettings.IsNew3ds,
|
||||
"filter_mode" => _settings.FilterMode,
|
||||
"swap_screen" => _settings.SwapScreen,
|
||||
"upright_screen" => _settings.UprightScreen,
|
||||
"custom_layout" => _settings.UseCustomLayout,
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
|
||||
private static readonly DateTime _epoch = new(1970, 1, 1, 0, 0, 0);
|
||||
|
||||
private ulong GetIntegerSettingCallback(string label) => label switch
|
||||
{
|
||||
"cpu_clock_percentage" => (ulong)_syncSettings.CpuClockPercentage,
|
||||
"graphics_api" => _supportsOpenGL43 ? (ulong)_syncSettings.GraphicsApi : (ulong)CitraSyncSettings.EGraphicsApi.Software,
|
||||
"region_value" => (ulong)_syncSettings.RegionValue,
|
||||
"init_clock" => _syncSettings.UseRealTime && !DeterministicEmulation ? 0UL : 1UL,
|
||||
"init_time" => (ulong)(_syncSettings.InitialTime - _epoch).TotalSeconds,
|
||||
"birthmonth" => (ulong)_syncSettings.CFGBirthdayMonth,
|
||||
"birthday" => (ulong)_syncSettings.CFGBirthdayDay,
|
||||
"language" => (ulong)_syncSettings.CFGSystemLanguage,
|
||||
"sound_mode" => (ulong)_syncSettings.CFGSoundOutputMode,
|
||||
"playcoins" => _syncSettings.PTMPlayCoins,
|
||||
"resolution_factor" => (ulong)_settings.ResolutionFactor,
|
||||
"texture_filter" => (ulong)_settings.TextureFilter,
|
||||
"mono_render_option" => (ulong)_settings.MonoRenderOption,
|
||||
"render_3d" => (ulong)_settings.StereoRenderOption,
|
||||
"factor_3d" => (ulong)_settings.Factor3D,
|
||||
"layout_option" => (ulong)_settings.LayoutOption,
|
||||
"custom_top_left" => (ulong)_settings.CustomLayoutTopScreenRectangle.Left,
|
||||
"custom_top_top" => (ulong)_settings.CustomLayoutTopScreenRectangle.Top,
|
||||
"custom_top_right" => (ulong)_settings.CustomLayoutTopScreenRectangle.Right,
|
||||
"custom_top_bottom" => (ulong)_settings.CustomLayoutTopScreenRectangle.Bottom,
|
||||
"custom_bottom_left" => (ulong)_settings.CustomLayoutBottomScreenRectangle.Left,
|
||||
"custom_bottom_top" => (ulong)_settings.CustomLayoutBottomScreenRectangle.Top,
|
||||
"custom_bottom_right" => (ulong)_settings.CustomLayoutBottomScreenRectangle.Right,
|
||||
"custom_bottom_bottom" => (ulong)_settings.CustomLayoutBottomScreenRectangle.Bottom,
|
||||
"custom_second_layer_opacity" => (ulong)_settings.CustomLayoutSecondLayerOpacity,
|
||||
"window_scale_factor" => (ulong)_settings.WindowFactor,
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
|
||||
private double GetFloatSettingCallback(string label) => label switch
|
||||
{
|
||||
"volume" => _syncSettings.Volume / 100.0,
|
||||
"bg_red" => _settings.BackgroundColor.R / 255.0,
|
||||
"bg_green" => _settings.BackgroundColor.G / 255.0,
|
||||
"bg_blue" => _settings.BackgroundColor.B / 255.0,
|
||||
"large_screen_proportion" => _settings.LargeScreenProportion,
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
|
||||
private void GetStringSettingCallback(string label, IntPtr buffer, int bufferSize)
|
||||
{
|
||||
var ret = label switch
|
||||
{
|
||||
"user_directory" => _userPath,
|
||||
"username" => _syncSettings.CFGUsername,
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(ret);
|
||||
var numToCopy = Math.Min(bytes.Length, bufferSize - 1);
|
||||
Marshal.Copy(bytes, 0, buffer, numToCopy);
|
||||
Marshal.WriteByte(buffer, numToCopy, 0);
|
||||
}
|
||||
|
||||
public CitraSettings GetSettings()
|
||||
=> _settings.Clone();
|
||||
|
||||
public CitraSyncSettings GetSyncSettings()
|
||||
=> _syncSettings.Clone();
|
||||
|
||||
public PutSettingsDirtyBits PutSettings(CitraSettings o)
|
||||
{
|
||||
_settings = o;
|
||||
_core.Citra_ReloadConfig(_context);
|
||||
return PutSettingsDirtyBits.None;
|
||||
}
|
||||
|
||||
public PutSettingsDirtyBits PutSyncSettings(CitraSyncSettings o)
|
||||
{
|
||||
var ret = CitraSyncSettings.NeedsReboot(_syncSettings, o);
|
||||
_syncSettings = o;
|
||||
return ret ? PutSettingsDirtyBits.RebootCore : PutSettingsDirtyBits.None;
|
||||
}
|
||||
|
||||
public class CitraSettings
|
||||
{
|
||||
[DisplayName("Resolution Scale Factor")]
|
||||
[Description("Scale factor for the 3DS resolution.")]
|
||||
[DefaultValue(1)]
|
||||
[Range(1, 10)]
|
||||
[TypeConverter(typeof(ConstrainedIntConverter))]
|
||||
public int ResolutionFactor { get; set; }
|
||||
|
||||
[DisplayName("Window Scale Factor")]
|
||||
[Description("Scale factor for the window.")]
|
||||
[DefaultValue(1)]
|
||||
[Range(1, 10)]
|
||||
[TypeConverter(typeof(ConstrainedIntConverter))]
|
||||
public int WindowFactor { get; set; }
|
||||
|
||||
public enum ETextureFilter
|
||||
{
|
||||
None = 0,
|
||||
Anime4K = 1,
|
||||
Bicubic = 2,
|
||||
NearestNeighbor = 3,
|
||||
ScaleForce = 4,
|
||||
xBRZ = 5
|
||||
}
|
||||
|
||||
[DisplayName("Texture Filter")]
|
||||
[Description("")]
|
||||
[DefaultValue(ETextureFilter.None)]
|
||||
public ETextureFilter TextureFilter { get; set; }
|
||||
|
||||
public enum EMonoRenderOption
|
||||
{
|
||||
LeftEye,
|
||||
RightEye
|
||||
}
|
||||
|
||||
[DisplayName("Mono Render Option")]
|
||||
[Description("Change Default Eye to Render When in Monoscopic Mode")]
|
||||
[DefaultValue(EMonoRenderOption.LeftEye)]
|
||||
public EMonoRenderOption MonoRenderOption { get; set; }
|
||||
|
||||
public enum EStereoRenderOption
|
||||
{
|
||||
Off,
|
||||
SideBySide,
|
||||
Anaglyph,
|
||||
Interlaced,
|
||||
ReverseInterlaced
|
||||
}
|
||||
|
||||
[DisplayName("Stereo Render Option")]
|
||||
[Description(" Whether and how Stereoscopic 3D should be rendered")]
|
||||
[DefaultValue(EStereoRenderOption.Off)]
|
||||
public EStereoRenderOption StereoRenderOption { get; set; }
|
||||
|
||||
[DisplayName("3D Intensity")]
|
||||
[Description("Change 3D Intensity")]
|
||||
[DefaultValue(0)]
|
||||
[Range(0, 100)]
|
||||
[TypeConverter(typeof(ConstrainedIntConverter))]
|
||||
public int Factor3D { get; set; }
|
||||
|
||||
[DisplayName("Enable Linear Filtering")]
|
||||
[Description("Whether to enable linear filtering or not")]
|
||||
[DefaultValue(true)]
|
||||
public bool FilterMode { get; set; }
|
||||
|
||||
[DisplayName("Background Color")]
|
||||
[Description("The clear color for the renderer. What shows up on the sides of the bottom screen.")]
|
||||
[DefaultValue(typeof(Color), "0xFF000000")]
|
||||
public Color BackgroundColor { get; set; }
|
||||
|
||||
public enum ELayoutOption
|
||||
{
|
||||
TopBottomScreen,
|
||||
SingleScreen,
|
||||
LargeScreen,
|
||||
SideScreen,
|
||||
HybridScreen = 5,
|
||||
MobilePortrait,
|
||||
MobileLandscape,
|
||||
}
|
||||
|
||||
[DisplayName("Layout Option")]
|
||||
[Description("Layout for the screen inside the render window.")]
|
||||
[DefaultValue(ELayoutOption.TopBottomScreen)]
|
||||
public ELayoutOption LayoutOption { get; set; }
|
||||
|
||||
[DisplayName("Swap Screen")]
|
||||
[Description("Swaps the prominent screen with the other screen.")]
|
||||
[DefaultValue(false)]
|
||||
public bool SwapScreen { get; set; }
|
||||
|
||||
[DisplayName("Upright Screen")]
|
||||
[Description("Toggle upright orientation, for book style games.")]
|
||||
[DefaultValue(false)]
|
||||
public bool UprightScreen { get; set; }
|
||||
|
||||
[DisplayName("Large Screen Proportion")]
|
||||
[Description("The proportion between the large and small screens when playing in Large Screen Small Screen layout.")]
|
||||
[DefaultValue(4.0f)]
|
||||
[Range(1.0f, 16.0f)]
|
||||
[TypeConverter(typeof(ConstrainedFloatConverter))]
|
||||
public float LargeScreenProportion { get; set; }
|
||||
|
||||
[DisplayName("Use Custom Layout")]
|
||||
[Description("Toggle custom layout on or off")]
|
||||
[DefaultValue(false)]
|
||||
public bool UseCustomLayout { get; set; }
|
||||
|
||||
[DisplayName("Custom Layout Top Screen Rectangle")]
|
||||
[Description("")]
|
||||
public Rectangle CustomLayoutTopScreenRectangle { get; set; }
|
||||
|
||||
[DisplayName("Custom Layout Bottom Screen Rectangle")]
|
||||
[Description("")]
|
||||
public Rectangle CustomLayoutBottomScreenRectangle { get; set; }
|
||||
|
||||
[DisplayName("Custom Layout Second Layer Opacity")]
|
||||
[Description("Opacity of second layer when using custom layout option (bottom screen unless swapped)")]
|
||||
[DefaultValue(100)]
|
||||
[Range(0, 100)]
|
||||
[TypeConverter(typeof(ConstrainedIntConverter))]
|
||||
public int CustomLayoutSecondLayerOpacity { get; set; }
|
||||
|
||||
public CitraSettings Clone()
|
||||
=> (CitraSettings)MemberwiseClone();
|
||||
|
||||
public CitraSettings()
|
||||
=> SettingsUtil.SetDefaultValues(this);
|
||||
}
|
||||
|
||||
public class CitraSyncSettings
|
||||
{
|
||||
[DisplayName("Use CPU JIT")]
|
||||
[Description("Whether to use the Just-In-Time (JIT) compiler for CPU emulation")]
|
||||
[DefaultValue(true)]
|
||||
public bool UseCpuJit { get; set; }
|
||||
|
||||
[DisplayName("CPU Clock Percentage")]
|
||||
[Description("Change the Clock Frequency of the emulated 3DS CPU.\n" +
|
||||
"Underclocking can increase the performance of the game at the risk of freezing.\n" +
|
||||
"Overclocking may fix lag that happens on console, but also comes with the risk of freezing.")]
|
||||
[DefaultValue(100)]
|
||||
[Range(25, 400)]
|
||||
[TypeConverter(typeof(ConstrainedIntConverter))]
|
||||
public int CpuClockPercentage { get; set; }
|
||||
|
||||
public enum EGraphicsApi
|
||||
{
|
||||
Software,
|
||||
OpenGL
|
||||
}
|
||||
|
||||
[DisplayName("Graphics API")]
|
||||
[Description("Whether to render using OpenGL or Software. Forced to software if OpenGL 4.3+ is not available.")]
|
||||
[DefaultValue(EGraphicsApi.OpenGL)]
|
||||
public EGraphicsApi GraphicsApi { get; set; }
|
||||
|
||||
[DisplayName("Use HW Shader")]
|
||||
[Description("Whether to use hardware shaders to emulate 3DS shaders")]
|
||||
[DefaultValue(true)]
|
||||
public bool UseHwShader { get; set; }
|
||||
|
||||
[DisplayName("Shaders Accurate Mul")]
|
||||
[Description("Whether to use accurate multiplication in hardware shaders")]
|
||||
[DefaultValue(true)]
|
||||
public bool ShadersAccurateMul { get; set; }
|
||||
|
||||
[DisplayName("Use Shader JIT")]
|
||||
[Description("Whether to use the Just-In-Time (JIT) compiler for shader emulation")]
|
||||
[DefaultValue(true)]
|
||||
public bool UseShaderJit { get; set; }
|
||||
|
||||
[DisplayName("Volume Percentage")]
|
||||
[Description("Output volume")]
|
||||
[DefaultValue(100)]
|
||||
[Range(0, 100)]
|
||||
[TypeConverter(typeof(ConstrainedIntConverter))]
|
||||
public int Volume { get; set; }
|
||||
|
||||
[DisplayName("Use Virtual SD")]
|
||||
[Description("Whether to create a virtual SD card.")]
|
||||
[DefaultValue(true)]
|
||||
public bool UseVirtualSd { get; set; }
|
||||
|
||||
[DisplayName("Is New 3DS")]
|
||||
[Description("The system model that Citra will try to emulate.")]
|
||||
[DefaultValue(true)]
|
||||
public bool IsNew3ds { get; set; }
|
||||
|
||||
public enum ERegionValue
|
||||
{
|
||||
Autodetect = -1,
|
||||
Japan,
|
||||
USA,
|
||||
Europe,
|
||||
Australia,
|
||||
China,
|
||||
Korea,
|
||||
Taiwan,
|
||||
}
|
||||
|
||||
[DisplayName("Region Value")]
|
||||
[Description("The system region that Citra will use during emulation")]
|
||||
[DefaultValue(ERegionValue.Autodetect)]
|
||||
public ERegionValue RegionValue { get; set; }
|
||||
|
||||
[DisplayName("Use Real Time")]
|
||||
[Description("If true, RTC clock will be based off of real time instead of emulated time. Ignored (set to false) when recording a movie.")]
|
||||
[DefaultValue(true)]
|
||||
public bool UseRealTime { get; set; }
|
||||
|
||||
[DisplayName("Initial Time")]
|
||||
[Description("Initial time of emulation.")]
|
||||
[DefaultValue(typeof(DateTime), "2010-01-01")]
|
||||
[TypeConverter(typeof(BizDateTimeConverter))]
|
||||
public DateTime InitialTime { get; set; }
|
||||
|
||||
[DisplayName("CFG Username")]
|
||||
[Description("The system username that Citra will use during emulation")]
|
||||
[DefaultValue("CITRA")]
|
||||
[MaxLength(10)]
|
||||
[TypeConverter(typeof(ConstrainedStringConverter))]
|
||||
public string CFGUsername { get; set; }
|
||||
|
||||
public enum ECFGBirthdayMonth
|
||||
{
|
||||
January = 1,
|
||||
February,
|
||||
March,
|
||||
April,
|
||||
May,
|
||||
June,
|
||||
July,
|
||||
August,
|
||||
September,
|
||||
October,
|
||||
November,
|
||||
December,
|
||||
}
|
||||
|
||||
[DisplayName("CFG Birthday Month")]
|
||||
[Description("The system birthday month that Citra will use during emulation")]
|
||||
[DefaultValue(ECFGBirthdayMonth.March)]
|
||||
public ECFGBirthdayMonth CFGBirthdayMonth { get; set; }
|
||||
|
||||
[DisplayName("CFG Birthday Day")]
|
||||
[Description("The system birthday day that Citra will use during emulation")]
|
||||
[DefaultValue(25)]
|
||||
[Range(1, 31)]
|
||||
[TypeConverter(typeof(ConstrainedIntConverter))]
|
||||
public int CFGBirthdayDay { get; set; }
|
||||
|
||||
public enum ECFGSystemLanguage
|
||||
{
|
||||
Japan,
|
||||
English,
|
||||
French,
|
||||
German,
|
||||
Italian,
|
||||
Spanish,
|
||||
Simplified_Chinese,
|
||||
Korean,
|
||||
Dutch,
|
||||
Portuguese,
|
||||
Russian,
|
||||
Traditional_Chinese
|
||||
}
|
||||
|
||||
[DisplayName("CFG System Language")]
|
||||
[Description("The system language that Citra will use during emulation")]
|
||||
[DefaultValue(ECFGSystemLanguage.English)]
|
||||
public ECFGSystemLanguage CFGSystemLanguage { get; set; }
|
||||
|
||||
public enum ECFGSoundOutputMode
|
||||
{
|
||||
Mono,
|
||||
Stereo,
|
||||
Surround
|
||||
}
|
||||
|
||||
[DisplayName("CFG Sound Output Mode")]
|
||||
[Description("The system sound output mode that Citra will use during emulation")]
|
||||
[DefaultValue(ECFGSoundOutputMode.Stereo)]
|
||||
public ECFGSoundOutputMode CFGSoundOutputMode { get; set; }
|
||||
|
||||
[DisplayName("CFG Sound Output Mode")]
|
||||
[Description("The system sound output mode that Citra will use during emulation")]
|
||||
[DefaultValue(typeof(ushort), "42")]
|
||||
public ushort PTMPlayCoins { get; set; }
|
||||
|
||||
public CitraSyncSettings Clone()
|
||||
=> (CitraSyncSettings)MemberwiseClone();
|
||||
|
||||
public static bool NeedsReboot(CitraSyncSettings x, CitraSyncSettings y)
|
||||
=> !DeepEquality.DeepEquals(x, y);
|
||||
|
||||
public CitraSyncSettings()
|
||||
=> SettingsUtil.SetDefaultValues(this);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using BizHawk.Emulation.Common;
|
||||
|
||||
namespace BizHawk.Emulation.Cores.Consoles.Nintendo._3DS
|
||||
{
|
||||
public partial class Citra : ISoundProvider
|
||||
{
|
||||
private short[] _sampleBuf = new short[1024 * 2];
|
||||
private int _nsamps;
|
||||
|
||||
private void ProcessSound()
|
||||
{
|
||||
_core.Citra_GetAudio(_context, out var buffer, out var frames);
|
||||
if (frames > _sampleBuf.Length)
|
||||
{
|
||||
_sampleBuf = new short[frames];
|
||||
}
|
||||
|
||||
Marshal.Copy(buffer, _sampleBuf, 0, frames);
|
||||
_nsamps = frames / 2;
|
||||
}
|
||||
|
||||
public bool CanProvideAsync => false;
|
||||
|
||||
public SyncSoundMode SyncMode => SyncSoundMode.Sync;
|
||||
|
||||
public void DiscardSamples()
|
||||
{
|
||||
_nsamps = 0;
|
||||
}
|
||||
|
||||
public void GetSamplesAsync(short[] samples)
|
||||
{
|
||||
throw new NotSupportedException("Aync mode is not supported");
|
||||
}
|
||||
|
||||
public void GetSamplesSync(out short[] samples, out int nsamp)
|
||||
{
|
||||
samples = _sampleBuf;
|
||||
nsamp = _nsamps;
|
||||
DiscardSamples();
|
||||
}
|
||||
|
||||
public void SetSyncMode(SyncSoundMode mode)
|
||||
{
|
||||
if (mode == SyncSoundMode.Async)
|
||||
{
|
||||
throw new NotSupportedException("Async mode is not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
|
||||
using BizHawk.Emulation.Common;
|
||||
|
||||
namespace BizHawk.Emulation.Cores.Consoles.Nintendo._3DS
|
||||
{
|
||||
public partial class Citra : IStatable
|
||||
{
|
||||
private byte[] _stateBuf = Array.Empty<byte>();
|
||||
|
||||
public void SaveStateBinary(BinaryWriter writer)
|
||||
{
|
||||
var stateLen = _core.Citra_StartSaveState(_context);
|
||||
writer.Write(stateLen);
|
||||
|
||||
if (stateLen > _stateBuf.Length)
|
||||
{
|
||||
_stateBuf = new byte[stateLen];
|
||||
}
|
||||
|
||||
_core.Citra_FinishSaveState(_context, _stateBuf);
|
||||
writer.Write(_stateBuf, 0, stateLen);
|
||||
|
||||
// other variables
|
||||
writer.Write(IsLagFrame);
|
||||
writer.Write(LagCount);
|
||||
writer.Write(Frame);
|
||||
// motion emu state
|
||||
writer.Write(_motionEmu.IsTilting);
|
||||
writer.Write(_motionEmu.TiltOrigin.X);
|
||||
writer.Write(_motionEmu.TiltOrigin.Y);
|
||||
writer.Write(_motionEmu.TiltDirection.X);
|
||||
writer.Write(_motionEmu.TiltDirection.Y);
|
||||
writer.Write(_motionEmu.TiltAngle);
|
||||
writer.Write(_motionEmu.PrevTiltQuaternion.X);
|
||||
writer.Write(_motionEmu.PrevTiltQuaternion.Y);
|
||||
writer.Write(_motionEmu.PrevTiltQuaternion.Z);
|
||||
writer.Write(_motionEmu.PrevTiltQuaternion.W);
|
||||
writer.Write(_motionEmu.Gravity.X);
|
||||
writer.Write(_motionEmu.Gravity.Y);
|
||||
writer.Write(_motionEmu.Gravity.Z);
|
||||
writer.Write(_motionEmu.AngularRate.X);
|
||||
writer.Write(_motionEmu.AngularRate.Y);
|
||||
writer.Write(_motionEmu.AngularRate.Z);
|
||||
}
|
||||
|
||||
public void LoadStateBinary(BinaryReader reader)
|
||||
{
|
||||
var stateLen = reader.ReadInt32();
|
||||
|
||||
if (stateLen > _stateBuf.Length)
|
||||
{
|
||||
_stateBuf = new byte[stateLen];
|
||||
}
|
||||
|
||||
reader.Read(_stateBuf, 0, stateLen);
|
||||
_core.Citra_LoadState(_context, _stateBuf, stateLen);
|
||||
|
||||
// other variables
|
||||
IsLagFrame = reader.ReadBoolean();
|
||||
LagCount = reader.ReadInt32();
|
||||
Frame = reader.ReadInt32();
|
||||
// motion emu state
|
||||
_motionEmu.IsTilting = reader.ReadBoolean();
|
||||
_motionEmu.TiltOrigin.X = reader.ReadSingle();
|
||||
_motionEmu.TiltOrigin.Y = reader.ReadSingle();
|
||||
_motionEmu.TiltDirection.X = reader.ReadSingle();
|
||||
_motionEmu.TiltDirection.Y = reader.ReadSingle();
|
||||
_motionEmu.TiltAngle = reader.ReadSingle();
|
||||
_motionEmu.PrevTiltQuaternion.X = reader.ReadSingle();
|
||||
_motionEmu.PrevTiltQuaternion.Y = reader.ReadSingle();
|
||||
_motionEmu.PrevTiltQuaternion.Z = reader.ReadSingle();
|
||||
_motionEmu.PrevTiltQuaternion.W = reader.ReadSingle();
|
||||
_motionEmu.Gravity.X = reader.ReadSingle();
|
||||
_motionEmu.Gravity.Y = reader.ReadSingle();
|
||||
_motionEmu.Gravity.Z = reader.ReadSingle();
|
||||
_motionEmu.AngularRate.X = reader.ReadSingle();
|
||||
_motionEmu.AngularRate.Y = reader.ReadSingle();
|
||||
_motionEmu.AngularRate.Z = reader.ReadSingle();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
using System;
|
||||
|
||||
using BizHawk.Emulation.Common;
|
||||
|
||||
namespace BizHawk.Emulation.Cores.Consoles.Nintendo._3DS
|
||||
{
|
||||
public class CitraVideoProvider : IVideoProvider
|
||||
{
|
||||
internal int Width = 400;
|
||||
internal int Height = 480;
|
||||
internal bool VideoDirty;
|
||||
|
||||
protected readonly LibCitra _core;
|
||||
protected readonly IntPtr _context;
|
||||
|
||||
public CitraVideoProvider(LibCitra core, IntPtr context)
|
||||
{
|
||||
_core = core;
|
||||
_context = context;
|
||||
}
|
||||
|
||||
// ReSharper disable ConvertToAutoPropertyWhenPossible
|
||||
public int VirtualWidth => Width;
|
||||
public int VirtualHeight => Height;
|
||||
public int BufferWidth => Width;
|
||||
public int BufferHeight => Height;
|
||||
public int VsyncNumerator => 268111856;
|
||||
public int VsyncDenominator => 4481136;
|
||||
public int BackgroundColor => 0;
|
||||
|
||||
private int[] _vbuf = new int[400 * 480];
|
||||
|
||||
public int[] GetVideoBuffer()
|
||||
{
|
||||
if (VideoDirty)
|
||||
{
|
||||
if (_vbuf.Length < Width * Height)
|
||||
{
|
||||
_vbuf = new int[Width * Height];
|
||||
}
|
||||
|
||||
_core.Citra_ReadFrameBuffer(_context, _vbuf);
|
||||
VideoDirty = false;
|
||||
}
|
||||
|
||||
return _vbuf;
|
||||
}
|
||||
}
|
||||
|
||||
public class CitraGLTextureProvider : CitraVideoProvider, IGLTextureProvider
|
||||
{
|
||||
public CitraGLTextureProvider(LibCitra core, IntPtr context)
|
||||
: base(core, context)
|
||||
{
|
||||
}
|
||||
|
||||
public int GetGLTexture()
|
||||
=> _core.Citra_GetGLTexture(_context);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
using BizHawk.BizInvoke;
|
||||
using BizHawk.Common;
|
||||
using BizHawk.Emulation.Common;
|
||||
|
||||
namespace BizHawk.Emulation.Cores.Consoles.Nintendo._3DS
|
||||
{
|
||||
[PortedCore(CoreNames.Citra, "Citra Emulator Project", "nightly-1943", "https://citra-emu.org", singleInstance: true, isReleased: false)]
|
||||
[ServiceNotApplicable(new[] { typeof(IDriveLight), typeof(IRegionable) })]
|
||||
public partial class Citra
|
||||
{
|
||||
private static readonly LibCitra _core;
|
||||
|
||||
static Citra()
|
||||
{
|
||||
var resolver = new DynamicLibraryImportResolver(
|
||||
OSTailoredCode.IsUnixHost ? "libcitra-headless.so" : "citra-headless.dll", hasLimitedLifetime: false);
|
||||
_core = BizInvoker.GetInvoker<LibCitra>(resolver, CallingConventionAdapters.Native);
|
||||
}
|
||||
|
||||
private static Citra CurrentCore;
|
||||
|
||||
private readonly IOpenGLProvider _openGLProvider;
|
||||
private readonly bool _supportsOpenGL43;
|
||||
private readonly List<object> _glContexts = new();
|
||||
private readonly LibCitra.ConfigCallbackInterface _configCallbackInterface;
|
||||
private readonly LibCitra.GLCallbackInterface _glCallbackInterface;
|
||||
private readonly LibCitra.InputCallbackInterface _inputCallbackInterface;
|
||||
private readonly IntPtr _context;
|
||||
private readonly CitraVideoProvider _citraVideoProvider;
|
||||
|
||||
[CoreConstructor(VSystemID.Raw._3DS)]
|
||||
public Citra(CoreLoadParameters<CitraSettings, CitraSyncSettings> lp)
|
||||
{
|
||||
if (lp.Roms.Exists(r => r.RomPath.Contains("|")))
|
||||
{
|
||||
throw new InvalidOperationException("3DS does not support compressed ROMs");
|
||||
}
|
||||
|
||||
CurrentCore?.Dispose();
|
||||
CurrentCore = this;
|
||||
|
||||
_serviceProvider = new(this);
|
||||
_settings = lp.Settings ?? new();
|
||||
_syncSettings = lp.SyncSettings ?? new();
|
||||
|
||||
DeterministicEmulation = lp.DeterministicEmulationRequested;
|
||||
_userPath = lp.Comm.CoreFileProvider.GetUserPath(SystemId, temp: DeterministicEmulation) + Path.DirectorySeparatorChar;
|
||||
_userPath = _userPath.Replace('\\', '/'); // Citra doesn't like backslashes in the user folder, for whatever reason
|
||||
|
||||
_configCallbackInterface.GetBoolean = GetBooleanSettingCallback;
|
||||
_configCallbackInterface.GetInteger = GetIntegerSettingCallback;
|
||||
_configCallbackInterface.GetFloat = GetFloatSettingCallback;
|
||||
_configCallbackInterface.GetString = GetStringSettingCallback;
|
||||
|
||||
_openGLProvider = lp.Comm.OpenGLProvider;
|
||||
_supportsOpenGL43 = _openGLProvider.GLVersion >= 430;
|
||||
if (!_supportsOpenGL43 && _syncSettings.GraphicsApi == CitraSyncSettings.EGraphicsApi.OpenGL)
|
||||
{
|
||||
lp.Comm.Notify("OpenGL 4.3 is not supported on this machine, falling back to software renderer", null);
|
||||
}
|
||||
|
||||
_glCallbackInterface.RequestGLContext = RequestGLContextCallback;
|
||||
_glCallbackInterface.ReleaseGLContext = ReleaseGLContextCallback;
|
||||
_glCallbackInterface.ActivateGLContext = ActivateGLContextCallback;
|
||||
_glCallbackInterface.GetGLProcAddress = GetGLProcAddressCallback;
|
||||
|
||||
_inputCallbackInterface.GetButton = GetButtonCallback;
|
||||
_inputCallbackInterface.GetAxis = GetAxisCallback;
|
||||
_inputCallbackInterface.GetTouch = GetTouchCallback;
|
||||
_inputCallbackInterface.GetMotion = GetMotionCallback;
|
||||
|
||||
_context = _core.Citra_CreateContext(ref _configCallbackInterface, ref _glCallbackInterface, ref _inputCallbackInterface);
|
||||
|
||||
if (_supportsOpenGL43 && _syncSettings.GraphicsApi == CitraSyncSettings.EGraphicsApi.OpenGL)
|
||||
{
|
||||
_citraVideoProvider = new CitraGLTextureProvider(_core, _context);
|
||||
}
|
||||
else
|
||||
{
|
||||
_citraVideoProvider = new(_core, _context);
|
||||
}
|
||||
|
||||
_serviceProvider.Register<IVideoProvider>(_citraVideoProvider);
|
||||
|
||||
var boot9 = lp.Comm.CoreFileProvider.GetFirmware(new("3DS", "boot9"));
|
||||
if (boot9 is not null)
|
||||
{
|
||||
File.WriteAllBytes(Path.Combine(_userPath, "sysdata", "boot9.bin"), boot9);
|
||||
}
|
||||
|
||||
var romPath = lp.Roms[0].RomPath;
|
||||
if (lp.Roms[0].Extension.ToLowerInvariant() == ".cia")
|
||||
{
|
||||
var message = new byte[1024];
|
||||
var res = _core.Citra_InstallCIA(_context, romPath, true, message, message.Length);
|
||||
var outMsg = Encoding.UTF8.GetString(message).TrimEnd();
|
||||
if (res)
|
||||
{
|
||||
romPath = outMsg;
|
||||
}
|
||||
else
|
||||
{
|
||||
Dispose();
|
||||
throw new(outMsg);
|
||||
}
|
||||
}
|
||||
|
||||
var errorMessage = new byte[1024];
|
||||
if (!_core.Citra_LoadROM(_context, romPath, errorMessage, errorMessage.Length))
|
||||
{
|
||||
Dispose();
|
||||
throw new($"{Encoding.UTF8.GetString(errorMessage).TrimEnd()}");
|
||||
}
|
||||
}
|
||||
|
||||
private IntPtr RequestGLContextCallback()
|
||||
{
|
||||
var context = _openGLProvider.RequestGLContext(4, 3, false);
|
||||
_glContexts.Add(context);
|
||||
var handle = GCHandle.Alloc(context, GCHandleType.Weak);
|
||||
return GCHandle.ToIntPtr(handle);
|
||||
}
|
||||
|
||||
private void ReleaseGLContextCallback(IntPtr context)
|
||||
{
|
||||
var handle = GCHandle.FromIntPtr(context);
|
||||
_openGLProvider.ReleaseGLContext(handle.Target);
|
||||
_glContexts.Remove(handle.Target);
|
||||
handle.Free();
|
||||
}
|
||||
|
||||
private void ActivateGLContextCallback(IntPtr context)
|
||||
{
|
||||
var handle = GCHandle.FromIntPtr(context);
|
||||
_openGLProvider.ActivateGLContext(handle.Target);
|
||||
}
|
||||
|
||||
private IntPtr GetGLProcAddressCallback(string proc)
|
||||
=> _openGLProvider.GetGLProcAddress(proc);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
using BizHawk.BizInvoke;
|
||||
|
||||
namespace BizHawk.Emulation.Cores.Consoles.Nintendo._3DS
|
||||
{
|
||||
public abstract class LibCitra
|
||||
{
|
||||
private const CallingConvention cc = CallingConvention.Cdecl;
|
||||
|
||||
[UnmanagedFunctionPointer(cc)]
|
||||
public delegate bool GetBooleanSettingCallback(string label);
|
||||
|
||||
[UnmanagedFunctionPointer(cc)]
|
||||
public delegate ulong GetIntegerSettingCallback(string label);
|
||||
|
||||
[UnmanagedFunctionPointer(cc)]
|
||||
public delegate double GetFloatSettingCallback(string label);
|
||||
|
||||
[UnmanagedFunctionPointer(cc)]
|
||||
public delegate void GetStringSettingCallback(string label, IntPtr buffer, int bufferSize);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct ConfigCallbackInterface
|
||||
{
|
||||
public GetBooleanSettingCallback GetBoolean;
|
||||
public GetIntegerSettingCallback GetInteger;
|
||||
public GetFloatSettingCallback GetFloat;
|
||||
public GetStringSettingCallback GetString;
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(cc)]
|
||||
public delegate IntPtr RequestGLContextCallback();
|
||||
|
||||
[UnmanagedFunctionPointer(cc)]
|
||||
public delegate void ReleaseGLContextCallback(IntPtr context);
|
||||
|
||||
[UnmanagedFunctionPointer(cc)]
|
||||
public delegate void ActivateGLContextCallback(IntPtr context);
|
||||
|
||||
[UnmanagedFunctionPointer(cc)]
|
||||
public delegate IntPtr GetGLProcAddressCallback(string proc);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct GLCallbackInterface
|
||||
{
|
||||
public RequestGLContextCallback RequestGLContext;
|
||||
public ReleaseGLContextCallback ReleaseGLContext;
|
||||
public ActivateGLContextCallback ActivateGLContext;
|
||||
public GetGLProcAddressCallback GetGLProcAddress;
|
||||
}
|
||||
|
||||
public enum Buttons
|
||||
{
|
||||
A,
|
||||
B,
|
||||
X,
|
||||
Y,
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
L,
|
||||
R,
|
||||
Start,
|
||||
Select,
|
||||
Debug,
|
||||
Gpio14,
|
||||
ZL,
|
||||
ZR,
|
||||
Home,
|
||||
}
|
||||
|
||||
public enum AnalogSticks
|
||||
{
|
||||
CirclePad,
|
||||
CStick,
|
||||
}
|
||||
|
||||
public enum Axes
|
||||
{
|
||||
X,
|
||||
Y
|
||||
}
|
||||
|
||||
[UnmanagedFunctionPointer(cc)]
|
||||
public delegate bool GetButtonCallback(Buttons button);
|
||||
|
||||
[UnmanagedFunctionPointer(cc)]
|
||||
public delegate void GetAxisCallback(AnalogSticks stick, out float x, out float y);
|
||||
|
||||
[UnmanagedFunctionPointer(cc)]
|
||||
public delegate bool GetTouchCallback(out float x, out float y);
|
||||
|
||||
[UnmanagedFunctionPointer(cc)]
|
||||
public delegate void GetMotionCallback(out float accelX, out float accelY, out float accelZ, out float gyroX, out float gyroY, out float gyroZ);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct InputCallbackInterface
|
||||
{
|
||||
public GetButtonCallback GetButton;
|
||||
public GetAxisCallback GetAxis;
|
||||
public GetTouchCallback GetTouch;
|
||||
public GetMotionCallback GetMotion;
|
||||
}
|
||||
|
||||
[BizImport(cc, Compatibility = true)]
|
||||
public abstract IntPtr Citra_CreateContext(
|
||||
ref ConfigCallbackInterface configCallbackInterface,
|
||||
ref GLCallbackInterface glCallbackInterface,
|
||||
ref InputCallbackInterface inputCallbackInterface);
|
||||
|
||||
[BizImport(cc)]
|
||||
public abstract void Citra_DestroyContext(IntPtr context);
|
||||
|
||||
[BizImport(cc)]
|
||||
public abstract bool Citra_InstallCIA(IntPtr context, string ciaPath, bool force, byte[] messageBuffer, int messageBufferLen);
|
||||
|
||||
[BizImport(cc)]
|
||||
public abstract bool Citra_LoadROM(IntPtr context, string romPath, byte[] errorMessageBuffer, int errorMessageBufferLen);
|
||||
|
||||
[BizImport(cc)]
|
||||
public abstract void Citra_RunFrame(IntPtr context);
|
||||
|
||||
[BizImport(cc)]
|
||||
public abstract void Citra_Reset(IntPtr context);
|
||||
|
||||
[BizImport(cc)]
|
||||
public abstract void Citra_GetVideoDimensions(IntPtr context, out int width, out int height);
|
||||
|
||||
[BizImport(cc)]
|
||||
public abstract int Citra_GetGLTexture(IntPtr context);
|
||||
|
||||
[BizImport(cc)]
|
||||
public abstract void Citra_ReadFrameBuffer(IntPtr context, int[] buffer);
|
||||
|
||||
[BizImport(cc)]
|
||||
public abstract void Citra_GetAudio(IntPtr context, out IntPtr buffer, out int frames);
|
||||
|
||||
[BizImport(cc)]
|
||||
public abstract void Citra_ReloadConfig(IntPtr context);
|
||||
|
||||
[BizImport(cc)]
|
||||
public abstract int Citra_StartSaveState(IntPtr context);
|
||||
|
||||
[BizImport(cc)]
|
||||
public abstract void Citra_FinishSaveState(IntPtr context, byte[] buffer);
|
||||
|
||||
[BizImport(cc)]
|
||||
public abstract void Citra_LoadState(IntPtr context, byte[] buffer, int stateLen);
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ namespace BizHawk.Emulation.Cores
|
|||
public const string Bsnes115 = "BSNESv115+";
|
||||
public const string C64Hawk = "C64Hawk";
|
||||
public const string ChannelFHawk = "ChannelFHawk";
|
||||
public const string Citra = "Citra";
|
||||
public const string ColecoHawk = "ColecoHawk";
|
||||
public const string CPCHawk = "CPCHawk";
|
||||
public const string Cygne = "Cygne/Mednafen";
|
||||
|
|
Loading…
Reference in New Issue