Author SHA1 Message Date
megboyzz a73f0594e5 try to cut devmenu 2026-07-31 14:44:47 +03:00
megboyzz feb32247ed try to cut devmenu 2026-07-31 14:19:44 +03:00
megboyzz 947ba44245 comeback commit! 2024-12-15 21:21:44 +03:00
31 changed files with 960 additions and 60 deletions
+15 -3
View File
@@ -8,11 +8,22 @@ android {
compileSdk = 34
defaultConfig {
applicationId = "com.ea.games.nfs13_na"
applicationId = "com.ea.games.nfs13_mod"
minSdk = 21
targetSdk = 34
versionCode = 1
versionName = "1.0"
// Matches the real game's own versionCode/versionName (and the
// main.1003128.com.ea.games.nfs13_mod.obb file already on the test
// device) so this build installs as an update rather than tripping
// Android's downgrade protection. NOTE: the claim that the game's own
// code looks up an OBB file via "main.<versionCode>.<package>.obb" is
// UNVERIFIED - grepped the entire launcher source tree (Java/Kotlin/
// XML) and libapp.so for "obb"/"addAssetPath"/"mountObb"/
// "StorageManager" and found zero references anywhere. This value
// just avoids the installer-level downgrade block; it is not known to
// affect asset/cache loading. See ANALYSIS.md for the still-open
// question of how this build actually locates game_cache/published/.
versionCode = 1003128
versionName = "1.3.128"
buildConfigField( "String", "DEV_MENU_VERSION", "\"0.1\"")
buildConfigField( "String", "DEV_MENU_ID", "\"DevMenu\"")
@@ -96,6 +107,7 @@ dependencies {
implementation(libs.ui.graphics)
implementation(libs.ui.tooling.preview)
implementation(libs.material3)
implementation(project(mapOf("path" to ":mpcore")))
testImplementation(libs.junit.v412)
//testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
@@ -24,6 +24,7 @@ import com.ea.EAMIO.StorageDirectory
import com.ea.easp.EASPHandler
import com.ea.games.nfs13_na.BuildConfig
import com.ea.nimble.ApplicationLifecycle
import ru.megboyzz.mpcore.NativeLib
import java.io.BufferedReader
import java.io.File
import java.io.FileReader
@@ -42,6 +43,7 @@ class GameActivity : ComponentActivity(), DrawFrameListener {
init {
System.loadLibrary("nimble")
System.loadLibrary("app")
System.loadLibrary("mpcore")
}
private var splashDelay: Long = 0
Binary file not shown.
+6
View File
@@ -8,6 +8,12 @@ java {
targetCompatibility = JavaVersion.VERSION_17
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "17"
}
}
dependencies {
implementation("javax.inject:javax.inject:1")
testImplementation("junit:junit:4.13.2")
@@ -1,6 +1,6 @@
package com.megboyzz.devmenu.domain.entities
enum class GameLang(private val lang: String) {
enum class GameLang(val lang: String) {
System("sys"),
Chinese("cn"),
Dutch("nl"),
@@ -0,0 +1,14 @@
package com.megboyzz.devmenu.domain.usecases.settings
import com.megboyzz.devmenu.domain.entities.GameLang
import com.megboyzz.devmenu.domain.repository.SettingsRepository
import javax.inject.Inject
class GetGameLangUseCase @Inject constructor(
private val settingsRepository: SettingsRepository
) {
operator fun invoke() = settingsRepository.gameLang
}
@@ -0,0 +1,13 @@
package com.megboyzz.devmenu.domain.usecases.settings
import com.megboyzz.devmenu.domain.entities.TrackingData
import com.megboyzz.devmenu.domain.repository.TrackingRepository
import javax.inject.Inject
class GetTrackingStateUseCase @Inject constructor(
private val trackingRepository: TrackingRepository
) {
operator fun invoke() = trackingRepository.trackingData
}
@@ -8,6 +8,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.bottomSheet.BottomSheetNavigator
import cafe.adriel.voyager.transitions.SlideTransition
import com.megboyzz.devmenu.ui.components.MainScaffold
import com.megboyzz.devmenu.ui.screens.HomeScreen
@@ -39,7 +40,9 @@ class MainActivity : ComponentActivity() {
BottomSheetNavigator(
sheetShape = RoundedCornerShape(topStart = 10.dp, topEnd = 10.dp)
) {
Navigator(HomeScreen(::runGame))
Navigator(HomeScreen(::runGame)) {
SlideTransition(it)
}
}
}
}
@@ -3,6 +3,8 @@ package com.megboyzz.devmenu.di.components
import android.content.Context
import com.megboyzz.devmenu.di.modules.DatabaseModule
import com.megboyzz.devmenu.di.modules.RepositoryModule
import com.megboyzz.devmenu.domain.usecases.settings.GetGameLangUseCase
import com.megboyzz.devmenu.domain.usecases.settings.GetTrackingStateUseCase
import com.megboyzz.devmenu.domain.usecases.settings.RemoveGameDataUseCase
import com.megboyzz.devmenu.domain.usecases.settings.SetGameLangUseCase
import com.megboyzz.devmenu.domain.usecases.settings.SetTrackingSaveFileUseCase
@@ -24,8 +26,12 @@ interface SettingsUseCaseComponent {
val setGameLangUseCase: SetGameLangUseCase
val getGameLangUseCase: GetGameLangUseCase
val setTrackingSaveFileUseCase: SetTrackingSaveFileUseCase
val getTrackingStateUseCase: GetTrackingStateUseCase
val turnOffDevMenuUseCase: TurnOffDevMenuUseCase
@Component.Builder
@@ -29,7 +29,7 @@ class SettingsRepositoryImpl @Inject constructor(
if(!localeFile.exists()){
localeFile.createNewFile()
localeFile.writeBytes(byteArrayOf(0x02, 0x0) + systemLang.name.toByteArray())
localeFile.writeBytes(byteArrayOf(0x02, 0x0) + systemLang.lang.toByteArray())
return systemLang
}
@@ -40,7 +40,7 @@ class SettingsRepositoryImpl @Inject constructor(
if(!localeFile.exists()) localeFile.createNewFile()
localeFile.writeBytes(byteArrayOf(0x02, 0x0) + value.name.toByteArray())
localeFile.writeBytes(byteArrayOf(0x02, 0x0) + value.lang.toByteArray())
}
@@ -29,7 +29,7 @@ class TrackingRepositoryImpl @Inject constructor(
override var trackingData: TrackingData
get() {
val enabled = sharedPreferences.getBoolean(trackingStateKey, false)
val path = sharedPreferences.getString(trackingStateKey, defaultPathToSave)?: defaultPathToSave
val path = sharedPreferences.getString(trackingSavePath, defaultPathToSave)?: defaultPathToSave
return TrackingData(
enabled = enabled,
@@ -1,14 +1,19 @@
package com.megboyzz.devmenu.ui
import android.annotation.SuppressLint
import android.content.Context
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import com.megboyzz.devmenu.MainActivity
import java.text.CharacterIterator
import java.text.SimpleDateFormat
import java.text.StringCharacterIterator
@@ -46,7 +51,39 @@ fun humanReadableByteCountSI(bytes: Long): String {
fun formatDate(date: Date) = SimpleDateFormat("dd.MM.yyyy HH:mm:ss").format(date)
fun Context.getInternalAppDataDirectory() = "/data/data/${packageName}"
fun Context.getExternalAppDataDirectory(): String {
val externalDir = getExternalFilesDir(null)
return externalDir?.absolutePath ?: ""
}
@Composable
fun Context.registerOnBackPressedAction(
action: () -> Unit
) {
val context = this
DisposableEffect(this) {
val callback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
action()
}
}
(context as ComponentActivity).onBackPressedDispatcher.addCallback(callback)
// Отменяем обратный вызов при уничтожении DisposableEffect
onDispose {
callback.remove()
}
}
}
/**
* ## Неработает((
*
* Возвращает лямбду того типа которая требуется в Composable контексте<br>
* например есть функция
*
@@ -62,5 +99,4 @@ fun formatDate(date: Date) = SimpleDateFormat("dd.MM.yyyy HH:mm:ss").format(date
*
* example(a = SomeType().asComposableLambda)
*/
val <T> T.asComposableLambda: @Composable () -> T
get() = { this }
val <T> T.asComposableLambda: @Composable () -> T get() = { this }
@@ -6,6 +6,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.megboyzz.devmenu.ui.models.FsElementModel
@@ -13,7 +14,7 @@ import com.megboyzz.devmenu.ui.models.FsElementModel
fun FsElement(
fsElementEntity: FsElementModel,
onClick: () -> Unit,
onLongPressClick: () -> Unit
onLongPressClick: () -> Unit = {}
) {
Column(
@@ -25,6 +26,9 @@ fun FsElement(
)
) {
FsElementIcon(type = fsElementEntity.type)
Text(text = fsElementEntity.name)
Text(
text = fsElementEntity.name,
textAlign = TextAlign.Center
)
}
}
@@ -1,7 +1,6 @@
package com.megboyzz.devmenu.ui.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
@@ -39,10 +38,20 @@ fun CoreBar(folders: List<String>) {
@Composable
fun StatusBarInCurFolder(
path: String
path: String,
internalPath: String = "",
externalPath: String = ""
) {
if(path.isEmpty()) return
val pathAsArray = path.split("/").filter { it.isNotEmpty() }
var pathProcess = path
if(internalPath.isNotEmpty() && pathProcess.contains(internalPath))
pathProcess = path.replace(internalPath, "Внутренее хранилище")
if(externalPath.isNotEmpty() && pathProcess.contains(externalPath))
pathProcess = path.replace(externalPath, "Внешнее хранилище")
val pathAsArray = pathProcess.split("/").filter { it.isNotEmpty() }
CoreBar(folders = pathAsArray)
}
@@ -4,6 +4,8 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
@@ -33,6 +35,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.megboyzz.devmenu.ui.SpacerHeight
import com.megboyzz.devmenu.ui.theme.DevMenuTheme
@OptIn(ExperimentalMaterial3Api::class)
@@ -42,7 +45,7 @@ fun MainScaffold(
searchValue: String,
onSearchValueChanged: (String) -> Unit,
onFloatingButtonClick: () -> Unit,
content: @Composable (PaddingValues) -> Unit
content: @Composable ColumnScope.() -> Unit
) {
var searchValue by remember { mutableStateOf(searchValue) }
@@ -66,11 +69,13 @@ fun MainScaffold(
shape = CircleShape,
content = {Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null)}
)
},
content = content
)
}
){
Column {
SpacerHeight(height = it.calculateTopPadding())
content()
}
}
}
@@ -1,5 +1,7 @@
package com.megboyzz.devmenu.ui.models
import com.megboyzz.devmenu.domain.entities.GameLang
enum class GameLangModel(private val lang: String) {
System("sys"),
Chinese("cn"),
@@ -16,4 +18,8 @@ enum class GameLangModel(private val lang: String) {
override fun toString() = lang
}
fun toDomain() = GameLang.values()[ordinal]
}
fun GameLang.toModel() = GameLangModel.values()[ordinal]
@@ -0,0 +1,125 @@
package com.megboyzz.devmenu.ui.screenmodels
import android.content.Context
import android.util.Log
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import com.megboyzz.devmenu.ui.components.ElementType
import com.megboyzz.devmenu.ui.getExternalAppDataDirectory
import com.megboyzz.devmenu.ui.getInternalAppDataDirectory
import com.megboyzz.devmenu.ui.models.FsElementModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import java.io.File
import java.util.Date
class HomeScreenModel(
private val context: Context
): ScreenModel {
private val TAG = this::class.simpleName
private val internalRef = FsElementModel(
"Внутренне хранилище",
ElementType.isDir,
0,
Date()
)
private val externalRef = FsElementModel(
"Внешнее хранилище",
ElementType.isDir,
0,
Date()
)
private val rootFs = listOf(
internalRef,
externalRef
)
val currentFolder = MutableStateFlow("")
val folderFiles = MutableStateFlow(listOf(internalRef, externalRef))
private fun isRoot() = currentFolder.value.isEmpty()
private fun getElements(path: String): List<FsElementModel>{
val here = File(path)
return if(here.exists()){
here.listFiles()!!.map {
FsElementModel(
name = it.name,
type = if(it.isFile) ElementType.isFile else ElementType.isDir,
size = it.length(),
date = Date(it.lastModified())
)
}
}else listOf()
}
fun enterRoot(){
screenModelScope.launch(Dispatchers.IO) {
currentFolder.emit("")
folderFiles.emit(rootFs)
}
}
fun enterFolder(elementModel: FsElementModel) {
if(isRoot()){
when(elementModel){
externalRef -> enterTheExternal()
internalRef -> enterTheInternal()
else -> Log.e(TAG, "enterFolder: error path! ${elementModel.name}")
}
}else{
screenModelScope.launch(Dispatchers.IO) {
currentFolder.emit(currentFolder.value + "/" + elementModel.name)
folderFiles.emit(getElements(currentFolder.value))
}
}
}
fun backFolder(){
val path = File(currentFolder.value)
val internal = context.getInternalAppDataDirectory()
val external = context.getExternalAppDataDirectory()
if(path.exists()){
screenModelScope.launch(Dispatchers.IO) {
when(currentFolder.value){
context.getInternalAppDataDirectory(), context.getExternalAppDataDirectory() -> enterRoot()
else -> {
currentFolder.emit(path.parentFile.absolutePath)
folderFiles.emit(getElements(currentFolder.value))
}
}
}
}
}
private fun enterTheInternal(){
screenModelScope.launch(Dispatchers.IO) {
currentFolder.emit(context.getInternalAppDataDirectory())
folderFiles.emit(getElements(currentFolder.value))
}
}
private fun enterTheExternal(){
screenModelScope.launch(Dispatchers.IO) {
currentFolder.emit(context.getExternalAppDataDirectory())
folderFiles.emit(getElements(currentFolder.value))
}
}
}
@@ -0,0 +1,62 @@
package com.megboyzz.devmenu.ui.screenmodels
import android.content.Context
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import com.megboyzz.devmenu.App
import com.megboyzz.devmenu.MainActivity
import com.megboyzz.devmenu.ui.models.GameLangModel
import com.megboyzz.devmenu.ui.models.toModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
class SettingsScreenModel(
context: Context
): ScreenModel {
private val settingsUseCaseComponent = (((context as MainActivity).application) as App).settingsUseCaseComponent
val gameLangModel = MutableStateFlow(settingsUseCaseComponent.getGameLangUseCase().toModel())
val trackingIsEnabled = MutableStateFlow(false)
init {
screenModelScope.launch(Dispatchers.IO) {
update()
}
}
private suspend fun update() {
gameLangModel.emit(settingsUseCaseComponent.getGameLangUseCase().toModel())
trackingIsEnabled.emit(settingsUseCaseComponent.getTrackingStateUseCase().enabled)
}
fun changeLang(gameLangModel: GameLangModel){
screenModelScope.launch(Dispatchers.IO) {
settingsUseCaseComponent.setGameLangUseCase(gameLangModel.toDomain())
update()
}
}
fun changeTrackingState(state: Boolean) {
screenModelScope.launch(Dispatchers.IO) {
val trackingData = settingsUseCaseComponent.getTrackingStateUseCase()
trackingData.copy(enabled = state)
settingsUseCaseComponent.setTrackingSaveFileUseCase(trackingData.enabled, trackingData.pathToSave)
update()
}
}
fun changeTrackingPath(path: String) {
screenModelScope.launch(Dispatchers.IO) {
val state = settingsUseCaseComponent.getTrackingStateUseCase()
state.copy(pathToSave = path)
settingsUseCaseComponent.setTrackingSaveFileUseCase(state.enabled, state.pathToSave)
update()
}
}
}
@@ -2,17 +2,19 @@ package com.megboyzz.devmenu.ui.screens
import androidx.compose.runtime.Composable
import cafe.adriel.voyager.core.screen.Screen
import com.megboyzz.devmenu.ui.components.ElementType
import com.megboyzz.devmenu.ui.components.FileContextMenu
import com.megboyzz.devmenu.ui.models.FsElementModel
class FileContextMenuScreen(
private val fsElementModel: FsElementModel,
private val onRemoveFile: () -> Unit,
private val onUpdateType: (ElementType) -> ElementType?
) : Screen {
@Composable
override fun Content() = FileContextMenu(
fsElementEntity = fsElementModel,
onRemoveElement = onRemoveFile,
onUpdateType = {null}
onUpdateType = onUpdateType
)
}
@@ -1,61 +1,115 @@
package com.megboyzz.devmenu.ui.screens
import android.annotation.SuppressLint
import androidx.activity.OnBackPressedCallback
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.bottomSheet.LocalBottomSheetNavigator
import com.megboyzz.devmenu.MainActivity
import com.megboyzz.devmenu.ui.SpacerHeight
import com.megboyzz.devmenu.ui.components.ElementType
import com.megboyzz.devmenu.ui.components.FsElement
import com.megboyzz.devmenu.ui.components.MainScaffold
import com.megboyzz.devmenu.ui.components.StatusBarInCurFolder
import com.megboyzz.devmenu.ui.getExternalAppDataDirectory
import com.megboyzz.devmenu.ui.getInternalAppDataDirectory
import com.megboyzz.devmenu.ui.models.FsElementModel
import com.megboyzz.devmenu.ui.registerOnBackPressedAction
import com.megboyzz.devmenu.ui.screenmodels.HomeScreenModel
import java.util.Date
class HomeScreen(
private val onFloatingButtonClick: () -> Unit
): Screen {
@SuppressLint("StateFlowValueCalledInComposition")
@Composable
override fun Content() {
val context = LocalContext.current
val bottomSheetNavigator = LocalBottomSheetNavigator.current
var searchValue by remember{ mutableStateOf("") }
val navigator = LocalNavigator.current
val fsElementModel = FsElementModel(
name = "my_txt.txt",
type = ElementType.isHidden,
size = 123,
date = Date()
)
var searchValue by remember { mutableStateOf("") }
val homeScreenModel = rememberScreenModel { HomeScreenModel(context) }
val fsState by homeScreenModel.folderFiles.collectAsState()
val currentFolder by homeScreenModel.currentFolder.collectAsState()
context.registerOnBackPressedAction {
homeScreenModel.backFolder()
}
MainScaffold(
onSettingsClick = { /*TODO Переход к настройкам */ },
onSettingsClick = { navigator?.push(SettingsScreen()) },
onSearchValueChanged = { searchValue = it },
searchValue = searchValue,
onFloatingButtonClick = onFloatingButtonClick
) {
Column {
SpacerHeight(height = it.calculateTopPadding())
FsElement(
fsElementEntity = fsElementModel,
onClick = { /*TODO*/ },
onLongPressClick = {
bottomSheetNavigator.show(
FileContextMenuScreen(
fsElementModel = fsElementModel,
onRemoveFile = {}
)
)
}
Column(
modifier = Modifier.padding(horizontal = 20.dp)
) {
StatusBarInCurFolder(
path = currentFolder,
externalPath = context.getExternalAppDataDirectory(),
internalPath = context.getInternalAppDataDirectory()
)
}
val filteredList by remember(searchValue, fsState) {
derivedStateOf {
fsState.filter { it.name.contains(searchValue, ignoreCase = true) }
}
}
LazyVerticalGrid(
columns = GridCells.Adaptive(80.dp),
horizontalArrangement = Arrangement.spacedBy(40.dp),
verticalArrangement = Arrangement.spacedBy(40.dp),
contentPadding = PaddingValues(20.dp)
){
items(filteredList){
FsElement(
fsElementEntity = it,
onClick = { homeScreenModel.enterFolder(it) },
onLongPressClick = {
bottomSheetNavigator.show(FileContextMenuScreen(
fsElementModel = it,
onRemoveFile = {},
onUpdateType = {null}
))
}
)
}
}
}
}
@@ -1,11 +1,41 @@
package com.megboyzz.devmenu.ui.screens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import cafe.adriel.voyager.core.model.rememberScreenModel
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.navigator.LocalNavigator
import com.megboyzz.devmenu.ui.components.SettingsScreenContent
import com.megboyzz.devmenu.ui.models.GameLangModel
import com.megboyzz.devmenu.ui.screenmodels.SettingsScreenModel
class SettingsScreen : Screen {
@Composable
override fun Content() {
TODO("Not yet implemented")
val navigator = LocalNavigator.current
val context = LocalContext.current
val settingsScreenModel = rememberScreenModel { SettingsScreenModel(context) }
val curLang by settingsScreenModel.gameLangModel.collectAsState()
val trackingEnabled by settingsScreenModel.trackingIsEnabled.collectAsState()
SettingsScreenContent(
onBackClick = { navigator?.pop() },
langModel = curLang,
onUpdateLanguage = { settingsScreenModel.changeLang(it) },
isTrackingSaveFile = trackingEnabled,
onUpdateTrackingSaveFileState = { settingsScreenModel.changeTrackingState(it) },
onChooseTrackingPathToSave = { },
onUploadSaveFile = { /*TODO*/ },
onDownloadSaveFile = { /*TODO*/ },
onDevMenuOff = {
})
}
}
+5 -1
View File
@@ -6,11 +6,15 @@ plugins {
android {
namespace = "ru.megboyzz.mpcore"
compileSdk = 33
compileSdk = 34
defaultConfig {
minSdk = 21
ndk {
abiFilters.add("armeabi-v7a")
}
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
externalNativeBuild {
+3 -1
View File
@@ -3,7 +3,9 @@ cmake_minimum_required(VERSION 3.22.1)
project("mpcore")
add_library(${CMAKE_PROJECT_NAME} SHARED
mpcore.cpp)
main.cpp
util/util.cpp
util/armhook.cpp)
target_link_libraries(${CMAKE_PROJECT_NAME}
android
+220
View File
@@ -0,0 +1,220 @@
#include <cstring>
#include <cstdio>
#include <android/log.h>
#include <jni.h>
#include <thread>
#include <chrono>
#include "main.h"
#include "util/util.h"
#include "util/armhook.h"
uintptr_t libapp = 0;
int (*print_internal_log)(int a1, const char *a2, ...);
void pending_thread(){
Log("Hello from thread!");
std::this_thread::sleep_for(std::chrono::milliseconds(10000));
int unk = APP_ADDR(0x00E4B8EC);
print_internal_log(unk, "Hello!");
unProtect(APP_ADDR(0x00E4A738));
}
// === EXPERIMENT: call TrackTestLayer::TrackTestLayer(path) directly ===
// See ANALYSIS.md §6d/6e for the reverse-engineering behind this. TrackTestLayer
// is a real, functional EA QA tool: its constructor loads an arbitrary track
// scene via the engine's generic scene loader, finds "start"/"finish" nodes,
// and spawns 8 reference cars bound to TrackNavigator - entirely bypassing
// RaceLoaderTask/Flow. We never found a legitimate, externally-triggerable path
// to it (the debug mode-string source is untraceable in shipped assets - see
// §6e), so this calls the constructor directly instead.
//
// libapp.so's internal "string" objects are a 3-word {begin,end,capacity}
// vector<char>-like struct (no small-string optimization), built via the
// sub_CF5F8 helper seen throughout the binary.
//
// CORRECTED after a live crash (see PROGRESS.md 2026-07-31): sub_CF5F8's real
// signature is `sub_CF5F8(dest, rangeBegin, rangeEnd)` - a [begin,end) range
// constructor (length = rangeEnd - rangeBegin, then memmove), NOT an implicit-
// strlen "assign from C-string" helper. Every caller seen during static
// analysis manually scanned for the string's end pointer first before calling
// it; first attempt here only passed 2 args, leaving the 3rd register (end)
// as garbage, which produced a bogus memmove length and SIGSEGV'd inside
// sub_CF5F8 itself (confirmed via the crash's PC landing at sub_CF5F8+0x68).
typedef void *(*StrAssignRange_t)(void *strObj, const char *rangeBegin, const char *rangeEnd);
typedef void *(*TrackTestLayerCtor_t)(void *self, void *pathStrObj);
void try_load_track_test_layer(){
// mpcore's JNI_OnLoad fires from GameActivity's Kotlin `init {}` block,
// i.e. before onCreate()/nativeOnCreate() - long before the engine's own
// resource manager and main loop are up. Give it time to boot first;
// this is a guess, not a traced signal - if this crashes or does nothing,
// that's the first thing to revisit.
Log("TrackTestLayer experiment: waiting 15s for engine boot...");
std::this_thread::sleep_for(std::chrono::seconds(15));
auto StrAssignRange = (StrAssignRange_t) APP_ADDR(0xcf5f8);
auto TrackTestLayerCtor = (TrackTestLayerCtor_t) APP_ADDR(0x24a1f4);
uint32_t pathStrObj[3] = {0, 0, 0};
// region4_chicago_track4 is referenced by TrackName in event_01_race.prefabs.sb
// (a real, shipped race event) and confirmed (via unpacking the .scene.sb with
// NFSMW12MobileTools) to contain top-level "actor" entities literally named
// "start" and "finish" - region1_foothills_track3 (first attempt) crashed on
// this exact lookup despite also containing "start"/"finish" actors somewhere,
// so nesting depth (not mere presence) may be what matters. See ANALYSIS.md §6f.
const char *trackPath = "published/prefabs/tracks/region4_chicago_track4.scene.sb";
StrAssignRange(pathStrObj, trackPath, trackPath + strlen(trackPath));
void *obj = malloc(0x110);
memset(obj, 0, 0x110);
Log("TrackTestLayer experiment: calling ctor @ 0x%08x obj=%p path=%s",
(unsigned) APP_ADDR(0x24a1f4), obj, trackPath);
TrackTestLayerCtor(obj, pathStrObj);
Log("TrackTestLayer experiment: ctor returned without crashing, obj=%p "
"(intentionally leaked for this test - not registered with any layer "
"manager, so it may not render even if construction succeeded)", obj);
}
// === ATTEMPT 2: hook RaceLoaderTask::BuildTrackScenePath instead ===
// See ANALYSIS.md §6f for why: TrackTestLayer's start/finish lookup crashes
// even on a confirmed race-linked track with the right data present, which
// points to a dependency on async load completion or ambient engine state a
// cold direct call can't replicate. Hooking the engine's own always-correctly-
// initialized code path (this function, called every time a real race loads)
// sidesteps that entirely - we let the player pick any real race from the
// menu and just substitute the track name at the last moment.
//
// IMPORTANT: unlike the Thumb-oriented code armhook.cpp's InstallHook/makeJMP
// assume, RaceLoaderTask_BuildTrackScenePath (0x2db384) is compiled in ARM
// mode (confirmed via disassembly: E92D41F0 = ARM "PUSH {r4-r8,lr}", not a
// Thumb encoding). Using the existing Thumb-oriented hook helpers here would
// misinterpret/corrupt the function. This installs a separate, ARM-mode-
// correct inline hook instead: overwrite the first 8 bytes (2 whole ARM
// instructions - safe, ARM instructions are always 4 bytes wide, so there's
// no mid-instruction split risk the way there is in mixed-width Thumb-2) with
// `LDR PC, [PC, #-4]` + hook-function-address. A trampoline re-executes the 2
// displaced instructions (both position-independent - PUSH/SUB, no PC-relative
// addressing - confirmed safe to relocate) then long-jumps back to target+8
// to resume the original function. The hook address's bit 0 is set
// automatically by the compiler (mpcore is Thumb-2 code) for correct
// ARM->Thumb interworking on the `LDR PC` jump into our hook; the trampoline's
// jump back to target+8 has bit 0 clear, staying in ARM mode as required.
typedef int (*BuildTrackScenePath_t)(uint32_t *raceLoaderTaskThis);
static BuildTrackScenePath_t g_origBuildTrackScenePath = nullptr;
static uint32_t *install_arm_inline_hook(uintptr_t targetAddr, void *hookFnPtr) {
auto *tramp = (uint32_t *) mmap(nullptr, 4096, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
auto *target = (uint32_t *) targetAddr;
tramp[0] = target[0]; // relocated original instr 0 (PUSH, position-independent)
tramp[1] = target[1]; // relocated original instr 1 (SUB SP, position-independent)
tramp[2] = 0xE51FF004; // LDR PC, [PC, #-4]
tramp[3] = (uint32_t) (targetAddr + 8); // jump back into original, ARM mode (bit0=0)
unProtect(targetAddr, 8);
target[0] = 0xE51FF004; // LDR PC, [PC, #-4]
target[1] = (uint32_t) hookFnPtr; // our Thumb hook (bit0=1, set by the compiler)
__builtin___clear_cache((char *) target, (char *) (target + 2));
__builtin___clear_cache((char *) tramp, (char *) (tramp + 4));
return tramp;
}
int hook_BuildTrackScenePath(uint32_t *raceLoaderTaskThis){
Log("hook_BuildTrackScenePath: fired, this=%p", raceLoaderTaskThis);
uint32_t raceDefPtr = raceLoaderTaskThis[8];
if (raceDefPtr) {
auto StrAssignRange = (StrAssignRange_t) APP_ADDR(0xcf5f8);
// Deliberately different from any of this build's real tracks, so a
// successful override is visually unmistakable regardless of which
// race the player actually picked in the menu.
const char *overrideTrack = "region3_colorado_track2";
void *trackNameStrObj = (void *) (raceDefPtr + 72);
StrAssignRange(trackNameStrObj, overrideTrack, overrideTrack + strlen(overrideTrack));
Log("hook_BuildTrackScenePath: raceDefPtr=0x%08x, overrode track name to '%s'",
raceDefPtr, overrideTrack);
} else {
Log("hook_BuildTrackScenePath: raceDefPtr is null, skipping override");
}
return g_origBuildTrackScenePath(raceLoaderTaskThis);
}
jint JNI_OnLoad(JavaVM *vm, void *reserved){
libapp = FindLibrary("libapp.so");
Log("Hello! my base addr is 0x%08x", libapp);
//InitHookStuff();
//unProtect(APP_ADDR(0x00A522C4));
//*(int *)APP_ADDR(0x00A522C4) = 0xE320F000;
//WriteMemory(APP_ADDR(0x00E4BA94), (uintptr_t)"my_tag", 6);
// TrackTestLayer experiment (2026-07-31): superseded by the
// RaceLoaderTask_BuildTrackScenePath hook below - see ANALYSIS.md §6f.
// std::thread(try_load_track_test_layer).detach();
// Installing the ARM inline hook is just a code patch - safe at any time,
// unlike calling into engine internals cold. No delay needed; the hook
// only fires later, whenever the player naturally starts a real race.
g_origBuildTrackScenePath = (BuildTrackScenePath_t)
install_arm_inline_hook(APP_ADDR(0x2db384), (void *) hook_BuildTrackScenePath);
Log("Installed RaceLoaderTask_BuildTrackScenePath hook, orig trampoline=%p",
(void *) g_origBuildTrackScenePath);
// raise(SIGSTOP) disabled for this experiment (2026-07-31): SIGSTOP
// suspends the entire process, including the thread spawned above, which
// would prevent the delayed call from ever firing. Re-enable if you need
// to attach a debugger before continuing normal boot - see PROGRESS.md.
//raise(SIGSTOP);
return JNI_VERSION_1_6;
}
/*
Log("Hello! my base addr is 0x%08x", libapp);
Log("Address i wanna to get 0x%08x", APP_ADDR(0xC8C9D8));
const char* str = (char *)APP_ADDR(0xC8C9D8);
Log("i found it! %s", str);
unProtect(APP_ADDR(0xC8C9D8));
WriteMemory(APP_ADDR(0xC8C9D8), (uintptr_t)"APPLICATION_NO", 14);
str = (char *)APP_ADDR(0xC8C9D8);
Log("i change it! %s", str);
//makeNOP(APP_ADDR(0x00652EB4), 2);
//makeNOP(APP_ADDR(0x00652E3C), 2);
unProtect(APP_ADDR(0x00652EB4));
*(char *)APP_ADDR(0x00652EB4) = 0xE320F000;
unProtect(APP_ADDR(0x00652E3C));
*(char *)APP_ADDR(0x00652E3C) = 0xE320F000;
*/
+6
View File
@@ -0,0 +1,6 @@
//Основная ссылка на начало основного процесса - libapp.so
extern uintptr_t libapp;
//Макрос для ссылок внутри памяти процесса
#define APP_ADDR(addr) (libapp + (addr))
-10
View File
@@ -1,10 +0,0 @@
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_ru_megboyzz_mpcore_NativeLib_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
+198
View File
@@ -0,0 +1,198 @@
#include <unistd.h> // system api
#include <assert.h> // assert()
#include <dlfcn.h> // dlopen
#include <cerrno>
#include <cstring>
#include <exception>
#include "armhook.h"
#include "util.h"
#include "../main.h"
uintptr_t mmap_start = 0;
uintptr_t mmap_end = 0;
uintptr_t memlib_start = 0;
uintptr_t memlib_end = 0;
//#define PAGE_SIZE 0x8124
#define HOOK_PROC "\x01\xB4\x01\xB4\x01\x48\x01\x90\x01\xBD\x00\xBF\x00\x00\x00\x00"
void unProtect(uintptr_t ptr, size_t dwSize)
{
if(mprotect(reinterpret_cast<void*>(ptr & 0xFFFFF000), PAGE_SIZE, PROT_WRITE | PROT_READ | PROT_EXEC) == -1)
{
const auto err = errno;
if(err == EACCES)
{
mprotect(reinterpret_cast<void*>(ptr & 0xFFFFF000), PAGE_SIZE, PROT_WRITE | PROT_READ);
return;
}
}
}
void makeNOP(uintptr_t addr, unsigned int count)
{
Log("Hello from makeNOP!");
unProtect(addr);
for(uintptr_t ptr = addr; ptr != (addr+(count * 2)); ptr += 2)
{
Log("Hello from loop makeNOP!");
*(char*)ptr = 0x00;
*(char*)(ptr+1) = 0x46;
}
cacheflush(addr, (uintptr_t)(addr + count * 2), 0);
}
void WriteMemory(uintptr_t dest, uintptr_t src, size_t size)
{
unProtect(dest);
memcpy((void*)dest, (void*)src, size);
cacheflush(dest, dest+size, 0);
}
void WriteMemory(uintptr_t dest, void* src, size_t size)
{
unProtect(dest);
memcpy((void*)dest, src, size);
cacheflush(dest, dest+size, 0);
}
void SetPointer(uintptr_t addr, void* value)
{
WriteMemory(addr, &value, sizeof(value));
}
void ReadMemory(uintptr_t dest, uintptr_t src, size_t size)
{
unProtect(src);
memcpy((void*)dest, (void*)src, size);
}
void InitHookStuff()
{
memlib_start = APP_ADDR(0x000A1B44);
memlib_end = memlib_start + 0x1A36;
mmap_start = (uintptr_t)mmap(0, PAGE_SIZE, PROT_WRITE | PROT_READ | PROT_EXEC, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
int code = mprotect((void*)(mmap_start & 0xFFFFF000), PAGE_SIZE, PROT_READ | PROT_EXEC | PROT_WRITE);
mmap_end = (mmap_start + PAGE_SIZE);
/*const auto err = errno;
Log("Status is %d && %d", err, code);
Log("mmap_stat = 0x%08x", mmap_start);
Log("mmap_end = 0x%08x", mmap_end);
Log("memlib_start = 0x%08x", memlib_start);
Log("memlib_end = 0x%08x", memlib_end);*/
//unProtect(APP_ADDR(0x1800AC));
}
void makeJMP(uintptr_t func, uintptr_t addr)
{
uint32_t code = ((addr - func - 4) >> 12) & 0x7FF | 0xF000 | ((((addr - func - 4) >> 1) & 0x7FF | 0xB800) << 16);
WriteMemory(func, (uintptr_t)&code, 4);
}
void makeBLX(uintptr_t func, uintptr_t addr)
{
uint32_t code = ((addr - func - 4) >> 12) & 0x7FF | ((addr - func - 4) << 15) | 0xF800F000;
WriteMemory(func, (uintptr_t)&code, 4);
}
void WriteHookProc(uintptr_t addr, uintptr_t func)
{
char code[16];
memcpy(code, HOOK_PROC, 16);
*(uint32_t*)&code[12] = (func | 1);
WriteMemory(addr, (uintptr_t)code, 16);
}
void InstallBLXHook(uintptr_t addr, uintptr_t func)
{
// memlib_start = 0
// memlib_end = 10
if(memlib_end < (memlib_start + 0x10) || mmap_end < (mmap_start + 0x20))
{
std::terminate();
}
makeBLX(addr, func);
WriteHookProc(addr, func);
memlib_start += 16;
}
void InstallJMPHook(uintptr_t addr, uintptr_t func)
{
if(memlib_end < (memlib_start + 0x10) || mmap_end < (mmap_start + 0x20))
{
std::terminate();
}
makeJMP(addr, func);
WriteHookProc(addr, func);
memlib_start += 16;
}
void InstallHook(uintptr_t addr, uintptr_t func, uintptr_t *orig)
{
if(memlib_end < (memlib_start + 0x10) || mmap_end < (mmap_start + 0x20))
{
Log("FAILED!");
std::terminate();
}
ReadMemory(mmap_start, addr, 4);
WriteHookProc(mmap_start+4, addr+4);
*orig = mmap_start+1;
mmap_start += 32;
makeJMP(addr, memlib_start);
WriteHookProc(memlib_start, func);
memlib_start += 16;
}
void RedirectCall(uintptr_t addr, uintptr_t func)
{
makeJMP(addr, memlib_start);
WriteHookProc(memlib_start, func);
memlib_start += 16;
}
void InstallMethodHook(uintptr_t addr, uintptr_t func)
{
unProtect(addr);
*(uintptr_t*)addr = func;
}
void CodeInject(uintptr_t addr, uintptr_t func, int reg)
{
char injectCode[12];
injectCode[0] = 0x01;
injectCode[1] = 0xA0 + reg;
injectCode[2] = (0x08 * reg) + reg;
injectCode[3] = 0x68;
injectCode[4] = 0x87 + (0x08 * reg);
injectCode[5] = 0x46;
injectCode[6] = injectCode[4];
injectCode[7] = injectCode[5];
*(uintptr_t*)&injectCode[8] = func;
WriteMemory(addr, (uintptr_t)injectCode, 12);
}
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <sys/mman.h>
extern uintptr_t mmap_start;
extern uintptr_t mmap_end;
extern uintptr_t memlib_start;
extern uintptr_t memlib_end;
void unProtect(uintptr_t ptr, size_t dwSize = 100);
void WriteMemory(uintptr_t dest, uintptr_t src, size_t size);
void ReadMemory(uintptr_t dest, uintptr_t src, size_t size);
void makeNOP(uintptr_t addr, unsigned int count);
void makeJMP(uintptr_t func, uintptr_t addr);
void makeBLX(uintptr_t func, uintptr_t addr);
void InitHookStuff();
void InstallHook(uintptr_t addr, uintptr_t func, uintptr_t *orig);
void InstallMethodHook(uintptr_t addr, uintptr_t func);
void InstallJMPHook(uintptr_t addr, uintptr_t func);
void InstallBLXHook(uintptr_t addr, uintptr_t func);
void CodeInject(uintptr_t addr, uintptr_t func, int reg);
void WriteMemory(uintptr_t dest, void* src, size_t size);
void SetPointer(uintptr_t addr, void* value);
template <typename Ret, typename... Args>
static Ret CallFunction(unsigned int address, Args... args)
{
return reinterpret_cast<Ret(__cdecl *)(Args...)>(address)(args...);
}
+52
View File
@@ -0,0 +1,52 @@
#include <cstring>
#include <unistd.h>
#include <cstdlib>
#include <android/log.h>
#include "util.h"
uintptr_t FindLibrary(const char *lib_name) {
char filename[0xFF] = {0},
buffer[2048] = {0};
FILE *fp = 0;
uintptr_t address = 0;
sprintf(filename, "/proc/%d/maps", getpid());
fp = fopen(filename, "rt");
if (fp == nullptr) {
Log("ERROR: can't open file %s", filename);
goto done;
}
while (fgets(buffer, sizeof(buffer), fp)) {
if (strstr(buffer, lib_name)) {
address = (uintptr_t) strtoul(buffer, 0, 16);
break;
}
}
done:
if (fp)
fclose(fp);
return address;
}
void Log(const char *fmt, ...) {
char buffer[0xFF];
static FILE *flLog = nullptr;
memset(buffer, 0, sizeof(buffer));
va_list arg;
va_start(arg, fmt);
vsnprintf(buffer, sizeof(buffer), fmt, arg);
va_end(arg);
__android_log_write(ANDROID_LOG_INFO, "mpcore_log", buffer);
if (flLog == nullptr) return;
fprintf(flLog, "%s\n", buffer);
fflush(flLog);
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
uintptr_t FindLibrary(const char* lib_name);
void Log(const char *fmt, ...);
@@ -2,11 +2,10 @@ package ru.megboyzz.mpcore
class NativeLib {
external fun stringFromJNI(): String
companion object {
init {
System.loadLibrary("mpcore")
//System.loadLibrary("mpcore")
}
}
}