diff --git a/.travis.yml b/.travis.yml
index 41a78d345..e87700b5b 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -28,6 +28,12 @@ matrix:
install: "./.travis/macos/deps.sh"
script: "./.travis/macos/build.sh"
after_success: "./.travis/macos/upload.sh"
+ - os: linux
+ env: NAME="linux build (frozen versions of dependencies)"
+ sudo: required
+ dist: trusty
+ services: docker
+ script: "./.travis/linux-frozen/build.sh"
deploy:
provider: releases
diff --git a/.travis/linux-frozen/build.sh b/.travis/linux-frozen/build.sh
new file mode 100755
index 000000000..25c655516
--- /dev/null
+++ b/.travis/linux-frozen/build.sh
@@ -0,0 +1,4 @@
+#!/bin/bash -ex
+
+docker pull ubuntu:16.04
+docker run -v $(pwd):/citra ubuntu:16.04 /bin/bash -ex /citra/.travis/linux-frozen/docker.sh
diff --git a/.travis/linux-frozen/docker.sh b/.travis/linux-frozen/docker.sh
new file mode 100755
index 000000000..edca9a7a1
--- /dev/null
+++ b/.travis/linux-frozen/docker.sh
@@ -0,0 +1,27 @@
+#!/bin/bash -ex
+
+cd /citra
+
+apt-get update
+apt-get install -y build-essential wget git python-launchpadlib libssl-dev
+
+# Install specific versions of packages with their dependencies
+# The apt repositories remove older versions regularly, so we can't use
+# apt-get and have to pull the packages directly from the archives.
+/citra/.travis/linux-frozen/install_package.py \
+ libsdl2-dev 2.0.4+dfsg1-2ubuntu2 xenial \
+ qtbase5-dev 5.2.1+dfsg-1ubuntu14.3 trusty \
+ libqt5opengl5-dev 5.2.1+dfsg-1ubuntu14.3 trusty \
+ libcurl4-openssl-dev 7.47.0-1ubuntu2.3 xenial \
+ libicu52 52.1-3ubuntu0.6 trusty
+
+# Get a recent version of CMake
+wget https://cmake.org/files/v3.9/cmake-3.9.0-Linux-x86_64.sh
+echo y | sh cmake-3.9.0-Linux-x86_64.sh --prefix=cmake
+export PATH=/citra/cmake/cmake-3.9.0-Linux-x86_64/bin:$PATH
+
+mkdir build && cd build
+cmake .. -DUSE_SYSTEM_CURL=ON -DCMAKE_BUILD_TYPE=Release
+make -j4
+
+ctest -VV -C Release
diff --git a/.travis/linux-frozen/install_package.py b/.travis/linux-frozen/install_package.py
new file mode 100755
index 000000000..bdf333716
--- /dev/null
+++ b/.travis/linux-frozen/install_package.py
@@ -0,0 +1,38 @@
+#!/usr/bin/python
+
+import sys, re, subprocess
+from launchpadlib.launchpad import Launchpad
+
+cachedir = '/.launchpadlib/cache/'
+launchpad = Launchpad.login_anonymously('grab build info', 'production', cachedir, version='devel')
+
+processed_packages = []
+deb_file_list = []
+
+def get_url(pkg, distro):
+ build_link = launchpad.archives.getByReference(reference='ubuntu').getPublishedBinaries(binary_name=pkg[0], distro_arch_series='https://api.launchpad.net/devel/ubuntu/'+distro+'/amd64', version=pkg[1], exact_match=True, order_by_date=True).entries[0]['build_link']
+ deb_name = pkg[0] + '_' + pkg[1] + '_amd64.deb'
+ deb_link = build_link + '/+files/' + deb_name
+ return [deb_link, deb_name]
+
+def list_dependencies(deb_file):
+ t=subprocess.check_output(['bash', '-c', 'dpkg -I ' + deb_file + ' | grep -oP "^ Depends\: \K.*$"'])
+ deps=[i.strip() for i in t.split(',')]
+ equals_re = re.compile(r'^(.*) \(= (.*)\)$')
+ return [equals_re.sub(r'\1=\2', i).split('=') for i in filter(equals_re.match, deps)]
+
+def get_package(pkg, distro):
+ if pkg in processed_packages:
+ return
+ print 'Getting ' + pkg[0] + '...'
+ url = get_url(pkg, distro)
+ subprocess.check_call(['wget', '--quiet', url[0], '-O', url[1]])
+ for dep in list_dependencies(url[1]):
+ get_package(dep, distro)
+ processed_packages.append(pkg)
+ deb_file_list.append('./' + url[1])
+
+for i in xrange(1, len(sys.argv), 3):
+ get_package([sys.argv[i], sys.argv[i + 1]], sys.argv[i + 2])
+
+subprocess.check_call(['apt-get', 'install', '-y'] + deb_file_list)
diff --git a/dist/icons/citra.png b/dist/icons/citra.png
new file mode 100644
index 000000000..83fa499dd
Binary files /dev/null and b/dist/icons/citra.png differ
diff --git a/dist/icons/icons.qrc b/dist/icons/icons.qrc
index f0c44862f..a25804907 100644
--- a/dist/icons/icons.qrc
+++ b/dist/icons/icons.qrc
@@ -2,5 +2,6 @@
checked.png
failed.png
+ citra.png
diff --git a/externals/dynarmic b/externals/dynarmic
index 8f15e3f70..d3fb60328 160000
--- a/externals/dynarmic
+++ b/externals/dynarmic
@@ -1 +1 @@
-Subproject commit 8f15e3f70cb96e56705e5de6ba97b5d09423a56b
+Subproject commit d3fb603287dd4b0b06b921b639ef79dc9a850cdc
diff --git a/externals/xbyak b/externals/xbyak
index fe4765d2f..c5da3778e 160000
--- a/externals/xbyak
+++ b/externals/xbyak
@@ -1 +1 @@
-Subproject commit fe4765d2fed4e990ea5e9661b6bc5fc9bf48ec16
+Subproject commit c5da3778e7f84013fe8c26fcf18a67881bd1e825
diff --git a/src/citra_qt/CMakeLists.txt b/src/citra_qt/CMakeLists.txt
index add7566c2..e4929a0b4 100644
--- a/src/citra_qt/CMakeLists.txt
+++ b/src/citra_qt/CMakeLists.txt
@@ -23,8 +23,10 @@ set(SRCS
debugger/profiler.cpp
debugger/registers.cpp
debugger/wait_tree.cpp
+ updater/updater.cpp
util/spinbox.cpp
util/util.cpp
+ aboutdialog.cpp
bootmanager.cpp
game_list.cpp
hotkeys.cpp
@@ -55,8 +57,11 @@ set(HEADERS
debugger/profiler.h
debugger/registers.h
debugger/wait_tree.h
+ updater/updater.h
+ updater/updater_p.h
util/spinbox.h
util/util.h
+ aboutdialog.h
bootmanager.h
game_list.h
game_list_p.h
@@ -75,6 +80,7 @@ set(UIS
configuration/configure_system.ui
configuration/configure_web.ui
debugger/registers.ui
+ aboutdialog.ui
hotkeys.ui
main.ui
)
diff --git a/src/citra_qt/aboutdialog.cpp b/src/citra_qt/aboutdialog.cpp
new file mode 100644
index 000000000..9c11168ba
--- /dev/null
+++ b/src/citra_qt/aboutdialog.cpp
@@ -0,0 +1,19 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "aboutdialog.h"
+#include "common/scm_rev.h"
+#include "ui_aboutdialog.h"
+
+AboutDialog::AboutDialog(QWidget* parent)
+ : QDialog(parent, Qt::WindowTitleHint | Qt::WindowCloseButtonHint | Qt::WindowSystemMenuHint),
+ ui(new Ui::AboutDialog) {
+ ui->setupUi(this);
+ ui->labelBuildInfo->setText(ui->labelBuildInfo->text().arg(
+ Common::g_build_name, Common::g_scm_branch, Common::g_scm_desc));
+}
+
+AboutDialog::~AboutDialog() {
+ delete ui;
+}
diff --git a/src/citra_qt/aboutdialog.h b/src/citra_qt/aboutdialog.h
new file mode 100644
index 000000000..606789774
--- /dev/null
+++ b/src/citra_qt/aboutdialog.h
@@ -0,0 +1,25 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#ifndef ABOUTDIALOG_H
+#define ABOUTDIALOG_H
+
+#include
+
+namespace Ui {
+class AboutDialog;
+}
+
+class AboutDialog : public QDialog {
+ Q_OBJECT
+
+public:
+ explicit AboutDialog(QWidget* parent = 0);
+ ~AboutDialog();
+
+private:
+ Ui::AboutDialog* ui;
+};
+
+#endif // ABOUTDIALOG_H
diff --git a/src/citra_qt/aboutdialog.ui b/src/citra_qt/aboutdialog.ui
new file mode 100644
index 000000000..32f0a0356
--- /dev/null
+++ b/src/citra_qt/aboutdialog.ui
@@ -0,0 +1,156 @@
+
+
+ AboutDialog
+
+
+
+ 0
+ 0
+ 752
+ 300
+
+
+
+ About Citra
+
+
+
+
+ 630
+ 250
+ 101
+ 32
+
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Ok
+
+
+
+
+
+ 20
+ 20
+ 241
+ 251
+
+
+
+ <html><head/><body><p><img src=":/icons/citra.png"/></p></body></html>
+
+
+
+
+
+ 270
+ 10
+ 100
+ 50
+
+
+
+ <html><head/><body><p><span style=" font-size:28pt;">Citra</span></p></body></html>
+
+
+
+
+
+ 270
+ 70
+ 451
+ 21
+
+
+
+ <html><head/><body><p>%1 | %2-%3</p></body></html>
+
+
+
+
+
+ 270
+ 90
+ 461
+ 131
+
+
+
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+<html><head><meta name="qrichtext" content="1" /><style type="text/css">
+p, li { white-space: pre-wrap; }
+</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8pt; font-weight:400; font-style:normal;">
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:12pt;">Citra is a free and open source 3DS emulator </span></p>
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:12pt;">licensed under GPLv2.0 or any later version.</span></p>
+<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p>
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:12pt;">This software should not be used to play games </span></p>
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:12pt;">you have not legally obtained.</span></p></body></html>
+
+
+
+
+
+ 270
+ 230
+ 361
+ 16
+
+
+
+ <html><head/><body><p><a href="https://citra-emu.org/"><span style=" text-decoration: underline; color:#0000ff;">Website</span></a> | <a href="https://community.citra-emu.org/"><span style=" text-decoration: underline; color:#0000ff;">Forum</span></a> | <a href="https://github.com/citra-emu"><span style=" text-decoration: underline; color:#0000ff;">Source Code</span></a> | <a href="//github.com/citra-emu/citra/graphs/contributors"><span style=" text-decoration: underline; color:#0000ff;">Contributors</span></a> | <a href="https://github.com/citra-emu/citra/blob/master/license.txt"><span style=" text-decoration: underline; color:#0000ff;">License</span></a></p></body></html>
+
+
+
+
+
+ 150
+ 270
+ 450
+ 16
+
+
+
+ <html><head/><body><p><span style=" font-size:7pt;">"3DS" is a trademark of Nintendo. Citra is not affiliated with Nintendo in any way.</span></p></body></html>
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ AboutDialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ AboutDialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/src/citra_qt/configuration/config.cpp b/src/citra_qt/configuration/config.cpp
index 601c8b0e3..97993e426 100644
--- a/src/citra_qt/configuration/config.cpp
+++ b/src/citra_qt/configuration/config.cpp
@@ -157,6 +157,12 @@ void Config::ReadValues() {
qt_config->beginGroup("UI");
UISettings::values.theme = qt_config->value("theme", UISettings::themes[0].second).toString();
+ qt_config->beginGroup("Updater");
+ UISettings::values.check_for_update_on_start =
+ qt_config->value("check_for_update_on_start", true).toBool();
+ UISettings::values.update_on_close = qt_config->value("update_on_close", false).toBool();
+ qt_config->endGroup();
+
qt_config->beginGroup("UILayout");
UISettings::values.geometry = qt_config->value("geometry").toByteArray();
UISettings::values.state = qt_config->value("state").toByteArray();
@@ -307,6 +313,11 @@ void Config::SaveValues() {
qt_config->beginGroup("UI");
qt_config->setValue("theme", UISettings::values.theme);
+ qt_config->beginGroup("Updater");
+ qt_config->setValue("check_for_update_on_start", UISettings::values.check_for_update_on_start);
+ qt_config->setValue("update_on_close", UISettings::values.update_on_close);
+ qt_config->endGroup();
+
qt_config->beginGroup("UILayout");
qt_config->setValue("geometry", UISettings::values.geometry);
qt_config->setValue("state", UISettings::values.state);
diff --git a/src/citra_qt/configuration/configure_general.cpp b/src/citra_qt/configuration/configure_general.cpp
index 939379717..f8f6c305d 100644
--- a/src/citra_qt/configuration/configure_general.cpp
+++ b/src/citra_qt/configuration/configure_general.cpp
@@ -20,6 +20,7 @@ ConfigureGeneral::ConfigureGeneral(QWidget* parent)
this->setConfiguration();
ui->toggle_cpu_jit->setEnabled(!Core::System::GetInstance().IsPoweredOn());
+ ui->updateBox->setVisible(UISettings::values.updater_found);
}
ConfigureGeneral::~ConfigureGeneral() {}
@@ -29,6 +30,9 @@ void ConfigureGeneral::setConfiguration() {
ui->toggle_check_exit->setChecked(UISettings::values.confirm_before_closing);
ui->toggle_cpu_jit->setChecked(Settings::values.use_cpu_jit);
+ ui->toggle_update_check->setChecked(UISettings::values.check_for_update_on_start);
+ ui->toggle_auto_update->setChecked(UISettings::values.update_on_close);
+
// The first item is "auto-select" with actual value -1, so plus one here will do the trick
ui->region_combobox->setCurrentIndex(Settings::values.region_value + 1);
@@ -40,6 +44,10 @@ void ConfigureGeneral::applyConfiguration() {
UISettings::values.confirm_before_closing = ui->toggle_check_exit->isChecked();
UISettings::values.theme =
ui->theme_combobox->itemData(ui->theme_combobox->currentIndex()).toString();
+
+ UISettings::values.check_for_update_on_start = ui->toggle_update_check->isChecked();
+ UISettings::values.update_on_close = ui->toggle_auto_update->isChecked();
+
Settings::values.region_value = ui->region_combobox->currentIndex() - 1;
Settings::values.use_cpu_jit = ui->toggle_cpu_jit->isChecked();
Settings::Apply();
diff --git a/src/citra_qt/configuration/configure_general.ui b/src/citra_qt/configuration/configure_general.ui
index eedf2cbb0..00e5b49f5 100644
--- a/src/citra_qt/configuration/configure_general.ui
+++ b/src/citra_qt/configuration/configure_general.ui
@@ -25,16 +25,16 @@
-
-
-
+
- Search sub-directories for games
+ Confirm exit while emulation is running
-
-
+
- Confirm exit while emulation is running
+ Search sub-directories for games
@@ -44,24 +44,51 @@
-
-
-
- Performance
-
-
-
-
-
-
-
-
-
- Enable CPU JIT
-
-
-
-
-
+
+
+ Updates
+
+
+ -
+
+
-
+
+
+ Check for updates on start
+
+
+
+ -
+
+
+ Silently auto update after closing
+
+
+
-
+
+
+
+
+ -
+
+
+ Performance
+
+
+
-
+
+
-
+
+
+ Enable CPU JIT
+
+
+
+
+
+
+
-
@@ -149,8 +176,7 @@
-
-
-
+
diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp
index e0de66e02..0e9e8f1c5 100644
--- a/src/citra_qt/main.cpp
+++ b/src/citra_qt/main.cpp
@@ -13,6 +13,7 @@
#include
#include
#include
+#include "citra_qt/aboutdialog.h"
#include "citra_qt/bootmanager.h"
#include "citra_qt/configuration/config.h"
#include "citra_qt/configuration/configure_dialog.h"
@@ -29,6 +30,7 @@
#include "citra_qt/hotkeys.h"
#include "citra_qt/main.h"
#include "citra_qt/ui_settings.h"
+#include "citra_qt/updater/updater.h"
#include "common/logging/backend.h"
#include "common/logging/filter.h"
#include "common/logging/log.h"
@@ -99,6 +101,7 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
InitializeDebugWidgets();
InitializeRecentFileMenuActions();
InitializeHotkeys();
+ ShowUpdaterWidgets();
SetDefaultUIGeometry();
RestoreUIState();
@@ -117,6 +120,10 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
// Show one-time "callout" messages to the user
ShowCallouts();
+ if (UISettings::values.check_for_update_on_start) {
+ CheckForUpdates();
+ }
+
QStringList args = QApplication::arguments();
if (args.length() >= 2) {
BootGame(args[1]);
@@ -138,6 +145,10 @@ void GMainWindow::InitializeWidgets() {
game_list = new GameList(this);
ui.horizontalLayout->addWidget(game_list);
+ // Setup updater
+ updater = new Updater(this);
+ UISettings::values.updater_found = updater->HasUpdater();
+
// Create status bar
message_label = new QLabel();
// Configured separately for left alignment
@@ -267,6 +278,13 @@ void GMainWindow::InitializeHotkeys() {
});
}
+void GMainWindow::ShowUpdaterWidgets() {
+ ui.action_Check_For_Updates->setVisible(UISettings::values.updater_found);
+ ui.action_Open_Maintenance_Tool->setVisible(UISettings::values.updater_found);
+
+ connect(updater, &Updater::CheckUpdatesDone, this, &GMainWindow::OnUpdateFound);
+}
+
void GMainWindow::SetDefaultUIGeometry() {
// geometry: 55% of the window contents are in the upper screen half, 45% in the lower half
const QRect screenRect = QApplication::desktop()->screenGeometry(this);
@@ -340,6 +358,15 @@ void GMainWindow::ConnectMenuEvents() {
connect(ui.action_Show_Status_Bar, &QAction::triggered, statusBar(), &QStatusBar::setVisible);
ui.action_Fullscreen->setShortcut(GetHotkey("Main Window", "Fullscreen", this)->key());
connect(ui.action_Fullscreen, &QAction::triggered, this, &GMainWindow::ToggleFullscreen);
+
+ // Help
+ connect(ui.action_FAQ, &QAction::triggered,
+ []() { QDesktopServices::openUrl(QUrl("https://citra-emu.org/wiki/faq/")); });
+ connect(ui.action_About, &QAction::triggered, this, &GMainWindow::OnMenuAboutCitra);
+ connect(ui.action_Check_For_Updates, &QAction::triggered, this,
+ &GMainWindow::OnCheckForUpdates);
+ connect(ui.action_Open_Maintenance_Tool, &QAction::triggered, this,
+ &GMainWindow::OnOpenUpdater);
}
void GMainWindow::OnDisplayTitleBars(bool show) {
@@ -362,6 +389,73 @@ void GMainWindow::OnDisplayTitleBars(bool show) {
}
}
+void GMainWindow::OnCheckForUpdates() {
+ explicit_update_check = true;
+ CheckForUpdates();
+}
+
+void GMainWindow::CheckForUpdates() {
+ if (updater->CheckForUpdates()) {
+ LOG_INFO(Frontend, "Update check started");
+ } else {
+ LOG_WARNING(Frontend, "Unable to start check for updates");
+ }
+}
+
+void GMainWindow::OnUpdateFound(bool found, bool error) {
+ if (error) {
+ LOG_WARNING(Frontend, "Update check failed");
+ return;
+ }
+
+ if (!found) {
+ LOG_INFO(Frontend, "No updates found");
+
+ // If the user explicitly clicked the "Check for Updates" button, we are
+ // going to want to show them a prompt anyway.
+ if (explicit_update_check) {
+ explicit_update_check = false;
+ ShowNoUpdatePrompt();
+ }
+ return;
+ }
+
+ if (emulation_running && !explicit_update_check) {
+ LOG_INFO(Frontend, "Update found, deferring as game is running");
+ defer_update_prompt = true;
+ return;
+ }
+
+ LOG_INFO(Frontend, "Update found!");
+ explicit_update_check = false;
+
+ ShowUpdatePrompt();
+}
+
+void GMainWindow::ShowUpdatePrompt() {
+ defer_update_prompt = false;
+
+ auto result = QMessageBox::question(
+ this, tr("Update available!"),
+ tr("An update for Citra is available. Do you wish to install it now?
"
+ "This will terminate emulation, if it is running."),
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
+
+ if (result == QMessageBox::Yes) {
+ updater->LaunchUIOnExit();
+ close();
+ }
+}
+
+void GMainWindow::ShowNoUpdatePrompt() {
+ QMessageBox::information(this, tr("No update found"), tr("No update has been found for Citra."),
+ QMessageBox::Ok, QMessageBox::Ok);
+}
+
+void GMainWindow::OnOpenUpdater() {
+ updater->LaunchUI();
+}
+
bool GMainWindow::LoadROM(const QString& filename) {
// Shutdown previous session if the emu thread is still active...
if (emu_thread != nullptr)
@@ -517,6 +611,10 @@ void GMainWindow::ShutdownGame() {
emu_frametime_label->setVisible(false);
emulation_running = false;
+
+ if (defer_update_prompt) {
+ ShowUpdatePrompt();
+ }
}
void GMainWindow::StoreRecentFile(const QString& filename) {
@@ -801,6 +899,11 @@ void GMainWindow::OnCoreError(Core::System::ResultStatus result, std::string det
}
}
+void GMainWindow::OnMenuAboutCitra() {
+ AboutDialog about{this};
+ about.exec();
+}
+
bool GMainWindow::ConfirmClose() {
if (emu_thread == nullptr || !UISettings::values.confirm_before_closing)
return true;
diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h
index 4efab9a4a..878b2becf 100644
--- a/src/citra_qt/main.h
+++ b/src/citra_qt/main.h
@@ -2,8 +2,7 @@
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
-#ifndef _CITRA_QT_MAIN_HXX_
-#define _CITRA_QT_MAIN_HXX_
+#pragma once
#include
#include
@@ -24,7 +23,9 @@ class GRenderWindow;
class MicroProfileDialog;
class ProfilerWidget;
class RegistersWidget;
+class Updater;
class WaitTreeWidget;
+class AboutDialog;
class GMainWindow : public QMainWindow {
Q_OBJECT
@@ -81,6 +82,10 @@ private:
void ShutdownGame();
void ShowCallouts();
+ void ShowUpdaterWidgets();
+ void ShowUpdatePrompt();
+ void ShowNoUpdatePrompt();
+ void CheckForUpdates();
/**
* Stores the filename in the recently loaded files list.
@@ -131,6 +136,11 @@ private slots:
void ToggleWindowMode();
void OnCreateGraphicsSurfaceViewer();
void OnCoreError(Core::System::ResultStatus, std::string);
+ /// Called whenever a user selects Help->About Citra
+ void OnMenuAboutCitra();
+ void OnUpdateFound(bool found, bool error);
+ void OnCheckForUpdates();
+ void OnOpenUpdater();
private:
void UpdateStatusBar();
@@ -163,6 +173,10 @@ private:
GraphicsVertexShaderWidget* graphicsVertexShaderWidget;
GraphicsTracingWidget* graphicsTracingWidget;
WaitTreeWidget* waitTreeWidget;
+ Updater* updater;
+
+ bool explicit_update_check = false;
+ bool defer_update_prompt = false;
QAction* actions_recent_files[max_recent_files_item];
@@ -171,5 +185,3 @@ protected:
void dragEnterEvent(QDragEnterEvent* event) override;
void dragMoveEvent(QDragMoveEvent* event) override;
};
-
-#endif // _CITRA_QT_MAIN_HXX_
diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui
index 617343e51..417a8a6a6 100644
--- a/src/citra_qt/main.ui
+++ b/src/citra_qt/main.ui
@@ -96,6 +96,10 @@
&Help
+
+
+
+
@@ -142,6 +146,11 @@
&Stop
+
+
+ FAQ
+
+
About Citra
@@ -205,6 +214,19 @@
Fullscreen
+
+
+ Modify Citra Install
+
+
+ Opens the maintenance tool to modify your Citra installation
+
+
+
+
+ Check for Updates
+
+
diff --git a/src/citra_qt/ui_settings.h b/src/citra_qt/ui_settings.h
index ffbe14bc3..dba4c5d3c 100644
--- a/src/citra_qt/ui_settings.h
+++ b/src/citra_qt/ui_settings.h
@@ -39,6 +39,10 @@ struct Values {
bool confirm_before_closing;
bool first_start;
+ bool updater_found;
+ bool update_on_close;
+ bool check_for_update_on_start;
+
QString roms_path;
QString symbols_path;
QString gamedir;
diff --git a/src/citra_qt/updater/updater.cpp b/src/citra_qt/updater/updater.cpp
new file mode 100644
index 000000000..b5a448ca3
--- /dev/null
+++ b/src/citra_qt/updater/updater.cpp
@@ -0,0 +1,304 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+//
+// Based on the original work by Felix Barx
+// Copyright (c) 2015, Felix Barz
+// All rights reserved.
+
+#include
+#include
+#include
+#include
+#include
+#include "citra_qt/ui_settings.h"
+#include "citra_qt/updater/updater.h"
+#include "citra_qt/updater/updater_p.h"
+#include "common/logging/log.h"
+
+#ifdef Q_OS_OSX
+#define DEFAULT_TOOL_PATH QStringLiteral("../../../../maintenancetool")
+#else
+#define DEFAULT_TOOL_PATH QStringLiteral("../maintenancetool")
+#endif
+
+Updater::Updater(QObject* parent) : Updater(DEFAULT_TOOL_PATH, parent) {}
+
+Updater::Updater(const QString& maintenance_tool_path, QObject* parent)
+ : QObject(parent), backend(std::make_unique(this)) {
+ backend->tool_path = UpdaterPrivate::ToSystemExe(maintenance_tool_path);
+}
+
+Updater::~Updater() = default;
+
+bool Updater::ExitedNormally() const {
+ return backend->normal_exit;
+}
+
+int Updater::ErrorCode() const {
+ return backend->last_error_code;
+}
+
+QByteArray Updater::ErrorLog() const {
+ return backend->last_error_log;
+}
+
+bool Updater::IsRunning() const {
+ return backend->running;
+}
+
+QList Updater::LatestUpdateInfo() const {
+ return backend->update_info;
+}
+
+bool Updater::HasUpdater() const {
+ return backend->HasUpdater();
+}
+
+bool Updater::CheckForUpdates() {
+ return backend->StartUpdateCheck();
+}
+
+void Updater::AbortUpdateCheck(int max_delay, bool async) {
+ backend->StopUpdateCheck(max_delay, async);
+}
+
+void Updater::LaunchUI() {
+ backend->LaunchUI();
+}
+
+void Updater::SilentlyUpdate() {
+ backend->SilentlyUpdate();
+}
+
+void Updater::LaunchUIOnExit() {
+ backend->LaunchUIOnExit();
+}
+
+Updater::UpdateInfo::UpdateInfo() = default;
+
+Updater::UpdateInfo::UpdateInfo(const Updater::UpdateInfo&) = default;
+
+Updater::UpdateInfo::UpdateInfo(QString name, QString version, quint64 size)
+ : name(std::move(name)), version(std::move(version)), size(size) {}
+
+UpdaterPrivate::UpdaterPrivate(Updater* parent_ptr) : QObject(nullptr), parent(parent_ptr) {
+ connect(qApp, &QCoreApplication::aboutToQuit, this, &UpdaterPrivate::AboutToExit,
+ Qt::DirectConnection);
+ qRegisterMetaType("QProcess::ExitStatus");
+}
+
+UpdaterPrivate::~UpdaterPrivate() {
+ if (main_process && main_process->state() != QProcess::NotRunning) {
+ main_process->kill();
+ main_process->waitForFinished(1000);
+ }
+}
+
+QString UpdaterPrivate::ToSystemExe(QString base_path) {
+#if defined(Q_OS_WIN32)
+ if (!base_path.endsWith(QStringLiteral(".exe")))
+ return base_path + QStringLiteral(".exe");
+ else
+ return base_path;
+#elif defined(Q_OS_OSX)
+ if (base_path.endsWith(QStringLiteral(".app")))
+ base_path.truncate(base_path.lastIndexOf(QStringLiteral(".")));
+ return base_path + QStringLiteral(".app/Contents/MacOS/") + QFileInfo(base_path).fileName();
+#elif defined(Q_OS_UNIX)
+ return base_path;
+#endif
+}
+
+bool UpdaterPrivate::HasUpdater() const {
+ QFileInfo tool_info(QCoreApplication::applicationDirPath(), tool_path);
+ return tool_info.exists();
+}
+
+bool UpdaterPrivate::StartUpdateCheck() {
+ if (running || !HasUpdater()) {
+ return false;
+ }
+
+ update_info.clear();
+ normal_exit = true;
+ last_error_code = EXIT_SUCCESS;
+ last_error_log.clear();
+
+ QFileInfo tool_info(QCoreApplication::applicationDirPath(), tool_path);
+ main_process = new QProcess(this);
+ main_process->setProgram(tool_info.absoluteFilePath());
+ main_process->setArguments({QStringLiteral("--checkupdates"), QStringLiteral("-v")});
+
+ connect(main_process,
+ static_cast(&QProcess::finished), this,
+ &UpdaterPrivate::UpdaterReady, Qt::QueuedConnection);
+ connect(main_process, static_cast(&QProcess::error),
+ this, &UpdaterPrivate::UpdaterError, Qt::QueuedConnection);
+
+ main_process->start(QIODevice::ReadOnly);
+ running = true;
+
+ emit parent->UpdateInfoChanged(update_info);
+ emit parent->RunningChanged(true);
+ return true;
+}
+
+void UpdaterPrivate::StopUpdateCheck(int delay, bool async) {
+ if (main_process == nullptr || main_process->state() == QProcess::NotRunning) {
+ return;
+ }
+
+ if (delay > 0) {
+ main_process->terminate();
+ if (async) {
+ QTimer* timer = new QTimer(this);
+ timer->setSingleShot(true);
+
+ connect(timer, &QTimer::timeout, [=]() {
+ StopUpdateCheck(0, false);
+ timer->deleteLater();
+ });
+
+ timer->start(delay);
+ } else {
+ if (!main_process->waitForFinished(delay)) {
+ main_process->kill();
+ main_process->waitForFinished(100);
+ }
+ }
+ } else {
+ main_process->kill();
+ main_process->waitForFinished(100);
+ }
+}
+
+XMLParseResult UpdaterPrivate::ParseResult(const QByteArray& output,
+ QList& out) {
+ const auto out_string = QString::fromUtf8(output);
+ const auto xml_begin = out_string.indexOf(QStringLiteral(""));
+ if (xml_begin < 0)
+ return XMLParseResult::NoUpdate;
+ const auto xml_end = out_string.indexOf(QStringLiteral(""), xml_begin);
+ if (xml_end < 0)
+ return XMLParseResult::NoUpdate;
+
+ QList updates;
+ QXmlStreamReader reader(out_string.mid(xml_begin, (xml_end + 10) - xml_begin));
+
+ reader.readNextStartElement();
+ // should always work because it was search for
+ if (reader.name() != QStringLiteral("updates")) {
+ return XMLParseResult::InvalidXML;
+ }
+
+ while (reader.readNextStartElement()) {
+ if (reader.name() != QStringLiteral("update"))
+ return XMLParseResult::InvalidXML;
+
+ auto ok = false;
+ Updater::UpdateInfo info(
+ reader.attributes().value(QStringLiteral("name")).toString(),
+ reader.attributes().value(QStringLiteral("version")).toString(),
+ reader.attributes().value(QStringLiteral("size")).toULongLong(&ok));
+
+ if (info.name.isEmpty() || info.version.isNull() || !ok)
+ return XMLParseResult::InvalidXML;
+ if (reader.readNextStartElement())
+ return XMLParseResult::InvalidXML;
+
+ updates.append(info);
+ }
+
+ if (reader.hasError()) {
+ LOG_ERROR(Frontend, "Cannot read xml for update: %s",
+ reader.errorString().toStdString().c_str());
+ return XMLParseResult::InvalidXML;
+ }
+
+ out = updates;
+ return XMLParseResult::Success;
+}
+
+void UpdaterPrivate::UpdaterReady(int exit_code, QProcess::ExitStatus exit_status) {
+ if (main_process == nullptr) {
+ return;
+ }
+
+ if (exit_status != QProcess::NormalExit) {
+ UpdaterError(QProcess::Crashed);
+ return;
+ }
+
+ normal_exit = true;
+ last_error_code = exit_code;
+ last_error_log = main_process->readAllStandardError();
+ const auto update_out = main_process->readAllStandardOutput();
+ main_process->deleteLater();
+ main_process = nullptr;
+
+ running = false;
+ emit parent->RunningChanged(false);
+
+ QList update_info;
+ auto err = ParseResult(update_out, update_info);
+ bool has_error = false;
+
+ if (err == XMLParseResult::Success) {
+ if (!update_info.isEmpty())
+ emit parent->UpdateInfoChanged(update_info);
+ } else if (err == XMLParseResult::InvalidXML) {
+ has_error = true;
+ }
+
+ emit parent->CheckUpdatesDone(!update_info.isEmpty(), has_error);
+}
+
+void UpdaterPrivate::UpdaterError(QProcess::ProcessError error) {
+ if (main_process) {
+ normal_exit = false;
+ last_error_code = error;
+ last_error_log = main_process->errorString().toUtf8();
+ main_process->deleteLater();
+ main_process = nullptr;
+
+ running = false;
+ emit parent->RunningChanged(false);
+ emit parent->CheckUpdatesDone(false, true);
+ }
+}
+
+void UpdaterPrivate::LaunchWithArguments(const QStringList& args) {
+ if (!HasUpdater()) {
+ return;
+ }
+
+ QFileInfo tool_info(QCoreApplication::applicationDirPath(), tool_path);
+
+ if (!QProcess::startDetached(tool_info.absoluteFilePath(), args, tool_info.absolutePath())) {
+ LOG_WARNING(Frontend, "Unable to start program %s",
+ tool_info.absoluteFilePath().toStdString().c_str());
+ }
+}
+
+void UpdaterPrivate::LaunchUI() {
+ LOG_INFO(Frontend, "Launching update UI...");
+ LaunchWithArguments(run_arguments);
+}
+
+void UpdaterPrivate::SilentlyUpdate() {
+ LOG_INFO(Frontend, "Launching silent update...");
+ LaunchWithArguments(silent_arguments);
+}
+
+void UpdaterPrivate::AboutToExit() {
+ if (launch_ui_on_exit) {
+ LaunchUI();
+ } else if (UISettings::values.update_on_close) {
+ SilentlyUpdate();
+ }
+}
+
+void UpdaterPrivate::LaunchUIOnExit() {
+ launch_ui_on_exit = true;
+}
diff --git a/src/citra_qt/updater/updater.h b/src/citra_qt/updater/updater.h
new file mode 100644
index 000000000..e54590534
--- /dev/null
+++ b/src/citra_qt/updater/updater.h
@@ -0,0 +1,169 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+//
+// Based on the original work by Felix Barx
+// Copyright (c) 2015, Felix Barz
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// * Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+//
+// * Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+//
+// * Neither the name of QtAutoUpdater nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+class UpdaterPrivate;
+
+/// The main updater. Can check for updates and run the maintenancetool as updater
+class Updater : public QObject {
+ Q_OBJECT;
+
+ /// Specifies whether the updater is currently checking for updates or not
+ Q_PROPERTY(bool running READ IsRunning NOTIFY RunningChanged);
+ /// Holds extended information about the last update check
+ Q_PROPERTY(QList update_info READ LatestUpdateInfo NOTIFY UpdateInfoChanged);
+
+public:
+ /// Provides information about updates for components
+ struct UpdateInfo {
+ /// The name of the component that has an update
+ QString name;
+ /// The new version for that compontent
+ QString version;
+ /// The update download size (in Bytes)
+ quint64 size = 0;
+
+ /**
+ * Default Constructor
+ */
+ UpdateInfo();
+
+ /**
+ * Copy Constructor
+ */
+ UpdateInfo(const UpdateInfo& other);
+
+ /**
+ * Constructor that takes name, version and size.
+ */
+ UpdateInfo(QString name, QString version, quint64 size);
+ };
+
+ /**
+ * Default constructor
+ **/
+ explicit Updater(QObject* parent = nullptr);
+
+ /**
+ * Constructor with an explicitly set path
+ **/
+ explicit Updater(const QString& maintenance_tool_path, QObject* parent = nullptr);
+
+ /**
+ * Destroys the updater and kills the update check (if running)
+ **/
+ ~Updater();
+
+ /**
+ * Returns `true`, if the updater exited normally
+ **/
+ bool ExitedNormally() const;
+
+ /**
+ * Returns the mainetancetools error code from the last update check, if any.
+ **/
+ int ErrorCode() const;
+
+ /**
+ * Returns the error output (stderr) of the last update
+ **/
+ QByteArray ErrorLog() const;
+
+ /**
+ * Returns if a update check is running.
+ **/
+ bool IsRunning() const;
+
+ /**
+ * Returns the latest update information available, if any.
+ **/
+ QList LatestUpdateInfo() const;
+
+ /**
+ * Launches the updater UI formally
+ **/
+ void LaunchUI();
+
+ /**
+ * Silently updates the application in the background
+ **/
+ void SilentlyUpdate();
+
+ /**
+ * Checks to see if a updater application is available
+ **/
+ bool HasUpdater() const;
+
+ /**
+ * Instead of silently updating, explictly open the UI on shutdown
+ **/
+ void LaunchUIOnExit();
+
+public slots:
+ /**
+ * Starts checking for updates
+ **/
+ bool CheckForUpdates();
+
+ /**
+ * Aborts checking for updates
+ **/
+ void AbortUpdateCheck(int max_delay = 5000, bool async = false);
+
+signals:
+ /**
+ * Will be emitted as soon as the updater finished checking for updates
+ **/
+ void CheckUpdatesDone(bool has_updates, bool has_error);
+
+ /**
+ * Emitted when a update check operation changes stage
+ **/
+ void RunningChanged(bool running);
+
+ /**
+ * Emitted when new update information has been found
+ **/
+ void UpdateInfoChanged(QList update_info);
+
+private:
+ std::unique_ptr backend;
+};
diff --git a/src/citra_qt/updater/updater_p.h b/src/citra_qt/updater/updater_p.h
new file mode 100644
index 000000000..175a95623
--- /dev/null
+++ b/src/citra_qt/updater/updater_p.h
@@ -0,0 +1,66 @@
+// Copyright 2017 Citra Emulator Project
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+//
+// Based on the original work by Felix Barx
+// Copyright (c) 2015, Felix Barz
+// All rights reserved.
+
+#pragma once
+
+#include
+#include "citra_qt/updater/updater.h"
+
+enum class XMLParseResult {
+ Success,
+ NoUpdate,
+ InvalidXML,
+};
+
+class UpdaterPrivate : public QObject {
+ Q_OBJECT;
+
+public:
+ explicit UpdaterPrivate(Updater* parent_ptr);
+ ~UpdaterPrivate();
+
+ static QString ToSystemExe(QString base_path);
+
+ bool HasUpdater() const;
+
+ bool StartUpdateCheck();
+ void StopUpdateCheck(int delay, bool async);
+
+ void LaunchWithArguments(const QStringList& args);
+ void LaunchUI();
+ void SilentlyUpdate();
+
+ void LaunchUIOnExit();
+
+public slots:
+ void UpdaterReady(int exit_code, QProcess::ExitStatus exit_status);
+ void UpdaterError(QProcess::ProcessError error);
+
+ void AboutToExit();
+
+private:
+ XMLParseResult ParseResult(const QByteArray& output, QList& out);
+
+ Updater* parent;
+
+ QString tool_path{};
+ QList update_info{};
+ bool normal_exit = true;
+ int last_error_code = 0;
+ QByteArray last_error_log = EXIT_SUCCESS;
+
+ bool running = false;
+ QProcess* main_process = nullptr;
+
+ bool launch_ui_on_exit = false;
+
+ QStringList run_arguments{"--updater"};
+ QStringList silent_arguments{"--silentUpdate"};
+
+ friend class Updater;
+};
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 0c7a72987..41a972134 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -96,14 +96,15 @@ System::ResultStatus System::Load(EmuWindow* emu_window, const std::string& file
ResultStatus init_result{Init(emu_window, system_mode.first.get())};
if (init_result != ResultStatus::Success) {
- LOG_CRITICAL(Core, "Failed to initialize system (Error %i)!", init_result);
+ LOG_CRITICAL(Core, "Failed to initialize system (Error %u)!",
+ static_cast(init_result));
System::Shutdown();
return init_result;
}
const Loader::ResultStatus load_result{app_loader->Load(Kernel::g_current_process)};
if (Loader::ResultStatus::Success != load_result) {
- LOG_CRITICAL(Core, "Failed to load ROM (Error %i)!", load_result);
+ LOG_CRITICAL(Core, "Failed to load ROM (Error %u)!", static_cast(load_result));
System::Shutdown();
switch (load_result) {
diff --git a/src/core/gdbstub/gdbstub.cpp b/src/core/gdbstub/gdbstub.cpp
index d6be16ef6..68704e72e 100644
--- a/src/core/gdbstub/gdbstub.cpp
+++ b/src/core/gdbstub/gdbstub.cpp
@@ -302,7 +302,7 @@ static void RemoveBreakpoint(BreakpointType type, PAddr addr) {
auto bp = p.find(addr);
if (bp != p.end()) {
LOG_DEBUG(Debug_GDBStub, "gdb: removed a breakpoint: %08x bytes at %08x of type %d\n",
- bp->second.len, bp->second.addr, type);
+ bp->second.len, bp->second.addr, static_cast(type));
p.erase(addr);
}
}
@@ -348,8 +348,8 @@ bool CheckBreakpoint(PAddr addr, BreakpointType type) {
if (bp->second.active && (addr >= bp->second.addr && addr < bp->second.addr + len)) {
LOG_DEBUG(Debug_GDBStub,
- "Found breakpoint type %d @ %08x, range: %08x - %08x (%d bytes)\n", type,
- addr, bp->second.addr, bp->second.addr + len, len);
+ "Found breakpoint type %d @ %08x, range: %08x - %08x (%u bytes)\n",
+ static_cast(type), addr, bp->second.addr, bp->second.addr + len, len);
return true;
}
}
@@ -738,8 +738,8 @@ static bool CommitBreakpoint(BreakpointType type, PAddr addr, u32 len) {
breakpoint.len = len;
p.insert({addr, breakpoint});
- LOG_DEBUG(Debug_GDBStub, "gdb: added %d breakpoint: %08x bytes at %08x\n", type, breakpoint.len,
- breakpoint.addr);
+ LOG_DEBUG(Debug_GDBStub, "gdb: added %d breakpoint: %08x bytes at %08x\n",
+ static_cast(type), breakpoint.len, breakpoint.addr);
return true;
}
diff --git a/src/core/hle/applets/applet.cpp b/src/core/hle/applets/applet.cpp
index 9c43ed2fd..879dfff0d 100644
--- a/src/core/hle/applets/applet.cpp
+++ b/src/core/hle/applets/applet.cpp
@@ -62,7 +62,7 @@ ResultCode Applet::Create(Service::APT::AppletId id) {
applets[id] = std::make_shared(id);
break;
default:
- LOG_ERROR(Service_APT, "Could not create applet %u", id);
+ LOG_ERROR(Service_APT, "Could not create applet %u", static_cast(id));
// TODO(Subv): Find the right error code
return ResultCode(ErrorDescription::NotFound, ErrorModule::Applet,
ErrorSummary::NotSupported, ErrorLevel::Permanent);
@@ -82,7 +82,7 @@ std::shared_ptr