forked from eden-emu/eden
		
	Merge pull request #6460 from Morph1984/fs-access-log-fix
fsp_srv: Fix filesystem access logging
This commit is contained in:
		
						commit
						9442c70be1
					
				
					 12 changed files with 64 additions and 44 deletions
				
			
		|  | @ -183,10 +183,6 @@ size_t WriteStringToFile(const std::filesystem::path& path, FileType type, | ||||||
| 
 | 
 | ||||||
| size_t AppendStringToFile(const std::filesystem::path& path, FileType type, | size_t AppendStringToFile(const std::filesystem::path& path, FileType type, | ||||||
|                           std::string_view string) { |                           std::string_view string) { | ||||||
|     if (!Exists(path)) { |  | ||||||
|         return WriteStringToFile(path, type, string); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (!IsFile(path)) { |     if (!IsFile(path)) { | ||||||
|         return 0; |         return 0; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -71,7 +71,7 @@ template <typename Path> | ||||||
| 
 | 
 | ||||||
| /**
 | /**
 | ||||||
|  * Writes a string to a file at path and returns the number of characters successfully written. |  * Writes a string to a file at path and returns the number of characters successfully written. | ||||||
|  * If an file already exists at path, its contents will be erased. |  * If a file already exists at path, its contents will be erased. | ||||||
|  * If the filesystem object at path is not a file, this function returns 0. |  * If the filesystem object at path is not a file, this function returns 0. | ||||||
|  * |  * | ||||||
|  * @param path Filesystem path |  * @param path Filesystem path | ||||||
|  | @ -95,7 +95,6 @@ template <typename Path> | ||||||
| 
 | 
 | ||||||
| /**
 | /**
 | ||||||
|  * Appends a string to a file at path and returns the number of characters successfully written. |  * Appends a string to a file at path and returns the number of characters successfully written. | ||||||
|  * If a file does not exist at path, WriteStringToFile is called instead. |  | ||||||
|  * If the filesystem object at path is not a file, this function returns 0. |  * If the filesystem object at path is not a file, this function returns 0. | ||||||
|  * |  * | ||||||
|  * @param path Filesystem path |  * @param path Filesystem path | ||||||
|  |  | ||||||
|  | @ -218,6 +218,7 @@ struct Values { | ||||||
|     std::string program_args; |     std::string program_args; | ||||||
|     bool dump_exefs; |     bool dump_exefs; | ||||||
|     bool dump_nso; |     bool dump_nso; | ||||||
|  |     bool enable_fs_access_log; | ||||||
|     bool reporting_services; |     bool reporting_services; | ||||||
|     bool quest_flag; |     bool quest_flag; | ||||||
|     bool disable_macro_jit; |     bool disable_macro_jit; | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ | ||||||
| #include "common/common_types.h" | #include "common/common_types.h" | ||||||
| #include "common/hex_util.h" | #include "common/hex_util.h" | ||||||
| #include "common/logging/log.h" | #include "common/logging/log.h" | ||||||
|  | #include "common/settings.h" | ||||||
| #include "common/string_util.h" | #include "common/string_util.h" | ||||||
| #include "core/core.h" | #include "core/core.h" | ||||||
| #include "core/file_sys/directory.h" | #include "core/file_sys/directory.h" | ||||||
|  | @ -785,6 +786,10 @@ FSP_SRV::FSP_SRV(Core::System& system_) | ||||||
|     }; |     }; | ||||||
|     // clang-format on
 |     // clang-format on
 | ||||||
|     RegisterHandlers(functions); |     RegisterHandlers(functions); | ||||||
|  | 
 | ||||||
|  |     if (Settings::values.enable_fs_access_log) { | ||||||
|  |         access_log_mode = AccessLogMode::SdCard; | ||||||
|  |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| FSP_SRV::~FSP_SRV() = default; | FSP_SRV::~FSP_SRV() = default; | ||||||
|  | @ -1041,9 +1046,9 @@ void FSP_SRV::DisableAutoSaveDataCreation(Kernel::HLERequestContext& ctx) { | ||||||
| 
 | 
 | ||||||
| void FSP_SRV::SetGlobalAccessLogMode(Kernel::HLERequestContext& ctx) { | void FSP_SRV::SetGlobalAccessLogMode(Kernel::HLERequestContext& ctx) { | ||||||
|     IPC::RequestParser rp{ctx}; |     IPC::RequestParser rp{ctx}; | ||||||
|     log_mode = rp.PopEnum<LogMode>(); |     access_log_mode = rp.PopEnum<AccessLogMode>(); | ||||||
| 
 | 
 | ||||||
|     LOG_DEBUG(Service_FS, "called, log_mode={:08X}", log_mode); |     LOG_DEBUG(Service_FS, "called, access_log_mode={}", access_log_mode); | ||||||
| 
 | 
 | ||||||
|     IPC::ResponseBuilder rb{ctx, 2}; |     IPC::ResponseBuilder rb{ctx, 2}; | ||||||
|     rb.Push(ResultSuccess); |     rb.Push(ResultSuccess); | ||||||
|  | @ -1054,7 +1059,7 @@ void FSP_SRV::GetGlobalAccessLogMode(Kernel::HLERequestContext& ctx) { | ||||||
| 
 | 
 | ||||||
|     IPC::ResponseBuilder rb{ctx, 3}; |     IPC::ResponseBuilder rb{ctx, 3}; | ||||||
|     rb.Push(ResultSuccess); |     rb.Push(ResultSuccess); | ||||||
|     rb.PushEnum(log_mode); |     rb.PushEnum(access_log_mode); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void FSP_SRV::OutputAccessLogToSdCard(Kernel::HLERequestContext& ctx) { | void FSP_SRV::OutputAccessLogToSdCard(Kernel::HLERequestContext& ctx) { | ||||||
|  | @ -1062,9 +1067,9 @@ void FSP_SRV::OutputAccessLogToSdCard(Kernel::HLERequestContext& ctx) { | ||||||
|     auto log = Common::StringFromFixedZeroTerminatedBuffer( |     auto log = Common::StringFromFixedZeroTerminatedBuffer( | ||||||
|         reinterpret_cast<const char*>(raw.data()), raw.size()); |         reinterpret_cast<const char*>(raw.data()), raw.size()); | ||||||
| 
 | 
 | ||||||
|     LOG_DEBUG(Service_FS, "called, log='{}'", log); |     LOG_DEBUG(Service_FS, "called"); | ||||||
| 
 | 
 | ||||||
|     reporter.SaveFilesystemAccessReport(log_mode, std::move(log)); |     reporter.SaveFSAccessLog(log); | ||||||
| 
 | 
 | ||||||
|     IPC::ResponseBuilder rb{ctx, 2}; |     IPC::ResponseBuilder rb{ctx, 2}; | ||||||
|     rb.Push(ResultSuccess); |     rb.Push(ResultSuccess); | ||||||
|  |  | ||||||
|  | @ -24,11 +24,10 @@ enum class AccessLogVersion : u32 { | ||||||
|     Latest = V7_0_0, |     Latest = V7_0_0, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| enum class LogMode : u32 { | enum class AccessLogMode : u32 { | ||||||
|     Off, |     None, | ||||||
|     Log, |     Log, | ||||||
|     RedirectToSdCard, |     SdCard, | ||||||
|     LogToSdCard = Log | RedirectToSdCard, |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| class FSP_SRV final : public ServiceFramework<FSP_SRV> { | class FSP_SRV final : public ServiceFramework<FSP_SRV> { | ||||||
|  | @ -59,13 +58,12 @@ private: | ||||||
| 
 | 
 | ||||||
|     FileSystemController& fsc; |     FileSystemController& fsc; | ||||||
|     const FileSys::ContentProvider& content_provider; |     const FileSys::ContentProvider& content_provider; | ||||||
|  |     const Core::Reporter& reporter; | ||||||
| 
 | 
 | ||||||
|     FileSys::VirtualFile romfs; |     FileSys::VirtualFile romfs; | ||||||
|     u64 current_process_id = 0; |     u64 current_process_id = 0; | ||||||
|     u32 access_log_program_index = 0; |     u32 access_log_program_index = 0; | ||||||
|     LogMode log_mode = LogMode::LogToSdCard; |     AccessLogMode access_log_mode = AccessLogMode::None; | ||||||
| 
 |  | ||||||
|     const Core::Reporter& reporter; |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| } // namespace Service::FileSystem
 | } // namespace Service::FileSystem
 | ||||||
|  |  | ||||||
|  | @ -195,7 +195,9 @@ json GetHLERequestContextData(Kernel::HLERequestContext& ctx, Core::Memory::Memo | ||||||
| 
 | 
 | ||||||
| namespace Core { | namespace Core { | ||||||
| 
 | 
 | ||||||
| Reporter::Reporter(System& system_) : system(system_) {} | Reporter::Reporter(System& system_) : system(system_) { | ||||||
|  |     ClearFSAccessLog(); | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| Reporter::~Reporter() = default; | Reporter::~Reporter() = default; | ||||||
| 
 | 
 | ||||||
|  | @ -362,22 +364,12 @@ void Reporter::SaveErrorReport(u64 title_id, ResultCode result, | ||||||
|     SaveToFile(std::move(out), GetPath("error_report", title_id, timestamp)); |     SaveToFile(std::move(out), GetPath("error_report", title_id, timestamp)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void Reporter::SaveFilesystemAccessReport(Service::FileSystem::LogMode log_mode, | void Reporter::SaveFSAccessLog(std::string_view log_message) const { | ||||||
|                                           std::string log_message) const { |     const auto access_log_path = | ||||||
|     if (!IsReportingEnabled()) |         Common::FS::GetYuzuPath(Common::FS::YuzuPath::SDMCDir) / "FsAccessLog.txt"; | ||||||
|         return; |  | ||||||
| 
 | 
 | ||||||
|     const auto timestamp = GetTimestamp(); |     void(Common::FS::AppendStringToFile(access_log_path, Common::FS::FileType::TextFile, | ||||||
|     const auto title_id = system.CurrentProcess()->GetTitleID(); |                                         log_message)); | ||||||
|     json out; |  | ||||||
| 
 |  | ||||||
|     out["yuzu_version"] = GetYuzuVersionData(); |  | ||||||
|     out["report_common"] = GetReportCommonData(title_id, ResultSuccess, timestamp); |  | ||||||
| 
 |  | ||||||
|     out["log_mode"] = fmt::format("{:08X}", static_cast<u32>(log_mode)); |  | ||||||
|     out["log_message"] = std::move(log_message); |  | ||||||
| 
 |  | ||||||
|     SaveToFile(std::move(out), GetPath("filesystem_access_report", title_id, timestamp)); |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void Reporter::SaveUserReport() const { | void Reporter::SaveUserReport() const { | ||||||
|  | @ -392,6 +384,18 @@ void Reporter::SaveUserReport() const { | ||||||
|                GetPath("user_report", title_id, timestamp)); |                GetPath("user_report", title_id, timestamp)); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | void Reporter::ClearFSAccessLog() const { | ||||||
|  |     const auto access_log_path = | ||||||
|  |         Common::FS::GetYuzuPath(Common::FS::YuzuPath::SDMCDir) / "FsAccessLog.txt"; | ||||||
|  | 
 | ||||||
|  |     Common::FS::IOFile access_log_file{access_log_path, Common::FS::FileAccessMode::Write, | ||||||
|  |                                        Common::FS::FileType::TextFile}; | ||||||
|  | 
 | ||||||
|  |     if (!access_log_file.IsOpen()) { | ||||||
|  |         LOG_ERROR(Common_Filesystem, "Failed to clear the filesystem access log."); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
| bool Reporter::IsReportingEnabled() const { | bool Reporter::IsReportingEnabled() const { | ||||||
|     return Settings::values.reporting_services; |     return Settings::values.reporting_services; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -16,10 +16,6 @@ namespace Kernel { | ||||||
| class HLERequestContext; | class HLERequestContext; | ||||||
| } // namespace Kernel
 | } // namespace Kernel
 | ||||||
| 
 | 
 | ||||||
| namespace Service::FileSystem { |  | ||||||
| enum class LogMode : u32; |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| namespace Service::LM { | namespace Service::LM { | ||||||
| struct LogMessage; | struct LogMessage; | ||||||
| } // namespace Service::LM
 | } // namespace Service::LM
 | ||||||
|  | @ -69,14 +65,15 @@ public: | ||||||
|                          std::optional<std::string> custom_text_main = {}, |                          std::optional<std::string> custom_text_main = {}, | ||||||
|                          std::optional<std::string> custom_text_detail = {}) const; |                          std::optional<std::string> custom_text_detail = {}) const; | ||||||
| 
 | 
 | ||||||
|     void SaveFilesystemAccessReport(Service::FileSystem::LogMode log_mode, |     void SaveFSAccessLog(std::string_view log_message) const; | ||||||
|                                     std::string log_message) const; |  | ||||||
| 
 | 
 | ||||||
|     // Can be used anywhere to generate a backtrace and general info report at any point during
 |     // Can be used anywhere to generate a backtrace and general info report at any point during
 | ||||||
|     // execution. Not intended to be used for anything other than debugging or testing.
 |     // execution. Not intended to be used for anything other than debugging or testing.
 | ||||||
|     void SaveUserReport() const; |     void SaveUserReport() const; | ||||||
| 
 | 
 | ||||||
| private: | private: | ||||||
|  |     void ClearFSAccessLog() const; | ||||||
|  | 
 | ||||||
|     bool IsReportingEnabled() const; |     bool IsReportingEnabled() const; | ||||||
| 
 | 
 | ||||||
|     System& system; |     System& system; | ||||||
|  |  | ||||||
|  | @ -647,6 +647,8 @@ void Config::ReadDebuggingValues() { | ||||||
|         ReadSetting(QStringLiteral("program_args"), QString{}).toString().toStdString(); |         ReadSetting(QStringLiteral("program_args"), QString{}).toString().toStdString(); | ||||||
|     Settings::values.dump_exefs = ReadSetting(QStringLiteral("dump_exefs"), false).toBool(); |     Settings::values.dump_exefs = ReadSetting(QStringLiteral("dump_exefs"), false).toBool(); | ||||||
|     Settings::values.dump_nso = ReadSetting(QStringLiteral("dump_nso"), false).toBool(); |     Settings::values.dump_nso = ReadSetting(QStringLiteral("dump_nso"), false).toBool(); | ||||||
|  |     Settings::values.enable_fs_access_log = | ||||||
|  |         ReadSetting(QStringLiteral("enable_fs_access_log"), false).toBool(); | ||||||
|     Settings::values.reporting_services = |     Settings::values.reporting_services = | ||||||
|         ReadSetting(QStringLiteral("reporting_services"), false).toBool(); |         ReadSetting(QStringLiteral("reporting_services"), false).toBool(); | ||||||
|     Settings::values.quest_flag = ReadSetting(QStringLiteral("quest_flag"), false).toBool(); |     Settings::values.quest_flag = ReadSetting(QStringLiteral("quest_flag"), false).toBool(); | ||||||
|  | @ -1258,6 +1260,8 @@ void Config::SaveDebuggingValues() { | ||||||
|                  QString::fromStdString(Settings::values.program_args), QString{}); |                  QString::fromStdString(Settings::values.program_args), QString{}); | ||||||
|     WriteSetting(QStringLiteral("dump_exefs"), Settings::values.dump_exefs, false); |     WriteSetting(QStringLiteral("dump_exefs"), Settings::values.dump_exefs, false); | ||||||
|     WriteSetting(QStringLiteral("dump_nso"), Settings::values.dump_nso, false); |     WriteSetting(QStringLiteral("dump_nso"), Settings::values.dump_nso, false); | ||||||
|  |     WriteSetting(QStringLiteral("enable_fs_access_log"), Settings::values.enable_fs_access_log, | ||||||
|  |                  false); | ||||||
|     WriteSetting(QStringLiteral("quest_flag"), Settings::values.quest_flag, false); |     WriteSetting(QStringLiteral("quest_flag"), Settings::values.quest_flag, false); | ||||||
|     WriteSetting(QStringLiteral("use_debug_asserts"), Settings::values.use_debug_asserts, false); |     WriteSetting(QStringLiteral("use_debug_asserts"), Settings::values.use_debug_asserts, false); | ||||||
|     WriteSetting(QStringLiteral("disable_macro_jit"), Settings::values.disable_macro_jit, false); |     WriteSetting(QStringLiteral("disable_macro_jit"), Settings::values.disable_macro_jit, false); | ||||||
|  |  | ||||||
|  | @ -28,17 +28,21 @@ ConfigureDebug::ConfigureDebug(QWidget* parent) : QWidget(parent), ui(new Ui::Co | ||||||
| ConfigureDebug::~ConfigureDebug() = default; | ConfigureDebug::~ConfigureDebug() = default; | ||||||
| 
 | 
 | ||||||
| void ConfigureDebug::SetConfiguration() { | void ConfigureDebug::SetConfiguration() { | ||||||
|     ui->toggle_console->setEnabled(!Core::System::GetInstance().IsPoweredOn()); |     const bool runtime_lock = !Core::System::GetInstance().IsPoweredOn(); | ||||||
|  | 
 | ||||||
|  |     ui->toggle_console->setEnabled(runtime_lock); | ||||||
|     ui->toggle_console->setChecked(UISettings::values.show_console); |     ui->toggle_console->setChecked(UISettings::values.show_console); | ||||||
|     ui->log_filter_edit->setText(QString::fromStdString(Settings::values.log_filter)); |     ui->log_filter_edit->setText(QString::fromStdString(Settings::values.log_filter)); | ||||||
|     ui->homebrew_args_edit->setText(QString::fromStdString(Settings::values.program_args)); |     ui->homebrew_args_edit->setText(QString::fromStdString(Settings::values.program_args)); | ||||||
|  |     ui->fs_access_log->setEnabled(runtime_lock); | ||||||
|  |     ui->fs_access_log->setChecked(Settings::values.enable_fs_access_log); | ||||||
|     ui->reporting_services->setChecked(Settings::values.reporting_services); |     ui->reporting_services->setChecked(Settings::values.reporting_services); | ||||||
|     ui->quest_flag->setChecked(Settings::values.quest_flag); |     ui->quest_flag->setChecked(Settings::values.quest_flag); | ||||||
|     ui->use_debug_asserts->setChecked(Settings::values.use_debug_asserts); |     ui->use_debug_asserts->setChecked(Settings::values.use_debug_asserts); | ||||||
|     ui->use_auto_stub->setChecked(Settings::values.use_auto_stub); |     ui->use_auto_stub->setChecked(Settings::values.use_auto_stub); | ||||||
|     ui->enable_graphics_debugging->setEnabled(!Core::System::GetInstance().IsPoweredOn()); |     ui->enable_graphics_debugging->setEnabled(runtime_lock); | ||||||
|     ui->enable_graphics_debugging->setChecked(Settings::values.renderer_debug); |     ui->enable_graphics_debugging->setChecked(Settings::values.renderer_debug); | ||||||
|     ui->disable_macro_jit->setEnabled(!Core::System::GetInstance().IsPoweredOn()); |     ui->disable_macro_jit->setEnabled(runtime_lock); | ||||||
|     ui->disable_macro_jit->setChecked(Settings::values.disable_macro_jit); |     ui->disable_macro_jit->setChecked(Settings::values.disable_macro_jit); | ||||||
|     ui->extended_logging->setChecked(Settings::values.extended_logging); |     ui->extended_logging->setChecked(Settings::values.extended_logging); | ||||||
| } | } | ||||||
|  | @ -47,6 +51,7 @@ void ConfigureDebug::ApplyConfiguration() { | ||||||
|     UISettings::values.show_console = ui->toggle_console->isChecked(); |     UISettings::values.show_console = ui->toggle_console->isChecked(); | ||||||
|     Settings::values.log_filter = ui->log_filter_edit->text().toStdString(); |     Settings::values.log_filter = ui->log_filter_edit->text().toStdString(); | ||||||
|     Settings::values.program_args = ui->homebrew_args_edit->text().toStdString(); |     Settings::values.program_args = ui->homebrew_args_edit->text().toStdString(); | ||||||
|  |     Settings::values.enable_fs_access_log = ui->fs_access_log->isChecked(); | ||||||
|     Settings::values.reporting_services = ui->reporting_services->isChecked(); |     Settings::values.reporting_services = ui->reporting_services->isChecked(); | ||||||
|     Settings::values.quest_flag = ui->quest_flag->isChecked(); |     Settings::values.quest_flag = ui->quest_flag->isChecked(); | ||||||
|     Settings::values.use_debug_asserts = ui->use_debug_asserts->isChecked(); |     Settings::values.use_debug_asserts = ui->use_debug_asserts->isChecked(); | ||||||
|  |  | ||||||
|  | @ -144,9 +144,16 @@ | ||||||
|    <item> |    <item> | ||||||
|     <widget class="QGroupBox" name="groupBox_5"> |     <widget class="QGroupBox" name="groupBox_5"> | ||||||
|      <property name="title"> |      <property name="title"> | ||||||
|       <string>Dump</string> |       <string>Debugging</string> | ||||||
|      </property> |      </property> | ||||||
|      <layout class="QVBoxLayout" name="verticalLayout_7"> |      <layout class="QVBoxLayout" name="verticalLayout_7"> | ||||||
|  |       <item> | ||||||
|  |        <widget class="QCheckBox" name="fs_access_log"> | ||||||
|  |         <property name="text"> | ||||||
|  |          <string>Enable FS Access Log</string> | ||||||
|  |         </property> | ||||||
|  |        </widget> | ||||||
|  |       </item> | ||||||
|       <item> |       <item> | ||||||
|        <widget class="QCheckBox" name="reporting_services"> |        <widget class="QCheckBox" name="reporting_services"> | ||||||
|         <property name="text"> |         <property name="text"> | ||||||
|  |  | ||||||
|  | @ -477,6 +477,8 @@ void Config::ReadValues() { | ||||||
|     Settings::values.program_args = sdl2_config->Get("Debugging", "program_args", ""); |     Settings::values.program_args = sdl2_config->Get("Debugging", "program_args", ""); | ||||||
|     Settings::values.dump_exefs = sdl2_config->GetBoolean("Debugging", "dump_exefs", false); |     Settings::values.dump_exefs = sdl2_config->GetBoolean("Debugging", "dump_exefs", false); | ||||||
|     Settings::values.dump_nso = sdl2_config->GetBoolean("Debugging", "dump_nso", false); |     Settings::values.dump_nso = sdl2_config->GetBoolean("Debugging", "dump_nso", false); | ||||||
|  |     Settings::values.enable_fs_access_log = | ||||||
|  |         sdl2_config->GetBoolean("Debugging", "enable_fs_access_log", false); | ||||||
|     Settings::values.reporting_services = |     Settings::values.reporting_services = | ||||||
|         sdl2_config->GetBoolean("Debugging", "reporting_services", false); |         sdl2_config->GetBoolean("Debugging", "reporting_services", false); | ||||||
|     Settings::values.quest_flag = sdl2_config->GetBoolean("Debugging", "quest_flag", false); |     Settings::values.quest_flag = sdl2_config->GetBoolean("Debugging", "quest_flag", false); | ||||||
|  |  | ||||||
|  | @ -338,6 +338,8 @@ record_frame_times = | ||||||
| dump_exefs=false | dump_exefs=false | ||||||
| # Determines whether or not yuzu will dump all NSOs it attempts to load while loading them | # Determines whether or not yuzu will dump all NSOs it attempts to load while loading them | ||||||
| dump_nso=false | dump_nso=false | ||||||
|  | # Determines whether or not yuzu will save the filesystem access log. | ||||||
|  | enable_fs_access_log=false | ||||||
| # Determines whether or not yuzu will report to the game that the emulated console is in Kiosk Mode | # Determines whether or not yuzu will report to the game that the emulated console is in Kiosk Mode | ||||||
| # false: Retail/Normal Mode (default), true: Kiosk Mode | # false: Retail/Normal Mode (default), true: Kiosk Mode | ||||||
| quest_flag = | quest_flag = | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Morph
						Morph