From 696ebde52702f4008b60359dba510997311c7ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=97=B1=20PixelyIon?= Date: Thu, 25 Jul 2019 01:49:43 +0530 Subject: [PATCH] Rewrite C++ parts and UI update This update took way way too long to create. However, it was worthwhile. :) --- .gitmodules | 6 + .idea/codeStyles/Project.xml | 34 +++ .idea/codeStyles/codeStyleConfig.xml | 5 + .idea/discord.xml | 4 + .idea/inspectionProfiles/Project_Default.xml | 6 + .idea/misc.xml | 35 +++ .idea/vcs.xml | 2 + app/CMakeLists.txt | 30 ++- app/build.gradle | 17 +- app/libraries/fmt | 1 + app/libraries/tinyxml2 | 1 + .../unicorn}/include/unicorn/arm64.h | 0 .../unicorn}/include/unicorn/platform.h | 0 .../unicorn}/include/unicorn/unicorn.h | 0 .../unicorn}/libunicorn.a | Bin app/src/main/AndroidManifest.xml | 43 ++-- app/src/main/cpp/core/arm/cpu.cpp | 89 ------- app/src/main/cpp/core/arm/cpu.h | 13 -- app/src/main/cpp/core/arm/memory.cpp | 40 ---- app/src/main/cpp/core/arm/memory.h | 30 --- app/src/main/cpp/core/hos/kernel/ipc.cpp | 49 ---- app/src/main/cpp/core/hos/kernel/ipc.h | 31 --- app/src/main/cpp/core/hos/kernel/kernel.cpp | 16 -- app/src/main/cpp/core/hos/kernel/kernel.h | 21 -- app/src/main/cpp/core/hos/kernel/svc.cpp | 219 ------------------ app/src/main/cpp/core/hos/kernel/svc.h | 5 - app/src/main/cpp/core/hos/loaders/nro.cpp | 55 ----- app/src/main/cpp/core/hos/loaders/nro.h | 31 --- app/src/main/cpp/lightswitch.cpp | 54 ++++- app/src/main/cpp/switch/common.cpp | 81 +++++++ app/src/main/cpp/switch/common.h | 71 ++++++ app/src/main/cpp/switch/constant.h | 20 ++ app/src/main/cpp/switch/device.h | 36 +++ app/src/main/cpp/switch/hw/cpu.cpp | 46 ++++ app/src/main/cpp/switch/hw/cpu.h | 29 +++ app/src/main/cpp/switch/hw/memory.cpp | 45 ++++ app/src/main/cpp/switch/hw/memory.h | 36 +++ app/src/main/cpp/switch/loader/loader.h | 24 ++ app/src/main/cpp/switch/loader/nro.cpp | 36 +++ app/src/main/cpp/switch/loader/nro.h | 43 ++++ app/src/main/cpp/switch/os/ipc.cpp | 31 +++ app/src/main/cpp/switch/os/ipc.h | 30 +++ app/src/main/cpp/switch/os/kernel.cpp | 12 + app/src/main/cpp/switch/os/kernel.h | 30 +++ app/src/main/cpp/switch/os/os.cpp | 49 ++++ app/src/main/cpp/switch/os/os.h | 26 +++ app/src/main/cpp/switch/os/svc.cpp | 60 +++++ app/src/main/cpp/switch/os/svc.h | 184 +++++++++++++++ .../lightswitch/lightswitch/GameAdapter.java | 149 ++++++++++++ .../lightswitch/HeaderAdapter.java | 189 +++++++++++++++ .../lightswitch/lightswitch/LogActivity.java | 141 +++++++++++ .../lightswitch/lightswitch/LogAdapter.java | 108 +++++++++ .../lightswitch/MainActivity.java | 92 ++++++-- .../lightswitch/lightswitch/NroLoader.java} | 7 +- .../lightswitch/SettingsActivity.java | 3 +- .../gq/cyuubi/lightswitch/FileAdapter.java | 114 --------- app/src/main/res/drawable/ic_clear.xml | 5 + .../drawable/{ic_console.xml => ic_log.xml} | 0 app/src/main/res/drawable/ic_search.xml | 5 + .../layout/{file_item.xml => game_item.xml} | 0 app/src/main/res/layout/log_activity.xml | 29 +++ app/src/main/res/layout/log_item.xml | 26 +++ app/src/main/res/layout/main_activity.xml | 23 +- app/src/main/res/layout/section_item.xml | 18 ++ app/src/main/res/menu/toolbar_log.xml | 15 ++ .../menu/{toolbar.xml => toolbar_main.xml} | 6 + .../mipmap-hdpi/ic_launcher_foreground.png | Bin 6696 -> 0 bytes .../mipmap-hdpi/ic_launcher_foreground.webp | Bin 0 -> 3700 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 3808 -> 0 bytes .../mipmap-mdpi/ic_launcher_foreground.webp | Bin 0 -> 2216 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 9872 -> 0 bytes .../mipmap-xhdpi/ic_launcher_foreground.webp | Bin 0 -> 5332 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 17391 -> 0 bytes .../mipmap-xxhdpi/ic_launcher_foreground.webp | Bin 0 -> 9524 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 25771 -> 0 bytes .../ic_launcher_foreground.webp | Bin 0 -> 14390 bytes app/src/main/res/values/array.xml | 18 +- app/src/main/res/values/colors.xml | 2 +- app/src/main/res/values/strings.xml | 21 +- app/src/main/res/values/styles.xml | 9 +- app/src/main/res/xml/preferences.xml | 11 + build.gradle | 3 +- gradle.properties | 7 +- 83 files changed, 1918 insertions(+), 809 deletions(-) create mode 100644 .gitmodules create mode 100644 .idea/codeStyles/Project.xml create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/discord.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 160000 app/libraries/fmt create mode 160000 app/libraries/tinyxml2 rename app/{src/main/cpp => libraries/unicorn}/include/unicorn/arm64.h (100%) rename app/{src/main/cpp => libraries/unicorn}/include/unicorn/platform.h (100%) rename app/{src/main/cpp => libraries/unicorn}/include/unicorn/unicorn.h (100%) rename app/{src/main/cpp/lib/arm64-v8a => libraries/unicorn}/libunicorn.a (100%) delete mode 100644 app/src/main/cpp/core/arm/cpu.cpp delete mode 100644 app/src/main/cpp/core/arm/cpu.h delete mode 100644 app/src/main/cpp/core/arm/memory.cpp delete mode 100644 app/src/main/cpp/core/arm/memory.h delete mode 100644 app/src/main/cpp/core/hos/kernel/ipc.cpp delete mode 100644 app/src/main/cpp/core/hos/kernel/ipc.h delete mode 100644 app/src/main/cpp/core/hos/kernel/kernel.cpp delete mode 100644 app/src/main/cpp/core/hos/kernel/kernel.h delete mode 100644 app/src/main/cpp/core/hos/kernel/svc.cpp delete mode 100644 app/src/main/cpp/core/hos/kernel/svc.h delete mode 100644 app/src/main/cpp/core/hos/loaders/nro.cpp delete mode 100644 app/src/main/cpp/core/hos/loaders/nro.h create mode 100644 app/src/main/cpp/switch/common.cpp create mode 100644 app/src/main/cpp/switch/common.h create mode 100644 app/src/main/cpp/switch/constant.h create mode 100644 app/src/main/cpp/switch/device.h create mode 100644 app/src/main/cpp/switch/hw/cpu.cpp create mode 100644 app/src/main/cpp/switch/hw/cpu.h create mode 100644 app/src/main/cpp/switch/hw/memory.cpp create mode 100644 app/src/main/cpp/switch/hw/memory.h create mode 100644 app/src/main/cpp/switch/loader/loader.h create mode 100644 app/src/main/cpp/switch/loader/nro.cpp create mode 100644 app/src/main/cpp/switch/loader/nro.h create mode 100644 app/src/main/cpp/switch/os/ipc.cpp create mode 100644 app/src/main/cpp/switch/os/ipc.h create mode 100644 app/src/main/cpp/switch/os/kernel.cpp create mode 100644 app/src/main/cpp/switch/os/kernel.h create mode 100644 app/src/main/cpp/switch/os/os.cpp create mode 100644 app/src/main/cpp/switch/os/os.h create mode 100644 app/src/main/cpp/switch/os/svc.cpp create mode 100644 app/src/main/cpp/switch/os/svc.h create mode 100644 app/src/main/java/emu/lightswitch/lightswitch/GameAdapter.java create mode 100644 app/src/main/java/emu/lightswitch/lightswitch/HeaderAdapter.java create mode 100644 app/src/main/java/emu/lightswitch/lightswitch/LogActivity.java create mode 100644 app/src/main/java/emu/lightswitch/lightswitch/LogAdapter.java rename app/src/main/java/{gq/cyuubi => emu/lightswitch}/lightswitch/MainActivity.java (53%) rename app/src/main/java/{gq/cyuubi/lightswitch/NroMeta.java => emu/lightswitch/lightswitch/NroLoader.java} (96%) rename app/src/main/java/{gq/cyuubi => emu/lightswitch}/lightswitch/SettingsActivity.java (96%) delete mode 100644 app/src/main/java/gq/cyuubi/lightswitch/FileAdapter.java create mode 100644 app/src/main/res/drawable/ic_clear.xml rename app/src/main/res/drawable/{ic_console.xml => ic_log.xml} (100%) create mode 100644 app/src/main/res/drawable/ic_search.xml rename app/src/main/res/layout/{file_item.xml => game_item.xml} (100%) create mode 100644 app/src/main/res/layout/log_activity.xml create mode 100644 app/src/main/res/layout/log_item.xml create mode 100644 app/src/main/res/layout/section_item.xml create mode 100644 app/src/main/res/menu/toolbar_log.xml rename app/src/main/res/menu/{toolbar.xml => toolbar_main.xml} (66%) delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..ca304b62 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "app/libraries/tinyxml2"] + path = app/libraries/tinyxml2 + url = https://github.com/leethomason/tinyxml2 +[submodule "app/libraries/fmt"] + path = app/libraries/fmt + url = https://github.com/fmtlib/fmt diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 00000000..b522fcfe --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,34 @@ + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 00000000..79ee123c --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml new file mode 100644 index 00000000..1a65715f --- /dev/null +++ b/.idea/discord.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..35ee3ce2 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 37a75096..51b39f74 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,5 +1,40 @@ + + + + diff --git a/.idea/vcs.xml b/.idea/vcs.xml index 35eb1ddf..c399eec8 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -2,5 +2,7 @@ + + \ No newline at end of file diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index ea548beb..7e3bf2ff 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -1,20 +1,28 @@ -cmake_minimum_required (VERSION 3.2) +cmake_minimum_required(VERSION 3.8) project(Lightswitch VERSION 1 LANGUAGES CXX) set_property(GLOBAL PROPERTY CMAKE_CXX_STANDARD 17 PROPERTY CMAKE_CXX_STANDARD_REQUIRED TRUE) +set(BUILD_TESTS FALSE) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED TRUE) +add_subdirectory("libraries/tinyxml2") +add_subdirectory("libraries/fmt") + set(source_DIR ${CMAKE_SOURCE_DIR}/src/main/cpp) -include_directories(${source_DIR}/include) +include_directories(libraries/unicorn/include) include_directories(${source_DIR}) add_library(lightswitch SHARED ${source_DIR}/lightswitch.cpp - ${source_DIR}/core/arm/cpu.cpp - ${source_DIR}/core/arm/memory.cpp - - ${source_DIR}/core/hos/kernel/ipc.cpp - ${source_DIR}/core/hos/kernel/kernel.cpp - ${source_DIR}/core/hos/kernel/svc.cpp - ${source_DIR}/core/hos/loaders/nro.cpp -) -target_link_libraries(lightswitch ${source_DIR}/lib/${ANDROID_ABI}/libunicorn.a) + ${source_DIR}/switch/os/os.cpp + ${source_DIR}/switch/os/ipc.cpp + ${source_DIR}/switch/os/kernel.cpp + ${source_DIR}/switch/os/svc.cpp + ${source_DIR}/switch/hw/cpu.cpp + ${source_DIR}/switch/hw/memory.cpp + ${source_DIR}/switch/common.cpp + ${source_DIR}/switch/loader/nro.cpp + ) +target_link_libraries(lightswitch ${CMAKE_SOURCE_DIR}/libraries/unicorn/libunicorn.a fmt tinyxml2) +target_compile_options(lightswitch PRIVATE -Wno-c++17-extensions) diff --git a/app/build.gradle b/app/build.gradle index 5e35c98b..e3730acc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,28 +4,28 @@ android { compileSdkVersion 29 buildToolsVersion "29.0.0" defaultConfig { - applicationId "gq.cyuubi.lightswitch" + applicationId "emu.lightswitch.lightswitch" minSdkVersion 21 targetSdkVersion 29 versionCode 1 versionName "1.0" - externalNativeBuild { - cmake { - cppFlags "" - } - } ndk { abiFilters "arm64-v8a" } } buildTypes { release { + minifyEnabled true + useProguard false + } + debug { minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + useProguard false } } externalNativeBuild { cmake { + version "3.8.0+" path "CMakeLists.txt" } } @@ -40,6 +40,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation 'androidx.preference:preference:1.1.0-beta01' + implementation 'androidx.preference:preference:1.1.0-rc01' implementation 'com.google.android.material:material:1.0.0' + implementation 'me.xdrop:fuzzywuzzy:1.2.0' } diff --git a/app/libraries/fmt b/app/libraries/fmt new file mode 160000 index 00000000..6a497e1d --- /dev/null +++ b/app/libraries/fmt @@ -0,0 +1 @@ +Subproject commit 6a497e1d061993cce54c2d71506a90155a3725e6 diff --git a/app/libraries/tinyxml2 b/app/libraries/tinyxml2 new file mode 160000 index 00000000..61a4c7d5 --- /dev/null +++ b/app/libraries/tinyxml2 @@ -0,0 +1 @@ +Subproject commit 61a4c7d507322c9f494f5880d4c94b60e4ce9590 diff --git a/app/src/main/cpp/include/unicorn/arm64.h b/app/libraries/unicorn/include/unicorn/arm64.h similarity index 100% rename from app/src/main/cpp/include/unicorn/arm64.h rename to app/libraries/unicorn/include/unicorn/arm64.h diff --git a/app/src/main/cpp/include/unicorn/platform.h b/app/libraries/unicorn/include/unicorn/platform.h similarity index 100% rename from app/src/main/cpp/include/unicorn/platform.h rename to app/libraries/unicorn/include/unicorn/platform.h diff --git a/app/src/main/cpp/include/unicorn/unicorn.h b/app/libraries/unicorn/include/unicorn/unicorn.h similarity index 100% rename from app/src/main/cpp/include/unicorn/unicorn.h rename to app/libraries/unicorn/include/unicorn/unicorn.h diff --git a/app/src/main/cpp/lib/arm64-v8a/libunicorn.a b/app/libraries/unicorn/libunicorn.a similarity index 100% rename from app/src/main/cpp/lib/arm64-v8a/libunicorn.a rename to app/libraries/unicorn/libunicorn.a diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d42488e2..1dece4ef 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,30 +1,39 @@ + xmlns:tools="http://schemas.android.com/tools" + package="emu.lightswitch.lightswitch"> - - + + - + android:allowBackup="true" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="true" + android:theme="@style/AppTheme" + tools:ignore="GoogleAppIndexingWarning"> + + android:name="android.support.PARENT_ACTIVITY" + android:value="emu.lightswitch.lightswitch.MainActivity"/> + + + - + - + diff --git a/app/src/main/cpp/core/arm/cpu.cpp b/app/src/main/cpp/core/arm/cpu.cpp deleted file mode 100644 index e543ba5e..00000000 --- a/app/src/main/cpp/core/arm/cpu.cpp +++ /dev/null @@ -1,89 +0,0 @@ -#include -#include "cpu.h" -#include "memory.h" -#include "core/hos/kernel/svc.h" - -// TODO: Handle Unicorn errors -namespace core::cpu { - uc_engine *uc; - - void HookInterrupt(uc_engine *uc, uint32_t intno, void *user_data); - - bool Initialize() - { - uc_open(UC_ARCH_ARM64, UC_MODE_ARM, &uc); - - uc_hook hook{}; - uc_hook_add(uc, &hook, UC_HOOK_INTR, (void *) HookInterrupt, 0, 1, 0); - - // Map stack memory - if (!memory::Map(0x3000000, 0x1000000, "Stack")) return false; - SetRegister(UC_ARM64_REG_SP, 0x3100000); - - // Map TLS memory - if (!memory::Map(0x2000000, 0x1000, "TLS")) return false; - SetRegister(UC_ARM64_REG_TPIDRRO_EL0, 0x2000000); - - return true; - } - - void Run(uint64_t address) - { - uc_err err = uc_emu_start(uc, address, 1ULL << 63, 0, 0); - if (err) syslog(LOG_ERR, "uc_emu_start failed: %s", uc_strerror(err)); - } - - uint64_t GetRegister(uint32_t regid) - { - uint64_t registerValue; - uc_reg_read(uc, regid, ®isterValue); - return registerValue; - } - - void SetRegister(uint32_t regid, uint64_t value) - { - uc_reg_write(uc, regid, &value); - } - - void HookInterrupt(uc_engine *uc, uint32_t intno, void *user_data) - { - if (intno == 2) - { - uint32_t instr{}; - uc_mem_read(uc, GetRegister(UC_ARM64_REG_PC) - 4, &instr, 4); - - uint32_t svcId = instr >> 5 & 0xFF; - - if (core::kernel::SvcHandler(svcId) == 0x177202) - uc_close(uc); - } - else - { - syslog(LOG_ERR, "Unhandled interrupt #%i", intno); - uc_close(uc); - } - } -} - -// FIXME: Move this back to memory.cpp - zephyren25 -namespace core::memory { - bool Map(uint64_t address, size_t size, std::string label) { - void *ptr = mmap((void*)(address), size, PROT_EXEC | PROT_READ | PROT_WRITE, - MAP_PRIVATE | MAP_ANON, 0, 0); - if (!ptr) - { - syslog(LOG_ERR, "Failed mapping region '%s'", label.c_str()); - return false; - } - - uc_err err = uc_mem_map_ptr(core::cpu::uc, address, size, UC_PROT_ALL, (void*)(address)); - if (err) - { - syslog(LOG_ERR, "UC map failed: %s", uc_strerror(err)); - return false; - } - - syslog(LOG_DEBUG, "Successfully mapped region '%s' to 0x%x", label.c_str(), address); - return true; - } -} \ No newline at end of file diff --git a/app/src/main/cpp/core/arm/cpu.h b/app/src/main/cpp/core/arm/cpu.h deleted file mode 100644 index 82cbc6ac..00000000 --- a/app/src/main/cpp/core/arm/cpu.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once -#include -#include - -namespace core::cpu { - bool Initialize(); - void Run(uint64_t address); - - uint64_t GetRegister(uint32_t regid); - void SetRegister(uint32_t regid, uint64_t value); - -// bool MapUnicorn(uint64_t address, size_t size); -} \ No newline at end of file diff --git a/app/src/main/cpp/core/arm/memory.cpp b/app/src/main/cpp/core/arm/memory.cpp deleted file mode 100644 index 0e8bf7d5..00000000 --- a/app/src/main/cpp/core/arm/memory.cpp +++ /dev/null @@ -1,40 +0,0 @@ -#include -#include -#include -#include "memory.h" - -namespace core::memory { - /*std::vector memoryRegions; - - bool Map(uint64_t address, size_t size, std::string label) { - void* ptr = mmap((void*)(address), size, PROT_EXEC | PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, 0, 0); - if (!ptr) { - syslog(LOG_ERR, "Failed mapping region '%s'", label.c_str()); - return false; - } - - syslog(LOG_INFO, "Mapping region '%s' to 0x%x, pointer %x", label.c_str(), address, (uint64_t)ptr); - - if (core::cpu::MapUnicorn(address, size, ptr)) return false; - - syslog(LOG_INFO, "Successfully mapped region '%s' to 0x%x", label.c_str(), address); - - memoryRegions.push_back({label, address, size, ptr}); - return true; - }*/ - - // TODO: Boundary checks - void Write(void* data, uint64_t offset, size_t size) { std::memcpy((void*)(offset), data, size); } - - void WriteU8(uint8_t value, uint64_t offset) { Write(reinterpret_cast(&value), offset, 1); } - void WriteU16(uint16_t value, uint64_t offset) { Write(reinterpret_cast(&value), offset, 2); } - void WriteU32(uint32_t value, uint64_t offset) { Write(reinterpret_cast(&value), offset, 4); } - void WriteU64(uint64_t value, uint64_t offset) { Write(reinterpret_cast(&value), offset, 8); } - - void Read(void* destination, uint64_t offset, size_t size) { std::memcpy(destination, (void*)(offset), size); } - - uint8_t ReadU8(uint64_t offset) { uint8_t value; Read(reinterpret_cast(&value), offset, 1); return value; } - uint16_t ReadU16(uint64_t offset) { uint16_t value; Read(reinterpret_cast(&value), offset, 2); return value; } - uint32_t ReadU32(uint64_t offset) { uint32_t value; Read(reinterpret_cast(&value), offset, 4); return value; } - uint64_t ReadU64(uint64_t offset) { uint64_t value; Read(reinterpret_cast(&value), offset, 8); return value; } -} \ No newline at end of file diff --git a/app/src/main/cpp/core/arm/memory.h b/app/src/main/cpp/core/arm/memory.h deleted file mode 100644 index f7e02762..00000000 --- a/app/src/main/cpp/core/arm/memory.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once -#include -#include - -#define BASE_ADDRESS 0x80000000 - -namespace core::memory { - /*struct MemoryRegion { - std::string label; - uint64_t address; - size_t size; - void* ptr; - };*/ - - bool Map(uint64_t address, size_t size, std::string label = {}); - - void Write(void* data, uint64_t offset, size_t size); - - void WriteU8(uint8_t value, uint64_t offset); - void WriteU16(uint16_t value, uint64_t offset); - void WriteU32(uint32_t value, uint64_t offset); - void WriteU64(uint64_t value, uint64_t offset); - - void Read(void* destination, uint64_t offset, size_t size); - - uint8_t ReadU8(uint64_t offset); - uint16_t ReadU16(uint64_t offset); - uint32_t ReadU32(uint64_t offset); - uint64_t ReadU64(uint64_t offset); -} \ No newline at end of file diff --git a/app/src/main/cpp/core/hos/kernel/ipc.cpp b/app/src/main/cpp/core/hos/kernel/ipc.cpp deleted file mode 100644 index 8fdba736..00000000 --- a/app/src/main/cpp/core/hos/kernel/ipc.cpp +++ /dev/null @@ -1,49 +0,0 @@ -#include -#include -#include "ipc.h" - -namespace core::kernel -{ - IpcRequest::IpcRequest(uint8_t* tlsPtr) - { - for(int i = 0; i < 32; i++) - syslog(LOG_DEBUG, "%02x\t%02x %02x %02x %02x %02x %02x %02x %02x", i*8, tlsPtr[0+(i*8)], tlsPtr[1+(i*8)], tlsPtr[2+(i*8)], tlsPtr[3+(i*8)], tlsPtr[4+(i*8)], tlsPtr[5+(i*8)], tlsPtr[6+(i*8)], tlsPtr[7+(i*8)]); - syslog(LOG_DEBUG, "-----------------------"); - uint32_t word1 = ((uint32_t*)tlsPtr)[1]; - type = *(uint16_t*)tlsPtr; - xCount = tlsPtr[2] & 0xF0 >> 4; - aCount = tlsPtr[2] & 0x0F; - bCount = tlsPtr[3] & 0xF0 >> 4; - wCount = tlsPtr[3] & 0x0F; - dataSize = word1 & 0x3FF; - dataPos = 8; - - if(tlsPtr[2] || tlsPtr[3]) - { - syslog(LOG_ERR, "IPC - X/A/B/W descriptors"); - exit(0); - } - - syslog(LOG_DEBUG, "Enable handle descriptor: %s", word1 >> 31 ? "yes" : "no"); - if(word1 >> 31) - { - syslog(LOG_ERR, "IPC - Handle descriptor"); - exit(0); - } - - // Align to 16 bytes - if((dataPos % 16) != 0) - dataPos += 16 - (dataPos % 16); - dataPtr = &tlsPtr[dataPos+16]; - - syslog(LOG_DEBUG, "Type: %x", type); - syslog(LOG_DEBUG, "X descriptors: 0x%x", xCount); - syslog(LOG_DEBUG, "A descriptors: 0x%x", aCount); - syslog(LOG_DEBUG, "B descriptors: 0x%x", bCount); - syslog(LOG_DEBUG, "W descriptors: 0x%x", wCount); - syslog(LOG_DEBUG, "Raw data size: 0x%x", word1 & 0x3FF); - syslog(LOG_DEBUG, "Data offset=%x, Data size=%x", dataPos, dataSize); - syslog(LOG_DEBUG, "Payload CmdId=%i", *((uint32_t*)&tlsPtr[dataPos+8])); - syslog(LOG_DEBUG, "Setting dataPtr to %08x", dataPos+16); - } -} \ No newline at end of file diff --git a/app/src/main/cpp/core/hos/kernel/ipc.h b/app/src/main/cpp/core/hos/kernel/ipc.h deleted file mode 100644 index 657392b2..00000000 --- a/app/src/main/cpp/core/hos/kernel/ipc.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once -#include - -namespace core::kernel -{ -class IpcRequest -{ -public: - IpcRequest(uint8_t* tlsPtr); - - template - T GetValue() - { - dataPos += sizeof(T); - return *reinterpret_cast(&dataPtr[dataPos-sizeof(T)]); - } - - uint16_t type, xCount, aCount, bCount, wCount; - uint32_t dataSize; - -private: - uint8_t* dataPtr; - uint32_t dataPos; -}; - -class IpcResponse -{ -public: - IpcResponse() {} -}; -} \ No newline at end of file diff --git a/app/src/main/cpp/core/hos/kernel/kernel.cpp b/app/src/main/cpp/core/hos/kernel/kernel.cpp deleted file mode 100644 index 7c4675af..00000000 --- a/app/src/main/cpp/core/hos/kernel/kernel.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include -#include -#include "kernel.h" - -namespace core::kernel -{ - std::unordered_map handles; - uint32_t handleIndex = 0xd001; - - uint32_t NewHandle(KObjectPtr obj) - { - handles.insert({handleIndex, obj}); - syslog(LOG_DEBUG, "Creating new handle 0x%x", handleIndex); - return handleIndex++; - } -} \ No newline at end of file diff --git a/app/src/main/cpp/core/hos/kernel/kernel.h b/app/src/main/cpp/core/hos/kernel/kernel.h deleted file mode 100644 index 309d3c23..00000000 --- a/app/src/main/cpp/core/hos/kernel/kernel.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once -#include -#include - -#define SM_HANDLE 0xd000 // sm: is hardcoded for now - -namespace core::kernel -{ - class KObject - { - public: - KObject(uint32_t handle) : handle(handle) {} - uint32_t Handle() { return handle; } - private: - uint32_t handle; - }; - - typedef std::shared_ptr KObjectPtr; - - uint32_t NewHandle(KObjectPtr obj); -} \ No newline at end of file diff --git a/app/src/main/cpp/core/hos/kernel/svc.cpp b/app/src/main/cpp/core/hos/kernel/svc.cpp deleted file mode 100644 index 5a1b9a9e..00000000 --- a/app/src/main/cpp/core/hos/kernel/svc.cpp +++ /dev/null @@ -1,219 +0,0 @@ -#include -#include -#include -#include -#include "core/arm/cpu.h" -#include "core/arm/memory.h" -#include "kernel.h" -#include "ipc.h" -#include "svc.h" - -using namespace core::cpu; -namespace core::kernel { - static uint32_t ConnectToNamedPort() - { - std::string port(8, '\0'); - memory::Read((void*)port.data(), GetRegister(UC_ARM64_REG_X1), 8); - - if(std::strcmp(port.c_str(), "sm:") == 0) - SetRegister(UC_ARM64_REG_W1, SM_HANDLE); - else - { - syslog(LOG_ERR, "svcConnectToNamedPort tried connecting to invalid port '%s'", port.c_str()); - exit(0); - } - - return 0; - } - - static uint32_t SendSyncRequest() - { - syslog(LOG_INFO, "svcSendSyncRequest called for handle 0x%x, dumping TLS:", GetRegister(UC_ARM64_REG_X0)); - uint8_t tls[0x100]; - memory::Read(&tls, 0x2000000, 0x100); - - core::kernel::IpcRequest* r = new core::kernel::IpcRequest(tls); - - exit(0); - return 0; - } - - static uint32_t OutputDebugString() - { - std::string debug(GetRegister(UC_ARM64_REG_X1), '\0'); - memory::Read((void*)debug.data(), GetRegister(UC_ARM64_REG_X0), GetRegister(UC_ARM64_REG_X1)); - - syslog(LOG_DEBUG, "svcOutputDebugString: %s", debug.c_str()); - return 0; - } - - static uint32_t GetInfo() - { - switch (GetRegister(UC_ARM64_REG_X1)) - { - case 8: // IsCurrentProcessBeingDebugged - SetRegister(UC_ARM64_REG_X1, 0); // We're just lying to ourselves. Think about it - break; - case 12: // AddressSpaceBaseAddr - SetRegister(UC_ARM64_REG_X1, BASE_ADDRESS); - break; - case 18: // TitleId - SetRegister(UC_ARM64_REG_X1, 0); // TODO: Add this - break; - default: - syslog(LOG_ERR, "Unimplemented GetInfo ID! ID1 = %i, ID2 = %i", GetRegister(UC_ARM64_REG_X1), GetRegister(UC_ARM64_REG_X3)); - return 0x177202; - } - - return 0; - } - - std::pair svcTable[] = - { - {0x00, nullptr}, - {0x01, nullptr}, - {0x02, nullptr}, - {0x03, nullptr}, - {0x04, nullptr}, - {0x05, nullptr}, - {0x06, nullptr}, - {0x07, nullptr}, - {0x08, nullptr}, - {0x09, nullptr}, - {0x0a, nullptr}, - {0x0b, nullptr}, - {0x0c, nullptr}, - {0x0d, nullptr}, - {0x0e, nullptr}, - {0x0f, nullptr}, - {0x10, nullptr}, - {0x11, nullptr}, - {0x12, nullptr}, - {0x13, nullptr}, - {0x14, nullptr}, - {0x15, nullptr}, - {0x16, nullptr}, - {0x17, nullptr}, - {0x18, nullptr}, - {0x19, nullptr}, - {0x1a, nullptr}, - {0x1b, nullptr}, - {0x1c, nullptr}, - {0x1d, nullptr}, - {0x1e, nullptr}, - {0x1f, ConnectToNamedPort}, - {0x20, nullptr}, - {0x21, SendSyncRequest}, - {0x22, nullptr}, - {0x23, nullptr}, - {0x24, nullptr}, - {0x25, nullptr}, - {0x26, nullptr}, - {0x27, OutputDebugString}, - {0x28, nullptr}, - {0x29, GetInfo}, - {0x2a, nullptr}, - {0x2b, nullptr}, - {0x2c, nullptr}, - {0x2d, nullptr}, - {0x2e, nullptr}, - {0x2f, nullptr}, - {0x30, nullptr}, - {0x31, nullptr}, - {0x32, nullptr}, - {0x33, nullptr}, - {0x34, nullptr}, - {0x35, nullptr}, - {0x36, nullptr}, - {0x37, nullptr}, - {0x38, nullptr}, - {0x39, nullptr}, - {0x3a, nullptr}, - {0x3b, nullptr}, - {0x3c, nullptr}, - {0x3d, nullptr}, - {0x3e, nullptr}, - {0x3f, nullptr}, - {0x40, nullptr}, - {0x41, nullptr}, - {0x42, nullptr}, - {0x43, nullptr}, - {0x44, nullptr}, - {0x45, nullptr}, - {0x46, nullptr}, - {0x47, nullptr}, - {0x48, nullptr}, - {0x49, nullptr}, - {0x4a, nullptr}, - {0x4b, nullptr}, - {0x4c, nullptr}, - {0x4d, nullptr}, - {0x4e, nullptr}, - {0x4f, nullptr}, - {0x50, nullptr}, - {0x51, nullptr}, - {0x52, nullptr}, - {0x53, nullptr}, - {0x54, nullptr}, - {0x55, nullptr}, - {0x56, nullptr}, - {0x57, nullptr}, - {0x58, nullptr}, - {0x59, nullptr}, - {0x5a, nullptr}, - {0x5b, nullptr}, - {0x5c, nullptr}, - {0x5d, nullptr}, - {0x5e, nullptr}, - {0x5f, nullptr}, - {0x60, nullptr}, - {0x61, nullptr}, - {0x62, nullptr}, - {0x63, nullptr}, - {0x64, nullptr}, - {0x65, nullptr}, - {0x66, nullptr}, - {0x67, nullptr}, - {0x68, nullptr}, - {0x69, nullptr}, - {0x6a, nullptr}, - {0x6b, nullptr}, - {0x6c, nullptr}, - {0x6d, nullptr}, - {0x6e, nullptr}, - {0x6f, nullptr}, - {0x70, nullptr}, - {0x71, nullptr}, - {0x72, nullptr}, - {0x73, nullptr}, - {0x74, nullptr}, - {0x75, nullptr}, - {0x76, nullptr}, - {0x77, nullptr}, - {0x78, nullptr}, - {0x79, nullptr}, - {0x7a, nullptr}, - {0x7b, nullptr}, - {0x7c, nullptr}, - {0x7d, nullptr}, - {0x7e, nullptr}, - {0x7f, nullptr} - }; - - uint32_t SvcHandler(uint32_t svc) - { - std::pair* result = &(svcTable[svc]); - - if (result->second) - { - uint32_t returnCode = result->second(); - SetRegister(UC_ARM64_REG_W0, returnCode); - return returnCode; - } - else - { - syslog(LOG_ERR, "Unimplemented SVC 0x%02x", svc); - return 0x177202; // "Unimplemented behaviour" - } - } -} \ No newline at end of file diff --git a/app/src/main/cpp/core/hos/kernel/svc.h b/app/src/main/cpp/core/hos/kernel/svc.h deleted file mode 100644 index 0d9b7ae4..00000000 --- a/app/src/main/cpp/core/hos/kernel/svc.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once - -namespace core::kernel { - uint32_t SvcHandler(uint32_t svc); -} \ No newline at end of file diff --git a/app/src/main/cpp/core/hos/loaders/nro.cpp b/app/src/main/cpp/core/hos/loaders/nro.cpp deleted file mode 100644 index 6414a842..00000000 --- a/app/src/main/cpp/core/hos/loaders/nro.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include -#include -#include -#include -#include "core/arm/cpu.h" -#include "core/arm/memory.h" -#include "nro.h" - -void ReadDataFromFile(std::string file, char* output, uint32_t offset, size_t size) -{ - std::ifstream f(file, std::ios::binary | std::ios::beg); - - f.seekg(offset); - f.read(output, size); - - f.close(); -} - -namespace core::loader { - bool LoadNro(std::string filePath) - { - syslog(LOG_INFO, "Loading NRO file %s\n", filePath.c_str()); - - NroHeader header; - ReadDataFromFile(filePath, reinterpret_cast(&header), 0x0, sizeof(NroHeader)); - if (header.magic != 0x304F524E) - { - syslog(LOG_ERR, "Invalid NRO magic 0x%x\n", header.magic); - return false; - } - - std::vector text, ro, data; - text.resize(header.segments[0].size); - ro.resize (header.segments[1].size); - data.resize(header.segments[2].size); - - ReadDataFromFile(filePath, reinterpret_cast(text.data()), header.segments[0].fileOffset, header.segments[0].size); - ReadDataFromFile(filePath, reinterpret_cast(ro.data()), header.segments[1].fileOffset, header.segments[1].size); - ReadDataFromFile(filePath, reinterpret_cast(data.data()), header.segments[2].fileOffset, header.segments[2].size); - - if (!memory::Map(BASE_ADDRESS, header.segments[0].size, ".text") || - !memory::Map(BASE_ADDRESS + header.segments[0].size, header.segments[1].size, ".ro") || - !memory::Map(BASE_ADDRESS + header.segments[0].size + header.segments[1].size, header.segments[2].size, ".data") || - !memory::Map(BASE_ADDRESS + header.segments[0].size + header.segments[1].size + header.segments[2].size, header.bssSize, ".bss")) - { - syslog(LOG_ERR, "Failed mapping regions for executable"); - return false; - } - - memory::Write(text.data(), BASE_ADDRESS, text.size()); - memory::Write(ro.data(), BASE_ADDRESS + header.segments[0].size, ro.size()); - memory::Write(data.data(), BASE_ADDRESS + header.segments[0].size + header.segments[1].size, data.size()); - return true; - } -} \ No newline at end of file diff --git a/app/src/main/cpp/core/hos/loaders/nro.h b/app/src/main/cpp/core/hos/loaders/nro.h deleted file mode 100644 index 1b8291d1..00000000 --- a/app/src/main/cpp/core/hos/loaders/nro.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once -#include - -namespace core::loader { - struct NroSegmentHeader { - uint32_t fileOffset; - uint32_t size; - }; - - struct NroHeader { - uint32_t unused; - uint32_t modOffset; - uint64_t padding; - - uint32_t magic; - uint32_t version; - uint32_t size; - uint32_t flags; - - NroSegmentHeader segments[3]; - - uint32_t bssSize; - uint32_t reserved0; - uint64_t buildId[4]; - uint64_t reserved1; - - NroSegmentHeader extraSegments[3]; - }; - - bool LoadNro(std::string filePath); -} \ No newline at end of file diff --git a/app/src/main/cpp/lightswitch.cpp b/app/src/main/cpp/lightswitch.cpp index fadc19a7..d3bbf0ae 100644 --- a/app/src/main/cpp/lightswitch.cpp +++ b/app/src/main/cpp/lightswitch.cpp @@ -1,16 +1,50 @@ #include #include -#include -#include -#include -#include +#include +#include +#include +#include "switch/device.h" +#include "switch/common.h" + +std::thread *game_thread; + +void signal_handle(int sig_no) { + throw lightSwitch::exception("A signal has been raised: " + std::to_string(sig_no)); +} + +void thread_main(std::string rom_path, std::string pref_path, std::string log_path) { + auto log = std::make_shared(log_path); + log->write(lightSwitch::Logger::INFO, "Launching ROM {0}", rom_path); +// long long i = 0; +// while(true){ +// log->write(lightSwitch::Logger::INFO, "#{0}", i); +// sleep(1); +// i++; +// } + auto settings = std::make_shared(pref_path); + try { + lightSwitch::device device(log, settings); + device.run(rom_path); + log->write(lightSwitch::Logger::INFO, "Emulation has ended!"); + } catch (std::exception &e) { + log->write(lightSwitch::Logger::ERROR, e.what()); + } catch (...) { + log->write(lightSwitch::Logger::ERROR, "An unknown exception has occurred."); + } +} extern "C" JNIEXPORT void JNICALL -Java_gq_cyuubi_lightswitch_MainActivity_loadFile(JNIEnv *env, jobject instance, jstring file_) { - const char *file = env->GetStringUTFChars(file_, 0); - core::cpu::Initialize(); - core::loader::LoadNro(file); - core::cpu::Run(BASE_ADDRESS); - env->ReleaseStringUTFChars(file_, file); +Java_emu_lightswitch_lightswitch_MainActivity_loadFile(JNIEnv *env, jobject instance, jstring rom_path_, + jstring pref_path_, jstring log_path_) { + const char *rom_path = env->GetStringUTFChars(rom_path_, 0); + const char *pref_path = env->GetStringUTFChars(pref_path_, 0); + const char *log_path = env->GetStringUTFChars(log_path_, 0); + // std::signal(SIGABRT, signal_handle); + if (game_thread) pthread_kill(game_thread->native_handle(), SIGABRT); + // Running on UI thread is not a good idea, any crashes and such will be propagated + game_thread = new std::thread(thread_main, std::string(rom_path, strlen(rom_path)), std::string(pref_path, strlen(pref_path)), std::string(log_path, strlen(log_path))); + env->ReleaseStringUTFChars(rom_path_, rom_path); + env->ReleaseStringUTFChars(pref_path_, pref_path); + env->ReleaseStringUTFChars(log_path_, log_path); } \ No newline at end of file diff --git a/app/src/main/cpp/switch/common.cpp b/app/src/main/cpp/switch/common.cpp new file mode 100644 index 00000000..f2417590 --- /dev/null +++ b/app/src/main/cpp/switch/common.cpp @@ -0,0 +1,81 @@ +#include "common.h" +#include +#include + +namespace lightSwitch { + // Settings + Settings::Settings(std::string pref_xml) { + tinyxml2::XMLDocument pref; + if (pref.LoadFile(pref_xml.c_str())) { + syslog(LOG_ERR, "TinyXML2 Error: %s", pref.ErrorStr()); + throw pref.ErrorID(); + } + tinyxml2::XMLElement *elem = pref.LastChild()->FirstChild()->ToElement(); + while (elem) { + switch (elem->Value()[0]) { + case 's': + string_map.insert( + std::pair((char *) elem->FindAttribute("name")->Value(), + (char *) elem->GetText())); + break; + case 'b': + bool_map.insert( + std::pair((char *) elem->FindAttribute("name")->Value(), + elem->FindAttribute("value")->BoolValue())); + default: + break; + }; + if (elem->NextSibling()) + elem = elem->NextSibling()->ToElement(); + else break; + } + pref.Clear(); + } + + char *Settings::GetString(char *key) { + return string_map.at(key); + } + + bool Settings::GetBool(char *key) { + return bool_map.at(key); + } + + void Settings::List() { + auto it_s = string_map.begin(); + while (it_s != string_map.end()) { + syslog(LOG_INFO, "Key: %s", it_s->first); + syslog(LOG_INFO, "Value: %s", GetString(it_s->first)); + it_s++; + } + auto it_b = bool_map.begin(); + while (it_b != bool_map.end()) { + syslog(LOG_INFO, "Key: %s", it_b->first); + syslog(LOG_INFO, "Value: %i", GetBool(it_b->first)); + it_b++; + } + } + + // Logger + Logger::Logger(std::string log_path) { + log_file.open(log_path, std::ios::app); + write_header("Logging started"); + } + + Logger::~Logger() { + write_header("Logging ended"); + } + + void Logger::write(Logger::LogLevel level, std::string str) { +#ifdef NDEBUG + if (level == DEBUG) + return; +#endif + log_file << "1|" << level_str[level] << "|" << str << "\n"; + log_file.flush(); + } + + void Logger::write_header(std::string str) { + log_file << "0|" << str << "\n"; + log_file.flush(); + } +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/common.h b/app/src/main/cpp/switch/common.h new file mode 100644 index 00000000..526a39c1 --- /dev/null +++ b/app/src/main/cpp/switch/common.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "hw/cpu.h" +#include "hw/memory.h" +#include "constant.h" + +namespace lightSwitch { + class Settings { + private: + struct KeyCompare { + bool operator()(char const *a, char const *b) const { + return std::strcmp(a, b) < 0; + } + }; + + std::map string_map; + std::map bool_map; + + public: + Settings(std::string pref_xml); + + char *GetString(char *key); + + bool GetBool(char *key); + + void List(); + }; + + class Logger { + private: + std::ofstream log_file; + const char *level_str[4] = {"0", "1", "2", "3"}; + int level_syslog[4] = {LOG_ERR, LOG_WARNING, LOG_INFO, LOG_DEBUG}; + + public: + enum LogLevel { + ERROR, + WARN, + INFO, + DEBUG + }; + + Logger(std::string log_path); + + ~Logger(); + + void write_header(std::string str); + + void write(LogLevel level, std::string str); + + template + void write(Logger::LogLevel level, const S &format_str, Args &&... args) { + write(level, fmt::format(format_str, args...)); + } + }; + + struct device_state { + std::shared_ptr cpu; + std::shared_ptr mem; + std::shared_ptr settings; + std::shared_ptr logger; + }; + //typedef std::shared_ptr device_state; +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/constant.h b/app/src/main/cpp/switch/constant.h new file mode 100644 index 00000000..9e026f18 --- /dev/null +++ b/app/src/main/cpp/switch/constant.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +namespace lightSwitch { + typedef std::runtime_error exception; + + namespace constant { + constexpr uint64_t base_addr = 0x80000000; + constexpr uint64_t stack_addr = 0x3000000; + constexpr size_t stack_size = 0x1000000; + constexpr uint64_t tls_addr = 0x2000000; + constexpr size_t tls_size = 0x1000; + constexpr uint32_t nro_magic = 0x304F524E; // NRO0 in reverse + constexpr uint_t svc_unimpl = 0x177202; // "Unimplemented behaviour" + constexpr uint32_t base_handle_index = 0xd001; + }; +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/device.h b/app/src/main/cpp/switch/device.h new file mode 100644 index 00000000..b95bbffc --- /dev/null +++ b/app/src/main/cpp/switch/device.h @@ -0,0 +1,36 @@ +#pragma once + +#include "os/os.h" +#include "loader/nro.h" + +namespace lightSwitch { + class device { + private: + std::shared_ptr cpu; + std::shared_ptr memory; + os::OS os; + device_state state; + const std::map ext_case = { + {"nro", 1}, + {"NRO", 1} + }; + public: + device(std::shared_ptr &logger, std::shared_ptr &settings) : cpu(new hw::Cpu()), memory(new hw::Memory(cpu->GetEngine())), state{cpu, memory, settings, logger}, os({cpu, memory, settings, logger}) {}; + + void run(std::string rom_file) { + try { + switch (ext_case.at(rom_file.substr(rom_file.find_last_of('.') + 1))) { + case 1: { + loader::NroLoader loader(rom_file, state); + break; + } + default: + break; + } + cpu->Execute(constant::base_addr); + } catch (std::out_of_range &e) { + throw exception("The ROM extension wasn't recognized."); + } + } + }; +}; \ No newline at end of file diff --git a/app/src/main/cpp/switch/hw/cpu.cpp b/app/src/main/cpp/switch/hw/cpu.cpp new file mode 100644 index 00000000..b44a47fe --- /dev/null +++ b/app/src/main/cpp/switch/hw/cpu.cpp @@ -0,0 +1,46 @@ +#include "cpu.h" +#include "../constant.h" + + +namespace lightSwitch::hw { + Cpu::Cpu() { + err = uc_open(UC_ARCH_ARM64, UC_MODE_ARM, &uc); + if (err) + throw std::runtime_error("An error occurred while running 'uc_open': " + std::string(uc_strerror(err))); + } + + Cpu::~Cpu() { + uc_close(uc); + } + + void Cpu::Execute(uint64_t address) { + // Set Registers + SetRegister(UC_ARM64_REG_SP, constant::stack_addr + 0x100000); // Stack Pointer (For some reason programs move the stack pointer backwards so 0x100000 is added) + SetRegister(UC_ARM64_REG_TPIDRRO_EL0, constant::tls_addr); // User Read-Only Thread ID Register + err = uc_emu_start(uc, address, std::numeric_limits::max(), 0, 0); + if (err) + throw std::runtime_error("An error occurred while running 'uc_emu_start': " + std::string(uc_strerror(err))); + } + + void Cpu::StopExecution() { + uc_emu_stop(uc); + } + + uint64_t Cpu::GetRegister(uint32_t reg_id) { + uint64_t registerValue; + err = uc_reg_read(uc, reg_id, ®isterValue); + if (err) + throw std::runtime_error("An error occurred while running 'uc_reg_read': " + std::string(uc_strerror(err))); + return registerValue; + } + + void Cpu::SetRegister(uint32_t regid, uint64_t value) { + err = uc_reg_write(uc, regid, &value); + if (err) + throw std::runtime_error("An error occurred while running 'uc_reg_write': " + std::string(uc_strerror(err))); + } + + uc_engine *Cpu::GetEngine() { + return uc; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/hw/cpu.h b/app/src/main/cpp/switch/hw/cpu.h new file mode 100644 index 00000000..bc9c343e --- /dev/null +++ b/app/src/main/cpp/switch/hw/cpu.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +namespace lightSwitch::hw { + class Cpu { + private: + uc_engine *uc; + uc_err err; + + public: + Cpu(); + + ~Cpu(); + + void SetHook(void *HookInterrupt); + + void Execute(uint64_t address); + + void StopExecution(); + + uint64_t GetRegister(uint32_t regid); + + void SetRegister(uint32_t regid, uint64_t value); + + uc_engine *GetEngine(); + }; +} diff --git a/app/src/main/cpp/switch/hw/memory.cpp b/app/src/main/cpp/switch/hw/memory.cpp new file mode 100644 index 00000000..a32c32d0 --- /dev/null +++ b/app/src/main/cpp/switch/hw/memory.cpp @@ -0,0 +1,45 @@ +#include +#include +#include +#include "memory.h" +#include "../constant.h" + +namespace lightSwitch::hw { + // TODO: Boundary checks + Memory::Memory(uc_engine *uc_) : uc(uc_) { + // Map stack memory + Memory::Map(constant::stack_addr, constant::stack_size, stack); + // Map TLS memory + Memory::Map(constant::tls_addr, constant::tls_size, tls); + } + + void Memory::Map(uint64_t address, size_t size, Region region) { + region_map.insert(std::pair(region, {address, size})); + void *ptr = mmap((void *) address, size, PROT_EXEC | PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, 0, 0); + if (!ptr) + throw exception("An occurred while mapping region"); + uc_err err = uc_mem_map_ptr(uc, address, size, UC_PROT_ALL, (void *) address); + if (err) + throw exception("Unicorn failed to map region: " + std::string(uc_strerror(err))); + } + + void Memory::Write(void *data, uint64_t offset, size_t size) { + std::memcpy((void *) offset, data, size); + } + + template + void Memory::Write(T value, uint64_t offset) { + Write(reinterpret_cast(&value), offset, sizeof(T)); + } + + void Memory::Read(void *destination, uint64_t offset, size_t size) { + std::memcpy(destination, (void *) (offset), size); + } + + template + T Memory::Read(uint64_t offset) { + T value; + Read(reinterpret_cast(&value), offset, sizeof(T)); + return value; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/hw/memory.h b/app/src/main/cpp/switch/hw/memory.h new file mode 100644 index 00000000..adf1d70a --- /dev/null +++ b/app/src/main/cpp/switch/hw/memory.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include + +namespace lightSwitch::hw { + class Memory { + private: + uc_engine *uc; + public: + enum Region { + stack, tls, text, rodata, data, bss + }; + struct RegionData { + uint64_t address; + size_t size; + }; + std::map region_map; + + Memory(uc_engine *uc_); + + void Map(uint64_t address, size_t size, Region region); + + void Write(void *data, uint64_t offset, size_t size); + + template + void Write(T value, uint64_t offset); + + void Read(void *destination, uint64_t offset, size_t size); + + template + T Read(uint64_t offset); + }; + +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/loader/loader.h b/app/src/main/cpp/switch/loader/loader.h new file mode 100644 index 00000000..fd32804d --- /dev/null +++ b/app/src/main/cpp/switch/loader/loader.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include "../common.h" + +namespace lightSwitch::loader { + class Loader { + protected: + std::string file_path; + std::ifstream file; + device_state state; + + template + void ReadOffset(T *output, uint32_t offset, size_t size) { + file.seekg(offset, std::ios_base::beg); + file.read(reinterpret_cast(output), size); + } + + virtual void Load(device_state state) = 0; + + public: + Loader(std::string &file_path_, device_state &state_) : file_path(file_path_), state(state_), file(file_path, std::ios::binary | std::ios::beg) {} + }; +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/loader/nro.cpp b/app/src/main/cpp/switch/loader/nro.cpp new file mode 100644 index 00000000..51cc75b1 --- /dev/null +++ b/app/src/main/cpp/switch/loader/nro.cpp @@ -0,0 +1,36 @@ +#include +#include "nro.h" + +namespace lightSwitch::loader { + void NroLoader::Load(device_state state) { + NroHeader header{}; + ReadOffset((uint32_t *) &header, 0x0, sizeof(NroHeader)); + if (header.magic != constant::nro_magic) + throw exception(fmt::format("Invalid NRO magic 0x{0:x}", header.magic)); + + auto text = new uint32_t[header.text.size](); + auto ro = new uint32_t[header.ro.size](); + auto data = new uint32_t[header.data.size](); + + ReadOffset(text, header.text.offset, header.text.size); + ReadOffset(ro, header.ro.offset, header.ro.size); + ReadOffset(data, header.data.offset, header.data.size); + + state.mem->Map(constant::base_addr, header.text.size, hw::Memory::text); + state.logger->write(Logger::DEBUG, "Successfully mapped region .text to 0x{0:x}, size is 0x{1:x}.", constant::base_addr, header.text.size); + state.mem->Map(constant::base_addr + header.text.size, header.ro.size, hw::Memory::rodata); + state.logger->write(Logger::DEBUG, "Successfully mapped region .ro to 0x{0:x}, size is 0x{1:x}.", constant::base_addr + header.text.size, header.ro.size); + state.mem->Map(constant::base_addr + header.text.size + header.ro.size, header.data.size, hw::Memory::data); + state.logger->write(Logger::DEBUG, "Successfully mapped region .data to 0x{0:x}, size is 0x{1:x}.", constant::base_addr + header.text.size + header.ro.size, header.data.size); + state.mem->Map(constant::base_addr + header.text.size + header.ro.size + header.data.size, header.bssSize, hw::Memory::bss); + state.logger->write(Logger::DEBUG, "Successfully mapped region .bss to 0x{0:x}, size is 0x{1:x}.", constant::base_addr + header.text.size + header.ro.size + header.data.size, header.bssSize); + + state.mem->Write(text, constant::base_addr, header.text.size); + state.mem->Write(ro, constant::base_addr + header.text.size, header.ro.size); + state.mem->Write(data, constant::base_addr + header.text.size + header.ro.size, header.data.size); + + delete[] text; + delete[] ro; + delete[] data; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/loader/nro.h b/app/src/main/cpp/switch/loader/nro.h new file mode 100644 index 00000000..6119d995 --- /dev/null +++ b/app/src/main/cpp/switch/loader/nro.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include "loader.h" + +namespace lightSwitch::loader { + class NroLoader : public Loader { + private: + struct NroSegmentHeader { + uint32_t offset; + uint32_t size; + }; + + struct NroHeader { + uint32_t : 32; + uint32_t mod_offset; + uint64_t : 64; + + uint32_t magic; + uint32_t version; + uint32_t size; + uint32_t flags; + + NroSegmentHeader text; + NroSegmentHeader ro; + NroSegmentHeader data; + + uint32_t bssSize; + uint32_t : 32; + uint64_t build_id[4]; + uint64_t : 64; + + NroSegmentHeader api_info; + NroSegmentHeader dynstr; + NroSegmentHeader dynsym; + }; + + void Load(device_state state); + + public: + NroLoader(std::string file_path, device_state state) : Loader(file_path, state) { Load(state); }; + }; +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/os/ipc.cpp b/app/src/main/cpp/switch/os/ipc.cpp new file mode 100644 index 00000000..9c5a6f90 --- /dev/null +++ b/app/src/main/cpp/switch/os/ipc.cpp @@ -0,0 +1,31 @@ +#include +#include +#include "ipc.h" + +namespace lightSwitch::os::ipc { + IpcRequest::IpcRequest(uint8_t *tls_ptr, device_state &state) : req_info((command_struct *) tls_ptr) { + state.logger->write(Logger::DEBUG, "Enable handle descriptor: {0}", (bool) req_info->handle_desc); + if (req_info->handle_desc) + throw exception("IPC - Handle descriptor"); + + // Align to 16 bytes + data_pos = 8; + data_pos = ((data_pos - 1) & ~(15U)) + 16; // ceil data_pos with multiples 16 + data_ptr = &tls_ptr[data_pos + sizeof(command_struct)]; + + state.logger->write(Logger::DEBUG, "Type: 0x{:x}", (uint8_t) req_info->type); + state.logger->write(Logger::DEBUG, "X descriptors: {}", (uint8_t) req_info->x_no); + state.logger->write(Logger::DEBUG, "A descriptors: {}", (uint8_t) req_info->a_no); + state.logger->write(Logger::DEBUG, "B descriptors: {}", (uint8_t) req_info->b_no); + state.logger->write(Logger::DEBUG, "W descriptors: {}", (uint8_t) req_info->w_no); + state.logger->write(Logger::DEBUG, "Raw data offset: 0x{:x}", data_pos); + state.logger->write(Logger::DEBUG, "Raw data size: {}", (uint16_t) req_info->data_sz); + state.logger->write(Logger::DEBUG, "Payload Command ID: {}", *((uint32_t *) &tls_ptr[data_pos + 8])); + } + + template + T IpcRequest::GetValue() { + data_pos += sizeof(T); + return *reinterpret_cast(&data_ptr[data_pos - sizeof(T)]); + } +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/os/ipc.h b/app/src/main/cpp/switch/os/ipc.h new file mode 100644 index 00000000..7df3002a --- /dev/null +++ b/app/src/main/cpp/switch/os/ipc.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include "switch/common.h" + +namespace lightSwitch::os::ipc { + class IpcRequest { + private: + uint8_t *data_ptr; + uint32_t data_pos; + public: + struct command_struct { + // https://switchbrew.org/wiki/IPC_Marshalling#IPC_Command_Structure + uint16_t type : 16; + uint8_t x_no : 4; + uint8_t a_no : 4; + uint8_t b_no : 4; + uint8_t w_no : 4; + uint16_t data_sz : 10; + uint8_t c_flags : 4; + uint32_t : 17; + bool handle_desc : 1; + } *req_info; + + IpcRequest(uint8_t *tlsPtr, device_state& state); + + template + T GetValue(); + }; +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/os/kernel.cpp b/app/src/main/cpp/switch/os/kernel.cpp new file mode 100644 index 00000000..dbf5cd4b --- /dev/null +++ b/app/src/main/cpp/switch/os/kernel.cpp @@ -0,0 +1,12 @@ +#include "kernel.h" +#include "svc.h" + +namespace lightSwitch::os { + Kernel::Kernel(device_state state_) : state(state_) {} + + uint32_t Kernel::NewHandle(KObjectPtr obj) { + handles.insert({handle_index, obj}); + state.logger->write(Logger::DEBUG, "Creating new handle 0x{0:x}", handle_index); + return handle_index++; + } +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/os/kernel.h b/app/src/main/cpp/switch/os/kernel.h new file mode 100644 index 00000000..0c33f659 --- /dev/null +++ b/app/src/main/cpp/switch/os/kernel.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include +#include "switch/common.h" + +namespace lightSwitch::os { + class KObject { + private: + uint32_t handle; + public: + KObject(uint32_t handle) : handle(handle) {} + + uint32_t Handle() { return handle; } + }; + + typedef std::shared_ptr KObjectPtr; + + class Kernel { + private: + device_state state; + uint32_t handle_index = constant::base_handle_index; + std::unordered_map handles; + public: + Kernel(device_state state_); + + uint32_t NewHandle(KObjectPtr obj); + }; +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/os/os.cpp b/app/src/main/cpp/switch/os/os.cpp new file mode 100644 index 00000000..583b82ca --- /dev/null +++ b/app/src/main/cpp/switch/os/os.cpp @@ -0,0 +1,49 @@ + +#include +#include "os.h" + +namespace lightSwitch::os { + OS::OS(device_state state_) : state(std::move(state_)) { + uc_err err = uc_hook_add(state.cpu->GetEngine(), &hook, UC_HOOK_INTR, + (void *) HookInterrupt, &state, 1, 0); + if (err) + throw std::runtime_error("An error occurred while running 'uc_hook_add': " + + std::string(uc_strerror(err))); + } + + OS::~OS() { + uc_hook_del(state.cpu->GetEngine(), hook); + } + + void OS::HookInterrupt(uc_engine *uc, uint32_t int_no, void *user_data) { + device_state state = *((device_state *) user_data); + try { + if (int_no == 2) { + uint32_t instr{}; + uc_err err = uc_mem_read(uc, state.cpu->GetRegister(UC_ARM64_REG_PC) - 4, &instr, 4); + if (err) + throw exception("An error occurred while running 'uc_mem_read': " + std::string(uc_strerror(err))); + uint32_t svcId = instr >> 5U & 0xFF; + SvcHandler(svcId, state); + } else { + state.logger->write(Logger::ERROR, "An unhandled interrupt has occurred: {}", int_no); + exit(int_no); + } + } catch (exception &e) { + state.logger->write(Logger::WARN, "An exception occurred during an interrupt: {}", e.what()); + } catch (...) { + state.logger->write(Logger::WARN, "An unknown exception has occurred."); + } + } + + void OS::SvcHandler(uint32_t svc, device_state &state) { + if (svc::svcTable[svc]) + (*svc::svcTable[svc])(state); + else + state.logger->write(Logger::WARN, "Unimplemented SVC 0x{0:x}", svc); + } + + void OS::SvcHandler(uint32_t svc) { + SvcHandler(svc, state); + } +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/os/os.h b/app/src/main/cpp/switch/os/os.h new file mode 100644 index 00000000..7d146ed7 --- /dev/null +++ b/app/src/main/cpp/switch/os/os.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include "switch/common.h" +#include "ipc.h" +#include "kernel.h" +#include "svc.h" + +namespace lightSwitch::os { + class OS { + private: + device_state state; + uc_hook hook{}; + public: + OS(device_state state_); + + ~OS(); + + static void HookInterrupt(uc_engine *uc, uint32_t int_no, void *user_data); + + static void SvcHandler(uint32_t svc, device_state &state); + + void SvcHandler(uint32_t svc); + }; +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/os/svc.cpp b/app/src/main/cpp/switch/os/svc.cpp new file mode 100644 index 00000000..ba603994 --- /dev/null +++ b/app/src/main/cpp/switch/os/svc.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include +#include +#include "svc.h" + +namespace lightSwitch::os::svc { + void ConnectToNamedPort(device_state &state) { + char port[constant::port_size]{0}; + state.mem->Read(port, state.cpu->GetRegister(UC_ARM64_REG_X1), constant::port_size); + if (std::strcmp(port, "sm:") == 0) + state.cpu->SetRegister(UC_ARM64_REG_W1, constant::sm_handle); + else { + state.logger->write(Logger::ERROR, "svcConnectToNamedPort tried connecting to invalid port \"{0}\"", port); + state.cpu->StopExecution(); + } + state.cpu->SetRegister(UC_ARM64_REG_W0, 0); + } + + void SendSyncRequest(device_state &state) { + state.logger->write(Logger::DEBUG, "svcSendSyncRequest called for handle 0x{0:x}.", state.cpu->GetRegister(UC_ARM64_REG_X0)); + uint8_t tls[constant::tls_ipc_size]; + state.mem->Read(&tls, constant::tls_addr, constant::tls_ipc_size); + ipc::IpcRequest request(tls, state); + state.cpu->SetRegister(UC_ARM64_REG_W0, 0); + } + + void OutputDebugString(device_state &state) { + std::string debug(state.cpu->GetRegister(UC_ARM64_REG_X1), '\0'); + state.mem->Read((void *) debug.data(), state.cpu->GetRegister(UC_ARM64_REG_X0), state.cpu->GetRegister(UC_ARM64_REG_X1)); + state.logger->write(Logger::INFO, "ROM Output: {0}", debug.c_str()); + state.cpu->SetRegister(UC_ARM64_REG_W0, 0); + } + + void GetInfo(device_state &state) { + switch (state.cpu->GetRegister(UC_ARM64_REG_X1)) { + case constant::infoState::AllowedCpuIdBitmask: + case constant::infoState::AllowedThreadPriorityMask: + case constant::infoState::IsCurrentProcessBeingDebugged: + state.cpu->SetRegister(UC_ARM64_REG_X1, 0); + break; + case constant::infoState::AddressSpaceBaseAddr: + state.cpu->SetRegister(UC_ARM64_REG_X1, constant::base_addr); + break; + case constant::infoState::TitleId: + state.cpu->SetRegister(UC_ARM64_REG_X1, 0); // TODO: Complete this + break; + default: + state.logger->write(Logger::WARN, "Unimplemented GetInfo call. ID1: {0}, ID2: {1}", state.cpu->GetRegister(UC_ARM64_REG_X1), state.cpu->GetRegister(UC_ARM64_REG_X3)); + state.cpu->SetRegister(UC_ARM64_REG_X1, constant::svc_unimpl); + return; + } + state.cpu->SetRegister(UC_ARM64_REG_W0, 0); + } + + void ExitProcess(device_state &state) { + state.cpu->StopExecution(); + } +} \ No newline at end of file diff --git a/app/src/main/cpp/switch/os/svc.h b/app/src/main/cpp/switch/os/svc.h new file mode 100644 index 00000000..dac3a9b3 --- /dev/null +++ b/app/src/main/cpp/switch/os/svc.h @@ -0,0 +1,184 @@ +#pragma once + +#include "ipc.h" +#include "../common.h" + +// https://switchbrew.org/wiki/SVC + +namespace lightSwitch::constant { + constexpr uint64_t sm_handle = 0xd000; // sm: is hardcoded for now + constexpr uint32_t tls_ipc_size = 0x100; + constexpr uint8_t port_size = 0x8; + namespace infoState { + // 1.0.0+ + constexpr uint8_t AllowedCpuIdBitmask = 0x0; + constexpr uint8_t AllowedThreadPriorityMask = 0x1; + constexpr uint8_t AliasRegionBaseAddr = 0x2; + constexpr uint8_t AliasRegionSize = 0x3; + constexpr uint8_t HeapRegionBaseAddr = 0x4; + constexpr uint8_t HeapRegionSize = 0x5; + constexpr uint8_t TotalMemoryAvailable = 0x6; + constexpr uint8_t TotalMemoryUsage = 0x7; + constexpr uint8_t IsCurrentProcessBeingDebugged = 0x8; + constexpr uint8_t ResourceLimit = 0x9; + constexpr uint8_t IdleTickCount = 0xA; + constexpr uint8_t RandomEntropy = 0xB; + // 2.0.0+ + constexpr uint8_t AddressSpaceBaseAddr = 0xC; + constexpr uint8_t AddressSpaceSize = 0xD; + constexpr uint8_t StackRegionBaseAddr = 0xE; + constexpr uint8_t StackRegionSize = 0xF; + // 3.0.0+ + constexpr uint8_t PersonalMmHeapSize = 0x10; + constexpr uint8_t PersonalMmHeapUsage = 0x11; + constexpr uint8_t TitleId = 0x12; + // 5.0.0+ + constexpr uint8_t UserExceptionContextAddr = 0x14; + // 6.0.0+ + constexpr uint8_t TotalMemoryAvailableWithoutMmHeap = 0x15; + constexpr uint8_t TotalMemoryUsedWithoutMmHeap = 0x16; + }; +}; + +namespace lightSwitch::os::svc { + void ConnectToNamedPort(device_state &state); + + void SendSyncRequest(device_state &state); + + void OutputDebugString(device_state &state); + + void GetInfo(device_state &state); + + void ExitProcess(device_state &state); + + void static (*svcTable[0x80])(device_state &) = { + nullptr, // 0x00 + nullptr, // 0x01 + nullptr, // 0x02 + nullptr, // 0x03 + nullptr, // 0x04 + nullptr, // 0x05 + nullptr, // 0x06 + ExitProcess, // 0x07 + nullptr, // 0x08 + nullptr, // 0x09 + nullptr, // 0x0a + nullptr, // 0x0b + nullptr, // 0x0c + nullptr, // 0x0d + nullptr, // 0x0e + nullptr, // 0x0f + nullptr, // 0x10 + nullptr, // 0x11 + nullptr, // 0x12 + nullptr, // 0x13 + nullptr, // 0x14 + nullptr, // 0x15 + nullptr, // 0x16 + nullptr, // 0x17 + nullptr, // 0x18 + nullptr, // 0x19 + nullptr, // 0x1a + nullptr, // 0x1b + nullptr, // 0x1c + nullptr, // 0x1d + nullptr, // 0x1e + ConnectToNamedPort, // 0x1f + nullptr, // 0x20 + SendSyncRequest, // 0x21 + nullptr, // 0x22 + nullptr, // 0x23 + nullptr, // 0x24 + nullptr, // 0x25 + nullptr, // 0x26 + OutputDebugString, // 0x27 + nullptr, // 0x28 + GetInfo, // 0x29 + nullptr, // 0x2a + nullptr, // 0x2b + nullptr, // 0x2c + nullptr, // 0x2d + nullptr, // 0x2e + nullptr, // 0x2f + nullptr, // 0x30 + nullptr, // 0x31 + nullptr, // 0x32 + nullptr, // 0x33 + nullptr, // 0x34 + nullptr, // 0x35 + nullptr, // 0x36 + nullptr, // 0x37 + nullptr, // 0x38 + nullptr, // 0x39 + nullptr, // 0x3a + nullptr, // 0x3b + nullptr, // 0x3c + nullptr, // 0x3d + nullptr, // 0x3e + nullptr, // 0x3f + nullptr, // 0x40 + nullptr, // 0x41 + nullptr, // 0x42 + nullptr, // 0x43 + nullptr, // 0x44 + nullptr, // 0x45 + nullptr, // 0x46 + nullptr, // 0x47 + nullptr, // 0x48 + nullptr, // 0x49 + nullptr, // 0x4a + nullptr, // 0x4b + nullptr, // 0x4c + nullptr, // 0x4d + nullptr, // 0x4e + nullptr, // 0x4f + nullptr, // 0x50 + nullptr, // 0x51 + nullptr, // 0x52 + nullptr, // 0x53 + nullptr, // 0x54 + nullptr, // 0x55 + nullptr, // 0x56 + nullptr, // 0x57 + nullptr, // 0x58 + nullptr, // 0x59 + nullptr, // 0x5a + nullptr, // 0x5b + nullptr, // 0x5c + nullptr, // 0x5d + nullptr, // 0x5e + nullptr, // 0x5f + nullptr, // 0x60 + nullptr, // 0x61 + nullptr, // 0x62 + nullptr, // 0x63 + nullptr, // 0x64 + nullptr, // 0x65 + nullptr, // 0x66 + nullptr, // 0x67 + nullptr, // 0x68 + nullptr, // 0x69 + nullptr, // 0x6a + nullptr, // 0x6b + nullptr, // 0x6c + nullptr, // 0x6d + nullptr, // 0x6e + nullptr, // 0x6f + nullptr, // 0x70 + nullptr, // 0x71 + nullptr, // 0x72 + nullptr, // 0x73 + nullptr, // 0x74 + nullptr, // 0x75 + nullptr, // 0x76 + nullptr, // 0x77 + nullptr, // 0x78 + nullptr, // 0x79 + nullptr, // 0x7a + nullptr, // 0x7b + nullptr, // 0x7c + nullptr, // 0x7d + nullptr, // 0x7e + nullptr // 0x7f + }; +} \ No newline at end of file diff --git a/app/src/main/java/emu/lightswitch/lightswitch/GameAdapter.java b/app/src/main/java/emu/lightswitch/lightswitch/GameAdapter.java new file mode 100644 index 00000000..6725756d --- /dev/null +++ b/app/src/main/java/emu/lightswitch/lightswitch/GameAdapter.java @@ -0,0 +1,149 @@ +package emu.lightswitch.lightswitch; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; +import androidx.annotation.NonNull; + +import java.io.File; +import java.io.IOException; +import java.util.Objects; + +class GameItem extends BaseItem { + private File file; + transient private TitleEntry meta; + private int index; + + GameItem(File file) { + this.file = file; + index = file.getName().lastIndexOf("."); + meta = NroLoader.getTitleEntry(getPath()); + if (meta == null) { + meta = new TitleEntry(file.getName(), GameAdapter.mContext.getString(R.string.aset_missing), null); + } + } + + public boolean hasIcon() { + return !getSubTitle().equals(GameAdapter.mContext.getString(R.string.aset_missing)); + } + + public Bitmap getIcon() { + return meta.getIcon(); + } + + public String getTitle() { + return meta.getName() + " (" + getType() + ")"; + } + + String getSubTitle() { + return meta.getAuthor(); + } + + public String getType() { + return file.getName().substring(index + 1).toUpperCase(); + } + + public File getFile() { + return file; + } + + public String getPath() { + return file.getAbsolutePath(); + } + + @Override + String key() { + if (meta.getIcon() == null) + return meta.getName(); + return meta.getName() + " " + meta.getAuthor(); + } +} + +public class GameAdapter extends HeaderAdapter implements View.OnClickListener { + + GameAdapter(Context context) { super(context); } + + @Override + public void load(File file) throws IOException, ClassNotFoundException { + super.load(file); + for (int i = 0; i < item_array.size(); i++) + item_array.set(i, new GameItem(item_array.get(i).getFile())); + notifyDataSetChanged(); + } + + @Override + public void onClick(View view) { + int position = (int) view.getTag(); + if (getItemViewType(position) == ContentType.Item) { + GameItem item = (GameItem) getItem(position); + if (view.getId() == R.id.icon) { + Dialog builder = new Dialog(mContext); + builder.requestWindowFeature(Window.FEATURE_NO_TITLE); + Objects.requireNonNull(builder.getWindow()).setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + ImageView imageView = new ImageView(mContext); + assert item != null; + imageView.setImageBitmap(item.getIcon()); + builder.addContentView(imageView, new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + builder.show(); + } + } + } + + @NonNull + @Override + public View getView(int position, View convertView, @NonNull ViewGroup parent) { + ViewHolder viewHolder; + int type = type_array.get(position).type; + if (convertView == null) { + if (type == ContentType.Item) { + viewHolder = new ViewHolder(); + LayoutInflater inflater = LayoutInflater.from(mContext); + convertView = inflater.inflate(R.layout.game_item, parent, false); + viewHolder.icon = convertView.findViewById(R.id.icon); + viewHolder.txtTitle = convertView.findViewById(R.id.text_title); + viewHolder.txtSub = convertView.findViewById(R.id.text_subtitle); + convertView.setTag(viewHolder); + } else { + viewHolder = new ViewHolder(); + LayoutInflater inflater = LayoutInflater.from(mContext); + convertView = inflater.inflate(R.layout.section_item, parent, false); + viewHolder.txtTitle = convertView.findViewById(R.id.text_title); + convertView.setTag(viewHolder); + } + } else { + viewHolder = (ViewHolder) convertView.getTag(); + } + if (type == ContentType.Item) { + GameItem data = (GameItem) getItem(position); + viewHolder.txtTitle.setText(data.getTitle()); + viewHolder.txtSub.setText(data.getSubTitle()); + Bitmap icon = data.getIcon(); + if (icon != null) { + viewHolder.icon.setImageBitmap(icon); + viewHolder.icon.setOnClickListener(this); + viewHolder.icon.setTag(position); + } else { + viewHolder.icon.setImageDrawable(mContext.getDrawable(R.drawable.ic_missing_icon)); + viewHolder.icon.setOnClickListener(null); + } + } else { + viewHolder.txtTitle.setText((String) getItem(position)); + } + return convertView; + } + + private static class ViewHolder { + ImageView icon; + TextView txtTitle; + TextView txtSub; + } +} diff --git a/app/src/main/java/emu/lightswitch/lightswitch/HeaderAdapter.java b/app/src/main/java/emu/lightswitch/lightswitch/HeaderAdapter.java new file mode 100644 index 00000000..35e0d169 --- /dev/null +++ b/app/src/main/java/emu/lightswitch/lightswitch/HeaderAdapter.java @@ -0,0 +1,189 @@ +package emu.lightswitch.lightswitch; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.SparseIntArray; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Filter; +import android.widget.Filterable; +import androidx.annotation.NonNull; +import me.xdrop.fuzzywuzzy.FuzzySearch; +import me.xdrop.fuzzywuzzy.model.ExtractedResult; + +import java.io.*; +import java.util.ArrayList; + +class ContentType implements Serializable { + transient static final int Header = 0; + transient static final int Item = 1; + public final int type; + public int index; + + ContentType(int index, int type) { + this(type); + this.index = index; + } + + private ContentType(int type) { + switch (type) { + case Item: + case Header: + break; + default: + throw (new IllegalArgumentException()); + } + this.type = type; + } +} + +abstract class BaseItem implements Serializable { + abstract String key(); +} + +abstract class HeaderAdapter extends BaseAdapter implements Filterable, Serializable { + @SuppressLint("StaticFieldLeak") + static Context mContext; + ArrayList type_array; + ArrayList item_array; + private ArrayList type_array_uf; + private ArrayList header_array; + private String search_term = ""; + + HeaderAdapter(Context context) { + mContext = context; + this.item_array = new ArrayList<>(); + this.header_array = new ArrayList<>(); + this.type_array_uf = new ArrayList<>(); + this.type_array = new ArrayList<>(); + } + + public void add(Object item, int type) { + if (type == ContentType.Item) { + item_array.add((ItemType) item); + type_array_uf.add(new ContentType(item_array.size() - 1, ContentType.Item)); + } else { + header_array.add((String) item); + type_array_uf.add(new ContentType(header_array.size() - 1, ContentType.Header)); + } + if(search_term.length()!=0) + this.getFilter().filter(search_term); + else + type_array=type_array_uf; + } + + public void save(File file) throws IOException { + State state = new State<>(item_array, header_array, type_array_uf); + FileOutputStream file_obj = new FileOutputStream(file); + ObjectOutputStream out = new ObjectOutputStream(file_obj); + out.writeObject(state); + out.close(); + file_obj.close(); + } + + void load(File file) throws IOException, ClassNotFoundException { + FileInputStream file_obj = new FileInputStream(file); + ObjectInputStream in = new ObjectInputStream(file_obj); + State state = (State) in.readObject(); + in.close(); + file_obj.close(); + if (state != null) { + this.item_array = state.item_array; + this.header_array = state.header_array; + this.type_array_uf = state.type_array; + this.getFilter().filter(search_term); + } + } + + public void clear() { + item_array.clear(); + header_array.clear(); + type_array_uf.clear(); + type_array.clear(); + notifyDataSetChanged(); + } + + @Override + public int getCount() { + return type_array.size(); + } + + @Override + public Object getItem(int i) { + ContentType type = type_array.get(i); + if (type.type == ContentType.Item) + return item_array.get(type.index); + else + return header_array.get(type.index); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getItemViewType(int position) { + return type_array.get(position).type; + } + + @Override + public int getViewTypeCount() { + return 2; + } + + @NonNull + @Override + public abstract View getView(int position, View convertView, @NonNull ViewGroup parent); + + @Override + public Filter getFilter() { + return new Filter() { + @Override + protected FilterResults performFiltering(CharSequence charSequence) { + FilterResults results = new FilterResults(); + search_term = ((String) charSequence).toLowerCase().replaceAll(" ", ""); + if (charSequence.length() == 0) { + results.values = type_array_uf; + results.count = type_array_uf.size(); + } else { + ArrayList filter_data = new ArrayList<>(); + ArrayList key_arr = new ArrayList<>(); + SparseIntArray key_ind = new SparseIntArray(); + for (int index = 0; index < type_array_uf.size(); index++) { + ContentType item = type_array_uf.get(index); + if (item.type == ContentType.Item) { + key_arr.add(item_array.get(item.index).key().toLowerCase()); + key_ind.append(key_arr.size() - 1, index); + } + } + for (ExtractedResult result : FuzzySearch.extractTop(search_term, key_arr, Math.max(1, 10 - search_term.length()))) + if (result.getScore() >= 35) + filter_data.add(type_array_uf.get(key_ind.get(result.getIndex()))); + results.values = filter_data; + results.count = filter_data.size(); + } + return results; + } + + @Override + protected void publishResults(CharSequence charSequence, FilterResults filterResults) { + type_array = (ArrayList) filterResults.values; + notifyDataSetChanged(); + } + }; + } + + class State implements Serializable { + private ArrayList item_array; + private ArrayList header_array; + private ArrayList type_array; + + State(ArrayList item_array, ArrayList header_array, ArrayList type_array) { + this.item_array = item_array; + this.header_array = header_array; + this.type_array = type_array; + } + } +} diff --git a/app/src/main/java/emu/lightswitch/lightswitch/LogActivity.java b/app/src/main/java/emu/lightswitch/lightswitch/LogActivity.java new file mode 100644 index 00000000..b192e52a --- /dev/null +++ b/app/src/main/java/emu/lightswitch/lightswitch/LogActivity.java @@ -0,0 +1,141 @@ +package emu.lightswitch.lightswitch; + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.os.FileObserver; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.ListView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.SearchView; +import androidx.appcompat.widget.Toolbar; +import androidx.preference.PreferenceManager; + +import java.io.*; + +import static java.lang.Thread.interrupted; + +public class LogActivity extends AppCompatActivity { + File log_file; + BufferedReader reader; + Thread thread; + LogAdapter adapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.log_activity); + setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) + actionBar.setDisplayHomeAsUpEnabled(true); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + final ListView log_list = this.findViewById(R.id.log_list); + adapter = new LogAdapter(this, Integer.parseInt(prefs.getString("log_level", "3")), getResources().getStringArray(R.array.log_level)); + log_list.setAdapter(adapter); + log_file = new File(getApplicationInfo().dataDir + "/log.bin"); + try { + InputStream inputStream = new FileInputStream(log_file); + reader = new BufferedReader(new InputStreamReader(inputStream)); + } catch (FileNotFoundException e) { + Log.w("Logger", "IO Error during access of log file: " + e.getMessage()); + Toast.makeText(getApplicationContext(), getString(R.string.file_missing), Toast.LENGTH_LONG).show(); + finish(); + } + thread = new Thread(new Runnable() { + @Override + public void run() { + @SuppressWarnings("deprecation") // Required as FileObserver(File) is only on API level 29 also no AndroidX version present + FileObserver observer = new FileObserver(log_file.getPath()) { + @Override + public void onEvent(int event, String path) { + if (event == FileObserver.MODIFY) { + try { + boolean done = false; + while (!done) { + final String line = reader.readLine(); + done = (line == null); + if (!done) { + runOnUiThread(new Runnable() { + @Override + public void run() { + adapter.add(line); + } + }); + } + } + } catch (IOException e) { + Log.w("Logger", "IO Error during access of log file: " + e.getMessage()); + Toast.makeText(getApplicationContext(), getString(R.string.io_error) + ": " + e.getMessage(), Toast.LENGTH_LONG).show(); + } + } + } + }; + observer.onEvent(FileObserver.MODIFY, log_file.getPath()); + observer.startWatching(); + while (!interrupted()) ; + observer.stopWatching(); + } + }); + thread.start(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.toolbar_log, menu); + MenuItem mSearch = menu.findItem(R.id.action_search_log); + final SearchView searchView = (SearchView) mSearch.getActionView(); + searchView.setSubmitButtonEnabled(false); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + public boolean onQueryTextSubmit(String query) { + searchView.setIconified(false); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + adapter.getFilter().filter(newText); + return true; + } + }); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case R.id.action_clear: + try { + FileWriter file = new FileWriter(log_file, false); + file.close(); + } catch (IOException e) { + Log.w("Logger", "IO Error while clearing the log file: " + e.getMessage()); + Toast.makeText(getApplicationContext(), getString(R.string.io_error) + ": " + e.getMessage(), Toast.LENGTH_LONG).show(); + } + Toast.makeText(getApplicationContext(), getString(R.string.cleared), Toast.LENGTH_LONG).show(); + finish(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + @Override + protected void onDestroy() { + super.onDestroy(); + try { + thread.interrupt(); + thread.join(); + reader.close(); + } catch (IOException e) { + Log.w("Logger", "IO Error during closing BufferedReader: " + e.getMessage()); + Toast.makeText(getApplicationContext(), getString(R.string.io_error) + ": " + e.getMessage(), Toast.LENGTH_LONG).show(); + } catch (NullPointerException ignored) { + } catch (InterruptedException ignored) { + } + } +} diff --git a/app/src/main/java/emu/lightswitch/lightswitch/LogAdapter.java b/app/src/main/java/emu/lightswitch/lightswitch/LogAdapter.java new file mode 100644 index 00000000..f85cefbb --- /dev/null +++ b/app/src/main/java/emu/lightswitch/lightswitch/LogAdapter.java @@ -0,0 +1,108 @@ +package emu.lightswitch.lightswitch; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; +import androidx.annotation.NonNull; +import com.google.android.material.snackbar.Snackbar; + +class LogItem extends BaseItem { + private String content; + private String level; + + LogItem(String content, String level) { + this.content = content; + this.level = level; + } + + public String getLevel() { + return level; + } + + public String getMessage() { + return content; + } + + @Override + String key() { + return getMessage(); + } +} + +public class LogAdapter extends HeaderAdapter implements View.OnLongClickListener { + private ClipboardManager clipboard; + private int debug_level; + private String[] level_str; + + LogAdapter(Context context, int debug_level, String[] level_str) { + super(context); + clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + this.debug_level = debug_level; + this.level_str = level_str; + } + + void add(final String log_line) { + String[] log_meta = log_line.split("\\|", 3); + if (log_meta[0].startsWith("1")) { + int level = Integer.parseInt(log_meta[1]); + if (level > this.debug_level) return; + super.add(new LogItem(log_meta[2], level_str[level]), ContentType.Item); + } else { + super.add(log_meta[1], ContentType.Header); + } + } + + @Override + public boolean onLongClick(View view) { + LogItem item = (LogItem) getItem(((ViewHolder) view.getTag()).position); + clipboard.setPrimaryClip(ClipData.newPlainText("Log Message", item.getLevel() + ": " + item.getMessage())); + Toast.makeText(view.getContext(), "Copied to clipboard", Snackbar.LENGTH_LONG).show(); + return false; + } + + @NonNull + @Override + public View getView(int position, View convertView, @NonNull ViewGroup parent) { + ViewHolder viewHolder; + int type = type_array.get(position).type; + if (convertView == null) { + if (type == ContentType.Item) { + viewHolder = new ViewHolder(); + LayoutInflater inflater = LayoutInflater.from(mContext); + convertView = inflater.inflate(R.layout.log_item, parent, false); + convertView.setOnLongClickListener(this); + viewHolder.txtTitle = convertView.findViewById(R.id.text_title); + viewHolder.txtSub = convertView.findViewById(R.id.text_subtitle); + convertView.setTag(viewHolder); + } else { + viewHolder = new ViewHolder(); + LayoutInflater inflater = LayoutInflater.from(mContext); + convertView = inflater.inflate(R.layout.section_item, parent, false); + viewHolder.txtTitle = convertView.findViewById(R.id.text_title); + convertView.setTag(viewHolder); + } + } else { + viewHolder = (ViewHolder) convertView.getTag(); + } + if (type == ContentType.Item) { + LogItem data = (LogItem) getItem(position); + viewHolder.txtTitle.setText(data.getMessage()); + viewHolder.txtSub.setText(data.getLevel()); + } else { + viewHolder.txtTitle.setText((String) getItem(position)); + } + viewHolder.position = position; + return convertView; + } + + private static class ViewHolder { + TextView txtTitle; + TextView txtSub; + int position; + } +} diff --git a/app/src/main/java/gq/cyuubi/lightswitch/MainActivity.java b/app/src/main/java/emu/lightswitch/lightswitch/MainActivity.java similarity index 53% rename from app/src/main/java/gq/cyuubi/lightswitch/MainActivity.java rename to app/src/main/java/emu/lightswitch/lightswitch/MainActivity.java index 57dfbf4f..6885f137 100644 --- a/app/src/main/java/gq/cyuubi/lightswitch/MainActivity.java +++ b/app/src/main/java/emu/lightswitch/lightswitch/MainActivity.java @@ -1,47 +1,48 @@ -package gq.cyuubi.lightswitch; +package emu.lightswitch.lightswitch; import android.Manifest; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Bundle; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.ListView; - import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.SearchView; import androidx.appcompat.widget.Toolbar; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.preference.PreferenceManager; - +import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.snackbar.Snackbar; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Objects; -public class MainActivity extends AppCompatActivity { +public class MainActivity extends AppCompatActivity implements View.OnClickListener { static { System.loadLibrary("lightswitch"); } SharedPreferences sharedPreferences; - FileAdapter adapter; + GameAdapter adapter; private void notifyUser(String text) { Snackbar.make(findViewById(android.R.id.content), text, Snackbar.LENGTH_SHORT).show(); - // Toast.makeText(getApplicationContext(), text, Toast.LENGTH_SHORT).show(); } private List findFile(String ext, File file, @Nullable List files) { - if (files == null) { + if (files == null) files = new ArrayList<>(); - } File[] list = file.listFiles(); if (list != null) { for (File file_i : list) { @@ -51,11 +52,12 @@ public class MainActivity extends AppCompatActivity { try { String file_str = file_i.getName(); if (ext.equalsIgnoreCase(file_str.substring(file_str.lastIndexOf(".") + 1))) { - if(NroMeta.verifyFile(file_i.getAbsolutePath())) { + if (NroLoader.verifyFile(file_i.getAbsolutePath())) { files.add(file_i); } } } catch (StringIndexOutOfBoundsException e) { + Log.w("findFile", Objects.requireNonNull(e.getMessage())); } } } @@ -63,11 +65,28 @@ public class MainActivity extends AppCompatActivity { return files; } - private void refresh_files() { + private void RefreshFiles(boolean try_load) { + if (try_load) { + try { + adapter.load(new File(getApplicationInfo().dataDir + "/roms.bin")); + return; + } catch (Exception e) { + Log.w("refreshFiles", "Ran into exception while loading: " + Objects.requireNonNull(e.getMessage())); + } + } adapter.clear(); List files = findFile("nro", new File(sharedPreferences.getString("search_location", "")), null); - for (File file : files) { - adapter.add(new GameItem(file, getApplicationContext())); + if (!files.isEmpty()) { + adapter.add(getString(R.string.nro), ContentType.Header); + for (File file : files) + adapter.add(new GameItem(file), ContentType.Item); + } else { + adapter.add(getString(R.string.no_rom), ContentType.Header); + } + try { + adapter.save(new File(getApplicationInfo().dataDir + "/roms.bin")); + } catch (IOException e) { + Log.w("refreshFiles", "Ran into exception while saving: " + Objects.requireNonNull(e.getMessage())); } } @@ -76,32 +95,55 @@ public class MainActivity extends AppCompatActivity { super.onCreate(savedInstanceState); if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1); - if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) { + if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_DENIED) System.exit(0); - } } setContentView(R.layout.main_activity); - setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); PreferenceManager.setDefaultValues(this, R.xml.preferences, false); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); - adapter = new FileAdapter(this, new ArrayList()); + setSupportActionBar((Toolbar) findViewById(R.id.toolbar)); + FloatingActionButton log_fab = findViewById(R.id.log_fab); + log_fab.setOnClickListener(this); + adapter = new GameAdapter(this); ListView game_list = findViewById(R.id.game_list); game_list.setAdapter(adapter); game_list.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView parent, View view, int position, long id) { - String path = ((GameItem) parent.getItemAtPosition(position)).getPath(); - notifyUser(getString(R.string.launch_string) + " " + path); - loadFile(path); + if (adapter.getItemViewType(position) == ContentType.Item) { + String path = ((GameItem) parent.getItemAtPosition(position)).getPath(); + notifyUser(getString(R.string.launching) + " " + path); + loadFile(path, getApplicationInfo().dataDir + "/shared_prefs/" + getApplicationInfo().packageName + "_preferences.xml", getApplicationInfo().dataDir + "/log.bin"); + } } }); - refresh_files(); + RefreshFiles(true); } @Override public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.toolbar, menu); - return true; + getMenuInflater().inflate(R.menu.toolbar_main, menu); + MenuItem mSearch = menu.findItem(R.id.action_search_main); + final SearchView searchView = (SearchView) mSearch.getActionView(); + searchView.setSubmitButtonEnabled(false); + searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + public boolean onQueryTextSubmit(String query) { + searchView.clearFocus(); + return false; + } + + @Override + public boolean onQueryTextChange(String newText) { + adapter.getFilter().filter(newText); + return true; + } + }); + return super.onCreateOptionsMenu(menu); + } + + public void onClick(View view) { + if (view.getId() == R.id.log_fab) + startActivity(new Intent(this, LogActivity.class)); } @Override @@ -111,8 +153,8 @@ public class MainActivity extends AppCompatActivity { startActivity(new Intent(this, SettingsActivity.class)); return true; case R.id.action_refresh: - notifyUser(getString(R.string.refresh_string)); - refresh_files(); + RefreshFiles(false); + notifyUser(getString(R.string.refreshed)); return true; default: return super.onOptionsItemSelected(item); @@ -120,5 +162,5 @@ public class MainActivity extends AppCompatActivity { } - public native void loadFile(String file); + public native void loadFile(String rom_path, String preference_path, String log_path); } diff --git a/app/src/main/java/gq/cyuubi/lightswitch/NroMeta.java b/app/src/main/java/emu/lightswitch/lightswitch/NroLoader.java similarity index 96% rename from app/src/main/java/gq/cyuubi/lightswitch/NroMeta.java rename to app/src/main/java/emu/lightswitch/lightswitch/NroLoader.java index 0f959f45..76928472 100644 --- a/app/src/main/java/gq/cyuubi/lightswitch/NroMeta.java +++ b/app/src/main/java/emu/lightswitch/lightswitch/NroLoader.java @@ -1,4 +1,4 @@ -package gq.cyuubi.lightswitch; +package emu.lightswitch.lightswitch; import android.graphics.Bitmap; import android.graphics.BitmapFactory; @@ -31,7 +31,7 @@ final class TitleEntry { } } -public class NroMeta { +public class NroLoader { public static TitleEntry getTitleEntry(String file) { try { RandomAccessFile f = new RandomAccessFile(file, "r"); @@ -70,7 +70,8 @@ public class NroMeta { return null; } } - public static boolean verifyFile(String file) { + + static boolean verifyFile(String file) { try { RandomAccessFile f = new RandomAccessFile(file, "r"); f.seek(0x10); // Skip to NroHeader.magic diff --git a/app/src/main/java/gq/cyuubi/lightswitch/SettingsActivity.java b/app/src/main/java/emu/lightswitch/lightswitch/SettingsActivity.java similarity index 96% rename from app/src/main/java/gq/cyuubi/lightswitch/SettingsActivity.java rename to app/src/main/java/emu/lightswitch/lightswitch/SettingsActivity.java index 73472b0e..12313e6e 100644 --- a/app/src/main/java/gq/cyuubi/lightswitch/SettingsActivity.java +++ b/app/src/main/java/emu/lightswitch/lightswitch/SettingsActivity.java @@ -1,7 +1,6 @@ -package gq.cyuubi.lightswitch; +package emu.lightswitch.lightswitch; import android.os.Bundle; - import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; diff --git a/app/src/main/java/gq/cyuubi/lightswitch/FileAdapter.java b/app/src/main/java/gq/cyuubi/lightswitch/FileAdapter.java deleted file mode 100644 index 0eb78811..00000000 --- a/app/src/main/java/gq/cyuubi/lightswitch/FileAdapter.java +++ /dev/null @@ -1,114 +0,0 @@ -package gq.cyuubi.lightswitch; - -import android.app.Dialog; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.drawable.ColorDrawable; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.Window; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.RelativeLayout; -import android.widget.TextView; - -import androidx.annotation.NonNull; - -import java.io.File; -import java.util.ArrayList; - -class GameItem { - File file; - TitleEntry meta; - - int index; - - public GameItem(File file, Context ctx) { - this.file = file; - index = file.getName().lastIndexOf("."); - meta = NroMeta.getTitleEntry(getPath()); - if(meta==null) { - meta = new TitleEntry(file.getName(), ctx.getString(R.string.aset_missing), null); - } - } - - public Bitmap getIcon() { - return meta.getIcon(); - } - - public String getTitle() { - return meta.getName() + " (" + getType() + ")"; - } - - public String getSubTitle() { - return meta.getAuthor(); - } - - public String getType() { - return file.getName().substring(index + 1).toUpperCase(); - } - - public String getPath() { - return file.getAbsolutePath(); - } -} - -public class FileAdapter extends ArrayAdapter implements View.OnClickListener { - Context mContext; - - public FileAdapter(Context context, @NonNull ArrayList data) { - super(context, R.layout.file_item, data); - this.mContext = context; - } - - @Override - public void onClick(View v) { - int position = (Integer) v.getTag(); - GameItem dataModel = getItem(position); - switch (v.getId()) { - case R.id.icon: - Dialog builder = new Dialog(mContext); - builder.requestWindowFeature(Window.FEATURE_NO_TITLE); - builder.getWindow().setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT)); - ImageView imageView = new ImageView(mContext); - imageView.setImageBitmap(dataModel.getIcon()); - builder.addContentView(imageView, new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - builder.show(); - break; - } - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - GameItem dataModel = getItem(position); - ViewHolder viewHolder; - if (convertView == null) { - viewHolder = new ViewHolder(); - LayoutInflater inflater = LayoutInflater.from(getContext()); - convertView = inflater.inflate(R.layout.file_item, parent, false); - viewHolder.icon = convertView.findViewById(R.id.icon); - viewHolder.txtTitle = convertView.findViewById(R.id.text_title); - viewHolder.txtSub = convertView.findViewById(R.id.text_subtitle); - convertView.setTag(viewHolder); - } else { - viewHolder = (ViewHolder) convertView.getTag(); - } - viewHolder.txtTitle.setText(dataModel.getTitle()); - viewHolder.txtSub.setText(dataModel.getSubTitle()); - Bitmap icon = dataModel.getIcon(); - if(icon!=null) { - viewHolder.icon.setImageBitmap(icon); - viewHolder.icon.setOnClickListener(this); - viewHolder.icon.setTag(position); - } - return convertView; - } - - private static class ViewHolder { - ImageView icon; - TextView txtTitle; - TextView txtSub; - } -} diff --git a/app/src/main/res/drawable/ic_clear.xml b/app/src/main/res/drawable/ic_clear.xml new file mode 100644 index 00000000..ea06934b --- /dev/null +++ b/app/src/main/res/drawable/ic_clear.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_console.xml b/app/src/main/res/drawable/ic_log.xml similarity index 100% rename from app/src/main/res/drawable/ic_console.xml rename to app/src/main/res/drawable/ic_log.xml diff --git a/app/src/main/res/drawable/ic_search.xml b/app/src/main/res/drawable/ic_search.xml new file mode 100644 index 00000000..90609629 --- /dev/null +++ b/app/src/main/res/drawable/ic_search.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/file_item.xml b/app/src/main/res/layout/game_item.xml similarity index 100% rename from app/src/main/res/layout/file_item.xml rename to app/src/main/res/layout/game_item.xml diff --git a/app/src/main/res/layout/log_activity.xml b/app/src/main/res/layout/log_activity.xml new file mode 100644 index 00000000..a993dd34 --- /dev/null +++ b/app/src/main/res/layout/log_activity.xml @@ -0,0 +1,29 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/log_item.xml b/app/src/main/res/layout/log_item.xml new file mode 100644 index 00000000..33bf1c19 --- /dev/null +++ b/app/src/main/res/layout/log_item.xml @@ -0,0 +1,26 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml index 92e727de..554ca32a 100644 --- a/app/src/main/res/layout/main_activity.xml +++ b/app/src/main/res/layout/main_activity.xml @@ -8,21 +8,32 @@ + app:layout_constraintTop_toTopOf="parent"/> + + \ No newline at end of file diff --git a/app/src/main/res/layout/section_item.xml b/app/src/main/res/layout/section_item.xml new file mode 100644 index 00000000..38302f89 --- /dev/null +++ b/app/src/main/res/layout/section_item.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/toolbar_log.xml b/app/src/main/res/menu/toolbar_log.xml new file mode 100644 index 00000000..399f2edc --- /dev/null +++ b/app/src/main/res/menu/toolbar_log.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/toolbar.xml b/app/src/main/res/menu/toolbar_main.xml similarity index 66% rename from app/src/main/res/menu/toolbar.xml rename to app/src/main/res/menu/toolbar_main.xml index acef1249..f77e5116 100644 --- a/app/src/main/res/menu/toolbar.xml +++ b/app/src/main/res/menu/toolbar_main.xml @@ -1,6 +1,12 @@ + S=ZQC{_3 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 q%ei)Dh5=YkP8LpC5d00=<< literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png deleted file mode 100644 index 3a093031a04d9badbcb505348b5054c8855b1e18..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9872 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 z 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 80379f947cf296aba0d1a9285bb6ca8df99a6d5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17391 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 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 0000000000000000000000000000000000000000..a8d119578f1d111427a1a869a16d5d8854c65d2a GIT binary patch literal 9524 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 aZU3 + + Error + Warn + Info + Debug + + + 0 + 1 + 2 + 3 + System Language English - sys - en - + sys + en + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 955130d0..beb6306e 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -3,4 +3,4 @@ #E60012 #AB0000 #E60012 - + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d658c887..27db1002 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,16 +1,29 @@ Lightswitch - + + Search + Settings + Logger Refresh - The list of ROMs has been refreshed. - Launching + The list of ROMs has been refreshed. + Launching ASET Header Missing Icon + Cannot find any ROMs + NROs + NSOs - Search Search Location + Logging + Log Level Localization Language + + Clear + + The log file was not found + An I/O error has occurred + The logs have been cleared diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 17a1fcd7..d5283e39 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -6,5 +6,12 @@ @color/colorPrimaryDark @color/colorAccent - + diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 50f306d6..12f31d90 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -25,6 +25,17 @@ app:title="@string/search_location" app:useSimpleSummaryProvider="true" /> + + + diff --git a/build.gradle b/build.gradle index 78fbc17a..f44f379e 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.4.1' + classpath 'com.android.tools.build:gradle:3.4.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -18,7 +18,6 @@ allprojects { repositories { google() jcenter() - } } diff --git a/gradle.properties b/gradle.properties index e92e8a7f..c85e7d8d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,15 +6,16 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m +org.gradle.jvmargs=-Xmx2g # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects org.gradle.parallel=true +# Enable Gradle Daemon +org.gradle.daemon=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app's APK # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true - +android.enableJetifier=true \ No newline at end of file