From 0b35fe863bdba861a0bdb40255c77ff66dfcdcab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 2 Nov 2026 19:54:57 +0100 Subject: [PATCH 1/5] More UI cleanup --- Common/UI/PopupScreens.cpp | 2 +- Core/ELF/PrxDecrypter.cpp | 23 +++++------ Core/HLE/sceKernelModule.cpp | 4 +- UI/GameButton.cpp | 79 ++++++++++++++++++++---------------- 4 files changed, 56 insertions(+), 52 deletions(-) diff --git a/Common/UI/PopupScreens.cpp b/Common/UI/PopupScreens.cpp index 6450eff1bc..9c49c9f7a0 100644 --- a/Common/UI/PopupScreens.cpp +++ b/Common/UI/PopupScreens.cpp @@ -23,7 +23,7 @@ static void TextToImage(std::string_view buttonText, ImageID *image) { if (buttonText == di->T("Delete") || buttonText == di->T("Move to trash")) { *image = ImageID("I_TRASHCAN"); } else if (buttonText == di->T("Back")) { - *image = ImageID("I_NAVIGATE_BACK"); + *image = ImageID("I_CHEVRON_LEFT"); } else if (buttonText == di->T("Add")) { *image = ImageID("I_PLUS"); } else if (buttonText == di->T("OK")) { diff --git a/Core/ELF/PrxDecrypter.cpp b/Core/ELF/PrxDecrypter.cpp index ebe91b6cb1..6fb0c69668 100644 --- a/Core/ELF/PrxDecrypter.cpp +++ b/Core/ELF/PrxDecrypter.cpp @@ -923,7 +923,7 @@ static bool KirkOutputFits(const KIRK_CMD1_ECDSA_HEADER *header, u32 size) { } static int pspDecryptType0(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 size) { - DEBUG_LOG(Log::Loader, "Decrypting tag %02X", (u32)*(u32_le *)&inbuf[0xD0]); + DEBUG_LOG(Log::Loader, "Decrypting tag %02X (type 0)", (u32)*(u32_le *)&inbuf[0xD0]); const auto decryptSize = *(s32_le*)&inbuf[0xB0]; const auto pti = GetTagInfo((u32)*(u32_le *)&inbuf[0xD0]); @@ -985,9 +985,8 @@ static int pspDecryptType0(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz return decryptSize; } -static int pspDecryptType1(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 size) -{ - INFO_LOG(Log::Loader, "Decrypting tag %02X", (u32)*(u32_le *)&inbuf[0xD0]); +static int pspDecryptType1(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 size) { + DEBUG_LOG(Log::Loader, "Decrypting tag %02X (type 1)", (u32)*(u32_le *)&inbuf[0xD0]); const auto decryptSize = *(s32_le*)&inbuf[0xB0]; const auto pti = GetTagInfo((u32)*(u32_le *)&inbuf[0xD0]); @@ -1045,9 +1044,8 @@ static int pspDecryptType1(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz return decryptSize; } -static int pspDecryptType2(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 size) -{ - INFO_LOG(Log::Loader, "Decrypting tag %02X", (u32)*(u32_le *)&inbuf[0xD0]); +static int pspDecryptType2(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 size) { + DEBUG_LOG(Log::Loader, "Decrypting tag %02X (type 2)", (u32)*(u32_le *)&inbuf[0xD0]); const auto decryptSize = *(s32_le*)&inbuf[0xB0]; const auto pti = GetTagInfo2((u32)*(u32_le *)&inbuf[0xD0]); @@ -1165,9 +1163,8 @@ static int pspFinishType5(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 size return decryptSize; } -static int pspDecryptType5(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 size, const u8 *seed) - { - INFO_LOG(Log::Loader, "Decrypting tag %02X", (u32)*(u32_le *)&inbuf[0xD0]); +static int pspDecryptType5(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 size, const u8 *seed) { + DEBUG_LOG(Log::Loader, "Decrypting tag %02X (type 5)", (u32)*(u32_le *)&inbuf[0xD0]); const auto pti = GetTagInfo2((u32)*(u32_le *)&inbuf[0xD0]); if (!pti) @@ -1206,7 +1203,7 @@ static int pspDecryptType3(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz { return -1; } - INFO_LOG(Log::Loader, "Decrypting tag %08X", (u32)*(u32_le *)&inbuf[0xD0]); + DEBUG_LOG(Log::Loader, "Decrypting tag %08X (type 3)", (u32)*(u32_le *)&inbuf[0xD0]); auto xorbuf = expandSeed(pti->key, pti->code, secureId); PRXType5 type5(inbuf); @@ -1221,7 +1218,7 @@ static int pspDecryptType3(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz static int pspDecryptType6(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 size) { - INFO_LOG(Log::Loader, "Decrypting tag %02X", (u32)*(u32_le *)&inbuf[0xD0]); + DEBUG_LOG(Log::Loader, "Decrypting tag %02X (type 6)", (u32)*(u32_le *)&inbuf[0xD0]); const auto decryptSize = *(s32_le*)&inbuf[0xB0]; const auto pti = GetTagInfo2((u32)*(u32_le *)&inbuf[0xD0]); @@ -1297,7 +1294,7 @@ static int pspDecryptType6(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 siz // type 6 writes its tail there. static int pspDecryptType9(KirkState *kirk, const u8 *inbuf, u8 *outbuf, u32 size) { - INFO_LOG(Log::Loader, "Decrypting tag %02X", (u32)*(u32_le *)&inbuf[0xD0]); + DEBUG_LOG(Log::Loader, "Decrypting tag %02X (type 9)", (u32)*(u32_le *)&inbuf[0xD0]); const auto decryptSize = *(s32_le*)&inbuf[0xB0]; const auto pti = GetTagInfo2((u32)*(u32_le *)&inbuf[0xD0]); diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 6b21faed0d..4c1c8bc701 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -850,7 +850,7 @@ void ImportVarSymbol(WriteVarSymbolState &state, const VarSymbolImport &var) { } // It hasn't been exported yet, but hopefully it will later. - INFO_LOG(Log::Loader, "Variable (%s,%08x) unresolved, storing for later resolving", var.moduleName, var.nid); + DEBUG_LOG(Log::Loader, "Variable (%s,%08x) unresolved, storing for later resolving", var.moduleName, var.nid); } void ExportVarSymbol(const VarSymbolExport &var) { @@ -945,7 +945,7 @@ void ImportFuncSymbol(const FuncSymbolImport &func, bool reimporting, const char // We used to report this, but I don't think it's very interesting anymore. WARN_LOG(Log::Loader, "Unknown syscall from known HLE module '%s': 0x%08x (import for '%s')", func.moduleName, func.nid, importingModule); } else { - INFO_LOG(Log::Loader, "Function (%s,%08x) unresolved in '%s', storing for later resolving", func.moduleName, func.nid, importingModule); + DEBUG_LOG(Log::Loader, "Function (%s,%08x) unresolved in '%s', storing for later resolving", func.moduleName, func.nid, importingModule); } if (shouldHLE || !reimporting) { diff --git a/UI/GameButton.cpp b/UI/GameButton.cpp index 452bcd2e5f..567ad237af 100644 --- a/UI/GameButton.cpp +++ b/UI/GameButton.cpp @@ -204,7 +204,6 @@ void GameButton::Draw(UIContext &dc) { } } - u32 color = 0, shadowColor = 0; using namespace UI; UI::Style style = dc.GetTheme().itemStyle; @@ -232,71 +231,79 @@ void GameButton::Draw(UIContext &dc) { } const float textX = ListTextX(viewMode_); - if (texture && ginfo) { - color = whiteAlpha(ease((time_now_d() - ginfo->icon.timeLoaded) * 2)); - shadowColor = blackAlpha(ease((time_now_d() - ginfo->icon.timeLoaded) * 2)); - float tw = texture->Width(); - float th = texture->Height(); - - // Adjust position so we don't stretch the image vertically or horizontally. - // Make sure it's not wider than 144 (like Doom Legacy homebrew), ugly in the grid mode. - float nw = std::min(iconBounds.h * tw / th, (float)iconBounds.w); - iconBounds.x += (iconBounds.w - nw) / 2.0f; - iconBounds.w = nw; - } - - int txOffset = down_ ? 4 : 0; - if (viewMode_ != GameBrowserViewMode::GRID) { - txOffset = 0; + int iconYOffset = 0; + if (viewMode_ == GameBrowserViewMode::GRID && down_) { + iconYOffset = 4; } Bounds overlayBounds = bounds_; // Render the icon, all modes have this. - int dropsize = 10; if (texture) { + float alpha = 1.0f; + float dropsize = 10; + + if (ginfo) { + float loadPhase = ease((time_now_d() - ginfo->icon.timeLoaded) * 2); + + loadPhase = 0.0f; + + alpha = loadPhase; + + float tw = texture->Width(); + float th = texture->Height(); + // Adjust position so we don't stretch the image vertically or horizontally. + // Make sure it's not wider than 144 (like Doom Legacy homebrew), ugly in the grid mode. + float nw = std::min(iconBounds.h * tw / th, (float)iconBounds.w); + iconBounds.x += (iconBounds.w - nw) / 2.0f; + iconBounds.w = nw; + } + if (viewMode_ == GameBrowserViewMode::LIST) { iconBounds.x += 4; } - if (txOffset) { + + if (iconYOffset) { + // Button pressed, move the icon down a bit and reduce the shadow. dropsize = 3; - iconBounds.y += txOffset * 2; - overlayBounds.y += txOffset * 2; + iconBounds.y += iconYOffset * 2; + overlayBounds.y += iconYOffset * 2; } + if (HasFocus()) { dc.Draw()->Flush(); dc.RebindTexture(); float pulse = sin(time_now_d() * 7.0) * 0.25 + 0.8; - dc.DrawRectDropShadow(iconBounds, 10.0f, pulse, alphaMul(color, pulse)); + dc.DrawRectDropShadow(iconBounds, 10.0f, pulse, style.fgColor); dc.Draw()->Flush(); } else { dc.Draw()->Flush(); dc.RebindTexture(); - dc.DrawRectDropShadow(iconBounds, 8.0f, 0.7f, shadowColor); - // dc.Draw()->DrawImage4Grid(dc.GetTheme().dropShadow4Grid, x - dropsize, y - dropsize * 0.5f, x + w + dropsize, y + h + dropsize * 1.5, alphaMul(shadowColor, 0.5f), 1.0f); + dc.DrawRectDropShadow(iconBounds, 8.0f, 0.7f, 0); dc.Draw()->Flush(); } dc.Draw()->Flush(); dc.GetDrawContext()->BindTexture(0, texture); - dc.Draw()->DrawTexRect(iconBounds, 0, 0, 1, 1, color); + dc.Draw()->DrawTexRect(iconBounds, 0, 0, 1, 1, 0xFFFFFFFF); dc.Draw()->Flush(); } - if (viewMode_ == GameBrowserViewMode::GRID && g_Config.bShowIDOnGameIcon && fileType != IdentifiedFileType::PSP_ELF && !id_version.empty()) { - std::string_view idStr = id_version; - if (fileType == IdentifiedFileType::PSP_SAVEDATA_DIRECTORY) { - auto ga = GetI18NCategory(I18NCat::GAME); - idStr = ga->T("SaveData"); - } - dc.SetFontScale(0.5f * g_Config.fGameGridScale, 0.5f * g_Config.fGameGridScale); - dc.DrawText(idStr, bounds_.x + 5, iconBounds.y + 1, 0xFF000000, ALIGN_TOPLEFT); - dc.DrawText(idStr, bounds_.x + 4, iconBounds.y, dc.GetTheme().infoStyle.fgColor, ALIGN_TOPLEFT); - dc.SetFontScale(1.0f, 1.0f); - } if (viewMode_ == GameBrowserViewMode::GRID) { + if (g_Config.bShowIDOnGameIcon && fileType != IdentifiedFileType::PSP_ELF && !id_version.empty()) { + std::string_view idStr = id_version; + if (fileType == IdentifiedFileType::PSP_SAVEDATA_DIRECTORY) { + auto ga = GetI18NCategory(I18NCat::GAME); + idStr = ga->T("SaveData"); + } + dc.SetFontScale(0.5f * g_Config.fGameGridScale, 0.5f * g_Config.fGameGridScale); + dc.DrawText(idStr, bounds_.x + 5, iconBounds.y + 1, 0xFF000000, ALIGN_TOPLEFT); + dc.DrawText(idStr, bounds_.x + 4, iconBounds.y, dc.GetTheme().infoStyle.fgColor, ALIGN_TOPLEFT); + dc.SetFontScale(1.0f, 1.0f); + } + if (imageIcon.isValid()) { Bounds bounds = bounds_; From 9d384d811dd2b9ffd658599e6182628b5633a83d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 2 Nov 2026 22:52:41 +0100 Subject: [PATCH 2/5] sceReg: Map the first settings to our config settings. Reset category version to 1 on firmware erase. --- Core/Config.cpp | 5 +- Core/Config.h | 8 +- Core/ConfigSettings.cpp | 33 ++++++ Core/ConfigSettings.h | 11 ++ Core/HLE/sceImpose.cpp | 4 + Core/HLE/sceReg.cpp | 241 ++++++++++++++++++++++++++------------- Core/HLE/sceUtility.cpp | 2 +- Core/Util/PSARUnpack.cpp | 3 + headless/Headless.cpp | 2 +- 9 files changed, 226 insertions(+), 83 deletions(-) diff --git a/Core/Config.cpp b/Core/Config.cpp index 17fad32ebc..46b20d5e68 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -1470,10 +1470,12 @@ static constexpr const char *paramDateFormatNames[] = { "YYYYMMDD", "MMDDYYYY", static constexpr const char *paramTimeFormatNames[] = { "24HR", "12HR" }; +// Most of this is just registry settings, stored in our ini format. sceReg reads/writes here. static constexpr ConfigSetting systemParamSettings[] = { ConfigSetting("PSPModel", SETTING(g_Config, iPSPModel), PSP_MODEL_SLIM, CfgFlag::PER_GAME | CfgFlag::REPORT) .Label(I18NCat::SYSTEM, "PSP Model").Choices(pSPModelNames), ConfigSetting("PSPFirmwareVersion", SETTING(g_Config, iFirmwareVersion), PSP_DEFAULT_FIRMWARE, CfgFlag::PER_GAME | CfgFlag::REPORT), + ConfigSetting("SceRegCategoryVersion", SETTING(g_Config, iSceRegCategoryVersion), 1, CfgFlag::DEFAULT), ConfigSetting("NickName", SETTING(g_Config, sNickName), "PPSSPP", CfgFlag::PER_GAME), ConfigSetting("MacAddress", SETTING(g_Config, sMACAddress), "", CfgFlag::PER_GAME), ConfigSetting("GameLanguage", SETTING(g_Config, iLanguage), -1, CfgFlag::PER_GAME | CfgFlag::REPORT) @@ -1482,7 +1484,8 @@ static constexpr ConfigSetting systemParamSettings[] = { .Label(I18NCat::SYSTEM, "Time Format").Choices(paramTimeFormatNames), ConfigSetting("ParamDateFormat", SETTING(g_Config, iDateFormat), PSP_SYSTEMPARAM_DATE_FORMAT_YYYYMMDD, CfgFlag::PER_GAME) .Label(I18NCat::SYSTEM, "Date Format").Choices(paramDateFormatNames), - ConfigSetting("TimeZone", SETTING(g_Config, iTimeZone), 0, CfgFlag::PER_GAME), + ConfigSetting("TimeZone", SETTING(g_Config, iTimeZoneOffset), 0, CfgFlag::PER_GAME), + ConfigSetting("TimeZoneArea", SETTING(g_Config, sTimeZoneArea), "sweden", CfgFlag::PER_GAME), // This doesn't really do much. Just cosmetic in the XMB ConfigSetting("DayLightSavings", SETTING(g_Config, bDayLightSavings), (bool) PSP_SYSTEMPARAM_DAYLIGHTSAVINGS_STD, CfgFlag::PER_GAME) .Label(I18NCat::SYSTEM, "Daylight savings"), ConfigSetting("ButtonPreference", SETTING(g_Config, iButtonPreference), PSP_SYSTEMPARAM_BUTTON_CROSS, CfgFlag::PER_GAME | CfgFlag::REPORT) diff --git a/Core/Config.h b/Core/Config.h index afa427572e..3fd4a7d524 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -601,7 +601,10 @@ public: // Risky JIT optimizations bool bDiscardRegsOnJRRA; - // SystemParam + // SystemParam settings - well, they are queries by systemparam but they're actually stored in the registry. + // So now, we mirror these out into the actual registry. + int iSceRegCategoryVersion; // The version of the registry. Resets to 1 if you install an older firmware. The system will reject too-new regs and rewrite too-old ones. + std::string sNickName; // AdHoc and system nickname std::string sMACAddress; // The Fuse ID of the PSP a game patch was installed on (12 hex digits), to decrypt it with. @@ -610,7 +613,8 @@ public: int iLanguage; int iTimeFormat; int iDateFormat; - int iTimeZone; + int iTimeZoneOffset; + std::string sTimeZoneArea; bool bDayLightSavings; int iButtonPreference; int iLockParentalLevel; diff --git a/Core/ConfigSettings.cpp b/Core/ConfigSettings.cpp index d8a03e0ed1..57d3d875c6 100644 --- a/Core/ConfigSettings.cpp +++ b/Core/ConfigSettings.cpp @@ -403,3 +403,36 @@ void ConfigSetting::ReportSetting(const ConfigBlock *configBlock, UrlEncoder &da return; } } + +int ConfigSettingRef::GetIntValue() const { + _dbg_assert_(setting); + _dbg_assert_(setting->GetType() == ConfigSetting::Type::TYPE_INT); + int intValue = 0; + if (setting->GetType() == ConfigSetting::Type::TYPE_INT) { + void *ptr = setting->VoidPtr(block); + memcpy(&intValue, ptr, 4); + } else if (setting->GetType() == ConfigSetting::Type::TYPE_BOOL) { + void *ptr = setting->VoidPtr(block); + bool boolValue = false; + memcpy(&boolValue, ptr, 1); + intValue = boolValue ? 1 : 0; + } else { + ERROR_LOG(Log::Config, "ConfigSettingRef::GetIntValue: Setting is not an int or bool"); + } + return intValue; +} + +void ConfigSettingRef::SetIntValue(int value) const { + _dbg_assert_(setting); + _dbg_assert_(setting->GetType() == ConfigSetting::Type::TYPE_INT); + if (setting->GetType() == ConfigSetting::Type::TYPE_INT) { + void *ptr = setting->VoidPtr(block); + memcpy(ptr, &value, 4); + } else if (setting->GetType() == ConfigSetting::Type::TYPE_BOOL) { + void *ptr = setting->VoidPtr(block); + bool boolValue = (value != 0); + memcpy(ptr, &boolValue, 1); + } else { + ERROR_LOG(Log::Config, "ConfigSettingRef::SetIntValue: Setting is not an int or bool"); + } +} diff --git a/Core/ConfigSettings.h b/Core/ConfigSettings.h index 8e59599558..cfe27c04a8 100644 --- a/Core/ConfigSettings.h +++ b/Core/ConfigSettings.h @@ -251,6 +251,9 @@ struct ConfigSetting { _dbg_assert_(type_ == Type::TYPE_FLOAT); return defaultCallback_.f ? defaultCallback_.f() : default_.f; } + Type GetType() const { + return type_; + } constexpr const ConfigSettingInfo &Info() const { return info_; @@ -274,6 +277,10 @@ struct ConfigSetting { const char *configBlockBase = (const char *)configBlock; return (const void *)(configBlockBase + offset_); } + void *VoidPtr(const ConfigBlock *configBlock) const { + const char *configBlockBase = (const char *)configBlock; + return (void *)(configBlockBase + offset_); + } private: static constexpr void Check(bool ok) { @@ -300,4 +307,8 @@ struct ConfigSettingRef { ConfigBlock *block; const ConfigSetting *setting; std::string_view section; + + // Easy accessors. + int GetIntValue() const; + void SetIntValue(int value) const; }; diff --git a/Core/HLE/sceImpose.cpp b/Core/HLE/sceImpose.cpp index 3faec2b55d..d03b1bc4bf 100644 --- a/Core/HLE/sceImpose.cpp +++ b/Core/HLE/sceImpose.cpp @@ -293,6 +293,10 @@ const HLEFunction sceImpose_driver[] = { {0X01EF0650, &WrapI_I, "sceImposeSetStatus", 'i', "i" }, {0X2462EFE4, &WrapI_I, "sceImposeSetStatus", 'i', "i" }, {0XC10C3D39, &WrapI_I, "sceImposeSetStatus", 'i', "i" }, + + // Unknown, spams in older firmwares (3.80). Probably aliases for Get something. + {0x21d3ea22, 0, "sceImpose_21D3EA22", '?', ""}, + {0xd4fda428, 0, "sceImpose_d4fda428", '?', ""}, }; void Register_sceImpose_driver() { diff --git a/Core/HLE/sceReg.cpp b/Core/HLE/sceReg.cpp index 415b13f5ba..177dc58786 100644 --- a/Core/HLE/sceReg.cpp +++ b/Core/HLE/sceReg.cpp @@ -9,17 +9,49 @@ #include "Common/Serialize/SerializeMap.h" #include "Common/StringUtils.h" #include "Core/Config.h" +#include "Core/ConfigSettings.h" #include "Core/System.h" #define SYSTEM_REGISTRY "/system" #define REG_KEYNAME_SIZE 27 -enum RegKeyTypes { - REG_TYPE_DIR = 1, - REG_TYPE_INT = 2, - REG_TYPE_STR = 3, - REG_TYPE_BIN = 4, -}; +// Mapping to PPSSPP settings +// Bools are stored as 4-byte INT registry keys, 0 or 1. +// +// CONFIG/SYSTEM/owner_name <-> g_Config.sNickName +// CONFIG/SYSTEM/XMB/language <-> g_Config.iLanguage +// CONFIG/SYSTEM/umd_autoboot <-> g_Config.bVshUmdAutoboot - and in addition, auto-controlled when booting UMD VIDEO. +// CONFIG/SYSTEM/umd_cache <-> (unmapped, ignore) +// CONFIG/SYSTEM/usb_autoconnect <-> (unmapped, ignore) +// CONFIG/DATE/date_format <-> g_Config.iDateFormat +// CONFIG/DATE/time_format <-> g_Config.iTimeFormat +// CONFIG/DATE/time_zone_area <-> g_Config.iTimeZoneArea +// CONFIG/DATE/time_zone_offset <-> g_Config.iTimeZoneOffset +// CONFIG/DATE/summer_time <-> g_Config.bDayLightSavings +// CONFIG/SYSTEM/LOCK/parental_level <-> g_Config.iLockParentalLevel, int +// CONFIG/DISPLAY/screensaver_start_time <-> (unmapped, int seconds) +// CONFIG/SYSTEM/POWER_SAVING/backlight_off_interval <-> (unmapped, int seconds) +// CONFIG/SYSTEM/POWER_SAVING/suspend_interval_interval <-> (unmapped, int seconds) +// CONFIG/SYSTEM/POWER_SAVING/wlan_mode <-> (unmapped, bool, "WLAN power save") +// CONFIG/SYSTEM/SOUND/avls <-> (unmapped, bool) +// CONFIG/SYSTEM/SOUND/dynamic_normalizer <-> (unmapped, bool) +// CONFIG/SYSTEM/SOUND/operation_sound_mode <-> (unmapped, bool) (click sound when navigating the XMB) +// CONFIG/RSS/download_items (unmapped, int) +// CONFIG/SYSTEM/XMB/button_assign (unmapped, int) << TODO +// CONFIG/SYSTEM/XMB/theme_type (unmapped, int) << TODO +// CONFIG/SYSTEM/XMB/THEME/color_mode (unmapped, int) << TODO +// CONFIG/SYSTEM/XMB/THEME/system_color (unmapped, int) << TODO +// CONFIG/SYSTEM/XMB/THEME/wallpaper_mode (unmapped, int) << TODO +// CONFIG/SYSTEM/XMB/THEME/custom_theme_mode (unmapped, int) << TODO +// +// UMD video stuff +// CONFIG/VIDEO/appended_volume (unmapped, int, 0-2) << TODO +// CONFIG/VIDEO/menu_language (unmapped, 2 byte binary) << TODO +// CONFIG/VIDEO/sound_language (unmapped, 2 byte binary) << TODO +// CONFIG/VIDEO/subtitle_language (unmapped, 2 byte binary) << TODO +// CONFIG/VIDEO/lr_button_enable (unmapped, bool) << TODO +// CONFIG/VIDEO/list_play_mode (unmapped, bool) << TODO +// CONFIG/VIDEO/title_display_mode (int, 0-1) << TODO typedef unsigned int REGHANDLE; @@ -57,12 +89,14 @@ enum class ValueType { struct KeyValue { std::string name; ValueType type; + // Fixed values. const char *strValue; // also can be used for bin. Note: can't be std::string/string_view, will cut off the length. int intValue; const KeyValue *dirContents; // intValue is the count. // If set, an INT key reads this setting (as 0 or 1), even after the guest writes it: the VSH // rewrites every key with its default at each boot, since category_version is below its schema. - bool (*boolSetting)() = nullptr; + void *configSetting; // Read/write according to config's type, we look it up by pointer (int or std::string for str/bin). + bool (*boolSetting)(); // Dynamic. Only used for UMD autoboot. }; static bool UmdAutoboot() { @@ -664,7 +698,7 @@ static const KeyValue tree_CONFIG_PREMO[] = { { "custom_video_buffer1", ValueType::INT, "", (int)0x0 }, // decimal: 0 { "custom_video_buffer2", ValueType::INT, "", (int)0x0 }, // decimal: 0 { "setting_internet", ValueType::INT, "", (int)0x0 }, // decimal: 0 - { "button_assign", ValueType::INT, "", (int)0x0 }, // decimal: 0 + { "button_assign", ValueType::INT, "", (int)0x0 }, // decimal: 0 NOTE: This is not the "real" button_assign, this is something else. { "flags", ValueType::INT, "", (int)0x0 }, // decimal: 0 { "account_id", ValueType::BIN, "", 16 }, // (all zero) { "login_id", ValueType::STR, "", 1 }, @@ -735,7 +769,7 @@ static const KeyValue tree_CONFIG_SYSTEM_XMB_THEME[] = { static const KeyValue tree_CONFIG_SYSTEM_XMB[] = { { "theme_type", ValueType::INT, "", (int)0x0 }, // decimal: 0 { "language", ValueType::INT, "", (int)0x1 }, // decimal: 1 - { "button_assign", ValueType::INT, "", (int)0x1 }, // decimal: 1 + { "button_assign", ValueType::INT, "", (int)0x1, nullptr, &g_Config.iButtonPreference}, { "THEME", ValueType::DIR, "", ARRAY_SIZE(tree_CONFIG_SYSTEM_XMB_THEME), tree_CONFIG_SYSTEM_XMB_THEME }, }; @@ -774,7 +808,7 @@ static const KeyValue tree_CONFIG_SYSTEM_CHARACTER_SET[] = { static const KeyValue tree_CONFIG_SYSTEM[] = { { "owner_name", ValueType::STR, "P939", 5 }, { "backlight_brightness", ValueType::INT, "", (int)0x3 }, // decimal: 3 - { "umd_autoboot", ValueType::INT, "", (int)0x1, nullptr, &UmdAutoboot }, // decimal: 1 + { "umd_autoboot", ValueType::INT, "", (int)0x1, nullptr, nullptr, &UmdAutoboot }, // decimal: 1 { "usb_charge", ValueType::INT, "", (int)0x1 }, // decimal: 1 { "umd_cache", ValueType::INT, "", (int)0x1 }, // decimal: 1 { "usb_auto_connect", ValueType::INT, "", (int)0x1 }, // decimal: 1 @@ -990,7 +1024,7 @@ static const KeyValue tree_REGISTRY[] = { // A real 6.6x PSP has 0x66 here, which is what this tree was dumped from - but the VSH treats a // category_version higher than the schema it knows as a corrupt registry and offers to reset your // settings instead of booting. So we go very low. - { "category_version", ValueType::INT, "", 1 }, + { "category_version", ValueType::INT, "", 1, nullptr, &g_Config.iSceRegCategoryVersion }, }; // There might be more categories. @@ -1143,6 +1177,21 @@ static const KeyValue *LookupCategory(std::string_view path, int *count) { return cur->keyvals.data(); } +static const KeyValue *LookupCategoryKey(const KeyValue *keyvals, const char *name, int count, int *index = nullptr) { + if (!keyvals || !count) { + return nullptr; + } + for (int i = 0; i < count; i++) { + if (equals(keyvals[i].name, name)) { + if (index) { + *index = i; + } + return &keyvals[i]; + } + }; + return nullptr; +} + void __RegDoState(PointerWrap &p) { auto s = p.Section("sceReg", 0, 4); if (!s) { @@ -1347,31 +1396,31 @@ int sceRegGetKeyInfo(int catHandle, const char *name, u32 outKeyHandleAddr, u32 return hleLogWarning(Log::sceReg, SCE_REG_ERROR_CATEGORY_NOT_FOUND); } - for (int i = 0; i < count; i++) { - if (equals(keyvals[i].name, name)) { - // Found it! - if (Memory::IsValid4AlignedAddress(outKeyHandleAddr)) { - // Let's just make the index the key handle. - Memory::WriteUnchecked_U32(i, outKeyHandleAddr); - } - if (Memory::IsValid4AlignedAddress(outTypeAddr)) { - // Let's just make the index the key handle. - Memory::WriteUnchecked_U32((int)keyvals[i].type, outTypeAddr); - } - int size = 0; - if (Memory::IsValid4AlignedAddress(outSizeAddr)) { - switch (keyvals[i].type) { - case ValueType::BIN: size = (int)keyvals[i].intValue; break; - case ValueType::STR: size = (int)keyvals[i].intValue; break; - case ValueType::DIR: size = 0; break; - case ValueType::INT: size = 4; break; - default: break; - } - // Let's just make the index the key handle. - Memory::WriteUnchecked_U32(size, outSizeAddr); - } - return hleLogDebug(Log::sceReg, 0, "handle: %d type: %d size: %d", i, (int)keyvals[i].type, size); + int index = 0; + const KeyValue *keyval = LookupCategoryKey(keyvals, name, count, &index); + if (keyval) { + // Found it! + if (Memory::IsValid4AlignedAddress(outKeyHandleAddr)) { + // Let's just make the index the key handle. + Memory::WriteUnchecked_U32(index, outKeyHandleAddr); } + if (Memory::IsValid4AlignedAddress(outTypeAddr)) { + // Let's just make the index the key handle. + Memory::WriteUnchecked_U32((int)keyval->type, outTypeAddr); + } + int size = 0; + if (Memory::IsValid4AlignedAddress(outSizeAddr)) { + switch (keyval->type) { + case ValueType::BIN: size = (int)keyval->intValue; break; + case ValueType::STR: size = (int)keyval->intValue; break; + case ValueType::DIR: size = 0; break; + case ValueType::INT: size = 4; break; + default: break; + } + // Let's just make the index the key handle. + Memory::WriteUnchecked_U32(size, outSizeAddr); + } + return hleLogDebug(Log::sceReg, 0, "type: %d size: %d", (int)keyval->type, size); } return hleLogWarning(Log::sceReg, -1, "key with name '%s' not found", name); @@ -1393,24 +1442,23 @@ int sceRegGetKeyInfoByName(int catHandle, const char *name, u32 typeAddr, u32 si return hleLogWarning(Log::sceReg, SCE_REG_ERROR_CATEGORY_NOT_FOUND); } - for (int i = 0; i < count; i++) { - if (equals(keyvals[i].name, name)) { - int size = 0; - if (Memory::IsValid4AlignedAddress(typeAddr)) { - Memory::WriteUnchecked_U32((int)keyvals[i].type, typeAddr); - } - if (Memory::IsValid4AlignedAddress(sizeAddr)) { - switch (keyvals[i].type) { - case ValueType::BIN: size = (int)keyvals[i].intValue; break; - case ValueType::STR: size = (int)keyvals[i].intValue; break; - case ValueType::DIR: size = 0; break; - case ValueType::INT: size = 4; break; - default: break; - } - Memory::WriteUnchecked_U32(size, sizeAddr); - } - return hleLogDebug(Log::sceReg, 0, "type: %d size: %d", (int)keyvals[i].type, size); + const KeyValue *keyval = LookupCategoryKey(keyvals, name, count); + if (keyval) { + int size = 0; + if (Memory::IsValid4AlignedAddress(typeAddr)) { + Memory::WriteUnchecked_U32((int)keyval->type, typeAddr); } + if (Memory::IsValid4AlignedAddress(sizeAddr)) { + switch (keyval->type) { + case ValueType::BIN: size = (int)keyval->intValue; break; + case ValueType::STR: size = (int)keyval->intValue; break; + case ValueType::DIR: size = 0; break; + case ValueType::INT: size = 4; break; + default: break; + } + Memory::WriteUnchecked_U32(size, sizeAddr); + } + return hleLogDebug(Log::sceReg, 0, "type: %d size: %d", (int)keyval->type, size); } return hleLogWarning(Log::sceReg, -1, "key with name '%s' not found", name); @@ -1421,7 +1469,7 @@ int sceRegGetKeyValue(int catHandle, int keyHandle, u32 bufAddr, u32 size) { return -1; } - auto iter = g_openCategories.find(catHandle); + const auto iter = g_openCategories.find(catHandle); if (iter == g_openCategories.end()) { return hleLogError(Log::sceReg, 0, "Not found"); } @@ -1440,14 +1488,14 @@ int sceRegGetKeyValue(int catHandle, int keyHandle, u32 bufAddr, u32 size) { switch (keyval.type) { case ValueType::BIN: Memory::MemcpyUnchecked(bufAddr, keyval.strValue, std::min(size, (u32)keyval.intValue)); - return hleLogDebug(Log::sceReg, 0); + return hleLogDebug(Log::sceReg, 0, "%s/%s: value: ", iter->second.path.c_str(), keyval.name.c_str()); case ValueType::STR: Memory::MemcpyUnchecked(bufAddr, keyval.strValue, std::min(size, (u32)keyval.intValue)); - return hleLogDebug(Log::sceReg, 0, "value: '%s'", keyval.strValue); + return hleLogDebug(Log::sceReg, 0, "%s/%s: value: '%s'", iter->second.path.c_str(), keyval.name.c_str(), keyval.strValue); case ValueType::INT: if (size >= sizeof(u32)) Memory::WriteUnchecked_U32(keyval.intValue, bufAddr); - return hleLogDebug(Log::sceReg, 0, "value: %d (0x%08x)", keyval.intValue, keyval.intValue); + return hleLogInfo(Log::sceReg, 0, "%s/%s: value: %d (0x%08x)", iter->second.path.c_str(), keyval.name.c_str(), keyval.intValue, keyval.intValue); case ValueType::DIR: case ValueType::FAIL: default: @@ -1456,6 +1504,7 @@ int sceRegGetKeyValue(int catHandle, int keyHandle, u32 bufAddr, u32 size) { } } +// TODO: This one seems to ignore overlays? int sceRegGetKeyValueByName(int catHandle, const char *name, u32 bufAddr, u32 size) { if (!name) { return hleLogError(Log::sceReg, -1, "Invalid name pointer"); @@ -1482,15 +1531,28 @@ int sceRegGetKeyValueByName(int catHandle, const char *name, u32 bufAddr, u32 si const KeyValue &keyval = keyvals[i]; switch (keyval.type) { case ValueType::BIN: - Memory::MemcpyUnchecked(bufAddr, keyval.strValue, std::min(size, (u32)keyval.intValue)); - return hleLogDebug(Log::sceReg, 0); case ValueType::STR: Memory::MemcpyUnchecked(bufAddr, keyval.strValue, std::min(size, (u32)keyval.intValue)); - return hleLogDebug(Log::sceReg, 0, "value: '%s'", keyval.strValue); + if (keyval.type == ValueType::BIN) { + return hleLogDebug(Log::sceReg, 0, "value: "); + } else { + return hleLogDebug(Log::sceReg, 0, "value: '%s'", keyval.strValue); + } case ValueType::INT: - if (size >= sizeof(u32)) - Memory::WriteUnchecked_U32(keyval.intValue, bufAddr); - return hleLogDebug(Log::sceReg, 0, "value: %d (0x%08x)", keyval.intValue, keyval.intValue); + { + int intValue = keyval.intValue; // The preset. + // Check for a config mapping. Read the value. + if (keyval.configSetting) { + if (const ConfigSettingRef *ref = g_Config.FindSetting(keyval.configSetting)) { + intValue = ref->GetIntValue(); + } + } + + if (size >= sizeof(u32)) { + Memory::WriteUnchecked_U32(intValue, bufAddr); + } + return hleLogDebug(Log::sceReg, 0, "value: %d (0x%08x)", intValue, intValue); + } case ValueType::DIR: case ValueType::FAIL: default: @@ -1552,6 +1614,23 @@ int sceRegSetKeyValue(int catHandle, const char *name, u32 bufAddr, u32 size) { return hleLogError(Log::sceReg, -1, "bad input addr"); } value.intValue = (int)Memory::ReadUnchecked_U32(bufAddr); + + // Write to the actual Config setting, if configured. + int count = 0; + const KeyValue *keyvals = LookupCategory(path, &count); + const KeyValue *keyval = LookupCategoryKey(keyvals, name, count); + if (keyval->configSetting) { + if (const ConfigSettingRef *ref = g_Config.FindSetting(keyval->configSetting)) { + ref->SetIntValue(value.intValue); + } + } + + g_overlay[path][name] = value; + InvalidateMergedCache(); + + // If we have a config setting mapped, merge with that. + + return hleLogInfo(Log::sceReg, 0, "set %s/%s to %d", path.c_str(), name, value.intValue); } else if (Memory::IsValidRange(bufAddr, size)) { const char *str = (const char *)Memory::GetPointerUnchecked(bufAddr); value.str.assign(str, size); @@ -1560,13 +1639,21 @@ int sceRegSetKeyValue(int catHandle, const char *name, u32 bufAddr, u32 size) { value.str.push_back('\0'); } value.intValue = (int)value.str.size(); + g_overlay[path][name] = value; + InvalidateMergedCache(); + if (type == ValueType::STR) { + return hleLogInfo(Log::sceReg, 0, "set %s/%s to '%.*s'", path.c_str(), name, (int)value.str.size(), value.str.c_str()); + } else { + if (value.str.size() == 2) { + // This is probably one of the languages. + return hleLogInfo(Log::sceReg, 0, "set %s/%s to binary data of size 2: '%c%c'", path.c_str(), name, value.str[0], value.str[1]); + } else { + return hleLogInfo(Log::sceReg, 0, "set %s/%s to binary data of size %d", path.c_str(), name, (int)value.str.size()); + } + } } else { return hleLogError(Log::sceReg, -1, "bad input addr"); } - - g_overlay[path][name] = value; - InvalidateMergedCache(); - return hleLogDebug(Log::sceReg, 0, "set %s/%s", path.c_str(), name); } int sceRegCreateKey(int catHandle, const char *name, int type, u32 size) { @@ -1580,19 +1667,15 @@ int sceRegCreateKey(int catHandle, const char *name, int type, u32 size) { } OverlayValue value; - switch (type) { - case REG_TYPE_DIR: - value.isDir = true; + switch ((ValueType)type) { + case ValueType::DIR: value.type = ValueType::DIR; + value.isDir = true; break; - case REG_TYPE_INT: - value.type = ValueType::INT; - break; - case REG_TYPE_STR: - value.type = ValueType::STR; - break; - case REG_TYPE_BIN: - value.type = ValueType::BIN; + case ValueType::INT: + case ValueType::STR: + case ValueType::BIN: + value.type = (ValueType)type; break; default: return hleLogError(Log::sceReg, -1, "bad type %d", type); @@ -1612,8 +1695,10 @@ int sceRegCreateKey(int catHandle, const char *name, int type, u32 size) { g_overlay[path][name] = value; InvalidateMergedCache(); - return hleLogDebug(Log::sceReg, 0, "create %s/%s (type %d)", path.c_str(), name, type); + + return hleLogInfo(Log::sceReg, 0, "create %s/%s (type %d)", path.c_str(), name, type); } + // Speculated signature int sceRegRemoveKey(int catHandle, int key) { return hleLogError(Log::sceReg, 0); diff --git a/Core/HLE/sceUtility.cpp b/Core/HLE/sceUtility.cpp index 9a080eaf64..e5f07e84d2 100644 --- a/Core/HLE/sceUtility.cpp +++ b/Core/HLE/sceUtility.cpp @@ -1577,7 +1577,7 @@ static u32 sceUtilityGetSystemParamInt(u32 id, u32 destaddr) { param = PSP_SYSTEMPARAM_TIME_FORMAT_24HR; break; case PSP_SYSTEMPARAM_ID_INT_TIMEZONE: - param = g_Config.iTimeZone; + param = g_Config.iTimeZoneOffset; break; case PSP_SYSTEMPARAM_ID_INT_DAYLIGHTSAVINGS: param = g_Config.bDayLightSavings?PSP_SYSTEMPARAM_DAYLIGHTSAVINGS_SAVING:PSP_SYSTEMPARAM_DAYLIGHTSAVINGS_STD; diff --git a/Core/Util/PSARUnpack.cpp b/Core/Util/PSARUnpack.cpp index 61d009f781..6df7a209af 100644 --- a/Core/Util/PSARUnpack.cpp +++ b/Core/Util/PSARUnpack.cpp @@ -1337,6 +1337,9 @@ bool EraseInstalledFirmware(const Path &nandRoot, std::string *error) { success = false; } } + + // Also reset the category version. + g_Config.iSceRegCategoryVersion = 1; return success; } diff --git a/headless/Headless.cpp b/headless/Headless.cpp index f4d4b1f4b6..f645780e76 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -1180,7 +1180,7 @@ int main(int argc, const char* argv[]) { g_Config.iTimeFormat = PSP_SYSTEMPARAM_TIME_FORMAT_24HR; g_Config.bEncryptSave = true; g_Config.sNickName = "shadow"; - g_Config.iTimeZone = 60; + g_Config.iTimeZoneOffset = 60; g_Config.iDateFormat = PSP_SYSTEMPARAM_DATE_FORMAT_DDMMYYYY; g_Config.iButtonPreference = PSP_SYSTEMPARAM_BUTTON_CROSS; g_Config.iLockParentalLevel = 9; From 2fd47cd7ebea5c5fe67c31fe990aa470fd7f6e91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 2 Nov 2026 23:06:35 +0100 Subject: [PATCH 3/5] sceReg: Sync string keys with config, map the rest of the listed settings owner_name and time_zone_area now read from and write to sNickName and sTimeZoneArea. Also maps language, the date/time keys, summer_time and parental_level. The config values are applied in the merged category view, so reads by handle and the key info sizes see them too, not just sceRegGetKeyValueByName. language reads through GetPSPLanguage() so that the -1 'auto' setting reads as an actual language. Co-Authored-By: Claude Opus 5.5 --- Core/ConfigSettings.cpp | 61 ++++++++++++-------- Core/ConfigSettings.h | 4 +- Core/HLE/sceReg.cpp | 119 +++++++++++++++++++++++----------------- 3 files changed, 111 insertions(+), 73 deletions(-) diff --git a/Core/ConfigSettings.cpp b/Core/ConfigSettings.cpp index 57d3d875c6..ce93b83451 100644 --- a/Core/ConfigSettings.cpp +++ b/Core/ConfigSettings.cpp @@ -406,33 +406,48 @@ void ConfigSetting::ReportSetting(const ConfigBlock *configBlock, UrlEncoder &da int ConfigSettingRef::GetIntValue() const { _dbg_assert_(setting); - _dbg_assert_(setting->GetType() == ConfigSetting::Type::TYPE_INT); - int intValue = 0; - if (setting->GetType() == ConfigSetting::Type::TYPE_INT) { - void *ptr = setting->VoidPtr(block); - memcpy(&intValue, ptr, 4); - } else if (setting->GetType() == ConfigSetting::Type::TYPE_BOOL) { - void *ptr = setting->VoidPtr(block); - bool boolValue = false; - memcpy(&boolValue, ptr, 1); - intValue = boolValue ? 1 : 0; - } else { - ERROR_LOG(Log::Config, "ConfigSettingRef::GetIntValue: Setting is not an int or bool"); + const void *ptr = setting->GetVoidPtr(block); + switch (setting->GetType()) { + case ConfigSetting::Type::TYPE_INT: + return *(const int *)ptr; + case ConfigSetting::Type::TYPE_BOOL: + return *(const bool *)ptr ? 1 : 0; + default: + ERROR_LOG(Log::Config, "ConfigSettingRef::GetIntValue: %.*s is not an int or bool", STR_VIEW(setting->IniKey())); + return 0; } - return intValue; } void ConfigSettingRef::SetIntValue(int value) const { _dbg_assert_(setting); - _dbg_assert_(setting->GetType() == ConfigSetting::Type::TYPE_INT); - if (setting->GetType() == ConfigSetting::Type::TYPE_INT) { - void *ptr = setting->VoidPtr(block); - memcpy(ptr, &value, 4); - } else if (setting->GetType() == ConfigSetting::Type::TYPE_BOOL) { - void *ptr = setting->VoidPtr(block); - bool boolValue = (value != 0); - memcpy(ptr, &boolValue, 1); - } else { - ERROR_LOG(Log::Config, "ConfigSettingRef::SetIntValue: Setting is not an int or bool"); + void *ptr = setting->VoidPtr(block); + switch (setting->GetType()) { + case ConfigSetting::Type::TYPE_INT: + *(int *)ptr = value; + break; + case ConfigSetting::Type::TYPE_BOOL: + *(bool *)ptr = value != 0; + break; + default: + ERROR_LOG(Log::Config, "ConfigSettingRef::SetIntValue: %.*s is not an int or bool", STR_VIEW(setting->IniKey())); + break; } } + +const std::string *ConfigSettingRef::GetStringPtr() const { + _dbg_assert_(setting); + if (setting->GetType() != ConfigSetting::Type::TYPE_STRING) { + ERROR_LOG(Log::Config, "ConfigSettingRef::GetStringPtr: %.*s is not a string", STR_VIEW(setting->IniKey())); + return nullptr; + } + return (const std::string *)setting->GetVoidPtr(block); +} + +void ConfigSettingRef::SetStringValue(std::string_view value) const { + _dbg_assert_(setting); + if (setting->GetType() != ConfigSetting::Type::TYPE_STRING) { + ERROR_LOG(Log::Config, "ConfigSettingRef::SetStringValue: %.*s is not a string", STR_VIEW(setting->IniKey())); + return; + } + *(std::string *)setting->VoidPtr(block) = std::string(value); +} diff --git a/Core/ConfigSettings.h b/Core/ConfigSettings.h index cfe27c04a8..938caa960f 100644 --- a/Core/ConfigSettings.h +++ b/Core/ConfigSettings.h @@ -308,7 +308,9 @@ struct ConfigSettingRef { const ConfigSetting *setting; std::string_view section; - // Easy accessors. + // Easy accessors. The int ones also handle bools, as 0 or 1. int GetIntValue() const; void SetIntValue(int value) const; + const std::string *GetStringPtr() const; // nullptr if not a string setting. + void SetStringValue(std::string_view value) const; }; diff --git a/Core/HLE/sceReg.cpp b/Core/HLE/sceReg.cpp index 177dc58786..f9f6bf61e9 100644 --- a/Core/HLE/sceReg.cpp +++ b/Core/HLE/sceReg.cpp @@ -5,6 +5,7 @@ #include "Core/HLE/HLE.h" #include "Core/HLE/FunctionWrappers.h" #include "Core/HLE/ErrorCodes.h" +#include "Core/HLE/sceUtility.h" #include "Common/Serialize/Serializer.h" #include "Common/Serialize/SerializeMap.h" #include "Common/StringUtils.h" @@ -19,13 +20,13 @@ // Bools are stored as 4-byte INT registry keys, 0 or 1. // // CONFIG/SYSTEM/owner_name <-> g_Config.sNickName -// CONFIG/SYSTEM/XMB/language <-> g_Config.iLanguage +// CONFIG/SYSTEM/XMB/language <-> g_Config.iLanguage (-1 means auto, and reads as what GetPSPLanguage() picks) // CONFIG/SYSTEM/umd_autoboot <-> g_Config.bVshUmdAutoboot - and in addition, auto-controlled when booting UMD VIDEO. // CONFIG/SYSTEM/umd_cache <-> (unmapped, ignore) // CONFIG/SYSTEM/usb_autoconnect <-> (unmapped, ignore) // CONFIG/DATE/date_format <-> g_Config.iDateFormat // CONFIG/DATE/time_format <-> g_Config.iTimeFormat -// CONFIG/DATE/time_zone_area <-> g_Config.iTimeZoneArea +// CONFIG/DATE/time_zone_area <-> g_Config.sTimeZoneArea // CONFIG/DATE/time_zone_offset <-> g_Config.iTimeZoneOffset // CONFIG/DATE/summer_time <-> g_Config.bDayLightSavings // CONFIG/SYSTEM/LOCK/parental_level <-> g_Config.iLockParentalLevel, int @@ -37,7 +38,7 @@ // CONFIG/SYSTEM/SOUND/dynamic_normalizer <-> (unmapped, bool) // CONFIG/SYSTEM/SOUND/operation_sound_mode <-> (unmapped, bool) (click sound when navigating the XMB) // CONFIG/RSS/download_items (unmapped, int) -// CONFIG/SYSTEM/XMB/button_assign (unmapped, int) << TODO +// CONFIG/SYSTEM/XMB/button_assign <-> g_Config.iButtonPreference // CONFIG/SYSTEM/XMB/theme_type (unmapped, int) << TODO // CONFIG/SYSTEM/XMB/THEME/color_mode (unmapped, int) << TODO // CONFIG/SYSTEM/XMB/THEME/system_color (unmapped, int) << TODO @@ -93,15 +94,17 @@ struct KeyValue { const char *strValue; // also can be used for bin. Note: can't be std::string/string_view, will cut off the length. int intValue; const KeyValue *dirContents; // intValue is the count. - // If set, an INT key reads this setting (as 0 or 1), even after the guest writes it: the VSH - // rewrites every key with its default at each boot, since category_version is below its schema. - void *configSetting; // Read/write according to config's type, we look it up by pointer (int or std::string for str/bin). - bool (*boolSetting)(); // Dynamic. Only used for UMD autoboot. + // If set, the key is synced both ways with this g_Config member (looked up by pointer): reads + // return the setting and guest writes go to it. INT keys map to int or bool settings, STR keys + // to std::string ones. + void *configSetting; + // If set, reads of an INT key return this instead, while writes still go to configSetting. + int (*readOverride)(); }; -static bool UmdAutoboot() { +static int UmdAutoboot() { // Forced on when a UMD Video disc picked from the game list boots the XMB. - return PSP_CoreParameter().umdAutoboot.value_or(g_Config.bVshUmdAutoboot); + return PSP_CoreParameter().umdAutoboot.value_or(g_Config.bVshUmdAutoboot) ? 1 : 0; } // Guest-written registry values (sceRegCreateKey / sceRegSetKeyValue), keyed by category path @@ -768,8 +771,8 @@ static const KeyValue tree_CONFIG_SYSTEM_XMB_THEME[] = { // Dump of /CONFIG/SYSTEM/XMB static const KeyValue tree_CONFIG_SYSTEM_XMB[] = { { "theme_type", ValueType::INT, "", (int)0x0 }, // decimal: 0 - { "language", ValueType::INT, "", (int)0x1 }, // decimal: 1 - { "button_assign", ValueType::INT, "", (int)0x1, nullptr, &g_Config.iButtonPreference}, + { "language", ValueType::INT, "", (int)0x1, nullptr, &g_Config.iLanguage, &GetPSPLanguage }, // decimal: 1 + { "button_assign", ValueType::INT, "", (int)0x1, nullptr, &g_Config.iButtonPreference }, { "THEME", ValueType::DIR, "", ARRAY_SIZE(tree_CONFIG_SYSTEM_XMB_THEME), tree_CONFIG_SYSTEM_XMB_THEME }, }; @@ -794,7 +797,7 @@ static const KeyValue tree_CONFIG_SYSTEM_POWER_SAVING[] = { // Dump of /CONFIG/SYSTEM/LOCK static const KeyValue tree_CONFIG_SYSTEM_LOCK[] = { { "password", ValueType::BIN, "0000", 4 }, - { "parental_level", ValueType::INT, "", (int)0x0 }, // decimal: 0 + { "parental_level", ValueType::INT, "", (int)0x0, nullptr, &g_Config.iLockParentalLevel }, // decimal: 0 { "browser_start", ValueType::INT, "", (int)0x0 }, // decimal: 0 }; @@ -806,9 +809,9 @@ static const KeyValue tree_CONFIG_SYSTEM_CHARACTER_SET[] = { // Dump of /CONFIG/SYSTEM static const KeyValue tree_CONFIG_SYSTEM[] = { - { "owner_name", ValueType::STR, "P939", 5 }, + { "owner_name", ValueType::STR, "P939", 5, nullptr, &g_Config.sNickName }, { "backlight_brightness", ValueType::INT, "", (int)0x3 }, // decimal: 3 - { "umd_autoboot", ValueType::INT, "", (int)0x1, nullptr, nullptr, &UmdAutoboot }, // decimal: 1 + { "umd_autoboot", ValueType::INT, "", (int)0x1, nullptr, &g_Config.bVshUmdAutoboot, &UmdAutoboot }, // decimal: 1 { "usb_charge", ValueType::INT, "", (int)0x1 }, // decimal: 1 { "umd_cache", ValueType::INT, "", (int)0x1 }, // decimal: 1 { "usb_auto_connect", ValueType::INT, "", (int)0x1 }, // decimal: 1 @@ -827,11 +830,11 @@ static const KeyValue tree_CONFIG_SYSTEM[] = { // Dump of /CONFIG/DATE static const KeyValue tree_CONFIG_DATE[] = { - { "time_format", ValueType::INT, "", (int)0x0 }, // decimal: 0 - { "date_format", ValueType::INT, "", (int)0x2 }, // decimal: 2 - { "summer_time", ValueType::INT, "", (int)0x0 }, // decimal: 0 - { "time_zone_offset", ValueType::INT, "", (int)0x3c }, // decimal: 60 - { "time_zone_area", ValueType::STR, "sweden", 7 }, + { "time_format", ValueType::INT, "", (int)0x0, nullptr, &g_Config.iTimeFormat }, // decimal: 0 + { "date_format", ValueType::INT, "", (int)0x2, nullptr, &g_Config.iDateFormat }, // decimal: 2 + { "summer_time", ValueType::INT, "", (int)0x0, nullptr, &g_Config.bDayLightSavings }, // decimal: 0 + { "time_zone_offset", ValueType::INT, "", (int)0x3c, nullptr, &g_Config.iTimeZoneOffset }, // decimal: 60 + { "time_zone_area", ValueType::STR, "sweden", 7, nullptr, &g_Config.sTimeZoneArea }, }; // Dump of /CONFIG/NETWORK/ADHOC @@ -1090,10 +1093,45 @@ static const KeyValue *LookupStaticCategory(const std::string &path, int *count) return curDir; } +// Points the config-mapped keys at the current g_Config values. Done on every lookup, since the +// settings can change outside the registry (in our UI, or a per-game config being loaded). +static void ApplyConfigValues(MergedCategory &merged) { + for (KeyValue &kv : merged.keyvals) { + if (kv.readOverride && kv.type == ValueType::INT) { + kv.intValue = kv.readOverride(); + continue; + } + if (!kv.configSetting) { + continue; + } + const ConfigSettingRef *ref = g_Config.FindSetting(kv.configSetting); + if (!ref) { + continue; + } + if (kv.type == ValueType::INT) { + kv.intValue = ref->GetIntValue(); + } else if (kv.type == ValueType::STR) { + const std::string *str = ref->GetStringPtr(); + if (!str) { + continue; + } + // Like the static dump, the length includes the null terminator. + const int len = (int)str->size() + 1; + if (kv.strValue && kv.intValue == len && memcmp(kv.strValue, str->c_str(), len) == 0) { + continue; + } + merged.strStorage.push_back(*str); + kv.strValue = merged.strStorage.back().c_str(); + kv.intValue = len; + } + } +} + // Merges the static dump entries for path with the overlay entries written by the guest. static const MergedCategory &GetMergedCategory(const std::string &path) { auto cached = g_mergedCache.find(path); if (cached != g_mergedCache.end()) { + ApplyConfigValues(cached->second); return cached->second; } @@ -1134,12 +1172,7 @@ static const MergedCategory &GetMergedCategory(const std::string &path) { } } - for (auto &kv : merged.keyvals) { - if (kv.boolSetting) { - kv.intValue = kv.boolSetting() ? 1 : 0; - } - } - + ApplyConfigValues(merged); return merged; } @@ -1539,20 +1572,10 @@ int sceRegGetKeyValueByName(int catHandle, const char *name, u32 bufAddr, u32 si return hleLogDebug(Log::sceReg, 0, "value: '%s'", keyval.strValue); } case ValueType::INT: - { - int intValue = keyval.intValue; // The preset. - // Check for a config mapping. Read the value. - if (keyval.configSetting) { - if (const ConfigSettingRef *ref = g_Config.FindSetting(keyval.configSetting)) { - intValue = ref->GetIntValue(); - } - } - if (size >= sizeof(u32)) { - Memory::WriteUnchecked_U32(intValue, bufAddr); + Memory::WriteUnchecked_U32(keyval.intValue, bufAddr); } - return hleLogDebug(Log::sceReg, 0, "value: %d (0x%08x)", intValue, intValue); - } + return hleLogDebug(Log::sceReg, 0, "value: %d (0x%08x)", keyval.intValue, keyval.intValue); case ValueType::DIR: case ValueType::FAIL: default: @@ -1582,9 +1605,13 @@ int sceRegSetKeyValue(int catHandle, const char *name, u32 bufAddr, u32 size) { // registry upgrade rewrites BIN keys like /CONFIG/NP/account_id, and reports the settings as // corrupted if they read back as anything else. ValueType type = ValueType::FAIL; + const ConfigSettingRef *configRef = nullptr; for (const auto &kv : GetMergedCategory(path).keyvals) { if (equals(kv.name, name)) { type = kv.type; + if (kv.configSetting) { + configRef = g_Config.FindSetting(kv.configSetting); + } break; } } @@ -1614,22 +1641,12 @@ int sceRegSetKeyValue(int catHandle, const char *name, u32 bufAddr, u32 size) { return hleLogError(Log::sceReg, -1, "bad input addr"); } value.intValue = (int)Memory::ReadUnchecked_U32(bufAddr); - - // Write to the actual Config setting, if configured. - int count = 0; - const KeyValue *keyvals = LookupCategory(path, &count); - const KeyValue *keyval = LookupCategoryKey(keyvals, name, count); - if (keyval->configSetting) { - if (const ConfigSettingRef *ref = g_Config.FindSetting(keyval->configSetting)) { - ref->SetIntValue(value.intValue); - } + if (configRef) { + configRef->SetIntValue(value.intValue); } g_overlay[path][name] = value; InvalidateMergedCache(); - - // If we have a config setting mapped, merge with that. - return hleLogInfo(Log::sceReg, 0, "set %s/%s to %d", path.c_str(), name, value.intValue); } else if (Memory::IsValidRange(bufAddr, size)) { const char *str = (const char *)Memory::GetPointerUnchecked(bufAddr); @@ -1639,6 +1656,10 @@ int sceRegSetKeyValue(int catHandle, const char *name, u32 bufAddr, u32 size) { value.str.push_back('\0'); } value.intValue = (int)value.str.size(); + if (configRef && type == ValueType::STR) { + // Up to the terminator, if the guest included one. + configRef->SetStringValue(std::string_view(value.str.c_str())); + } g_overlay[path][name] = value; InvalidateMergedCache(); if (type == ValueType::STR) { From 4b74eccc38ecdc7d246ac262c2e9b38554b39ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 2 Nov 2026 23:36:06 +0100 Subject: [PATCH 4/5] sceReg: Set category_version to the installed firmware's schema version The shell writes the default of every registry key added after the stored category_version, and some of those (umd_autoboot) are now mapped to our config. Each firmware's version is the default in its registry schema table, in vshmain.prx (common_util.prx before 2.00). Installing a firmware now stores it, and headless sets it from the NAND's firmware. Also builds the config setting lookup in headless and libretro: without g_Config.Init(), sceReg served its static dump instead of the mapped config values. Co-Authored-By: Claude Opus 5.5 --- Core/Util/PSARUnpack.cpp | 32 ++++++++++++++++++++++++++++++++ Core/Util/PSARUnpack.h | 3 +++ headless/Headless.cpp | 10 ++++++++++ libretro/libretro.cpp | 2 ++ 4 files changed, 47 insertions(+) diff --git a/Core/Util/PSARUnpack.cpp b/Core/Util/PSARUnpack.cpp index 6df7a209af..320bcc4cf3 100644 --- a/Core/Util/PSARUnpack.cpp +++ b/Core/Util/PSARUnpack.cpp @@ -1343,6 +1343,33 @@ bool EraseInstalledFirmware(const Path &nandRoot, std::string *error) { return success; } +int SceRegCategoryVersionForFirmware(std::string_view version) { + // The default of /REGISTRY/category_version in each firmware's registry schema, a table of + // { name, type, size, default } entries in vsh/module/vshmain.prx (common_util.prx before 2.00). + // When the stored version is below this, the shell writes the default of every key added since + // that version (from 1, that includes umd_autoboot). One above it means a corrupt registry. + static const struct { + int firmware; // The first firmware with this version, as FirmwareVersionToInt() gives it. + int categoryVersion; + } versions[] = { + { 100, 0x00 }, { 150, 0x01 }, { 200, 0x06 }, { 250, 0x13 }, { 260, 0x14 }, { 270, 0x1b }, + { 280, 0x20 }, { 300, 0x26 }, { 310, 0x2c }, { 330, 0x31 }, { 350, 0x35 }, { 360, 0x36 }, + { 370, 0x3b }, { 380, 0x3e }, { 395, 0x40 }, { 405, 0x43 }, { 420, 0x46 }, { 500, 0x4c }, + { 550, 0x58 }, { 570, 0x61 }, { 600, 0x63 }, { 620, 0x64 }, { 630, 0x66 }, + }; + const int fw = FirmwareVersionToInt(version); + if (fw < versions[0].firmware) { + return 1; + } + int categoryVersion = versions[0].categoryVersion; + for (const auto &v : versions) { + if (fw >= v.firmware) { + categoryVersion = v.categoryVersion; + } + } + return categoryVersion; +} + int FirmwareVersionToInt(std::string_view version) { const size_t dot = version.find('.'); if (dot == std::string_view::npos || dot == 0 || dot + 1 >= version.size()) { @@ -1674,6 +1701,11 @@ bool InstallFirmware(const Path &updater, const Path &nandRoot, const PSARUnpack if (ok) { ok = SwapStagingIntoPlace(nandRoot, staging, error); } + if (ok) { + // Match the registry to the new firmware's schema, so its shell doesn't reset the + // settings (which sceReg maps to our config) to its defaults at first boot. + g_Config.iSceRegCategoryVersion = SceRegCategoryVersionForFirmware(stats->firmwareVersion); + } // Whether it worked or not, don't leave the staging copy behind. if (File::Exists(staging) && !File::DeleteDirRecursively(staging)) { diff --git a/Core/Util/PSARUnpack.h b/Core/Util/PSARUnpack.h index 4663fb71b8..ab9abe471d 100644 --- a/Core/Util/PSARUnpack.h +++ b/Core/Util/PSARUnpack.h @@ -181,6 +181,9 @@ bool EraseInstalledFirmware(const Path &nandRoot, std::string *error); // makes the result safe to compare with: an unknown version is older than every real one. int FirmwareVersionToInt(std::string_view version); +// The /REGISTRY/category_version the shell of this firmware expects. 1 for an unknown version. +int SceRegCategoryVersionForFirmware(std::string_view version); + // The firmware versions we can actually boot the VSH (XMB) on. Every other version loads, but // the module patches it needs are version-specific, so it won't get anywhere. bool FirmwareVersionSupportsVSH(std::string_view version); diff --git a/headless/Headless.cpp b/headless/Headless.cpp index f645780e76..7847602ca9 100644 --- a/headless/Headless.cpp +++ b/headless/Headless.cpp @@ -941,6 +941,8 @@ int main(int argc, const char* argv[]) { #endif PROFILE_INIT(); TimeInit(); + // Builds the pointer to setting lookup, which sceReg's registry mapping uses. + g_Config.Init(); #if PPSSPP_PLATFORM(WINDOWS) if (!IsDebuggerPresent()) { SetCleanExitOnAssert(); @@ -1445,6 +1447,14 @@ int main(int argc, const char* argv[]) { coreParameter.nandRoot = nand; } + // Headless doesn't keep a config, so set the registry version that installing this firmware + // in the app would have stored. + { + InstalledFirmwareInfo firmwareInfo; + ReadInstalledFirmwareInfo(g_Config.nandRootDirectory, &firmwareInfo, false); + g_Config.iSceRegCategoryVersion = SceRegCategoryVersionForFirmware(firmwareInfo.version); + } + // Placed here rather than with the other early-exit subcommands above, because resolving a // "flash0:/kd/foo.prx" module path needs nandRootDirectory, which is only settled just above. if (cmdLineOptions.reDecrypt.has_value()) { diff --git a/libretro/libretro.cpp b/libretro/libretro.cpp index b6f3fa5c76..95052bd89f 100644 --- a/libretro/libretro.cpp +++ b/libretro/libretro.cpp @@ -1225,6 +1225,8 @@ void retro_init(void) if (environ_cb(RETRO_ENVIRONMENT_GET_INPUT_BITMASKS, NULL)) libretro_supports_bitmasks = true; + // Builds the pointer to setting lookup, which sceReg's registry mapping uses. + g_Config.Init(); g_Config.Load("", ""); g_Config.iInternalResolution = 0; From 08d1e837391e8af16d128d2bebec0c7ba2e680eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 3 Nov 2026 00:41:52 +0100 Subject: [PATCH 5/5] sceReg: Map the XMB theme keys to new config settings theme_type and CONFIG/SYSTEM/XMB/THEME's color_mode, wallpaper_mode, system_color and custom_theme_mode now persist in the config. Their defaults (0) are the firmware's schema defaults. Co-Authored-By: Claude Opus 5.5 --- Core/Config.cpp | 5 +++++ Core/Config.h | 6 ++++++ Core/HLE/sceReg.cpp | 20 ++++++++++---------- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/Core/Config.cpp b/Core/Config.cpp index 46b20d5e68..4151f2ff4e 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -1498,6 +1498,11 @@ static constexpr ConfigSetting systemParamSettings[] = { ConfigSetting("WlanPowerSave", SETTING(g_Config, bWlanPowerSave), (bool) PSP_SYSTEMPARAM_WLAN_POWERSAVE_OFF, CfgFlag::PER_GAME), ConfigSetting("EncryptSave", SETTING(g_Config, bEncryptSave), true, CfgFlag::PER_GAME | CfgFlag::REPORT), ConfigSetting("VshUmdAutoboot", SETTING(g_Config, bVshUmdAutoboot), false, CfgFlag::DEFAULT), + ConfigSetting("XmbThemeType", SETTING(g_Config, iXmbThemeType), 0, CfgFlag::DEFAULT), + ConfigSetting("XmbThemeColorMode", SETTING(g_Config, iXmbThemeColorMode), 0, CfgFlag::DEFAULT), + ConfigSetting("XmbThemeWallpaperMode", SETTING(g_Config, iXmbThemeWallpaperMode), 0, CfgFlag::DEFAULT), + ConfigSetting("XmbThemeSystemColor", SETTING(g_Config, iXmbThemeSystemColor), 0, CfgFlag::DEFAULT), + ConfigSetting("XmbThemeCustomThemeMode", SETTING(g_Config, iXmbThemeCustomThemeMode), 0, CfgFlag::DEFAULT), ConfigSetting("MemStickSize", SETTING(g_Config, iMemStickSizeGB), 16, CfgFlag::DEFAULT) .Label(I18NCat::SYSTEM, "Memory Stick size").Range(1, 32).Units("GB"), ConfigSetting("FuseID", SETTING(g_Config, sFuseID), "", CfgFlag::DEFAULT), diff --git a/Core/Config.h b/Core/Config.h index 3fd4a7d524..ea5f82caa9 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -621,6 +621,12 @@ public: bool bEncryptSave; // /CONFIG/SYSTEM/umd_autoboot: whether the VSH starts an inserted disc by itself. bool bVshUmdAutoboot; + // /CONFIG/SYSTEM/XMB (language and button_assign are iLanguage and iButtonPreference). + int iXmbThemeType; + int iXmbThemeColorMode; + int iXmbThemeWallpaperMode; + int iXmbThemeSystemColor; + int iXmbThemeCustomThemeMode; // Networking bool bEnableAdhocServer; diff --git a/Core/HLE/sceReg.cpp b/Core/HLE/sceReg.cpp index f9f6bf61e9..cf559ea990 100644 --- a/Core/HLE/sceReg.cpp +++ b/Core/HLE/sceReg.cpp @@ -39,11 +39,11 @@ // CONFIG/SYSTEM/SOUND/operation_sound_mode <-> (unmapped, bool) (click sound when navigating the XMB) // CONFIG/RSS/download_items (unmapped, int) // CONFIG/SYSTEM/XMB/button_assign <-> g_Config.iButtonPreference -// CONFIG/SYSTEM/XMB/theme_type (unmapped, int) << TODO -// CONFIG/SYSTEM/XMB/THEME/color_mode (unmapped, int) << TODO -// CONFIG/SYSTEM/XMB/THEME/system_color (unmapped, int) << TODO -// CONFIG/SYSTEM/XMB/THEME/wallpaper_mode (unmapped, int) << TODO -// CONFIG/SYSTEM/XMB/THEME/custom_theme_mode (unmapped, int) << TODO +// CONFIG/SYSTEM/XMB/theme_type <-> g_Config.iXmbThemeType +// CONFIG/SYSTEM/XMB/THEME/color_mode <-> g_Config.iXmbThemeColorMode +// CONFIG/SYSTEM/XMB/THEME/system_color <-> g_Config.iXmbThemeSystemColor +// CONFIG/SYSTEM/XMB/THEME/wallpaper_mode <-> g_Config.iXmbThemeWallpaperMode +// CONFIG/SYSTEM/XMB/THEME/custom_theme_mode <-> g_Config.iXmbThemeCustomThemeMode // // UMD video stuff // CONFIG/VIDEO/appended_volume (unmapped, int, 0-2) << TODO @@ -762,15 +762,15 @@ static const KeyValue tree_CONFIG_ONESEG[] = { // Dump of /CONFIG/SYSTEM/XMB/THEME static const KeyValue tree_CONFIG_SYSTEM_XMB_THEME[] = { - { "color_mode", ValueType::INT, "", (int)0x0 }, // decimal: 0 - { "wallpaper_mode", ValueType::INT, "", (int)0x0 }, // decimal: 0 - { "system_color", ValueType::INT, "", (int)0x0 }, // decimal: 0 - { "custom_theme_mode", ValueType::INT, "", (int)0x0 }, // decimal: 0 + { "color_mode", ValueType::INT, "", (int)0x0, nullptr, &g_Config.iXmbThemeColorMode }, // decimal: 0 + { "wallpaper_mode", ValueType::INT, "", (int)0x0, nullptr, &g_Config.iXmbThemeWallpaperMode }, // decimal: 0 + { "system_color", ValueType::INT, "", (int)0x0, nullptr, &g_Config.iXmbThemeSystemColor }, // decimal: 0 + { "custom_theme_mode", ValueType::INT, "", (int)0x0, nullptr, &g_Config.iXmbThemeCustomThemeMode }, // decimal: 0 }; // Dump of /CONFIG/SYSTEM/XMB static const KeyValue tree_CONFIG_SYSTEM_XMB[] = { - { "theme_type", ValueType::INT, "", (int)0x0 }, // decimal: 0 + { "theme_type", ValueType::INT, "", (int)0x0, nullptr, &g_Config.iXmbThemeType }, // decimal: 0 { "language", ValueType::INT, "", (int)0x1, nullptr, &g_Config.iLanguage, &GetPSPLanguage }, // decimal: 1 { "button_assign", ValueType::INT, "", (int)0x1, nullptr, &g_Config.iButtonPreference }, { "THEME", ValueType::DIR, "", ARRAY_SIZE(tree_CONFIG_SYSTEM_XMB_THEME), tree_CONFIG_SYSTEM_XMB_THEME },