forked from MeloNX/MeloNX
move game view to new activity
This commit is contained in:
parent
8c0bd460d9
commit
83c9e4fcb2
@ -75,6 +75,12 @@ tasks.named("preBuild") {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
implementation 'com.google.android.material:material:1.9.0'
|
||||
implementation platform('androidx.compose:compose-bom:2023.03.00')
|
||||
implementation platform('androidx.compose:compose-bom:2023.03.00')
|
||||
androidTestImplementation platform('androidx.compose:compose-bom:2023.03.00')
|
||||
androidTestImplementation platform('androidx.compose:compose-bom:2023.03.00')
|
||||
runtimeOnly project(":libryujinx")
|
||||
implementation 'androidx.core:core-ktx:1.10.1'
|
||||
implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0')
|
||||
|
@ -2,7 +2,10 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature android:name="android.hardware.audio.output" android:required="true" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.audio.output"
|
||||
android:required="true" />
|
||||
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
@ -12,23 +15,29 @@
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:appCategory="game"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:isGame="true"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:largeHeap="true"
|
||||
android:appCategory="game"
|
||||
android:theme="@style/Theme.RyujinxAndroid"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".GameActivity"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|uiMode"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/title_activity_game"
|
||||
android:theme="@style/Theme.RyujinxAndroid" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:hardwareAccelerated="false"
|
||||
android:configChanges="density|fontScale|keyboard|keyboardHidden|layoutDirection|locale|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"
|
||||
android:theme="@style/Theme.RyujinxAndroid">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
@ -225,3 +225,9 @@ void debug_break(int code){
|
||||
if(code >= 3)
|
||||
int r = 0;
|
||||
}
|
||||
|
||||
extern "C"
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_ryujinx_android_NativeHelpers_setTurboMode(JNIEnv *env, jobject thiz, jboolean enable) {
|
||||
adrenotools_set_turbo(enable);
|
||||
}
|
||||
|
@ -0,0 +1,346 @@
|
||||
package org.ryujinx.android
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.PointerEventType
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import compose.icons.CssGgIcons
|
||||
import compose.icons.cssggicons.ToolbarBottom
|
||||
import org.ryujinx.android.ui.theme.RyujinxAndroidTheme
|
||||
import org.ryujinx.android.viewmodels.MainViewModel
|
||||
import org.ryujinx.android.viewmodels.QuickSettings
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class GameActivity : ComponentActivity() {
|
||||
private var physicalControllerManager: PhysicalControllerManager = PhysicalControllerManager(this)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
MainActivity.mainViewModel!!.physicalControllerManager = physicalControllerManager
|
||||
setContent {
|
||||
RyujinxAndroidTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
GameView(mainViewModel = MainActivity.mainViewModel!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
|
||||
event?.apply {
|
||||
if(physicalControllerManager.onKeyEvent(this))
|
||||
return true
|
||||
}
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean {
|
||||
ev?.apply {
|
||||
physicalControllerManager.onMotionEvent(this)
|
||||
}
|
||||
return super.dispatchGenericMotionEvent(ev)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
|
||||
NativeHelpers().setTurboMode(false)
|
||||
force60HzRefreshRate(false)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
setFullScreen(true)
|
||||
NativeHelpers().setTurboMode(true)
|
||||
force60HzRefreshRate(true)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
|
||||
NativeHelpers().setTurboMode(false)
|
||||
force60HzRefreshRate(false)
|
||||
}
|
||||
|
||||
private fun force60HzRefreshRate(enable : Boolean) {
|
||||
// Hack for MIUI devices since they don't support the standard Android APIs
|
||||
try {
|
||||
val setFpsIntent = Intent("com.miui.powerkeeper.SET_ACTIVITY_FPS")
|
||||
setFpsIntent.putExtra("package_name", "org.ryujinx.android")
|
||||
setFpsIntent.putExtra("isEnter", enable)
|
||||
sendBroadcast(setFpsIntent)
|
||||
} catch (_ : Exception) {
|
||||
}
|
||||
|
||||
if (enable)
|
||||
display?.supportedModes?.minByOrNull { abs(it.refreshRate - 60f) }?.let { window.attributes.preferredDisplayModeId = it.modeId }
|
||||
else
|
||||
display?.supportedModes?.maxByOrNull { it.refreshRate }?.let { window.attributes.preferredDisplayModeId = it.modeId }
|
||||
}
|
||||
|
||||
private fun setFullScreen(fullscreen: Boolean) {
|
||||
requestedOrientation =
|
||||
if (fullscreen) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_FULL_USER
|
||||
|
||||
val insets = WindowCompat.getInsetsController(window, window.decorView)
|
||||
|
||||
insets.apply {
|
||||
if (fullscreen) {
|
||||
insets.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
|
||||
insets.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
} else {
|
||||
insets.show(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
|
||||
insets.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun GameView(mainViewModel: MainViewModel) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
factory = { context ->
|
||||
GameHost(context, mainViewModel)
|
||||
}
|
||||
)
|
||||
GameOverlay(mainViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun GameOverlay(mainViewModel: MainViewModel) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
GameStats(mainViewModel)
|
||||
|
||||
val ryujinxNative = RyujinxNative()
|
||||
|
||||
val showController = remember {
|
||||
mutableStateOf(QuickSettings(mainViewModel.activity).useVirtualController)
|
||||
}
|
||||
val enableVsync = remember {
|
||||
mutableStateOf(QuickSettings(mainViewModel.activity).enableVsync)
|
||||
}
|
||||
val showMore = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
// touch surface
|
||||
Surface(color = Color.Transparent, modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(0.dp)
|
||||
.pointerInput(Unit) {
|
||||
awaitPointerEventScope {
|
||||
while (true) {
|
||||
val event = awaitPointerEvent()
|
||||
if (!showController.value)
|
||||
continue
|
||||
|
||||
val change = event
|
||||
.component1()
|
||||
.firstOrNull()
|
||||
change?.apply {
|
||||
val position = this.position
|
||||
|
||||
when (event.type) {
|
||||
PointerEventType.Press -> {
|
||||
ryujinxNative.inputSetTouchPoint(
|
||||
position.x.roundToInt(),
|
||||
position.y.roundToInt()
|
||||
)
|
||||
}
|
||||
|
||||
PointerEventType.Release -> {
|
||||
ryujinxNative.inputReleaseTouchPoint()
|
||||
|
||||
}
|
||||
|
||||
PointerEventType.Move -> {
|
||||
ryujinxNative.inputSetTouchPoint(
|
||||
position.x.roundToInt(),
|
||||
position.y.roundToInt()
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
}
|
||||
GameController.Compose(mainViewModel)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(8.dp)
|
||||
) {
|
||||
IconButton(modifier = Modifier.padding(4.dp), onClick = {
|
||||
showMore.value = true
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = CssGgIcons.ToolbarBottom,
|
||||
contentDescription = "Open Panel"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if(showMore.value){
|
||||
Popup(alignment = Alignment.BottomCenter, onDismissRequest = {showMore.value = false}) {
|
||||
Surface(modifier = Modifier.padding(16.dp),
|
||||
shape = MaterialTheme.shapes.medium) {
|
||||
Row(modifier = Modifier.padding(8.dp)) {
|
||||
IconButton(modifier = Modifier.padding(4.dp), onClick = {
|
||||
showMore.value = false
|
||||
showController.value = !showController.value
|
||||
mainViewModel.controller?.setVisible(showController.value)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.videoGame(),
|
||||
contentDescription = "Toggle Virtual Pad"
|
||||
)
|
||||
}
|
||||
IconButton(modifier = Modifier.padding(4.dp), onClick = {
|
||||
showMore.value = false
|
||||
enableVsync.value = !enableVsync.value
|
||||
RyujinxNative().graphicsRendererSetVsync(enableVsync.value)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.vSync(),
|
||||
tint = if(enableVsync.value) Color.Green else Color.Red,
|
||||
contentDescription = "Toggle VSync"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val showBackNotice = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
showBackNotice.value = true
|
||||
}
|
||||
|
||||
if (showBackNotice.value) {
|
||||
AlertDialog(onDismissRequest = { showBackNotice.value = false }) {
|
||||
Column {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
.wrapContentHeight(),
|
||||
shape = MaterialTheme.shapes.large,
|
||||
tonalElevation = AlertDialogDefaults.TonalElevation
|
||||
) {
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(text = "Are you sure you want to exit the game?")
|
||||
Text(text = "All unsaved data will be lost!")
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Button(onClick = {
|
||||
mainViewModel.closeGame()
|
||||
setFullScreen(false)
|
||||
finishActivity(0)
|
||||
}, modifier = Modifier.padding(16.dp)) {
|
||||
Text(text = "Exit Game")
|
||||
}
|
||||
|
||||
Button(onClick = {
|
||||
showBackNotice.value = false
|
||||
}, modifier = Modifier.padding(16.dp)) {
|
||||
Text(text = "Dismiss")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GameStats(mainViewModel: MainViewModel) {
|
||||
val fifo = remember {
|
||||
mutableStateOf(0.0)
|
||||
}
|
||||
val gameFps = remember {
|
||||
mutableStateOf(0.0)
|
||||
}
|
||||
val gameTime = remember {
|
||||
mutableStateOf(0.0)
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.surface.copy(0.4f)
|
||||
) {
|
||||
Column {
|
||||
var gameTimeVal = 0.0
|
||||
if (!gameTime.value.isInfinite())
|
||||
gameTimeVal = gameTime.value
|
||||
Text(text = "${String.format("%.3f", fifo.value)} %")
|
||||
Text(text = "${String.format("%.3f", gameFps.value)} FPS")
|
||||
Text(text = "${String.format("%.3f", gameTimeVal)} ms")
|
||||
}
|
||||
}
|
||||
|
||||
mainViewModel.setStatStates(fifo, gameFps, gameTime)
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package org.ryujinx.android
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.view.SurfaceHolder
|
||||
@ -7,22 +8,19 @@ import android.view.SurfaceView
|
||||
import org.ryujinx.android.viewmodels.GameModel
|
||||
import org.ryujinx.android.viewmodels.MainViewModel
|
||||
import org.ryujinx.android.viewmodels.QuickSettings
|
||||
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
|
||||
import java.io.File
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback {
|
||||
@SuppressLint("ViewConstructor")
|
||||
class GameHost(context: Context?, private val mainViewModel: MainViewModel) : SurfaceView(context), SurfaceHolder.Callback {
|
||||
private var game: GameModel? = null
|
||||
private var _isClosed: Boolean = false
|
||||
private var _renderingThreadWatcher: Thread? = null
|
||||
private var _height: Int = 0
|
||||
private var _width: Int = 0
|
||||
private var _updateThread: Thread? = null
|
||||
private var nativeInterop: NativeGraphicsInterop? = null
|
||||
private var _guestThread: Thread? = null
|
||||
private var _isInit: Boolean = false
|
||||
private var _isStarted: Boolean = false
|
||||
private var _nativeWindow: Long = 0
|
||||
|
||||
private var _nativeRyujinx: RyujinxNative = RyujinxNative()
|
||||
|
||||
@ -48,6 +46,11 @@ class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceVie
|
||||
_width = width
|
||||
_height = height
|
||||
|
||||
_nativeRyujinx.graphicsRendererSetSize(
|
||||
width,
|
||||
height
|
||||
)
|
||||
|
||||
if(_isStarted)
|
||||
{
|
||||
_nativeRyujinx.inputSetClientSize(width, height)
|
||||
@ -70,7 +73,7 @@ class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceVie
|
||||
private fun start(surfaceHolder: SurfaceHolder) {
|
||||
mainViewModel.gameHost = this
|
||||
if(_isStarted)
|
||||
return;
|
||||
return
|
||||
|
||||
game = mainViewModel.gameModel
|
||||
|
||||
@ -85,7 +88,7 @@ class GameHost(context: Context?, val mainViewModel: MainViewModel) : SurfaceVie
|
||||
mainViewModel.controller?.connect()
|
||||
}
|
||||
|
||||
mainViewModel.activity.physicalControllerManager.connect()
|
||||
mainViewModel.physicalControllerManager?.connect()
|
||||
|
||||
_nativeRyujinx.graphicsRendererSetSize(
|
||||
surfaceHolder.surfaceFrame.width(),
|
||||
|
@ -4,7 +4,6 @@ import android.content.ContentUris
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.DocumentsContract
|
||||
import android.provider.MediaStore
|
||||
@ -12,10 +11,9 @@ import android.provider.MediaStore
|
||||
class Helpers {
|
||||
companion object{
|
||||
fun getPath(context: Context, uri: Uri): String? {
|
||||
val isKitKatorAbove = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT
|
||||
|
||||
// DocumentProvider
|
||||
if (isKitKatorAbove && DocumentsContract.isDocumentUri(context, uri)) {
|
||||
if (DocumentsContract.isDocumentUri(context, uri)) {
|
||||
// ExternalStorageProvider
|
||||
if (isExternalStorageDocument(uri)) {
|
||||
val docId = DocumentsContract.getDocumentId(uri)
|
||||
@ -57,7 +55,7 @@ class Helpers {
|
||||
return null
|
||||
}
|
||||
|
||||
fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array<String>?): String? {
|
||||
private fun getDataColumn(context: Context, uri: Uri?, selection: String?, selectionArgs: Array<String>?): String? {
|
||||
var cursor: Cursor? = null
|
||||
val column = "_data"
|
||||
val projection = arrayOf(column)
|
||||
@ -73,15 +71,15 @@ class Helpers {
|
||||
return null
|
||||
}
|
||||
|
||||
fun isExternalStorageDocument(uri: Uri): Boolean {
|
||||
private fun isExternalStorageDocument(uri: Uri): Boolean {
|
||||
return "com.android.externalstorage.documents" == uri.authority
|
||||
}
|
||||
|
||||
fun isDownloadsDocument(uri: Uri): Boolean {
|
||||
private fun isDownloadsDocument(uri: Uri): Boolean {
|
||||
return "com.android.providers.downloads.documents" == uri.authority
|
||||
}
|
||||
|
||||
fun isMediaDocument(uri: Uri): Boolean {
|
||||
private fun isMediaDocument(uri: Uri): Boolean {
|
||||
return "com.android.providers.media.documents" == uri.authority
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
package org.ryujinx.android
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.KeyboardArrowUp
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
@ -1,39 +1,23 @@
|
||||
package org.ryujinx.android
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.media.AudioDeviceInfo
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.anggrayudi.storage.SimpleStorageHelper
|
||||
import org.ryujinx.android.ui.theme.RyujinxAndroidTheme
|
||||
import org.ryujinx.android.viewmodels.MainViewModel
|
||||
import org.ryujinx.android.viewmodels.VulkanDriverViewModel
|
||||
import org.ryujinx.android.views.HomeViews
|
||||
import org.ryujinx.android.views.MainView
|
||||
import java.io.File
|
||||
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
var physicalControllerManager: PhysicalControllerManager = PhysicalControllerManager(this)
|
||||
private var _isInit: Boolean = false
|
||||
var storageHelper: SimpleStorageHelper? = null
|
||||
companion object {
|
||||
@ -61,60 +45,7 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
external fun getRenderingThreadId() : Long
|
||||
external fun initVm()
|
||||
|
||||
fun setFullScreen(fullscreen: Boolean) {
|
||||
requestedOrientation =
|
||||
if (fullscreen) ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE else ActivityInfo.SCREEN_ORIENTATION_FULL_USER
|
||||
|
||||
val insets = WindowCompat.getInsetsController(window, window.decorView)
|
||||
|
||||
insets.apply {
|
||||
if (fullscreen) {
|
||||
insets.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
|
||||
insets.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
} else {
|
||||
insets.show(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
|
||||
insets.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAudioDevice () : Int {
|
||||
val audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
|
||||
val devices = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
|
||||
|
||||
return if (devices.isEmpty())
|
||||
0
|
||||
else {
|
||||
val speaker = devices.find { it.type == AudioDeviceInfo.TYPE_BUILTIN_SPEAKER }
|
||||
val earPiece = devices.find { it.type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES || it.type == AudioDeviceInfo.TYPE_WIRED_HEADSET }
|
||||
if(earPiece != null)
|
||||
return earPiece.id
|
||||
if(speaker != null)
|
||||
return speaker.id
|
||||
devices.first().id
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
|
||||
event?.apply {
|
||||
if(physicalControllerManager.onKeyEvent(this))
|
||||
return true;
|
||||
}
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
override fun dispatchGenericMotionEvent(ev: MotionEvent?): Boolean {
|
||||
ev?.apply {
|
||||
physicalControllerManager.onMotionEvent(this)
|
||||
}
|
||||
return super.dispatchGenericMotionEvent(ev)
|
||||
}
|
||||
private external fun initVm()
|
||||
|
||||
private fun initialize() {
|
||||
if (_isInit)
|
||||
@ -150,15 +81,6 @@ class MainActivity : ComponentActivity() {
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
/*Box {
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
factory = { context ->
|
||||
GameHost(context)
|
||||
}
|
||||
)
|
||||
controller.Compose(lifecycleScope, lifecycle)
|
||||
}*/
|
||||
MainView.Main(mainViewModel = this)
|
||||
}
|
||||
}
|
||||
@ -176,16 +98,3 @@ class MainActivity : ComponentActivity() {
|
||||
storageHelper?.onRestoreInstanceState(savedInstanceState)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Greeting(name: String, modifier: Modifier = Modifier) {
|
||||
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun GreetingPreview() {
|
||||
RyujinxAndroidTheme {
|
||||
HomeViews.Home()
|
||||
}
|
||||
}
|
@ -1,7 +1,5 @@
|
||||
package org.ryujinx.android
|
||||
|
||||
import android.view.Surface
|
||||
|
||||
class NativeGraphicsInterop {
|
||||
var VkCreateSurface: Long = 0
|
||||
var SurfaceHandle: Long = 0
|
||||
|
@ -9,12 +9,14 @@ class NativeHelpers {
|
||||
System.loadLibrary("ryujinxjni")
|
||||
}
|
||||
}
|
||||
external fun releaseNativeWindow(window:Long) : Unit
|
||||
external fun releaseNativeWindow(window:Long)
|
||||
external fun createSurface(vkInstance:Long, window:Long) : Long
|
||||
external fun getCreateSurfacePtr() : Long
|
||||
external fun getNativeWindow(surface:Surface) : Long
|
||||
external fun attachCurrentThread() : Unit
|
||||
external fun detachCurrentThread() : Unit
|
||||
external fun attachCurrentThread()
|
||||
external fun detachCurrentThread()
|
||||
|
||||
external fun loadDriver(nativeLibPath:String, privateAppsPath:String, driverName:String) : Long
|
||||
|
||||
external fun setTurboMode(enable: Boolean)
|
||||
}
|
@ -4,10 +4,10 @@ import android.os.Build
|
||||
import android.os.PerformanceHintManager
|
||||
import androidx.annotation.RequiresApi
|
||||
|
||||
class PerformanceManager(val performanceHintManager: PerformanceHintManager) {
|
||||
class PerformanceManager(private val performanceHintManager: PerformanceHintManager) {
|
||||
private var _isEnabled: Boolean = false
|
||||
private var renderingSession: PerformanceHintManager.Session? = null
|
||||
val DEFAULT_TARGET_NS = 16666666L
|
||||
private val DEFAULT_TARGET_NS = 16666666L
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.S)
|
||||
fun initializeRenderingSession(threadId : Long){
|
||||
|
@ -3,13 +3,13 @@ package org.ryujinx.android
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
|
||||
class PhysicalControllerManager(val activity: MainActivity) {
|
||||
class PhysicalControllerManager(val activity: GameActivity) {
|
||||
private var controllerId: Int = -1
|
||||
private var ryujinxNative: RyujinxNative = RyujinxNative()
|
||||
|
||||
fun onKeyEvent(event: KeyEvent) : Boolean{
|
||||
if(controllerId != -1) {
|
||||
val id = GetGamePadButtonInputId(event.keyCode)
|
||||
val id = getGamePadButtonInputId(event.keyCode)
|
||||
|
||||
if(id != GamePadButtonInputId.None) {
|
||||
when (event.action) {
|
||||
@ -45,7 +45,7 @@ class PhysicalControllerManager(val activity: MainActivity) {
|
||||
controllerId = ryujinxNative.inputConnectGamepad(0)
|
||||
}
|
||||
|
||||
fun GetGamePadButtonInputId(keycode: Int): GamePadButtonInputId {
|
||||
private fun getGamePadButtonInputId(keycode: Int): GamePadButtonInputId {
|
||||
return when (keycode) {
|
||||
KeyEvent.KEYCODE_BUTTON_A -> GamePadButtonInputId.B
|
||||
KeyEvent.KEYCODE_BUTTON_B -> GamePadButtonInputId.A
|
||||
|
@ -35,18 +35,18 @@ class RyujinxNative {
|
||||
external fun deviceGetGameInfo(fileDescriptor: Int, isXci:Boolean): GameInfo
|
||||
external fun deviceGetGameInfoFromPath(path: String): GameInfo
|
||||
external fun deviceLoadDescriptor(fileDescriptor: Int, isXci:Boolean): Boolean
|
||||
external fun graphicsRendererSetSize(width: Int, height: Int): Unit
|
||||
external fun graphicsRendererSetVsync(enabled: Boolean): Unit
|
||||
external fun graphicsRendererRunLoop(): Unit
|
||||
external fun inputInitialize(width: Int, height: Int): Unit
|
||||
external fun inputSetClientSize(width: Int, height: Int): Unit
|
||||
external fun inputSetTouchPoint(x: Int, y: Int): Unit
|
||||
external fun inputReleaseTouchPoint(): Unit
|
||||
external fun inputUpdate(): Unit
|
||||
external fun inputSetButtonPressed(button: Int, id: Int): Unit
|
||||
external fun inputSetButtonReleased(button: Int, id: Int): Unit
|
||||
external fun graphicsRendererSetSize(width: Int, height: Int)
|
||||
external fun graphicsRendererSetVsync(enabled: Boolean)
|
||||
external fun graphicsRendererRunLoop()
|
||||
external fun inputInitialize(width: Int, height: Int)
|
||||
external fun inputSetClientSize(width: Int, height: Int)
|
||||
external fun inputSetTouchPoint(x: Int, y: Int)
|
||||
external fun inputReleaseTouchPoint()
|
||||
external fun inputUpdate()
|
||||
external fun inputSetButtonPressed(button: Int, id: Int)
|
||||
external fun inputSetButtonReleased(button: Int, id: Int)
|
||||
external fun inputConnectGamepad(index: Int): Int
|
||||
external fun inputSetStickAxis(stick: Int, x: Float, y: Float, id: Int): Unit
|
||||
external fun inputSetStickAxis(stick: Int, x: Float, y: Float, id: Int)
|
||||
external fun graphicsSetSurface(surface: Long)
|
||||
external fun deviceCloseEmulation()
|
||||
external fun deviceSignalEmulationClose()
|
||||
|
@ -2,10 +2,12 @@ package org.ryujinx.android.viewmodels
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.PerformanceHintManager
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.navigation.NavHostController
|
||||
import org.ryujinx.android.GameActivity
|
||||
import org.ryujinx.android.GameController
|
||||
import org.ryujinx.android.GameHost
|
||||
import org.ryujinx.android.GraphicsConfiguration
|
||||
@ -13,6 +15,7 @@ import org.ryujinx.android.MainActivity
|
||||
import org.ryujinx.android.NativeGraphicsInterop
|
||||
import org.ryujinx.android.NativeHelpers
|
||||
import org.ryujinx.android.PerformanceManager
|
||||
import org.ryujinx.android.PhysicalControllerManager
|
||||
import org.ryujinx.android.RegionCode
|
||||
import org.ryujinx.android.RyujinxNative
|
||||
import org.ryujinx.android.SystemLanguage
|
||||
@ -20,6 +23,7 @@ import java.io.File
|
||||
|
||||
@SuppressLint("WrongConstant")
|
||||
class MainViewModel(val activity: MainActivity) {
|
||||
var physicalControllerManager: PhysicalControllerManager? = null
|
||||
var gameModel: GameModel? = null
|
||||
var gameHost: GameHost? = null
|
||||
var controller: GameController? = null
|
||||
@ -44,23 +48,17 @@ class MainViewModel(val activity: MainActivity) {
|
||||
RyujinxNative().deviceSignalEmulationClose()
|
||||
gameHost?.close()
|
||||
RyujinxNative().deviceCloseEmulation()
|
||||
goBack()
|
||||
activity.setFullScreen(false)
|
||||
}
|
||||
|
||||
fun goBack(){
|
||||
navController?.popBackStack()
|
||||
}
|
||||
|
||||
fun loadGame(game:GameModel) : Boolean {
|
||||
var nativeRyujinx = RyujinxNative()
|
||||
val nativeRyujinx = RyujinxNative()
|
||||
|
||||
val descriptor = game.open()
|
||||
|
||||
if(descriptor == 0)
|
||||
return false
|
||||
|
||||
gameModel = game;
|
||||
gameModel = game
|
||||
|
||||
val settings = QuickSettings(activity)
|
||||
|
||||
@ -68,43 +66,46 @@ class MainViewModel(val activity: MainActivity) {
|
||||
EnableShaderCache = settings.enableShaderCache
|
||||
EnableTextureRecompression = settings.enableTextureRecompression
|
||||
ResScale = settings.resScale
|
||||
BackendThreading = org.ryujinx.android.BackendThreading.Auto.ordinal
|
||||
})
|
||||
|
||||
if(!success)
|
||||
return false
|
||||
|
||||
val nativeHelpers = NativeHelpers()
|
||||
var nativeInterop = NativeGraphicsInterop()
|
||||
nativeInterop!!.VkRequiredExtensions = arrayOf(
|
||||
val nativeInterop = NativeGraphicsInterop()
|
||||
nativeInterop.VkRequiredExtensions = arrayOf(
|
||||
"VK_KHR_surface", "VK_KHR_android_surface"
|
||||
)
|
||||
nativeInterop!!.VkCreateSurface = nativeHelpers.getCreateSurfacePtr()
|
||||
nativeInterop!!.SurfaceHandle = 0
|
||||
nativeInterop.VkCreateSurface = nativeHelpers.getCreateSurfacePtr()
|
||||
nativeInterop.SurfaceHandle = 0
|
||||
|
||||
var driverViewModel = VulkanDriverViewModel(activity);
|
||||
var drivers = driverViewModel.getAvailableDrivers()
|
||||
val driverViewModel = VulkanDriverViewModel(activity)
|
||||
val drivers = driverViewModel.getAvailableDrivers()
|
||||
|
||||
var driverHandle = 0L;
|
||||
var driverHandle = 0L
|
||||
|
||||
if (driverViewModel.selected.isNotEmpty()) {
|
||||
var metaData = drivers.find { it.driverPath == driverViewModel.selected }
|
||||
val metaData = drivers.find { it.driverPath == driverViewModel.selected }
|
||||
|
||||
metaData?.apply {
|
||||
var privatePath = activity.filesDir;
|
||||
var privateDriverPath = privatePath.canonicalPath + "/driver/"
|
||||
val privatePath = activity.filesDir
|
||||
val privateDriverPath = privatePath.canonicalPath + "/driver/"
|
||||
val pD = File(privateDriverPath)
|
||||
if (pD.exists())
|
||||
pD.deleteRecursively()
|
||||
|
||||
pD.mkdirs()
|
||||
|
||||
var driver = File(driverViewModel.selected)
|
||||
var parent = driver.parentFile
|
||||
val driver = File(driverViewModel.selected)
|
||||
val parent = driver.parentFile
|
||||
if (parent != null) {
|
||||
for (file in parent.walkTopDown()) {
|
||||
if (file.absolutePath == parent.absolutePath)
|
||||
continue
|
||||
file.copyTo(File(privateDriverPath + file.name), true)
|
||||
}
|
||||
}
|
||||
|
||||
driverHandle = NativeHelpers().loadDriver(
|
||||
activity.applicationInfo.nativeLibraryDir!! + "/",
|
||||
@ -116,7 +117,7 @@ class MainViewModel(val activity: MainActivity) {
|
||||
}
|
||||
|
||||
success = nativeRyujinx.graphicsInitializeRenderer(
|
||||
nativeInterop!!.VkRequiredExtensions!!,
|
||||
nativeInterop.VkRequiredExtensions!!,
|
||||
driverHandle
|
||||
)
|
||||
if(!success)
|
||||
@ -175,6 +176,8 @@ class MainViewModel(val activity: MainActivity) {
|
||||
this.controller = controller
|
||||
}
|
||||
|
||||
fun backCalled() {
|
||||
fun navigateToGame() {
|
||||
val intent = Intent(activity, GameActivity::class.java)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
@ -387,10 +387,7 @@ class HomeViews {
|
||||
viewModel.mainViewModel?.loadGame(gameModel) ?: false
|
||||
if (success) {
|
||||
launchOnUiThread {
|
||||
viewModel.mainViewModel?.activity?.setFullScreen(
|
||||
true
|
||||
)
|
||||
viewModel.mainViewModel?.navController?.navigate("game")
|
||||
viewModel.mainViewModel?.navigateToGame()
|
||||
}
|
||||
} else {
|
||||
gameModel.close()
|
||||
|
@ -1,48 +1,11 @@
|
||||
package org.ryujinx.android.views
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.layout.wrapContentWidth
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.PointerEventType
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import compose.icons.CssGgIcons
|
||||
import compose.icons.cssggicons.ToolbarBottom
|
||||
import org.ryujinx.android.GameController
|
||||
import org.ryujinx.android.GameHost
|
||||
import org.ryujinx.android.Icons
|
||||
import org.ryujinx.android.RyujinxNative
|
||||
import org.ryujinx.android.viewmodels.MainViewModel
|
||||
import org.ryujinx.android.viewmodels.QuickSettings
|
||||
import org.ryujinx.android.viewmodels.SettingsViewModel
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class MainView {
|
||||
companion object {
|
||||
@ -53,7 +16,6 @@ class MainView {
|
||||
|
||||
NavHost(navController = navController, startDestination = "home") {
|
||||
composable("home") { HomeViews.Home(mainViewModel.homeViewModel, navController) }
|
||||
composable("game") { GameView(mainViewModel) }
|
||||
composable("settings") {
|
||||
SettingViews.Main(
|
||||
SettingsViewModel(
|
||||
@ -64,207 +26,5 @@ class MainView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GameView(mainViewModel: MainViewModel) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
AndroidView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
factory = { context ->
|
||||
GameHost(context, mainViewModel)
|
||||
}
|
||||
)
|
||||
GameOverlay(mainViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun GameOverlay(mainViewModel: MainViewModel) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
GameStats(mainViewModel)
|
||||
|
||||
val ryujinxNative = RyujinxNative()
|
||||
|
||||
var showController = remember {
|
||||
mutableStateOf(QuickSettings(mainViewModel.activity).useVirtualController)
|
||||
}
|
||||
var enableVsync = remember {
|
||||
mutableStateOf(QuickSettings(mainViewModel.activity).enableVsync)
|
||||
}
|
||||
var showMore = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
// touch surface
|
||||
Surface(color = Color.Transparent, modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(0.dp)
|
||||
.pointerInput(Unit) {
|
||||
awaitPointerEventScope {
|
||||
while (true) {
|
||||
val event = awaitPointerEvent()
|
||||
if (!showController.value)
|
||||
continue
|
||||
|
||||
val change = event
|
||||
.component1()
|
||||
.firstOrNull()
|
||||
change?.apply {
|
||||
val position = this.position
|
||||
|
||||
when (event.type) {
|
||||
PointerEventType.Press -> {
|
||||
ryujinxNative.inputSetTouchPoint(
|
||||
position.x.roundToInt(),
|
||||
position.y.roundToInt()
|
||||
)
|
||||
}
|
||||
|
||||
PointerEventType.Release -> {
|
||||
ryujinxNative.inputReleaseTouchPoint()
|
||||
|
||||
}
|
||||
|
||||
PointerEventType.Move -> {
|
||||
ryujinxNative.inputSetTouchPoint(
|
||||
position.x.roundToInt(),
|
||||
position.y.roundToInt()
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}) {
|
||||
}
|
||||
GameController.Compose(mainViewModel)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(8.dp)
|
||||
) {
|
||||
IconButton(modifier = Modifier.padding(4.dp), onClick = {
|
||||
showMore.value = true
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = CssGgIcons.ToolbarBottom,
|
||||
contentDescription = "Open Panel"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if(showMore.value){
|
||||
Popup(alignment = Alignment.BottomCenter, onDismissRequest = {showMore.value = false}) {
|
||||
Surface(modifier = Modifier.padding(16.dp),
|
||||
shape = MaterialTheme.shapes.medium) {
|
||||
Row(modifier = Modifier.padding(8.dp)) {
|
||||
IconButton(modifier = Modifier.padding(4.dp), onClick = {
|
||||
showMore.value = false
|
||||
showController.value = !showController.value
|
||||
mainViewModel.controller?.setVisible(showController.value)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.videoGame(),
|
||||
contentDescription = "Toggle Virtual Pad"
|
||||
)
|
||||
}
|
||||
IconButton(modifier = Modifier.padding(4.dp), onClick = {
|
||||
showMore.value = false
|
||||
enableVsync.value = !enableVsync.value
|
||||
RyujinxNative().graphicsRendererSetVsync(enableVsync.value)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.vSync(),
|
||||
tint = if(enableVsync.value) Color.Green else Color.Red,
|
||||
contentDescription = "Toggle VSync"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var showBackNotice = remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
BackHandler {
|
||||
showBackNotice.value = true
|
||||
}
|
||||
|
||||
if (showBackNotice.value) {
|
||||
AlertDialog(onDismissRequest = { showBackNotice.value = false }) {
|
||||
Column {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.wrapContentWidth()
|
||||
.wrapContentHeight(),
|
||||
shape = MaterialTheme.shapes.large,
|
||||
tonalElevation = AlertDialogDefaults.TonalElevation
|
||||
) {
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(text = "Are you sure you want to exit the game?")
|
||||
Text(text = "All unsaved data will be lost!")
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Button(onClick = {
|
||||
mainViewModel.closeGame()
|
||||
}, modifier = Modifier.padding(16.dp)) {
|
||||
Text(text = "Exit Game")
|
||||
}
|
||||
|
||||
Button(onClick = {
|
||||
showBackNotice.value = false
|
||||
}, modifier = Modifier.padding(16.dp)) {
|
||||
Text(text = "Dismiss")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@Composable
|
||||
fun GameStats(mainViewModel: MainViewModel) {
|
||||
val fifo = remember {
|
||||
mutableStateOf(0.0)
|
||||
}
|
||||
val gameFps = remember {
|
||||
mutableStateOf(0.0)
|
||||
}
|
||||
val gameTime = remember {
|
||||
mutableStateOf(0.0)
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = MaterialTheme.colorScheme.surface.copy(0.4f)
|
||||
) {
|
||||
Column {
|
||||
var gameTimeVal = 0.0
|
||||
if (!gameTime.value.isInfinite())
|
||||
gameTimeVal = gameTime.value
|
||||
Text(text = "${String.format("%.3f", fifo.value)} %")
|
||||
Text(text = "${String.format("%.3f", gameFps.value)} FPS")
|
||||
Text(text = "${String.format("%.3f", gameTimeVal)} ms")
|
||||
}
|
||||
}
|
||||
|
||||
mainViewModel.setStatStates(fifo, gameFps, gameTime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user