Logging: Add customizable logging backends and fmtlib based macros
* Change the logging backend to support multiple sinks through the Backend Interface * Add a new set of logging macros to use fmtlib instead. * Qt: Compile as GUI application on windows to make the console hidden by default. Add filter configuration and a button to open log location. * SDL: Migrate to the new logging macros
This commit is contained in:
parent
3cda637cb1
commit
0daac3020e
@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
#include "citra/config.h"
|
#include "citra/config.h"
|
||||||
#include "citra/emu_window/emu_window_sdl2.h"
|
#include "citra/emu_window/emu_window_sdl2.h"
|
||||||
|
#include "common/common_paths.h"
|
||||||
#include "common/file_util.h"
|
#include "common/file_util.h"
|
||||||
#include "common/logging/backend.h"
|
#include "common/logging/backend.h"
|
||||||
#include "common/logging/filter.h"
|
#include "common/logging/filter.h"
|
||||||
@ -60,39 +61,39 @@ static void PrintVersion() {
|
|||||||
static void OnStateChanged(const Network::RoomMember::State& state) {
|
static void OnStateChanged(const Network::RoomMember::State& state) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case Network::RoomMember::State::Idle:
|
case Network::RoomMember::State::Idle:
|
||||||
LOG_DEBUG(Network, "Network is idle");
|
NGLOG_DEBUG(Network, "Network is idle");
|
||||||
break;
|
break;
|
||||||
case Network::RoomMember::State::Joining:
|
case Network::RoomMember::State::Joining:
|
||||||
LOG_DEBUG(Network, "Connection sequence to room started");
|
NGLOG_DEBUG(Network, "Connection sequence to room started");
|
||||||
break;
|
break;
|
||||||
case Network::RoomMember::State::Joined:
|
case Network::RoomMember::State::Joined:
|
||||||
LOG_DEBUG(Network, "Successfully joined to the room");
|
NGLOG_DEBUG(Network, "Successfully joined to the room");
|
||||||
break;
|
break;
|
||||||
case Network::RoomMember::State::LostConnection:
|
case Network::RoomMember::State::LostConnection:
|
||||||
LOG_DEBUG(Network, "Lost connection to the room");
|
NGLOG_DEBUG(Network, "Lost connection to the room");
|
||||||
break;
|
break;
|
||||||
case Network::RoomMember::State::CouldNotConnect:
|
case Network::RoomMember::State::CouldNotConnect:
|
||||||
LOG_ERROR(Network, "State: CouldNotConnect");
|
NGLOG_ERROR(Network, "State: CouldNotConnect");
|
||||||
exit(1);
|
exit(1);
|
||||||
break;
|
break;
|
||||||
case Network::RoomMember::State::NameCollision:
|
case Network::RoomMember::State::NameCollision:
|
||||||
LOG_ERROR(
|
NGLOG_ERROR(
|
||||||
Network,
|
Network,
|
||||||
"You tried to use the same nickname then another user that is connected to the Room");
|
"You tried to use the same nickname then another user that is connected to the Room");
|
||||||
exit(1);
|
exit(1);
|
||||||
break;
|
break;
|
||||||
case Network::RoomMember::State::MacCollision:
|
case Network::RoomMember::State::MacCollision:
|
||||||
LOG_ERROR(Network, "You tried to use the same MAC-Address then another user that is "
|
NGLOG_ERROR(Network, "You tried to use the same MAC-Address then another user that is "
|
||||||
"connected to the Room");
|
"connected to the Room");
|
||||||
exit(1);
|
exit(1);
|
||||||
break;
|
break;
|
||||||
case Network::RoomMember::State::WrongPassword:
|
case Network::RoomMember::State::WrongPassword:
|
||||||
LOG_ERROR(Network, "Room replied with: Wrong password");
|
NGLOG_ERROR(Network, "Room replied with: Wrong password");
|
||||||
exit(1);
|
exit(1);
|
||||||
break;
|
break;
|
||||||
case Network::RoomMember::State::WrongVersion:
|
case Network::RoomMember::State::WrongVersion:
|
||||||
LOG_ERROR(Network,
|
NGLOG_ERROR(Network,
|
||||||
"You are using a different version then the room you are trying to connect to");
|
"You are using a different version then the room you are trying to connect to");
|
||||||
exit(1);
|
exit(1);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@ -119,7 +120,7 @@ int main(int argc, char** argv) {
|
|||||||
auto argv_w = CommandLineToArgvW(GetCommandLineW(), &argc_w);
|
auto argv_w = CommandLineToArgvW(GetCommandLineW(), &argc_w);
|
||||||
|
|
||||||
if (argv_w == nullptr) {
|
if (argv_w == nullptr) {
|
||||||
LOG_CRITICAL(Frontend, "Failed to get command line arguments");
|
NGLOG_CRITICAL(Frontend, "Failed to get command line arguments");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -155,7 +156,7 @@ int main(int argc, char** argv) {
|
|||||||
break;
|
break;
|
||||||
case 'i': {
|
case 'i': {
|
||||||
const auto cia_progress = [](size_t written, size_t total) {
|
const auto cia_progress = [](size_t written, size_t total) {
|
||||||
LOG_INFO(Frontend, "%02zu%%", (written * 100 / total));
|
NGLOG_INFO(Frontend, "{:02d}%", (written * 100 / total));
|
||||||
};
|
};
|
||||||
if (Service::AM::InstallCIA(std::string(optarg), cia_progress) !=
|
if (Service::AM::InstallCIA(std::string(optarg), cia_progress) !=
|
||||||
Service::AM::InstallStatus::Success)
|
Service::AM::InstallStatus::Success)
|
||||||
@ -223,23 +224,27 @@ int main(int argc, char** argv) {
|
|||||||
LocalFree(argv_w);
|
LocalFree(argv_w);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Log::Filter log_filter(Log::Level::Debug);
|
|
||||||
Log::SetFilter(&log_filter);
|
|
||||||
|
|
||||||
MicroProfileOnThreadCreate("EmuThread");
|
MicroProfileOnThreadCreate("EmuThread");
|
||||||
SCOPE_EXIT({ MicroProfileShutdown(); });
|
SCOPE_EXIT({ MicroProfileShutdown(); });
|
||||||
|
|
||||||
if (filepath.empty()) {
|
if (filepath.empty()) {
|
||||||
LOG_CRITICAL(Frontend, "Failed to load ROM: No ROM specified");
|
NGLOG_CRITICAL(Frontend, "Failed to load ROM: No ROM specified");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!movie_record.empty() && !movie_play.empty()) {
|
if (!movie_record.empty() && !movie_play.empty()) {
|
||||||
LOG_CRITICAL(Frontend, "Cannot both play and record a movie");
|
NGLOG_CRITICAL(Frontend, "Cannot both play and record a movie");
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log::Filter log_filter;
|
||||||
log_filter.ParseFilterString(Settings::values.log_filter);
|
log_filter.ParseFilterString(Settings::values.log_filter);
|
||||||
|
Log::SetGlobalFilter(log_filter);
|
||||||
|
|
||||||
|
Log::AddBackend(std::make_unique<Log::ColorConsoleBackend>());
|
||||||
|
FileUtil::CreateFullPath(FileUtil::GetUserPath(D_LOGS_IDX));
|
||||||
|
Log::AddBackend(
|
||||||
|
std::make_unique<Log::FileBackend>(FileUtil::GetUserPath(D_LOGS_IDX) + LOG_FILE));
|
||||||
|
|
||||||
// Apply the command line arguments
|
// Apply the command line arguments
|
||||||
Settings::values.gdbstub_port = gdb_port;
|
Settings::values.gdbstub_port = gdb_port;
|
||||||
@ -258,28 +263,28 @@ int main(int argc, char** argv) {
|
|||||||
|
|
||||||
switch (load_result) {
|
switch (load_result) {
|
||||||
case Core::System::ResultStatus::ErrorGetLoader:
|
case Core::System::ResultStatus::ErrorGetLoader:
|
||||||
LOG_CRITICAL(Frontend, "Failed to obtain loader for %s!", filepath.c_str());
|
NGLOG_CRITICAL(Frontend, "Failed to obtain loader for {}!", filepath);
|
||||||
return -1;
|
return -1;
|
||||||
case Core::System::ResultStatus::ErrorLoader:
|
case Core::System::ResultStatus::ErrorLoader:
|
||||||
LOG_CRITICAL(Frontend, "Failed to load ROM!");
|
NGLOG_CRITICAL(Frontend, "Failed to load ROM!");
|
||||||
return -1;
|
return -1;
|
||||||
case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted:
|
case Core::System::ResultStatus::ErrorLoader_ErrorEncrypted:
|
||||||
LOG_CRITICAL(Frontend, "The game that you are trying to load must be decrypted before "
|
NGLOG_CRITICAL(Frontend, "The game that you are trying to load must be decrypted before "
|
||||||
"being used with Citra. \n\n For more information on dumping and "
|
"being used with Citra. \n\n For more information on dumping and "
|
||||||
"decrypting games, please refer to: "
|
"decrypting games, please refer to: "
|
||||||
"https://citra-emu.org/wiki/dumping-game-cartridges/");
|
"https://citra-emu.org/wiki/dumping-game-cartridges/");
|
||||||
return -1;
|
return -1;
|
||||||
case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat:
|
case Core::System::ResultStatus::ErrorLoader_ErrorInvalidFormat:
|
||||||
LOG_CRITICAL(Frontend, "Error while loading ROM: The ROM format is not supported.");
|
NGLOG_CRITICAL(Frontend, "Error while loading ROM: The ROM format is not supported.");
|
||||||
return -1;
|
return -1;
|
||||||
case Core::System::ResultStatus::ErrorNotInitialized:
|
case Core::System::ResultStatus::ErrorNotInitialized:
|
||||||
LOG_CRITICAL(Frontend, "CPUCore not initialized");
|
NGLOG_CRITICAL(Frontend, "CPUCore not initialized");
|
||||||
return -1;
|
return -1;
|
||||||
case Core::System::ResultStatus::ErrorSystemMode:
|
case Core::System::ResultStatus::ErrorSystemMode:
|
||||||
LOG_CRITICAL(Frontend, "Failed to determine system mode!");
|
NGLOG_CRITICAL(Frontend, "Failed to determine system mode!");
|
||||||
return -1;
|
return -1;
|
||||||
case Core::System::ResultStatus::ErrorVideoCore:
|
case Core::System::ResultStatus::ErrorVideoCore:
|
||||||
LOG_CRITICAL(Frontend, "VideoCore not initialized");
|
NGLOG_CRITICAL(Frontend, "VideoCore not initialized");
|
||||||
return -1;
|
return -1;
|
||||||
case Core::System::ResultStatus::Success:
|
case Core::System::ResultStatus::Success:
|
||||||
break; // Expected case
|
break; // Expected case
|
||||||
@ -291,11 +296,11 @@ int main(int argc, char** argv) {
|
|||||||
if (auto member = Network::GetRoomMember().lock()) {
|
if (auto member = Network::GetRoomMember().lock()) {
|
||||||
member->BindOnChatMessageRecieved(OnMessageReceived);
|
member->BindOnChatMessageRecieved(OnMessageReceived);
|
||||||
member->BindOnStateChanged(OnStateChanged);
|
member->BindOnStateChanged(OnStateChanged);
|
||||||
LOG_DEBUG(Network, "Start connection to %s:%u with nickname %s", address.c_str(), port,
|
NGLOG_DEBUG(Network, "Start connection to {}:{} with nickname {}", address, port,
|
||||||
nickname.c_str());
|
nickname);
|
||||||
member->Join(nickname, address.c_str(), port, 0, Network::NoPreferredMac, password);
|
member->Join(nickname, address.c_str(), port, 0, Network::NoPreferredMac, password);
|
||||||
} else {
|
} else {
|
||||||
LOG_ERROR(Network, "Could not access RoomMember");
|
NGLOG_ERROR(Network, "Could not access RoomMember");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,17 +27,17 @@ bool Config::LoadINI(const std::string& default_contents, bool retry) {
|
|||||||
const char* location = this->sdl2_config_loc.c_str();
|
const char* location = this->sdl2_config_loc.c_str();
|
||||||
if (sdl2_config->ParseError() < 0) {
|
if (sdl2_config->ParseError() < 0) {
|
||||||
if (retry) {
|
if (retry) {
|
||||||
LOG_WARNING(Config, "Failed to load %s. Creating file from defaults...", location);
|
NGLOG_WARNING(Config, "Failed to load {}. Creating file from defaults...", location);
|
||||||
FileUtil::CreateFullPath(location);
|
FileUtil::CreateFullPath(location);
|
||||||
FileUtil::WriteStringToFile(true, default_contents, location);
|
FileUtil::WriteStringToFile(true, default_contents, location);
|
||||||
sdl2_config = std::make_unique<INIReader>(location); // Reopen file
|
sdl2_config = std::make_unique<INIReader>(location); // Reopen file
|
||||||
|
|
||||||
return LoadINI(default_contents, false);
|
return LoadINI(default_contents, false);
|
||||||
}
|
}
|
||||||
LOG_ERROR(Config, "Failed.");
|
NGLOG_ERROR(Config, "Failed.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
LOG_INFO(Config, "Successfully loaded %s", location);
|
NGLOG_INFO(Config, "Successfully loaded {}", location);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,10 +49,18 @@ static const std::array<int, Settings::NativeButton::NumButtons> default_buttons
|
|||||||
|
|
||||||
static const std::array<std::array<int, 5>, Settings::NativeAnalog::NumAnalogs> default_analogs{{
|
static const std::array<std::array<int, 5>, Settings::NativeAnalog::NumAnalogs> default_analogs{{
|
||||||
{
|
{
|
||||||
SDL_SCANCODE_UP, SDL_SCANCODE_DOWN, SDL_SCANCODE_LEFT, SDL_SCANCODE_RIGHT, SDL_SCANCODE_D,
|
SDL_SCANCODE_UP,
|
||||||
|
SDL_SCANCODE_DOWN,
|
||||||
|
SDL_SCANCODE_LEFT,
|
||||||
|
SDL_SCANCODE_RIGHT,
|
||||||
|
SDL_SCANCODE_D,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
SDL_SCANCODE_I, SDL_SCANCODE_K, SDL_SCANCODE_J, SDL_SCANCODE_L, SDL_SCANCODE_D,
|
SDL_SCANCODE_I,
|
||||||
|
SDL_SCANCODE_K,
|
||||||
|
SDL_SCANCODE_J,
|
||||||
|
SDL_SCANCODE_L,
|
||||||
|
SDL_SCANCODE_D,
|
||||||
},
|
},
|
||||||
}};
|
}};
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ EmuWindow_SDL2::EmuWindow_SDL2() {
|
|||||||
|
|
||||||
// Initialize the window
|
// Initialize the window
|
||||||
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
|
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
|
||||||
LOG_CRITICAL(Frontend, "Failed to initialize SDL2! Exiting...");
|
NGLOG_CRITICAL(Frontend, "Failed to initialize SDL2! Exiting...");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,19 +89,19 @@ EmuWindow_SDL2::EmuWindow_SDL2() {
|
|||||||
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
|
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
|
||||||
|
|
||||||
if (render_window == nullptr) {
|
if (render_window == nullptr) {
|
||||||
LOG_CRITICAL(Frontend, "Failed to create SDL2 window: %s", SDL_GetError());
|
NGLOG_CRITICAL(Frontend, "Failed to create SDL2 window: {}", SDL_GetError());
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
gl_context = SDL_GL_CreateContext(render_window);
|
gl_context = SDL_GL_CreateContext(render_window);
|
||||||
|
|
||||||
if (gl_context == nullptr) {
|
if (gl_context == nullptr) {
|
||||||
LOG_CRITICAL(Frontend, "Failed to create SDL2 GL context: %s", SDL_GetError());
|
NGLOG_CRITICAL(Frontend, "Failed to create SDL2 GL context: {}", SDL_GetError());
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!gladLoadGLLoader(static_cast<GLADloadproc>(SDL_GL_GetProcAddress))) {
|
if (!gladLoadGLLoader(static_cast<GLADloadproc>(SDL_GL_GetProcAddress))) {
|
||||||
LOG_CRITICAL(Frontend, "Failed to initialize GL functions: %s", SDL_GetError());
|
NGLOG_CRITICAL(Frontend, "Failed to initialize GL functions: {}", SDL_GetError());
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +28,8 @@ add_executable(citra-qt
|
|||||||
configuration/configure_system.h
|
configuration/configure_system.h
|
||||||
configuration/configure_web.cpp
|
configuration/configure_web.cpp
|
||||||
configuration/configure_web.h
|
configuration/configure_web.h
|
||||||
|
debugger/console.h
|
||||||
|
debugger/console.cpp
|
||||||
debugger/graphics/graphics.cpp
|
debugger/graphics/graphics.cpp
|
||||||
debugger/graphics/graphics.h
|
debugger/graphics/graphics.h
|
||||||
debugger/graphics/graphics_breakpoint_observer.cpp
|
debugger/graphics/graphics_breakpoint_observer.cpp
|
||||||
@ -138,6 +140,10 @@ if (APPLE)
|
|||||||
target_sources(citra-qt PRIVATE ${MACOSX_ICON})
|
target_sources(citra-qt PRIVATE ${MACOSX_ICON})
|
||||||
set_target_properties(citra-qt PROPERTIES MACOSX_BUNDLE TRUE)
|
set_target_properties(citra-qt PROPERTIES MACOSX_BUNDLE TRUE)
|
||||||
set_target_properties(citra-qt PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist)
|
set_target_properties(citra-qt PROPERTIES MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist)
|
||||||
|
elseif(WIN32)
|
||||||
|
# compile as a win32 gui application instead of a console application
|
||||||
|
target_link_libraries(citra-qt PRIVATE Qt5::WinMain)
|
||||||
|
set_target_properties(citra-qt PROPERTIES LINK_FLAGS_RELEASE "/SUBSYSTEM:WINDOWS")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
create_target_directory_groups(citra-qt)
|
create_target_directory_groups(citra-qt)
|
||||||
|
@ -24,10 +24,18 @@ const std::array<int, Settings::NativeButton::NumButtons> Config::default_button
|
|||||||
|
|
||||||
const std::array<std::array<int, 5>, Settings::NativeAnalog::NumAnalogs> Config::default_analogs{{
|
const std::array<std::array<int, 5>, Settings::NativeAnalog::NumAnalogs> Config::default_analogs{{
|
||||||
{
|
{
|
||||||
Qt::Key_Up, Qt::Key_Down, Qt::Key_Left, Qt::Key_Right, Qt::Key_D,
|
Qt::Key_Up,
|
||||||
|
Qt::Key_Down,
|
||||||
|
Qt::Key_Left,
|
||||||
|
Qt::Key_Right,
|
||||||
|
Qt::Key_D,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Qt::Key_I, Qt::Key_K, Qt::Key_J, Qt::Key_L, Qt::Key_D,
|
Qt::Key_I,
|
||||||
|
Qt::Key_K,
|
||||||
|
Qt::Key_J,
|
||||||
|
Qt::Key_L,
|
||||||
|
Qt::Key_D,
|
||||||
},
|
},
|
||||||
}};
|
}};
|
||||||
|
|
||||||
@ -216,6 +224,7 @@ void Config::ReadValues() {
|
|||||||
UISettings::values.confirm_before_closing = qt_config->value("confirmClose", true).toBool();
|
UISettings::values.confirm_before_closing = qt_config->value("confirmClose", true).toBool();
|
||||||
UISettings::values.first_start = qt_config->value("firstStart", true).toBool();
|
UISettings::values.first_start = qt_config->value("firstStart", true).toBool();
|
||||||
UISettings::values.callout_flags = qt_config->value("calloutFlags", 0).toUInt();
|
UISettings::values.callout_flags = qt_config->value("calloutFlags", 0).toUInt();
|
||||||
|
UISettings::values.show_console = qt_config->value("showConsole", false).toBool();
|
||||||
|
|
||||||
qt_config->endGroup();
|
qt_config->endGroup();
|
||||||
}
|
}
|
||||||
@ -357,6 +366,7 @@ void Config::SaveValues() {
|
|||||||
qt_config->setValue("confirmClose", UISettings::values.confirm_before_closing);
|
qt_config->setValue("confirmClose", UISettings::values.confirm_before_closing);
|
||||||
qt_config->setValue("firstStart", UISettings::values.first_start);
|
qt_config->setValue("firstStart", UISettings::values.first_start);
|
||||||
qt_config->setValue("calloutFlags", UISettings::values.callout_flags);
|
qt_config->setValue("calloutFlags", UISettings::values.callout_flags);
|
||||||
|
qt_config->setValue("showConsole", UISettings::values.show_console);
|
||||||
|
|
||||||
qt_config->endGroup();
|
qt_config->endGroup();
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,27 @@
|
|||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#include <QDesktopServices>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
#include "citra_qt/configuration/configure_debug.h"
|
#include "citra_qt/configuration/configure_debug.h"
|
||||||
|
#include "citra_qt/debugger/console.h"
|
||||||
|
#include "citra_qt/ui_settings.h"
|
||||||
|
#include "common/file_util.h"
|
||||||
|
#include "common/logging/backend.h"
|
||||||
|
#include "common/logging/filter.h"
|
||||||
|
#include "common/logging/log.h"
|
||||||
|
#include "core/core.h"
|
||||||
#include "core/settings.h"
|
#include "core/settings.h"
|
||||||
#include "ui_configure_debug.h"
|
#include "ui_configure_debug.h"
|
||||||
|
|
||||||
ConfigureDebug::ConfigureDebug(QWidget* parent) : QWidget(parent), ui(new Ui::ConfigureDebug) {
|
ConfigureDebug::ConfigureDebug(QWidget* parent) : QWidget(parent), ui(new Ui::ConfigureDebug) {
|
||||||
ui->setupUi(this);
|
ui->setupUi(this);
|
||||||
this->setConfiguration();
|
this->setConfiguration();
|
||||||
|
connect(ui->open_log_button, &QPushButton::pressed, []() {
|
||||||
|
QString path = QString::fromStdString(FileUtil::GetUserPath(D_LOGS_IDX));
|
||||||
|
QDesktopServices::openUrl(QUrl::fromLocalFile(path));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfigureDebug::~ConfigureDebug() {}
|
ConfigureDebug::~ConfigureDebug() {}
|
||||||
@ -17,11 +31,20 @@ void ConfigureDebug::setConfiguration() {
|
|||||||
ui->toggle_gdbstub->setChecked(Settings::values.use_gdbstub);
|
ui->toggle_gdbstub->setChecked(Settings::values.use_gdbstub);
|
||||||
ui->gdbport_spinbox->setEnabled(Settings::values.use_gdbstub);
|
ui->gdbport_spinbox->setEnabled(Settings::values.use_gdbstub);
|
||||||
ui->gdbport_spinbox->setValue(Settings::values.gdbstub_port);
|
ui->gdbport_spinbox->setValue(Settings::values.gdbstub_port);
|
||||||
|
ui->toggle_console->setEnabled(!Core::System::GetInstance().IsPoweredOn());
|
||||||
|
ui->toggle_console->setChecked(UISettings::values.show_console);
|
||||||
|
ui->log_filter_edit->setText(QString::fromStdString(Settings::values.log_filter));
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigureDebug::applyConfiguration() {
|
void ConfigureDebug::applyConfiguration() {
|
||||||
Settings::values.use_gdbstub = ui->toggle_gdbstub->isChecked();
|
Settings::values.use_gdbstub = ui->toggle_gdbstub->isChecked();
|
||||||
Settings::values.gdbstub_port = ui->gdbport_spinbox->value();
|
Settings::values.gdbstub_port = ui->gdbport_spinbox->value();
|
||||||
|
UISettings::values.show_console = ui->toggle_console->isChecked();
|
||||||
|
Settings::values.log_filter = ui->log_filter_edit->text().toStdString();
|
||||||
|
Debugger::ToggleConsole();
|
||||||
|
Log::Filter filter;
|
||||||
|
filter.ParseFilterString(Settings::values.log_filter);
|
||||||
|
Log::SetGlobalFilter(filter);
|
||||||
Settings::Apply();
|
Settings::Apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,6 +72,47 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="groupBox_2">
|
||||||
|
<property name="title">
|
||||||
|
<string>Logging</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Global Log Filter</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="log_filter_edit"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="toggle_console">
|
||||||
|
<property name="text">
|
||||||
|
<string>Show Log Console (Windows Only)</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="open_log_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>Open Log Location</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<spacer name="verticalSpacer">
|
<spacer name="verticalSpacer">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
|
34
src/citra_qt/debugger/console.cpp
Normal file
34
src/citra_qt/debugger/console.cpp
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// Copyright 2018 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include <wincon.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "citra_qt/debugger/console.h"
|
||||||
|
#include "citra_qt/ui_settings.h"
|
||||||
|
|
||||||
|
namespace Debugger {
|
||||||
|
void ToggleConsole() {
|
||||||
|
#ifdef _WIN32
|
||||||
|
if (UISettings::values.show_console) {
|
||||||
|
if (AllocConsole()) {
|
||||||
|
freopen_s((FILE**)stdin, "CONIN$", "r", stdin);
|
||||||
|
freopen_s((FILE**)stdout, "CONOUT$", "w", stdout);
|
||||||
|
freopen_s((FILE**)stderr, "CONOUT$", "w", stderr);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (FreeConsole()) {
|
||||||
|
// In order to close the console, we have to also detach the streams on it.
|
||||||
|
// Just redirect them to NUL if there is no console window
|
||||||
|
freopen_s((FILE**)stdin, "NUL", "r", stdin);
|
||||||
|
freopen_s((FILE**)stdout, "NUL", "w", stdout);
|
||||||
|
freopen_s((FILE**)stderr, "NUL", "w", stderr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
} // namespace Debugger
|
14
src/citra_qt/debugger/console.h
Normal file
14
src/citra_qt/debugger/console.h
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// Copyright 2018 Citra Emulator Project
|
||||||
|
// Licensed under GPLv2 or any later version
|
||||||
|
// Refer to the license.txt file included.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace Debugger {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses the WINAPI to hide or show the stderr console. This function is a placeholder until we can
|
||||||
|
* get a real qt logging window which would work for all platforms.
|
||||||
|
*/
|
||||||
|
void ToggleConsole();
|
||||||
|
} // namespace Debugger
|
@ -20,6 +20,7 @@
|
|||||||
#include "citra_qt/compatdb.h"
|
#include "citra_qt/compatdb.h"
|
||||||
#include "citra_qt/configuration/config.h"
|
#include "citra_qt/configuration/config.h"
|
||||||
#include "citra_qt/configuration/configure_dialog.h"
|
#include "citra_qt/configuration/configure_dialog.h"
|
||||||
|
#include "citra_qt/debugger/console.h"
|
||||||
#include "citra_qt/debugger/graphics/graphics.h"
|
#include "citra_qt/debugger/graphics/graphics.h"
|
||||||
#include "citra_qt/debugger/graphics/graphics_breakpoints.h"
|
#include "citra_qt/debugger/graphics/graphics_breakpoints.h"
|
||||||
#include "citra_qt/debugger/graphics/graphics_cmdlists.h"
|
#include "citra_qt/debugger/graphics/graphics_cmdlists.h"
|
||||||
@ -34,6 +35,7 @@
|
|||||||
#include "citra_qt/main.h"
|
#include "citra_qt/main.h"
|
||||||
#include "citra_qt/ui_settings.h"
|
#include "citra_qt/ui_settings.h"
|
||||||
#include "citra_qt/updater/updater.h"
|
#include "citra_qt/updater/updater.h"
|
||||||
|
#include "common/common_paths.h"
|
||||||
#include "common/logging/backend.h"
|
#include "common/logging/backend.h"
|
||||||
#include "common/logging/filter.h"
|
#include "common/logging/filter.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
@ -369,6 +371,7 @@ void GMainWindow::RestoreUIState() {
|
|||||||
|
|
||||||
ui.action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar);
|
ui.action_Show_Status_Bar->setChecked(UISettings::values.show_status_bar);
|
||||||
statusBar()->setVisible(ui.action_Show_Status_Bar->isChecked());
|
statusBar()->setVisible(ui.action_Show_Status_Bar->isChecked());
|
||||||
|
Debugger::ToggleConsole();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GMainWindow::ConnectWidgetEvents() {
|
void GMainWindow::ConnectWidgetEvents() {
|
||||||
@ -1295,8 +1298,7 @@ void GMainWindow::SyncMenuUISettings() {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
Log::Filter log_filter(Log::Level::Info);
|
Log::AddBackend(std::make_unique<Log::ColorConsoleBackend>());
|
||||||
Log::SetFilter(&log_filter);
|
|
||||||
|
|
||||||
MicroProfileOnThreadCreate("Frontend");
|
MicroProfileOnThreadCreate("Frontend");
|
||||||
SCOPE_EXIT({ MicroProfileShutdown(); });
|
SCOPE_EXIT({ MicroProfileShutdown(); });
|
||||||
@ -1314,7 +1316,12 @@ int main(int argc, char* argv[]) {
|
|||||||
|
|
||||||
GMainWindow main_window;
|
GMainWindow main_window;
|
||||||
// After settings have been loaded by GMainWindow, apply the filter
|
// After settings have been loaded by GMainWindow, apply the filter
|
||||||
|
Log::Filter log_filter;
|
||||||
log_filter.ParseFilterString(Settings::values.log_filter);
|
log_filter.ParseFilterString(Settings::values.log_filter);
|
||||||
|
Log::SetGlobalFilter(log_filter);
|
||||||
|
FileUtil::CreateFullPath(FileUtil::GetUserPath(D_LOGS_IDX));
|
||||||
|
Log::AddBackend(
|
||||||
|
std::make_unique<Log::FileBackend>(FileUtil::GetUserPath(D_LOGS_IDX) + LOG_FILE));
|
||||||
|
|
||||||
main_window.show();
|
main_window.show();
|
||||||
return app.exec();
|
return app.exec();
|
||||||
|
@ -56,6 +56,8 @@ struct Values {
|
|||||||
std::vector<Shortcut> shortcuts;
|
std::vector<Shortcut> shortcuts;
|
||||||
|
|
||||||
uint32_t callout_flags;
|
uint32_t callout_flags;
|
||||||
|
|
||||||
|
bool show_console;
|
||||||
};
|
};
|
||||||
|
|
||||||
extern Values values;
|
extern Values values;
|
||||||
|
@ -93,7 +93,7 @@ endif()
|
|||||||
|
|
||||||
create_target_directory_groups(common)
|
create_target_directory_groups(common)
|
||||||
|
|
||||||
target_link_libraries(common PUBLIC Boost::boost microprofile)
|
target_link_libraries(common PUBLIC Boost::boost fmt microprofile)
|
||||||
if (ARCHITECTURE_x86_64)
|
if (ARCHITECTURE_x86_64)
|
||||||
target_link_libraries(common PRIVATE xbyak)
|
target_link_libraries(common PRIVATE xbyak)
|
||||||
endif()
|
endif()
|
||||||
|
@ -36,8 +36,12 @@
|
|||||||
#define SDMC_DIR "sdmc"
|
#define SDMC_DIR "sdmc"
|
||||||
#define NAND_DIR "nand"
|
#define NAND_DIR "nand"
|
||||||
#define SYSDATA_DIR "sysdata"
|
#define SYSDATA_DIR "sysdata"
|
||||||
|
#define LOG_DIR "log"
|
||||||
|
|
||||||
// Filenames
|
// Filenames
|
||||||
|
// Files in the directory returned by GetUserPath(D_LOGS_IDX)
|
||||||
|
#define LOG_FILE "citra_log.txt"
|
||||||
|
|
||||||
// Files in the directory returned by GetUserPath(D_CONFIG_IDX)
|
// Files in the directory returned by GetUserPath(D_CONFIG_IDX)
|
||||||
#define EMU_CONFIG "emu.ini"
|
#define EMU_CONFIG "emu.ini"
|
||||||
#define DEBUGGER_CONFIG "debugger.ini"
|
#define DEBUGGER_CONFIG "debugger.ini"
|
||||||
|
@ -715,6 +715,8 @@ const std::string& GetUserPath(const unsigned int DirIDX, const std::string& new
|
|||||||
paths[D_SDMC_IDX] = paths[D_USER_IDX] + SDMC_DIR DIR_SEP;
|
paths[D_SDMC_IDX] = paths[D_USER_IDX] + SDMC_DIR DIR_SEP;
|
||||||
paths[D_NAND_IDX] = paths[D_USER_IDX] + NAND_DIR DIR_SEP;
|
paths[D_NAND_IDX] = paths[D_USER_IDX] + NAND_DIR DIR_SEP;
|
||||||
paths[D_SYSDATA_IDX] = paths[D_USER_IDX] + SYSDATA_DIR DIR_SEP;
|
paths[D_SYSDATA_IDX] = paths[D_USER_IDX] + SYSDATA_DIR DIR_SEP;
|
||||||
|
// TODO: Put the logs in a better location for each OS
|
||||||
|
paths[D_LOGS_IDX] = paths[D_USER_IDX] + LOG_DIR DIR_SEP;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newPath.empty()) {
|
if (!newPath.empty()) {
|
||||||
@ -873,20 +875,19 @@ bool IOFile::Flush() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool IOFile::Resize(u64 size) {
|
bool IOFile::Resize(u64 size) {
|
||||||
if (!IsOpen() ||
|
if (!IsOpen() || 0 !=
|
||||||
0 !=
|
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
// ector: _chsize sucks, not 64-bit safe
|
// ector: _chsize sucks, not 64-bit safe
|
||||||
// F|RES: changed to _chsize_s. i think it is 64-bit safe
|
// F|RES: changed to _chsize_s. i think it is 64-bit safe
|
||||||
_chsize_s(_fileno(m_file), size)
|
_chsize_s(_fileno(m_file), size)
|
||||||
#else
|
#else
|
||||||
// TODO: handle 64bit and growing
|
// TODO: handle 64bit and growing
|
||||||
ftruncate(fileno(m_file), size)
|
ftruncate(fileno(m_file), size)
|
||||||
#endif
|
#endif
|
||||||
)
|
)
|
||||||
m_good = false;
|
m_good = false;
|
||||||
|
|
||||||
return m_good;
|
return m_good;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace FileUtil
|
||||||
|
@ -224,6 +224,10 @@ public:
|
|||||||
return WriteArray(&object, 1);
|
return WriteArray(&object, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
size_t WriteString(const std::string& str) {
|
||||||
|
return WriteArray(str.c_str(), str.length());
|
||||||
|
}
|
||||||
|
|
||||||
bool IsOpen() const {
|
bool IsOpen() const {
|
||||||
return nullptr != m_file;
|
return nullptr != m_file;
|
||||||
}
|
}
|
||||||
@ -253,7 +257,7 @@ private:
|
|||||||
bool m_good = true;
|
bool m_good = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace FileUtil
|
||||||
|
|
||||||
// To deal with Windows being dumb at unicode:
|
// To deal with Windows being dumb at unicode:
|
||||||
template <typename T>
|
template <typename T>
|
||||||
|
@ -4,16 +4,108 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <chrono>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <future>
|
||||||
|
#include <memory>
|
||||||
|
#include <thread>
|
||||||
#include "common/assert.h"
|
#include "common/assert.h"
|
||||||
#include "common/common_funcs.h" // snprintf compatibility define
|
#include "common/common_funcs.h" // snprintf compatibility define
|
||||||
#include "common/logging/backend.h"
|
#include "common/logging/backend.h"
|
||||||
#include "common/logging/filter.h"
|
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "common/logging/text_formatter.h"
|
#include "common/logging/text_formatter.h"
|
||||||
|
#include "common/string_util.h"
|
||||||
|
#include "common/threadsafe_queue.h"
|
||||||
|
|
||||||
namespace Log {
|
namespace Log {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static state as a singleton.
|
||||||
|
*/
|
||||||
|
class Impl {
|
||||||
|
public:
|
||||||
|
static Impl& Instance() {
|
||||||
|
static Impl backend;
|
||||||
|
return backend;
|
||||||
|
}
|
||||||
|
|
||||||
|
Impl(Impl const&) = delete;
|
||||||
|
const Impl& operator=(Impl const&) = delete;
|
||||||
|
|
||||||
|
Common::MPSCQueue<Log::Entry>& GetQueue() {
|
||||||
|
return message_queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AddBackend(std::unique_ptr<Backend> backend) {
|
||||||
|
backends.push_back(std::move(backend));
|
||||||
|
}
|
||||||
|
|
||||||
|
void RemoveBackend(const std::string& backend_name) {
|
||||||
|
std::remove_if(backends.begin(), backends.end(),
|
||||||
|
[&backend_name](const auto& i) { return i->GetName() == backend_name; });
|
||||||
|
}
|
||||||
|
|
||||||
|
const Filter& GetGlobalFilter() const {
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetGlobalFilter(const Filter& f) {
|
||||||
|
filter = f;
|
||||||
|
}
|
||||||
|
|
||||||
|
Backend* GetBackend(const std::string& backend_name) {
|
||||||
|
auto it = std::find_if(backends.begin(), backends.end(), [&backend_name](const auto& i) {
|
||||||
|
return i->GetName() == backend_name;
|
||||||
|
});
|
||||||
|
if (it == backends.end())
|
||||||
|
return nullptr;
|
||||||
|
return it->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Impl() : running(true) {
|
||||||
|
backend_thread = std::async(std::launch::async, [&] {
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
Entry entry;
|
||||||
|
while (running) {
|
||||||
|
if (!message_queue.Pop(entry)) {
|
||||||
|
std::this_thread::sleep_for(1ms);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const auto& backend : backends) {
|
||||||
|
backend->Write(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
~Impl() {
|
||||||
|
if (running) {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::atomic_bool running;
|
||||||
|
std::future<void> backend_thread;
|
||||||
|
std::vector<std::unique_ptr<Backend>> backends;
|
||||||
|
Common::MPSCQueue<Log::Entry> message_queue;
|
||||||
|
Filter filter;
|
||||||
|
};
|
||||||
|
|
||||||
|
void ConsoleBackend::Write(const Entry& entry) {
|
||||||
|
PrintMessage(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ColorConsoleBackend::Write(const Entry& entry) {
|
||||||
|
PrintColoredMessage(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FileBackend::Write(const Entry& entry) {
|
||||||
|
if (!file.IsOpen()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
file.WriteString(FormatLogMessage(entry) + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
/// Macro listing all log classes. Code should define CLS and SUB as desired before invoking this.
|
/// Macro listing all log classes. Code should define CLS and SUB as desired before invoking this.
|
||||||
#define ALL_LOG_CLASSES() \
|
#define ALL_LOG_CLASSES() \
|
||||||
CLS(Log) \
|
CLS(Log) \
|
||||||
@ -113,45 +205,65 @@ const char* GetLevelName(Level log_level) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Entry CreateEntry(Class log_class, Level log_level, const char* filename, unsigned int line_nr,
|
Entry CreateEntry(Class log_class, Level log_level, const char* filename, unsigned int line_nr,
|
||||||
const char* function, const char* format, va_list args) {
|
const char* function, const std::string& message) {
|
||||||
using std::chrono::steady_clock;
|
|
||||||
using std::chrono::duration_cast;
|
using std::chrono::duration_cast;
|
||||||
|
using std::chrono::steady_clock;
|
||||||
|
|
||||||
static steady_clock::time_point time_origin = steady_clock::now();
|
static steady_clock::time_point time_origin = steady_clock::now();
|
||||||
|
|
||||||
std::array<char, 4 * 1024> formatting_buffer;
|
|
||||||
|
|
||||||
Entry entry;
|
Entry entry;
|
||||||
entry.timestamp = duration_cast<std::chrono::microseconds>(steady_clock::now() - time_origin);
|
entry.timestamp = duration_cast<std::chrono::microseconds>(steady_clock::now() - time_origin);
|
||||||
entry.log_class = log_class;
|
entry.log_class = log_class;
|
||||||
entry.log_level = log_level;
|
entry.log_level = log_level;
|
||||||
|
entry.filename = std::string(Common::TrimSourcePath(filename));
|
||||||
snprintf(formatting_buffer.data(), formatting_buffer.size(), "%s:%s:%u", filename, function,
|
entry.line_num = line_nr;
|
||||||
line_nr);
|
entry.function = std::string(function);
|
||||||
entry.location = std::string(formatting_buffer.data());
|
entry.message = message;
|
||||||
|
|
||||||
vsnprintf(formatting_buffer.data(), formatting_buffer.size(), format, args);
|
|
||||||
entry.message = std::string(formatting_buffer.data());
|
|
||||||
|
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Filter* filter = nullptr;
|
void SetGlobalFilter(const Filter& filter) {
|
||||||
|
Impl::Instance().SetGlobalFilter(filter);
|
||||||
void SetFilter(Filter* new_filter) {
|
|
||||||
filter = new_filter;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void LogMessage(Class log_class, Level log_level, const char* filename, unsigned int line_nr,
|
void AddBackend(std::unique_ptr<Backend> backend) {
|
||||||
const char* function, const char* format, ...) {
|
Impl::Instance().AddBackend(std::move(backend));
|
||||||
if (filter != nullptr && !filter->CheckMessage(log_class, log_level))
|
}
|
||||||
return;
|
|
||||||
|
|
||||||
|
void RemoveBackend(const std::string& backend_name) {
|
||||||
|
Impl::Instance().RemoveBackend(backend_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Backend* GetBackend(const std::string& backend_name) {
|
||||||
|
return Impl::Instance().GetBackend(backend_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void LogMessage(Class log_class, Level log_level, const char* filename, unsigned int line_num,
|
||||||
|
const char* function, const char* format, ...) {
|
||||||
|
auto filter = Impl::Instance().GetGlobalFilter();
|
||||||
|
if (!filter.CheckMessage(log_class, log_level))
|
||||||
|
return;
|
||||||
|
std::array<char, 4 * 1024> formatting_buffer;
|
||||||
va_list args;
|
va_list args;
|
||||||
va_start(args, format);
|
va_start(args, format);
|
||||||
Entry entry = CreateEntry(log_class, log_level, filename, line_nr, function, format, args);
|
vsnprintf(formatting_buffer.data(), formatting_buffer.size(), format, args);
|
||||||
va_end(args);
|
va_end(args);
|
||||||
|
Entry entry = CreateEntry(log_class, log_level, filename, line_num, function,
|
||||||
|
std::string(formatting_buffer.data()));
|
||||||
|
|
||||||
PrintColoredMessage(entry);
|
Impl::Instance().GetQueue().Push(std::move(entry));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FmtLogMessage(Class log_class, Level log_level, const char* filename, unsigned int line_num,
|
||||||
|
const char* function, const char* format, fmt::ArgList args) {
|
||||||
|
auto filter = Impl::Instance().GetGlobalFilter();
|
||||||
|
if (!filter.CheckMessage(log_class, log_level))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Entry entry =
|
||||||
|
CreateEntry(log_class, log_level, filename, line_num, function, fmt::format(format, args));
|
||||||
|
|
||||||
|
Impl::Instance().GetQueue().Push(std::move(entry));
|
||||||
}
|
}
|
||||||
|
} // namespace Log
|
||||||
|
@ -6,8 +6,11 @@
|
|||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdarg>
|
#include <cstdarg>
|
||||||
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include "common/file_util.h"
|
||||||
|
#include "common/logging/filter.h"
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
|
|
||||||
namespace Log {
|
namespace Log {
|
||||||
@ -22,15 +25,80 @@ struct Entry {
|
|||||||
std::chrono::microseconds timestamp;
|
std::chrono::microseconds timestamp;
|
||||||
Class log_class;
|
Class log_class;
|
||||||
Level log_level;
|
Level log_level;
|
||||||
std::string location;
|
std::string filename;
|
||||||
|
unsigned int line_num;
|
||||||
|
std::string function;
|
||||||
std::string message;
|
std::string message;
|
||||||
|
|
||||||
Entry() = default;
|
Entry() = default;
|
||||||
Entry(Entry&& o) = default;
|
Entry(Entry&& o) = default;
|
||||||
|
|
||||||
Entry& operator=(Entry&& o) = default;
|
Entry& operator=(Entry&& o) = default;
|
||||||
|
Entry& operator=(const Entry& o) = default;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for logging backends. As loggers can be created and removed at runtime, this can be
|
||||||
|
* used by a frontend for adding a custom logging backend as needed
|
||||||
|
*/
|
||||||
|
class Backend {
|
||||||
|
public:
|
||||||
|
virtual ~Backend() = default;
|
||||||
|
virtual void SetFilter(const Filter& new_filter) {
|
||||||
|
filter = new_filter;
|
||||||
|
}
|
||||||
|
virtual const char* GetName() const = 0;
|
||||||
|
virtual void Write(const Entry& entry) = 0;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Filter filter;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backend that writes to stderr without any color commands
|
||||||
|
*/
|
||||||
|
class ConsoleBackend : public Backend {
|
||||||
|
public:
|
||||||
|
const char* GetName() const override {
|
||||||
|
return "console";
|
||||||
|
}
|
||||||
|
void Write(const Entry& entry) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backend that writes to stderr and with color
|
||||||
|
*/
|
||||||
|
class ColorConsoleBackend : public Backend {
|
||||||
|
public:
|
||||||
|
const char* GetName() const override {
|
||||||
|
return "color_console";
|
||||||
|
}
|
||||||
|
void Write(const Entry& entry) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backend that writes to a file passed into the constructor
|
||||||
|
*/
|
||||||
|
class FileBackend : public Backend {
|
||||||
|
public:
|
||||||
|
FileBackend(const std::string& filename) : file(filename, "w") {}
|
||||||
|
|
||||||
|
const char* GetName() const override {
|
||||||
|
return "file";
|
||||||
|
}
|
||||||
|
|
||||||
|
void Write(const Entry& entry) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
FileUtil::IOFile file;
|
||||||
|
};
|
||||||
|
|
||||||
|
void AddBackend(std::unique_ptr<Backend> backend);
|
||||||
|
|
||||||
|
void RemoveBackend(const std::string& backend_name);
|
||||||
|
|
||||||
|
Backend* GetBackend(const std::string& backend_name);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the name of the passed log class as a C-string. Subclasses are separated by periods
|
* Returns the name of the passed log class as a C-string. Subclasses are separated by periods
|
||||||
* instead of underscores as in the enumeration.
|
* instead of underscores as in the enumeration.
|
||||||
@ -44,7 +112,13 @@ const char* GetLevelName(Level log_level);
|
|||||||
|
|
||||||
/// Creates a log entry by formatting the given source location, and message.
|
/// Creates a log entry by formatting the given source location, and message.
|
||||||
Entry CreateEntry(Class log_class, Level log_level, const char* filename, unsigned int line_nr,
|
Entry CreateEntry(Class log_class, Level log_level, const char* filename, unsigned int line_nr,
|
||||||
const char* function, const char* format, va_list args);
|
const char* function, const char* format, const std::string& message);
|
||||||
|
|
||||||
void SetFilter(Filter* filter);
|
/**
|
||||||
}
|
* The global filter will prevent any messages from even being processed if they are filtered. Each
|
||||||
|
* backend can have a filter, but if the level is lower than the global filter, the backend will
|
||||||
|
* never get the message
|
||||||
|
*/
|
||||||
|
void SetGlobalFilter(const Filter& filter);
|
||||||
|
|
||||||
|
} // namespace Log
|
||||||
|
@ -19,7 +19,7 @@ namespace Log {
|
|||||||
class Filter {
|
class Filter {
|
||||||
public:
|
public:
|
||||||
/// Initializes the filter with all classes having `default_level` as the minimum level.
|
/// Initializes the filter with all classes having `default_level` as the minimum level.
|
||||||
Filter(Level default_level);
|
Filter(Level default_level = Level::Info);
|
||||||
|
|
||||||
/// Resets the filter so that all classes have `level` as the minimum displayed level.
|
/// Resets the filter so that all classes have `level` as the minimum displayed level.
|
||||||
void ResetAll(Level level);
|
void ResetAll(Level level);
|
||||||
@ -50,4 +50,4 @@ public:
|
|||||||
private:
|
private:
|
||||||
std::array<Level, (size_t)Class::Count> class_levels;
|
std::array<Level, (size_t)Class::Count> class_levels;
|
||||||
};
|
};
|
||||||
}
|
} // namespace Log
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <fmt/format.h>
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
|
||||||
namespace Log {
|
namespace Log {
|
||||||
@ -98,7 +99,7 @@ enum class Class : ClassType {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Logs a message to the global logger.
|
/// Logs a message to the global logger.
|
||||||
void LogMessage(Class log_class, Level log_level, const char* filename, unsigned int line_nr,
|
void LogMessage(Class log_class, Level log_level, const char* filename, unsigned int line_num,
|
||||||
const char* function,
|
const char* function,
|
||||||
#ifdef _MSC_VER
|
#ifdef _MSC_VER
|
||||||
_Printf_format_string_
|
_Printf_format_string_
|
||||||
@ -110,6 +111,12 @@ void LogMessage(Class log_class, Level log_level, const char* filename, unsigned
|
|||||||
#endif
|
#endif
|
||||||
;
|
;
|
||||||
|
|
||||||
|
/// Logs a message to the global logger, this time with 100% moar fmtlib
|
||||||
|
void FmtLogMessage(Class log_class, Level log_level, const char* filename, unsigned int line_num,
|
||||||
|
const char* function, const char* format, fmt::ArgList);
|
||||||
|
|
||||||
|
FMT_VARIADIC(void, FmtLogMessage, Class, Level, const char*, unsigned int, const char*, const char*)
|
||||||
|
|
||||||
} // namespace Log
|
} // namespace Log
|
||||||
|
|
||||||
#define LOG_GENERIC(log_class, log_level, ...) \
|
#define LOG_GENERIC(log_class, log_level, ...) \
|
||||||
@ -132,3 +139,28 @@ void LogMessage(Class log_class, Level log_level, const char* filename, unsigned
|
|||||||
LOG_GENERIC(::Log::Class::log_class, ::Log::Level::Error, __VA_ARGS__)
|
LOG_GENERIC(::Log::Class::log_class, ::Log::Level::Error, __VA_ARGS__)
|
||||||
#define LOG_CRITICAL(log_class, ...) \
|
#define LOG_CRITICAL(log_class, ...) \
|
||||||
LOG_GENERIC(::Log::Class::log_class, ::Log::Level::Critical, __VA_ARGS__)
|
LOG_GENERIC(::Log::Class::log_class, ::Log::Level::Critical, __VA_ARGS__)
|
||||||
|
|
||||||
|
// Define the fmt lib macros
|
||||||
|
#ifdef _DEBUG
|
||||||
|
#define NGLOG_TRACE(log_class, fmt, ...) \
|
||||||
|
::Log::FmtLogMessage(::Log::Class::log_class, ::Log::Level::Trace, __FILE__, __LINE__, \
|
||||||
|
__func__, fmt, ##__VA_ARGS__)
|
||||||
|
#else
|
||||||
|
#define NGLOG_TRACE(log_class, fmt, ...) (void(0))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define NGLOG_DEBUG(log_class, fmt, ...) \
|
||||||
|
::Log::FmtLogMessage(::Log::Class::log_class, ::Log::Level::Debug, __FILE__, __LINE__, \
|
||||||
|
__func__, fmt, ##__VA_ARGS__)
|
||||||
|
#define NGLOG_INFO(log_class, fmt, ...) \
|
||||||
|
::Log::FmtLogMessage(::Log::Class::log_class, ::Log::Level::Info, __FILE__, __LINE__, \
|
||||||
|
__func__, fmt, ##__VA_ARGS__)
|
||||||
|
#define NGLOG_WARNING(log_class, fmt, ...) \
|
||||||
|
::Log::FmtLogMessage(::Log::Class::log_class, ::Log::Level::Warning, __FILE__, __LINE__, \
|
||||||
|
__func__, fmt, ##__VA_ARGS__)
|
||||||
|
#define NGLOG_ERROR(log_class, fmt, ...) \
|
||||||
|
::Log::FmtLogMessage(::Log::Class::log_class, ::Log::Level::Error, __FILE__, __LINE__, \
|
||||||
|
__func__, fmt, ##__VA_ARGS__)
|
||||||
|
#define NGLOG_CRITICAL(log_class, fmt, ...) \
|
||||||
|
::Log::FmtLogMessage(::Log::Class::log_class, ::Log::Level::Critical, __FILE__, __LINE__, \
|
||||||
|
__func__, fmt, ##__VA_ARGS__)
|
||||||
|
@ -18,50 +18,26 @@
|
|||||||
|
|
||||||
namespace Log {
|
namespace Log {
|
||||||
|
|
||||||
// TODO(bunnei): This should be moved to a generic path manipulation library
|
std::string FormatLogMessage(const Entry& entry) {
|
||||||
const char* TrimSourcePath(const char* path, const char* root) {
|
|
||||||
const char* p = path;
|
|
||||||
|
|
||||||
while (*p != '\0') {
|
|
||||||
const char* next_slash = p;
|
|
||||||
while (*next_slash != '\0' && *next_slash != '/' && *next_slash != '\\') {
|
|
||||||
++next_slash;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool is_src = Common::ComparePartialString(p, next_slash, root);
|
|
||||||
p = next_slash;
|
|
||||||
|
|
||||||
if (*p != '\0') {
|
|
||||||
++p;
|
|
||||||
}
|
|
||||||
if (is_src) {
|
|
||||||
path = p;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
|
|
||||||
void FormatLogMessage(const Entry& entry, char* out_text, size_t text_len) {
|
|
||||||
unsigned int time_seconds = static_cast<unsigned int>(entry.timestamp.count() / 1000000);
|
unsigned int time_seconds = static_cast<unsigned int>(entry.timestamp.count() / 1000000);
|
||||||
unsigned int time_fractional = static_cast<unsigned int>(entry.timestamp.count() % 1000000);
|
unsigned int time_fractional = static_cast<unsigned int>(entry.timestamp.count() % 1000000);
|
||||||
|
|
||||||
const char* class_name = GetLogClassName(entry.log_class);
|
const char* class_name = GetLogClassName(entry.log_class);
|
||||||
const char* level_name = GetLevelName(entry.log_level);
|
const char* level_name = GetLevelName(entry.log_level);
|
||||||
|
|
||||||
snprintf(out_text, text_len, "[%4u.%06u] %s <%s> %s: %s", time_seconds, time_fractional,
|
return fmt::format("[{:4d}.{:06d}] {} <{}> {}:{}:{}: {}", time_seconds, time_fractional,
|
||||||
class_name, level_name, TrimSourcePath(entry.location.c_str()), entry.message.c_str());
|
class_name, level_name, entry.filename, entry.function, entry.line_num,
|
||||||
|
entry.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PrintMessage(const Entry& entry) {
|
void PrintMessage(const Entry& entry) {
|
||||||
std::array<char, 4 * 1024> format_buffer;
|
auto str = FormatLogMessage(entry) + "\n";
|
||||||
FormatLogMessage(entry, format_buffer.data(), format_buffer.size());
|
fputs(str.c_str(), stderr);
|
||||||
fputs(format_buffer.data(), stderr);
|
|
||||||
fputc('\n', stderr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PrintColoredMessage(const Entry& entry) {
|
void PrintColoredMessage(const Entry& entry) {
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
static HANDLE console_handle = GetStdHandle(STD_ERROR_HANDLE);
|
HANDLE console_handle = GetStdHandle(STD_ERROR_HANDLE);
|
||||||
|
|
||||||
CONSOLE_SCREEN_BUFFER_INFO original_info = {0};
|
CONSOLE_SCREEN_BUFFER_INFO original_info = {0};
|
||||||
GetConsoleScreenBufferInfo(console_handle, &original_info);
|
GetConsoleScreenBufferInfo(console_handle, &original_info);
|
||||||
@ -129,4 +105,4 @@ void PrintColoredMessage(const Entry& entry) {
|
|||||||
#undef ESC
|
#undef ESC
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
} // namespace Log
|
||||||
|
@ -10,22 +10,10 @@ namespace Log {
|
|||||||
|
|
||||||
struct Entry;
|
struct Entry;
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to trim an arbitrary prefix from `path`, leaving only the part starting at `root`. It's
|
|
||||||
* intended to be used to strip a system-specific build directory from the `__FILE__` macro,
|
|
||||||
* leaving only the path relative to the sources root.
|
|
||||||
*
|
|
||||||
* @param path The input file path as a null-terminated string
|
|
||||||
* @param root The name of the root source directory as a null-terminated string. Path up to and
|
|
||||||
* including the last occurrence of this name will be stripped
|
|
||||||
* @return A pointer to the same string passed as `path`, but starting at the trimmed portion
|
|
||||||
*/
|
|
||||||
const char* TrimSourcePath(const char* path, const char* root = "src");
|
|
||||||
|
|
||||||
/// Formats a log entry into the provided text buffer.
|
/// Formats a log entry into the provided text buffer.
|
||||||
void FormatLogMessage(const Entry& entry, char* out_text, size_t text_len);
|
std::string FormatLogMessage(const Entry& entry);
|
||||||
/// Formats and prints a log entry to stderr.
|
/// Formats and prints a log entry to stderr.
|
||||||
void PrintMessage(const Entry& entry);
|
void PrintMessage(const Entry& entry);
|
||||||
/// Prints the same message as `PrintMessage`, but colored acoording to the severity level.
|
/// Prints the same message as `PrintMessage`, but colored acoording to the severity level.
|
||||||
void PrintColoredMessage(const Entry& entry);
|
void PrintColoredMessage(const Entry& entry);
|
||||||
}
|
} // namespace Log
|
||||||
|
@ -202,7 +202,7 @@ bool SplitPath(const std::string& full_path, std::string* _pPath, std::string* _
|
|||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
":"
|
":"
|
||||||
#endif
|
#endif
|
||||||
);
|
);
|
||||||
if (std::string::npos == dir_end)
|
if (std::string::npos == dir_end)
|
||||||
dir_end = 0;
|
dir_end = 0;
|
||||||
else
|
else
|
||||||
@ -462,4 +462,26 @@ std::string StringFromFixedZeroTerminatedBuffer(const char* buffer, size_t max_l
|
|||||||
|
|
||||||
return std::string(buffer, len);
|
return std::string(buffer, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const char* TrimSourcePath(const char* path, const char* root) {
|
||||||
|
const char* p = path;
|
||||||
|
|
||||||
|
while (*p != '\0') {
|
||||||
|
const char* next_slash = p;
|
||||||
|
while (*next_slash != '\0' && *next_slash != '/' && *next_slash != '\\') {
|
||||||
|
++next_slash;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_src = Common::ComparePartialString(p, next_slash, root);
|
||||||
|
p = next_slash;
|
||||||
|
|
||||||
|
if (*p != '\0') {
|
||||||
|
++p;
|
||||||
|
}
|
||||||
|
if (is_src) {
|
||||||
|
path = p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path;
|
||||||
}
|
}
|
||||||
|
} // namespace Common
|
||||||
|
@ -134,4 +134,17 @@ bool ComparePartialString(InIt begin, InIt end, const char* other) {
|
|||||||
* NUL-terminated then the string ends at max_len characters.
|
* NUL-terminated then the string ends at max_len characters.
|
||||||
*/
|
*/
|
||||||
std::string StringFromFixedZeroTerminatedBuffer(const char* buffer, size_t max_len);
|
std::string StringFromFixedZeroTerminatedBuffer(const char* buffer, size_t max_len);
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Attempts to trim an arbitrary prefix from `path`, leaving only the part starting at `root`. It's
|
||||||
|
* intended to be used to strip a system-specific build directory from the `__FILE__` macro,
|
||||||
|
* leaving only the path relative to the sources root.
|
||||||
|
*
|
||||||
|
* @param path The input file path as a null-terminated string
|
||||||
|
* @param root The name of the root source directory as a null-terminated string. Path up to and
|
||||||
|
* including the last occurrence of this name will be stripped
|
||||||
|
* @return A pointer to the same string passed as `path`, but starting at the trimmed portion
|
||||||
|
*/
|
||||||
|
const char* TrimSourcePath(const char* path, const char* root = "src");
|
||||||
|
|
||||||
|
} // namespace Common
|
||||||
|
@ -4,14 +4,13 @@
|
|||||||
|
|
||||||
#include "audio_core/dsp_interface.h"
|
#include "audio_core/dsp_interface.h"
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
|
#include "core/frontend/emu_window.h"
|
||||||
#include "core/gdbstub/gdbstub.h"
|
#include "core/gdbstub/gdbstub.h"
|
||||||
#include "core/hle/service/hid/hid.h"
|
#include "core/hle/service/hid/hid.h"
|
||||||
#include "core/hle/service/ir/ir.h"
|
#include "core/hle/service/ir/ir.h"
|
||||||
#include "core/settings.h"
|
#include "core/settings.h"
|
||||||
#include "video_core/video_core.h"
|
#include "video_core/video_core.h"
|
||||||
|
|
||||||
#include "core/frontend/emu_window.h"
|
|
||||||
|
|
||||||
namespace Settings {
|
namespace Settings {
|
||||||
|
|
||||||
Values values = {};
|
Values values = {};
|
||||||
|
@ -54,9 +54,21 @@ constexpr int NUM_BUTTONS_IR = BUTTON_IR_END - BUTTON_IR_BEGIN;
|
|||||||
constexpr int NUM_BUTTONS_NS = BUTTON_NS_END - BUTTON_NS_BEGIN;
|
constexpr int NUM_BUTTONS_NS = BUTTON_NS_END - BUTTON_NS_BEGIN;
|
||||||
|
|
||||||
static const std::array<const char*, NumButtons> mapping = {{
|
static const std::array<const char*, NumButtons> mapping = {{
|
||||||
"button_a", "button_b", "button_x", "button_y", "button_up", "button_down", "button_left",
|
"button_a",
|
||||||
"button_right", "button_l", "button_r", "button_start", "button_select", "button_zl",
|
"button_b",
|
||||||
"button_zr", "button_home",
|
"button_x",
|
||||||
|
"button_y",
|
||||||
|
"button_up",
|
||||||
|
"button_down",
|
||||||
|
"button_left",
|
||||||
|
"button_right",
|
||||||
|
"button_l",
|
||||||
|
"button_r",
|
||||||
|
"button_start",
|
||||||
|
"button_select",
|
||||||
|
"button_zl",
|
||||||
|
"button_zr",
|
||||||
|
"button_home",
|
||||||
}};
|
}};
|
||||||
} // namespace NativeButton
|
} // namespace NativeButton
|
||||||
|
|
||||||
@ -69,7 +81,8 @@ enum Values {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static const std::array<const char*, NumAnalogs> mapping = {{
|
static const std::array<const char*, NumAnalogs> mapping = {{
|
||||||
"circle_pad", "c_stick",
|
"circle_pad",
|
||||||
|
"c_stick",
|
||||||
}};
|
}};
|
||||||
} // namespace NativeAnalog
|
} // namespace NativeAnalog
|
||||||
|
|
||||||
@ -116,8 +129,6 @@ struct Values {
|
|||||||
float bg_green;
|
float bg_green;
|
||||||
float bg_blue;
|
float bg_blue;
|
||||||
|
|
||||||
std::string log_filter;
|
|
||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
std::string sink_id;
|
std::string sink_id;
|
||||||
bool enable_audio_stretching;
|
bool enable_audio_stretching;
|
||||||
@ -130,6 +141,7 @@ struct Values {
|
|||||||
// Debugging
|
// Debugging
|
||||||
bool use_gdbstub;
|
bool use_gdbstub;
|
||||||
u16 gdbstub_port;
|
u16 gdbstub_port;
|
||||||
|
std::string log_filter;
|
||||||
|
|
||||||
// Movie
|
// Movie
|
||||||
std::string movie_play;
|
std::string movie_play;
|
||||||
|
Loading…
Reference in New Issue
Block a user