mirror of
https://github.com/izzy2lost/WeeU.git
synced 2026-07-06 00:19:59 -07:00
Added support for opening titles using file managers
This commit is contained in:
@@ -9,10 +9,10 @@
|
||||
<application
|
||||
android:name="info.cemu.Cemu.CemuApplication"
|
||||
android:allowBackup="true"
|
||||
android:appCategory="game"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:appCategory="game"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
@@ -21,10 +21,31 @@
|
||||
<activity
|
||||
android:name=".emulation.EmulationActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:screenOrientation="userLandscape"
|
||||
tools:ignore="DiscouragedApi" />
|
||||
tools:ignore="DiscouragedApi">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:scheme="content" />
|
||||
<data android:mimeType="*/*" />
|
||||
<data android:host="*" />
|
||||
<data android:pathPattern=".*\\.wud" />
|
||||
<data android:pathPattern=".*\\.wux" />
|
||||
<data android:pathPattern=".*\\.wua" />
|
||||
<data android:pathPattern=".*\\.wuhb" />
|
||||
<data android:pathPattern=".*\\.iso" />
|
||||
<data android:pathPattern=".*\\.elf" />
|
||||
<data android:pathPattern=".*\\.rpx" />
|
||||
<!--
|
||||
TODO?
|
||||
<data android:pathPattern=".*/title.tmd" />
|
||||
-->
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".settings.SettingsActivity"
|
||||
android:exported="false"
|
||||
|
||||
@@ -17,7 +17,7 @@ class AndroidGameTitleLoadedCallback : public GameTitleLoadedCallback
|
||||
{
|
||||
JNIUtils::ScopedJNIENV env;
|
||||
jstring name = env->NewStringUTF(game.name.c_str());
|
||||
jlong titleId = static_cast<const jlong>(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<const jint*>(icon->intColors()));
|
||||
}
|
||||
env->CallVoidMethod(*m_gameTitleLoadedCallbackObj, m_onGameTitleLoadedMID, static_cast<jlong>(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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -4,5 +4,48 @@
|
||||
|
||||
namespace CafeSystemUtils
|
||||
{
|
||||
void startGame(TitleId titleId);
|
||||
};
|
||||
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
|
||||
@@ -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()
|
||||
|
||||
@@ -72,6 +72,8 @@ void GameTitleLoader::titleRefresh(TitleId titleId)
|
||||
Game& game = m_gameInfos[baseTitleId];
|
||||
std::optional<TitleInfo> 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();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
struct Game
|
||||
{
|
||||
std::string name;
|
||||
std::optional<fs::path> 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();
|
||||
|
||||
@@ -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<TitleId>(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<AndroidGameTitleLoadedCallback>(onGameTitleLoadedMID, game_title_loaded_callback));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<EmulationData> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package info.cemu.Cemu.emulation;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public record EmulationData(Optional<EmulationError> emulationError) {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package info.cemu.Cemu.emulation;
|
||||
|
||||
public record EmulationError(String errorMessage) {
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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> emulationData = new MutableLiveData<>(new EmulationData(Optional.empty()));
|
||||
|
||||
public void setEmulationError(EmulationError emulationError) {
|
||||
emulationData.setValue(new EmulationData(Optional.of(emulationError)));
|
||||
}
|
||||
|
||||
public LiveData<EmulationData> getEmulationData() {
|
||||
return emulationData;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
@@ -25,17 +25,17 @@ public class GameAdapter extends ListAdapter<Game, GameAdapter.ViewHolder> {
|
||||
public static final DiffUtil.ItemCallback<Game> 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<Game, GameAdapter.ViewHolder> {
|
||||
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<Game, GameAdapter.ViewHolder> {
|
||||
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<Game, GameAdapter.ViewHolder> {
|
||||
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 {
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
+22
-23
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -168,4 +168,10 @@
|
||||
<string name="remove_game_path">Remove game path</string>
|
||||
<string name="game_paths_settings">Game paths</string>
|
||||
<string name="game_path_already_added">Game path already added</string>
|
||||
<string name="quit">Quit</string>
|
||||
<string name="error">Error</string>
|
||||
<string name="game_files_unknown_error">Unable to launch game\nPath: %1$s</string>
|
||||
<string name="no_disk_key">Could not decrypt title. Make sure that keys.txt contains the correct disc key for this title.</string>
|
||||
<string name="no_title_tik">Could not decrypt title because title.tik is missing.</string>
|
||||
<string name="game_not_found">Unable to launch game because the base files were not found.</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user