From 356b65c0f604411ac5efae667f5d75ff388e0340 Mon Sep 17 00:00:00 2001 From: Rouven Bauer Date: Sun, 16 Jun 2019 18:15:48 +0200 Subject: [PATCH] Auto update (#61) * WIP: auto update * Using sce mutex insted of std mutex * Make network thread safe * Fix re-entrant mutex usage in activity * Reworked font - Added methods to draw fonts and clip to rectangle. - vita2d draws fonts relative to their baseline => added methods to compensate * Improved mutex logging * Update check + dialog * Version creation tool fixed to use little endian * Fix broken log function name format Didn't handle functions correctly that returned std::* * Fixed nan progress when downloading things * Adding updater app * Rename src/updater.* -> src/update.* * DialogView: refactor macros to cpp + focus on up/down * Lock PS and power buttons in updater app * Update thread opens dialog on error (instead of crashing) * Rename update thread Updater->Update * Fix "have to close vhbb" popup when staring updater app * Show plain background while updating * Minor improvments in CMakeList * adding compiled release files * Fix version comparison only woriking on big endian * Remove debug logging * Start working threads (icon fetch etc) after update check is done * Refactor std::string in macros to c-style strings * Use shared_ptr for DialogViewResult + code style * Only link debugnet in updater if enabled * Add missing newlines * Using YAML to store latest version + download url * Make updater log to file * Copy updater from resources instead of embedding it * Improve misc code style * Add fixme about merging the duplicate debug log sources together * updater: fix style issues * Remove unused Font::Draw() * Rename Font::DrawFromBaseline to Font::Draw * Update version YAML URL --- CMakeLists.txt | 402 +++++++++++++---------- assets/.gitignore | 1 + assets/spr/img_dialog_msg_bg.png | Bin 0 -> 1340304 bytes assets/spr/img_dialog_msg_btn.png | Bin 0 -> 72174 bytes assets/spr/img_dialog_msg_btn_active.png | Bin 0 -> 72174 bytes assets/spr/img_dialog_msg_btn_focus.png | Bin 0 -> 72174 bytes assets/spr/img_updater_icon.png | Bin 0 -> 2768 bytes release/.gitignore | 1 + release/version.yml | 2 + src/Views/HomebrewView/homebrewView.cpp | 12 +- src/Views/IMEView.cpp | 37 ++- src/Views/IMEView.h | 20 +- src/Views/ListView/listItem.cpp | 2 +- src/Views/ListView/searchView.cpp | 8 +- src/Views/ProgressView/progressView.cpp | 19 +- src/Views/ProgressView/progressView.h | 5 +- src/Views/commonDialog.h | 20 ++ src/Views/dialogView.cpp | 242 ++++++++++++++ src/Views/dialogView.h | 56 ++++ src/Views/splash.h | 4 +- src/Views/statusBar.cpp | 2 +- src/activity.cpp | 45 +-- src/activity.h | 2 +- src/concurrency.cpp | 33 ++ src/concurrency.h | 18 + src/database.cpp | 14 +- src/database.h | 2 +- src/debug.cpp | 4 +- src/debug.h | 15 +- src/fetch_load_icons_thread.cpp | 4 +- src/filesystem.cpp | 114 ++++++- src/filesystem.h | 9 +- src/font.cpp | 71 +++- src/font.h | 14 +- src/global_include.h | 1 + src/macros.h | 15 +- src/network.cpp | 17 +- src/network.h | 1 + src/shapes.h | 21 +- src/update.cpp | 245 ++++++++++++++ src/update.h | 22 ++ src/utils.cpp | 90 +++++ src/utils.h | 12 + src/vhbb.cpp | 53 ++- src/vitaPackage.cpp | 386 ++++++++++++++-------- src/vitaPackage.h | 47 ++- src_updater/CMakeLists.txt | 71 ++++ src_updater/debug.cpp | 111 +++++++ src_updater/debug.h | 32 ++ src_updater/updater.cpp | 229 +++++++++++++ 50 files changed, 2067 insertions(+), 464 deletions(-) create mode 100644 assets/.gitignore create mode 100644 assets/spr/img_dialog_msg_bg.png create mode 100644 assets/spr/img_dialog_msg_btn.png create mode 100644 assets/spr/img_dialog_msg_btn_active.png create mode 100644 assets/spr/img_dialog_msg_btn_focus.png create mode 100644 assets/spr/img_updater_icon.png create mode 100644 release/.gitignore create mode 100644 release/version.yml create mode 100644 src/Views/commonDialog.h create mode 100644 src/Views/dialogView.cpp create mode 100644 src/Views/dialogView.h create mode 100644 src/concurrency.cpp create mode 100644 src/concurrency.h create mode 100644 src/update.cpp create mode 100644 src/update.h create mode 100644 src_updater/CMakeLists.txt create mode 100644 src_updater/debug.cpp create mode 100644 src_updater/debug.h create mode 100644 src_updater/updater.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 584339c..c6338ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,177 +1,225 @@ -cmake_minimum_required(VERSION 2.8) - -if(NOT DEFINED CMAKE_TOOLCHAIN_FILE) - if(DEFINED ENV{VITASDK}) - set(CMAKE_TOOLCHAIN_FILE "$ENV{VITASDK}/share/vita.toolchain.cmake" CACHE PATH "toolchain file") - else() - message(FATAL_ERROR "Please define VITASDK to point to your SDK path!") - endif() -endif() - - -set(SHORT_NAME VitaHBBrowser) -project(${SHORT_NAME}) -include("${VITASDK}/share/vita.cmake" REQUIRED) - -set(ENV{PKG_CONFIG_PATH} "$ENV{VITASDK}/arm-vita-eabi/lib/pkgconfig") -include(FindPkgConfig) - -set(VITA_APP_NAME "Vita Homebrew Browser") -set(VITA_TITLEID "VHBB00001") -set(VITA_VERSION "00.83") -add_definitions(-DVITA_VERSION="${VITA_VERSION}") - -option(DEBUGNET "Enable debugnet for logging" ON) - -# Default build type -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE Debug CACHE STRING "Default build" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release") # For CMake GUI -endif() - - -find_package(Git) -if(NOT Git_FOUND) - message(WARNING "Git not found, using unknown as tag...") - add_definitions(-DGIT_COMMIT="unknown") -else() - execute_process( - COMMAND ${GIT_EXECUTABLE} rev-parse HEAD - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} - OUTPUT_VARIABLE DGIT_COMMIT - OUTPUT_STRIP_TRAILING_WHITESPACE - ERROR_QUIET - ) - - add_definitions(-DGIT_COMMIT="${DGIT_COMMIT}") -endif() - - -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-sign-compare -Wno-unused-parameter -std=c++11") - -#set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -Og") -#set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -Og") - -#set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -Os") -#set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Os") - -find_package(CURL REQUIRED) -find_package(OpenSSL REQUIRED) -pkg_check_modules(CURLPP REQUIRED curlpp) - -include_directories( - src/ -) - -FUNCTION(ADD_RESOURCES out_var) - SET(result) - FOREACH(in_f ${ARGN}) - SET(out_f "${CMAKE_CURRENT_BINARY_DIR}/${in_f}.o") - GET_FILENAME_COMPONENT(out_dir ${out_f} DIRECTORY) - ADD_CUSTOM_COMMAND(OUTPUT ${out_f} - COMMAND ${CMAKE_COMMAND} -E make_directory ${out_dir} - COMMAND ${CMAKE_LINKER} -r -b binary -o ${out_f} ${in_f} - DEPENDS ${in_f} - WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} - COMMENT "Building resource ${out_f}" - VERBATIM - ) - LIST(APPEND result ${out_f}) - ENDFOREACH() - SET(${out_var} "${result}" PARENT_SCOPE) -ENDFUNCTION() - -file(GLOB_RECURSE res_files RELATIVE ${CMAKE_SOURCE_DIR} assets/head.bin assets/*.png assets/*.jpeg assets/*.yml assets/*.ttf assets/*.wav assets/*.ogg) -add_resources(PROJECT_RESOURCES ${res_files}) - -file(GLOB_RECURSE PROJECT_SOURCE_FILES "src/*.h" "src/*.hpp" "src/*.cpp" "src/*.c") - -set(VITA_ELF_NAME ${SHORT_NAME}.elf) -add_executable(${VITA_ELF_NAME} - ${PROJECT_RESOURCES} - ${PROJECT_SOURCE_FILES} -) - -if(CMAKE_BUILD_TYPE MATCHES Debug) - message("Debug mode") - add_definitions(-DDEBUG) - if(DEBUGNET) - message("Debugnet turned on") - add_definitions(-DDEBUGNET) - target_link_libraries(${VITA_ELF_NAME} debugnet) - file(STRINGS "debugnetip.txt" DEBUGNETIP) - add_definitions(-DDEBUGNETIP="${DEBUGNETIP}") - endif() -endif(CMAKE_BUILD_TYPE MATCHES Debug) - -target_link_libraries(${VITA_ELF_NAME} - yaml-cpp - m - vita2d - SceDisplay_stub - SceGxm_stub - SceSysmodule_stub - SceCtrl_stub - SceTouch_stub - ScePgf_stub - SceCommonDialog_stub - freetype - png - jpeg - z - m - c - SceNet_stub - SceNetCtl_stub - SceHttp_stub - SceSsl_stub - ${CURLPP_LDFLAGS} - ${CURL_LIBRARIES} - ${OPENSSL_LIBRARIES} - ftpvita - SceAppMgr_stub - SceAppUtil_stub - ScePromoterUtil_stub - SceIme_stub - ScePower_stub - SceAudio_stub - SceAudiodec_stub - SceVshBridge_stub - pthread -) - -vita_create_self(${SHORT_NAME}.self ${VITA_ELF_NAME} UNSAFE UNCOMPRESSED) -vita_create_vpk(${SHORT_NAME}.vpk ${VITA_TITLEID} ${SHORT_NAME}.self - VERSION ${VITA_VERSION} - NAME ${VITA_APP_NAME} - FILE sce_sys/icon0.png sce_sys/icon0.png - FILE sce_sys/livearea/contents/bg.png sce_sys/livearea/contents/bg.png - FILE sce_sys/livearea/contents/startup.png sce_sys/livearea/contents/startup.png - FILE sce_sys/livearea/contents/template.xml sce_sys/livearea/contents/template.xml - - FILE assets/icons.zip resources/icons.zip - FILE assets/fonts/segoeui.ttf resources/fonts/segoeui.ttf -) - -get_filename_component(psvitaipFilePath psvitaip.txt REALPATH) -if(EXISTS "${psvitaipFilePath}") - file(STRINGS "${psvitaipFilePath}" PSVITAIP) - - add_custom_target(send - COMMAND curl -T ${SHORT_NAME}.self ftp://${PSVITAIP}:1337/ux0:/app/${VITA_TITLEID}/eboot.bin - DEPENDS ${SHORT_NAME}.self - ) - - add_custom_target(shellsend - COMMAND psp2shell_cli ${PSVITAIP} 3333 load ${VITA_TITLEID} ${SHORT_NAME}.self - DEPENDS ${SHORT_NAME}.self - ) - - add_custom_target(vpksend - COMMAND curl -T ${SHORT_NAME}.vpk ftp://${PSVITAIP}:1337/ux0:/ - DEPENDS ${SHORT_NAME}.vpk - ) -else() - message("Couldn't find psvitaip.txt. Put the IP-address of you vita there to get new build targets.") -endif() +cmake_minimum_required(VERSION 2.8) + +if(NOT DEFINED CMAKE_TOOLCHAIN_FILE) + if(DEFINED ENV{VITASDK}) + set(CMAKE_TOOLCHAIN_FILE "$ENV{VITASDK}/share/vita.toolchain.cmake" CACHE PATH "toolchain file") + else() + message(FATAL_ERROR "Please define VITASDK to point to your SDK path!") + endif() +endif() + + +set(SHORT_NAME VitaHBBrowser) +project(${SHORT_NAME}) +include("${VITASDK}/share/vita.cmake" REQUIRED) + +set(ENV{PKG_CONFIG_PATH} "$ENV{VITASDK}/arm-vita-eabi/lib/pkgconfig") +include(FindPkgConfig) + +set(VITA_APP_NAME "Vita Homebrew Browser") +set(VITA_TITLEID "VHBB00001") +set(UPDATER_TITLEID "VHBBUPDT1") +set(VITA_VERSION "00.83") + +set(VERSION_YAML_URL "https://vhbb.download/version.php") +set(UPDATE_URL "https://github.com/devnoname120/vhbb/releases/download/${VITA_VERSION}/VitaHBBrowser.vpk") +# Old test URL's +#set(VERSION_YAML_URL "https://github.com/robsdedude/vhbb/raw/updateTestBranch/release/version.yml") +#set(UPDATE_URL "https://github.com/robsdedude/vhbb/raw/updateTestBranch/release/VitaHBBrowser.vpk") + + +set(PACKAGE_TEMP_FOLDER "ux0:/temp/pkg/") + +add_subdirectory(src_updater) +create_updater("${SHORT_NAME}" "${VITA_TITLEID}" ${UPDATER_TITLEID} "${PACKAGE_TEMP_FOLDER}") + +option(DEBUGNET "Enable debugnet for logging" ON) + +# Default build type +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Debug CACHE STRING "Default build" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release") # For CMake GUI +endif() + + +find_package(Git) +if(NOT Git_FOUND) + message(WARNING "Git not found, using unknown as tag...") + add_definitions(-DGIT_COMMIT="unknown") +else() + execute_process( + COMMAND ${GIT_EXECUTABLE} rev-parse HEAD + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + OUTPUT_VARIABLE DGIT_COMMIT + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + + add_definitions(-DGIT_COMMIT="${DGIT_COMMIT}") +endif() + + +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wno-sign-compare -Wno-unused-parameter -std=c++11") + +#set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -Og") +#set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -Og") + +#set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -Os") +#set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -Os") + +find_package(CURL REQUIRED) +find_package(OpenSSL REQUIRED) +pkg_check_modules(CURLPP REQUIRED curlpp) + +include_directories( + src/ +) + +FUNCTION(ADD_RESOURCES out_var) + SET(result) + FOREACH(in_f ${ARGN}) + SET(out_f "${CMAKE_CURRENT_BINARY_DIR}/${in_f}.o") + GET_FILENAME_COMPONENT(out_dir ${out_f} DIRECTORY) + ADD_CUSTOM_COMMAND(OUTPUT ${out_f} + COMMAND ${CMAKE_COMMAND} -E make_directory ${out_dir} + COMMAND ${CMAKE_LINKER} -r -b binary -o ${out_f} ${in_f} + DEPENDS ${in_f} + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Building resource ${out_f}" + VERBATIM + ) + LIST(APPEND result ${out_f}) + ENDFOREACH() + SET(${out_var} "${result}" PARENT_SCOPE) +ENDFUNCTION() + +file(GLOB_RECURSE res_files RELATIVE ${CMAKE_SOURCE_DIR} + assets/head.bin + assets/*.png + assets/*.jpeg + assets/*.yml + assets/*.ttf + assets/*.wav + assets/*.ogg) + +add_resources(PROJECT_RESOURCES ${res_files}) + +file(GLOB_RECURSE PROJECT_SOURCE_FILES "src/*.h" "src/*.hpp" "src/*.cpp" "src/*.c") + +set(VITA_ELF_NAME ${SHORT_NAME}.elf) +add_executable(${VITA_ELF_NAME} + ${PROJECT_RESOURCES} + ${PROJECT_SOURCE_FILES} +) + +target_compile_definitions(${VITA_ELF_NAME} + PRIVATE + VITA_VERSION="${VITA_VERSION}" + VITA_TITLEID="${VITA_TITLEID}" + VHBB_SHORT_NAME="${SHORT_NAME}" + PACKAGE_TEMP_FOLDER="${PACKAGE_TEMP_FOLDER}" + UPDATER_TITLEID="${UPDATER_TITLEID}" + VERSION_YAML_URL="${VERSION_YAML_URL}" +) + +if(CMAKE_BUILD_TYPE MATCHES Debug) + message("Debug mode") + add_definitions(-DDEBUG) + if(DEBUGNET) + message("Debugnet turned on") + add_definitions(-DDEBUGNET) + target_link_libraries(${VITA_ELF_NAME} debugnet) + file(STRINGS "debugnetip.txt" DEBUGNETIP) + add_definitions(-DDEBUGNETIP="${DEBUGNETIP}") + endif() +endif(CMAKE_BUILD_TYPE MATCHES Debug) + +target_link_libraries(${VITA_ELF_NAME} + yaml-cpp + m + vita2d + SceDisplay_stub + SceGxm_stub + SceSysmodule_stub + SceCtrl_stub + SceTouch_stub + ScePgf_stub + SceCommonDialog_stub + freetype + png + jpeg + z + m + c + SceNet_stub + SceNetCtl_stub + SceHttp_stub + SceSsl_stub + ${CURLPP_LDFLAGS} + ${CURL_LIBRARIES} + ${OPENSSL_LIBRARIES} + ftpvita + SceAppMgr_stub + SceAppUtil_stub + ScePromoterUtil_stub + SceIme_stub + ScePower_stub + SceAudio_stub + SceAudiodec_stub + SceVshBridge_stub + pthread +) + +vita_create_self(${SHORT_NAME}.self ${VITA_ELF_NAME} UNSAFE UNCOMPRESSED) +vita_create_vpk(${SHORT_NAME}.vpk ${VITA_TITLEID} ${SHORT_NAME}.self + VERSION ${VITA_VERSION} + NAME ${VITA_APP_NAME} + FILE sce_sys/icon0.png sce_sys/icon0.png + FILE sce_sys/livearea/contents/bg.png sce_sys/livearea/contents/bg.png + FILE sce_sys/livearea/contents/startup.png sce_sys/livearea/contents/startup.png + FILE sce_sys/livearea/contents/template.xml sce_sys/livearea/contents/template.xml + + FILE assets/icons.zip resources/icons.zip + FILE assets/fonts/segoeui.ttf resources/fonts/segoeui.ttf + + FILE ${CMAKE_BINARY_DIR}/${${SHORT_NAME}_UPDATER_EBOOT_NAME} resources/updater/eboot.bin + FILE ${CMAKE_BINARY_DIR}/${${SHORT_NAME}_UPDATER_SFO_NAME} resources/updater/param.sfo +) + +add_dependencies(${SHORT_NAME}.vpk_ + ${${SHORT_NAME}_UPDATER_EBOOT_NAME} + ${${SHORT_NAME}_UPDATER_SFO_NAME} +) + +set(RELEASE_DIR ${CMAKE_SOURCE_DIR}/release) +add_custom_target(release + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_BINARY_DIR}/${SHORT_NAME}.vpk ${RELEASE_DIR}/ + COMMAND ${CMAKE_COMMAND} -E echo "version: \"${VITA_VERSION}\"" > "${RELEASE_DIR}/version.yml" + COMMAND ${CMAKE_COMMAND} -E echo "url: \"${UPDATE_URL}\"" >> "${RELEASE_DIR}/version.yml" + VERBATIM + COMMENT "Creating new release files" + DEPENDS ${SHORT_NAME}.vpk +) + +get_filename_component(psvitaipFilePath psvitaip.txt REALPATH) +if(EXISTS "${psvitaipFilePath}") + file(STRINGS "${psvitaipFilePath}" PSVITAIP) + + add_custom_target(send + COMMAND curl -T ${SHORT_NAME}.self ftp://${PSVITAIP}:1337/ux0:/app/${VITA_TITLEID}/eboot.bin + DEPENDS ${SHORT_NAME}.self + ) + + add_custom_target(shellsend + COMMAND psp2shell_cli ${PSVITAIP} 3333 load ${VITA_TITLEID} ${SHORT_NAME}.self + DEPENDS ${SHORT_NAME}.self + ) + + add_custom_target(vpksend + COMMAND curl -T ${SHORT_NAME}.vpk ftp://${PSVITAIP}:1337/ux0:/ + DEPENDS ${SHORT_NAME}.vpk + ) +else() + message("Couldn't find psvitaip.txt. Put the IP-address of you vita there to get new build targets.") +endif() diff --git a/assets/.gitignore b/assets/.gitignore new file mode 100644 index 0000000..022ecdf --- /dev/null +++ b/assets/.gitignore @@ -0,0 +1 @@ +/updater/ \ No newline at end of file diff --git a/assets/spr/img_dialog_msg_bg.png b/assets/spr/img_dialog_msg_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..7beb25cb69191139416057e42819f30af1e2b82f GIT binary patch literal 1340304 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 literal 0 HcmV?d00001 diff --git a/assets/spr/img_dialog_msg_btn.png b/assets/spr/img_dialog_msg_btn.png new file mode 100644 index 0000000000000000000000000000000000000000..d4c0af1b4ab0227a5bcacb9fe650c75efb59f37c GIT binary patch literal 72174 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 Df{UJX literal 0 HcmV?d00001 diff --git a/assets/spr/img_dialog_msg_btn_focus.png b/assets/spr/img_dialog_msg_btn_focus.png new file mode 100644 index 0000000000000000000000000000000000000000..f7b4492a1416c9f50f79deebd5e00c921ef53569 GIT binary patch literal 72174 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 literal 0 HcmV?d00001 diff --git a/assets/spr/img_updater_icon.png b/assets/spr/img_updater_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..387850fdfd0edb9b730ec875dcf233e02b6144f2 GIT binary patch literal 2768 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 ubDVSDownload(SERVER_BASE_URL + path, SCREENSHOTS_FOLDER + "/" + filename); + Network::get_instance()->Download(std::string(SERVER_BASE_URL) + path, std::string(SCREENSHOTS_FOLDER "/") + filename); // FIXME Should give false to Texture() so as not to cache but for some reason the destructor is called and so the vita2d resource is freed (cf ~Texture()) - screenshots.emplace_back(SCREENSHOTS_FOLDER + "/" + filename, false); - sceIoRemove((SCREENSHOTS_FOLDER + "/" + filename).c_str()); + screenshots.emplace_back(std::string(SCREENSHOTS_FOLDER "/") + filename, false); + sceIoRemove((std::string(SCREENSHOTS_FOLDER "/") + filename).c_str()); } catch (const std::exception &ex) { log_printf(DBG_ERROR, "Cannot download screenshot %s", path.c_str()); } @@ -138,7 +138,7 @@ int HomebrewView::Display() sb.Display(); img_preview_infobg.Draw(Point(HB_X, HB_Y + 300)); - + font_40.Draw(Point(HB_X + 225, HB_Y + 88), hb_.name, COLOR_WHITE); font_25.Draw(Point(HB_X + 225, HB_Y + 115), hb_.author, COLOR_AQUA); font_25.Draw(Point(HB_X + 225, HB_Y + 144), hb_.version, COLOR_WHITE); diff --git a/src/Views/IMEView.cpp b/src/Views/IMEView.cpp index 267d7b0..8c29d5d 100644 --- a/src/Views/IMEView.cpp +++ b/src/Views/IMEView.cpp @@ -2,17 +2,12 @@ #include -#include - -#include - #include "IMEView.h" IMEView::IMEView() { log_printf(DBG_DEBUG, "IMEView::IMEView()"); - auto sce_common_dialog_config_param = SceCommonDialogConfigParam{}; - sceCommonDialogSetConfigParam(&sce_common_dialog_config_param); + commonDialogSetConfig(); // FIXME HACK: when IMEView is passed to Activity::AddView() it's destroyed once the activity is closed // Keeping an internal shared_ptr of itself makes sure that it's never destroyed me_ptr = std::shared_ptr(this); @@ -37,6 +32,12 @@ void IMEView::closeIMEView() { void IMEView::prepare(std::shared_ptr result, std::string title, std::string initialText, SceUInt32 maxInputLength) { log_printf(DBG_DEBUG, "Created IMEView \"%s\"", title.c_str()); + if (_status == COMMON_DIALOG_STATUS_RUNNING) { + log_printf(DBG_WARNING, "Canceling current IMEView"); + sceImeDialogTerm(); + if (_result) + _result->status = COMMON_DIALOG_STATUS_CANCELED; + } std::wstring_convert, char16_t> converter; _title = converter.from_bytes(title); _result = result; @@ -89,37 +90,37 @@ int IMEView::Display() { "https://github.com/vitasdk/vita-headers/blob/master/include/psp2/common_dialog.h", res); if (_result) - _result->status = IMEVIEW_STATUS_CANCELED; + _result->status = COMMON_DIALOG_STATUS_CANCELED; request_destroy = true; sceImeDialogTerm(); } return 0; } - auto new_status = (IMEViewStatus)sceImeDialogGetStatus(); + auto new_status = (CommonDialogStatus)sceImeDialogGetStatus(); if (_status != new_status) switch (new_status) { - case IMEVIEW_STATUS_NONE: - log_printf(DBG_DEBUG, "IMEView status \"IMEVIEW_STATUS_NONE\""); + case COMMON_DIALOG_STATUS_NONE: + log_printf(DBG_DEBUG, "IMEView status \"COMMON_DIALOG_STATUS_NONE\""); break; - case IMEVIEW_STATUS_RUNNING: - log_printf(DBG_DEBUG, "IMEView status \"IMEVIEW_STATUS_RUNNING\""); + case COMMON_DIALOG_STATUS_RUNNING: + log_printf(DBG_DEBUG, "IMEView status \"COMMON_DIALOG_STATUS_RUNNING\""); break; - case IMEVIEW_STATUS_FINISHED: - log_printf(DBG_DEBUG, "IMEView status \"IMEVIEW_STATUS_FINISHED\""); + case COMMON_DIALOG_STATUS_FINISHED: + log_printf(DBG_DEBUG, "IMEView status \"COMMON_DIALOG_STATUS_FINISHED\""); break; - case IMEVIEW_STATUS_CANCELED: - log_printf(DBG_DEBUG, "IMEView status \"IMEVIEW_STATUS_CANCELED\""); + case COMMON_DIALOG_STATUS_CANCELED: + log_printf(DBG_DEBUG, "IMEView status \"COMMON_DIALOG_STATUS_CANCELED\""); break; } _status = new_status; - if (_status == IMEVIEW_STATUS_FINISHED) { + if (_status == COMMON_DIALOG_STATUS_FINISHED) { SceImeDialogResult result={}; sceImeDialogGetResult(&result); if (result.button == SCE_IME_DIALOG_BUTTON_CLOSE) - _status = IMEVIEW_STATUS_CANCELED; + _status = COMMON_DIALOG_STATUS_CANCELED; else { std::wstring_convert, char16_t> converter; _input_text_buffer_utf8 = converter.to_bytes((char16_t*)_input_text_buffer_utf16); diff --git a/src/Views/IMEView.h b/src/Views/IMEView.h index 167b971..f52dc5b 100644 --- a/src/Views/IMEView.h +++ b/src/Views/IMEView.h @@ -6,21 +6,15 @@ #include #include -#include -#include -#include -#include +#include "../global_include.h" +#include "commonDialog.h" +#include "../singleton.h" +#include "View.h" +#include "activity.h" -enum IMEViewStatus { - IMEVIEW_STATUS_NONE = SCE_COMMON_DIALOG_STATUS_NONE, - IMEVIEW_STATUS_RUNNING = SCE_COMMON_DIALOG_STATUS_RUNNING, - IMEVIEW_STATUS_FINISHED = SCE_COMMON_DIALOG_STATUS_FINISHED, - IMEVIEW_STATUS_CANCELED -}; - struct IMEViewResult { - IMEViewStatus status = IMEVIEW_STATUS_NONE; + CommonDialogStatus status = COMMON_DIALOG_STATUS_NONE; std::string userText = ""; }; @@ -48,7 +42,7 @@ class IMEView : Singleton, public View { SceUInt32 _maxTextLength; SceWChar16 *_input_text_buffer_utf16 = nullptr; std::string _input_text_buffer_utf8; - IMEViewStatus _status = IMEVIEW_STATUS_NONE; + CommonDialogStatus _status = COMMON_DIALOG_STATUS_NONE; std::shared_ptr _result; bool shown_dialog = false; }; diff --git a/src/Views/ListView/listItem.cpp b/src/Views/ListView/listItem.cpp index 24ed44c..a28f579 100644 --- a/src/Views/ListView/listItem.cpp +++ b/src/Views/ListView/listItem.cpp @@ -16,7 +16,7 @@ ListItem::ListItem(Homebrew hb) : font_32(Font(std::string(FONT_DIR "segoeui.ttf"), 32)), img_itm_panel(Texture(&_binary_assets_spr_img_itm_panel_png_start)), img_itm_panel_highlight(Texture(&_binary_assets_spr_img_itm_panel_highlight_png_start)), - img_icon_(Texture(ICONS_FOLDER + "/" + hb.icon)), + img_icon_(Texture(std::string(ICONS_FOLDER "/") + hb.icon)), img_itm_label_game(Texture(&_binary_assets_spr_img_itm_label_game_png_start)), img_itm_label_port(Texture(&_binary_assets_spr_img_itm_label_port_png_start)), img_itm_label_emu(Texture(&_binary_assets_spr_img_itm_label_emu_png_start)), diff --git a/src/Views/ListView/searchView.cpp b/src/Views/ListView/searchView.cpp index 82bfc74..7398c29 100644 --- a/src/Views/ListView/searchView.cpp +++ b/src/Views/ListView/searchView.cpp @@ -9,7 +9,7 @@ void SearchView::startSearch() { void SearchView::SignalSelected() { log_printf(DBG_DEBUG, "SearchView::SignalSelected"); - if (_ime_search_view_result->status != IMEVIEW_STATUS_RUNNING) + if (_ime_search_view_result->status != COMMON_DIALOG_STATUS_RUNNING) startSearch(); } @@ -19,11 +19,11 @@ void SearchView::SignalDeselected() { } bool SearchView::IsReadyToShow() { - return _ime_search_view_result->status == IMEVIEW_STATUS_FINISHED; + return _ime_search_view_result->status == COMMON_DIALOG_STATUS_FINISHED; } int SearchView::Display() { - if (_ime_search_view_result->status == IMEVIEW_STATUS_FINISHED) { + if (_ime_search_view_result->status == COMMON_DIALOG_STATUS_FINISHED) { log_printf(DBG_DEBUG, "Processing finished search dialog: \"%s\"", _ime_search_view_result->userText.c_str()); if (lastQuery != _ime_search_view_result->userText) { auto db = Database::get_instance(); @@ -37,7 +37,7 @@ int SearchView::Display() { } else { log_printf(DBG_DEBUG, "Ignore search: same filter as before"); } - _ime_search_view_result->status = IMEVIEW_STATUS_NONE; + _ime_search_view_result->status = COMMON_DIALOG_STATUS_NONE; } return ListView::Display(); } diff --git a/src/Views/ProgressView/progressView.cpp b/src/Views/ProgressView/progressView.cpp index b03a449..4472c2b 100644 --- a/src/Views/ProgressView/progressView.cpp +++ b/src/Views/ProgressView/progressView.cpp @@ -11,12 +11,21 @@ extern unsigned char _binary_assets_spr_img_dialog_btn_png_start; ProgressView::ProgressView(InfoProgress progress, Homebrew hb) : - progress_(std::move(progress)), - hb_(std::move(hb)), + ProgressView(std::move(progress), hb.name, std::string(ICONS_FOLDER "/") + hb.icon) +{ +} + +ProgressView::ProgressView(InfoProgress progress, std::string hb_name, std::string icon_path) : + ProgressView(std::move(progress), std::move(hb_name), Texture(icon_path)) +{ +} +ProgressView::ProgressView(InfoProgress progress, std::string hb_name, Texture icon_texture) : + hb_name(std::move(hb_name)), + progress_(std::move(progress)), font_24(Font(std::string(FONT_DIR "segoeui.ttf"), 24)), //thid_(thid), - img_icon(Texture(ICONS_FOLDER + "/" + hb_.icon)), - img_dialog_progress_bg(Texture(&_binary_assets_spr_img_dialog_progress_bg_png_start)), + img_icon(icon_texture), + img_dialog_progress_bg(Texture(&_binary_assets_spr_img_dialog_progress_bg_png_start)), img_dialog_progress_bar(Texture(&_binary_assets_spr_img_dialog_progress_bar_png_start)), img_dialog_progress_bar_glow(Texture(&_binary_assets_spr_img_dialog_progress_bar_glow_png_start)), img_dialog_btn(Texture(&_binary_assets_spr_img_dialog_btn_png_start)) @@ -40,7 +49,7 @@ int ProgressView::Display() // Background img_dialog_progress_bg.Draw(Point(PROGRESS_VIEW_X, PROGRESS_VIEW_Y)); // Name - font_24.Draw(Point(PROGRESS_VIEW_X + 197, PROGRESS_VIEW_Y + 53), hb_.name); + font_24.Draw(Point(PROGRESS_VIEW_X + 197, PROGRESS_VIEW_Y + 53), hb_name); // Message font_24.Draw(Point(PROGRESS_VIEW_X + 197, PROGRESS_VIEW_Y + 117), progress_.message()); // Progress bar diff --git a/src/Views/ProgressView/progressView.h b/src/Views/ProgressView/progressView.h index 95f4a89..0520a0a 100644 --- a/src/Views/ProgressView/progressView.h +++ b/src/Views/ProgressView/progressView.h @@ -19,16 +19,19 @@ class ProgressView: public View { public: ProgressView(InfoProgress progress, Homebrew hb); + ProgressView(InfoProgress progress, std::string hb_name, std::string icon_path); + ProgressView(InfoProgress progress, std::string hb_name, Texture icon_texture); int HandleInput(int focus, const Input& input) override; int Display() override; // Wait in millisecond void Finish(unsigned int wait = 300); + + std::string hb_name; private: uint32_t finish_tick = 0; InfoProgress progress_; - Homebrew hb_; Font font_24; Texture img_icon; diff --git a/src/Views/commonDialog.h b/src/Views/commonDialog.h new file mode 100644 index 0000000..e8eb757 --- /dev/null +++ b/src/Views/commonDialog.h @@ -0,0 +1,20 @@ +# pragma once + +#include "../global_include.h" + +enum CommonDialogStatus { + COMMON_DIALOG_STATUS_NONE = SCE_COMMON_DIALOG_STATUS_NONE, + COMMON_DIALOG_STATUS_RUNNING = SCE_COMMON_DIALOG_STATUS_RUNNING, + COMMON_DIALOG_STATUS_FINISHED = SCE_COMMON_DIALOG_STATUS_FINISHED, + COMMON_DIALOG_STATUS_CANCELED +}; + +inline void commonDialogSetConfig() { + SceCommonDialogConfigParam sce_common_dialog_config_param; + sceCommonDialogConfigParamInit(&sce_common_dialog_config_param); + sce_common_dialog_config_param.language = SCE_SYSTEM_PARAM_LANG_ENGLISH_US; + sce_common_dialog_config_param.enterButtonAssign = SCE_SYSTEM_PARAM_ENTER_BUTTON_CROSS; + int res = sceCommonDialogSetConfigParam(&sce_common_dialog_config_param); + if (res) + log_printf(DBG_ERROR, "sceCommonDialogSetConfigParam failed: %0.8x", res); +}; diff --git a/src/Views/dialogView.cpp b/src/Views/dialogView.cpp new file mode 100644 index 0000000..555602b --- /dev/null +++ b/src/Views/dialogView.cpp @@ -0,0 +1,242 @@ +#include + +#include "dialogView.h" + +#define DIALOG_WIDTH 760 +#define DIALOG_HEIGHT 440 +#define DIALOG_PADDING_X 79 +#define DIALOG_PADDING_Y 30 +#define DIALOG_BTN_WIDTH 322 +#define DIALOG_BTN_HEIGHT 56 +#define DIALOG_BTN_PADDING 5 +#define DIALOG_MSG_HEIGHT 315 +#define DIALOG_X ((SCREEN_WIDTH-DIALOG_WIDTH)/2) +#define DIALOG_Y ((SCREEN_HEIGHT-DIALOG_HEIGHT)/2) +#define DIALOG_BTN_Y (DIALOG_Y+365) +#define DIALOG_BTN_INNER_TOP_Y (DIALOG_BTN_Y+DIALOG_BTN_PADDING) +#define DIALOG_BTN_INNER_BOT_Y (DIALOG_BTN_Y+DIALOG_BTN_HEIGHT-DIALOG_BTN_PADDING) +#define DIALOG_BTN_1_OF_1_X (DIALOG_X+DIALOG_WIDTH/2-DIALOG_BTN_WIDTH/2) +#define DIALOG_BTN_1_OF_1_INNER_LEFT_X (DIALOG_BTN_1_OF_1_X+DIALOG_BTN_PADDING) +#define DIALOG_BTN_1_OF_1_INNER_RIGHT_X (DIALOG_BTN_1_OF_1_X+DIALOG_BTN_WIDTH-DIALOG_BTN_PADDING) +#define DIALOG_BTN_1_OF_2_X (DIALOG_X+44) +#define DIALOG_BTN_1_OF_2_INNER_LEFT_X (DIALOG_BTN_1_OF_2_X+DIALOG_BTN_PADDING) +#define DIALOG_BTN_1_OF_2_INNER_RIGHT_X (DIALOG_BTN_1_OF_2_X+DIALOG_BTN_WIDTH-DIALOG_BTN_PADDING) +#define DIALOG_BTN_2_OF_2_X (DIALOG_X+394) +#define DIALOG_BTN_2_OF_2_INNER_LEFT_X (DIALOG_BTN_2_OF_2_X+DIALOG_BTN_PADDING) +#define DIALOG_BTN_2_OF_2_INNER_RIGHT_X (DIALOG_BTN_2_OF_2_X+DIALOG_BTN_WIDTH-DIALOG_BTN_PADDING) +#define DIALOG_FOCUS_GLOW_HALF_CYCLE_FRAMES 45 + + +extern unsigned char _binary_assets_spr_img_dialog_msg_bg_png_start; +extern unsigned char _binary_assets_spr_img_dialog_msg_btn_png_start; +extern unsigned char _binary_assets_spr_img_dialog_msg_btn_active_png_start; +extern unsigned char _binary_assets_spr_img_dialog_msg_btn_focus_png_start; + +DialogView::DialogView() : + msg_font(Font(std::string(FONT_DIR "segoeui.ttf"), 28)), + btn_font(Font(std::string(FONT_DIR "segoeui.ttf"), 25)), + img_dialog_msg_bg(Texture(&_binary_assets_spr_img_dialog_msg_bg_png_start)), + img_dialog_msg_btn(Texture(&_binary_assets_spr_img_dialog_msg_btn_png_start)), + img_dialog_msg_btn_active(Texture(&_binary_assets_spr_img_dialog_msg_btn_active_png_start)), + img_dialog_msg_btn_focus(Texture(&_binary_assets_spr_img_dialog_msg_btn_focus_png_start)) +{ + priority = 550; + log_printf(DBG_DEBUG, "DialogView::DialogView()"); + // FIXME HACK: when DialogView is passed to Activity::AddView() it's destroyed once the activity is crashed + // Keeping an internal shared_ptr of itself makes sure that it's never destroyed + me_ptr = std::shared_ptr(this); +} + +DialogView::~DialogView() { + log_printf(DBG_WARNING, "DialogView destructor called"); +} + +void DialogView::openDialogView(std::shared_ptr result, std::string message, DialogType type) { + DialogView *dialogView = DialogView::create_instance(); + dialogView->prepare(std::move(result), std::move(message), type); + Activity::get_instance()->AddView(dialogView->me_ptr); +} + +void DialogView::prepare(std::shared_ptr result, std::string message, DialogType type) { + log_printf(DBG_DEBUG, "Created DialogView \"%s\"", message.c_str()); + if (_status == COMMON_DIALOG_STATUS_RUNNING) { + log_printf(DBG_WARNING, "Canceling current DialogView"); + request_destroy = true; + _status = COMMON_DIALOG_STATUS_CANCELED; + if (_result) + _result->status = COMMON_DIALOG_STATUS_CANCELED; + } + _result = result; + _message = msg_font.FitString(message, DIALOG_WIDTH - DIALOG_PADDING_X * 2); + request_destroy = false; + _accepted = false; + _status = COMMON_DIALOG_STATUS_RUNNING; + _btnTouched = _btnFocus = -1; + if (_result) { + _result->status = COMMON_DIALOG_STATUS_RUNNING; + } + _type = type; +} + +int DialogView::GetGlowCycleAlpha() { + float alphaCount; + if (_focusGlowCycle <= DIALOG_FOCUS_GLOW_HALF_CYCLE_FRAMES) { + alphaCount = _focusGlowCycle; + } else { + alphaCount = DIALOG_FOCUS_GLOW_HALF_CYCLE_FRAMES * 2 - _focusGlowCycle; + } + return int(alphaCount / DIALOG_FOCUS_GLOW_HALF_CYCLE_FRAMES * 255); +} + +void DialogView::HandleBtnFocus(const Input& input) { + auto btnFocusBefore = _btnFocus; + if (input.KeyNewPressed(SCE_CTRL_RIGHT)) { + switch (_type) { + case DIALOG_TYPE_OK: + _btnFocus = 0; + break; + case DIALOG_TYPE_YESNO: + _btnFocus = 1; + break; + } + log_printf(DBG_DEBUG, "SCE_CTRL_RIGHT, _btnFocus %i", _btnFocus); + } + if (input.KeyNewPressed(SCE_CTRL_LEFT) || input.KeyNewPressed(SCE_CTRL_UP) || input.KeyNewPressed(SCE_CTRL_DOWN)) { + _btnFocus = 0; + log_printf(DBG_DEBUG, "SCE_CTRL_LEFT||SCE_CTRL_UP||SCE_CTRL_DOWN , _btnFocus %i", _btnFocus); + } + if (input.TouchPressed()) { + _btnFocus = -1; + } + if (_btnFocus >= 0) { + if (btnFocusBefore < 0 || // focus button after none was focused + (btnFocusBefore != _btnFocus && GetGlowCycleAlpha() < 200)) { // change focused button and highlight was nearly gone + log_printf(DBG_DEBUG, "Resetting glow cycle"); + _focusGlowCycle = 0; + } + } + if (input.KeyNewPressed(SCE_CTRL_CROSS) && _btnFocus >= 0) { + log_printf(DBG_DEBUG, "SCE_CTRL_CROSS, choosing button %i", _btnFocus); + _status = COMMON_DIALOG_STATUS_FINISHED; + if (_type == DIALOG_TYPE_YESNO && _btnFocus == 1) { + _accepted = true; + } + } +} + +int DialogView::GetTouchedBtnIdx(const Input &input) { + if (!input.TouchPressed()) return -1; + switch (_type) { + case DIALOG_TYPE_OK: { + auto btn_tl = Point(DIALOG_BTN_1_OF_1_INNER_LEFT_X, DIALOG_BTN_INNER_TOP_Y); + auto btn_br = Point(DIALOG_BTN_1_OF_1_INNER_RIGHT_X, DIALOG_BTN_INNER_BOT_Y); + if (input.TouchInRectangle(Rectangle(btn_tl, btn_br))) { + return 0; + } + break; + } + case DIALOG_TYPE_YESNO: + // NO button + auto btn_tl = Point(DIALOG_BTN_1_OF_2_INNER_LEFT_X, DIALOG_BTN_INNER_TOP_Y); + auto btn_br = Point(DIALOG_BTN_1_OF_2_INNER_RIGHT_X, DIALOG_BTN_INNER_BOT_Y); + if (input.TouchInRectangle(Rectangle(btn_tl, btn_br))) { + return 0; + } + // YES button + btn_tl = Point(DIALOG_BTN_2_OF_2_INNER_LEFT_X, DIALOG_BTN_INNER_TOP_Y); + btn_br = Point(DIALOG_BTN_2_OF_2_INNER_RIGHT_X, DIALOG_BTN_INNER_BOT_Y); + if (input.TouchInRectangle(Rectangle(btn_tl, btn_br))) { + return 1; + } + break; + } + return -1; +} + +void DialogView::HandleBtnTouch(const Input& input) { + if (input.TouchPressed()) { + if (input.TouchNewPressed()) { + _btnTouched = GetTouchedBtnIdx(input); + } else if (_btnTouched >= 0 && _btnTouched != GetTouchedBtnIdx(input)){ + _btnTouched = -1; + } + } else { + if (_btnTouched >= 0) { + _status = COMMON_DIALOG_STATUS_FINISHED; + if (_type == DIALOG_TYPE_YESNO && _btnTouched == 1) { + _accepted = true; + } + _btnTouched = -1; + } + } +} + +int DialogView::HandleInput(int focus, const Input& input) { + if(!focus) return 0; + auto oldStatus = _status; + HandleBtnFocus(input); + HandleBtnTouch(input); + + if (oldStatus != _status) { + if (_result) { + _result->status = _status; + _result->accepted = _accepted; + } + if (_status == COMMON_DIALOG_STATUS_FINISHED || _status == COMMON_DIALOG_STATUS_CANCELED) { + request_destroy = true; + _status = COMMON_DIALOG_STATUS_NONE; + } + } + + return 0; +} + +void DialogView::DrawBtn(const std::string &text, const Point &sprPt, const Rectangle &textRect, int idx) { + if (_btnTouched == idx) { + img_dialog_msg_btn_active.Draw(sprPt); + } else { + img_dialog_msg_btn.Draw(sprPt); + } + btn_font.DrawCentered(textRect, text, COLOR_WHITE, true); + if(_btnFocus == idx) { + img_dialog_msg_btn_focus.DrawExt(sprPt, GetGlowCycleAlpha()); + } +} + +int DialogView::Display() { + if (request_destroy) { // done here! + return 0; + } + + // backdrop + vita2d_draw_rectangle(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, RGBA8(0, 0, 0, 190)); + + img_dialog_msg_bg.Draw(Point(DIALOG_X, DIALOG_Y)); + + msg_font.DrawCenteredVertical(Rectangle(Point(DIALOG_X + DIALOG_PADDING_X, DIALOG_Y + DIALOG_PADDING_Y), + Point(DIALOG_X + DIALOG_WIDTH - DIALOG_PADDING_X, DIALOG_Y + DIALOG_PADDING_Y + DIALOG_MSG_HEIGHT)), + _message, COLOR_WHITE, true); + + switch (_type) { + case DIALOG_TYPE_OK: + DrawBtn("Ok", Point(DIALOG_BTN_1_OF_1_X, DIALOG_BTN_Y), + Rectangle(Point(DIALOG_BTN_1_OF_1_INNER_LEFT_X, DIALOG_BTN_INNER_TOP_Y), + Point(DIALOG_BTN_1_OF_1_INNER_RIGHT_X, DIALOG_BTN_INNER_BOT_Y)), + 0); + break; + case DIALOG_TYPE_YESNO: + DrawBtn("No", Point(DIALOG_BTN_1_OF_2_X, DIALOG_BTN_Y), + Rectangle(Point(DIALOG_BTN_1_OF_2_INNER_LEFT_X, DIALOG_BTN_INNER_TOP_Y), + Point(DIALOG_BTN_1_OF_2_INNER_RIGHT_X, DIALOG_BTN_INNER_BOT_Y)), + 0); + DrawBtn("Yes", Point(DIALOG_BTN_2_OF_2_X, DIALOG_BTN_Y), + Rectangle(Point(DIALOG_BTN_2_OF_2_INNER_LEFT_X, DIALOG_BTN_INNER_TOP_Y), + Point(DIALOG_BTN_2_OF_2_INNER_RIGHT_X, DIALOG_BTN_INNER_BOT_Y)), + 1); + break; + } + + _focusGlowCycle = (_focusGlowCycle + 1) % (DIALOG_FOCUS_GLOW_HALF_CYCLE_FRAMES * 2); + + return 0; +} diff --git a/src/Views/dialogView.h b/src/Views/dialogView.h new file mode 100644 index 0000000..f1b648e --- /dev/null +++ b/src/Views/dialogView.h @@ -0,0 +1,56 @@ +#pragma once + +#include "../global_include.h" +#include "../singleton.h" +#include "../font.h" +#include "commonDialog.h" +#include "activity.h" +#include "View.h" + + +enum DialogType { + DIALOG_TYPE_OK, + DIALOG_TYPE_YESNO +}; + +struct DialogViewResult { + CommonDialogStatus status = COMMON_DIALOG_STATUS_NONE; + bool accepted = false; +}; + +class DialogView : Singleton, public View { +public: + DialogView(); + ~DialogView(); + + static void openDialogView(std::shared_ptr result, std::string message, DialogType type); + + int Display() override; + int HandleInput(int focus, const Input& input) override; + +private: + std::shared_ptr me_ptr; + + Font msg_font; + Font btn_font; + + Texture img_dialog_msg_bg; + Texture img_dialog_msg_btn; + Texture img_dialog_msg_btn_active; + Texture img_dialog_msg_btn_focus; + + void prepare(std::shared_ptr result, std::string message, DialogType type); + void DrawBtn(const std::string &text, const Point &sprPt, const Rectangle &textRect, int idx); + int GetGlowCycleAlpha(); + void HandleBtnFocus(const Input& input); + int GetTouchedBtnIdx(const Input& input); + void HandleBtnTouch(const Input& input); + + CommonDialogStatus _status = COMMON_DIALOG_STATUS_NONE; + bool _accepted = false; + std::shared_ptr _result; + std::string _message; + DialogType _type; + int _btnFocus, _btnTouched = -1; + unsigned int _focusGlowCycle = 0; +}; diff --git a/src/Views/splash.h b/src/Views/splash.h index 0cf4596..79283d8 100644 --- a/src/Views/splash.h +++ b/src/Views/splash.h @@ -7,9 +7,9 @@ #define SPLASH_FADING_STEP_SIZE 4 #ifdef DEBUG - #define SPLASH_STATIC_DURATION_IN_FRAMES 2*60 + #define SPLASH_STATIC_DURATION_IN_FRAMES (2*60) #else - #define SPLASH_STATIC_DURATION_IN_FRAMES 5*60 + #define SPLASH_STATIC_DURATION_IN_FRAMES (5*60) #endif typedef enum { diff --git a/src/Views/statusBar.cpp b/src/Views/statusBar.cpp index e120510..949a214 100644 --- a/src/Views/statusBar.cpp +++ b/src/Views/statusBar.cpp @@ -118,7 +118,7 @@ int StatusBar::displayDate() char string[64]; sprintf(string, "%s %s", date_string, time_string); - + font_22.Draw(Point(700, 22), string, COLOR_WHITE); return 0; diff --git a/src/activity.cpp b/src/activity.cpp index 8aa380f..1d2ea7e 100644 --- a/src/activity.cpp +++ b/src/activity.cpp @@ -5,28 +5,33 @@ Activity::~Activity() = default; int Activity::HandleInput(int focus, const Input& input) { - std::lock_guard lock(mtx_); - - if (views_.size() > 1) { - for (auto it = begin(views_), it_last = --end(views_); it != it_last; ) { - (*it)->HandleInput(0, input); - if ((*it)->request_destroy) { - it = views_.erase(it); - } else { - ++it; - } + { + std::lock_guard lock(mtx_); + + if (views_.size() > 1) { + for (auto it = begin(views_), it_last = --end(views_); it != it_last;) { + (*it)->HandleInput(0, input); + if ((*it)->request_destroy) { + it = views_.erase(it); + } else { + ++it; + } + } + } else if (views_.empty()) { + return 0; } - } else if (views_.empty()) { - return 0; } views_.back()->HandleInput(focus, input); - views_.erase( - std::remove_if(views_.begin(), views_.end(), - [](const std::shared_ptr &view) { return view->request_destroy; }), - views_.end()); + { + std::lock_guard lock(mtx_); + views_.erase( + std::remove_if(views_.begin(), views_.end(), + [](const std::shared_ptr &view) { return view->request_destroy; }), + views_.end()); + } return 0; } @@ -34,7 +39,7 @@ int Activity::HandleInput(int focus, const Input& input) int Activity::Display() { - std::lock_guard lock(mtx_); + std::lock_guard lock(mtx_); if (views_.empty()) return 0; @@ -48,14 +53,14 @@ int Activity::Display() void Activity::AddView(std::shared_ptr view) { - std::lock_guard lock(mtx_); + std::lock_guard lock(mtx_); views_queue.push_back(view); } void Activity::FlushQueue() { - std::lock_guard lock(mtx_); + std::lock_guard lock(mtx_); std::move(views_queue.begin(), views_queue.end(), std::back_inserter(views_)); views_queue.erase(views_queue.begin(),views_queue.end()); @@ -66,7 +71,7 @@ void Activity::FlushQueue() bool Activity::HasActivity() { - std::lock_guard lock(mtx_); + std::lock_guard lock(mtx_); return !views_.empty(); } diff --git a/src/activity.h b/src/activity.h index a01a1e5..819073a 100644 --- a/src/activity.h +++ b/src/activity.h @@ -20,7 +20,7 @@ friend class Singleton; private: - std::mutex mtx_; + SceMutex mtx_ = SceMutex("activity_mtx"); std::vector> views_; std::vector> views_queue; }; diff --git a/src/concurrency.cpp b/src/concurrency.cpp new file mode 100644 index 0000000..fd5d5ef --- /dev/null +++ b/src/concurrency.cpp @@ -0,0 +1,33 @@ +#include "concurrency.h" + +SceMutex::SceMutex(std::string name) : + name(name) +{ + id = sceKernelCreateMutex(name.c_str(), 0, 0, nullptr); +} +SceMutex::~SceMutex() { + int ret = sceKernelDeleteMutex(id); + if (ret < 0) { + log_printf(DBG_ERROR, "%s: sceKernelDeleteMutex(%i) = %i (0x%04x)\n" + "You might find the error code here: https://psp2sdk.github.io/error_8h.html", + name.c_str(), id, ret, ret); + } +} + +void SceMutex::lock() { + int ret = sceKernelLockMutex(id, 1, nullptr); + if (ret < 0) { + log_printf(DBG_ERROR, "%s: sceKernelLockMutex(%i, 1, nullptr) = %i (0x%04x)\n" + "You might find the error code here: https://psp2sdk.github.io/error_8h.html", + name.c_str(), id, ret, ret); + } +} + +void SceMutex::unlock() { + int ret = sceKernelUnlockMutex(id, 1); + if (ret < 0) { + log_printf(DBG_ERROR, "%s: sceKernelUnlockMutex(%i, 1) = %i (0x%04x)\n" + "You might find the error code here: https://psp2sdk.github.io/error_8h.html", + name.c_str(), id, ret, ret); + } +} diff --git a/src/concurrency.h b/src/concurrency.h new file mode 100644 index 0000000..969a09e --- /dev/null +++ b/src/concurrency.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +#include "debug.h" + +class SceMutex { +public: + SceMutex(std::string name="unnamed_mutex"); + ~SceMutex(); + void lock(); + void unlock(); + +private: + SceUID id; + std::string name; +}; diff --git a/src/database.cpp b/src/database.cpp index 1554c29..b306746 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -59,24 +59,24 @@ HomebrewSearchRating SearchQuery::operator()(const Homebrew &hb) const { int Database::DownloadIcons() { - sceIoMkdir(ICONS_FOLDER.c_str(), 0777); + sceIoMkdir(ICONS_FOLDER, 0777); - if (access((ICONS_FOLDER + "/init.txt").c_str(), F_OK) == -1) { + if (access(ICONS_FOLDER "/init.txt", F_OK) == -1) { // TODO display progess to user - auto zip = Zipfile(ICON_ZIP); + auto zip = Zipfile(std::string(ICON_ZIP)); - zip.Unzip(ICONS_FOLDER + "/"); + zip.Unzip(std::string(ICONS_FOLDER "/")); - int fd = sceIoOpen((ICONS_FOLDER + "/init.txt").c_str(), SCE_O_WRONLY|SCE_O_CREAT, 0777); + int fd = sceIoOpen(ICONS_FOLDER "/init.txt", SCE_O_WRONLY|SCE_O_CREAT, 0777); sceIoWrite(fd, "ok", 2); sceIoClose(fd); } for (auto hb : homebrews) { - std::string path = ICONS_FOLDER + "/" + hb.icon; + auto path = std::string(ICONS_FOLDER "/") + hb.icon; if (access(path.c_str(), F_OK) == -1) { - std::string url = ICON_URL_PREFIX + hb.icon; + auto url = std::string(ICON_URL_PREFIX) + hb.icon; Network::get_instance()->Download(url, path); } diff --git a/src/database.h b/src/database.h index df8039b..565e188 100644 --- a/src/database.h +++ b/src/database.h @@ -6,7 +6,7 @@ #include "homebrew.h" #include "singleton.h" -#define ICON_ZIP std::string("ux0:/app/VHBB00001/resources/icons.zip") +#define ICON_ZIP VHBB_RESOURCES "/resources/icons.zip" class Database: public Singleton { friend class Singleton; diff --git a/src/debug.cpp b/src/debug.cpp index bb7deac..fbd30f7 100644 --- a/src/debug.cpp +++ b/src/debug.cpp @@ -17,7 +17,7 @@ int log_init(bool log_to_file) #endif if (log_to_file) { - sceIoMkdir(VHBB_LOG_DIR.c_str(), 0777); + sceIoMkdir(VHBB_LOG_DIR, 0777); SceDateTime logTime; memset(&logTime, 0, sizeof(logTime)); @@ -28,7 +28,7 @@ int log_init(bool log_to_file) snprintf(formattedTime, sizeof(formattedTime), "%04d-%02d-%02d_%02d-%02d-%02d", logTime.year, logTime.month, logTime.day, logTime.hour, logTime.minute, logTime.second); - std::string log_file = VHBB_LOG_DIR + "/VHBB_" + formattedTime + ".log"; + std::string log_file = std::string(VHBB_LOG_DIR "/VHBB_") + formattedTime + ".log"; g_log_fd = sceIoOpen(log_file.c_str(), SCE_O_WRONLY | SCE_O_CREAT | SCE_O_TRUNC, 0777); diff --git a/src/debug.h b/src/debug.h index c34b189..9317810 100644 --- a/src/debug.h +++ b/src/debug.h @@ -2,15 +2,14 @@ #include -#define VHBB_LOG_DIR std::string("ux0:/log") +#define VHBB_LOG_DIR "ux0:/log" -inline std::string methodName(const std::string& prettyFunction) -{ - size_t colons = prettyFunction.find("::"); - size_t begin = prettyFunction.substr(0,colons).rfind(' ') + 1; - size_t end = prettyFunction.rfind('(') - begin; +inline std::string methodName(const std::string &prettyFunction) { + size_t args_start = prettyFunction.find('('); + size_t begin = prettyFunction.substr(0, args_start).rfind(' ') + 1; + size_t length = args_start - begin; - return prettyFunction.substr(begin,end) + "()"; + return prettyFunction.substr(begin, length) + "()"; } @@ -30,4 +29,4 @@ bool log_assert(bool expr); #define log_printf(level,format,...) _log_printf(level,(std::string("[") + std::string(__FILE__) + ":" + std::to_string(__LINE__) + " " +__METHOD_NAME__ + "] " + format + "\n").c_str(),##__VA_ARGS__) #define log_assert(expr) if(expr){log_printf(DBG_ERROR, (std::string("Assertion error ==> ") + #expr).c_str()); return true;}else{return false;} -//#define log_printf(level,format,...) _log_printf(level,format"\n",##__VA_ARGS__) \ No newline at end of file +//#define log_printf(level,format,...) _log_printf(level,format"\n",##__VA_ARGS__) diff --git a/src/fetch_load_icons_thread.cpp b/src/fetch_load_icons_thread.cpp index 0d7ac85..eeb1f0a 100644 --- a/src/fetch_load_icons_thread.cpp +++ b/src/fetch_load_icons_thread.cpp @@ -17,8 +17,8 @@ void FetchLoadIcons(unsigned int arglen, std::atomic *db_done) try { // TODO check if fails auto dl = Network::get_instance(); - dl->Download(API_ENDPOINT, API_LOCAL); - auto db = Database::create_instance(API_LOCAL); + dl->Download(std::string(API_ENDPOINT), std::string(API_LOCAL)); + auto db = Database::create_instance(std::string(API_LOCAL)); db->DownloadIcons(); if (db_done) *db_done = true; diff --git a/src/filesystem.cpp b/src/filesystem.cpp index 1dcf94f..399f5a1 100644 --- a/src/filesystem.cpp +++ b/src/filesystem.cpp @@ -1,6 +1,9 @@ #include "filesystem.h" #include +// TODO: Encapsulate these functions into a Filesystem class + + // Path must end with '/' int removePath(std::string path) { // sceIoDopen doesn't work if there is a '/' at the end @@ -48,4 +51,113 @@ int removePath(std::string path) { } return 1; -} \ No newline at end of file +} + +int readFile(const std::string &path, void *buffer, SceSize size) { + SceUID fd = sceIoOpen(path.c_str(), SCE_O_RDONLY, 0); + if (fd < 0) + return fd; + + int read = sceIoRead(fd, buffer, size); + + sceIoClose(fd); + return read; +} + +int readFile(const std::string &path, std::string &content) { + SceUID fd = sceIoOpen(path.c_str(), SCE_O_RDONLY, 0); + if (fd < 0) + return fd; + + int size = sceIoLseek32(fd, 0, SCE_SEEK_END); + sceIoLseek32(fd, 0, SCE_SEEK_SET); + + if (size < 0) + return size; + + char buf[size + 1]; + + int read = sceIoRead(fd, buf, size); + + if (read < 0) + return read; + + sceIoClose(fd); + + buf[read] = 0; // add null char terminator + + content.reserve((uint) size); + content.assign(buf); + + return read; +} + + +#define CP_BUF_SIZE (128 * 1024) + +int copyFile(const std::string &path_source, const std::string &path_dest) { + // The source and destination paths are identical + if (std_string_iequals(path_source, path_dest)) { + log_printf(DBG_ERROR, "source equals destination: %s", path_source.c_str()); + return -1; + } + + // The destination is a subfolder of the source folder + unsigned int len = path_source.length(); + if (std_string_iequals(path_source.substr(0, len), path_dest.substr(0, len)) && (path_dest[len] == '/' || path_dest[len - 1] == '/')) { + log_printf(DBG_ERROR, "source (%s) is sub-dir of dst (%s)", path_source.c_str(), path_dest.c_str()); + return -2; + } + + SceUID fdsrc = sceIoOpen(path_source.c_str(), SCE_O_RDONLY, 0); + if (fdsrc < 0) { + log_printf(DBG_ERROR, "couldn't open source %s: 0x%08x", path_source.c_str(), fdsrc); + return fdsrc; + } + + SceUID fddst = sceIoOpen(path_dest.c_str(), SCE_O_WRONLY | SCE_O_CREAT | SCE_O_TRUNC, 0777); + if (fddst < 0) { + sceIoClose(fdsrc); + log_printf(DBG_ERROR, "couldn't read destination %s: 0x%08x", path_dest.c_str(), fddst); + return fddst; + } + + std::vector buf(CP_BUF_SIZE); + + while (true) { + int read = sceIoRead(fdsrc, buf.data(), CP_BUF_SIZE); + + if (read < 0) { + log_printf(DBG_ERROR, "source read error %s: 0x%08x", path_source.c_str(), read); + sceIoClose(fddst); + sceIoClose(fdsrc); + sceIoRemove(path_dest.c_str()); + return read; + } + + if (read == 0) + break; + + int written = sceIoWrite(fddst, buf.data(), (SceSize) read); + + if (written < 0) { + log_printf(DBG_ERROR, "destination write error %s: 0x%08x", path_dest.c_str(), written); + sceIoClose(fddst); + sceIoClose(fdsrc); + sceIoRemove(path_dest.c_str()); + return written; + } + + } + + // Inherit file stat + SceIoStat stat; + memset(&stat, 0, sizeof(SceIoStat)); + sceIoGetstatByFd(fdsrc, &stat); + sceIoChstatByFd(fddst, &stat, 0x3B); + + sceIoClose(fddst); + sceIoClose(fdsrc); + + return 0; +} diff --git a/src/filesystem.h b/src/filesystem.h index 8d3990e..a09cce7 100644 --- a/src/filesystem.h +++ b/src/filesystem.h @@ -1,5 +1,10 @@ -#include - #pragma once +#include + int removePath(std::string path); + +int readFile(const std::string &path, void *buffer, SceSize size); +int readFile(const std::string &path, std::string &content); + +int copyFile(const std::string &path_source, const std::string &path_dest); diff --git a/src/font.cpp b/src/font.cpp index b43229e..b3b2a8b 100644 --- a/src/font.cpp +++ b/src/font.cpp @@ -17,22 +17,79 @@ Font::Font(const std::string &path, unsigned int fSize) { size = fSize; } -int Font::Draw(const Point &pt, const std::string &text, unsigned int color) { +int Font::Draw(const Point &pt, const std::string &text, unsigned int color, + unsigned int maxWidth, unsigned int maxHeight) { + if (maxWidth > 0 && maxHeight > 0) { + return DrawClip(pt, text, Rectangle(pt, Point(pt.x + maxWidth, pt.y + maxHeight))); + } return vita2d_font_draw_text(font, pt.x, pt.y, color, size, text.c_str()); } -int Font::DrawCentered(const Rectangle &rect, const std::string &text, int color) { +int Font::DrawClip(const Point &pt, const std::string &text, const Rectangle &clipRect, unsigned int color) { + bool enabledClipping = false; + if (!vita2d_get_clipping_enabled()) { + enabledClipping = true; + vita2d_enable_clipping(); + } + vita2d_set_clip_rectangle(clipRect.topLeft.x, clipRect.topLeft.y, clipRect.bottomRight.x, clipRect.bottomRight.y); + auto ret = vita2d_font_draw_text(font, pt.x, pt.y, color, size, text.c_str()); + if (enabledClipping) { + vita2d_disable_clipping(); + } + return ret; +} + +int Font::DrawCentered(const Rectangle &rect, const std::string &text, unsigned int color, bool clip) { //log_printf(DBG_DEBUG, "DrawCentered: %f,%f:%f,%f", rect.topLeft.x, rect.topLeft.y, rect.bottomRight.x, rect.bottomRight.y); int width, height; vita2d_font_text_dimensions(font, size, text.c_str(), &width, &height); //log_printf(DBG_DEBUG, "Dimensions: %d, %d", width, height); - int posX = rect.topLeft.x + (rect.bottomRight.x - rect.topLeft.x - width)/2; + double posX = rect.topLeft.x + (rect.Width() - width)/2.0; + // +size/3 roughly aligns the font's median line with the middle of rect + double posY = rect.topLeft.y + rect.Height()/2 + size/3.0 - (height-size)/2.0; + + //log_printf(DBG_DEBUG, "Pos: %d, %d", posX, posY); + if (!clip) { + return Draw(Point(posX, posY), text, color); + } else { + return DrawClip(Point(posX, posY), text, rect, color); + } +} - // FIXME Should be height/2 but it doesn't look good with it - int posY = rect.topLeft.y + (rect.bottomRight.y - rect.topLeft.y)/2 + height/3; +int Font::DrawCenteredVertical(const Rectangle &rect, const std::string &text, unsigned int color, bool clip) { + int height = vita2d_font_text_height(font, size, text.c_str()); + double posY = rect.topLeft.y + rect.Height()/2 + size/3.0 - (height-size)/2.0; //log_printf(DBG_DEBUG, "Pos: %d, %d", posX, posY); + if (!clip) { + return Draw(Point(rect.topLeft.x, posY), text, color); + } else { + return DrawClip(Point(rect.topLeft.x, posY), text, rect, color); + } +} + +std::string Font::FitString(const std::string &text, int maxWidth) { + log_printf(DBG_DEBUG, "FitString: \"%s\", %i", text.c_str(), maxWidth); + int numSpaces = std::count(text.begin(), text.end(), ' '); + if (!numSpaces) { + return std::string(text); + } + std::vector words = split_string(text); + + std::string res = words[0]; + for (auto i = 1; i < words.size(); i++) { + std::string try_res = res + " " + words[i]; + int lineWidth = vita2d_font_text_width(font, size, try_res.c_str()); + if (lineWidth <= maxWidth) { + log_printf(DBG_DEBUG, "\"%s\" fits: %i", words[i].c_str(), lineWidth); + res = std::move(try_res); + } else { + log_printf(DBG_DEBUG, "\"%s\" overflows: %i", words[i].c_str(), lineWidth); + res += "\n" + words[i]; + } + } - return Draw(Point(posX, posY), text, color); -} \ No newline at end of file + log_printf(DBG_DEBUG, "Fitted string: \"%s\"", res.c_str()); + return res; +} diff --git a/src/font.h b/src/font.h index a73c5bc..694ff44 100644 --- a/src/font.h +++ b/src/font.h @@ -4,17 +4,23 @@ #include "shapes.h" -#define FONT_DIR "ux0:/app/VHBB00001/resources/fonts/" +#define FONT_DIR VHBB_RESOURCES "/fonts/" class Font { public: Font(const std::string &path, unsigned int fSize); - int Draw(const Point &pt, const std::string &text, unsigned int color = COLOR_WHITE); - int DrawCentered(const Rectangle &rect, const std::string &text, int color = COLOR_WHITE); + int Draw(const Point &pt, const std::string &text, unsigned int color = COLOR_WHITE, + unsigned int maxWidth = 0, unsigned int maxHeight = 0); + + int DrawClip(const Point &pt, const std::string &text, const Rectangle &clipRect, unsigned int color=COLOR_WHITE); + int DrawCentered(const Rectangle &rect, const std::string &text, unsigned int color=COLOR_WHITE, bool clip=false); + int DrawCenteredVertical(const Rectangle &rect, const std::string &text, + unsigned int color=COLOR_WHITE, bool clip=false); + std::string FitString(const std::string &text, int maxWidth); static std::unordered_map, vita2d_font*> fontCache; private: vita2d_font *font; unsigned int size; -}; \ No newline at end of file +}; diff --git a/src/global_include.h b/src/global_include.h index 5d04380..9b536c0 100644 --- a/src/global_include.h +++ b/src/global_include.h @@ -53,6 +53,7 @@ #include +#include "concurrency.h" #include "debug.h" #include "macros.h" #include "screen.h" diff --git a/src/macros.h b/src/macros.h index c50a799..7d50c47 100644 --- a/src/macros.h +++ b/src/macros.h @@ -2,16 +2,17 @@ #include -#define VHBB_DATA std::string("ux0:/data/VitaHbBrowser") +#define VHBB_DATA "ux0:/data/VitaHbBrowser" +#define VHBB_RESOURCES "ux0:/app/" VITA_TITLEID "/resources" -#define API_ENDPOINT std::string("https://rinnegatamante.it/vitadb/list_hbs_yaml.php") -#define API_LOCAL std::string("ux0:/data/VitaHbBrowser/homebrews.yaml") +#define API_ENDPOINT "https://rinnegatamante.it/vitadb/list_hbs_yaml.php" +#define API_LOCAL "ux0:/data/VitaHbBrowser/homebrews.yaml" -#define ICONS_FOLDER std::string("ux0:/data/VitaHbBrowser/icons") -#define ICON_URL_PREFIX std::string("https://rinnegatamante.it/vitadb/icons/") +#define ICONS_FOLDER "ux0:/data/VitaHbBrowser/icons" +#define ICON_URL_PREFIX "https://rinnegatamante.it/vitadb/icons/" -#define SERVER_BASE_URL std::string("https://rinnegatamante.it/vitadb/") -#define SCREENSHOTS_FOLDER std::string("ux0:/data/VitaHbBrowser/screenshots") +#define SERVER_BASE_URL "https://rinnegatamante.it/vitadb/" +#define SCREENSHOTS_FOLDER "ux0:/data/VitaHbBrowser/screenshots" enum { COLOR_WHITE = RGBA8(255, 255, 255, 255), diff --git a/src/network.cpp b/src/network.cpp index aaaa81f..1ca8a4f 100644 --- a/src/network.cpp +++ b/src/network.cpp @@ -16,6 +16,7 @@ class ProgressClass { } int ProgressClassCallback(double dltotal, double dlnow, double ultotal, double ulnow) { + if (dltotal == 0) return CURLE_OK; m_progress.percent((float)(dlnow / dltotal) * 100); return CURLE_OK; } @@ -37,9 +38,9 @@ class WriterFileClass size_t WriterFileClassCallback(char* ptr, size_t size, size_t nmemb) { int ret = sceIoWrite(m_fd, ptr, size*nmemb); - if (ret < 0) + if (ret < 0) { cURLpp::raiseException(std::runtime_error("Network: Couldn't write data")); - + } return ret; } @@ -54,8 +55,9 @@ class WriterFileClass WriterFileClass::WriterFileClass(std::string dest) { m_fd = sceIoOpen(dest.c_str(), SCE_O_WRONLY | SCE_O_CREAT | SCE_O_TRUNC, 0777); - if (m_fd < 0) + if (m_fd < 0) { cURLpp::raiseException(std::runtime_error("Network: Couldn't write data")); + } } Network::Network() @@ -105,7 +107,6 @@ Network::~Network() int Network::Download(std::string url, std::string dest, InfoProgress *progress) { log_printf(DBG_DEBUG, "Downloading %s to %s", url.c_str(), dest.c_str()); - try { curlpp::Easy request; @@ -137,6 +138,7 @@ int Network::Download(std::string url, std::string dest, InfoProgress *progress) for (unsigned int retries=1; retries <= 3; retries++) { try { + std::lock_guard lock(mtx_); request.perform(); break; } catch (curlpp::RuntimeError &e) { @@ -151,12 +153,13 @@ int Network::Download(std::string url, std::string dest, InfoProgress *progress) } } catch (curlpp::RuntimeError &e) { - log_printf(DBG_ERROR, "cURLpp exception: %s", e.what()); - throw std::runtime_error("Network: Cannot send request"); + log_printf(DBG_ERROR, "cURLpp exception: %s %s ", e.what(), url.c_str()); + throw std::runtime_error(std_string_format("NetworkError: %s", e.what()).c_str()); } if(progress) progress->percent(100); + log_printf(DBG_DEBUG, "Done downloading %s", url.c_str()); return 0; } @@ -190,7 +193,7 @@ InternetStatus Network::TestConnection() sendRes = sceHttpSendRequest(req, nullptr, 0); - + res = sceHttpGetStatusCode(req, &statusCode); if (sendRes < 0 || res < 0 || statusCode != 200) { diff --git a/src/network.h b/src/network.h index 1f5bad9..9743a19 100644 --- a/src/network.h +++ b/src/network.h @@ -27,5 +27,6 @@ friend class Singleton; InternetStatus TestConnection(); private: int templateId_; + SceMutex mtx_ = SceMutex("network_mtx"); }; diff --git a/src/shapes.h b/src/shapes.h index 9ab3210..4236167 100644 --- a/src/shapes.h +++ b/src/shapes.h @@ -3,18 +3,21 @@ #include class Point { - public: - double x; - double y; - Point(double _x, double _y); +public: + double x; + double y; + Point(double _x, double _y); }; class Rectangle { - public: - const Point &topLeft; - const Point &bottomRight; +public: + const Point &topLeft; + const Point &bottomRight; - Rectangle(const Point &aTopLeft, const Point &aBottomRight) : topLeft(aTopLeft), bottomRight(aBottomRight) {}; + Rectangle(const Point &aTopLeft, const Point &aBottomRight) : topLeft(aTopLeft), bottomRight(aBottomRight) {}; - int Inside(const Point &pt) const; + int Inside(const Point &pt) const; + + inline double Width() const { return bottomRight.x - topLeft.x; } + inline double Height() const { return bottomRight.y - topLeft.y; } }; diff --git a/src/update.cpp b/src/update.cpp new file mode 100644 index 0000000..6605a56 --- /dev/null +++ b/src/update.cpp @@ -0,0 +1,245 @@ +#include "update.h" + +#include +#include +#include +#include + +#include "network.h" +#include "filesystem.h" +#include "vitaPackage.h" +#include "zip.h" +#include "Views/dialogView.h" + +#define VERSION_YAML_PATH (VHBB_DATA "/latest_version.yml") + +struct VersionYAML { + std::string version; + std::string url; +}; + +namespace YAML { + template<> + struct convert { + static bool decode(const Node &node, VersionYAML &version) { + version.version = node["version"].as(); + version.url = node["url"].as(); + return true; + } + }; +}; + +struct VersionInfo { + std::array latestVersion, currentVersion; + std::string url; +}; + + + +enum UpdateState { + UPDATE_STATE_RUNNING, + UPDATE_STATE_DONE, + UPDATE_STATE_READY_TO_LAUNCH_UPDATER +}; + +typedef std::atomic AtomicUpdateState; + +extern unsigned char _binary_assets_spr_img_updater_icon_png_start; +AtomicUpdateState updateState{UPDATE_STATE_RUNNING}; + +// Note: std::isdigit is not a constexpr so cannot be used to replace this function +constexpr int matchDigit(const char *text) { + return *text == '0' || *text == '1' || *text == '2' || *text == '3' || *text == '4' || + *text == '5' || *text == '6' || *text == '7' || *text == '8' || *text == '9'; +} + +constexpr int matchVersionString(const char *text) { + // expect version to be =~ \d\d\.\d\d + return *text != '\0' && matchDigit(text) && *(text+1) != '\0' && matchDigit(text+1) && + *(text+2) != '\0' && *(text+2) == '.' && + *(text+3) != '\0' && matchDigit(text+3) && *(text+4) != '\0' && matchDigit(text+4) && + *(text+5) == '\0'; +} + +int readVersionYAML(const std::string &filePath, VersionInfo &vInfo) { + std::string versionYamlContent; + int res = readFile(std::string(VERSION_YAML_PATH), versionYamlContent); + if (res < 0) { + log_printf(DBG_ERROR, "Couldn't read version file %s: 0x%08x", filePath.c_str(), res); + return res; + } + YAML::Node node = YAML::Load(versionYamlContent); + VersionYAML v = node.as(); + + try { + vInfo.latestVersion[0] = std::stoi(v.version.substr(0, 2)); + vInfo.latestVersion[1] = std::stoi(v.version.substr(3, 2)); + log_printf(DBG_DEBUG, "Version field successfully parsed %i %i", vInfo.latestVersion[0], vInfo.latestVersion[1]); + } catch (const std::invalid_argument& e) { + log_printf(DBG_ERROR, "Couldn't parse content of version field %s: %s", filePath.c_str(), v.version.c_str()); + return -1; + } + + vInfo.url = v.url; + + return 0; +} + +int Update::getVersionInfo(bool &available, std::string &url) { + int res; + res = sceIoRemove(VERSION_YAML_PATH); + log_printf(DBG_DEBUG, "sceIoRemove(%s) = 0x%08x", VERSION_YAML_PATH, res); + try { + res = Network::get_instance()->Download(std::string(VERSION_YAML_URL), std::string(VERSION_YAML_PATH)); + } catch (const std::runtime_error &err) { + log_printf(DBG_ERROR, "Couldn't download version.yml: %s", err.what()); + DialogView::openDialogView(nullptr, std_string_format("Couldn't check for update\n\n%s", err.what()), + DIALOG_TYPE_OK); + return false; + } + if (res) { + log_printf(DBG_ERROR, "Couldn't download version.yml 0x%08x", res); + return res; + } + + VersionInfo vInfo; + + res = readVersionYAML(std::string(VERSION_YAML_PATH), vInfo); + if (res < 0) { + return res; + } + + log_printf(DBG_INFO, "Latest online version: %02i.%02i", vInfo.latestVersion[0], vInfo.latestVersion[1]); + res = sceIoRemove(VERSION_YAML_PATH); + if (res) { + log_printf(DBG_ERROR, "Couldn't delete %s: 0x%08x", VERSION_YAML_PATH, res); + } + auto currentVersionStr = std::string(VITA_VERSION); + static_assert(matchVersionString(VITA_VERSION), + "VITA_VERSION=" VITA_VERSION " but must match \\d\\d\\.\\d\\d"); + vInfo.currentVersion[0] = std::stoi(currentVersionStr.substr(0, 2)); + vInfo.currentVersion[1] = std::stoi(currentVersionStr.substr(3, 2)); + + if (vInfo.latestVersion > vInfo.currentVersion) { + log_printf(DBG_INFO, "Current version " VITA_VERSION " is outdated."); + available = true; + url = vInfo.url; + } else { + available = false; + log_printf(DBG_DEBUG, "Current version " VITA_VERSION " is up-to-date"); + } + + return 0; +} + +std::shared_ptr Update::startBackgroundView() { + auto bgView = std::make_shared(); + bgView->priority = 749; + Activity::get_instance()->AddView(bgView); + return bgView; +} + +std::shared_ptr Update::startProgressView(InfoProgress progress, std::string title) { + auto progressView = std::make_shared(progress, std::move(title), Texture(&_binary_assets_spr_img_updater_icon_png_start)); + progressView->priority = 750; + Activity::get_instance()->AddView(progressView); + return progressView; +} + +void Update::updateThread(unsigned int arglen, void* argv[]) { + auto updateState_ptr = (AtomicUpdateState*)argv[0]; + bool updateExists = false; + std::string updateURL; + Update::getVersionInfo(updateExists, updateURL); + if (updateExists) { + auto res = std::make_shared(); + DialogView::openDialogView(res, "A new version of VHBB is available.\nDo you want to update?", DIALOG_TYPE_YESNO); + while (res->status == COMMON_DIALOG_STATUS_RUNNING || res->status == COMMON_DIALOG_STATUS_NONE) { + sceKernelDelayThread(16666); // roughly 1/60 second + } + if (res->accepted) { + log_printf(DBG_INFO, "User chose to update VHBB"); + InfoProgress progress; + auto progressView = startProgressView(progress, "Update Helper (1/2)"); + auto bgView = startBackgroundView(); + try { + installUpdater(progress.Range(0, 20)); + progressView->hb_name = "VHBB Update (2/2)"; + prepareUpdateFiles(updateURL, progress.Range(20, 100)); + progress.message("Finished"); + progressView->Finish(700); + sceKernelDelayThread(700000); + bgView->request_destroy = true; + updateState_ptr->store(UPDATE_STATE_READY_TO_LAUNCH_UPDATER); + return; + } catch (std::exception &ex) { + const std::string &update_failed = std_string_format("Update failed: %s", ex.what()); + log_printf(DBG_ERROR, update_failed); + progress.message(update_failed); + progressView->Finish(4000); + sceKernelDelayThread(4000000); + bgView->request_destroy = true; + } + } else { + log_printf(DBG_INFO, "User refused to update VHBB"); + } + } + updateState_ptr->store(UPDATE_STATE_DONE); +} + + +void Update::startUpdateThread() { + SceUID thid = sceKernelCreateThread("update_check_thread", (SceKernelThreadEntry)Update::updateThread, + 0x40, 0x20000, 0, 0, nullptr); + auto updateState_ptr = &updateState; + sceKernelStartThread(thid, sizeof(updateState_ptr), &updateState_ptr); +} + +void Update::tick() { + if (updateState.load() == UPDATE_STATE_READY_TO_LAUNCH_UPDATER) + Update::startUpdaterApp(); +} + +bool Update::checkIsDone() { + return updateState.load() == UPDATE_STATE_DONE; +} + +void Update::installUpdater(InfoProgress progress) { + log_printf(DBG_DEBUG, "Installing updater"); + int ret = UpdaterPackage().InstallUpdater(std::move(progress)); + if (ret >= 0) { + log_printf(DBG_DEBUG, "Installed updater"); + } else { + log_printf(DBG_DEBUG, "Installing updater failed: 0x%08x", ret); + throw std::runtime_error(std_string_format("Couldn't install updater (0x%08x)", ret)); + } +} + +void Update::prepareUpdateFiles(const std::string &updateURL, InfoProgress progress) { + log_printf(DBG_DEBUG, "Downloading update vpk"); + progress.message("Downloading update..."); + + Network::get_instance()->Download(updateURL, std::string("ux0:/temp/download.vpk"), progress.Range(0, 60)); + + log_printf(DBG_DEBUG, "Extracting update vpk"); + auto pkg = UpdatePackage(std::string("ux0:/temp/download.vpk")); + InfoProgress progress2 = progress.Range(60, 95); + pkg.Extract(&progress2); + log_printf(DBG_DEBUG, "Extracted update vpk"); + pkg.MakeHeadBin(); + progress.percent(100); + log_printf(DBG_DEBUG, "Made update head.bin"); +} + +void Update::startUpdaterApp() { + log_printf(DBG_DEBUG, "Starting updater " UPDATER_TITLEID); + char uri[32]; + sprintf(uri, "psgm:play?titleid=%s", UPDATER_TITLEID); + + int ret = sceAppMgrLaunchAppByUri(0xFFFFF, uri); + if (ret == 0) { + sceKernelExitProcess(0); + } else { + log_printf(DBG_WARNING, "Couldn't start updater 0x%08x", ret); + } +} diff --git a/src/update.h b/src/update.h new file mode 100644 index 0000000..bb6e1a1 --- /dev/null +++ b/src/update.h @@ -0,0 +1,22 @@ +#pragma once + +#include "global_include.h" + +#include "Views/background.h" +#include "Views/ProgressView/progressView.h" + +class Update { +public: + static void startUpdateThread(); + static void tick(); // needs to be called frequently from main thread + static bool checkIsDone(); + +private: + static void updateThread(unsigned int arglen, void* argv[]); + static int getVersionInfo(bool &available, std::string &url); + static std::shared_ptr startBackgroundView(); + static std::shared_ptr startProgressView(InfoProgress progress, std::string title); + static void installUpdater(InfoProgress progress); + static void prepareUpdateFiles(const std::string &updateURL, InfoProgress progress); + static void startUpdaterApp(); +}; diff --git a/src/utils.cpp b/src/utils.cpp index 6a9dd98..868bef4 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -1 +1,91 @@ #include "utils.h" + +#include +#include +#include + + +bool std_string_iequals(const std::string &a, const std::string &b) { + return std::equal(a.begin(), a.end(), b.begin(), + [](const char &a, const char &b) { + return std::tolower(a) == std::tolower(b); + }); +} + + +std::string join_strings(const std::vector& v, char c) { + std::string s; + + for (auto p = v.begin(); p != v.end(); ++p) { + s += *p; + if (p != v.end() - 1) + s += c; + } + return s; +} + +std::vector split_string(const std::string &s, char delim) { + std::vector res; + char word[s.size() + 1]; + unsigned wordIdx = 0; + for(const char &chr : s) { + if (chr != delim) { + word[wordIdx++] = chr; + } else { + word[wordIdx] = '\0'; + res.emplace_back(std::string(word)); + wordIdx = 0; + } + } + if (wordIdx) { + word[wordIdx] = '\0'; + res.emplace_back(std::string(word)); + } + return res; +} + + +// FIXME Use a robust library instead such as fmtlib +// https://github.com/fmtlib/fmt +// From https://stackoverflow.com/a/69911/2376882 +std::string std_string_format_ap_list(const char *fmt, va_list ap) { + // Allocate a buffer on the stack that's big enough for us almost + // all the time. Be prepared to allocate dynamically if it doesn't fit. + size_t size = 1024; + char stackbuf[1024]; + std::vector dynamicbuf; + char *buf = &stackbuf[0]; + va_list ap_copy; + + while (true) { + // Try to vsnprintf into our buffer. + va_copy(ap_copy, ap); + int needed = vsnprintf(buf, size, fmt, ap); + va_end(ap_copy); + + // NB. C99 (which modern Linux and OS X follow) says vsnprintf + // failure returns the length it would have needed. But older + // glibc and current Windows return -1 for failure, i.e., not + // telling us how much was needed. + + if (needed <= (int)size && needed >= 0) { + // It fit fine so we're done. + return std::string(buf, (size_t) needed); + } + + // vsnprintf reported that it wanted to write more characters + // than we allotted. So try again using a dynamic buffer. This + // doesn't happen very often if we chose our initial size well. + size = (needed > 0) ? (needed+1) : (size*2); + dynamicbuf.resize(size); + buf = &dynamicbuf[0]; + } +} + +std::string std_string_format(const char *fmt, ...) { + va_list ap; + va_start (ap, fmt); + std::string buf = std_string_format_ap_list(fmt, ap); + va_end (ap); + return buf; +} diff --git a/src/utils.h b/src/utils.h index 0513400..a976d56 100644 --- a/src/utils.h +++ b/src/utils.h @@ -1,5 +1,10 @@ +#pragma once + #define _countof(a) (sizeof(a)/sizeof(*(a))) +#include +#include + inline double lerpd(double value, double from_max, double to_max) { @@ -18,3 +23,10 @@ T align_left(T a, V b) { return a - b; } + +bool std_string_iequals(const std::string &a, const std::string &b); + +std::string join_strings(const std::vector &v, char c); +std::vector split_string(const std::string &s, char delim=' '); + +std::string std_string_format (const char *fmt, ...); diff --git a/src/vhbb.cpp b/src/vhbb.cpp index eb62a30..74da864 100644 --- a/src/vhbb.cpp +++ b/src/vhbb.cpp @@ -10,6 +10,8 @@ #include "nosleep_thread.h" #include "fetch_load_icons_thread.h" #include "vitasdk_quirks.h" +#include "update.h" +#include "vitaPackage.h" void debug_start() { @@ -53,16 +55,44 @@ void network_test() { // https://bitbucket.org/xerpi/vita-ftploader/src/87ef1d13a8aaf092f376cbf2818a22cd0e481fd6/plugin/main.c?at=master&fileviewer=file-view-default#main.c-155 } +void mainLoopTick(Input &input, Activity &activity) { + vita2d_start_drawing(); + vita2d_clear_screen(); + + input.Get(); + + activity.FlushQueue(); + activity.HandleInput(1, input); + activity.Display(); + + vita2d_end_drawing(); + vita2d_common_dialog_update(); + vita2d_swap_buffers(); + sceDisplayWaitVblankStart(); +} + int main() { - sceIoMkdir(VHBB_DATA.c_str(), 0777); + sceIoMkdir(VHBB_DATA, 0777); std::set_terminate(terminate_logger); debug_start(); // Sleep invalidates file descriptors StartNoSleepThread(); + + // remove updater app if installed + { // extra scope needed to cause VitaPackage cleanup procedure + auto updaterPkg = InstalledVitaPackage(UPDATER_TITLEID); + if(updaterPkg.IsInstalled()) { + log_printf(DBG_DEBUG, "Found updater installed -> removing"); + int ret = updaterPkg.Uninstall(); + if (ret < 0) + log_printf(DBG_ERROR, "updaterPkg.Uninstall() = 0x%08x", ret); + } + } + network_test(); - StartFetchLoadIconsThread(); + Update::startUpdateThread(); vita2d_init(); vita2d_set_clear_color(COLOR_BLACK); @@ -74,20 +104,15 @@ int main() { splash->priority = 200; activity.AddView(splash); - while (true) { - vita2d_start_drawing(); - vita2d_clear_screen(); - - input.Get(); + while (!Update::checkIsDone()) { + Update::tick(); + mainLoopTick(input, activity); + } - activity.FlushQueue(); - activity.HandleInput(1, input); - activity.Display(); + StartFetchLoadIconsThread(); - vita2d_end_drawing(); - vita2d_common_dialog_update(); - vita2d_swap_buffers(); - sceDisplayWaitVblankStart(); + while (true) { + mainLoopTick(input, activity); } return 0; diff --git a/src/vitaPackage.cpp b/src/vitaPackage.cpp index 5c389f4..b42bb09 100644 --- a/src/vitaPackage.cpp +++ b/src/vitaPackage.cpp @@ -7,19 +7,26 @@ #define ntohl __builtin_bswap32 +#define UPDATER_SRC_EBOOT_PATH VHBB_RESOURCES "/updater/eboot.bin" +#define UPDATER_SRC_SFO_PATH VHBB_RESOURCES "/updater/param.sfo" + +#define UPDATER_DST_EBOOT_PATH PACKAGE_TEMP_FOLDER "eboot.bin" +#define UPDATER_DST_SFO_DIR PACKAGE_TEMP_FOLDER "sce_sys/" +#define UPDATER_DST_SFO_PATH PACKAGE_TEMP_FOLDER "sce_sys/param.sfo" + extern unsigned char _binary_assets_head_bin_start; extern unsigned char _binary_assets_head_bin_size; #define SFO_MAGIC 0x46535000 static void fpkg_hmac(const uint8_t *data, unsigned int len, uint8_t hmac[16]) { - SHA1_CTX ctx; + SHA1_CTX ctx; char sha1[20]; char buf[64]; sha1_init(&ctx); sha1_update(&ctx, (BYTE*)data, len); - sha1_final(&ctx, (BYTE*)sha1); + sha1_final(&ctx, (BYTE*)sha1); memset(buf, 0, 64); memcpy(&buf[0], &sha1[4], 8); @@ -33,7 +40,7 @@ static void fpkg_hmac(const uint8_t *data, unsigned int len, uint8_t hmac[16]) { sha1_init(&ctx); sha1_update(&ctx, (BYTE*)buf, 64); - sha1_final(&ctx, (BYTE*)sha1); + sha1_final(&ctx, (BYTE*)sha1); memcpy(hmac, sha1, 16); } @@ -55,11 +62,11 @@ typedef struct SfoEntry { } __attribute__((packed)) SfoEntry; int getSfoString(char *buffer, const char *name, char *string, unsigned int length) { - auto *header = (SfoHeader *)buffer; - auto *entries = (SfoEntry *)((uint32_t)buffer + sizeof(SfoHeader)); + auto *header = (SfoHeader *)buffer; + auto *entries = (SfoEntry *)((uint32_t)buffer + sizeof(SfoHeader)); if (header->magic != SFO_MAGIC) - return -1; + return -1; int i; for (i = 0; i < header->count; i++) { @@ -75,6 +82,7 @@ int getSfoString(char *buffer, const char *name, char *string, unsigned int leng } int WriteFile(const char *file, const void *buf, unsigned int size) { + log_printf(DBG_DEBUG, "Writing file %s @%p+%u", file, buf, size); SceUID fd = sceIoOpen(file, SCE_O_WRONLY | SCE_O_CREAT | SCE_O_TRUNC, 0777); if (fd < 0) return fd; @@ -115,157 +123,259 @@ int allocateReadFile(const char *file, char **buffer) { int makeHeadBin() { - uint8_t hmac[16]; - uint32_t off; - uint32_t len; - uint32_t out; - - if (checkFileExist((PACKAGE_TEMP_FOLDER + "sce_sys/package/head.bin").c_str())) - return 0; - - // Read param.sfo - char *sfo_buffer = nullptr; - int res = allocateReadFile((PACKAGE_TEMP_FOLDER + "sce_sys/param.sfo").c_str(), &sfo_buffer); - if (res < 0) - return res; - - // Get title id - char titleid[12]; - memset(titleid, 0, sizeof(titleid)); - getSfoString(sfo_buffer, "TITLE_ID", titleid, sizeof(titleid)); - - // Enforce TITLE_ID format - if (strlen(titleid) != 9) - return -1; - - // Get content id - char contentid[48]; - memset(contentid, 0, sizeof(contentid)); - getSfoString(sfo_buffer, "CONTENT_ID", contentid, sizeof(contentid)); - - // Free sfo buffer - free(sfo_buffer); - - // Allocate head.bin buffer - uint8_t *head_bin = (uint8_t *)malloc((size_t)&_binary_assets_head_bin_size); - memcpy(head_bin, (void *)&_binary_assets_head_bin_start, (size_t)&_binary_assets_head_bin_size); - - // Write full title id - char full_title_id[48]; - snprintf(full_title_id, sizeof(full_title_id), "EP9000-%s_00-0000000000000000", titleid); - strncpy((char *)&head_bin[0x30], strlen(contentid) > 0 ? contentid : full_title_id, 48); - - // hmac of pkg header - len = ntohl(*(uint32_t *)&head_bin[0xD0]); - fpkg_hmac(&head_bin[0], len, hmac); - memcpy(&head_bin[len], hmac, 16); - - // hmac of pkg info - off = ntohl(*(uint32_t *)&head_bin[0x8]); - len = ntohl(*(uint32_t *)&head_bin[0x10]); - out = ntohl(*(uint32_t *)&head_bin[0xD4]); - fpkg_hmac(&head_bin[off], len-64, hmac); - memcpy(&head_bin[out], hmac, 16); - - // hmac of everything - len = ntohl(*(uint32_t *)&head_bin[0xE8]); - fpkg_hmac(&head_bin[0], len, hmac); - memcpy(&head_bin[len], hmac, 16); - - // Make dir - sceIoMkdir((PACKAGE_TEMP_FOLDER + "sce_sys/package").c_str(), 0777); - - // Write head.bin - WriteFile((PACKAGE_TEMP_FOLDER + "sce_sys/package/head.bin").c_str(), head_bin, (unsigned int)&_binary_assets_head_bin_size); - - free(head_bin); - - return 0; -} + uint8_t hmac[16]; + uint32_t off; + uint32_t len; + uint32_t out; + + if (checkFileExist(PACKAGE_TEMP_FOLDER "sce_sys/package/head.bin")) + return 0; + + // Read param.sfo + char *sfo_buffer = nullptr; + int res = allocateReadFile(PACKAGE_TEMP_FOLDER "sce_sys/param.sfo", &sfo_buffer); + if (res < 0) + return res; + + // Get title id + char titleid[12]; + memset(titleid, 0, sizeof(titleid)); + getSfoString(sfo_buffer, "TITLE_ID", titleid, sizeof(titleid)); + + // Enforce TITLE_ID format + if (strlen(titleid) != 9) + return -1; -#define ntohl __builtin_bswap32 + // Get content id + char contentid[48]; + memset(contentid, 0, sizeof(contentid)); + getSfoString(sfo_buffer, "CONTENT_ID", contentid, sizeof(contentid)); + + // Free sfo buffer + free(sfo_buffer); + + // Allocate head.bin buffer + uint8_t *head_bin = (uint8_t *)malloc((size_t)&_binary_assets_head_bin_size); + memcpy(head_bin, (void *)&_binary_assets_head_bin_start, (size_t)&_binary_assets_head_bin_size); + + // Write full title id + char full_title_id[48]; + snprintf(full_title_id, sizeof(full_title_id), "EP9000-%s_00-0000000000000000", titleid); + strncpy((char *)&head_bin[0x30], strlen(contentid) > 0 ? contentid : full_title_id, 48); + + // hmac of pkg header + len = ntohl(*(uint32_t *)&head_bin[0xD0]); + fpkg_hmac(&head_bin[0], len, hmac); + memcpy(&head_bin[len], hmac, 16); + + // hmac of pkg info + off = ntohl(*(uint32_t *)&head_bin[0x8]); + len = ntohl(*(uint32_t *)&head_bin[0x10]); + out = ntohl(*(uint32_t *)&head_bin[0xD4]); + fpkg_hmac(&head_bin[off], len-64, hmac); + memcpy(&head_bin[out], hmac, 16); + + // hmac of everything + len = ntohl(*(uint32_t *)&head_bin[0xE8]); + fpkg_hmac(&head_bin[0], len, hmac); + memcpy(&head_bin[len], hmac, 16); + + // Make dir + sceIoMkdir(PACKAGE_TEMP_FOLDER "sce_sys/package", 0777); + + // Write head.bin + WriteFile(PACKAGE_TEMP_FOLDER "sce_sys/package/head.bin", head_bin, (unsigned int)&_binary_assets_head_bin_size); + + free(head_bin); + + log_printf(DBG_DEBUG, "Created head.bin for %s", titleid); + return 0; +} VitaPackage::VitaPackage(const std::string vpk) : - vpk_(vpk) + vpk_(vpk) { - // ScePaf is required for PromoterUtil - uint32_t ptr[0x100] = {0}; + log_printf(DBG_INFO, "Loading PAF"); + // ScePaf is required for PromoterUtil + uint32_t ptr[0x100] = {0}; ptr[0] = 0; ptr[1] = (uint32_t)&ptr[0]; uint32_t scepaf_argp[] = {0x400000, 0xEA60, 0x40000, 0, 0}; - sceSysmoduleLoadModuleInternalWithArg(SCE_SYSMODULE_INTERNAL_PAF, sizeof(scepaf_argp), scepaf_argp, ptr); + sceSysmoduleLoadModuleInternalWithArg(SCE_SYSMODULE_INTERNAL_PAF, sizeof(scepaf_argp), scepaf_argp, ptr); - sceSysmoduleLoadModuleInternal(SCE_SYSMODULE_INTERNAL_PROMOTER_UTIL); - scePromoterUtilityInit(); + sceSysmoduleLoadModuleInternal(SCE_SYSMODULE_INTERNAL_PROMOTER_UTIL); + scePromoterUtilityInit(); } VitaPackage::~VitaPackage() { - scePromoterUtilityExit(); - sceSysmoduleUnloadModuleInternal(SCE_SYSMODULE_INTERNAL_PROMOTER_UTIL); + log_printf(DBG_INFO, "Unloading PAF"); + scePromoterUtilityExit(); + sceSysmoduleUnloadModuleInternal(SCE_SYSMODULE_INTERNAL_PROMOTER_UTIL); +} + +void VitaPackage::Extract(InfoProgress *progress) { + int ret = removePath(std::string(PACKAGE_TEMP_FOLDER)); + + if (ret < 0) { + log_printf(DBG_ERROR, "removePath(%s) = 0x%08X", PACKAGE_TEMP_FOLDER, ret); + } + + sceIoMkdir(PACKAGE_TEMP_FOLDER, 0777); + + Zipfile vpk_file = Zipfile(vpk_); + + vpk_file.Unzip(std::string(PACKAGE_TEMP_FOLDER), progress); + sceIoRemove(vpk_.c_str()); +} + +int VitaPackage::InstallExtracted(InfoProgress *progress) { + log_printf(DBG_DEBUG, "Installing extracted"); + progress->message("Installing..."); + int ret = makeHeadBin(); + if (ret < 0) { + log_printf(DBG_ERROR, "Can't make head.bin for %s: 0x%08X", vpk_.c_str(), ret); + throw std::runtime_error("Error faking app signature"); + } + log_printf(DBG_DEBUG, "head.bin created"); + + ret = scePromoterUtilityPromotePkg(PACKAGE_TEMP_FOLDER, 0); + if (ret < 0) { + log_printf(DBG_ERROR, "Can't Promote %s: scePromoterUtilityPromotePkg(%s, 0) = 0x%08X", + vpk_.c_str(), PACKAGE_TEMP_FOLDER, ret); + throw std::runtime_error("Error installing app"); + } + log_printf(DBG_DEBUG, "Package promotion started"); + + int state = 0; + unsigned int i = 0; + do { + ret = scePromoterUtilityGetState(&state); + if (ret < 0) { + log_printf(DBG_ERROR, "Can't Promote %s: scePromoterUtilityGetState() = 0x%08X", vpk_.c_str(), ret); + throw std::runtime_error("Error while instaling"); + } + + i+= 1; + if (i<50 && progress) progress->percent(i*2); + sceKernelDelayThread(150 * 1000); + } while (state); + + int result = 0; + ret = scePromoterUtilityGetResult(&result); + if (ret < 0) { + log_printf(DBG_DEBUG, "Package promotion ended: failure"); + log_printf(DBG_ERROR, "Can't Promote %s: scePromoterUtilityGetResult() = 0x%08X", vpk_.c_str(), ret); + throw std::runtime_error("Installation failed"); + } + log_printf(DBG_DEBUG, "Package promotion ended: success"); + + removePath(std::string(PACKAGE_TEMP_FOLDER)); + + if(progress) progress->percent(100); + return 0; } int VitaPackage::Install(InfoProgress progress) { - return Install(&progress); + return Install(&progress); } int VitaPackage::Install(InfoProgress *progress) { - int ret = removePath(PACKAGE_TEMP_FOLDER); - - if (ret < 0) { - log_printf(DBG_ERROR, "removePath() = 0x%08X", ret); - } - - sceIoMkdir(PACKAGE_TEMP_FOLDER.c_str(), 0777); - - Zipfile vpk_file = Zipfile(vpk_); - - InfoProgress progress2; - if (progress) progress2 = progress->Range(0, 60); - vpk_file.Unzip(PACKAGE_TEMP_FOLDER, &progress2); - sceIoRemove(vpk_.c_str()); - - progress->message("Installing..."); - ret = makeHeadBin(); - if (ret < 0) { - log_printf(DBG_ERROR, "Can't make head.bin for : 0x%08X", vpk_.c_str(), ret); - throw std::runtime_error("Error faking app signature"); - } - - InfoProgress progress3; - if(progress) progress3 = progress->Range(60, 100); - ret = scePromoterUtilityPromotePkg(PACKAGE_TEMP_FOLDER.c_str(), 0); - if (ret < 0) { - log_printf(DBG_ERROR, "Can't Promote %s: scePromoterUtilityPromotePkgWithRif() = 0x%08X", vpk_.c_str(), ret); - throw std::runtime_error("Error installing app"); - } - - int state = 0; - unsigned int i = 0; - do { - ret = scePromoterUtilityGetState(&state); - if (ret < 0) { - log_printf(DBG_ERROR, "Can't Promote %s: scePromoterUtilityGetState() = 0x%08X", vpk_.c_str(), ret); - throw std::runtime_error("Error while instaling"); - } - - i+= 1; - if (i<50 && progress) progress3.percent(i*2); - sceKernelDelayThread(150 * 1000); - } while (state); - - int result = 0; - ret = scePromoterUtilityGetResult(&result); - if (ret < 0) { - log_printf(DBG_ERROR, "Can't Promote %s: scePromoterUtilityGetResult() = 0x%08X", vpk_.c_str(), ret); - throw std::runtime_error("Installation failed"); - } - - removePath(PACKAGE_TEMP_FOLDER); - - if(progress) progress->percent(100); - return 0; + if (progress) { + InfoProgress progress2; + progress2 = progress->Range(0, 60); + Extract(&progress2); + } else { + Extract(); + } + + if (progress) { + InfoProgress progress3 = progress->Range(60, 100); + return InstallExtracted(&progress3); + } else { + return InstallExtracted(); + } +} + + +int UpdaterPackage::InstallUpdater(InfoProgress progress) { + return InstallUpdater(&progress); +} + +int UpdaterPackage::InstallUpdater(InfoProgress *progress) { + int ret = removePath(std::string(PACKAGE_TEMP_FOLDER)); + if (ret < 0) { + log_printf(DBG_ERROR, "removePath(%s) = 0x%08X", PACKAGE_TEMP_FOLDER, ret); + } + + ret = sceIoMkdir(PACKAGE_TEMP_FOLDER, 0777); + if (ret < 0) + log_printf(DBG_ERROR, "sceIoMkdir(%s, 0777) = 0x%08X", PACKAGE_TEMP_FOLDER, ret); + + ret = sceIoMkdir(UPDATER_DST_SFO_DIR, 0777); + if (ret < 0) + log_printf(DBG_ERROR, "sceIoMkdir(%s, 0777) = 0x%08X", UPDATER_DST_SFO_DIR, ret); + + log_printf(DBG_DEBUG, "Copying %s -> %s", UPDATER_SRC_EBOOT_PATH, UPDATER_DST_EBOOT_PATH); + ret = copyFile(UPDATER_SRC_EBOOT_PATH, UPDATER_DST_EBOOT_PATH); + + if(progress) + progress->percent(0); + + if (ret < 0) { + log_printf(DBG_ERROR, "Update failed: Couldn't write eboot.bin"); + return ret; + } + + log_printf(DBG_DEBUG, "Copying %s -> %s", UPDATER_SRC_SFO_PATH, UPDATER_DST_SFO_PATH); + ret = copyFile(UPDATER_SRC_SFO_PATH, UPDATER_DST_SFO_PATH); + if(progress) + progress->percent(10); + + if (ret < 0) { + log_printf(DBG_ERROR, "Update failed: Couldn't write param.sfo"); + return ret; + } + + log_printf(DBG_DEBUG, "Installing extracted updater"); + if(progress) + progress->percent(20); + + if(progress) { + InfoProgress progress2 = progress->Range(20, 100); + return InstallExtracted(&progress2); + } else { + return InstallExtracted(); + } +} + +void UpdatePackage::MakeHeadBin() { + int ret = makeHeadBin(); + if (ret < 0) { + log_printf(DBG_ERROR, "Can't make head.bin for Update: 0x%08X", ret); + throw std::runtime_error("Error faking app signature"); + } + log_printf(DBG_DEBUG, "head.bin created"); +} + +bool InstalledVitaPackage::IsInstalled() { + int res; + int ret = scePromoterUtilityCheckExist(title_id.c_str(), &res); + if (res < 0) { + log_printf(DBG_ERROR, "scePromoterUtilityCheckExist(%s)=0x%08x", title_id.c_str(), res); + return false; + } + return ret >= 0; +} + +int InstalledVitaPackage::Uninstall(InfoProgress progress) { + return Uninstall(&progress); +} + +int InstalledVitaPackage::Uninstall(InfoProgress *progress) { + sceAppMgrDestroyOtherApp(); + return scePromoterUtilityDeletePkg(title_id.c_str()); } diff --git a/src/vitaPackage.h b/src/vitaPackage.h index 9a0e774..a3431f3 100644 --- a/src/vitaPackage.h +++ b/src/vitaPackage.h @@ -3,17 +3,52 @@ #include #include "infoProgress.h" -#define PACKAGE_TEMP_FOLDER std::string("ux0:/temp/pkg/") +#ifndef PACKAGE_TEMP_FOLDER + #define PACKAGE_TEMP_FOLDER "ux0:/temp/pkg/" +#endif class VitaPackage{ public: - explicit VitaPackage(std::string vpk); - ~VitaPackage(); + explicit VitaPackage(std::string vpk); + ~VitaPackage(); - int Install(InfoProgress progress); - int Install(InfoProgress *progress = nullptr); + int Install(InfoProgress progress); + int Install(InfoProgress *progress = nullptr); + + // subroutines of Install + void Extract(InfoProgress *progress = nullptr); + int InstallExtracted(InfoProgress *progress = nullptr); + +private: + std::string vpk_; +}; + +class UpdaterPackage : private VitaPackage { +public: + UpdaterPackage() : VitaPackage("VHBBUpdater") {}; + + int InstallUpdater(InfoProgress progress); + int InstallUpdater(InfoProgress *progress = nullptr); +}; + +class UpdatePackage : private VitaPackage { +public: + explicit UpdatePackage(std::string vpk) : VitaPackage(vpk) {}; + + void Extract(InfoProgress *progress = nullptr) { VitaPackage::Extract(progress); } + void MakeHeadBin(); +}; + +class InstalledVitaPackage : private VitaPackage { +public: + explicit InstalledVitaPackage(std::string title_id) : VitaPackage(""), title_id(std::move(title_id)) {} + + bool IsInstalled(); + + int Uninstall(InfoProgress progress); + int Uninstall(InfoProgress *progress = nullptr); private: - std::string vpk_; + std::string title_id; }; diff --git a/src_updater/CMakeLists.txt b/src_updater/CMakeLists.txt new file mode 100644 index 0000000..093dd37 --- /dev/null +++ b/src_updater/CMakeLists.txt @@ -0,0 +1,71 @@ +cmake_minimum_required(VERSION 2.8) + +macro(SET_EXPORTED VAR VAL EXPORT_PREFIX) + set("${VAR}" "${VAL}") + set("${EXPORT_PREFIX}${VAR}" "${VAL}" PARENT_SCOPE) +endmacro(SET_EXPORTED) + +set(UPDATER_SRC_DIR ${CMAKE_CURRENT_LIST_DIR} CACHE INTERNAL "") + +function(create_updater MAIN_SHORT_NAME MAIN_TITLEID UPDATER_TITLEID PKG_FOLDER) + # e.g. create_updater(VitaHBBrowser VHBB00001 VHBB00002 ux0:/temp/pkg/) + + if(NOT (MAIN_SHORT_NAME AND MAIN_TITLEID AND UPDATER_TITLEID AND PKG_FOLDER)) + message(WARNING "Not all of `create_updater`'s the arguments were given:\n" + "\"${MAIN_SHORT_NAME}\", \"${MAIN_TITLEID}\", \"${UPDATER_TITLEID}\", \"${PKG_FOLDER}\"") + endif() + + set_exported(UPDATER_SHORT_NAME "${MAIN_SHORT_NAME}Updater" "${MAIN_SHORT_NAME}_") + set_exported(UPDATER_TITLEID ${UPDATER_TITLEID} "${MAIN_SHORT_NAME}_") + set_exported(UPDATER_VERSION 00.01 "${MAIN_SHORT_NAME}_") + + + set(UPDATER_ELF_NAME ${UPDATER_SHORT_NAME}.elf) + set(UPDATER_VELF_NAME ${UPDATER_SHORT_NAME}.velf) + set(UPDATER_SELF_NAME ${UPDATER_SHORT_NAME}.self) + set_exported(UPDATER_EBOOT_NAME ${UPDATER_SELF_NAME} "${MAIN_SHORT_NAME}_") + set_exported(UPDATER_SFO_NAME ${UPDATER_SHORT_NAME}_param.sfo "${MAIN_SHORT_NAME}_") + + file(GLOB_RECURSE PROJECT_SOURCE_FILES ${UPDATER_SRC_DIR}/*.h ${UPDATER_SRC_DIR}/*.hpp ${UPDATER_SRC_DIR}/*.cpp ${UPDATER_SRC_DIR}/*.c) + + add_executable(${UPDATER_ELF_NAME} + ${PROJECT_SOURCE_FILES} + ) + + if(CMAKE_BUILD_TYPE MATCHES Debug AND DEBUGNET) + target_link_libraries(${UPDATER_ELF_NAME} debugnet) + endif() + + target_compile_definitions(${UPDATER_ELF_NAME} + PRIVATE UPDATE_TITLEID="${MAIN_TITLEID}" + PACKAGE_DIR="${PKG_FOLDER}" + UPDATER_VERSION="${UPDATER_VERSION}" + ) + target_link_libraries(${UPDATER_ELF_NAME} + SceNetCtl_stub + SceNet_stub + SceShellSvc_stub + SceAppMgr_stub + ScePromoterUtil_stub + SceSysmodule_stub + SceVshBridge_stub + ) + + add_custom_command(OUTPUT ${UPDATER_VELF_NAME} + COMMAND ${VITA_ELF_CREATE} ${UPDATER_ELF_NAME} ${UPDATER_VELF_NAME} + DEPENDS ${UPDATER_ELF_NAME} + COMMENT "Converting to Sony ELF ${UPDATER_VELF_NAME}" VERBATIM + ) + set(UPDATER_MAKE_FSELF_FLAGS -c) + add_custom_target(${UPDATER_SELF_NAME} + COMMAND ${VITA_MAKE_FSELF} ${UPDATER_MAKE_FSELF_FLAGS} ${UPDATER_VELF_NAME} ${UPDATER_SELF_NAME} + DEPENDS ${UPDATER_VELF_NAME} + COMMENT "Creating SELF ${UPDATER_SELF_NAME}" + ) + + set(UPDATER_MKSFOEX_FLAGS -s APP_VER=${UPDATER_VERSION} -s TITLE_ID=${UPDATER_TITLEID}) + add_custom_target(${UPDATER_SFO_NAME} + COMMAND ${VITA_MKSFOEX} ${UPDATER_MKSFOEX_FLAGS} ${UPDATER_SHORT_NAME} ${UPDATER_SFO_NAME} + COMMENT "Generating param.sfo for ${UPDATER_SHORT_NAME}" + ) +endfunction(create_updater) diff --git a/src_updater/debug.cpp b/src_updater/debug.cpp new file mode 100644 index 0000000..7577c6f --- /dev/null +++ b/src_updater/debug.cpp @@ -0,0 +1,111 @@ +// FIXME Merge with src/debug.cpp + +#include "debug.h" + +#include +#include + +#include +#include +#include +#include +#include + +#ifdef DEBUGNET +#include +#endif + + +int g_log_fd = -1; +bool g_log_to_file = false; + +extern "C" { +int _vshSblGetSystemSwVersion(SceKernelFwInfo *data); +} + +int log_init(bool log_to_file) +{ + g_log_to_file = log_to_file; + +#ifdef DEBUGNET + debugNetInit(DEBUGNETIP, 18194, 3); +#endif + + if (log_to_file) { + sceIoMkdir(LOG_DIR, 0777); + + SceDateTime logTime; + memset(&logTime, 0, sizeof(logTime)); + + sceRtcGetCurrentClockLocalTime(&logTime); + + char formattedTime[40] = {0}; + snprintf(formattedTime, sizeof(formattedTime), "%04d-%02d-%02d_%02d-%02d-%02d", + logTime.year, logTime.month, logTime.day, logTime.hour, logTime.minute, logTime.second); + + std::string log_file = std::string(LOG_DIR "/" LOG_FILE "_") + formattedTime + ".log"; + + g_log_fd = sceIoOpen(log_file.c_str(), SCE_O_WRONLY | SCE_O_CREAT | SCE_O_TRUNC, 0777); + } + + _log_printf(DBG_INFO, "Updater for " UPDATE_TITLEID " started.\n"); + _log_printf(DBG_INFO, "- Version: %s\n", UPDATER_VERSION); + + SceKernelFwInfo data; + data.size = sizeof(SceKernelFwInfo); + + if (_vshSblGetSystemSwVersion(&data) >= 0) { // sceKernelGetSystemSwVersion is spoofed version + char version[16]; + snprintf(version, 16, "%s", data.versionString); + _log_printf(DBG_INFO, "- OS: %s\n", version); + } + + _log_printf(DBG_INFO, "\n"); + + return 0; +} + +int _log_printf(int level, const char *format, ...) { + // If no logging is needed at all +#ifndef DEBUGNET + if (!g_log_to_file) + return 0; +#endif + + va_list args; + va_start(args, format); + + char buf[512]; + vsnprintf(buf, 512, format, args); + + va_end(args); + +#ifdef DEBUGNET + char buf_colored[512]; + switch (level) { + case DBG_INFO: + snprintf(buf_colored, 512, "\033[1;34;7m[INFO]\033[0m\033[1;34m %s\033[0m", buf); + break; + case DBG_ERROR: + snprintf(buf_colored, 512, "\033[1;31;7m[ERROR]\033[0m\033[1;31m %s\033[0m", buf); + break; + case DBG_WARNING: + snprintf(buf_colored, 512, "\033[1;35;7m[WARNING]\033[0m\033[1;35m %s\033[0m", buf); + break; + case DBG_DEBUG: + snprintf(buf_colored, 512, "\033[1;30;7m[DEBUG]\033[0m\033[1;30m %s\033[0m", buf); + break; + default: + snprintf(buf_colored, 512, "\033[1;31;7m[UNK]\033[0m\033[1;31m %s\033[0m", buf); + break; + } + debugNetUDPSend(buf_colored); +#endif + + if (g_log_to_file) { + sceIoWrite(g_log_fd, buf, strlen(buf)); + sceIoSyncByFd(g_log_fd); // TODO Is this actually required? + } + + return 0; +} diff --git a/src_updater/debug.h b/src_updater/debug.h new file mode 100644 index 0000000..f2d2d6c --- /dev/null +++ b/src_updater/debug.h @@ -0,0 +1,32 @@ +// FIXME Merge with src/debug.h + +#pragma once + +#include + +#define LOG_DIR "ux0:/log" +#define LOG_FILE UPDATE_TITLEID "_updater" + +inline std::string methodName(const std::string &prettyFunction) { + size_t args_start = prettyFunction.find('('); + size_t begin = prettyFunction.substr(0, args_start).rfind(' ') + 1; + size_t length = args_start - begin; + + return prettyFunction.substr(begin, length) + "()"; +} + + +#define __METHOD_NAME__ methodName(__PRETTY_FUNCTION__) +#define __FILENAME__ (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__) + + +#define DBG_NONE 0 +#define DBG_INFO 1 +#define DBG_ERROR 2 +#define DBG_WARNING 3 +#define DBG_DEBUG 4 + +int log_init(bool log_to_file = false); +int _log_printf(int level, const char *format, ...); + +#define log_printf(level,format,...) _log_printf(level,(std::string("[") + std::string(__FILENAME__) + ":" + std::to_string(__LINE__) + " " +__METHOD_NAME__ + "] " + format + "\n").c_str(),##__VA_ARGS__) diff --git a/src_updater/updater.cpp b/src_updater/updater.cpp new file mode 100644 index 0000000..2bd7d1e --- /dev/null +++ b/src_updater/updater.cpp @@ -0,0 +1,229 @@ +#ifndef UPDATE_TITLEID + #error UPDATE_TITLEID must be specified +#endif +#ifndef PACKAGE_DIR + #warning PACKAGE_DIR not set using default "ux0:data/pkg/" + #define PACKAGE_DIR "ux0:data/pkg/" +#endif + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "debug.h" + + +struct SfoHeader { + uint32_t magic; + uint32_t version; + uint32_t keyofs; + uint32_t valofs; + uint32_t count; +} __attribute__((packed)); + +struct SfoEntry { + uint16_t nameofs; + uint8_t alignment; + uint8_t type; + uint32_t valsize; + uint32_t totalsize; + uint32_t dataofs; +} __attribute__((packed)); + +int launchAppByUriExit(const char *titleid) { + char uri[32]; + sprintf(uri, "psgm:play?titleid=%s", titleid); + + sceAppMgrLaunchAppByUri(0xFFFFF, uri); + sceKernelExitProcess(0); + + return 0; +} + +static int loadScePaf() { + static uint32_t argp[] = { 0x180000, 0xFFFFFFFF, 0xFFFFFFFF, 1, 0xFFFFFFFF, 0xFFFFFFFF }; + + int result = -1; + + uint32_t buf[4]; + buf[0] = sizeof(buf); + buf[1] = (uint32_t)&result; + buf[2] = -1; + buf[3] = -1; + + return sceSysmoduleLoadModuleInternalWithArg(SCE_SYSMODULE_INTERNAL_PAF, sizeof(argp), argp, buf); +} + +static int unloadScePaf() { + uint32_t buf = 0; + return sceSysmoduleUnloadModuleInternalWithArg(SCE_SYSMODULE_INTERNAL_PAF, 0, nullptr, &buf); +} + +int promoteApp(const char *path) { + int res; + + res = loadScePaf(); + if (res < 0) { + log_printf(DBG_ERROR, "loadScePaf() = 0x%08X", res); + return res; + } + + res = sceSysmoduleLoadModuleInternal(SCE_SYSMODULE_INTERNAL_PROMOTER_UTIL); + if (res < 0) { + log_printf(DBG_ERROR, "sceSysmoduleLoadModuleInternal(SCE_SYSMODULE_INTERNAL_PROMOTER_UTIL) = 0x%08X", res); + return res; + } + + res = scePromoterUtilityInit(); + if (res < 0) { + log_printf(DBG_ERROR, "scePromoterUtilityInit() = 0x%08X", res); + return res; + } + + res = scePromoterUtilityPromotePkgWithRif(path, 1); + if (res < 0) { + log_printf(DBG_ERROR, "scePromoterUtilityPromotePkgWithRif(%s, 1) = 0x%08X", path, res); + return res; + } + + res = scePromoterUtilityExit(); + if (res < 0) { + log_printf(DBG_ERROR, "scePromoterUtilityExit() = 0x%08X", res); + return res; + } + + res = sceSysmoduleUnloadModuleInternal(SCE_SYSMODULE_INTERNAL_PROMOTER_UTIL); + if (res < 0) { + log_printf(DBG_ERROR, "sceSysmoduleUnloadModuleInternal(SCE_SYSMODULE_INTERNAL_PROMOTER_UTIL) = 0x%08X", res); + return res; + } + + res = unloadScePaf(); + if (res < 0) { + log_printf(DBG_ERROR, "unloadScePaf() = 0x%08X", res); + return res; + } + + return res; +} + +char *get_title_id(const char *filename) { + log_printf(DBG_DEBUG, "Extracting titleid from %s", filename); + char *res = nullptr; + long size = 0; + FILE *fin = nullptr; + char *buf = nullptr; + int i, ret; + + SfoHeader *header; + SfoEntry *entry; + + fin = fopen(filename, "rb"); + if (!fin) { + log_printf(DBG_ERROR, "fopen(%s, \"rb\") = 0x%08X", filename, fin); + goto cleanup; + } + + ret = fseek(fin, 0, SEEK_END); + if (ret != 0) { + log_printf(DBG_ERROR, "fseek(fin, 0, SEEK_END) = 0x%08X", ret); + goto cleanup; + } + + size = ftell(fin); + if (size == -1) { + log_printf(DBG_ERROR, "ftell(fin) = 0x%08X", size); + goto cleanup; + } + + ret = fseek(fin, 0, SEEK_SET); + if (ret != 0) { + log_printf(DBG_ERROR, "fseek(fin, 0, SEEK_SET) = 0x%08X", ret); + goto cleanup; + } + + buf = (char *)calloc(1, size + 1); + if (!buf) { + log_printf(DBG_ERROR, "Couldn't allocate %i bytes of memory to read sfo file", size); + goto cleanup; + } + + ret = fread(buf, size, 1, fin); + if (ret != 1) { + log_printf(DBG_ERROR, "fread(buf, %i, 1, fin) = 0x%08X", size, ret); + goto cleanup; + } + + header = (SfoHeader*)buf; + entry = (SfoEntry*)(buf + sizeof(SfoHeader)); + log_printf(DBG_DEBUG, "SFO header announces %i entries.", header->count); + + for (i = 0; i < header->count; ++i, ++entry) { + const char *name = buf + header->keyofs + entry->nameofs; + const char *value = buf + header->valofs + entry->dataofs; + + if (name >= buf + size || value >= buf + size) + break; + + log_printf(DBG_DEBUG, "SFO body %s: %s", name, value); + if (strcmp(name, "TITLE_ID") == 0) + res = strdup(value); + } + + log_printf(DBG_DEBUG, "Found title id: %s", res); + + cleanup: + if (buf) + free(buf); + if (fin) + fclose(fin); + + return res; +} + +int main() { + log_init(true); + + int ret; + ret = sceShellUtilInitEvents(0); + if (ret < 0) + log_printf(DBG_ERROR, "sceShellUtilInitEvents(0)=0x%08X", ret); + + ret = sceShellUtilLock(SCE_SHELL_UTIL_LOCK_TYPE_PS_BTN); + if (ret < 0) + log_printf(DBG_ERROR, "sceShellUtilLock(SCE_SHELL_UTIL_LOCK_TYPE_PS_BTN)=0x%08X", ret); + + ret = sceShellUtilLock(SCE_SHELL_UTIL_LOCK_TYPE_POWEROFF_MENU); + if (ret < 0) + log_printf(DBG_ERROR, "sceShellUtilLock(SCE_SHELL_UTIL_LOCK_TYPE_POWEROFF_MENU)=0x%08X", ret); + + log_printf(DBG_DEBUG, "All button locks done"); + + sceAppMgrDestroyOtherApp(); + log_printf(DBG_INFO, "Killed other apps"); + + char *titleid = get_title_id(PACKAGE_DIR "/sce_sys/param.sfo"); + log_printf(DBG_DEBUG, "Found staged app: %s; looking for: %s", titleid, UPDATE_TITLEID); + + if (titleid && strcmp(titleid, UPDATE_TITLEID) == 0) { + log_printf(DBG_INFO, "Staged update found -> Start promoting"); + promoteApp(PACKAGE_DIR); + } else { + log_printf(DBG_WARNING, "Staged app %s didn't match expected app %s", titleid, UPDATE_TITLEID); + }; + + log_printf(DBG_INFO, "All done. Starting updated app: %s", UPDATE_TITLEID); + + sceKernelDelayThread(250000); + launchAppByUriExit(UPDATE_TITLEID); + + return 0; +}