From 15fe65c964a3d3e34d756b0f032b949a3c390647 Mon Sep 17 00:00:00 2001 From: TheTechnician27 Date: Fri, 17 Jan 2025 01:15:59 -0600 Subject: [PATCH] Settings: Allow user to manually set RTC per-game --- pcsx2-qt/SettingWidgetBinder.h | 125 +++++++++++++++++- pcsx2-qt/Settings/EmulationSettingsWidget.cpp | 21 ++- pcsx2-qt/Settings/EmulationSettingsWidget.h | 3 +- pcsx2-qt/Settings/EmulationSettingsWidget.ui | 24 +++- pcsx2/CDVD/CDVD.cpp | 36 ++++- pcsx2/Config.h | 10 +- pcsx2/Pcsx2Config.cpp | 17 ++- 7 files changed, 227 insertions(+), 9 deletions(-) diff --git a/pcsx2-qt/SettingWidgetBinder.h b/pcsx2-qt/SettingWidgetBinder.h index f76df54b8c747..c6fc85722dee2 100644 --- a/pcsx2-qt/SettingWidgetBinder.h +++ b/pcsx2-qt/SettingWidgetBinder.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0+ #pragma once @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -656,6 +657,26 @@ namespace SettingWidgetBinder } }; + template <> + struct SettingAccessor + { + static int getYear(const QDateTimeEdit* widget) { return widget->date().year(); } + static int getMonth(const QDateTimeEdit* widget) { return widget->date().month(); } + static int getDay(const QDateTimeEdit* widget) { return widget->date().day(); } + + static int getHour(const QDateTimeEdit* widget) { return widget->time().hour(); } + static int getMinute(const QDateTimeEdit* widget) { return widget->time().minute(); } + static int getSecond(const QDateTimeEdit* widget) { return widget->time().second(); } + + static void setDateTime(QDateTimeEdit* widget, const QDate date, const QTime time) { widget->setDateTime(QDateTime(date, time)); } + + template + static void connectValueChanged(QDateTimeEdit* widget, F func) + { + widget->connect(widget, &QDateTimeEdit::dateTimeChanged, func); + } + }; + /// Binds a widget's value to a setting, updating it when the value changes. template @@ -1239,4 +1260,106 @@ namespace SettingWidgetBinder widget->connect(widget, &QLineEdit::editingFinished, widget, std::move(value_changed)); } + + // No need to pass a section or key since this is only used once and has six keys associated with it + static inline void BindWidgetToDateTimeSetting(SettingsInterface* sif, QDateTimeEdit* widget) + { + using Accessor = SettingAccessor; + + int YEAR_OFFSET = 2000; + int DEFAULT_YEAR = 0; + int DEFAULT_MONTH = 1; + int DEFAULT_DAY = 1; + int DEFAULT_HOUR = 0; + int DEFAULT_MINUTE = 0; + int DEFAULT_SECOND = 0; + + std::string SECTION = "EmuCore"; + std::string YEAR_KEY = "RtcYear"; + std::string MONTH_KEY = "RtcMonth"; + std::string DAY_KEY = "RtcDay"; + std::string HOUR_KEY = "RtcHour"; + std::string MINUTE_KEY = "RtcMinute"; + std::string SECOND_KEY = "RtcSecond"; + + // Fetch settings from .ini + const s32 year_value = + Host::GetBaseIntSettingValue(SECTION.c_str(), YEAR_KEY.c_str(), static_cast(DEFAULT_YEAR)); + const s32 month_value = + Host::GetBaseIntSettingValue(SECTION.c_str(), MONTH_KEY.c_str(), static_cast(DEFAULT_MONTH)); + const s32 day_value = + Host::GetBaseIntSettingValue(SECTION.c_str(), DAY_KEY.c_str(), static_cast(DEFAULT_DAY)); + const s32 hour_value = + Host::GetBaseIntSettingValue(SECTION.c_str(), HOUR_KEY.c_str(), static_cast(DEFAULT_HOUR)); + const s32 minute_value = + Host::GetBaseIntSettingValue(SECTION.c_str(), MINUTE_KEY.c_str(), static_cast(DEFAULT_MINUTE)); + const s32 second_value = + Host::GetBaseIntSettingValue(SECTION.c_str(), SECOND_KEY.c_str(), static_cast(DEFAULT_SECOND)); + + if (sif) + { + int sif_year_value = DEFAULT_YEAR; + int sif_month_value = DEFAULT_MONTH; + int sif_day_value = DEFAULT_DAY; + int sif_hour_value = DEFAULT_HOUR; + int sif_minute_value = DEFAULT_MINUTE; + int sif_second_value = DEFAULT_SECOND; + + // Get Settings Interface values or default if that fails + if (!sif->GetIntValue(SECTION.c_str(), YEAR_KEY.c_str(), &sif_year_value)) { sif_year_value = DEFAULT_YEAR; } + if (!sif->GetIntValue(SECTION.c_str(), MONTH_KEY.c_str(), &sif_month_value)) { sif_month_value = DEFAULT_MONTH; } + if (!sif->GetIntValue(SECTION.c_str(), DAY_KEY.c_str(), &sif_day_value)) { sif_day_value = DEFAULT_DAY; } + if (!sif->GetIntValue(SECTION.c_str(), HOUR_KEY.c_str(), &sif_hour_value)) { sif_hour_value = DEFAULT_HOUR; } + if (!sif->GetIntValue(SECTION.c_str(), MINUTE_KEY.c_str(), &sif_minute_value)) { sif_minute_value = DEFAULT_MINUTE; } + if (!sif->GetIntValue(SECTION.c_str(), SECOND_KEY.c_str(), &sif_second_value)) { sif_second_value = DEFAULT_SECOND; } + + // No need to check for valid date since QDateTime resets to minimum upon becoming invalid + Accessor::setDateTime(widget, QDate(static_cast(sif_year_value + YEAR_OFFSET), static_cast(sif_month_value), static_cast(sif_day_value)), + QTime(static_cast(sif_hour_value), static_cast(sif_minute_value), static_cast(sif_second_value))); + + // Update the settings interface and reload the game settings when changed + Accessor::connectValueChanged(widget, [sif, widget, SECTION = std::move(SECTION), YEAR_KEY = std::move(YEAR_KEY), MONTH_KEY = std::move(MONTH_KEY), + DAY_KEY = std::move(DAY_KEY), HOUR_KEY = std::move(HOUR_KEY), MINUTE_KEY = std::move(MINUTE_KEY), SECOND_KEY = std::move(SECOND_KEY), YEAR_OFFSET = std::move(YEAR_OFFSET)]() { + + sif->SetIntValue(SECTION.c_str(), YEAR_KEY.c_str(), Accessor::getYear(widget) - YEAR_OFFSET); + sif->SetIntValue(SECTION.c_str(), MONTH_KEY.c_str(), Accessor::getMonth(widget)); + sif->SetIntValue(SECTION.c_str(), DAY_KEY.c_str(), Accessor::getDay(widget)); + sif->SetIntValue(SECTION.c_str(), HOUR_KEY.c_str(), Accessor::getHour(widget)); + sif->SetIntValue(SECTION.c_str(), MINUTE_KEY.c_str(), Accessor::getMinute(widget)); + sif->SetIntValue(SECTION.c_str(), SECOND_KEY.c_str(), Accessor::getSecond(widget)); + + QtHost::SaveGameSettings(sif, true); + g_emu_thread->reloadGameSettings(); + }); + } + + else + { + // No need to check for valid date since QDateTime resets to minimum upon becoming invalid + Accessor::setDateTime(widget, QDate(static_cast(year_value + YEAR_OFFSET), static_cast(month_value), static_cast(day_value)), + QTime(static_cast(hour_value), static_cast(minute_value), static_cast(second_value))); + + // Update and apply base settings with values from widget when user changes it in UI + Accessor::connectValueChanged(widget, [widget, SECTION = std::move(SECTION), YEAR_KEY = std::move(YEAR_KEY), MONTH_KEY = std::move(MONTH_KEY), + DAY_KEY = std::move(DAY_KEY), HOUR_KEY = std::move(HOUR_KEY), MINUTE_KEY = std::move(MINUTE_KEY), SECOND_KEY = std::move(SECOND_KEY), YEAR_OFFSET = std::move(YEAR_OFFSET)]() { + + const int new_year_value = Accessor::getYear(widget); + const int new_month_value = Accessor::getMonth(widget); + const int new_day_value = Accessor::getDay(widget); + const int new_hour_value = Accessor::getHour(widget); + const int new_minute_value = Accessor::getMinute(widget); + const int new_second_value = Accessor::getSecond(widget); + + Host::SetBaseIntSettingValue(SECTION.c_str(), YEAR_KEY.c_str(), new_year_value - YEAR_OFFSET); + Host::SetBaseIntSettingValue(SECTION.c_str(), MONTH_KEY.c_str(), new_month_value); + Host::SetBaseIntSettingValue(SECTION.c_str(), DAY_KEY.c_str(), new_day_value); + Host::SetBaseIntSettingValue(SECTION.c_str(), HOUR_KEY.c_str(), new_hour_value); + Host::SetBaseIntSettingValue(SECTION.c_str(), MINUTE_KEY.c_str(), new_minute_value); + Host::SetBaseIntSettingValue(SECTION.c_str(), SECOND_KEY.c_str(), new_second_value); + Host::CommitBaseSettingChanges(); + g_emu_thread->applySettings(); + }); + } + + } } // namespace SettingWidgetBinder diff --git a/pcsx2-qt/Settings/EmulationSettingsWidget.cpp b/pcsx2-qt/Settings/EmulationSettingsWidget.cpp index 73700df181bff..98d210cfc8e3c 100644 --- a/pcsx2-qt/Settings/EmulationSettingsWidget.cpp +++ b/pcsx2-qt/Settings/EmulationSettingsWidget.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0+ #include @@ -48,6 +48,12 @@ EmulationSettingsWidget::EmulationSettingsWidget(SettingsWindow* dialog, QWidget if (m_dialog->isPerGameSettings()) { + SettingWidgetBinder::BindWidgetToDateTimeSetting(sif, m_ui.rtcDateTime); + m_ui.rtcDateTime->setDateRange(QDate(2000, 1, 1), QDate(2099, 12, 31)); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.manuallySetRealTimeClock, "EmuCore", "ManuallySetRealTimeClock", false); + connect(m_ui.manuallySetRealTimeClock, &QCheckBox::checkStateChanged, this, &EmulationSettingsWidget::onManuallySetRealTimeClockChanged); + EmulationSettingsWidget::onManuallySetRealTimeClockChanged(); + m_ui.eeCycleRate->insertItem( 0, tr("Use Global Setting [%1]") .arg(m_ui.eeCycleRate->itemText( @@ -74,6 +80,8 @@ EmulationSettingsWidget::EmulationSettingsWidget(SettingsWindow* dialog, QWidget } else { + m_ui.rtcGroup->hide(); + SettingWidgetBinder::BindWidgetToBoolSetting(sif, m_ui.cheats, "EmuCore", "EnableCheats", false); // Allow for FastCDVD for per-game settings only @@ -146,6 +154,11 @@ EmulationSettingsWidget::EmulationSettingsWidget(SettingsWindow* dialog, QWidget dialog->registerWidgetHelp(m_ui.useVSyncForTiming, tr("Use Host VSync Timing"), tr("Unchecked"), tr("When synchronizing with the host refresh rate, this option disable's PCSX2's internal frame timing, and uses the host instead. " "Can result in smoother frame pacing, but at the cost of increased input latency.")); + dialog->registerWidgetHelp(m_ui.manuallySetRealTimeClock, tr("Manually Set Real-Time Clock"), tr("Unchecked"), + tr("Manually set a real-time clock to use for the virtual PlayStation 2 instead of using your OS' system clock.")); + dialog->registerWidgetHelp(m_ui.rtcDateTime, tr("Real-Time Clock"), tr("Current date and time"), + tr("Real-time clock (RTC) used by the virtual PlayStation 2. NOTE: This assumes you have your PS2 set to the default timezone of GMT+0 and default DST of Summer Time. " + "Some games require an RTC date/time set after their release date.")); updateOptimalFramePacing(); updateUseVSyncForTimingEnabled(); @@ -292,3 +305,9 @@ void EmulationSettingsWidget::updateUseVSyncForTimingEnabled() const bool sync_to_host_refresh = m_dialog->getEffectiveBoolValue("EmuCore/GS", "SyncToHostRefreshRate", false); m_ui.useVSyncForTiming->setEnabled(vsync && sync_to_host_refresh); } + +void EmulationSettingsWidget::onManuallySetRealTimeClockChanged() +{ + const bool enabled = m_dialog->getEffectiveBoolValue("EmuCore", "ManuallySetRealTimeClock", false); + m_ui.rtcDateTime->setEnabled(enabled); +} diff --git a/pcsx2-qt/Settings/EmulationSettingsWidget.h b/pcsx2-qt/Settings/EmulationSettingsWidget.h index caa95e22f77e1..0cd9087fb7ae1 100644 --- a/pcsx2-qt/Settings/EmulationSettingsWidget.h +++ b/pcsx2-qt/Settings/EmulationSettingsWidget.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0+ #pragma once @@ -25,6 +25,7 @@ private Q_SLOTS: void handleSpeedComboChange(QComboBox* cb, const char* section, const char* key); void updateOptimalFramePacing(); void updateUseVSyncForTimingEnabled(); + void onManuallySetRealTimeClockChanged(); SettingsWindow* m_dialog; diff --git a/pcsx2-qt/Settings/EmulationSettingsWidget.ui b/pcsx2-qt/Settings/EmulationSettingsWidget.ui index 424db2b38b608..e1f56b55dd152 100644 --- a/pcsx2-qt/Settings/EmulationSettingsWidget.ui +++ b/pcsx2-qt/Settings/EmulationSettingsWidget.ui @@ -24,7 +24,7 @@ 0 - + Speed Control @@ -195,7 +195,7 @@ - + Frame Pacing / Latency Control @@ -268,6 +268,26 @@ + + + + Real-Time Clock + + + + + + Manually Set Real-Time Clock + + + + + + + + + + diff --git a/pcsx2/CDVD/CDVD.cpp b/pcsx2/CDVD/CDVD.cpp index 9bd3c9b3bf3d8..b268470aad464 100644 --- a/pcsx2/CDVD/CDVD.cpp +++ b/pcsx2/CDVD/CDVD.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0+ #include "CDVD/CDVD.h" @@ -28,6 +28,9 @@ #include #include +#ifndef _WIN32 +#include +#endif #include cdvdStruct cdvd; @@ -917,9 +920,38 @@ void cdvdReset() cdvd.ReadTime = cdvdBlockReadTime(MODE_DVDROM); cdvd.RotSpeed = cdvdRotationTime(MODE_DVDROM); + if (EmuConfig.ManuallySetRealTimeClock) + { + // Convert to GMT+9 (assumes GMT+0) + std::tm tm{}; + tm.tm_sec = EmuConfig.RtcSecond; + tm.tm_min = EmuConfig.RtcMinute; + tm.tm_hour = EmuConfig.RtcHour; + tm.tm_mday = EmuConfig.RtcDay; + tm.tm_mon = EmuConfig.RtcMonth - 1; + tm.tm_year = EmuConfig.RtcYear + 100; // 2000 - 1900 + tm.tm_isdst = 1; + + // Need this instead of mktime for timezone independence + std::time_t t = 0; + #if defined(_WIN32) + t = _mkgmtime(&tm) + 32400; //60 * 60 * 9 for GMT+9 + gmtime_s(&tm, &t); + #else + t = timegm(&tm) + 32400; + gmtime_r(&t, &tm); + #endif + + cdvd.RTC.second = tm.tm_sec; + cdvd.RTC.minute = tm.tm_min; + cdvd.RTC.hour = tm.tm_hour; + cdvd.RTC.day = tm.tm_mday; + cdvd.RTC.month = tm.tm_mon + 1; + cdvd.RTC.year = tm.tm_year - 100; + } // If we are recording, always use the same RTC setting // for games that use the RTC to seed their RNG -- this is very important to be the same everytime! - if (g_InputRecording.isActive()) + else if (g_InputRecording.isActive()) { Console.WriteLn("Input Recording Active - Using Constant RTC of 04-03-2020 (DD-MM-YYYY)"); // Why not just 0 everything? Some games apparently require the date to be valid in terms of when diff --git a/pcsx2/Config.h b/pcsx2/Config.h index 403900ff2c7ea..e8e4631130360 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0+ #pragma once @@ -1282,6 +1282,7 @@ struct Pcsx2Config InhibitScreensaver : 1, BackupSavestate : 1, McdFolderAutoManage : 1, + ManuallySetRealTimeClock : 1, HostFs : 1, @@ -1315,6 +1316,13 @@ struct Pcsx2Config int PINESlot; + int RtcYear = 0; + int RtcMonth = 1; + int RtcDay = 1; + int RtcHour = 0; + int RtcMinute = 0; + int RtcSecond = 0; + // Set at runtime, not loaded from config. std::string CurrentBlockdump; std::string CurrentIRX; diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp index 655c11af5885f..ae9fcb2605df2 100644 --- a/pcsx2/Pcsx2Config.cpp +++ b/pcsx2/Pcsx2Config.cpp @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2002-2024 PCSX2 Dev Team +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team // SPDX-License-Identifier: GPL-3.0+ #include "common/CocoaTools.h" @@ -1898,6 +1898,7 @@ Pcsx2Config::Pcsx2Config() InhibitScreensaver = true; BackupSavestate = true; WarnAboutUnsafeSettings = true; + ManuallySetRealTimeClock = false; // To be moved to FileMemoryCard pluign (someday) for (uint slot = 0; slot < 8; ++slot) @@ -1910,6 +1911,12 @@ Pcsx2Config::Pcsx2Config() GzipIsoIndexTemplate = "$(f).pindex.tmp"; PINESlot = 28011; + RtcYear = 0; + RtcMonth = 1; + RtcDay = 1; + RtcHour = 0; + RtcMinute = 0; + RtcSecond = 0; } void Pcsx2Config::LoadSaveCore(SettingsWrapper& wrap) @@ -1940,6 +1947,8 @@ void Pcsx2Config::LoadSaveCore(SettingsWrapper& wrap) SettingsWrapBitBool(WarnAboutUnsafeSettings); + SettingsWrapBitBool(ManuallySetRealTimeClock); + // Process various sub-components: Speedhacks.LoadSave(wrap); @@ -1959,6 +1968,12 @@ void Pcsx2Config::LoadSaveCore(SettingsWrapper& wrap) SettingsWrapEntry(GzipIsoIndexTemplate); SettingsWrapEntry(PINESlot); + SettingsWrapEntry(RtcYear); + SettingsWrapEntry(RtcMonth); + SettingsWrapEntry(RtcDay); + SettingsWrapEntry(RtcHour); + SettingsWrapEntry(RtcMinute); + SettingsWrapEntry(RtcSecond); // For now, this in the derived config for backwards ini compatibility. SettingsWrapEntryEx(CurrentBlockdump, "BlockDumpSaveDirectory");