Add account settings

This commit is contained in:
SSimco
2025-08-09 17:33:07 +03:00
parent 44f0e5acd3
commit f209413051
50 changed files with 2083 additions and 880 deletions
+3 -5
View File
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.21.1)
cmake_minimum_required(VERSION 3.25)
option(ENABLE_VCPKG "Enable the vcpkg package manager" ON)
option(MACOS_BUNDLE "The executable when built on macOS will be created as an application bundle" OFF)
@@ -78,10 +78,8 @@ add_definitions(-DEMULATOR_VERSION_PATCH=${EMULATOR_VERSION_PATCH})
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
# enable link time optimization for release builds
if(NOT ANDROID)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE ON)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO ON)
endif()
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELEASE ON)
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_RELWITHDEBINFO ON)
if (MSVC)
set_property(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT CemuBin)
+32 -23
View File
@@ -47,7 +47,7 @@ val cemuDataFilesFolder = "../../../bin"
android {
namespace = "info.cemu.cemu"
compileSdk = 35
ndkVersion = "26.1.10909125"
ndkVersion = "26.3.11579264"
defaultConfig {
applicationId = "info.cemu.cemu"
minSdk = 31
@@ -70,7 +70,9 @@ android {
packaging {
jniLibs.useLegacyPackaging = true
}
val keystoreFilePath: String? = System.getenv("ANDROID_KEYSTORE_FILE")
signingConfigs {
if (keystoreFilePath != null) {
create("releaseSigningConfig") {
@@ -117,33 +119,35 @@ android {
}
externalNativeBuild {
cmake {
version = "3.22.1"
version = "3.25.0+"
path = file("../../../CMakeLists.txt")
}
}
val versionArguments = if (versionMajor != null && versionMinor != null)
arrayOf(
"-DEMULATOR_VERSION_MAJOR=$versionMajor",
"-DEMULATOR_VERSION_MINOR=$versionMinor"
) else emptyArray()
val cmakeArguments = arrayOf(
"-DANDROID_STL=c++_shared",
"-DENABLE_VCPKG=ON",
"-DVCPKG_TARGET_ANDROID=ON",
"-DENABLE_SDL=OFF",
"-DENABLE_WXWIDGETS=OFF",
"-DENABLE_OPENGL=OFF",
"-DENABLE_BLUEZ=OFF",
"-DBUNDLE_SPEEX=ON",
"-DENABLE_DISCORD_RPC=OFF",
"-DENABLE_NSYSHID_LIBUSB=OFF",
"-DENABLE_WAYLAND=OFF",
"-DENABLE_HIDAPI=OFF"
) + versionArguments
defaultConfig {
externalNativeBuild {
cmake {
arguments.addAll(cmakeArguments)
arguments(
"-DANDROID_STL=c++_shared",
"-DENABLE_VCPKG=ON",
"-DVCPKG_TARGET_ANDROID=ON",
"-DENABLE_SDL=OFF",
"-DENABLE_WXWIDGETS=OFF",
"-DENABLE_OPENGL=OFF",
"-DENABLE_BLUEZ=OFF",
"-DBUNDLE_SPEEX=ON",
"-DENABLE_DISCORD_RPC=OFF",
"-DENABLE_NSYSHID_LIBUSB=OFF",
"-DENABLE_WAYLAND=OFF",
"-DENABLE_HIDAPI=OFF"
)
if (versionMajor != null && versionMinor != null) {
arguments.addAll(
arrayOf(
"-DEMULATOR_VERSION_MAJOR=$versionMajor",
"-DEMULATOR_VERSION_MINOR=$versionMinor"
)
)
}
abiFilters("arm64-v8a")
}
}
@@ -226,13 +230,18 @@ dependencies {
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.compose.material3)
testImplementation(libs.junit)
testImplementation(libs.kotlin.reflect)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
implementation(libs.okhttp)
implementation(libs.okhttp.coroutines)
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.google.android.material)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.core.ktx)
}
@@ -0,0 +1,14 @@
package info.cemu.cemu
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class PlaceholderInstrumentedTest {
@Test
fun test() {
assertTrue(true)
}
}
@@ -7,6 +7,7 @@ add_library(CemuAndroid SHARED
Image.cpp
JNIUtils.cpp
NativeActiveSettings.cpp
NativeAccount.cpp
NativeEmulation.cpp
NativeGameTitles.cpp
NativeGraphicPacks.cpp
+10
View File
@@ -22,6 +22,16 @@ namespace JNIUtils
return env->NewStringUTF(str.c_str());
}
inline jstring toJString(JNIEnv* env, std::string_view str)
{
return toJString(env, std::string(str));
}
inline jstring toJString(JNIEnv* env, std::wstring_view str)
{
return toJString(env, boost::nowide::narrow(str));
}
jobject createJavaStringArrayList(JNIEnv* env, const std::vector<std::string>& stringList);
jobject createJavaStringArrayList(JNIEnv* env, const std::vector<std::wstring>& stringList);
@@ -0,0 +1,256 @@
#include <util/helpers/SystemException.h>
#include "WindowSystem.h"
#include "JNIUtils.h"
#include "AndroidAudio.h"
#include "AndroidEmulatedController.h"
#include "AndroidFilesystemCallbacks.h"
#include "Cafe/HW/Latte/Core/LatteOverlay.h"
#include "Cafe/HW/Latte/Renderer/Vulkan/VulkanAPI.h"
#include "Cafe/HW/Latte/Renderer/Vulkan/VulkanRenderer.h"
#include "Cafe/CafeSystem.h"
#include "GameTitleLoader.h"
#include "input/ControllerFactory.h"
#include "input/InputManager.h"
#include "input/api/Android/AndroidController.h"
#include "input/api/Android/AndroidControllerProvider.h"
#include "config/ActiveSettings.h"
#include "Cemu/ncrypto/ncrypto.h"
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
Java_info_cemu_cemu_nativeinterface_NativeAccount_createAccount(JNIEnv* env, [[maybe_unused]] jclass clazz, jint persistent_id, jstring mii_name)
{
uint32 persistentId = static_cast<uint32>(persistent_id);
std::string miiName = JNIUtils::toString(env, mii_name);
Account account(persistentId, boost::nowide::widen(miiName));
account.Save();
Account::RefreshAccounts();
}
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
Java_info_cemu_cemu_nativeinterface_NativeAccount_deleteAccount([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint persistent_id)
{
uint32 persistentId = static_cast<uint32>(persistent_id);
const auto& account = Account::GetAccount(persistentId);
if (account.GetPersistentId() != persistentId)
{
return;
}
const fs::path path = account.GetFileName();
try
{
fs::remove_all(path.parent_path());
Account::RefreshAccounts();
} catch (const std::exception& ex)
{
cemuLog_log(LogType::Force, "Failed to delete account {} {}", path.c_str(), ex.what());
}
}
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
Java_info_cemu_cemu_nativeinterface_NativeAccount_saveAccount(JNIEnv* env, [[maybe_unused]] jclass clazz, jobject account_java)
{
using namespace std::chrono;
jclass accountClass = env->FindClass("info/cemu/cemu/nativeinterface/NativeAccount$Account");
auto getAccountField = [&](const char* fieldName, const char* sig, auto getFieldFn) -> auto {
auto getField = std::bind(getFieldFn, env, account_java, std::placeholders::_1);
return getField(env->GetFieldID(accountClass, fieldName, sig));
};
uint32 persistentId = getAccountField("persistentId", "I", &JNIEnv::GetIntField);
auto account = Account::GetAccount(persistentId);
if (account.GetPersistentId() != persistentId)
{
return;
}
jstring miiNameJava = static_cast<jstring>(getAccountField("miiName", "Ljava/lang/String;", &JNIEnv::GetObjectField));
account.SetMiiName(boost::nowide::widen(JNIUtils::toString(env, miiNameJava)));
account.SetCountry(getAccountField("country", "I", &JNIEnv::GetIntField));
account.SetGender(getAccountField("gender", "B", &JNIEnv::GetByteField));
jstring emailJava = static_cast<jstring>(getAccountField("email", "Ljava/lang/String;", &JNIEnv::GetObjectField));
account.SetEmail(JNIUtils::toString(env, emailJava));
auto birthdayMillis = milliseconds(getAccountField("birthday", "J", &JNIEnv::GetLongField));
system_clock::time_point birthdayTimePoint(birthdayMillis);
year_month_day birthdayYMD(floor<days>(time_point(birthdayTimePoint)));
if (birthdayYMD.ok())
{
account.SetBirthYear(static_cast<sint32>(birthdayYMD.year()));
account.SetBirthMonth(static_cast<uint32>(birthdayYMD.month()));
account.SetBirthDay(static_cast<uint32>(birthdayYMD.day()));
}
account.Save();
Account::RefreshAccounts();
}
jlong toUnixTimestampMillis(std::chrono::year_month_day ymd)
{
using namespace std::chrono;
if (!ymd.ok())
return 0;
auto millis = duration_cast<milliseconds>(system_clock::time_point(sys_days(ymd)).time_since_epoch());
return millis.count();
}
extern "C" [[maybe_unused]] JNIEXPORT jobjectArray JNICALL
Java_info_cemu_cemu_nativeinterface_NativeAccount_getAccounts(JNIEnv* env, [[maybe_unused]] jclass clazz)
{
using namespace std::chrono;
jclass accountClass = env->FindClass("info/cemu/cemu/nativeinterface/NativeAccount$Account");
jmethodID accountCtrId = env->GetMethodID(accountClass, "<init>", "(ILjava/lang/String;JBLjava/lang/String;IZ)V");
const auto& accounts = Account::GetAccounts();
jsize accountsCount = static_cast<jsize>(accounts.size());
auto accountsJArray = env->NewObjectArray(accountsCount, accountClass, nullptr);
for (jint i = 0; i < accounts.size(); i++)
{
const auto& account = accounts[i];
jint persistentId = static_cast<jint>(account.GetPersistentId());
jstring miiName = JNIUtils::toJString(env, account.GetMiiName());
jlong birthday = toUnixTimestampMillis(year_month_day(year(account.GetBirthYear()), month(account.GetBirthMonth()), day(account.GetBirthDay())));
jbyte gender = static_cast<jbyte>(account.GetGender());
jstring email = JNIUtils::toJString(env, account.GetEmail());
jint country = static_cast<jint>(account.GetCountry());
jboolean isValid = account.IsValidOnlineAccount();
jobject accountJObj = env->NewObject(
accountClass,
accountCtrId,
persistentId,
miiName,
birthday,
gender,
email,
country,
isValid);
env->SetObjectArrayElement(accountsJArray, i, accountJObj);
}
return accountsJArray;
}
extern "C" [[maybe_unused]] JNIEXPORT jobjectArray JNICALL
Java_info_cemu_cemu_nativeinterface_NativeAccount_getAccountCountries(JNIEnv* env, [[maybe_unused]] jclass clazz)
{
jclass countryClass = env->FindClass("info/cemu/cemu/nativeinterface/NativeAccount$AccountCountry");
jmethodID countryCtrId = env->GetMethodID(countryClass, "<init>", "(ILjava/lang/String;)V");
struct Country
{
jint index;
const char* name;
};
std::vector<Country> countries;
for (int i = 0; i < NCrypto::GetCountryCount(); ++i)
{
const auto countryName = NCrypto::GetCountryAsString(i);
if (countryName && (i == 0 || !boost::equals(countryName, "NN")))
{
countries.push_back({.index = i, .name = countryName});
}
}
jobjectArray countriesJava = env->NewObjectArray(countries.size(), countryClass, nullptr);
for (int i = 0; i < countries.size(); ++i)
{
const auto& country = countries[i];
jobject countryJava = env->NewObject(countryClass,
countryCtrId,
country.index,
env->NewStringUTF(country.name));
env->SetObjectArrayElement(countriesJava, i, countryJava);
}
return countriesJava;
}
extern "C" [[maybe_unused]] JNIEXPORT jobjectArray JNICALL
Java_info_cemu_cemu_nativeinterface_NativeAccount_getAccountValidationErrors(JNIEnv* env, [[maybe_unused]] jclass clazz, jint persistent_id)
{
using ErrorType = std::pair<jclass, jmethodID>;
auto getErrorType = [&](const char* className, const char* ctrSig = "()V") -> ErrorType {
using namespace std::placeholders;
jclass errorClass = env->FindClass(className);
jmethodID ctrMID = env->GetMethodID(errorClass, "<init>", ctrSig);
return std::make_pair(errorClass, ctrMID);
};
auto newError = [&](const ErrorType& error, auto... args) {
return env->NewObject(error.first, error.second, args...);
};
auto missingOTPError = getErrorType("info/cemu/cemu/nativeinterface/NativeAccount$MissingOTP");
auto corruptedOTPError = getErrorType("info/cemu/cemu/nativeinterface/NativeAccount$CorruptedOTP");
auto missingSEEPROMError = getErrorType("info/cemu/cemu/nativeinterface/NativeAccount$MissingSEEPROM");
auto corruptedSEEPROMError = getErrorType("info/cemu/cemu/nativeinterface/NativeAccount$CorruptedSEEPROM");
auto missingFileError = getErrorType("info/cemu/cemu/nativeinterface/NativeAccount$MissingFile", "(Ljava/lang/String;)V");
auto accountError = getErrorType("info/cemu/cemu/nativeinterface/NativeAccount$AccountError", "(I)V");
auto baseErrorType = env->FindClass("info/cemu/cemu/nativeinterface/NativeAccount$OnlineValidationError");
uint32 persistentId = persistent_id;
auto account = Account::GetAccount(persistentId);
const auto validator = account.ValidateOnlineFiles();
if (account.GetPersistentId() != persistentId || validator.IsValid())
{
return env->NewObjectArray(0, baseErrorType, nullptr);
}
std::vector<jobject> errors;
if (validator.otp == OnlineValidator::FileState::Missing)
errors.push_back(newError(missingOTPError));
else if (validator.otp == OnlineValidator::FileState::Corrupted)
errors.push_back(newError(corruptedOTPError));
if (validator.seeprom == OnlineValidator::FileState::Missing)
errors.push_back(newError(missingSEEPROMError));
else if (validator.seeprom == OnlineValidator::FileState::Corrupted)
errors.push_back(newError(corruptedSEEPROMError));
if (!validator.missing_files.empty())
{
int counter = 0;
for (const auto& missingFile : validator.missing_files)
{
errors.push_back(newError(missingFileError, JNIUtils::toJString(env, missingFile)));
++counter;
if (counter > 10)
{
break;
}
}
}
if (!validator.valid_account && validator.account_error != OnlineAccountError::kNone)
{
errors.push_back(newError(accountError, validator.account_error));
}
jobjectArray errorsJava = env->NewObjectArray(errors.size(), baseErrorType, nullptr);
for (int i = 0; i < errors.size(); i++)
{
env->SetObjectArrayElement(errorsJava, i, errors[i]);
}
return errorsJava;
}
extern "C" [[maybe_unused]] JNIEXPORT jboolean JNICALL
Java_info_cemu_cemu_nativeinterface_NativeAccount_isOTPPresent([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz)
{
return NCrypto::OTP_IsPresent();
}
extern "C" [[maybe_unused]] JNIEXPORT jboolean JNICALL
Java_info_cemu_cemu_nativeinterface_NativeAccount_isSEEPROMPresent([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz)
{
return NCrypto::SEEPROM_IsPresent();
}
@@ -33,4 +33,10 @@ extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
Java_info_cemu_cemu_nativeinterface_NativeActiveSettings_setInternalDir(JNIEnv* env, [[maybe_unused]] jclass clazz, jstring internal_dir)
{
ActiveSettings::SetInternalDir(JNIUtils::toString(env, internal_dir));
}
extern "C" [[maybe_unused]] JNIEXPORT jboolean JNICALL
Java_info_cemu_cemu_nativeinterface_NativeActiveSettings_hasRequiredOnlineFiles(JNIEnv* env, [[maybe_unused]] jclass clazz)
{
return ActiveSettings::HasRequiredOnlineFiles();
}
@@ -15,7 +15,7 @@ namespace NativeLocalization
}
} // namespace NativeLocalization
extern "C" JNIEXPORT [[maybe_unused]] void JNICALL
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
Java_info_cemu_cemu_nativeinterface_NativeLocalization_setTranslations(JNIEnv* env, [[maybe_unused]] jclass clazz, jobject translations)
{
NativeLocalization::g_messages.clear();
@@ -1,6 +1,7 @@
#include "JNIUtils.h"
#include "audio/IAudioAPI.h"
#include "config/CemuConfig.h"
#include "config/NetworkSettings.h"
extern "C" [[maybe_unused]] JNIEXPORT jint JNICALL
Java_info_cemu_cemu_nativeinterface_NativeSettings_getOverlayPosition([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz)
@@ -175,7 +176,7 @@ Java_info_cemu_cemu_nativeinterface_NativeSettings_addGamesPath(JNIEnv* env, [[m
{
auto& gamePaths = GetConfig().game_paths;
auto gamePath = JNIUtils::toString(env, uri);
if (std::any_of(gamePaths.begin(), gamePaths.end(), [&](auto path) { return path == gamePath; }))
if (std::any_of(gamePaths.begin(), gamePaths.end(), [&](const auto& path) { return path == gamePath; }))
return;
gamePaths.push_back(gamePath);
}
@@ -185,7 +186,7 @@ Java_info_cemu_cemu_nativeinterface_NativeSettings_removeGamesPath(JNIEnv* env,
{
auto gamePath = JNIUtils::toString(env, uri);
auto& gamePaths = GetConfig().game_paths;
std::erase_if(gamePaths, [&](auto path) { return path == gamePath; });
std::erase_if(gamePaths, [&](const auto& path) { return path == gamePath; });
}
extern "C" [[maybe_unused]] JNIEXPORT jobject JNICALL
@@ -320,8 +321,9 @@ Java_info_cemu_cemu_nativeinterface_NativeSettings_getAudioLatency([[maybe_unuse
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
Java_info_cemu_cemu_nativeinterface_NativeSettings_setAudioLatency([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint latency)
{
GetConfig().audio_delay = latency / 12;
// IAudioAPI::SetAudioDelayOverride(GetConfig().audio_delay);
sint32 audioDelay = latency / 12;
GetConfig().audio_delay = audioDelay;
IAudioAPI::SetAudioDelay(audioDelay);
}
extern "C" [[maybe_unused]] JNIEXPORT jint JNICALL
@@ -351,8 +353,38 @@ Java_info_cemu_cemu_nativeinterface_NativeSettings_setCustomDriverPath(JNIEnv* e
GetConfig().custom_driver_path = JNIUtils::toString(env, custom_driver_path);
}
extern "C" [[maybe_unused]] JNIEXPORT jint JNICALL
Java_info_cemu_cemu_nativeinterface_NativeSettings_getAccountNetworkService([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint persistent_id)
{
return static_cast<jint>(GetConfig().GetAccountNetworkService(persistent_id));
}
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
Java_info_cemu_cemu_nativeinterface_NativeSettings_setAccountNetworkService([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint persistent_id, jint network_service)
{
GetConfig().SetAccountSelectedService(persistent_id, static_cast<NetworkService>(network_service));
}
extern "C" [[maybe_unused]] JNIEXPORT jint JNICALL
Java_info_cemu_cemu_nativeinterface_NativeSettings_getAccountPersistentId([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz)
{
return static_cast<jint>(GetConfig().account.m_persistent_id);
}
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
Java_info_cemu_cemu_nativeinterface_NativeSettings_setAccountPersistentId([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jint persistent_id)
{
GetConfig().account.m_persistent_id = persistent_id;
}
extern "C" [[maybe_unused]] JNIEXPORT jboolean JNICALL
Java_info_cemu_cemu_nativeinterface_NativeSettings_hasCustomNetworkConfiguration([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz)
{
return NetworkConfig::XMLExists();
}
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
Java_info_cemu_cemu_nativeinterface_NativeSettings_saveSettings([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz)
{
GetConfigHandle().Save();
}
}
@@ -38,12 +38,10 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.PlatformImeOptions
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import info.cemu.cemu.R
@OptIn(ExperimentalMaterial3Api::class)
@@ -1,33 +0,0 @@
package info.cemu.cemu.core.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.semantics.Role
fun Modifier.clickableWithNoRipple(
enabled: Boolean = true,
onClickLabel: String? = null,
role: Role? = null,
onClick: () -> Unit
): Modifier = composed(
inspectorInfo = debugInspectorInfo {
name = "clickable"
properties["enabled"] = enabled
properties["onClickLabel"] = onClickLabel
properties["role"] = role
properties["onClick"] = onClick
}
) {
Modifier.clickable(
enabled = enabled,
onClickLabel = onClickLabel,
onClick = onClick,
role = role,
indication = null,
interactionSource = remember { MutableInteractionSource() }
)
}
@@ -19,27 +19,27 @@ import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import info.cemu.cemu.R
import info.cemu.cemu.core.translation.tr
@Composable
fun SingleSelection(
label: String,
choice: String,
choices: List<String>,
choices: Collection<String>,
isChoiceEnabled: (String) -> Boolean = { true },
modifier: Modifier = Modifier,
enabled: Boolean = true,
modifier: Modifier = Modifier.fillMaxWidth(),
onChoiceChanged: (String) -> Unit,
) {
SingleSelection(
@@ -48,6 +48,7 @@ fun SingleSelection(
choices = choices,
modifier = modifier,
choiceToString = { it },
enabled = enabled,
isChoiceEnabled = isChoiceEnabled,
onChoiceChanged = onChoiceChanged,
)
@@ -58,10 +59,11 @@ fun SingleSelection(
fun <T> SingleSelection(
label: String,
initialChoice: () -> T,
choices: List<T>,
choices: Collection<T>,
modifier: Modifier = Modifier.fillMaxWidth(),
choiceToString: @Composable (T) -> String,
isChoiceEnabled: (T) -> Boolean = { true },
modifier: Modifier = Modifier,
enabled: Boolean = true,
onChoiceChanged: (T) -> Unit,
) {
var choice by rememberSaveable { mutableStateOf(initialChoice()) }
@@ -71,6 +73,7 @@ fun <T> SingleSelection(
choices = choices,
modifier = modifier,
isChoiceEnabled = isChoiceEnabled,
enabled = enabled,
choiceToString = choiceToString,
onChoiceChanged = { newChoice ->
choice = newChoice
@@ -83,32 +86,45 @@ fun <T> SingleSelection(
fun <T> SingleSelection(
label: String,
choice: T,
choices: List<T>,
choices: Collection<T>,
choiceToString: @Composable (T) -> String,
modifier: Modifier = Modifier,
modifier: Modifier = Modifier.fillMaxWidth(),
enabled: Boolean = true,
isChoiceEnabled: (T) -> Boolean = { true },
onChoiceChanged: (T) -> Unit,
) {
var showSelectDialog by rememberSaveable { mutableStateOf(false) }
Column(
modifier = modifier
.clickable { showSelectDialog = true; }
.fillMaxWidth()
.padding(8.dp),
verticalArrangement = Arrangement.Center,
) {
Text(
text = label,
modifier = Modifier.padding(vertical = 8.dp),
fontWeight = FontWeight.Medium,
fontSize = 20.sp,
)
Text(
text = choiceToString(choice),
modifier = Modifier.padding(vertical = 8.dp),
fontSize = 16.sp,
)
val clickableModifier = if (enabled) {
Modifier.clickable { showSelectDialog = true }
} else {
Modifier
}
CompositionLocalProvider(
LocalContentColor provides
MaterialTheme.colorScheme.onSurface.copy(alpha = if (enabled) 1f else 0.38f)
)
{
Column(
modifier = modifier
.then(clickableModifier)
.padding(8.dp),
verticalArrangement = Arrangement.Center,
) {
Text(
text = label,
modifier = Modifier.padding(vertical = 8.dp),
fontWeight = FontWeight.Medium,
fontSize = 20.sp,
)
Text(
text = choiceToString(choice),
modifier = Modifier.padding(vertical = 8.dp),
fontSize = 16.sp,
)
}
}
if (showSelectDialog) {
SelectDialog(
label = label,
@@ -126,7 +142,7 @@ fun <T> SingleSelection(
private fun <T> SelectDialog(
label: String,
currentChoice: T,
choices: List<T>,
choices: Collection<T>,
isChoiceEnabled: (T) -> Boolean,
choiceToString: @Composable (T) -> String,
onDismissRequest: () -> Unit,
@@ -1,16 +1,16 @@
package info.cemu.cemu.core.nativeenummapper
import info.cemu.cemu.core.translation.tr
import info.cemu.cemu.nativeinterface.NativeGameTitles
import info.cemu.cemu.core.translation.tr
import info.cemu.cemu.nativeinterface.NativeGameTitles.ConsoleRegion
fun regionToString(region: Int): String = when (region) {
NativeGameTitles.CONSOLE_REGION_JPN -> tr("Japan")
NativeGameTitles.CONSOLE_REGION_USA -> tr("USA")
NativeGameTitles.CONSOLE_REGION_EUR -> tr("Europe")
NativeGameTitles.CONSOLE_REGION_AUS_DEPR -> tr("Australia")
NativeGameTitles.CONSOLE_REGION_CHN -> tr("China")
NativeGameTitles.CONSOLE_REGION_KOR -> tr("Korea")
NativeGameTitles.CONSOLE_REGION_TWN -> tr("Taiwan")
NativeGameTitles.CONSOLE_REGION_AUTO -> tr("Auto")
ConsoleRegion.JPN -> tr("Japan")
ConsoleRegion.USA -> tr("USA")
ConsoleRegion.EUR -> tr("Europe")
ConsoleRegion.AUS_DEPR -> tr("Australia")
ConsoleRegion.CHN -> tr("China")
ConsoleRegion.KOR -> tr("Korea")
ConsoleRegion.TWN -> tr("Taiwan")
ConsoleRegion.AUTO -> tr("Auto")
else -> tr("Many")
}
@@ -1,13 +1,13 @@
package info.cemu.cemu.core.nativeenummapper
import info.cemu.cemu.core.translation.tr
import info.cemu.cemu.nativeinterface.NativeInput
import info.cemu.cemu.nativeinterface.NativeInput.EmulatedControllerType
fun controllerTypeToString(type: Int) = when (type) {
NativeInput.EMULATED_CONTROLLER_TYPE_DISABLED -> tr("Disabled")
NativeInput.EMULATED_CONTROLLER_TYPE_VPAD -> tr("Wii U GamePad")
NativeInput.EMULATED_CONTROLLER_TYPE_PRO -> tr("Wii U Pro Controller")
NativeInput.EMULATED_CONTROLLER_TYPE_WIIMOTE -> tr("Wiimote")
NativeInput.EMULATED_CONTROLLER_TYPE_CLASSIC -> tr("Wii U Classic Controller")
EmulatedControllerType.DISABLED -> tr("Disabled")
EmulatedControllerType.VPAD -> tr("Wii U GamePad")
EmulatedControllerType.PRO -> tr("Wii U Pro Controller")
EmulatedControllerType.WIIMOTE -> tr("Wiimote")
EmulatedControllerType.CLASSIC -> tr("Wii U Classic Controller")
else -> throw IllegalArgumentException("Invalid controller type: $type")
}
@@ -1,6 +1,5 @@
package info.cemu.cemu.core.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
@@ -21,7 +20,7 @@ fun CemuTheme(
content: @Composable () -> Unit,
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
dynamicColor -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
@@ -60,6 +60,8 @@ fun setLanguage(languageCode: String, context: Context) {
CurrentLanguage = translation.locale.language
}
fun getCurrentLocale() = I18n.locale
fun setTranslations(context: Context) {
val assetTranslations = context.assets.list(TRANSLATIONS_FOLDER)?.filter { language ->
if (language == DEFAULT_LANGUAGE)
@@ -65,7 +65,7 @@ class EmulationActivity : AppCompatActivity() {
NativeEmulation.setSurface(surfaceHolder.surface, isMainCanvas)
surfaceSet = true
} catch (exception: NativeException) {
onEmulationError(tr(">Failed creating surface: {0}", exception.message!!))
onEmulationError(tr("Failed creating surface: {0}", exception.message!!))
}
}
@@ -241,9 +241,8 @@ class EmulationActivity : AppCompatActivity() {
setFullscreen()
binding = ActivityEmulationBinding.inflate(layoutInflater)
inputOverlaySurfaceView = binding.inputOverlay
inputOverlaySurfaceView.setVisible(overlaySettings.isOverlayEnabled)
initializeInputOverlay()
binding.sideMenu.configureSideMenu()
@@ -294,16 +293,8 @@ class EmulationActivity : AppCompatActivity() {
val mainCanvasHolder = mainCanvas.holder
mainCanvasHolder.addCallback(CanvasSurfaceHolderCallback(isMainCanvas = true))
mainCanvasHolder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
}
override fun surfaceChanged(
holder: SurfaceHolder,
format: Int,
width: Int,
height: Int,
) {
mainCanvasHolder.addCallback(object : SurfaceChangedListener() {
override fun surfaceChanged() {
if (hasEmulationError) {
return
}
@@ -312,13 +303,16 @@ class EmulationActivity : AppCompatActivity() {
startGame(launchPath)
}
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
}
})
mainCanvas.setOnTouchListener(CanvasOnTouchListener(isTV = true))
}
private fun initializeInputOverlay() {
inputOverlaySurfaceView = binding.inputOverlay
inputOverlaySurfaceView.setVisible(overlaySettings.isOverlayEnabled)
}
private fun toastMessage(text: String) {
toast?.cancel()
toast = Toast.makeText(this, text, Toast.LENGTH_SHORT)
@@ -327,15 +321,16 @@ class EmulationActivity : AppCompatActivity() {
private fun startGame(launchPath: String) {
val result = NativeEmulation.startGame(launchPath)
if (result == NativeEmulation.START_GAME_SUCCESSFUL) {
if (result == NativeEmulation.StartGameStatusCode.SUCCESSFUL) {
return
}
val errorMessage = when (result) {
NativeEmulation.START_GAME_ERROR_GAME_BASE_FILES_NOT_FOUND -> tr("Unable to launch game because the base files were not found.")
NativeEmulation.START_GAME_ERROR_NO_DISC_KEY -> tr("Could not decrypt title. Make sure that keys.txt contains the correct disc key for this title.")
NativeEmulation.START_GAME_ERROR_NO_TITLE_TIK -> tr("Could not decrypt title because title.tik is missing.")
NativeEmulation.StartGameStatusCode.ERROR_GAME_BASE_FILES_NOT_FOUND -> tr("Unable to launch game because the base files were not found.")
NativeEmulation.StartGameStatusCode.ERROR_NO_DISC_KEY -> tr("Could not decrypt title. Make sure that keys.txt contains the correct disc key for this title.")
NativeEmulation.StartGameStatusCode.ERROR_NO_TITLE_TIK -> tr("Could not decrypt title because title.tik is missing.")
else -> tr("Unable to launch game\nPath: {0}", launchPath)
}
onEmulationError(errorMessage)
}
@@ -0,0 +1,23 @@
package info.cemu.cemu.emulation
import android.view.SurfaceHolder
abstract class SurfaceChangedListener : SurfaceHolder.Callback {
override fun surfaceCreated(surfaceHolder: SurfaceHolder) {
}
abstract fun surfaceChanged()
override fun surfaceChanged(
surfaceHolder: SurfaceHolder,
format: Int,
width: Int,
height: Int
) {
surfaceChanged()
}
override fun surfaceDestroyed(surfaceHolder: SurfaceHolder) {
}
}
@@ -11,6 +11,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import info.cemu.cemu.core.components.ScreenContent
import info.cemu.cemu.core.nativeenummapper.regionToString
import info.cemu.cemu.core.translation.tr
import info.cemu.cemu.nativeinterface.NativeGameTitles
import info.cemu.cemu.nativeinterface.NativeGameTitles.Game
@@ -106,16 +107,3 @@ private fun <T> TitleDetailsEntry(entryName: String, entryData: T?) {
)
}
}
fun regionToString(region: Int): String = when (region) {
NativeGameTitles.CONSOLE_REGION_JPN -> tr("Japan")
NativeGameTitles.CONSOLE_REGION_USA -> tr("USA")
NativeGameTitles.CONSOLE_REGION_EUR -> tr("Europe")
NativeGameTitles.CONSOLE_REGION_AUS_DEPR -> tr("Australia")
NativeGameTitles.CONSOLE_REGION_CHN -> tr("China")
NativeGameTitles.CONSOLE_REGION_KOR -> tr("Korea")
NativeGameTitles.CONSOLE_REGION_TWN -> tr("Taiwan")
NativeGameTitles.CONSOLE_REGION_AUTO -> tr("Auto")
else -> tr("Many")
}
@@ -59,10 +59,10 @@ fun GameProfileEditScreen(game: NativeGameTitles.Game?, navigateBack: () -> Unit
label = tr("CPU mode"),
initialChoice = { NativeGameTitles.getCpuModeForTitle(titleId) },
choices = listOf(
NativeGameTitles.CPU_MODE_SINGLECOREINTERPRETER,
NativeGameTitles.CPU_MODE_SINGLECORERECOMPILER,
NativeGameTitles.CPU_MODE_MULTICORERECOMPILER,
NativeGameTitles.CPU_MODE_AUTO
NativeGameTitles.CPUMode.SINGLECOREINTERPRETER,
NativeGameTitles.CPUMode.SINGLECORERECOMPILER,
NativeGameTitles.CPUMode.MULTICORERECOMPILER,
NativeGameTitles.CPUMode.AUTO
),
choiceToString = { cpuMode -> cpuModeToString(cpuMode) },
onChoiceChanged = { cpuMode -> NativeGameTitles.setCpuModeForTitle(titleId, cpuMode) }
@@ -80,8 +80,8 @@ fun GameProfileEditScreen(game: NativeGameTitles.Game?, navigateBack: () -> Unit
}
private fun cpuModeToString(cpuMode: Int): String = when (cpuMode) {
NativeGameTitles.CPU_MODE_SINGLECOREINTERPRETER -> tr("Single-core interpreter")
NativeGameTitles.CPU_MODE_SINGLECORERECOMPILER -> tr("Single-core recompiler")
NativeGameTitles.CPU_MODE_MULTICORERECOMPILER -> tr("Multi-core recompiler")
NativeGameTitles.CPUMode.SINGLECOREINTERPRETER -> tr("Single-core interpreter")
NativeGameTitles.CPUMode.SINGLECORERECOMPILER -> tr("Single-core recompiler")
NativeGameTitles.CPUMode.MULTICORERECOMPILER -> tr("Multi-core recompiler")
else -> tr("Auto (recommended)")
}

Some files were not shown because too many files have changed in this diff Show More