Rewrite C++ parts and UI update
This update took way way too long to create. However, it was worthwhile. :)
6
.gitmodules
vendored
Normal file
@ -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
|
34
.idea/codeStyles/Project.xml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<option name="RIGHT_MARGIN" value="400" />
|
||||||
|
<option name="WRAP_WHEN_TYPING_REACHES_RIGHT_MARGIN" value="true" />
|
||||||
|
<option name="SOFT_MARGINS" value="80,140" />
|
||||||
|
<Objective-C>
|
||||||
|
<option name="INDENT_DIRECTIVE_AS_CODE" value="true" />
|
||||||
|
<option name="KEEP_STRUCTURES_IN_ONE_LINE" value="true" />
|
||||||
|
<option name="FUNCTION_PARAMETERS_WRAP" value="0" />
|
||||||
|
<option name="FUNCTION_CALL_ARGUMENTS_WRAP" value="0" />
|
||||||
|
<option name="CLASS_CONSTRUCTOR_INIT_LIST_WRAP" value="0" />
|
||||||
|
</Objective-C>
|
||||||
|
<Objective-C-extensions>
|
||||||
|
<extensions>
|
||||||
|
<pair source="cpp" header="h" fileNamingConvention="SNAKE_CASE" />
|
||||||
|
<pair source="c" header="h" fileNamingConvention="SNAKE_CASE" />
|
||||||
|
</extensions>
|
||||||
|
</Objective-C-extensions>
|
||||||
|
<XML>
|
||||||
|
<option name="XML_ATTRIBUTE_WRAP" value="0" />
|
||||||
|
</XML>
|
||||||
|
<editorconfig>
|
||||||
|
<option name="ENABLED" value="false" />
|
||||||
|
</editorconfig>
|
||||||
|
<codeStyleSettings language="JAVA">
|
||||||
|
<option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" />
|
||||||
|
<option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
|
||||||
|
<option name="KEEP_SIMPLE_LAMBDAS_IN_ONE_LINE" value="true" />
|
||||||
|
<option name="KEEP_SIMPLE_CLASSES_IN_ONE_LINE" value="true" />
|
||||||
|
<option name="KEEP_MULTIPLE_EXPRESSIONS_IN_ONE_LINE" value="true" />
|
||||||
|
<option name="WRAP_LONG_LINES" value="true" />
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
4
.idea/discord.xml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DiscordIntegrationProjectSettings" description="Lightswitch is an experimental Nintendo Switch emulator for Android phones." />
|
||||||
|
</project>
|
6
.idea/inspectionProfiles/Project_Default.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="SSBasedInspection" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
</profile>
|
||||||
|
</component>
|
@ -1,5 +1,40 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="NullableNotNullManager">
|
||||||
|
<option name="myDefaultNullable" value="org.jetbrains.annotations.Nullable" />
|
||||||
|
<option name="myDefaultNotNull" value="androidx.annotation.NonNull" />
|
||||||
|
<option name="myNullables">
|
||||||
|
<value>
|
||||||
|
<list size="10">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
|
||||||
|
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
|
||||||
|
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
|
||||||
|
<item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
|
||||||
|
<item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
|
||||||
|
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
|
||||||
|
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
|
||||||
|
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
|
||||||
|
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
|
||||||
|
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="myNotNulls">
|
||||||
|
<value>
|
||||||
|
<list size="9">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
|
||||||
|
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
|
||||||
|
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
|
||||||
|
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
|
||||||
|
<item index="4" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
|
||||||
|
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
|
||||||
|
<item index="6" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
|
||||||
|
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
|
||||||
|
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
</component>
|
</component>
|
||||||
|
@ -2,5 +2,7 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/app/libraries/fmt" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/app/libraries/tinyxml2" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
@ -1,20 +1,28 @@
|
|||||||
cmake_minimum_required (VERSION 3.2)
|
cmake_minimum_required(VERSION 3.8)
|
||||||
project(Lightswitch VERSION 1 LANGUAGES CXX)
|
project(Lightswitch VERSION 1 LANGUAGES CXX)
|
||||||
set_property(GLOBAL PROPERTY CMAKE_CXX_STANDARD 17 PROPERTY CMAKE_CXX_STANDARD_REQUIRED TRUE)
|
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)
|
set(source_DIR ${CMAKE_SOURCE_DIR}/src/main/cpp)
|
||||||
|
|
||||||
include_directories(${source_DIR}/include)
|
include_directories(libraries/unicorn/include)
|
||||||
include_directories(${source_DIR})
|
include_directories(${source_DIR})
|
||||||
|
|
||||||
add_library(lightswitch SHARED
|
add_library(lightswitch SHARED
|
||||||
${source_DIR}/lightswitch.cpp
|
${source_DIR}/lightswitch.cpp
|
||||||
${source_DIR}/core/arm/cpu.cpp
|
${source_DIR}/switch/os/os.cpp
|
||||||
${source_DIR}/core/arm/memory.cpp
|
${source_DIR}/switch/os/ipc.cpp
|
||||||
|
${source_DIR}/switch/os/kernel.cpp
|
||||||
${source_DIR}/core/hos/kernel/ipc.cpp
|
${source_DIR}/switch/os/svc.cpp
|
||||||
${source_DIR}/core/hos/kernel/kernel.cpp
|
${source_DIR}/switch/hw/cpu.cpp
|
||||||
${source_DIR}/core/hos/kernel/svc.cpp
|
${source_DIR}/switch/hw/memory.cpp
|
||||||
${source_DIR}/core/hos/loaders/nro.cpp
|
${source_DIR}/switch/common.cpp
|
||||||
|
${source_DIR}/switch/loader/nro.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(lightswitch ${source_DIR}/lib/${ANDROID_ABI}/libunicorn.a)
|
target_link_libraries(lightswitch ${CMAKE_SOURCE_DIR}/libraries/unicorn/libunicorn.a fmt tinyxml2)
|
||||||
|
target_compile_options(lightswitch PRIVATE -Wno-c++17-extensions)
|
||||||
|
@ -4,28 +4,28 @@ android {
|
|||||||
compileSdkVersion 29
|
compileSdkVersion 29
|
||||||
buildToolsVersion "29.0.0"
|
buildToolsVersion "29.0.0"
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId "gq.cyuubi.lightswitch"
|
applicationId "emu.lightswitch.lightswitch"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 1
|
versionCode 1
|
||||||
versionName "1.0"
|
versionName "1.0"
|
||||||
externalNativeBuild {
|
|
||||||
cmake {
|
|
||||||
cppFlags ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters "arm64-v8a"
|
abiFilters "arm64-v8a"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
|
minifyEnabled true
|
||||||
|
useProguard false
|
||||||
|
}
|
||||||
|
debug {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
useProguard false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
externalNativeBuild {
|
externalNativeBuild {
|
||||||
cmake {
|
cmake {
|
||||||
|
version "3.8.0+"
|
||||||
path "CMakeLists.txt"
|
path "CMakeLists.txt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,6 +40,7 @@ dependencies {
|
|||||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
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 'com.google.android.material:material:1.0.0'
|
||||||
|
implementation 'me.xdrop:fuzzywuzzy:1.2.0'
|
||||||
}
|
}
|
||||||
|
1
app/libraries/fmt
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 6a497e1d061993cce54c2d71506a90155a3725e6
|
1
app/libraries/tinyxml2
Submodule
@ -0,0 +1 @@
|
|||||||
|
Subproject commit 61a4c7d507322c9f494f5880d4c94b60e4ce9590
|
@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="gq.cyuubi.lightswitch">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="emu.lightswitch.lightswitch">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||||
<uses-permission android:name="android.permission.READ_INTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_INTERNAL_STORAGE"/>
|
||||||
@ -11,14 +12,22 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme"
|
||||||
|
tools:ignore="GoogleAppIndexingWarning">
|
||||||
|
<activity android:name=".LogActivity"
|
||||||
|
android:label="@string/log"
|
||||||
|
android:parentActivityName=".MainActivity">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
|
android:value="emu.lightswitch.lightswitch.MainActivity"/>
|
||||||
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".SettingsActivity"
|
android:name=".SettingsActivity"
|
||||||
android:label="@string/settings"
|
android:label="@string/settings"
|
||||||
android:parentActivityName=".MainActivity">
|
android:parentActivityName=".MainActivity">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.PARENT_ACTIVITY"
|
android:name="android.support.PARENT_ACTIVITY"
|
||||||
android:value="gq.cyuubi.lightswitch.MainActivity" />
|
android:value="emu.lightswitch.lightswitch.MainActivity"/>
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".MainActivity">
|
<activity android:name=".MainActivity">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
|
@ -1,89 +0,0 @@
|
|||||||
#include <sys/mman.h>
|
|
||||||
#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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <syslog.h>
|
|
||||||
#include <unicorn/unicorn.h>
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
#include <sys/mman.h>
|
|
||||||
#include <syslog.h>
|
|
||||||
#include <vector>
|
|
||||||
#include "memory.h"
|
|
||||||
|
|
||||||
namespace core::memory {
|
|
||||||
/*std::vector<MemoryRegion> 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<void*>(&value), offset, 1); }
|
|
||||||
void WriteU16(uint16_t value, uint64_t offset) { Write(reinterpret_cast<void*>(&value), offset, 2); }
|
|
||||||
void WriteU32(uint32_t value, uint64_t offset) { Write(reinterpret_cast<void*>(&value), offset, 4); }
|
|
||||||
void WriteU64(uint64_t value, uint64_t offset) { Write(reinterpret_cast<void*>(&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<void*>(&value), offset, 1); return value; }
|
|
||||||
uint16_t ReadU16(uint64_t offset) { uint16_t value; Read(reinterpret_cast<void*>(&value), offset, 2); return value; }
|
|
||||||
uint32_t ReadU32(uint64_t offset) { uint32_t value; Read(reinterpret_cast<void*>(&value), offset, 4); return value; }
|
|
||||||
uint64_t ReadU64(uint64_t offset) { uint64_t value; Read(reinterpret_cast<void*>(&value), offset, 8); return value; }
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <string>
|
|
||||||
#include <unicorn/unicorn.h>
|
|
||||||
|
|
||||||
#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);
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
#include <syslog.h>
|
|
||||||
#include <cstdlib>
|
|
||||||
#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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
namespace core::kernel
|
|
||||||
{
|
|
||||||
class IpcRequest
|
|
||||||
{
|
|
||||||
public:
|
|
||||||
IpcRequest(uint8_t* tlsPtr);
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
T GetValue()
|
|
||||||
{
|
|
||||||
dataPos += sizeof(T);
|
|
||||||
return *reinterpret_cast<T*>(&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() {}
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
#include <unordered_map>
|
|
||||||
#include <syslog.h>
|
|
||||||
#include "kernel.h"
|
|
||||||
|
|
||||||
namespace core::kernel
|
|
||||||
{
|
|
||||||
std::unordered_map<uint32_t, KObjectPtr> 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++;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <cstdint>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
#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<KObject> KObjectPtr;
|
|
||||||
|
|
||||||
uint32_t NewHandle(KObjectPtr obj);
|
|
||||||
}
|
|
@ -1,219 +0,0 @@
|
|||||||
#include <cstdint>
|
|
||||||
#include <string>
|
|
||||||
#include <syslog.h>
|
|
||||||
#include <utility>
|
|
||||||
#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<int, uint32_t(*)()> 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<int, uint32_t(*)()>* 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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
namespace core::kernel {
|
|
||||||
uint32_t SvcHandler(uint32_t svc);
|
|
||||||
}
|
|
@ -1,55 +0,0 @@
|
|||||||
#include <fstream>
|
|
||||||
#include <syslog.h>
|
|
||||||
#include <sys/mman.h>
|
|
||||||
#include <vector>
|
|
||||||
#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<char *>(&header), 0x0, sizeof(NroHeader));
|
|
||||||
if (header.magic != 0x304F524E)
|
|
||||||
{
|
|
||||||
syslog(LOG_ERR, "Invalid NRO magic 0x%x\n", header.magic);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<uint32_t> 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<char *>(text.data()), header.segments[0].fileOffset, header.segments[0].size);
|
|
||||||
ReadDataFromFile(filePath, reinterpret_cast<char *>(ro.data()), header.segments[1].fileOffset, header.segments[1].size);
|
|
||||||
ReadDataFromFile(filePath, reinterpret_cast<char *>(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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
@ -1,16 +1,50 @@
|
|||||||
#include <jni.h>
|
#include <jni.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <syslog.h>
|
#include <csignal>
|
||||||
#include <core/arm/cpu.h>
|
#include <thread>
|
||||||
#include <core/hos/loaders/nro.h>
|
#include <pthread.h>
|
||||||
#include <core/arm/memory.h>
|
#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<lightSwitch::Logger>(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<lightSwitch::Settings>(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"
|
extern "C"
|
||||||
JNIEXPORT void JNICALL
|
JNIEXPORT void JNICALL
|
||||||
Java_gq_cyuubi_lightswitch_MainActivity_loadFile(JNIEnv *env, jobject instance, jstring file_) {
|
Java_emu_lightswitch_lightswitch_MainActivity_loadFile(JNIEnv *env, jobject instance, jstring rom_path_,
|
||||||
const char *file = env->GetStringUTFChars(file_, 0);
|
jstring pref_path_, jstring log_path_) {
|
||||||
core::cpu::Initialize();
|
const char *rom_path = env->GetStringUTFChars(rom_path_, 0);
|
||||||
core::loader::LoadNro(file);
|
const char *pref_path = env->GetStringUTFChars(pref_path_, 0);
|
||||||
core::cpu::Run(BASE_ADDRESS);
|
const char *log_path = env->GetStringUTFChars(log_path_, 0);
|
||||||
env->ReleaseStringUTFChars(file_, file);
|
// 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);
|
||||||
}
|
}
|
81
app/src/main/cpp/switch/common.cpp
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
#include "common.h"
|
||||||
|
#include <tinyxml2.h>
|
||||||
|
#include <syslog.h>
|
||||||
|
|
||||||
|
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 *, char *>((char *) elem->FindAttribute("name")->Value(),
|
||||||
|
(char *) elem->GetText()));
|
||||||
|
break;
|
||||||
|
case 'b':
|
||||||
|
bool_map.insert(
|
||||||
|
std::pair<char *, bool>((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();
|
||||||
|
}
|
||||||
|
}
|
71
app/src/main/cpp/switch/common.h
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <fstream>
|
||||||
|
#include <syslog.h>
|
||||||
|
#include <string>
|
||||||
|
#include <sstream>
|
||||||
|
#include <memory>
|
||||||
|
#include <fmt/format.h>
|
||||||
|
#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<char *, char *, KeyCompare> string_map;
|
||||||
|
std::map<char *, bool, KeyCompare> 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<typename S, typename... Args>
|
||||||
|
void write(Logger::LogLevel level, const S &format_str, Args &&... args) {
|
||||||
|
write(level, fmt::format(format_str, args...));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct device_state {
|
||||||
|
std::shared_ptr<hw::Cpu> cpu;
|
||||||
|
std::shared_ptr<hw::Memory> mem;
|
||||||
|
std::shared_ptr<Settings> settings;
|
||||||
|
std::shared_ptr<Logger> logger;
|
||||||
|
};
|
||||||
|
//typedef std::shared_ptr<device_state_struct> device_state;
|
||||||
|
}
|
20
app/src/main/cpp/switch/constant.h
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
}
|
36
app/src/main/cpp/switch/device.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "os/os.h"
|
||||||
|
#include "loader/nro.h"
|
||||||
|
|
||||||
|
namespace lightSwitch {
|
||||||
|
class device {
|
||||||
|
private:
|
||||||
|
std::shared_ptr<hw::Cpu> cpu;
|
||||||
|
std::shared_ptr<hw::Memory> memory;
|
||||||
|
os::OS os;
|
||||||
|
device_state state;
|
||||||
|
const std::map<std::string, int> ext_case = {
|
||||||
|
{"nro", 1},
|
||||||
|
{"NRO", 1}
|
||||||
|
};
|
||||||
|
public:
|
||||||
|
device(std::shared_ptr<Logger> &logger, std::shared_ptr<Settings> &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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
46
app/src/main/cpp/switch/hw/cpu.cpp
Normal file
@ -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<uint64_t>::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;
|
||||||
|
}
|
||||||
|
}
|
29
app/src/main/cpp/switch/hw/cpu.h
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <syslog.h>
|
||||||
|
#include <unicorn/unicorn.h>
|
||||||
|
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
}
|
45
app/src/main/cpp/switch/hw/memory.cpp
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#include <sys/mman.h>
|
||||||
|
#include <syslog.h>
|
||||||
|
#include <vector>
|
||||||
|
#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, RegionData>(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<typename T>
|
||||||
|
void Memory::Write(T value, uint64_t offset) {
|
||||||
|
Write(reinterpret_cast<void *>(&value), offset, sizeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Memory::Read(void *destination, uint64_t offset, size_t size) {
|
||||||
|
std::memcpy(destination, (void *) (offset), size);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
T Memory::Read(uint64_t offset) {
|
||||||
|
T value;
|
||||||
|
Read(reinterpret_cast<void *>(&value), offset, sizeof(T));
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
36
app/src/main/cpp/switch/hw/memory.h
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unicorn/unicorn.h>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
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, RegionData> 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<typename T>
|
||||||
|
void Write(T value, uint64_t offset);
|
||||||
|
|
||||||
|
void Read(void *destination, uint64_t offset, size_t size);
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
T Read(uint64_t offset);
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
24
app/src/main/cpp/switch/loader/loader.h
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include "../common.h"
|
||||||
|
|
||||||
|
namespace lightSwitch::loader {
|
||||||
|
class Loader {
|
||||||
|
protected:
|
||||||
|
std::string file_path;
|
||||||
|
std::ifstream file;
|
||||||
|
device_state state;
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
void ReadOffset(T *output, uint32_t offset, size_t size) {
|
||||||
|
file.seekg(offset, std::ios_base::beg);
|
||||||
|
file.read(reinterpret_cast<char *>(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) {}
|
||||||
|
};
|
||||||
|
}
|
36
app/src/main/cpp/switch/loader/nro.cpp
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#include <vector>
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
}
|
43
app/src/main/cpp/switch/loader/nro.h
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#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); };
|
||||||
|
};
|
||||||
|
}
|
31
app/src/main/cpp/switch/os/ipc.cpp
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#include <syslog.h>
|
||||||
|
#include <cstdlib>
|
||||||
|
#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<typename T>
|
||||||
|
T IpcRequest::GetValue() {
|
||||||
|
data_pos += sizeof(T);
|
||||||
|
return *reinterpret_cast<T *>(&data_ptr[data_pos - sizeof(T)]);
|
||||||
|
}
|
||||||
|
}
|
30
app/src/main/cpp/switch/os/ipc.h
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#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<typename T>
|
||||||
|
T GetValue();
|
||||||
|
};
|
||||||
|
}
|
12
app/src/main/cpp/switch/os/kernel.cpp
Normal file
@ -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++;
|
||||||
|
}
|
||||||
|
}
|
30
app/src/main/cpp/switch/os/kernel.h
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <unordered_map>
|
||||||
|
#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<KObject> KObjectPtr;
|
||||||
|
|
||||||
|
class Kernel {
|
||||||
|
private:
|
||||||
|
device_state state;
|
||||||
|
uint32_t handle_index = constant::base_handle_index;
|
||||||
|
std::unordered_map<uint32_t, KObjectPtr> handles;
|
||||||
|
public:
|
||||||
|
Kernel(device_state state_);
|
||||||
|
|
||||||
|
uint32_t NewHandle(KObjectPtr obj);
|
||||||
|
};
|
||||||
|
}
|
49
app/src/main/cpp/switch/os/os.cpp
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
#include <unicorn/arm64.h>
|
||||||
|
#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);
|
||||||
|
}
|
||||||
|
}
|
26
app/src/main/cpp/switch/os/os.h
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <err.h>
|
||||||
|
#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);
|
||||||
|
};
|
||||||
|
}
|
60
app/src/main/cpp/switch/os/svc.cpp
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <syslog.h>
|
||||||
|
#include <utility>
|
||||||
|
#include <unicorn/arm64.h>
|
||||||
|
#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();
|
||||||
|
}
|
||||||
|
}
|
184
app/src/main/cpp/switch/os/svc.h
Normal file
@ -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
|
||||||
|
};
|
||||||
|
}
|
149
app/src/main/java/emu/lightswitch/lightswitch/GameAdapter.java
Normal file
@ -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<GameItem> 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;
|
||||||
|
}
|
||||||
|
}
|
189
app/src/main/java/emu/lightswitch/lightswitch/HeaderAdapter.java
Normal file
@ -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<ItemType extends BaseItem> extends BaseAdapter implements Filterable, Serializable {
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
static Context mContext;
|
||||||
|
ArrayList<ContentType> type_array;
|
||||||
|
ArrayList<ItemType> item_array;
|
||||||
|
private ArrayList<ContentType> type_array_uf;
|
||||||
|
private ArrayList<String> 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<ContentType> filter_data = new ArrayList<>();
|
||||||
|
ArrayList<String> 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<ContentType>) filterResults.values;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class State<StateType> implements Serializable {
|
||||||
|
private ArrayList<StateType> item_array;
|
||||||
|
private ArrayList<String> header_array;
|
||||||
|
private ArrayList<ContentType> type_array;
|
||||||
|
|
||||||
|
State(ArrayList<StateType> item_array, ArrayList<String> header_array, ArrayList<ContentType> type_array) {
|
||||||
|
this.item_array = item_array;
|
||||||
|
this.header_array = header_array;
|
||||||
|
this.type_array = type_array;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
141
app/src/main/java/emu/lightswitch/lightswitch/LogActivity.java
Normal file
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
108
app/src/main/java/emu/lightswitch/lightswitch/LogAdapter.java
Normal file
@ -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<LogItem> 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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,47 +1,48 @@
|
|||||||
package gq.cyuubi.lightswitch;
|
package emu.lightswitch.lightswitch;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.appcompat.widget.SearchView;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.core.app.ActivityCompat;
|
import androidx.core.app.ActivityCompat;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class MainActivity extends AppCompatActivity {
|
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
|
||||||
|
|
||||||
static {
|
static {
|
||||||
System.loadLibrary("lightswitch");
|
System.loadLibrary("lightswitch");
|
||||||
}
|
}
|
||||||
|
|
||||||
SharedPreferences sharedPreferences;
|
SharedPreferences sharedPreferences;
|
||||||
FileAdapter adapter;
|
GameAdapter adapter;
|
||||||
|
|
||||||
private void notifyUser(String text) {
|
private void notifyUser(String text) {
|
||||||
Snackbar.make(findViewById(android.R.id.content), text, Snackbar.LENGTH_SHORT).show();
|
Snackbar.make(findViewById(android.R.id.content), text, Snackbar.LENGTH_SHORT).show();
|
||||||
// Toast.makeText(getApplicationContext(), text, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<File> findFile(String ext, File file, @Nullable List<File> files) {
|
private List<File> findFile(String ext, File file, @Nullable List<File> files) {
|
||||||
if (files == null) {
|
if (files == null)
|
||||||
files = new ArrayList<>();
|
files = new ArrayList<>();
|
||||||
}
|
|
||||||
File[] list = file.listFiles();
|
File[] list = file.listFiles();
|
||||||
if (list != null) {
|
if (list != null) {
|
||||||
for (File file_i : list) {
|
for (File file_i : list) {
|
||||||
@ -51,11 +52,12 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
try {
|
try {
|
||||||
String file_str = file_i.getName();
|
String file_str = file_i.getName();
|
||||||
if (ext.equalsIgnoreCase(file_str.substring(file_str.lastIndexOf(".") + 1))) {
|
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);
|
files.add(file_i);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (StringIndexOutOfBoundsException e) {
|
} catch (StringIndexOutOfBoundsException e) {
|
||||||
|
Log.w("findFile", Objects.requireNonNull(e.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,11 +65,28 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
return files;
|
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();
|
adapter.clear();
|
||||||
List<File> files = findFile("nro", new File(sharedPreferences.getString("search_location", "")), null);
|
List<File> files = findFile("nro", new File(sharedPreferences.getString("search_location", "")), null);
|
||||||
for (File file : files) {
|
if (!files.isEmpty()) {
|
||||||
adapter.add(new GameItem(file, getApplicationContext()));
|
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,33 +95,56 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
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) {
|
||||||
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, 1);
|
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);
|
System.exit(0);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
setContentView(R.layout.main_activity);
|
setContentView(R.layout.main_activity);
|
||||||
setSupportActionBar((Toolbar) findViewById(R.id.toolbar));
|
|
||||||
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
|
||||||
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
adapter = new FileAdapter(this, new ArrayList<GameItem>());
|
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);
|
ListView game_list = findViewById(R.id.game_list);
|
||||||
game_list.setAdapter(adapter);
|
game_list.setAdapter(adapter);
|
||||||
game_list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
game_list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||||
|
if (adapter.getItemViewType(position) == ContentType.Item) {
|
||||||
String path = ((GameItem) parent.getItemAtPosition(position)).getPath();
|
String path = ((GameItem) parent.getItemAtPosition(position)).getPath();
|
||||||
notifyUser(getString(R.string.launch_string) + " " + path);
|
notifyUser(getString(R.string.launching) + " " + path);
|
||||||
loadFile(path);
|
loadFile(path, getApplicationInfo().dataDir + "/shared_prefs/" + getApplicationInfo().packageName + "_preferences.xml", getApplicationInfo().dataDir + "/log.bin");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
refresh_files();
|
RefreshFiles(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
getMenuInflater().inflate(R.menu.toolbar, menu);
|
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 true;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
return super.onCreateOptionsMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onClick(View view) {
|
||||||
|
if (view.getId() == R.id.log_fab)
|
||||||
|
startActivity(new Intent(this, LogActivity.class));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
@ -111,8 +153,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
startActivity(new Intent(this, SettingsActivity.class));
|
startActivity(new Intent(this, SettingsActivity.class));
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_refresh:
|
case R.id.action_refresh:
|
||||||
notifyUser(getString(R.string.refresh_string));
|
RefreshFiles(false);
|
||||||
refresh_files();
|
notifyUser(getString(R.string.refreshed));
|
||||||
return true;
|
return true;
|
||||||
default:
|
default:
|
||||||
return super.onOptionsItemSelected(item);
|
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);
|
||||||
}
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package gq.cyuubi.lightswitch;
|
package emu.lightswitch.lightswitch;
|
||||||
|
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
@ -31,7 +31,7 @@ final class TitleEntry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class NroMeta {
|
public class NroLoader {
|
||||||
public static TitleEntry getTitleEntry(String file) {
|
public static TitleEntry getTitleEntry(String file) {
|
||||||
try {
|
try {
|
||||||
RandomAccessFile f = new RandomAccessFile(file, "r");
|
RandomAccessFile f = new RandomAccessFile(file, "r");
|
||||||
@ -70,7 +70,8 @@ public class NroMeta {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public static boolean verifyFile(String file) {
|
|
||||||
|
static boolean verifyFile(String file) {
|
||||||
try {
|
try {
|
||||||
RandomAccessFile f = new RandomAccessFile(file, "r");
|
RandomAccessFile f = new RandomAccessFile(file, "r");
|
||||||
f.seek(0x10); // Skip to NroHeader.magic
|
f.seek(0x10); // Skip to NroHeader.magic
|
@ -1,7 +1,6 @@
|
|||||||
package gq.cyuubi.lightswitch;
|
package emu.lightswitch.lightswitch;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
@ -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<GameItem> implements View.OnClickListener {
|
|
||||||
Context mContext;
|
|
||||||
|
|
||||||
public FileAdapter(Context context, @NonNull ArrayList<GameItem> 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;
|
|
||||||
}
|
|
||||||
}
|
|
5
app/src/main/res/drawable/ic_clear.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:alpha="0.85" android:height="24dp"
|
||||||
|
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#FFF" android:pathData="M5,13h14v-2L5,11v2zM3,17h14v-2L3,15v2zM7,7v2h14L21,7L7,7z"/>
|
||||||
|
</vector>
|
5
app/src/main/res/drawable/ic_search.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<vector android:alpha="0.85" android:height="24dp"
|
||||||
|
android:tint="#FFFFFF" android:viewportHeight="24.0"
|
||||||
|
android:viewportWidth="24.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
|
||||||
|
</vector>
|
29
app/src/main/res/layout/log_activity.xml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".LogActivity">
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.Toolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?attr/colorPrimary"
|
||||||
|
android:minHeight="?attr/actionBarSize"
|
||||||
|
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
|
||||||
|
android:popupTheme="@style/ThemeOverlay.MaterialComponents.Light"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/log_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:transcriptMode="normal"
|
||||||
|
android:fastScrollEnabled="true"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/toolbar"/>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
26
app/src/main/res/layout/log_item.xml
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:padding="15dp"
|
||||||
|
android:background="?attr/selectableItemBackground">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="5dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:textSize="15sp"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceListItem" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_subtitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/text_title"
|
||||||
|
android:layout_alignStart="@id/text_title"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="@android:color/tertiary_text_light" />
|
||||||
|
</RelativeLayout>
|
@ -8,21 +8,32 @@
|
|||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
<androidx.appcompat.widget.Toolbar
|
||||||
android:id="@+id/toolbar"
|
android:id="@+id/toolbar"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/colorPrimary"
|
android:background="?attr/colorPrimary"
|
||||||
android:minHeight="?attr/actionBarSize"
|
android:minHeight="?attr/actionBarSize"
|
||||||
android:theme="@style/ThemeOverlay.AppCompat.Dark"
|
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
|
||||||
|
android:popupTheme="@style/ThemeOverlay.MaterialComponents.Light"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"/>
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
<ListView
|
<ListView
|
||||||
android:id="@+id/game_list"
|
android:id="@+id/game_list"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
|
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
|
||||||
|
|
||||||
|
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/log_fab"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_log" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
18
app/src/main/res/layout/section_item.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:clickable="false"
|
||||||
|
android:padding="12dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/colorPrimary"
|
||||||
|
android:textSize="13sp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:layout_marginStart="5dp"/>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
15
app/src/main/res/menu/toolbar_log.xml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_search_log"
|
||||||
|
android:icon="@drawable/ic_search"
|
||||||
|
android:title="@string/search"
|
||||||
|
app:showAsAction="ifRoom|withText"
|
||||||
|
app:actionViewClass="androidx.appcompat.widget.SearchView"/>
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_clear"
|
||||||
|
android:icon="@drawable/ic_clear"
|
||||||
|
android:title="@string/clear"
|
||||||
|
app:showAsAction="ifRoom"/>
|
||||||
|
</menu>
|
@ -1,6 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_search_main"
|
||||||
|
android:icon="@drawable/ic_search"
|
||||||
|
android:title="@string/search"
|
||||||
|
app:showAsAction="ifRoom|withText"
|
||||||
|
app:actionViewClass="androidx.appcompat.widget.SearchView"/>
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_settings"
|
android:id="@+id/action_settings"
|
||||||
android:icon="@drawable/ic_settings"
|
android:icon="@drawable/ic_settings"
|
Before Width: | Height: | Size: 6.5 KiB |
BIN
app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 3.7 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 9.6 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 17 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 9.3 KiB |
Before Width: | Height: | Size: 25 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
Normal file
After Width: | Height: | Size: 14 KiB |
@ -1,5 +1,17 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
<string-array name="log_level">
|
||||||
|
<item>Error</item>
|
||||||
|
<item>Warn</item>
|
||||||
|
<item>Info</item>
|
||||||
|
<item>Debug</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="log_level_val">
|
||||||
|
<item>0</item>
|
||||||
|
<item>1</item>
|
||||||
|
<item>2</item>
|
||||||
|
<item>3</item>
|
||||||
|
</string-array>
|
||||||
<string-array name="language_names">
|
<string-array name="language_names">
|
||||||
<item>System Language</item>
|
<item>System Language</item>
|
||||||
<item>English</item>
|
<item>English</item>
|
||||||
|
@ -1,16 +1,29 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Lightswitch</string>
|
<string name="app_name">Lightswitch</string>
|
||||||
<!-- Toolbar -->
|
<!-- Common -->
|
||||||
|
<string name="search">Search</string>
|
||||||
|
<!-- Toolbar Main -->
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
|
<string name="log">Logger</string>
|
||||||
<string name="refresh">Refresh</string>
|
<string name="refresh">Refresh</string>
|
||||||
<!-- Main -->
|
<!-- Main -->
|
||||||
<string name="refresh_string">The list of ROMs has been refreshed.</string>
|
<string name="refreshed">The list of ROMs has been refreshed.</string>
|
||||||
<string name="launch_string">Launching</string>
|
<string name="launching">Launching</string>
|
||||||
<string name="aset_missing">ASET Header Missing</string>
|
<string name="aset_missing">ASET Header Missing</string>
|
||||||
<string name="icon">Icon</string>
|
<string name="icon">Icon</string>
|
||||||
|
<string name="no_rom">Cannot find any ROMs</string>
|
||||||
|
<string name="nro">NROs</string>
|
||||||
|
<string name="nso">NSOs</string>
|
||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
<string name="search">Search</string>
|
|
||||||
<string name="search_location">Search Location</string>
|
<string name="search_location">Search Location</string>
|
||||||
|
<string name="logging">Logging</string>
|
||||||
|
<string name="log_level">Log Level</string>
|
||||||
<string name="localization">Localization</string>
|
<string name="localization">Localization</string>
|
||||||
<string name="localization_language">Language</string>
|
<string name="localization_language">Language</string>
|
||||||
|
<!-- Toolbar Logger -->
|
||||||
|
<string name="clear">Clear</string>
|
||||||
|
<!-- Logger -->
|
||||||
|
<string name="file_missing">The log file was not found</string>
|
||||||
|
<string name="io_error">An I/O error has occurred</string>
|
||||||
|
<string name="cleared">The logs have been cleared</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -6,5 +6,12 @@
|
|||||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
<item name="colorAccent">@color/colorAccent</item>
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
</style>
|
</style>
|
||||||
|
<style name="ToolbarTheme">
|
||||||
|
<!-- Query Text Color -->
|
||||||
|
<item name="android:textColorPrimary">@android:color/white</item>
|
||||||
|
<!-- Hint Color -->
|
||||||
|
<item name="android:textColorHint">@android:color/darker_gray</item>
|
||||||
|
<!-- Icon Color -->
|
||||||
|
<item name="android:tint">@android:color/white</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -25,6 +25,17 @@
|
|||||||
app:title="@string/search_location"
|
app:title="@string/search_location"
|
||||||
app:useSimpleSummaryProvider="true" />
|
app:useSimpleSummaryProvider="true" />
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
<PreferenceCategory
|
||||||
|
android:key="category_log"
|
||||||
|
android:title="@string/logging">
|
||||||
|
<ListPreference
|
||||||
|
android:defaultValue="2"
|
||||||
|
android:entries="@array/log_level"
|
||||||
|
android:entryValues="@array/log_level_val"
|
||||||
|
app:key="log_level"
|
||||||
|
app:title="@string/log_level"
|
||||||
|
app:useSimpleSummaryProvider="true" />
|
||||||
|
</PreferenceCategory>
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
android:key="category_localization"
|
android:key="category_localization"
|
||||||
android:title="@string/localization">
|
android:title="@string/localization">
|
||||||
|
@ -7,7 +7,7 @@ buildscript {
|
|||||||
|
|
||||||
}
|
}
|
||||||
dependencies {
|
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
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
// in the individual module build.gradle files
|
// in the individual module build.gradle files
|
||||||
@ -18,7 +18,6 @@ allprojects {
|
|||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
jcenter()
|
jcenter()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,15 +6,16 @@
|
|||||||
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
# Specifies the JVM arguments used for the daemon process.
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
# The setting is particularly useful for tweaking memory settings.
|
# 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.
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
# This option should only be used with decoupled projects. More details, visit
|
# 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
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
org.gradle.parallel=true
|
org.gradle.parallel=true
|
||||||
|
# Enable Gradle Daemon
|
||||||
|
org.gradle.daemon=true
|
||||||
# AndroidX package structure to make it clearer which packages are bundled with the
|
# 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
|
# Android operating system, and which are packaged with your app's APK
|
||||||
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
# Automatically convert third-party libraries to use AndroidX
|
# Automatically convert third-party libraries to use AndroidX
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
|
||||||
|