2020-11-17 21:40:19 +00:00
using ARMeilleure.Translation ;
2020-06-16 18:28:02 +00:00
using ARMeilleure.Translation.PTC ;
2019-09-02 16:03:57 +00:00
using Gtk ;
2020-03-25 17:09:38 +00:00
using LibHac.Common ;
using LibHac.Ns ;
2019-09-02 16:03:57 +00:00
using Ryujinx.Audio ;
2021-02-26 00:11:56 +00:00
using Ryujinx.Audio.Backends.Dummy ;
using Ryujinx.Audio.Backends.OpenAL ;
using Ryujinx.Audio.Backends.SoundIo ;
using Ryujinx.Audio.Integration ;
2020-08-30 16:51:53 +00:00
using Ryujinx.Common.Configuration ;
2019-09-02 16:03:57 +00:00
using Ryujinx.Common.Logging ;
2020-08-18 01:49:37 +00:00
using Ryujinx.Common.System ;
2020-01-05 11:49:44 +00:00
using Ryujinx.Configuration ;
2020-01-21 22:23:11 +00:00
using Ryujinx.Graphics.GAL ;
2019-10-13 06:02:07 +00:00
using Ryujinx.Graphics.OpenGL ;
2020-01-05 11:49:44 +00:00
using Ryujinx.HLE.FileSystem ;
2020-01-21 22:23:11 +00:00
using Ryujinx.HLE.FileSystem.Content ;
2020-09-21 03:45:30 +00:00
using Ryujinx.HLE.HOS ;
2021-01-08 08:14:13 +00:00
using Ryujinx.Modules ;
using Ryujinx.Ui.App ;
using Ryujinx.Ui.Applet ;
using Ryujinx.Ui.Helper ;
using Ryujinx.Ui.Widgets ;
using Ryujinx.Ui.Windows ;
2019-09-02 16:03:57 +00:00
using System ;
2020-01-05 11:49:44 +00:00
using System.Diagnostics ;
2019-09-02 16:03:57 +00:00
using System.IO ;
2021-01-18 20:33:58 +00:00
using System.Reflection ;
2021-01-08 08:14:13 +00:00
using System.Runtime.InteropServices ;
2019-09-02 16:03:57 +00:00
using System.Threading ;
2019-12-21 19:52:31 +00:00
using System.Threading.Tasks ;
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
using GUI = Gtk . Builder . ObjectAttribute ;
2021-03-22 18:40:07 +00:00
using PtcLoadingState = ARMeilleure . Translation . PTC . PtcLoadingState ;
using ShaderCacheLoadingState = Ryujinx . Graphics . Gpu . Shader . ShaderCacheState ;
2019-11-29 04:32:51 +00:00
namespace Ryujinx.Ui
2019-09-02 16:03:57 +00:00
{
public class MainWindow : Window
{
2021-01-08 08:14:13 +00:00
private readonly VirtualFileSystem _virtualFileSystem ;
private readonly ContentManager _contentManager ;
2019-09-02 16:03:57 +00:00
2021-01-08 08:14:13 +00:00
private UserChannelPersistence _userChannelPersistence ;
2019-09-02 16:03:57 +00:00
2021-01-08 08:14:13 +00:00
private HLE . Switch _emulationContext ;
2019-09-02 16:03:57 +00:00
2021-01-08 08:14:13 +00:00
private WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution ;
2020-09-21 03:45:30 +00:00
2021-01-08 08:14:13 +00:00
private readonly ApplicationLibrary _applicationLibrary ;
private readonly GtkHostUiHandler _uiHandler ;
private readonly AutoResetEvent _deviceExitStatus ;
private readonly ListStore _tableStore ;
2020-02-06 11:38:24 +00:00
2021-01-08 08:14:13 +00:00
private bool _updatingGameTable ;
private bool _gameLoaded ;
private bool _ending ;
2019-09-02 16:03:57 +00:00
2021-01-08 08:14:13 +00:00
private string _currentEmulatedGamePath = null ;
2021-03-18 20:40:20 +00:00
private string _lastScannedAmiiboId = "" ;
private bool _lastScannedAmiiboShowAll = false ;
2021-01-08 08:14:13 +00:00
public GlRenderer GlRendererWidget ;
2020-05-03 02:00:53 +00:00
2020-04-20 21:59:59 +00:00
#pragma warning disable CS0169 , CS0649 , IDE0044
2020-02-12 00:56:19 +00:00
2020-09-29 20:05:25 +00:00
[GUI] public MenuItem ExitMenuItem ;
[GUI] public MenuItem UpdateMenuItem ;
[GUI] MenuBar _menuBar ;
[GUI] Box _footerBox ;
[GUI] Box _statusBar ;
2021-03-18 20:40:20 +00:00
[GUI] MenuItem _optionMenu ;
[GUI] MenuItem _actionMenu ;
2020-09-29 20:05:25 +00:00
[GUI] MenuItem _stopEmulation ;
2021-01-11 15:03:37 +00:00
[GUI] MenuItem _simulateWakeUpMessage ;
2021-03-18 20:40:20 +00:00
[GUI] MenuItem _scanAmiibo ;
2020-09-29 20:05:25 +00:00
[GUI] MenuItem _fullScreen ;
2020-12-01 22:02:27 +00:00
[GUI] CheckMenuItem _startFullScreen ;
2020-09-29 20:05:25 +00:00
[GUI] CheckMenuItem _favToggle ;
[GUI] MenuItem _firmwareInstallDirectory ;
[GUI] MenuItem _firmwareInstallFile ;
2020-10-13 20:54:42 +00:00
[GUI] Label _fifoStatus ;
2020-09-29 20:05:25 +00:00
[GUI] CheckMenuItem _iconToggle ;
[GUI] CheckMenuItem _developerToggle ;
[GUI] CheckMenuItem _appToggle ;
[GUI] CheckMenuItem _timePlayedToggle ;
[GUI] CheckMenuItem _versionToggle ;
[GUI] CheckMenuItem _lastPlayedToggle ;
[GUI] CheckMenuItem _fileExtToggle ;
[GUI] CheckMenuItem _pathToggle ;
[GUI] CheckMenuItem _fileSizeToggle ;
[GUI] Label _dockedMode ;
2020-12-16 02:19:07 +00:00
[GUI] Label _aspectRatio ;
2020-09-29 20:05:25 +00:00
[GUI] Label _gameStatus ;
[GUI] TreeView _gameTable ;
[GUI] TreeSelection _gameTableSelection ;
[GUI] ScrolledWindow _gameTableWindow ;
[GUI] Label _gpuName ;
[GUI] Label _progressLabel ;
[GUI] Label _firmwareVersionLabel ;
2021-03-03 00:39:36 +00:00
[GUI] ProgressBar _progressBar ;
2020-09-29 20:05:25 +00:00
[GUI] Box _viewBox ;
[GUI] Label _vSyncStatus ;
[GUI] Box _listStatusBox ;
2021-03-03 00:39:36 +00:00
[GUI] Label _loadingStatusLabel ;
[GUI] ProgressBar _loadingStatusBar ;
2020-04-20 21:59:59 +00:00
#pragma warning restore CS0649 , IDE0044 , CS0169
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
public MainWindow ( ) : this ( new Builder ( "Ryujinx.Ui.MainWindow.glade" ) ) { }
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
private MainWindow ( Builder builder ) : base ( builder . GetObject ( "_mainWin" ) . Handle )
2019-09-02 16:03:57 +00:00
{
2019-11-29 04:32:51 +00:00
builder . Autoconnect ( this ) ;
2021-01-08 08:14:13 +00:00
// Apply custom theme if needed.
ThemeHelper . ApplyTheme ( ) ;
// Sets overridden fields.
2020-03-30 22:10:13 +00:00
int monitorWidth = Display . PrimaryMonitor . Geometry . Width * Display . PrimaryMonitor . ScaleFactor ;
int monitorHeight = Display . PrimaryMonitor . Geometry . Height * Display . PrimaryMonitor . ScaleFactor ;
2021-01-08 08:14:13 +00:00
DefaultWidth = monitorWidth < 1280 ? monitorWidth : 1280 ;
DefaultHeight = monitorHeight < 760 ? monitorHeight : 760 ;
2019-11-29 04:32:51 +00:00
2021-01-18 20:33:58 +00:00
Icon = new Gdk . Pixbuf ( Assembly . GetExecutingAssembly ( ) , "Ryujinx.Ui.Resources.Logo_Ryujinx.png" ) ;
2021-01-08 08:14:13 +00:00
Title = $"Ryujinx {Program.Version}" ;
2019-12-22 02:49:51 +00:00
2021-01-08 08:14:13 +00:00
// Hide emulation context status bar.
_statusBar . Hide ( ) ;
2020-01-05 11:49:44 +00:00
2021-01-08 08:14:13 +00:00
// Instanciate HLE objects.
2020-11-19 00:34:28 +00:00
_virtualFileSystem = VirtualFileSystem . CreateInstance ( ) ;
_contentManager = new ContentManager ( _virtualFileSystem ) ;
2021-01-08 08:14:13 +00:00
_userChannelPersistence = new UserChannelPersistence ( ) ;
2019-09-02 16:03:57 +00:00
2021-01-08 08:14:13 +00:00
// Instanciate GUI objects.
_applicationLibrary = new ApplicationLibrary ( _virtualFileSystem ) ;
_uiHandler = new GtkHostUiHandler ( this ) ;
_deviceExitStatus = new AutoResetEvent ( false ) ;
2020-01-05 11:49:44 +00:00
2021-01-08 08:14:13 +00:00
WindowStateEvent + = WindowStateEvent_Changed ;
DeleteEvent + = Window_Close ;
2020-01-05 11:49:44 +00:00
2021-01-08 08:14:13 +00:00
_applicationLibrary . ApplicationAdded + = Application_Added ;
_applicationLibrary . ApplicationCountUpdated + = ApplicationCount_Updated ;
2021-03-18 20:40:20 +00:00
_actionMenu . StateChanged + = ActionMenu_StateChanged ;
2021-01-08 08:14:13 +00:00
_gameTable . ButtonReleaseEvent + = Row_Clicked ;
_fullScreen . Activated + = FullScreen_Toggled ;
2020-01-21 22:23:11 +00:00
2021-01-08 08:14:13 +00:00
GlRenderer . StatusUpdatedEvent + = Update_StatusBar ;
2019-09-02 16:03:57 +00:00
2020-12-01 22:02:27 +00:00
if ( ConfigurationState . Instance . Ui . StartFullscreen )
{
_startFullScreen . Active = true ;
}
2021-03-18 20:40:20 +00:00
_actionMenu . Sensitive = false ;
2019-09-02 16:03:57 +00:00
2019-12-21 19:52:31 +00:00
if ( ConfigurationState . Instance . Ui . GuiColumns . FavColumn ) _favToggle . Active = true ;
if ( ConfigurationState . Instance . Ui . GuiColumns . IconColumn ) _iconToggle . Active = true ;
if ( ConfigurationState . Instance . Ui . GuiColumns . AppColumn ) _appToggle . Active = true ;
if ( ConfigurationState . Instance . Ui . GuiColumns . DevColumn ) _developerToggle . Active = true ;
if ( ConfigurationState . Instance . Ui . GuiColumns . VersionColumn ) _versionToggle . Active = true ;
if ( ConfigurationState . Instance . Ui . GuiColumns . TimePlayedColumn ) _timePlayedToggle . Active = true ;
if ( ConfigurationState . Instance . Ui . GuiColumns . LastPlayedColumn ) _lastPlayedToggle . Active = true ;
if ( ConfigurationState . Instance . Ui . GuiColumns . FileExtColumn ) _fileExtToggle . Active = true ;
if ( ConfigurationState . Instance . Ui . GuiColumns . FileSizeColumn ) _fileSizeToggle . Active = true ;
if ( ConfigurationState . Instance . Ui . GuiColumns . PathColumn ) _pathToggle . Active = true ;
2019-11-29 04:32:51 +00:00
2021-02-09 09:24:37 +00:00
_favToggle . Toggled + = Fav_Toggled ;
_iconToggle . Toggled + = Icon_Toggled ;
_appToggle . Toggled + = App_Toggled ;
_developerToggle . Toggled + = Developer_Toggled ;
_versionToggle . Toggled + = Version_Toggled ;
_timePlayedToggle . Toggled + = TimePlayed_Toggled ;
_lastPlayedToggle . Toggled + = LastPlayed_Toggled ;
_fileExtToggle . Toggled + = FileExt_Toggled ;
_fileSizeToggle . Toggled + = FileSize_Toggled ;
_pathToggle . Toggled + = Path_Toggled ;
2019-11-29 04:32:51 +00:00
_gameTable . Model = _tableStore = new ListStore (
2019-12-21 19:52:31 +00:00
typeof ( bool ) ,
typeof ( Gdk . Pixbuf ) ,
typeof ( string ) ,
typeof ( string ) ,
typeof ( string ) ,
typeof ( string ) ,
typeof ( string ) ,
typeof ( string ) ,
typeof ( string ) ,
2020-03-25 17:09:38 +00:00
typeof ( string ) ,
typeof ( BlitStruct < ApplicationControlProperty > ) ) ;
2019-12-21 19:52:31 +00:00
2021-01-08 08:14:13 +00:00
_tableStore . SetSortFunc ( 5 , SortHelper . TimePlayedSort ) ;
_tableStore . SetSortFunc ( 6 , SortHelper . LastPlayedSort ) ;
_tableStore . SetSortFunc ( 8 , SortHelper . FileSizeSort ) ;
2020-06-26 10:30:16 +00:00
int columnId = ConfigurationState . Instance . Ui . ColumnSort . SortColumnId ;
bool ascending = ConfigurationState . Instance . Ui . ColumnSort . SortAscending ;
_tableStore . SetSortColumnId ( columnId , ascending ? SortType . Ascending : SortType . Descending ) ;
2019-11-29 04:32:51 +00:00
2020-04-25 13:02:44 +00:00
_gameTable . EnableSearch = true ;
_gameTable . SearchColumn = 2 ;
2019-11-29 04:32:51 +00:00
UpdateColumns ( ) ;
UpdateGameTable ( ) ;
2020-01-12 02:10:55 +00:00
2020-07-03 23:29:36 +00:00
ConfigurationState . Instance . Ui . GameDirs . Event + = ( sender , args ) = >
{
if ( args . OldValue ! = args . NewValue )
{
UpdateGameTable ( ) ;
}
} ;
2020-01-12 02:10:55 +00:00
Task . Run ( RefreshFirmwareLabel ) ;
2019-11-29 04:32:51 +00:00
}
2021-01-08 08:14:13 +00:00
private void WindowStateEvent_Changed ( object o , WindowStateEventArgs args )
2020-07-23 13:12:19 +00:00
{
_fullScreen . Label = args . Event . NewWindowState . HasFlag ( Gdk . WindowState . Fullscreen ) ? "Exit Fullscreen" : "Enter Fullscreen" ;
}
2019-11-29 04:32:51 +00:00
private void UpdateColumns ( )
2019-09-02 16:03:57 +00:00
{
2019-11-29 04:32:51 +00:00
foreach ( TreeViewColumn column in _gameTable . Columns )
{
_gameTable . RemoveColumn ( column ) ;
}
2019-09-02 16:03:57 +00:00
2019-11-29 04:32:51 +00:00
CellRendererToggle favToggle = new CellRendererToggle ( ) ;
favToggle . Toggled + = FavToggle_Toggled ;
2019-12-21 19:52:31 +00:00
if ( ConfigurationState . Instance . Ui . GuiColumns . FavColumn ) _gameTable . AppendColumn ( "Fav" , favToggle , "active" , 0 ) ;
if ( ConfigurationState . Instance . Ui . GuiColumns . IconColumn ) _gameTable . AppendColumn ( "Icon" , new CellRendererPixbuf ( ) , "pixbuf" , 1 ) ;
if ( ConfigurationState . Instance . Ui . GuiColumns . AppColumn ) _gameTable . AppendColumn ( "Application" , new CellRendererText ( ) , "text" , 2 ) ;
if ( ConfigurationState . Instance . Ui . GuiColumns . DevColumn ) _gameTable . AppendColumn ( "Developer" , new CellRendererText ( ) , "text" , 3 ) ;
if ( ConfigurationState . Instance . Ui . GuiColumns . VersionColumn ) _gameTable . AppendColumn ( "Version" , new CellRendererText ( ) , "text" , 4 ) ;
if ( ConfigurationState . Instance . Ui . GuiColumns . TimePlayedColumn ) _gameTable . AppendColumn ( "Time Played" , new CellRendererText ( ) , "text" , 5 ) ;
if ( ConfigurationState . Instance . Ui . GuiColumns . LastPlayedColumn ) _gameTable . AppendColumn ( "Last Played" , new CellRendererText ( ) , "text" , 6 ) ;
if ( ConfigurationState . Instance . Ui . GuiColumns . FileExtColumn ) _gameTable . AppendColumn ( "File Ext" , new CellRendererText ( ) , "text" , 7 ) ;
if ( ConfigurationState . Instance . Ui . GuiColumns . FileSizeColumn ) _gameTable . AppendColumn ( "File Size" , new CellRendererText ( ) , "text" , 8 ) ;
if ( ConfigurationState . Instance . Ui . GuiColumns . PathColumn ) _gameTable . AppendColumn ( "Path" , new CellRendererText ( ) , "text" , 9 ) ;
2019-11-29 04:32:51 +00:00
foreach ( TreeViewColumn column in _gameTable . Columns )
2019-09-02 16:03:57 +00:00
{
2020-06-26 10:30:16 +00:00
switch ( column . Title )
{
case "Fav" :
column . SortColumnId = 0 ;
column . Clicked + = Column_Clicked ;
break ;
case "Application" :
column . SortColumnId = 2 ;
column . Clicked + = Column_Clicked ;
break ;
case "Developer" :
column . SortColumnId = 3 ;
column . Clicked + = Column_Clicked ;
break ;
case "Version" :
column . SortColumnId = 4 ;
column . Clicked + = Column_Clicked ;
break ;
case "Time Played" :
column . SortColumnId = 5 ;
column . Clicked + = Column_Clicked ;
break ;
case "Last Played" :
column . SortColumnId = 6 ;
column . Clicked + = Column_Clicked ;
break ;
case "File Ext" :
column . SortColumnId = 7 ;
column . Clicked + = Column_Clicked ;
break ;
case "File Size" :
column . SortColumnId = 8 ;
column . Clicked + = Column_Clicked ;
break ;
case "Path" :
column . SortColumnId = 9 ;
column . Clicked + = Column_Clicked ;
break ;
}
2019-09-02 16:03:57 +00:00
}
2019-12-21 19:52:31 +00:00
}
2021-01-08 08:14:13 +00:00
private void InitializeSwitchInstance ( )
2019-12-21 19:52:31 +00:00
{
2020-01-21 22:23:11 +00:00
_virtualFileSystem . Reload ( ) ;
2021-02-26 00:11:56 +00:00
IRenderer renderer = new Renderer ( ) ;
IHardwareDeviceDriver deviceDriver = new DummyHardwareDeviceDriver ( ) ;
2021-01-08 08:14:13 +00:00
if ( ConfigurationState . Instance . System . AudioBackend . Value = = AudioBackend . SoundIo )
{
2021-02-26 00:11:56 +00:00
if ( SoundIoHardwareDeviceDriver . IsSupported )
2021-01-08 08:14:13 +00:00
{
2021-02-26 00:11:56 +00:00
deviceDriver = new SoundIoHardwareDeviceDriver ( ) ;
2021-01-08 08:14:13 +00:00
}
else
{
Logger . Warning ? . Print ( LogClass . Audio , "SoundIO is not supported, falling back to dummy audio out." ) ;
}
}
else if ( ConfigurationState . Instance . System . AudioBackend . Value = = AudioBackend . OpenAl )
{
2021-02-26 00:11:56 +00:00
if ( OpenALHardwareDeviceDriver . IsSupported )
2021-01-08 08:14:13 +00:00
{
2021-02-26 00:11:56 +00:00
deviceDriver = new OpenALHardwareDeviceDriver ( ) ;
2021-01-08 08:14:13 +00:00
}
else
{
Logger . Warning ? . Print ( LogClass . Audio , "OpenAL is not supported, trying to fall back to SoundIO." ) ;
2021-02-26 00:11:56 +00:00
if ( SoundIoHardwareDeviceDriver . IsSupported )
2021-01-08 08:14:13 +00:00
{
Logger . Warning ? . Print ( LogClass . Audio , "Found SoundIO, changing configuration." ) ;
ConfigurationState . Instance . System . AudioBackend . Value = AudioBackend . SoundIo ;
SaveConfig ( ) ;
2021-02-26 00:11:56 +00:00
deviceDriver = new SoundIoHardwareDeviceDriver ( ) ;
2021-01-08 08:14:13 +00:00
}
else
{
Logger . Warning ? . Print ( LogClass . Audio , "SoundIO is not supported, falling back to dummy audio out." ) ;
}
}
}
2021-04-04 12:06:59 +00:00
var memoryConfiguration = ConfigurationState . Instance . System . ExpandRam . Value
? HLE . MemoryConfiguration . MemoryConfiguration6GB
: HLE . MemoryConfiguration . MemoryConfiguration4GB ;
_emulationContext = new HLE . Switch (
_virtualFileSystem ,
_contentManager ,
_userChannelPersistence ,
renderer ,
deviceDriver ,
memoryConfiguration )
2020-08-03 01:30:58 +00:00
{
UiHandler = _uiHandler
} ;
2019-12-21 19:52:31 +00:00
2021-01-08 08:14:13 +00:00
_emulationContext . Initialize ( ) ;
2019-09-02 16:03:57 +00:00
}
2021-03-03 00:39:36 +00:00
private void SetupProgressUiHandlers ( )
{
2021-03-22 18:40:07 +00:00
Ptc . PtcStateChanged - = ProgressHandler ;
Ptc . PtcStateChanged + = ProgressHandler ;
2021-03-03 00:39:36 +00:00
2021-03-22 18:40:07 +00:00
_emulationContext . Gpu . ShaderCacheStateChanged - = ProgressHandler ;
_emulationContext . Gpu . ShaderCacheStateChanged + = ProgressHandler ;
2021-03-03 00:39:36 +00:00
}
2021-03-22 18:40:07 +00:00
private void ProgressHandler < T > ( T state , int current , int total ) where T : Enum
2021-03-03 00:39:36 +00:00
{
2021-03-22 18:40:07 +00:00
bool visible ;
string label ;
2021-03-03 00:39:36 +00:00
2021-03-22 18:40:07 +00:00
switch ( state )
2021-03-03 00:39:36 +00:00
{
2021-03-22 18:40:07 +00:00
case PtcLoadingState ptcState :
visible = ptcState ! = PtcLoadingState . Loaded ;
label = $"PTC : {current}/{total}" ;
break ;
case ShaderCacheLoadingState shaderCacheState :
visible = shaderCacheState ! = ShaderCacheLoadingState . Loaded ;
label = $"Shaders : {current}/{total}" ;
break ;
default :
throw new ArgumentException ( $"Unknown Progress Handler type {typeof(T)}" ) ;
}
2021-03-03 00:39:36 +00:00
Application . Invoke ( delegate
{
2021-03-22 18:40:07 +00:00
_loadingStatusLabel . Text = label ;
_loadingStatusBar . Fraction = total > 0 ? ( double ) current / total : 0 ;
_loadingStatusBar . Visible = visible ;
_loadingStatusLabel . Visible = visible ;
2021-03-03 00:39:36 +00:00
} ) ;
}
2021-01-08 08:14:13 +00:00
public void UpdateGameTable ( )
2019-09-02 16:03:57 +00:00
{
2020-07-03 23:29:36 +00:00
if ( _updatingGameTable | | _gameLoaded )
2019-09-02 16:03:57 +00:00
{
2019-11-29 04:32:51 +00:00
return ;
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
_updatingGameTable = true ;
_tableStore . Clear ( ) ;
2020-01-31 18:21:46 +00:00
Thread applicationLibraryThread = new Thread ( ( ) = >
{
2021-01-08 08:14:13 +00:00
_applicationLibrary . LoadApplications ( ConfigurationState . Instance . Ui . GameDirs , ConfigurationState . Instance . System . Language ) ;
2019-11-29 04:32:51 +00:00
2020-01-31 18:21:46 +00:00
_updatingGameTable = false ;
} ) ;
2021-01-08 08:14:13 +00:00
applicationLibraryThread . Name = "GUI.ApplicationLibraryThread" ;
2020-01-31 18:21:46 +00:00
applicationLibraryThread . IsBackground = true ;
applicationLibraryThread . Start ( ) ;
2019-09-02 16:03:57 +00:00
}
2021-01-08 08:14:13 +00:00
[Conditional("RELEASE")]
public void PerformanceCheck ( )
2019-09-02 16:03:57 +00:00
{
2021-01-08 08:14:13 +00:00
if ( ConfigurationState . Instance . Logger . EnableDebug . Value )
2019-09-02 16:03:57 +00:00
{
2021-01-08 08:14:13 +00:00
MessageDialog debugWarningDialog = new MessageDialog ( this , DialogFlags . Modal , MessageType . Warning , ButtonsType . YesNo , null )
{
Title = "Ryujinx - Warning" ,
Text = "You have debug logging enabled, which is designed to be used by developers only." ,
SecondaryText = "For optimal performance, it's recommended to disable debug logging. Would you like to disable debug logging now?"
} ;
if ( debugWarningDialog . Run ( ) = = ( int ) ResponseType . Yes )
{
ConfigurationState . Instance . Logger . EnableDebug . Value = false ;
SaveConfig ( ) ;
}
debugWarningDialog . Dispose ( ) ;
2019-09-02 16:03:57 +00:00
}
2021-01-08 08:14:13 +00:00
if ( ! string . IsNullOrWhiteSpace ( ConfigurationState . Instance . Graphics . ShadersDumpPath . Value ) )
2019-09-02 16:03:57 +00:00
{
2021-01-08 08:14:13 +00:00
MessageDialog shadersDumpWarningDialog = new MessageDialog ( this , DialogFlags . Modal , MessageType . Warning , ButtonsType . YesNo , null )
2020-03-30 21:39:46 +00:00
{
2021-01-08 08:14:13 +00:00
Title = "Ryujinx - Warning" ,
Text = "You have shader dumping enabled, which is designed to be used by developers only." ,
SecondaryText = "For optimal performance, it's recommended to disable shader dumping. Would you like to disable shader dumping now?"
} ;
2020-03-30 21:39:46 +00:00
2021-01-08 08:14:13 +00:00
if ( shadersDumpWarningDialog . Run ( ) = = ( int ) ResponseType . Yes )
{
ConfigurationState . Instance . Graphics . ShadersDumpPath . Value = "" ;
SaveConfig ( ) ;
2020-03-30 21:39:46 +00:00
}
2021-01-08 08:14:13 +00:00
shadersDumpWarningDialog . Dispose ( ) ;
}
}
2020-03-30 21:39:46 +00:00
2021-01-08 08:14:13 +00:00
public void LoadApplication ( string path )
{
if ( _gameLoaded )
{
2021-03-18 22:44:39 +00:00
GtkDialog . CreateInfoDialog ( "A game has already been loaded" , "Please stop emulation or close the emulator before launching another game." ) ;
2021-01-08 08:14:13 +00:00
}
else
{
PerformanceCheck ( ) ;
2020-03-30 21:39:46 +00:00
2019-09-19 23:59:48 +00:00
Logger . RestartTime ( ) ;
2021-01-08 08:14:13 +00:00
InitializeSwitchInstance ( ) ;
2020-01-21 22:23:11 +00:00
2020-07-07 02:41:07 +00:00
UpdateGraphicsConfig ( ) ;
2019-12-21 19:52:31 +00:00
2021-03-22 18:40:07 +00:00
SetupProgressUiHandlers ( ) ;
2020-09-01 09:09:42 +00:00
SystemVersion firmwareVersion = _contentManager . GetCurrentFirmwareVersion ( ) ;
bool isDirectory = Directory . Exists ( path ) ;
if ( ! SetupValidator . CanStartApplication ( _contentManager , path , out UserError userError ) )
{
if ( SetupValidator . CanFixStartApplication ( _contentManager , path , userError , out firmwareVersion ) )
{
if ( userError = = UserError . NoFirmware )
{
2021-01-08 08:14:13 +00:00
string message = $"Would you like to install the firmware embedded in this game? (Firmware {firmwareVersion.VersionString})" ;
2020-09-01 09:09:42 +00:00
2021-01-08 08:14:13 +00:00
ResponseType responseDialog = ( ResponseType ) GtkDialog . CreateConfirmationDialog ( "No Firmware Installed" , message ) . Run ( ) ;
2020-09-01 09:09:42 +00:00
2021-01-08 08:14:13 +00:00
if ( responseDialog ! = ResponseType . Yes )
{
2020-09-01 09:09:42 +00:00
UserErrorDialog . CreateUserErrorDialog ( userError ) ;
2021-01-08 08:14:13 +00:00
_emulationContext . Dispose ( ) ;
2020-09-01 09:09:42 +00:00
return ;
}
}
if ( ! SetupValidator . TryFixStartApplication ( _contentManager , path , userError , out _ ) )
{
UserErrorDialog . CreateUserErrorDialog ( userError ) ;
2021-01-08 08:14:13 +00:00
_emulationContext . Dispose ( ) ;
2020-09-01 09:09:42 +00:00
return ;
}
// Tell the user that we installed a firmware for them.
if ( userError = = UserError . NoFirmware )
{
firmwareVersion = _contentManager . GetCurrentFirmwareVersion ( ) ;
RefreshFirmwareLabel ( ) ;
2021-01-08 08:14:13 +00:00
string message = $"No installed firmware was found but Ryujinx was able to install firmware {firmwareVersion.VersionString} from the provided game.\nThe emulator will now start." ;
GtkDialog . CreateInfoDialog ( $"Firmware {firmwareVersion.VersionString} was installed" , message ) ;
2020-09-01 09:09:42 +00:00
}
}
else
{
UserErrorDialog . CreateUserErrorDialog ( userError ) ;
2021-01-08 08:14:13 +00:00
_emulationContext . Dispose ( ) ;
2020-09-01 09:09:42 +00:00
return ;
}
}
Logger . Notice . Print ( LogClass . Application , $"Using Firmware Version: {firmwareVersion?.VersionString}" ) ;
2020-05-05 11:51:04 +00:00
2019-09-02 16:03:57 +00:00
if ( Directory . Exists ( path ) )
{
string [ ] romFsFiles = Directory . GetFiles ( path , "*.istorage" ) ;
if ( romFsFiles . Length = = 0 )
{
romFsFiles = Directory . GetFiles ( path , "*.romfs" ) ;
}
if ( romFsFiles . Length > 0 )
{
2020-08-03 23:32:53 +00:00
Logger . Info ? . Print ( LogClass . Application , "Loading as cart with RomFS." ) ;
2021-01-08 08:14:13 +00:00
_emulationContext . LoadCart ( path , romFsFiles [ 0 ] ) ;
2019-09-02 16:03:57 +00:00
}
else
{
2020-08-03 23:32:53 +00:00
Logger . Info ? . Print ( LogClass . Application , "Loading as cart WITHOUT RomFS." ) ;
2021-01-08 08:14:13 +00:00
_emulationContext . LoadCart ( path ) ;
2019-09-02 16:03:57 +00:00
}
}
else if ( File . Exists ( path ) )
{
switch ( System . IO . Path . GetExtension ( path ) . ToLowerInvariant ( ) )
{
case ".xci" :
2020-08-03 23:32:53 +00:00
Logger . Info ? . Print ( LogClass . Application , "Loading as XCI." ) ;
2021-01-08 08:14:13 +00:00
_emulationContext . LoadXci ( path ) ;
2019-09-02 16:03:57 +00:00
break ;
case ".nca" :
2020-08-03 23:32:53 +00:00
Logger . Info ? . Print ( LogClass . Application , "Loading as NCA." ) ;
2021-01-08 08:14:13 +00:00
_emulationContext . LoadNca ( path ) ;
2019-09-02 16:03:57 +00:00
break ;
case ".nsp" :
case ".pfs0" :
2020-08-03 23:32:53 +00:00
Logger . Info ? . Print ( LogClass . Application , "Loading as NSP." ) ;
2021-01-08 08:14:13 +00:00
_emulationContext . LoadNsp ( path ) ;
2019-09-02 16:03:57 +00:00
break ;
default :
2021-03-18 22:44:39 +00:00
Logger . Info ? . Print ( LogClass . Application , "Loading as Homebrew." ) ;
2019-09-02 16:03:57 +00:00
try
{
2021-01-08 08:14:13 +00:00
_emulationContext . LoadProgram ( path ) ;
2019-09-02 16:03:57 +00:00
}
catch ( ArgumentOutOfRangeException )
{
2021-03-18 22:44:39 +00:00
Logger . Error ? . Print ( LogClass . Application , "The specified file is not supported by Ryujinx." ) ;
2019-09-02 16:03:57 +00:00
}
break ;
}
}
else
{
2020-08-03 23:32:53 +00:00
Logger . Warning ? . Print ( LogClass . Application , "Please specify a valid XCI/NCA/NSP/PFS0/NRO file." ) ;
2021-01-08 08:14:13 +00:00
_emulationContext . Dispose ( ) ;
2020-03-29 03:25:54 +00:00
return ;
2019-09-02 16:03:57 +00:00
}
2021-01-08 08:14:13 +00:00
_currentEmulatedGamePath = path ;
2020-01-21 22:23:11 +00:00
2020-02-13 17:43:29 +00:00
_deviceExitStatus . Reset ( ) ;
2020-02-06 11:38:24 +00:00
2020-11-17 21:40:19 +00:00
Translator . IsReadyForTranslation . Reset ( ) ;
#if MACOS_BUILD
2021-01-08 08:14:13 +00:00
CreateGameWindow ( ) ;
2020-11-17 21:40:19 +00:00
#else
Thread windowThread = new Thread ( ( ) = >
{
2021-01-08 08:14:13 +00:00
CreateGameWindow ( ) ;
2020-11-17 21:40:19 +00:00
} )
{
Name = "GUI.WindowThread"
} ;
windowThread . Start ( ) ;
#endif
2021-03-18 20:40:20 +00:00
_gameLoaded = true ;
_actionMenu . Sensitive = true ;
_lastScannedAmiiboId = "" ;
2019-09-02 16:03:57 +00:00
2020-01-12 02:10:55 +00:00
_firmwareInstallFile . Sensitive = false ;
_firmwareInstallDirectory . Sensitive = false ;
2021-01-08 08:14:13 +00:00
DiscordIntegrationModule . SwitchToPlayingState ( _emulationContext . Application . TitleIdText , _emulationContext . Application . TitleName ) ;
2019-09-02 16:03:57 +00:00
2021-01-08 08:14:13 +00:00
_applicationLibrary . LoadAndSaveMetaData ( _emulationContext . Application . TitleIdText , appMetadata = >
2019-09-02 16:03:57 +00:00
{
2020-01-12 03:01:04 +00:00
appMetadata . LastPlayed = DateTime . UtcNow . ToString ( ) ;
} ) ;
2019-09-02 16:03:57 +00:00
}
}
2021-01-08 08:14:13 +00:00
private void CreateGameWindow ( )
2019-09-02 16:03:57 +00:00
{
2021-01-08 08:14:13 +00:00
if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
2020-08-18 01:49:37 +00:00
{
_windowsMultimediaTimerResolution = new WindowsMultimediaTimerResolution ( 1 ) ;
}
2021-01-24 23:02:00 +00:00
DisplaySleep . Prevent ( ) ;
2021-01-08 08:14:13 +00:00
GlRendererWidget = new GlRenderer ( _emulationContext , ConfigurationState . Instance . Logger . GraphicsDebugLevel ) ;
2020-02-12 00:56:19 +00:00
Application . Invoke ( delegate
2019-09-02 16:03:57 +00:00
{
2020-02-12 00:56:19 +00:00
_viewBox . Remove ( _gameTableWindow ) ;
2021-01-08 08:14:13 +00:00
GlRendererWidget . Expand = true ;
_viewBox . Child = GlRendererWidget ;
2020-02-12 00:56:19 +00:00
2021-01-08 08:14:13 +00:00
GlRendererWidget . ShowAll ( ) ;
EditFooterForGameRenderer ( ) ;
2020-07-23 13:12:19 +00:00
2021-01-08 08:14:13 +00:00
if ( Window . State . HasFlag ( Gdk . WindowState . Fullscreen ) )
2020-07-23 13:12:19 +00:00
{
ToggleExtraWidgets ( false ) ;
}
2020-12-01 22:02:27 +00:00
else if ( ConfigurationState . Instance . Ui . StartFullscreen . Value )
{
FullScreen_Toggled ( null , null ) ;
}
2020-02-12 00:56:19 +00:00
} ) ;
2021-01-08 08:14:13 +00:00
GlRendererWidget . WaitEvent . WaitOne ( ) ;
2020-02-12 00:56:19 +00:00
2021-01-08 08:14:13 +00:00
GlRendererWidget . Start ( ) ;
2020-02-12 00:56:19 +00:00
2020-06-16 18:28:02 +00:00
Ptc . Close ( ) ;
PtcProfiler . Stop ( ) ;
2021-01-08 08:14:13 +00:00
_emulationContext . Dispose ( ) ;
2020-02-13 17:43:29 +00:00
_deviceExitStatus . Set ( ) ;
// NOTE: Everything that is here will not be executed when you close the UI.
2020-02-12 00:56:19 +00:00
Application . Invoke ( delegate
{
2021-01-08 08:14:13 +00:00
if ( Window . State . HasFlag ( Gdk . WindowState . Fullscreen ) )
2020-07-23 13:12:19 +00:00
{
ToggleExtraWidgets ( true ) ;
}
2021-01-08 08:14:13 +00:00
GlRendererWidget . Exit ( ) ;
2020-02-12 00:56:19 +00:00
2021-04-04 12:06:59 +00:00
if ( GlRendererWidget . Window ! = Window & & GlRendererWidget . Window ! = null )
2020-02-12 00:56:19 +00:00
{
2021-01-08 08:14:13 +00:00
GlRendererWidget . Window . Dispose ( ) ;
2020-02-12 00:56:19 +00:00
}
2021-01-08 08:14:13 +00:00
GlRendererWidget . Dispose ( ) ;
2020-08-18 01:49:37 +00:00
_windowsMultimediaTimerResolution ? . Dispose ( ) ;
_windowsMultimediaTimerResolution = null ;
2021-01-24 23:02:00 +00:00
DisplaySleep . Restore ( ) ;
2020-02-13 17:43:29 +00:00
2021-01-19 02:31:59 +00:00
_viewBox . Remove ( GlRendererWidget ) ;
2020-02-12 00:56:19 +00:00
_viewBox . Add ( _gameTableWindow ) ;
_gameTableWindow . Expand = true ;
2021-01-08 08:14:13 +00:00
Window . Title = $"Ryujinx {Program.Version}" ;
2020-02-12 00:56:19 +00:00
2020-02-13 17:43:29 +00:00
_emulationContext = null ;
_gameLoaded = false ;
2021-01-08 08:14:13 +00:00
GlRendererWidget = null ;
2020-02-13 17:43:29 +00:00
DiscordIntegrationModule . SwitchToMainMenu ( ) ;
RecreateFooterForMenu ( ) ;
2020-02-12 00:56:19 +00:00
UpdateColumns ( ) ;
UpdateGameTable ( ) ;
Task . Run ( RefreshFirmwareLabel ) ;
2020-09-21 03:45:30 +00:00
Task . Run ( HandleRelaunch ) ;
2020-01-21 22:23:11 +00:00
2021-03-18 20:40:20 +00:00
_actionMenu . Sensitive = false ;
2020-01-21 22:23:11 +00:00
_firmwareInstallFile . Sensitive = true ;
_firmwareInstallDirectory . Sensitive = true ;
} ) ;
2020-02-13 17:43:29 +00:00
}
private void RecreateFooterForMenu ( )
{
2020-03-07 02:40:06 +00:00
_listStatusBox . Show ( ) ;
_statusBar . Hide ( ) ;
2020-02-13 17:43:29 +00:00
}
2020-02-12 00:56:19 +00:00
2021-01-08 08:14:13 +00:00
private void EditFooterForGameRenderer ( )
2020-02-13 17:43:29 +00:00
{
2020-03-07 02:40:06 +00:00
_listStatusBox . Hide ( ) ;
_statusBar . Show ( ) ;
2020-02-12 00:56:19 +00:00
}
public void ToggleExtraWidgets ( bool show )
{
2021-01-08 08:14:13 +00:00
if ( GlRendererWidget ! = null )
2020-02-12 00:56:19 +00:00
{
if ( show )
{
_menuBar . ShowAll ( ) ;
2020-03-07 02:40:06 +00:00
_footerBox . Show ( ) ;
_statusBar . Show ( ) ;
2020-02-12 00:56:19 +00:00
}
else
{
_menuBar . Hide ( ) ;
_footerBox . Hide ( ) ;
}
}
2020-01-21 22:23:11 +00:00
}
2021-01-08 08:14:13 +00:00
private void UpdateGameMetadata ( string titleId )
2020-01-21 22:23:11 +00:00
{
if ( _gameLoaded )
{
2021-01-08 08:14:13 +00:00
_applicationLibrary . LoadAndSaveMetaData ( titleId , appMetadata = >
2020-01-21 22:23:11 +00:00
{
DateTime lastPlayedDateTime = DateTime . Parse ( appMetadata . LastPlayed ) ;
double sessionTimePlayed = DateTime . UtcNow . Subtract ( lastPlayedDateTime ) . TotalSeconds ;
2019-09-02 16:03:57 +00:00
2020-01-21 22:23:11 +00:00
appMetadata . TimePlayed + = Math . Round ( sessionTimePlayed , MidpointRounding . AwayFromZero ) ;
} ) ;
2019-09-02 16:03:57 +00:00
}
}
2021-01-08 08:14:13 +00:00
public void UpdateGraphicsConfig ( )
2020-07-07 02:41:07 +00:00
{
2020-12-16 02:19:07 +00:00
int resScale = ConfigurationState . Instance . Graphics . ResScale ;
2020-07-07 02:41:07 +00:00
float resScaleCustom = ConfigurationState . Instance . Graphics . ResScaleCustom ;
2020-12-16 02:19:07 +00:00
Graphics . Gpu . GraphicsConfig . ResScale = ( resScale = = - 1 ) ? resScaleCustom : resScale ;
Graphics . Gpu . GraphicsConfig . MaxAnisotropy = ConfigurationState . Instance . Graphics . MaxAnisotropy ;
Graphics . Gpu . GraphicsConfig . ShadersDumpPath = ConfigurationState . Instance . Graphics . ShadersDumpPath ;
2020-11-12 23:15:34 +00:00
Graphics . Gpu . GraphicsConfig . EnableShaderCache = ConfigurationState . Instance . Graphics . EnableShaderCache ;
2020-07-07 02:41:07 +00:00
}
2021-01-08 08:14:13 +00:00
public void SaveConfig ( )
2020-05-03 02:00:53 +00:00
{
2020-05-03 10:08:21 +00:00
ConfigurationState . Instance . ToFileFormat ( ) . SaveConfig ( Program . ConfigurationPath ) ;
2020-05-03 02:00:53 +00:00
}
2021-01-08 08:14:13 +00:00
private void End ( )
2019-09-02 16:03:57 +00:00
{
2019-11-29 04:32:51 +00:00
if ( _ending )
{
return ;
}
_ending = true ;
2021-01-08 08:14:13 +00:00
if ( _emulationContext ! = null )
2019-09-02 16:03:57 +00:00
{
2021-01-08 08:14:13 +00:00
UpdateGameMetadata ( _emulationContext . Application . TitleIdText ) ;
2020-02-06 11:38:24 +00:00
2021-01-08 08:14:13 +00:00
if ( GlRendererWidget ! = null )
2020-02-06 11:38:24 +00:00
{
2021-01-08 08:14:13 +00:00
// We tell the widget that we are exiting.
GlRendererWidget . Exit ( ) ;
2020-02-13 17:43:29 +00:00
// Wait for the other thread to dispose the HLE context before exiting.
_deviceExitStatus . WaitOne ( ) ;
2021-01-19 02:31:59 +00:00
GlRendererWidget . Dispose ( ) ;
2020-02-06 11:38:24 +00:00
}
2019-09-02 16:03:57 +00:00
}
2020-01-21 22:23:11 +00:00
Dispose ( ) ;
2021-01-08 08:14:13 +00:00
Program . Exit ( ) ;
2020-01-21 22:23:11 +00:00
Application . Quit ( ) ;
}
2021-01-08 08:14:13 +00:00
//
// Events
//
2019-12-22 02:49:51 +00:00
private void Application_Added ( object sender , ApplicationAddedEventArgs args )
2019-11-29 04:32:51 +00:00
{
Application . Invoke ( delegate
{
_tableStore . AppendValues (
2019-12-22 02:49:51 +00:00
args . AppData . Favorite ,
new Gdk . Pixbuf ( args . AppData . Icon , 75 , 75 ) ,
$"{args.AppData.TitleName}\n{args.AppData.TitleId.ToUpper()}" ,
args . AppData . Developer ,
args . AppData . Version ,
args . AppData . TimePlayed ,
args . AppData . LastPlayed ,
args . AppData . FileExtension ,
args . AppData . FileSize ,
2020-03-25 17:09:38 +00:00
args . AppData . Path ,
args . AppData . ControlHolder ) ;
2020-01-31 18:21:46 +00:00
} ) ;
}
2019-12-22 02:49:51 +00:00
2020-01-31 18:21:46 +00:00
private void ApplicationCount_Updated ( object sender , ApplicationCountUpdatedEventArgs args )
{
Application . Invoke ( delegate
{
2019-12-22 02:49:51 +00:00
_progressLabel . Text = $"{args.NumAppsLoaded}/{args.NumAppsFound} Games Loaded" ;
2020-01-31 18:21:46 +00:00
float barValue = 0 ;
if ( args . NumAppsFound ! = 0 )
{
barValue = ( float ) args . NumAppsLoaded / args . NumAppsFound ;
}
2021-03-03 00:39:36 +00:00
_progressBar . Fraction = barValue ;
2020-06-26 10:30:16 +00:00
2021-01-08 08:14:13 +00:00
// Reset the vertical scrollbar to the top when titles finish loading
if ( args . NumAppsLoaded = = args . NumAppsFound )
2020-06-26 10:30:16 +00:00
{
_gameTableWindow . Vadjustment . Value = 0 ;
}
2019-11-29 04:32:51 +00:00
} ) ;
}
2020-03-07 02:40:06 +00:00
private void Update_StatusBar ( object sender , StatusUpdatedEventArgs args )
{
Application . Invoke ( delegate
{
2020-12-16 02:19:07 +00:00
_gameStatus . Text = args . GameStatus ;
_fifoStatus . Text = args . FifoStatus ;
_gpuName . Text = args . GpuName ;
_dockedMode . Text = args . DockedMode ;
_aspectRatio . Text = args . AspectRatio ;
2020-03-07 02:40:06 +00:00
if ( args . VSyncEnabled )
{
_vSyncStatus . Attributes = new Pango . AttrList ( ) ;
_vSyncStatus . Attributes . Insert ( new Pango . AttrForeground ( 11822 , 60138 , 51657 ) ) ;
}
else
{
_vSyncStatus . Attributes = new Pango . AttrList ( ) ;
_vSyncStatus . Attributes . Insert ( new Pango . AttrForeground ( ushort . MaxValue , 17733 , 21588 ) ) ;
}
} ) ;
}
2019-11-29 04:32:51 +00:00
private void FavToggle_Toggled ( object sender , ToggledArgs args )
{
_tableStore . GetIter ( out TreeIter treeIter , new TreePath ( args . Path ) ) ;
2021-01-08 08:14:13 +00:00
string titleId = _tableStore . GetValue ( treeIter , 2 ) . ToString ( ) . Split ( "\n" ) [ 1 ] . ToLower ( ) ;
bool newToggleValue = ! ( bool ) _tableStore . GetValue ( treeIter , 0 ) ;
2019-10-13 06:02:07 +00:00
2020-01-12 03:01:04 +00:00
_tableStore . SetValue ( treeIter , 0 , newToggleValue ) ;
2019-11-29 04:32:51 +00:00
2021-01-08 08:14:13 +00:00
_applicationLibrary . LoadAndSaveMetaData ( titleId , appMetadata = >
2019-11-29 04:32:51 +00:00
{
2020-01-12 03:01:04 +00:00
appMetadata . Favorite = newToggleValue ;
} ) ;
2019-11-29 04:32:51 +00:00
}
2020-06-26 10:30:16 +00:00
private void Column_Clicked ( object sender , EventArgs args )
{
TreeViewColumn column = ( TreeViewColumn ) sender ;
ConfigurationState . Instance . Ui . ColumnSort . SortColumnId . Value = column . SortColumnId ;
ConfigurationState . Instance . Ui . ColumnSort . SortAscending . Value = column . SortOrder = = SortType . Ascending ;
SaveConfig ( ) ;
}
2019-11-29 04:32:51 +00:00
private void Row_Activated ( object sender , RowActivatedArgs args )
2019-09-02 16:03:57 +00:00
{
2019-12-22 02:49:51 +00:00
_gameTableSelection . GetSelected ( out TreeIter treeIter ) ;
2021-01-08 08:14:13 +00:00
2019-11-29 04:32:51 +00:00
string path = ( string ) _tableStore . GetValue ( treeIter , 9 ) ;
2019-09-02 16:03:57 +00:00
LoadApplication ( path ) ;
}
2020-11-19 00:34:28 +00:00
private void VSyncStatus_Clicked ( object sender , ButtonReleaseEventArgs args )
{
_emulationContext . EnableDeviceVsync = ! _emulationContext . EnableDeviceVsync ;
2021-03-18 23:09:33 +00:00
Logger . Info ? . Print ( LogClass . Application , $"VSync toggled to: {_emulationContext.EnableDeviceVsync}" ) ;
2020-11-19 00:34:28 +00:00
}
private void DockedMode_Clicked ( object sender , ButtonReleaseEventArgs args )
{
ConfigurationState . Instance . System . EnableDockedMode . Value = ! ConfigurationState . Instance . System . EnableDockedMode . Value ;
}
2020-12-16 02:19:07 +00:00
private void AspectRatio_Clicked ( object sender , ButtonReleaseEventArgs args )
{
AspectRatio aspectRatio = ConfigurationState . Instance . Graphics . AspectRatio . Value ;
ConfigurationState . Instance . Graphics . AspectRatio . Value = ( ( int ) aspectRatio + 1 ) > Enum . GetNames ( typeof ( AspectRatio ) ) . Length - 1 ? AspectRatio . Fixed4x3 : aspectRatio + 1 ;
}
2019-12-22 02:49:51 +00:00
private void Row_Clicked ( object sender , ButtonReleaseEventArgs args )
{
2021-01-08 08:14:13 +00:00
if ( args . Event . Button ! = 3 /* Right Click */ )
{
return ;
}
2019-12-22 02:49:51 +00:00
_gameTableSelection . GetSelected ( out TreeIter treeIter ) ;
2021-01-08 08:14:13 +00:00
if ( treeIter . UserData = = IntPtr . Zero )
{
return ;
}
string titleFilePath = _tableStore . GetValue ( treeIter , 9 ) . ToString ( ) ;
string titleName = _tableStore . GetValue ( treeIter , 2 ) . ToString ( ) . Split ( "\n" ) [ 0 ] ;
string titleId = _tableStore . GetValue ( treeIter , 2 ) . ToString ( ) . Split ( "\n" ) [ 1 ] . ToLower ( ) ;
2019-12-22 02:49:51 +00:00
2020-03-25 17:09:38 +00:00
BlitStruct < ApplicationControlProperty > controlData = ( BlitStruct < ApplicationControlProperty > ) _tableStore . GetValue ( treeIter , 10 ) ;
2021-01-08 08:14:13 +00:00
_ = new GameTableContextMenu ( this , _virtualFileSystem , titleFilePath , titleName , titleId , controlData ) ;
2019-12-22 02:49:51 +00:00
}
2019-11-29 04:32:51 +00:00
private void Load_Application_File ( object sender , EventArgs args )
2019-09-02 16:03:57 +00:00
{
2021-01-08 08:14:13 +00:00
using ( FileChooserDialog fileChooser = new FileChooserDialog ( "Choose the file to open" , this , FileChooserAction . Open , "Cancel" , ResponseType . Cancel , "Open" , ResponseType . Accept ) )
2019-09-02 16:03:57 +00:00
{
2021-01-08 08:14:13 +00:00
fileChooser . Filter = new FileFilter ( ) ;
fileChooser . Filter . AddPattern ( "*.nsp" ) ;
fileChooser . Filter . AddPattern ( "*.pfs0" ) ;
fileChooser . Filter . AddPattern ( "*.xci" ) ;
fileChooser . Filter . AddPattern ( "*.nca" ) ;
fileChooser . Filter . AddPattern ( "*.nro" ) ;
fileChooser . Filter . AddPattern ( "*.nso" ) ;
if ( fileChooser . Run ( ) = = ( int ) ResponseType . Accept )
{
LoadApplication ( fileChooser . Filename ) ;
}
2019-09-02 16:03:57 +00:00
}
}
2019-11-29 04:32:51 +00:00
private void Load_Application_Folder ( object sender , EventArgs args )
2019-09-02 16:03:57 +00:00
{
2021-01-08 08:14:13 +00:00
using ( FileChooserDialog fileChooser = new FileChooserDialog ( "Choose the folder to open" , this , FileChooserAction . SelectFolder , "Cancel" , ResponseType . Cancel , "Open" , ResponseType . Accept ) )
2019-09-02 16:03:57 +00:00
{
2021-01-08 08:14:13 +00:00
if ( fileChooser . Run ( ) = = ( int ) ResponseType . Accept )
{
LoadApplication ( fileChooser . Filename ) ;
}
2019-09-02 16:03:57 +00:00
}
}
2019-11-29 04:32:51 +00:00
private void Open_Ryu_Folder ( object sender , EventArgs args )
2019-09-02 16:03:57 +00:00
{
2021-01-08 08:14:13 +00:00
OpenHelper . OpenFolder ( AppDataManager . BaseDirPath ) ;
2019-09-02 16:03:57 +00:00
}
2020-09-20 04:31:05 +00:00
private void OpenLogsFolder_Pressed ( object sender , EventArgs args )
{
string logPath = System . IO . Path . Combine ( AppDomain . CurrentDomain . BaseDirectory , "Logs" ) ;
2021-01-08 08:14:13 +00:00
new DirectoryInfo ( logPath ) . Create ( ) ;
2020-09-20 04:31:05 +00:00
2021-01-08 08:14:13 +00:00
OpenHelper . OpenFolder ( logPath ) ;
2020-09-20 04:31:05 +00:00
}
2019-11-29 04:32:51 +00:00
private void Exit_Pressed ( object sender , EventArgs args )
2019-09-02 16:03:57 +00:00
{
2021-01-14 22:30:52 +00:00
if ( ! _gameLoaded | | ! ConfigurationState . Instance . ShowConfirmExit | | GtkDialog . CreateExitDialog ( ) )
2020-10-09 23:06:48 +00:00
{
2021-01-08 08:14:13 +00:00
End ( ) ;
2020-10-09 23:06:48 +00:00
}
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
private void Window_Close ( object sender , DeleteEventArgs args )
2019-09-02 16:03:57 +00:00
{
2021-01-14 22:30:52 +00:00
if ( ! _gameLoaded | | ! ConfigurationState . Instance . ShowConfirmExit | | GtkDialog . CreateExitDialog ( ) )
2020-10-09 23:06:48 +00:00
{
2021-01-08 08:14:13 +00:00
End ( ) ;
2020-10-09 23:06:48 +00:00
}
else
{
args . RetVal = true ;
}
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
private void StopEmulation_Pressed ( object sender , EventArgs args )
2019-09-02 16:03:57 +00:00
{
2021-01-08 08:14:13 +00:00
GlRendererWidget ? . Exit ( ) ;
2019-09-02 16:03:57 +00:00
}
2020-01-12 02:10:55 +00:00
private void Installer_File_Pressed ( object o , EventArgs args )
{
2021-01-08 08:14:13 +00:00
FileChooserDialog fileChooser = new FileChooserDialog ( "Choose the firmware file to open" , this , FileChooserAction . Open , "Cancel" , ResponseType . Cancel , "Open" , ResponseType . Accept ) ;
2020-01-12 02:10:55 +00:00
fileChooser . Filter = new FileFilter ( ) ;
fileChooser . Filter . AddPattern ( "*.zip" ) ;
fileChooser . Filter . AddPattern ( "*.xci" ) ;
HandleInstallerDialog ( fileChooser ) ;
}
private void Installer_Directory_Pressed ( object o , EventArgs args )
{
2021-01-08 08:14:13 +00:00
FileChooserDialog directoryChooser = new FileChooserDialog ( "Choose the firmware directory to open" , this , FileChooserAction . SelectFolder , "Cancel" , ResponseType . Cancel , "Open" , ResponseType . Accept ) ;
2020-01-12 02:10:55 +00:00
HandleInstallerDialog ( directoryChooser ) ;
}
private void HandleInstallerDialog ( FileChooserDialog fileChooser )
{
if ( fileChooser . Run ( ) = = ( int ) ResponseType . Accept )
{
try
{
string filename = fileChooser . Filename ;
fileChooser . Dispose ( ) ;
2020-05-03 02:00:53 +00:00
SystemVersion firmwareVersion = _contentManager . VerifyFirmwarePackage ( filename ) ;
2020-01-12 02:10:55 +00:00
2021-01-08 08:14:13 +00:00
string dialogTitle = $"Install Firmware {firmwareVersion.VersionString}" ;
2020-01-12 02:10:55 +00:00
if ( firmwareVersion = = null )
{
2021-01-08 08:14:13 +00:00
GtkDialog . CreateErrorDialog ( $"A valid system firmware was not found in {filename}." ) ;
2020-01-12 02:10:55 +00:00
return ;
}
2020-05-03 02:00:53 +00:00
SystemVersion currentVersion = _contentManager . GetCurrentFirmwareVersion ( ) ;
2020-01-12 02:10:55 +00:00
string dialogMessage = $"System version {firmwareVersion.VersionString} will be installed." ;
if ( currentVersion ! = null )
{
2021-01-08 08:14:13 +00:00
dialogMessage + = $"\n\nThis will replace the current system version {currentVersion.VersionString}. " ;
2020-01-12 02:10:55 +00:00
}
2021-01-08 08:14:13 +00:00
dialogMessage + = "\n\nDo you want to continue?" ;
2020-01-12 02:10:55 +00:00
2021-01-08 08:14:13 +00:00
ResponseType responseInstallDialog = ( ResponseType ) GtkDialog . CreateConfirmationDialog ( dialogTitle , dialogMessage ) . Run ( ) ;
2020-01-12 02:10:55 +00:00
2021-01-08 08:14:13 +00:00
MessageDialog waitingDialog = GtkDialog . CreateWaitingDialog ( dialogTitle , "Installing firmware..." ) ;
2020-01-12 02:10:55 +00:00
2021-01-08 08:14:13 +00:00
if ( responseInstallDialog = = ResponseType . Yes )
2020-01-12 02:10:55 +00:00
{
2020-08-03 23:32:53 +00:00
Logger . Info ? . Print ( LogClass . Application , $"Installing firmware {firmwareVersion.VersionString}" ) ;
2021-04-04 12:06:59 +00:00
2020-01-12 02:10:55 +00:00
Thread thread = new Thread ( ( ) = >
{
2021-01-08 08:14:13 +00:00
Application . Invoke ( delegate
2020-01-12 02:10:55 +00:00
{
2021-01-08 08:14:13 +00:00
waitingDialog . Run ( ) ;
} ) ;
2020-01-12 02:10:55 +00:00
try
{
2020-01-21 22:23:11 +00:00
_contentManager . InstallFirmware ( filename ) ;
2020-01-12 02:10:55 +00:00
2021-01-08 08:14:13 +00:00
Application . Invoke ( delegate
2020-01-12 02:10:55 +00:00
{
2021-01-08 08:14:13 +00:00
waitingDialog . Dispose ( ) ;
2020-01-12 02:10:55 +00:00
2021-01-08 08:14:13 +00:00
string message = $"System version {firmwareVersion.VersionString} successfully installed." ;
2020-01-12 02:10:55 +00:00
2021-01-08 08:14:13 +00:00
GtkDialog . CreateInfoDialog ( dialogTitle , message ) ;
Logger . Info ? . Print ( LogClass . Application , message ) ;
} ) ;
2020-01-12 02:10:55 +00:00
}
catch ( Exception ex )
{
2021-01-08 08:14:13 +00:00
Application . Invoke ( delegate
2020-01-12 02:10:55 +00:00
{
2021-01-08 08:14:13 +00:00
waitingDialog . Dispose ( ) ;
2020-01-12 02:10:55 +00:00
2021-01-08 08:14:13 +00:00
GtkDialog . CreateErrorDialog ( ex . Message ) ;
} ) ;
2020-01-12 02:10:55 +00:00
}
finally
{
RefreshFirmwareLabel ( ) ;
}
} ) ;
2020-01-13 00:21:54 +00:00
thread . Name = "GUI.FirmwareInstallerThread" ;
2020-01-12 02:10:55 +00:00
thread . Start ( ) ;
}
}
2021-01-26 17:45:07 +00:00
catch ( LibHac . MissingKeyException ex )
{
Logger . Error ? . Print ( LogClass . Application , ex . ToString ( ) ) ;
UserErrorDialog . CreateUserErrorDialog ( UserError . NoKeys ) ;
}
2020-01-12 02:10:55 +00:00
catch ( Exception ex )
{
2021-01-08 08:14:13 +00:00
GtkDialog . CreateErrorDialog ( ex . Message ) ;
}
}
else
{
fileChooser . Dispose ( ) ;
}
}
2020-01-12 02:10:55 +00:00
2021-01-08 08:14:13 +00:00
private void RefreshFirmwareLabel ( )
{
SystemVersion currentFirmware = _contentManager . GetCurrentFirmwareVersion ( ) ;
2020-01-12 02:10:55 +00:00
2021-01-08 08:14:13 +00:00
Application . Invoke ( delegate
{
_firmwareVersionLabel . Text = currentFirmware ! = null ? currentFirmware . VersionString : "0.0.0" ;
} ) ;
}
2020-01-12 02:10:55 +00:00
2021-01-08 08:14:13 +00:00
private void HandleRelaunch ( )
{
if ( _userChannelPersistence . PreviousIndex ! = - 1 & & _userChannelPersistence . ShouldRestart )
{
_userChannelPersistence . ShouldRestart = false ;
2020-01-12 02:10:55 +00:00
2021-01-08 08:14:13 +00:00
LoadApplication ( _currentEmulatedGamePath ) ;
2020-01-12 02:10:55 +00:00
}
else
{
2021-01-08 08:14:13 +00:00
// otherwise, clear state.
_userChannelPersistence = new UserChannelPersistence ( ) ;
_currentEmulatedGamePath = null ;
2020-01-12 02:10:55 +00:00
}
}
2020-12-01 22:02:27 +00:00
private void FullScreen_Toggled ( object sender , EventArgs args )
2019-09-02 16:03:57 +00:00
{
2021-01-08 08:14:13 +00:00
if ( ! Window . State . HasFlag ( Gdk . WindowState . Fullscreen ) )
2019-09-02 16:03:57 +00:00
{
Fullscreen ( ) ;
2020-02-12 00:56:19 +00:00
ToggleExtraWidgets ( false ) ;
2019-09-02 16:03:57 +00:00
}
else
{
Unfullscreen ( ) ;
2020-02-12 00:56:19 +00:00
ToggleExtraWidgets ( true ) ;
2019-09-02 16:03:57 +00:00
}
}
2020-12-01 22:02:27 +00:00
private void StartFullScreen_Toggled ( object sender , EventArgs args )
{
ConfigurationState . Instance . Ui . StartFullscreen . Value = _startFullScreen . Active ;
SaveConfig ( ) ;
}
2019-11-29 04:32:51 +00:00
private void Settings_Pressed ( object sender , EventArgs args )
2019-09-02 16:03:57 +00:00
{
2021-02-20 00:34:41 +00:00
SettingsWindow settingsWindow = new SettingsWindow ( this , _virtualFileSystem , _contentManager ) ;
settingsWindow . SetSizeRequest ( ( int ) ( settingsWindow . DefaultWidth * Program . WindowScaleFactor ) , ( int ) ( settingsWindow . DefaultHeight * Program . WindowScaleFactor ) ) ;
settingsWindow . Show ( ) ;
2019-09-02 16:03:57 +00:00
}
2020-12-16 00:41:42 +00:00
private void Simulate_WakeUp_Message_Pressed ( object sender , EventArgs args )
{
2021-01-08 08:14:13 +00:00
if ( _emulationContext ! = null )
{
_emulationContext . System . SimulateWakeUpMessage ( ) ;
}
2020-12-16 00:41:42 +00:00
}
2021-03-18 20:40:20 +00:00
private void ActionMenu_StateChanged ( object o , StateChangedArgs args )
{
_scanAmiibo . Sensitive = _emulationContext ! = null & & _emulationContext . System . SearchingForAmiibo ( out int _ ) ;
}
private void Scan_Amiibo ( object sender , EventArgs args )
{
if ( _emulationContext . System . SearchingForAmiibo ( out int deviceId ) )
{
AmiiboWindow amiiboWindow = new AmiiboWindow
{
LastScannedAmiiboShowAll = _lastScannedAmiiboShowAll ,
LastScannedAmiiboId = _lastScannedAmiiboId ,
DeviceId = deviceId ,
TitleId = _emulationContext . Application . TitleIdText . ToUpper ( )
} ;
amiiboWindow . DeleteEvent + = AmiiboWindow_DeleteEvent ;
amiiboWindow . Show ( ) ;
}
else
{
GtkDialog . CreateInfoDialog ( $"Amiibo" , "The game is currently not ready to receive Amiibo scan data. Ensure that you have an Amiibo-compatible game open and ready to receive Amiibo scan data." ) ;
}
}
private void AmiiboWindow_DeleteEvent ( object sender , DeleteEventArgs args )
{
if ( ( ( AmiiboWindow ) sender ) . AmiiboId ! = "" & & ( ( AmiiboWindow ) sender ) . Response = = ResponseType . Ok )
{
_lastScannedAmiiboId = ( ( AmiiboWindow ) sender ) . AmiiboId ;
_lastScannedAmiiboShowAll = ( ( AmiiboWindow ) sender ) . LastScannedAmiiboShowAll ;
_emulationContext . System . ScanAmiibo ( ( ( AmiiboWindow ) sender ) . DeviceId , ( ( AmiiboWindow ) sender ) . AmiiboId , ( ( AmiiboWindow ) sender ) . UseRandomUuid ) ;
}
}
2019-11-29 04:32:51 +00:00
private void Update_Pressed ( object sender , EventArgs args )
2019-09-02 16:03:57 +00:00
{
2020-09-29 20:05:25 +00:00
if ( Updater . CanUpdate ( true ) )
2019-09-02 16:03:57 +00:00
{
2021-02-23 15:19:02 +00:00
Updater . BeginParse ( this , true ) . ContinueWith ( task = >
{
2021-03-18 22:44:39 +00:00
Logger . Error ? . Print ( LogClass . Application , $"Updater error: {task.Exception}" ) ;
2021-02-23 15:19:02 +00:00
} , TaskContinuationOptions . OnlyOnFaulted ) ;
2019-09-02 16:03:57 +00:00
}
}
2019-11-29 04:32:51 +00:00
private void About_Pressed ( object sender , EventArgs args )
2019-09-02 16:03:57 +00:00
{
2021-02-20 00:34:41 +00:00
AboutWindow aboutWindow = new AboutWindow ( ) ;
aboutWindow . SetSizeRequest ( ( int ) ( aboutWindow . DefaultWidth * Program . WindowScaleFactor ) , ( int ) ( aboutWindow . DefaultHeight * Program . WindowScaleFactor ) ) ;
aboutWindow . Show ( ) ;
2019-11-29 04:32:51 +00:00
}
private void Fav_Toggled ( object sender , EventArgs args )
{
2019-12-21 19:52:31 +00:00
ConfigurationState . Instance . Ui . GuiColumns . FavColumn . Value = _favToggle . Active ;
2019-11-29 04:32:51 +00:00
2019-12-21 19:52:31 +00:00
SaveConfig ( ) ;
2019-11-29 04:32:51 +00:00
UpdateColumns ( ) ;
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
private void Icon_Toggled ( object sender , EventArgs args )
2019-09-02 16:03:57 +00:00
{
2019-12-21 19:52:31 +00:00
ConfigurationState . Instance . Ui . GuiColumns . IconColumn . Value = _iconToggle . Active ;
2019-11-29 04:32:51 +00:00
2019-12-21 19:52:31 +00:00
SaveConfig ( ) ;
2019-11-29 04:32:51 +00:00
UpdateColumns ( ) ;
2019-09-02 16:03:57 +00:00
}
2021-02-09 09:24:37 +00:00
private void App_Toggled ( object sender , EventArgs args )
2019-09-02 16:03:57 +00:00
{
2019-12-21 19:52:31 +00:00
ConfigurationState . Instance . Ui . GuiColumns . AppColumn . Value = _appToggle . Active ;
2019-11-29 04:32:51 +00:00
2019-12-21 19:52:31 +00:00
SaveConfig ( ) ;
2019-11-29 04:32:51 +00:00
UpdateColumns ( ) ;
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
private void Developer_Toggled ( object sender , EventArgs args )
2019-09-02 16:03:57 +00:00
{
2019-12-21 19:52:31 +00:00
ConfigurationState . Instance . Ui . GuiColumns . DevColumn . Value = _developerToggle . Active ;
2019-11-29 04:32:51 +00:00
2019-12-21 19:52:31 +00:00
SaveConfig ( ) ;
2019-11-29 04:32:51 +00:00
UpdateColumns ( ) ;
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
private void Version_Toggled ( object sender , EventArgs args )
2019-09-02 16:03:57 +00:00
{
2019-12-21 19:52:31 +00:00
ConfigurationState . Instance . Ui . GuiColumns . VersionColumn . Value = _versionToggle . Active ;
2019-11-29 04:32:51 +00:00
2019-12-21 19:52:31 +00:00
SaveConfig ( ) ;
2019-11-29 04:32:51 +00:00
UpdateColumns ( ) ;
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
private void TimePlayed_Toggled ( object sender , EventArgs args )
2019-09-02 16:03:57 +00:00
{
2019-12-21 19:52:31 +00:00
ConfigurationState . Instance . Ui . GuiColumns . TimePlayedColumn . Value = _timePlayedToggle . Active ;
2019-11-29 04:32:51 +00:00
2019-12-21 19:52:31 +00:00
SaveConfig ( ) ;
2019-11-29 04:32:51 +00:00
UpdateColumns ( ) ;
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
private void LastPlayed_Toggled ( object sender , EventArgs args )
2019-09-02 16:03:57 +00:00
{
2019-12-21 19:52:31 +00:00
ConfigurationState . Instance . Ui . GuiColumns . LastPlayedColumn . Value = _lastPlayedToggle . Active ;
2019-11-29 04:32:51 +00:00
2019-12-21 19:52:31 +00:00
SaveConfig ( ) ;
2019-11-29 04:32:51 +00:00
UpdateColumns ( ) ;
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
private void FileExt_Toggled ( object sender , EventArgs args )
2019-09-02 16:03:57 +00:00
{
2019-12-21 19:52:31 +00:00
ConfigurationState . Instance . Ui . GuiColumns . FileExtColumn . Value = _fileExtToggle . Active ;
2019-11-29 04:32:51 +00:00
2019-12-21 19:52:31 +00:00
SaveConfig ( ) ;
2019-11-29 04:32:51 +00:00
UpdateColumns ( ) ;
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
private void FileSize_Toggled ( object sender , EventArgs args )
2019-09-02 16:03:57 +00:00
{
2019-12-21 19:52:31 +00:00
ConfigurationState . Instance . Ui . GuiColumns . FileSizeColumn . Value = _fileSizeToggle . Active ;
2019-11-29 04:32:51 +00:00
2019-12-21 19:52:31 +00:00
SaveConfig ( ) ;
2019-11-29 04:32:51 +00:00
UpdateColumns ( ) ;
2019-09-02 16:03:57 +00:00
}
2019-11-29 04:32:51 +00:00
private void Path_Toggled ( object sender , EventArgs args )
2019-09-02 16:03:57 +00:00
{
2019-12-21 19:52:31 +00:00
ConfigurationState . Instance . Ui . GuiColumns . PathColumn . Value = _pathToggle . Active ;
2019-11-29 04:32:51 +00:00
2019-12-21 19:52:31 +00:00
SaveConfig ( ) ;
2019-11-29 04:32:51 +00:00
UpdateColumns ( ) ;
}
private void RefreshList_Pressed ( object sender , ButtonReleaseEventArgs args )
{
UpdateGameTable ( ) ;
}
2019-09-02 16:03:57 +00:00
}
2021-01-19 02:31:59 +00:00
}