package com.ea.ironmonkey import android.annotation.SuppressLint import android.app.ActivityManager import android.app.AlertDialog import android.content.Intent import android.content.IntentFilter import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.content.res.AssetManager import android.content.res.Configuration import android.hardware.SensorManager import android.media.AudioManager import android.view.InputDevice import android.opengl.GLES20 import android.os.Build import android.os.Bundle import android.os.Handler import android.os.PowerManager.WakeLock import android.os.SystemClock import android.util.DisplayMetrics import android.view.Gravity import android.view.KeyEvent import android.view.MotionEvent import android.view.View import android.view.WindowManager import android.view.inputmethod.InputMethodManager import android.widget.FrameLayout import androidx.appcompat.app.AppCompatActivity import androidx.compose.material3.MaterialTheme import androidx.compose.ui.platform.ComposeView import com.ea.EAIO.EAIO import com.ea.EAMIO.StorageDirectory import com.ea.ironmonkey.Log.d import com.ea.ironmonkey.Log.i import com.ea.ironmonkey.Log.setEnable import com.ea.ironmonkey.ObbHelper.getObbFileName import com.ea.ironmonkey.domain.AssetLocationType import com.ea.nimble.ApplicationLifecycle import com.ea.nimble.Global import nfs.mod.mpcore.GameInput import nfs.mod.mpcore.MultiplayerCore.loadCore import nfs.mod.mpcore.MultiplayerCore.loadEmulatedLibapp import nfs.mod.mpcore.SyntheticInputDispatcher import org.fmod.FMODAudioDevice import java.io.File import java.io.IOException import java.io.InputStream import java.util.Locale import java.util.concurrent.TimeUnit import javax.microedition.khronos.egl.EGLConfig import javax.microedition.khronos.opengles.GL10 import androidx.core.net.toUri import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import com.ea.ironmonkey.domain.FSNode import org.apache.http.BuildConfig import kotlin.system.exitProcess class GameActivityMain : AppCompatActivity(), DrawFrameListener { lateinit var accelerometer: Accelerometer private lateinit var gameGLSurfaceView: GameGLSurfaceView private lateinit var gameRenderer: GameRenderer private lateinit var handler: Handler private lateinit var mAd: AlertDialog.Builder private lateinit var mFMODAudioDevice: FMODAudioDevice private lateinit var mFrameLayout: FrameLayout //private lateinit var mOrientationListener: OrientationEventListener private val mResources: MutableMap<*, *>? = null private lateinit var mWakeLock: WakeLock lateinit var runLoop: RunLoop private var splash = SplashScreen(this) private var splashCounter = 0 private var splashDelay: Long = 0 private var splashTimer: Long = 0 private var laststate = 0 private var isSleepModeEnabled = true private var mRotation = 0 // nativeRestoreContext() is a real native call that can block for a // long time (on the current emulated engine, indefinitely - see // ARM64_TRANSLATION_LAYER.md's render-stall root cause: it transitively // reaches NimbleWrapper::InitNimble(), which gets stuck polling an // empty directory). Real hardware keeps this kind of init work off the // GLThread entirely (confirmed via a native trace comparison - the // real device's own version-check activity and its GL calls run on two // separate OS threads). Dispatched to its own background thread here so // onDrawFrame - which MUST keep running every frame to keep rendering - // never blocks on it, instead of calling it synchronously every frame // like the original code did. @Volatile private var restoreContextThreadStarted = false @Volatile private var restoreContextDone = false @Volatile private var restoreContextResult = false // cont.44 DEBUG: fires MultiplayerCore.triggerCarSelectTest() on // demand via `adb shell am broadcast -a nfs.mod.mpcore.TEST_OPEN_CARSELECT // -p com.ea.games.nfs13_arm` - stands in for a future real lobby-overlay // "select car" button, letting the on-demand car_select-opening call be // exercised at an arbitrary moment (not just automatically at boot) for // live testing. See PROGRESS.md cont.44. private val carSelectTestReceiver = object : android.content.BroadcastReceiver() { override fun onReceive(context: android.content.Context?, intent: Intent?) { nfs.mod.mpcore.MultiplayerCore.triggerCarSelectTest() } } // cont.48 DEBUG: fires the EXPERIMENTAL true-direct-jump variant via // `adb shell am broadcast -a nfs.mod.mpcore.TEST_TRUE_DIRECT_CARSELECT // -p com.ea.games.nfs13_arm`. See PROGRESS.md cont.48. private val trueDirectCarSelectTestReceiver = object : android.content.BroadcastReceiver() { override fun onReceive(context: android.content.Context?, intent: Intent?) { nfs.mod.mpcore.MultiplayerCore.triggerTrueDirectCarSelectJump() } } private fun updateRequestedOrientation(i: Int) { } // Keys the SYSTEM must handle, not the game. // // This used to be a flat `= false`, which combined with the callers' // `return !IsSystemKey(keyCode)` meant the activity claimed EVERY key it // ever saw - including volume. That is why changing the volume needed the // notification shade: the keys were being swallowed before Android could // act on them. fun IsSystemKey(i: Int) = when (i) { KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_MUTE -> true else -> false } companion object { private const val DOWNLOAD_PROPERTIES = "downloadcontent/config.properties" const val STATE_ADC_PROCESS: Int = 4 const val STATE_ADC_START: Int = 3 const val STATE_FULL_PROCESS: Int = 6 const val STATE_FULL_START: Int = 5 const val STATE_GAME_START: Int = 8 const val STATE_GOOGLE_DRM: Int = 2 const val STATE_RESTORE_CONTEXT: Int = 7 const val STATE_SPLASH: Int = 0 const val STATE_SPLASH_PROCESS: Int = 1 const val TAG: String = "GameActivityMain" // Нужен в BitmapGraphics (( lateinit var assetManager: AssetManager var mAssetLocationType = AssetLocationType.EXTERNAL var isAssetsReady: Boolean = false private var mAudioManager: AudioManager? = null private var oldState = 0 var isAnyMusicPlaying: Boolean = false @JvmName("isAnyMusicPlaying") @JvmStatic get private set var state: Int = 0 fun isAtLeastAPI(i: Int): Boolean { return Build.VERSION.SDK_INT >= i } @JvmStatic fun GetDeviceName() = Build.MODEL val osVersion: String @JvmStatic get() = Build.VERSION.RELEASE @JvmStatic fun isAmazon() = false @JvmStatic fun GetDefaultLanguage() = Locale.getDefault().toString().substring(0, 2) @JvmStatic fun GetDeviceLocale(): String { val upperCase = Locale.getDefault().toString().replace('_', '-').uppercase(Locale.getDefault()) d(TAG, "GetDeviceLocale locale = $upperCase") return upperCase } @JvmStatic fun GetApplicationVersion() = BuildConfig.VERSION_NAME } fun useAssetsFileSystem(): Boolean { val result = mAssetLocationType != AssetLocationType.EXTERNAL d(TAG, "useAssetsFileSystem() called, mAssetLocationType=$mAssetLocationType, result=$result, thread=${Thread.currentThread().name}") return result } external fun nativeOnCreate() /** * Hooks the fatal signals so a native crash leaves a report behind. Must be * called AFTER loadCore(); see its call site. */ private fun installNativeCrashHandler() { try { val dir = java.io.File(getExternalFilesDir(null), "crashes") val info = packageManager.getPackageInfo(packageName, 0) nativeInstallCrashHandler( dir.absolutePath, "${info.versionName} (${info.longVersionCode}) ${Build.MANUFACTURER} ${Build.MODEL} " + "Android ${Build.VERSION.RELEASE}" ) } catch (e: Throwable) { // Diagnostics must never stop the game from starting - but say so, // otherwise "no crash reports" looks like "no crashes". i(TAG, "installNativeCrashHandler FAILED - native crashes will go unreported: $e") } } /** See crash_handler.cpp. Writes to `dir`; `buildStamp` goes at the top of every report. */ external fun nativeInstallCrashHandler(dir: String, buildStamp: String) external fun nativeOnDestroy() external fun nativeOnMusicPlayerStateChanged() external fun nativeOnOrientationChange(i: Int) external fun nativeOnPause() external fun nativeOnPhysicalKeyDown(keyCode: Int, scanCode: Int) external fun nativeOnPhysicalKeyUp(i: Int, i2: Int) external fun nativeOnPhysicalKeyboardVisibilityChanged(z: Boolean) external fun nativeOnPhysicalNavigationVisibilityChanged(z: Boolean) external fun nativeOnRestart() external fun nativeOnResume() external fun nativeOnStart() external fun nativeOnStop() external fun nativeRestoreContext(): Boolean external fun nativeSurfaceChanged(gl10: GL10?, i: Int, i2: Int) external fun nativeSurfaceCreated(gl10: GL10?, eGLConfig: EGLConfig?) public override fun onCreate(bundle: Bundle?) { setEnable(true) Companion.assetManager = assets i(TAG, "onCreate") super.onCreate(bundle) // Volume keys should move the MUSIC stream - the one the game actually // plays through - rather than whatever Android would pick by default. volumeControlStream = AudioManager.STREAM_MUSIC applyImmersiveMode() handler = Handler() if (!isAtLeastAPI(18) && getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) } if (isAtLeastAPI(28)) { window.attributes.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES window.addFlags(67108864) } window.setFlags(1024, 1024) requestWindowFeature(1) /*mOrientationListener = object : OrientationEventListener(applicationContext) { override fun onOrientationChanged(i: Int) { if (this@GameActivityMain.mRotation == (this@GameActivityMain.window .windowManager.defaultDisplay.rotation .also { this@GameActivityMain.mRotation = it }) ) if (Settings.System.getInt( this@GameActivityMain.contentResolver, "accelerometer_rotation", 0 ) == 1 || !isAtLeastAPI(18) ) { this@GameActivityMain.accelerometer.updateOrientation(this@GameActivityMain.mRotation) this@GameActivityMain.nativeOnOrientationChange(this@GameActivityMain.mRotation) d( TAG, "OrientationEventListener::onOrientationChanged mRotation = " + this@GameActivityMain.mRotation.toString() ) return } d(TAG, "OrientationEventListener::onOrientationChanged rotation disabled!") } } if (mOrientationListener.canDetectOrientation()) { mOrientationListener.enable() }*/ mAudioManager = getSystemService(AUDIO_SERVICE) as AudioManager? isAnyMusicPlaying = mAudioManager!!.isMusicActive() i(TAG, "onCreate() isMusicActive = $isAnyMusicPlaying") mFMODAudioDevice = FMODAudioDevice() d(TAG, "obb.size check: mAssetLocationType before = $mAssetLocationType") if (mAssetLocationType != AssetLocationType.ASSETS) { try { val open2 = getResources().assets.open("obb.size") mAssetLocationType = AssetLocationType.OBB open2.close() d(TAG, "obb.size check: opened OK, mAssetLocationType now OBB") } catch (e2: IOException) { Log.e(TAG, "obb.size check FAILED: " + e2.message) } } else { d(TAG, "obb.size check: skipped, already ASSETS") } val sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager val defaultSensor = sensorManager.getDefaultSensor(1) val rotation = window.getWindowManager().getDefaultDisplay().getRotation() when (rotation) { 0 -> d(TAG, "Orientation: ROTATION_0") 1 -> d(TAG, "Orientation: ROTATION_90") 2 -> d(TAG, "Orientation: ROTATION_180") 3 -> d(TAG, "Orientation: ROTATION_270") } mRotation = rotation updateRequestedOrientation(rotation) accelerometer = Accelerometer(sensorManager, defaultSensor, rotation) gameGLSurfaceView = GameGLSurfaceView(this) // mpcore lives in a separate Gradle module with no dependency on // `app` (app depends on mpcore, not the other way - a reverse // reference here would be circular), so it can't call back into this // Activity by type. Hand it a plain interface implementation instead. GameInput.dispatcher = object : SyntheticInputDispatcher { override fun dispatchTap(x: Float, y: Float) = dispatchSyntheticTap(x, y) } gameRenderer = GameRenderer(this) gameRenderer.setDrawFrameListener(this) gameGLSurfaceView.setRenderer(gameRenderer) runLoop = RunLoop(gameGLSurfaceView) mFrameLayout = FrameLayout(this) mFrameLayout.addView(gameGLSurfaceView) mFrameLayout.addView(buildCarSelectionOverlay()) setContentView(mFrameLayout) // ARM64-only-device prototype (see /ARM64_TRANSLATION_LAYER.md): // fmodex/fmodevent/Nimble/app are all armeabi-v7a-only builds (no // arm64-v8a .so shipped for any of them, confirmed this session - // see jniLibs/) - System.loadLibrary() for them would throw // UnsatisfiedLinkError immediately on an arm64-v8a-only APK. libapp.so // is loaded through mpcore's embedded ARM32 emulation core instead // (see loadCore() below); FMOD/Nimble audio and the real game // render/boot loop are NOT bridged yet - this build proves the // emulation-core load+hook mechanism, not a running game. // // BuildConfig.NATIVE32 (the "native32" Gradle flavor - see // app/build.gradle.kts and the native-arm32-trace-harness plan in // ARM64_TRANSLATION_LAYER.md) restores the ORIGINAL, real loading // path instead: on genuinely 32-bit-capable hardware (the Galaxy // A9), load the real, unmodified armeabi-v7a libraries directly, no // emulation involved - the ground-truth reference build for that // investigation. Never true for the normal (translated) flavor. if (com.ea.games.nfs13_arm.BuildConfig.NATIVE32) { System.loadLibrary("fmodex") System.loadLibrary("fmodevent") System.loadLibrary("c++_shared") System.loadLibrary(Global.NIMBLE_ID) System.loadLibrary("app") } else { loadCore() // Immediately after loadCore(), and not a line earlier: this is // the first moment libmpcore.so is loaded, so it is the first // moment the native symbol exists. Installing it up in onCreate's // opening lines threw UnsatisfiedLinkError, silently leaving every // crash unreported. installNativeCrashHandler() loadEmulatedLibappFromAssets() } // RECEIVER_EXPORTED: needs to be reachable from an external `adb // shell am broadcast` sender (there's no in-app sender for this // debug-only trigger), and ContextCompat handles the API 33+ // required-flag requirement transparently across minSdk 27. androidx.core.content.ContextCompat.registerReceiver( this, carSelectTestReceiver, IntentFilter("nfs.mod.mpcore.TEST_OPEN_CARSELECT"), androidx.core.content.ContextCompat.RECEIVER_EXPORTED ) androidx.core.content.ContextCompat.registerReceiver( this, trueDirectCarSelectTestReceiver, IntentFilter("nfs.mod.mpcore.TEST_TRUE_DIRECT_CARSELECT"), androidx.core.content.ContextCompat.RECEIVER_EXPORTED ) d(TAG, "Init EAIO/EAMIO") EAIO.Startup(this) StorageDirectory.Startup(this) ApplicationLifecycle.onActivityCreate(bundle, this) d(TAG, "nativeOnCreate") nativeOnCreate() } // ARM64-only-device prototype (see /ARM64_TRANSLATION_LAYER.md): copies // the bundled armeabi-v7a libapp.so (assets/native_probe/libapp.so - a // raw asset, not jniLibs, since this app declares only arm64-v8a) to a // real file the first time, then hands mpcore's embedded ARM32 // emulation core the path. Idempotent - skips the copy if the file // already exists with a plausible size. private fun loadEmulatedLibappFromAssets() { val outFile = File(filesDir, "libapp_armeabi_v7a.so") try { if (!outFile.exists() || outFile.length() < 1_000_000L) { assets.open("native_probe/libapp.so").use { input -> outFile.outputStream().use { output -> input.copyTo(output) } } d(TAG, "loadEmulatedLibappFromAssets: extracted asset to " + outFile.absolutePath) } // The game's own ARM32 FMOD (task #67). Shipped as raw assets for the // same reason libapp.so is: this APK declares only arm64-v8a, so // jniLibs/armeabi-v7a is never packaged. The engine loads them as // secondary guest images so libapp's FMOD imports reach real code // instead of fmod_shims.cpp's no-ops. for (lib in arrayOf("libfmodex.so", "libfmodevent.so")) { val f = File(filesDir, lib) if (!f.exists() || f.length() < 1_000L) { assets.open("native_probe/" + lib).use { input -> f.outputStream().use { output -> input.copyTo(output) } } d(TAG, "loadEmulatedLibappFromAssets: extracted " + f.absolutePath) } } val ok = loadEmulatedLibapp(outFile.absolutePath) d(TAG, "loadEmulatedLibappFromAssets: loadEmulatedLibapp -> " + ok) } catch (e: IOException) { d(TAG, "loadEmulatedLibappFromAssets: failed to extract/load asset: " + e) } } fun forEach(input: String?): Array { return arrayOf(input) } fun getAssetSize(str: String): Int { d(TAG, "getAssetSize filename = " + str) val file = File(str) val parent = file.getParent() if (parent == null) { if (mResources!!.containsKey(str)) { d(TAG, "getAssetSize mResources contains $str") return -1 } d(TAG, "getAssetSize mResources not contains $str") } else { d(TAG, "getAssetSize path = $parent") if (mResources!!.containsKey(parent)) { d(TAG, "getAssetSize mResources contains $parent") val map = mResources[parent] as MutableMap<*, *>? val name = file.getName() d(TAG, "getAssetSize name = $name") if (map!!.containsKey(name)) { d(TAG, "getAssetSize dir contains $name") val fSNode = map[name] as FSNode val i = if (fSNode.directory) -1 else fSNode.size d( TAG, "getAssetSize filename = " + str + ", directory = " + fSNode.directory.toString() + ", size = " + fSNode.size.toString() ) return i } d(TAG, "getAssetSize dir not contains $name") } else { d(TAG, "getAssetSize mResources not contains $parent") } } return -2 } val isObbAssets: Boolean get() = mAssetLocationType == AssetLocationType.OBB val isFullApkAssets: Boolean get() = mAssetLocationType == AssetLocationType.ASSETS private val versionCode: Int get() { try { return getPackageManager().getPackageInfo(getPackageName(), 0).versionCode } catch (e: PackageManager.NameNotFoundException) { e.printStackTrace() return 0 } } val obbFullPath: String get() { i( javaClass.getName(), obbDir.toString() + "/" + getObbFileName(this, versionCode) ) return obbDir.toString() + "/" + getObbFileName(this, versionCode) } val assetManager: AssetManager? get() = getResources().getAssets() fun enableSleepMode() { if (isSleepModeEnabled) { return } isSleepModeEnabled = true wakeLockRelease() } fun disableSleepMode() { if (isSleepModeEnabled) { isSleepModeEnabled = false wakeLockAcquire() } } private fun checkAnyMusicActive() { if (mAudioManager != null) { isAnyMusicPlaying = mAudioManager!!.isMusicActive() nativeOnMusicPlayerStateChanged() } } public override fun onStart() { CallGC() super.onStart() wakeLockAcquire() nativeOnStart() ApplicationLifecycle.onActivityStart(this) } public override fun onRestart() { i(TAG, "onRestart") super.onRestart() ApplicationLifecycle.onActivityRestart(this) nativeOnRestart() } public override fun onPause() { i(TAG, "onPause state=$state") super.onPause() mFMODAudioDevice.stop() gameGLSurfaceView.onPause() ApplicationLifecycle.onActivityPause(this) nativeOnPause() } private fun ForceHideVirtualKeyboard() { val currentFocus = getCurrentFocus() if (currentFocus != null) { (getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow( currentFocus.getWindowToken(), 0 ) } window.setSoftInputMode(3) } public override fun onResume() { i(TAG, "onResume") super.onResume() if (state != 7) { oldState = state state = 7 gameRenderer.setDrawFrameListener(this) } gameGLSurfaceView.onResume() ApplicationLifecycle.onActivityResume(this) nativeOnResume() checkAnyMusicActive() mFMODAudioDevice.start() i(TAG, "onResume() isMusicActive = $isAnyMusicPlaying") } public override fun onStop() { i(TAG, "onStop") super.onStop() wakeLockRelease() nativeOnStop() } public override fun onDestroy() { i(TAG, "onDestroy") unregisterReceiver(carSelectTestReceiver) unregisterReceiver(trueDirectCarSelectTestReceiver) super.onDestroy() if (state == 8) { ApplicationLifecycle.onActivityDestroy(this) nativeOnDestroy() } StorageDirectory.Shutdown() EAIO.Shutdown() exitProcess(0) } @SuppressLint("InvalidWakeLockTag") fun wakeLockAcquire() { if (isSleepModeEnabled) return if (mWakeLock.isHeld) return mWakeLock.acquire(10*60*1000L /*10 minutes*/) } fun wakeLockRelease() { if (isSleepModeEnabled) return if (mWakeLock.isHeld) return mWakeLock.release() } override fun onSaveInstanceState(bundle: Bundle) { super.onSaveInstanceState(bundle) ApplicationLifecycle.onActivitySaveInstanceState(bundle, this) } public override fun onActivityResult(i: Int, i2: Int, intent: Intent?) { super.onActivityResult(i, i2, intent) ApplicationLifecycle.onActivityResult(i, i2, intent, this) } override fun onKeyDown(keyCode: Int, keyEvent: KeyEvent): Boolean { super.onKeyDown(keyCode, keyEvent) // Checked BEFORE the state gate on purpose: the gate returns true for // every key whenever the game is not in STATE_GAME_START, which would // otherwise keep swallowing volume through the whole load. if (IsSystemKey(keyCode)) return false if (state != 8) { return true } if ((keyCode == 4 || keyCode == 108) && keyEvent.repeatCount > 0) { return true } val scanCode = keyEvent.scanCode gameGLSurfaceView.queueEvent { nativeOnPhysicalKeyDown(keyCode, scanCode) } return !IsSystemKey(keyCode) } override fun onKeyUp(i: Int, keyEvent: KeyEvent): Boolean { super.onKeyUp(i, keyEvent) if (IsSystemKey(i)) return false if (state != 8) { return true } val scanCode = keyEvent.scanCode gameGLSurfaceView.queueEvent { this@GameActivityMain.nativeOnPhysicalKeyUp(i, scanCode) } return !IsSystemKey(i) } @Deprecated("Deprecated in Java") override fun onBackPressed() { //super.onBackPressed() ApplicationLifecycle.onBackPressed() } override fun onConfigurationChanged(configuration: Configuration) { d(TAG, "onConfigurationChanged($configuration)") super.onConfigurationChanged(configuration) val rotation = window.windowManager.defaultDisplay.rotation when (rotation) { 0 -> i(TAG, "Orientation: ROTATION_0") 1 -> i(TAG, "Orientation: ROTATION_90") 2 -> i(TAG, "Orientation: ROTATION_180") 3 -> i(TAG, "Orientation: ROTATION_270") } } /** * Hides the status and navigation bars for the duration of the game. * * Nothing did this before, which is why the navigation bar sat on top of * the game. TRANSIENT_BARS_BY_SWIPE keeps them reachable - a swipe from the * edge brings them back temporarily - so this hides the bars without taking * the system away from the player. * * Must be re-applied on every focus gain: Android restores the bars after a * dialog, the shade, or a task switch, and a one-shot call in onCreate * silently stops working the first time any of those happens. */ private fun applyImmersiveMode() { WindowCompat.setDecorFitsSystemWindows(window, false) WindowInsetsControllerCompat(window, window.decorView).apply { hide(WindowInsetsCompat.Type.systemBars()) systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE } } override fun onWindowFocusChanged(z: Boolean) { if (z) applyImmersiveMode() super.onWindowFocusChanged(z) i(TAG, "onWindowsFocusChanged($z) state=$state") getGameGLSurfaceView().renderMode = if (z) 1 else 0 if (!z) { if (mFMODAudioDevice.isMixing) { mFMODAudioDevice.stop() checkAnyMusicActive() } i(TAG, "onWindowFocusChanged() isMusicActive = $isAnyMusicPlaying") ForceHideVirtualKeyboard() nativeOnPhysicalKeyDown(131, 0) nativeOnPhysicalKeyUp(131, 0) if (state == 8) { oldState = state state = 7 gameRenderer.setDrawFrameListener(this) } } else { if (!mFMODAudioDevice.isMixing) { checkAnyMusicActive() mFMODAudioDevice.start() } i(TAG, "onWindowFocusChanged() isMusicActive = $isAnyMusicPlaying") } ApplicationLifecycle.onActivityWindowFocusChanged(z, this) } fun getGameGLSurfaceView(): GameGLSurfaceView { return gameGLSurfaceView } // Synthesizes a tap (ACTION_DOWN + ACTION_UP) directly into the game's // own GLSurfaceView, exactly as if the player had touched the screen at // (x, y). Used by mpcore's GameEvents-driven automation (see // ANALYSIS/PROGRESS.md - "trigger the native car-select Flow screen // without needing FlowManager's internal API") to replay a known-working // tap sequence instead of reverse-engineering the transition call. // No special permission needed: this dispatches into our own view tree, // it isn't a system-wide input injection. fun dispatchSyntheticTap(x: Float, y: Float) { i(TAG, "dispatchSyntheticTap: ($x, $y) on $gameGLSurfaceView") val downTime = SystemClock.uptimeMillis() val down = MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, x, y, 0) // GameGLSurfaceView.onTouchEvent() branches on event.getSource() (see // its processTouchScreenEvent/processTouchPadEvent split) - a // MotionEvent.obtain()'d event defaults to SOURCE_UNKNOWN (0), which // matches neither branch, so the View layer happily "consumes" it // (dispatchTouchEvent returns true) while the native touch handler // never actually runs. Must set this explicitly. down.source = InputDevice.SOURCE_TOUCHSCREEN val downConsumed = gameGLSurfaceView.dispatchTouchEvent(down) i(TAG, "dispatchSyntheticTap: ACTION_DOWN consumed=$downConsumed") down.recycle() handler.postDelayed({ val upTime = SystemClock.uptimeMillis() val up = MotionEvent.obtain(downTime, upTime, MotionEvent.ACTION_UP, x, y, 0) up.source = InputDevice.SOURCE_TOUCHSCREEN gameGLSurfaceView.dispatchTouchEvent(up) up.recycle() }, 60) } fun GetViewRoot() = window.decorView.getRootView().parent fun CallGC() { d(TAG, "Call garbage collector") System.gc() } fun ShowMessage(str: String?, strArr: Array, z: Boolean) { d(TAG, "ShowMessage msg=$str finish=$z") mAd = AlertDialog.Builder(this) mAd.setMessage(str) mAd.setCancelable(false) mAd.setPositiveButton(strArr[0]) { dialogInterface, i -> if (z) { this@GameActivityMain.finish() } } handler.postDelayed({ this@GameActivityMain.mAd.show() }, 20L) } override fun onLowMemory() { val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager val memoryInfo = ActivityManager.MemoryInfo() activityManager.getMemoryInfo(memoryInfo) super.onLowMemory() } fun GetNaturalOrientation(): Int { val width: Int val height: Int val defaultDisplay = window.windowManager.defaultDisplay when (defaultDisplay.rotation) { 0, 2 -> { width = defaultDisplay.getWidth() height = defaultDisplay.getHeight() } 1, 3 -> { width = defaultDisplay.getHeight() height = defaultDisplay.getWidth() } else -> { height = 0 width = 0 } } if (width > height) { d(TAG, "NaturalOrientation = LANDSCAPE") return 0 } d(TAG, "NaturalOrientation = PORTRAIT") return 1 } val displayMetrics: DisplayMetrics? get() { val displayMetrics = DisplayMetrics() windowManager.getDefaultDisplay().getRealMetrics(displayMetrics) return displayMetrics } // Example UI-layer consumer for GameEvents.onCarSelected (PROGRESS.md // cont.63b) - a small ComposeView overlay, top-start corner, drawn above // the game's own GLSurfaceView. Registers CarSelectionState as a // GameEventListener so it starts updating as soon as the player // confirms a car; the badge itself only appears once that first fires. private fun buildCarSelectionOverlay(): ComposeView { registerCarSelectionListener() return ComposeView(this).apply { layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.TOP or Gravity.START, ).apply { setMargins(24, 24, 24, 24) } setContent { MaterialTheme { CarSelectionBadge(selection = CarSelectionState.current) } } } } override fun setContentView(view: View) { d(TAG, "setContentView($view)") if (view === mFrameLayout) { super.setContentView(view) return } val childCount = mFrameLayout.childCount for (i in 0.. { //splash = SplashScreen(this) splash.init( gl10, gameRenderer.width, gameRenderer.height ) state = STATE_SPLASH_PROCESS splashCounter = 3 } STATE_SPLASH_PROCESS -> { splashTimer = System.currentTimeMillis() + 2000 state = STATE_GAME_START } STATE_RESTORE_CONTEXT -> { if (splashDelay < System.currentTimeMillis() && hasWindowFocus()) { state = if (oldState == STATE_GAME_START) { splashTimer = System.currentTimeMillis() + 2000 STATE_GAME_START } else { oldState } } } STATE_GAME_START -> { if (splashTimer < System.currentTimeMillis()) { if (!restoreContextThreadStarted) { restoreContextThreadStarted = true d(TAG, "nativeRestoreContext: starting background thread from tid=" + Thread.currentThread().id) Thread { d(TAG, "nativeRestoreContext: background thread running, tid=" + Thread.currentThread().id) restoreContextResult = nativeRestoreContext() restoreContextDone = true d(TAG, "nativeRestoreContext: background thread finished, result=$restoreContextResult") }.apply { isDaemon = true }.start() } if (restoreContextDone && restoreContextResult) { isAssetsReady = true nativeOnStart() nativeOnResume() gameRenderer.setDrawFrameListener(null) shouldCleanupSplash = true } } } } // Очистка экрана GLES20.glClearColor(1.0f, 0.0f, 0.0f, 1.0f) //GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT or GLES20.GL_DEPTH_BUFFER_BIT) // Отрисовка сплеш-скрина если он существует splash.draw( gl10, gameRenderer.width, gameRenderer.height ) // Очистка сплеш-скрина если нужно /*if (shouldCleanupSplash) { splash.destroy(gl10) }*/ } val totalMemory: Int get() = 40000 fun openURL(str: String) { val replace = str.replace("http://", "https://") startActivity(Intent("android.intent.action.VIEW", replace.toUri())) } val performanceScore: Float get() = 6.6f fun needInstallWallpaper(): Boolean { i(TAG, "needInstallWallpaper()") return false } fun installWallpaper() {} val utcTime: Long get() = TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()) val notchesBoundingRects: Array get() { if (Build.VERSION.SDK_INT < 28) { return emptyArray() } val displayCutout = window.decorView.rootWindowInsets?.displayCutout if (displayCutout == null) { return emptyArray() } val boundingRects = displayCutout.boundingRects return boundingRects.map { rect -> intArrayOf(rect.left, rect.top, rect.width(), rect.height()) }.toTypedArray() } }