From 80ea25b10f9ca33f88d4d7f2dbfb03ef9168ba46 Mon Sep 17 00:00:00 2001
From: SSimco <37044560+SSimco@users.noreply.github.com>
Date: Sat, 21 Sep 2024 20:48:39 +0300
Subject: [PATCH] Added support for opening titles using file managers
---
src/android/app/src/main/AndroidManifest.xml | 25 +++++++-
.../main/cpp/AndroidGameTitleLoadedCallback.h | 6 +-
.../app/src/main/cpp/CafeSystemUtils.cpp | 47 ++++++++-------
.../app/src/main/cpp/CafeSystemUtils.h | 47 ++++++++++++++-
src/android/app/src/main/cpp/EmulationState.h | 4 +-
.../app/src/main/cpp/GameTitleLoader.cpp | 2 +
.../app/src/main/cpp/GameTitleLoader.h | 2 +-
src/android/app/src/main/cpp/native-lib.cpp | 26 ++++++++-
.../java/info/cemu/Cemu/NativeLibrary.java | 19 +++++-
.../Cemu/emulation/EmulationActivity.java | 58 +++++++++++++++----
.../cemu/Cemu/emulation/EmulationData.java | 6 ++
.../cemu/Cemu/emulation/EmulationError.java | 4 ++
.../Cemu/emulation/EmulationFragment.java | 48 ++++++++++++---
.../Cemu/emulation/EmulationViewModel.java | 19 ++++++
.../java/info/cemu/Cemu/gameview/Game.java | 23 +-------
.../info/cemu/Cemu/gameview/GameAdapter.java | 18 +++---
.../cemu/Cemu/gameview/GameViewModel.java | 4 +-
.../cemu/Cemu/gameview/GamesFragment.java | 2 +-
.../settings/gamespath/GamePathsFragment.java | 45 +++++++-------
.../app/src/main/res/values/strings.xml | 6 ++
20 files changed, 300 insertions(+), 111 deletions(-)
create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationData.java
create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationError.java
create mode 100644 src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationViewModel.java
diff --git a/src/android/app/src/main/AndroidManifest.xml b/src/android/app/src/main/AndroidManifest.xml
index 34166107..d83049f8 100644
--- a/src/android/app/src/main/AndroidManifest.xml
+++ b/src/android/app/src/main/AndroidManifest.xml
@@ -9,10 +9,10 @@
+ tools:ignore="DiscouragedApi">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
NewStringUTF(game.name.c_str());
- jlong titleId = static_cast(game.titleId);
+ jstring path = game.path.has_value() ? env->NewStringUTF(game.path->c_str()) : nullptr;
int width = -1, height = -1;
jintArray jIconData = nullptr;
if (icon)
@@ -27,9 +27,11 @@ class AndroidGameTitleLoadedCallback : public GameTitleLoadedCallback
jIconData = env->NewIntArray(width * height);
env->SetIntArrayRegion(jIconData, 0, width * height, reinterpret_cast(icon->intColors()));
}
- env->CallVoidMethod(*m_gameTitleLoadedCallbackObj, m_onGameTitleLoadedMID, static_cast(titleId), name, jIconData, width, height);
+ env->CallVoidMethod(*m_gameTitleLoadedCallbackObj, m_onGameTitleLoadedMID, path, name, jIconData, width, height);
if (jIconData != nullptr)
env->DeleteLocalRef(jIconData);
+ if (path != nullptr)
+ env->DeleteLocalRef(path);
env->DeleteLocalRef(name);
}
};
diff --git a/src/android/app/src/main/cpp/CafeSystemUtils.cpp b/src/android/app/src/main/cpp/CafeSystemUtils.cpp
index cbf7fcbe..4c4b55c7 100644
--- a/src/android/app/src/main/cpp/CafeSystemUtils.cpp
+++ b/src/android/app/src/main/cpp/CafeSystemUtils.cpp
@@ -5,47 +5,50 @@
namespace CafeSystemUtils
{
- void startGame(TitleId titleId)
+ void startGame(const fs::path& launchPath)
{
- TitleInfo launchTitle;
-
- if (!CafeTitleList::GetFirstByTitleId(titleId, launchTitle))
- return;
-
+ TitleInfo launchTitle{launchPath};
if (launchTitle.IsValid())
{
// the title might not be in the TitleList, so we add it as a temporary entry
- CafeTitleList::AddTitleFromPath(launchTitle.GetPath());
+ CafeTitleList::AddTitleFromPath(launchPath);
// title is valid, launch from TitleId
TitleId baseTitleId;
if (!CafeTitleList::FindBaseTitleId(launchTitle.GetAppTitleId(), baseTitleId))
{
+ throw GameBaseFilesNotFoundException();
}
- CafeSystem::STATUS_CODE statusCode = CafeSystem::PrepareForegroundTitle(baseTitleId);
- if (statusCode == CafeSystem::STATUS_CODE::INVALID_RPX)
- {
- }
- else if (statusCode == CafeSystem::STATUS_CODE::UNABLE_TO_MOUNT)
- {
- }
- else if (statusCode != CafeSystem::STATUS_CODE::SUCCESS)
+ CafeSystem::STATUS_CODE r = CafeSystem::PrepareForegroundTitle(baseTitleId);
+ if (r != CafeSystem::STATUS_CODE::SUCCESS)
{
+ throw UnknownGameFilesException();
}
}
else // if (launchTitle.GetFormat() == TitleInfo::TitleDataFormat::INVALID_STRUCTURE )
{
- // title is invalid, if its an RPX/ELF we can launch it directly
- // otherwise its an error
- CafeTitleFileType fileType = DetermineCafeSystemFileType(launchTitle.GetPath());
+ // title is invalid, if it's an RPX/ELF we can launch it directly
+ // otherwise it's an error
+ CafeTitleFileType fileType = DetermineCafeSystemFileType(launchPath);
if (fileType == CafeTitleFileType::RPX || fileType == CafeTitleFileType::ELF)
{
- CafeSystem::STATUS_CODE statusCode = CafeSystem::PrepareForegroundTitleFromStandaloneRPX(
- launchTitle.GetPath());
- if (statusCode != CafeSystem::STATUS_CODE::SUCCESS)
+ CafeSystem::STATUS_CODE r = CafeSystem::PrepareForegroundTitleFromStandaloneRPX(launchPath);
+ if (r != CafeSystem::STATUS_CODE::SUCCESS)
{
- return;
+ throw UnknownGameFilesException();
}
}
+ else if (launchTitle.GetInvalidReason() == TitleInfo::InvalidReason::NO_DISC_KEY)
+ {
+ throw NoDiscKeyException();
+ }
+ else if (launchTitle.GetInvalidReason() == TitleInfo::InvalidReason::NO_TITLE_TIK)
+ {
+ throw NoTitleTikException();
+ }
+ else
+ {
+ throw UnknownGameFilesException();
+ }
}
CafeSystem::LaunchForegroundTitle();
}
diff --git a/src/android/app/src/main/cpp/CafeSystemUtils.h b/src/android/app/src/main/cpp/CafeSystemUtils.h
index 99d8d3e0..8cdaed43 100644
--- a/src/android/app/src/main/cpp/CafeSystemUtils.h
+++ b/src/android/app/src/main/cpp/CafeSystemUtils.h
@@ -4,5 +4,48 @@
namespace CafeSystemUtils
{
- void startGame(TitleId titleId);
-};
\ No newline at end of file
+ class GameFilesException : public std::exception
+ {
+ public:
+ explicit GameFilesException(const std::string& message)
+ : m_message(message) {}
+
+ const char* what() const noexcept override
+ {
+ return m_message.c_str();
+ }
+
+ private:
+ std::string m_message;
+ };
+
+ class GameBaseFilesNotFoundException : public GameFilesException
+ {
+ public:
+ GameBaseFilesNotFoundException()
+ : GameFilesException("Game base files not found.") {}
+ };
+
+ class NoDiscKeyException : public GameFilesException
+ {
+ public:
+ NoDiscKeyException()
+ : GameFilesException("No disc key found.") {}
+ };
+
+ class NoTitleTikException : public GameFilesException
+ {
+ public:
+ NoTitleTikException()
+ : GameFilesException("No title ticket found.") {}
+ };
+
+ class UnknownGameFilesException : public GameFilesException
+ {
+ public:
+ UnknownGameFilesException()
+ : GameFilesException("Unknown error occurred during game launch.") {}
+ };
+
+ void startGame(const fs::path& launchPath);
+}; // namespace CafeSystemUtils
\ No newline at end of file
diff --git a/src/android/app/src/main/cpp/EmulationState.h b/src/android/app/src/main/cpp/EmulationState.h
index 3bdebc1e..91815538 100644
--- a/src/android/app/src/main/cpp/EmulationState.h
+++ b/src/android/app/src/main/cpp/EmulationState.h
@@ -268,11 +268,11 @@ class EmulationState
m_gameTitleLoader.reloadGameTitles();
}
- void startGame(TitleId titleId)
+ void startGame(const fs::path& gamePath)
{
GuiSystem::getWindowInfo().set_keystates_up();
initializeAudioDevices();
- CafeSystemUtils::startGame(titleId);
+ CafeSystemUtils::startGame(gamePath);
}
void refreshGraphicPacks()
diff --git a/src/android/app/src/main/cpp/GameTitleLoader.cpp b/src/android/app/src/main/cpp/GameTitleLoader.cpp
index 38ba545b..884ed04e 100644
--- a/src/android/app/src/main/cpp/GameTitleLoader.cpp
+++ b/src/android/app/src/main/cpp/GameTitleLoader.cpp
@@ -72,6 +72,8 @@ void GameTitleLoader::titleRefresh(TitleId titleId)
Game& game = m_gameInfos[baseTitleId];
std::optional titleInfo = getFirstTitleInfoByTitleId(titleId);
game.titleId = baseTitleId;
+ if (titleInfo.has_value())
+ game.path = titleInfo->GetPath();
game.name = getNameByTitleId(baseTitleId, titleInfo);
game.version = gameInfo.GetVersion();
game.region = gameInfo.GetRegion();
diff --git a/src/android/app/src/main/cpp/GameTitleLoader.h b/src/android/app/src/main/cpp/GameTitleLoader.h
index 21709bb2..9cbb49df 100644
--- a/src/android/app/src/main/cpp/GameTitleLoader.h
+++ b/src/android/app/src/main/cpp/GameTitleLoader.h
@@ -8,6 +8,7 @@
struct Game
{
std::string name;
+ std::optional path;
uint32 secondsPlayed;
uint16 dlc;
uint16 version;
@@ -41,7 +42,6 @@ class GameTitleLoader
void reloadGameTitles();
~GameTitleLoader();
void titleRefresh(TitleId titleId);
- void addGamePath(const fs::path& path);
private:
void loadGameTitles();
diff --git a/src/android/app/src/main/cpp/native-lib.cpp b/src/android/app/src/main/cpp/native-lib.cpp
index 312d9fef..8b69978e 100644
--- a/src/android/app/src/main/cpp/native-lib.cpp
+++ b/src/android/app/src/main/cpp/native-lib.cpp
@@ -26,9 +26,29 @@ Java_info_cemu_Cemu_NativeLibrary_setSurfaceSize([[maybe_unused]] JNIEnv* env, [
}
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
-Java_info_cemu_Cemu_NativeLibrary_startGame([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jlong title_id)
+Java_info_cemu_Cemu_NativeLibrary_startGame([[maybe_unused]] JNIEnv* env, [[maybe_unused]] jclass clazz, jstring launchPath)
{
- s_emulationState.startGame(static_cast(title_id));
+ using namespace CafeSystemUtils;
+ try
+ {
+ s_emulationState.startGame(JNIUtils::JStringToString(env, launchPath));
+ } catch (const GameBaseFilesNotFoundException& exception)
+ {
+ jclass exceptionClass = env->FindClass("info/cemu/Cemu/NativeLibrary$GameBaseFilesNotFoundException");
+ env->ThrowNew(exceptionClass, exception.what());
+ } catch (const NoDiscKeyException& exception)
+ {
+ jclass exceptionClass = env->FindClass("info/cemu/Cemu/NativeLibrary$NoDiscKeyException");
+ env->ThrowNew(exceptionClass, exception.what());
+ } catch (const NoTitleTikException& exception)
+ {
+ jclass exceptionClass = env->FindClass("info/cemu/Cemu/NativeLibrary$NoTitleTikException");
+ env->ThrowNew(exceptionClass, exception.what());
+ } catch (const std::exception& exception)
+ {
+ jclass exceptionClass = env->FindClass("info/cemu/Cemu/NativeLibrary$UnknownGameFilesException");
+ env->ThrowNew(exceptionClass, exception.what());
+ }
}
extern "C" [[maybe_unused]] JNIEXPORT void JNICALL
@@ -40,7 +60,7 @@ Java_info_cemu_Cemu_NativeLibrary_setGameTitleLoadedCallback(JNIEnv* env, [[mayb
return;
}
jclass gameTitleLoadedCallbackClass = env->GetObjectClass(game_title_loaded_callback);
- jmethodID onGameTitleLoadedMID = env->GetMethodID(gameTitleLoadedCallbackClass, "onGameTitleLoaded", "(JLjava/lang/String;[III)V");
+ jmethodID onGameTitleLoadedMID = env->GetMethodID(gameTitleLoadedCallbackClass, "onGameTitleLoaded", "(Ljava/lang/String;Ljava/lang/String;[III)V");
env->DeleteLocalRef(gameTitleLoadedCallbackClass);
s_emulationState.setOnGameTitleLoaded(std::make_shared(onGameTitleLoadedMID, game_title_loaded_callback));
}
diff --git a/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java b/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java
index b00204db..8b02da36 100644
--- a/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java
+++ b/src/android/app/src/main/java/info/cemu/Cemu/NativeLibrary.java
@@ -22,14 +22,29 @@ public class NativeLibrary {
public static native void initializerRenderer(Surface surface);
- public static native void startGame(long titleId);
+ public static class GameFilesException extends RuntimeException {
+ }
+
+ public static class GameBaseFilesNotFoundException extends GameFilesException {
+ }
+
+ public static class NoDiscKeyException extends GameFilesException {
+ }
+
+ public static class NoTitleTikException extends GameFilesException {
+ }
+
+ public static class UnknownGameFilesException extends GameFilesException {
+ }
+
+ public static native void startGame(String launchPath);
public static native void setReplaceTVWithPadView(boolean swapped);
public static native void recreateRenderSurface(boolean isMainCanvas);
public interface GameTitleLoadedCallback {
- void onGameTitleLoaded(long titleId, String title, int[] colors, int width, int height);
+ void onGameTitleLoaded(String path, String title, int[] colors, int width, int height);
}
public static native void setGameTitleLoadedCallback(GameTitleLoadedCallback gameTitleLoadedCallback);
diff --git a/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationActivity.java b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationActivity.java
index a696cb57..b23794ac 100644
--- a/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationActivity.java
+++ b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationActivity.java
@@ -1,5 +1,8 @@
package info.cemu.Cemu.emulation;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.MotionEvent;
@@ -8,6 +11,8 @@ import androidx.appcompat.app.AppCompatActivity;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@@ -15,10 +20,10 @@ import info.cemu.Cemu.R;
import info.cemu.Cemu.databinding.ActivityEmulationBinding;
import info.cemu.Cemu.input.InputManager;
-public class EmulationActivity extends AppCompatActivity {
-
- public static final String GAME_TITLE_ID = "GameTitleId";
- private long gameTitleId;
+public class EmulationActivity extends AppCompatActivity implements Observer {
+ private EmulationViewModel viewModel;
+ private boolean hasEmulationError;
+ public static final String LAUNCH_PATH = "LAUNCH_PATH";
private ActivityEmulationBinding binding;
private EmulationFragment emulationFragment;
private final InputManager inputManager = new InputManager();
@@ -40,16 +45,26 @@ public class EmulationActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- Bundle extras = getIntent().getExtras();
+ viewModel = new ViewModelProvider(this).get(EmulationViewModel.class);
+ viewModel.getEmulationData().observe(this, this);
+ Intent intent = getIntent();
+ Bundle extras = intent.getExtras();
+ Uri data = intent.getData();
+ String launchPath = null;
if (extras != null) {
- gameTitleId = extras.getLong(GAME_TITLE_ID);
+ launchPath = extras.getString(LAUNCH_PATH);
+ } else if (data != null) {
+ launchPath = data.toString();
+ }
+ if (launchPath == null) {
+ throw new RuntimeException("launchPath is null");
}
setFullscreen();
binding = ActivityEmulationBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
emulationFragment = (EmulationFragment) getSupportFragmentManager().findFragmentById(R.id.emulation_frame);
if (emulationFragment == null) {
- emulationFragment = new EmulationFragment(gameTitleId);
+ emulationFragment = new EmulationFragment(launchPath);
getSupportFragmentManager()
.beginTransaction()
.add(R.id.emulation_frame, emulationFragment)
@@ -66,10 +81,18 @@ public class EmulationActivity extends AppCompatActivity {
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
builder.setTitle(R.string.exit_confirmation_title)
.setMessage(R.string.exit_confirm_message)
- .setPositiveButton(R.string.yes, (dialog, which) -> {
- finishAffinity();
- System.exit(0);
- }).setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
+ .setPositiveButton(R.string.yes, (dialog, which) -> quit())
+ .setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel())
+ .create()
+ .show();
+ }
+
+ private void onEmulationError(EmulationError emulationError) {
+ MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
+ builder.setTitle(R.string.error)
+ .setMessage(emulationError.errorMessage())
+ .setNeutralButton(R.string.quit, (dialog, which) -> dialog.dismiss())
+ .setOnDismissListener(dialog -> quit())
.create()
.show();
}
@@ -80,4 +103,17 @@ public class EmulationActivity extends AppCompatActivity {
controller.hide(WindowInsetsCompat.Type.systemBars());
controller.setSystemBarsBehavior(WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
}
+
+ private void quit() {
+ finishAffinity();
+ System.exit(0);
+ }
+
+ @Override
+ public void onChanged(EmulationData emulationData) {
+ if (emulationData.emulationError().isPresent() && !hasEmulationError) {
+ hasEmulationError = true;
+ onEmulationError(emulationData.emulationError().get());
+ }
+ }
}
\ No newline at end of file
diff --git a/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationData.java b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationData.java
new file mode 100644
index 00000000..b77bd791
--- /dev/null
+++ b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationData.java
@@ -0,0 +1,6 @@
+package info.cemu.Cemu.emulation;
+
+import java.util.Optional;
+
+public record EmulationData(Optional emulationError) {
+}
diff --git a/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationError.java b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationError.java
new file mode 100644
index 00000000..d7c5f14f
--- /dev/null
+++ b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationError.java
@@ -0,0 +1,4 @@
+package info.cemu.Cemu.emulation;
+
+public record EmulationError(String errorMessage) {
+}
diff --git a/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationFragment.java
index 54d2a705..29657d4a 100644
--- a/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationFragment.java
+++ b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationFragment.java
@@ -1,5 +1,7 @@
package info.cemu.Cemu.emulation;
+import static androidx.core.app.ActivityCompat.finishAffinity;
+
import android.annotation.SuppressLint;
import android.content.res.Configuration;
import android.graphics.SurfaceTexture;
@@ -20,6 +22,9 @@ import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.PopupMenu;
import androidx.fragment.app.Fragment;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import info.cemu.Cemu.NativeLibrary;
import info.cemu.Cemu.R;
@@ -53,6 +58,7 @@ public class EmulationFragment extends Fragment implements PopupMenu.OnMenuItemC
return true;
}
case MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
+ currentPointerId = -1;
NativeLibrary.onTouchUp(x, y, isTV);
return true;
}
@@ -80,10 +86,11 @@ public class EmulationFragment extends Fragment implements PopupMenu.OnMenuItemC
@Override
public void surfaceChanged(@NonNull SurfaceHolder surfaceHolder, int format, int width, int height) {
NativeLibrary.setSurfaceSize(width, height, isMainCanvas);
- if (!surfaceSet) {
- surfaceSet = true;
- NativeLibrary.setSurface(surfaceHolder.getSurface(), isMainCanvas);
+ if (surfaceSet) {
+ return;
}
+ surfaceSet = true;
+ NativeLibrary.setSurface(surfaceHolder.getSurface(), isMainCanvas);
}
@Override
@@ -93,7 +100,7 @@ public class EmulationFragment extends Fragment implements PopupMenu.OnMenuItemC
}
}
- private final long gameTitleId;
+ private final String launchPath;
private boolean isGameRunning;
private SurfaceView padCanvas;
private SurfaceTexture testSurfaceTexture;
@@ -104,9 +111,10 @@ public class EmulationFragment extends Fragment implements PopupMenu.OnMenuItemC
private PopupMenu settingsMenu;
private InputOverlaySurfaceView inputOverlaySurfaceView;
private SensorManager sensorManager;
+ private EmulationViewModel viewModel;
- public EmulationFragment(long gameTitleId) {
- this.gameTitleId = gameTitleId;
+ public EmulationFragment(String launchPath) {
+ this.launchPath = launchPath;
}
InputOverlaySettingsProvider.OverlaySettings overlaySettings;
@@ -221,6 +229,12 @@ public class EmulationFragment extends Fragment implements PopupMenu.OnMenuItemC
return false;
}
+ @Override
+ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ viewModel = new ViewModelProvider(requireActivity()).get(EmulationViewModel.class);
+ }
+
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = FragmentEmulationBinding.inflate(inflater, container, false);
@@ -271,7 +285,7 @@ public class EmulationFragment extends Fragment implements PopupMenu.OnMenuItemC
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
if (!isGameRunning) {
isGameRunning = true;
- NativeLibrary.startGame(gameTitleId);
+ startGame();
}
}
@@ -282,4 +296,24 @@ public class EmulationFragment extends Fragment implements PopupMenu.OnMenuItemC
mainCanvas.setOnTouchListener(new OnSurfaceTouchListener(true));
return binding.getRoot();
}
+
+ private void startGame() {
+ try {
+ NativeLibrary.startGame(launchPath);
+ } catch (NativeLibrary.GameBaseFilesNotFoundException exception) {
+ onEmulationError(getString(R.string.game_not_found));
+ } catch (NativeLibrary.NoDiscKeyException exception) {
+ onEmulationError(getString(R.string.no_disk_key));
+ } catch (NativeLibrary.NoTitleTikException exception) {
+ onEmulationError(getString(R.string.no_title_tik));
+ } catch (NativeLibrary.GameFilesException exception) {
+ onEmulationError(getString(R.string.game_files_unknown_error, launchPath));
+ }
+ }
+
+ private void onEmulationError(String errorMessage) {
+ if (viewModel == null)
+ return;
+ viewModel.setEmulationError(new EmulationError(errorMessage));
+ }
}
\ No newline at end of file
diff --git a/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationViewModel.java b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationViewModel.java
new file mode 100644
index 00000000..686532cd
--- /dev/null
+++ b/src/android/app/src/main/java/info/cemu/Cemu/emulation/EmulationViewModel.java
@@ -0,0 +1,19 @@
+package info.cemu.Cemu.emulation;
+
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import java.util.Optional;
+
+public class EmulationViewModel extends ViewModel {
+ private final MutableLiveData emulationData = new MutableLiveData<>(new EmulationData(Optional.empty()));
+
+ public void setEmulationError(EmulationError emulationError) {
+ emulationData.setValue(new EmulationData(Optional.of(emulationError)));
+ }
+
+ public LiveData getEmulationData() {
+ return emulationData;
+ }
+}
diff --git a/src/android/app/src/main/java/info/cemu/Cemu/gameview/Game.java b/src/android/app/src/main/java/info/cemu/Cemu/gameview/Game.java
index a907b725..6045e907 100644
--- a/src/android/app/src/main/java/info/cemu/Cemu/gameview/Game.java
+++ b/src/android/app/src/main/java/info/cemu/Cemu/gameview/Game.java
@@ -2,26 +2,5 @@ package info.cemu.Cemu.gameview;
import android.graphics.Bitmap;
-public class Game {
- Long titleId;
- String title;
- Bitmap icon;
-
- public Game(Long titleId, String title, Bitmap icon) {
- this.titleId = titleId;
- this.title = title;
- this.icon = icon;
- }
-
- public Bitmap getIcon() {
- return icon;
- }
-
- public Long getTitleId() {
- return titleId;
- }
-
- public String getTitle() {
- return title;
- }
+public record Game(String path, String title, Bitmap icon) {
}
diff --git a/src/android/app/src/main/java/info/cemu/Cemu/gameview/GameAdapter.java b/src/android/app/src/main/java/info/cemu/Cemu/gameview/GameAdapter.java
index 5e74f951..bfd4469b 100644
--- a/src/android/app/src/main/java/info/cemu/Cemu/gameview/GameAdapter.java
+++ b/src/android/app/src/main/java/info/cemu/Cemu/gameview/GameAdapter.java
@@ -25,17 +25,17 @@ public class GameAdapter extends ListAdapter {
public static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback<>() {
@Override
public boolean areItemsTheSame(@NonNull Game oldItem, @NonNull Game newItem) {
- return oldItem.titleId.equals(newItem.titleId);
+ return oldItem.path().equals(newItem.path());
}
@Override
public boolean areContentsTheSame(@NonNull Game oldItem, @NonNull Game newItem) {
- return oldItem.titleId.equals(newItem.titleId);
+ return oldItem.path().equals(newItem.path());
}
};
public interface GameTitleClickAction {
- void action(long titleId);
+ void action(String gamePath);
}
public GameAdapter(GameTitleClickAction gameTitleClickAction) {
@@ -50,7 +50,7 @@ public class GameAdapter extends ListAdapter {
super.submitList(orignalGameList);
return;
}
- super.submitList(orignalGameList.stream().filter(g -> g.title.toLowerCase(Locale.US).contains(this.filterText)).collect(Collectors.toList()));
+ super.submitList(orignalGameList.stream().filter(g -> g.title().toLowerCase(Locale.US).contains(this.filterText)).collect(Collectors.toList()));
}
@NonNull
@@ -64,11 +64,11 @@ public class GameAdapter extends ListAdapter {
public void onBindViewHolder(@NonNull GameAdapter.ViewHolder holder, int position) {
Game game = getItem(position);
if (game != null) {
- holder.icon.setImageBitmap(game.getIcon());
- holder.text.setText(game.getTitle());
+ holder.icon.setImageBitmap(game.icon());
+ holder.text.setText(game.title());
holder.itemView.setOnClickListener(v -> {
- long titleId = game.titleId;
- gameTitleClickAction.action(titleId);
+ String gamePath = game.path();
+ gameTitleClickAction.action(gamePath);
});
}
}
@@ -82,7 +82,7 @@ public class GameAdapter extends ListAdapter {
super.submitList(orignalGameList);
return;
}
- super.submitList(orignalGameList.stream().filter(g -> g.title.toLowerCase(Locale.US).contains(this.filterText)).collect(Collectors.toList()));
+ super.submitList(orignalGameList.stream().filter(g -> g.title().toLowerCase(Locale.US).contains(this.filterText)).collect(Collectors.toList()));
}
public static class ViewHolder extends RecyclerView.ViewHolder {
diff --git a/src/android/app/src/main/java/info/cemu/Cemu/gameview/GameViewModel.java b/src/android/app/src/main/java/info/cemu/Cemu/gameview/GameViewModel.java
index dd7bc79b..45006439 100644
--- a/src/android/app/src/main/java/info/cemu/Cemu/gameview/GameViewModel.java
+++ b/src/android/app/src/main/java/info/cemu/Cemu/gameview/GameViewModel.java
@@ -25,11 +25,11 @@ public class GameViewModel extends ViewModel {
public GameViewModel() {
this.gamesData = new MutableLiveData<>();
- NativeLibrary.setGameTitleLoadedCallback((titleId, title, colors, width, height) -> {
+ NativeLibrary.setGameTitleLoadedCallback((path, title, colors, width, height) -> {
Bitmap icon = null;
if (colors != null)
icon = Bitmap.createBitmap(colors, width, height, Bitmap.Config.ARGB_8888);
- Game game = new Game(titleId, title, icon);
+ Game game = new Game(path, title, icon);
synchronized (GameViewModel.this) {
games.add(game);
gamesData.postValue(new ArrayList<>(games));
diff --git a/src/android/app/src/main/java/info/cemu/Cemu/gameview/GamesFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/gameview/GamesFragment.java
index 9b746830..a621c3c1 100644
--- a/src/android/app/src/main/java/info/cemu/Cemu/gameview/GamesFragment.java
+++ b/src/android/app/src/main/java/info/cemu/Cemu/gameview/GamesFragment.java
@@ -27,7 +27,7 @@ public class GamesFragment extends Fragment {
super.onCreate(savedInstanceState);
gameAdapter = new GameAdapter(titleId -> {
Intent intent = new Intent(getContext(), EmulationActivity.class);
- intent.putExtra(EmulationActivity.GAME_TITLE_ID, titleId);
+ intent.putExtra(EmulationActivity.LAUNCH_PATH, titleId);
startActivity(intent);
});
gameViewModel = new ViewModelProvider(this).get(GameViewModel.class);
diff --git a/src/android/app/src/main/java/info/cemu/Cemu/settings/gamespath/GamePathsFragment.java b/src/android/app/src/main/java/info/cemu/Cemu/settings/gamespath/GamePathsFragment.java
index d267c28f..98edc1a1 100644
--- a/src/android/app/src/main/java/info/cemu/Cemu/settings/gamespath/GamePathsFragment.java
+++ b/src/android/app/src/main/java/info/cemu/Cemu/settings/gamespath/GamePathsFragment.java
@@ -40,31 +40,30 @@ public class GamePathsFragment extends Fragment {
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
folderSelectionLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
- if (result.getResultCode() == RESULT_OK) {
- Intent data = result.getData();
- if (data != null) {
- Uri uri = Objects.requireNonNull(data.getData());
- requireActivity().getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
- DocumentFile documentFile = DocumentFile.fromTreeUri(requireContext(), uri);
- if (documentFile == null) return;
- String gamesPath = documentFile.getUri().toString();
- if (gamesPaths.stream().anyMatch(p -> p.equals(gamesPath))) {
- Toast.makeText(requireContext(), R.string.game_path_already_added, Toast.LENGTH_LONG).show();
- return;
- }
- NativeLibrary.addGamesPath(gamesPath);
- gamesPaths = Stream.concat(Stream.of(gamesPath), gamesPaths.stream()).collect(Collectors.toList());
- gamePathAdapter.submitList(gamesPaths);
- }
+ if (result.getResultCode() != RESULT_OK) {
+ return;
}
+ Intent data = result.getData();
+ if (data == null) {
+ return;
+ }
+ Uri uri = Objects.requireNonNull(data.getData());
+ requireActivity().getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ DocumentFile documentFile = DocumentFile.fromTreeUri(requireContext(), uri);
+ if (documentFile == null) return;
+ String gamesPath = documentFile.getUri().toString();
+ if (gamesPaths.stream().anyMatch(p -> p.equals(gamesPath))) {
+ Toast.makeText(requireContext(), R.string.game_path_already_added, Toast.LENGTH_LONG).show();
+ return;
+ }
+ NativeLibrary.addGamesPath(gamesPath);
+ gamesPaths = Stream.concat(Stream.of(gamesPath), gamesPaths.stream()).collect(Collectors.toList());
+ gamePathAdapter.submitList(gamesPaths);
});
- gamePathAdapter = new GamePathAdapter(new GamePathAdapter.OnRemoveGamePath() {
- @Override
- public void onRemoveGamePath(String path) {
- NativeLibrary.removeGamesPath(path);
- gamesPaths = gamesPaths.stream().filter(p -> !p.equals(path)).collect(Collectors.toList());
- gamePathAdapter.submitList(gamesPaths);
- }
+ gamePathAdapter = new GamePathAdapter(path -> {
+ NativeLibrary.removeGamesPath(path);
+ gamesPaths = gamesPaths.stream().filter(p -> !p.equals(path)).collect(Collectors.toList());
+ gamePathAdapter.submitList(gamesPaths);
});
}
diff --git a/src/android/app/src/main/res/values/strings.xml b/src/android/app/src/main/res/values/strings.xml
index fc5545fb..20d4b87c 100644
--- a/src/android/app/src/main/res/values/strings.xml
+++ b/src/android/app/src/main/res/values/strings.xml
@@ -168,4 +168,10 @@
Remove game path
Game paths
Game path already added
+ Quit
+ Error
+ Unable to launch game\nPath: %1$s
+ Could not decrypt title. Make sure that keys.txt contains the correct disc key for this title.
+ Could not decrypt title because title.tik is missing.
+ Unable to launch game because the base files were not found.
\ No newline at end of file