Add kEnableTrackSubstitutionHook toggle flag

Lets the track-substitution hook be enabled/disabled by flipping one
constexpr bool in main.cpp instead of commenting/uncommenting code, so
before/after comparisons don't need back-and-forth edits.
This commit is contained in:
2026-08-05 15:24:15 +03:00
parent 1cd5547b04
commit d8a1f34a1c
+105 -237
View File
@@ -1,5 +1,7 @@
#include <cstring>
#include <cstdio>
#include <cstdint>
#include <cerrno>
#include <android/log.h>
#include <jni.h>
#include "main.h"
@@ -73,249 +75,115 @@ bool is_memory_writable(void* addr, size_t size) {
using namespace std;
// Базовый класс - Животное
class Animal {
protected:
string name;
int age;
// ---- RaceLoaderTask_BuildTrackScenePath hook (see ANALYSIS.md §6j) ----
// Target compiled in ARM mode (PUSH {R4-R11,LR}; ADD R11,SP,#0x1C - both
// position-independent, safe to relocate into the trampoline as-is).
#define BUILDTRACKSCENEPATH_OFFSET 0x2a8424
public:
// Конструктор
Animal(const string& name, int age) : name(name), age(age) {
cout << "Animal constructor: " << name << endl;
typedef int (*BuildTrackScenePathFn)(void* raceLoaderTask);
static BuildTrackScenePathFn orig_BuildTrackScenePath = nullptr;
// Deliberately different from any real event's track, so a successful
// override is visually unmistakable. region3/colorado was tried first and
// abandoned: it's cut/incomplete content in this build - models/environments/
// has no colorado/ folder at all (only chicago, desert, foothills, garage,
// newyork), even though region3_colorado_track2.scene.sb itself contains
// full embedded geometry. The scene's internal m3g loader still tries to
// open "/published/models/environments/colorado/region3_colorado_track2.m3g"
// as a loose file, which was never shipped -> geometry never loads -> every
// downstream consumer (spatial index, checkpoints, ...) sees empty/zero data
// and null-derefs, which is what the whole sub_53A5FC/sub_52A9B8/sub_52A620/
// sub_58E5E8 crash chain actually was (see ANALYSIS.md §6n/§6o). Switched to
// region4_chicago_track4/chicago instead: confirmed shipped and playable
// (event_01_race.prefabs.sb's real TrackName, verified start/finish actors
// in the scene, and a full models/environments/chicago/ folder present).
static const char* kOverrideTrackName = "region4_chicago_track4";
// Environment prefabs are per-track variants ("chicago1.prefabs.sb" ..
// "chicago6.prefabs.sb", matching "region4_chicago_track1".."track6"), not a
// single generic "chicago.prefabs.sb" - confirmed live: env="chicago" alone
// hit "Could not open database at published/prefabs/environments/chicago.
// prefabs.sb" followed by an immediate SIGSEGV. Only colorado shipped as one
// un-numbered colorado.prefabs.sb instead of colorado1..6 - further evidence
// region3/colorado is unfinished/cut content (see kOverrideTrackName above).
static const char* kOverrideEnvName = "chicago4";
int Hook_BuildTrackScenePath(void* a1) {
// a1[8] (word offset 8 = byte 32): pointer to the RaceDefinition-like
// struct. Track name is a {begin,end} pair at byte offsets +72/+76,
// environment name likewise at +100/+104. BuildTrackScenePath only
// READS these fields (never frees them), so we just repoint begin/end
// at our own static buffers instead of freeing/reallocating the
// originals - avoids guessing an unconfirmed capacity-field offset.
// The original buffers are deliberately leaked (two small allocations
// per race load - negligible).
void* raceDef = *(void**)((uint8_t*)a1 + 32);
if (raceDef) {
size_t trackLen = strlen(kOverrideTrackName);
*(const char**)((uint8_t*)raceDef + 72) = kOverrideTrackName;
*(const char**)((uint8_t*)raceDef + 76) = kOverrideTrackName + trackLen;
size_t envLen = strlen(kOverrideEnvName);
*(const char**)((uint8_t*)raceDef + 100) = kOverrideEnvName;
*(const char**)((uint8_t*)raceDef + 104) = kOverrideEnvName + envLen;
Log("BuildTrackScenePath hook fired: track -> %s, env -> %s", kOverrideTrackName, kOverrideEnvName);
} else {
Log("BuildTrackScenePath hook fired but raceDef (a1[8]) is NULL, skipping override");
}
// Виртуальный деструктор
virtual ~Animal() {
cout << "Animal destructor: " << name << endl;
}
// Виртуальный метод (будет переопределяться)
virtual void makeSound() const {
cout << name << " makes a generic animal sound" << endl;
}
// Не виртуальный метод (не будет переопределяться)
void sleep() const {
cout << name << " is sleeping" << endl;
}
string getName() const { return name; }
int getAge() const { return age; }
};
// Производный класс - Млекопитающее
class Mammal : public Animal {
protected:
bool hasFur;
public:
// Конструктор
Mammal(const string& name, int age, bool hasFur)
: Animal(name, age), hasFur(hasFur) {
cout << "Mammal constructor: " << name << endl;
}
// Деструктор
~Mammal() override {
cout << "Mammal destructor: " << name << endl;
}
// Переопределение метода
void makeSound() const override {
cout << name << " makes a mammal sound" << endl;
}
// Новый метод
void feedMilk() const {
cout << name << " is feeding milk" << endl;
}
};
// Производный класс - Птица
class Bird : public Animal {
protected:
double wingspan;
public:
// Конструктор
Bird(const string& name, int age, double wingspan)
: Animal(name, age), wingspan(wingspan) {
cout << "Bird constructor: " << name << endl;
}
// Деструктор
~Bird() override {
cout << "Bird destructor: " << name << endl;
}
// Переопределение метода
void makeSound() const override {
cout << name << " chirps" << endl;
}
// Новый метод
void fly() const {
cout << name << " is flying with wingspan " << wingspan << "m" << endl;
}
};
// Производный класс от Млекопитающего - Собака
class Dog : public Mammal {
private:
string breed;
public:
// Конструктор
Dog(const string& name, int age, bool hasFur, const string& breed)
: Mammal(name, age, hasFur), breed(breed) {
cout << "Dog constructor: " << name << endl;
}
// Деструктор
~Dog() override {
cout << "Dog destructor: " << name << endl;
}
// Переопределение метода
void makeSound() const override {
cout << name << " barks: Woof! Woof!" << endl;
}
// Новый метод
void fetch() const {
cout << name << " is fetching the ball" << endl;
}
};
// Производный класс от Млекопитающего - Кошка
class Cat : public Mammal {
private:
int lives;
public:
// Конструктор
Cat(const string& name, int age, bool hasFur, int lives = 9)
: Mammal(name, age, hasFur), lives(lives) {
cout << "Cat constructor: " << name << endl;
}
// Деструктор
~Cat() override {
cout << "Cat destructor: " << name << endl;
}
// Переопределение метода
void makeSound() const override {
cout << name << " meows: Meow! Meow!" << endl;
}
// Новый метод
void purr() const {
cout << name << " is purring" << endl;
}
};
// Производный класс от Птицы - Орел
class Eagle : public Bird {
private:
double visionRange;
public:
// Конструктор
Eagle(const string& name, int age, double wingspan, double visionRange)
: Bird(name, age, wingspan), visionRange(visionRange) {
cout << "Eagle constructor: " << name << endl;
}
// Деструктор
~Eagle() override {
cout << "Eagle destructor: " << name << endl;
}
// Метод НЕ переопределяется (используется версия из Bird)
// void makeSound() const override {...}
// Новый метод
void hunt() const {
cout << name << " is hunting with vision range " << visionRange << "km" << endl;
}
};
// Производный класс от Птицы - Попугай
class Parrot : public Bird {
private:
bool canTalk;
public:
// Конструктор
Parrot(const string& name, int age, double wingspan, bool canTalk)
: Bird(name, age, wingspan), canTalk(canTalk) {
cout << "Parrot constructor: " << name << endl;
}
// Деструктор
~Parrot() override {
cout << "Parrot destructor: " << name << endl;
}
// Переопределение метода
void makeSound() const override {
if (canTalk) {
cout << name << " says: Hello! Polly wants a cracker!" << endl;
} else {
cout << name << " squawks" << endl;
}
}
// Новый метод
void repeat(const string& phrase) const {
if (canTalk) {
cout << name << " repeats: " << phrase << endl;
}
}
};
// Пример использования
int main() {
cout << "=== Creating objects ===" << endl;
Dog dog("Rex", 3, true, "German Shepherd");
Cat cat("Whiskers", 2, true);
Eagle eagle("Thor", 5, 2.1, 3.5);
Parrot parrot("Polly", 1, 0.3, true);
cout << "\n=== Testing methods ===" << endl;
dog.makeSound(); // Переопределен в Dog
dog.fetch(); // Уникальный метод Dog
cat.makeSound(); // Переопределен в Cat
cat.purr(); // Уникальный метод Cat
eagle.makeSound(); // НЕ переопределен - использует версию Bird
eagle.hunt(); // Уникальный метод Eagle
parrot.makeSound(); // Переопределен в Parrot
parrot.repeat("I love C++"); // Уникальный метод Parrot
cout << "\n=== Testing polymorphism ===" << endl;
Animal* animals[] = {&dog, &cat, &eagle, &parrot};
for (Animal* animal : animals) {
animal->makeSound(); // Полиморфный вызов
animal->sleep(); // Не виртуальный метод - всегда Animal::sleep()
}
cout << "\n=== Objects going out of scope ===" << endl;
// Деструкторы будут вызваны автоматически
return 0;
return orig_BuildTrackScenePath(a1);
}
static bool InstallBuildTrackScenePathHook() {
uintptr_t target = (uintptr_t)libapp_base + BUILDTRACKSCENEPATH_OFFSET;
uint32_t* target32 = (uint32_t*)target;
void* tramp = mmap(nullptr, (size_t)getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (tramp == MAP_FAILED) {
Log("BuildTrackScenePath hook: mmap trampoline failed");
return false;
}
uint32_t* tramp32 = (uint32_t*)tramp;
// Relocate the 2 displaced original ARM instructions verbatim (both
// confirmed position-independent: plain PUSH and ADD, no PC-relative
// addressing), then jump back into the function body past them.
tramp32[0] = target32[0];
tramp32[1] = target32[1];
tramp32[2] = 0xE51FF004; // LDR PC, [PC, #-4]
tramp32[3] = (uint32_t)(target + 8);
orig_BuildTrackScenePath = (BuildTrackScenePathFn)tramp;
uintptr_t page = target & ~((uintptr_t)getpagesize() - 1);
if (mprotect((void*)page, (size_t)getpagesize(), PROT_READ | PROT_WRITE | PROT_EXEC) != 0) {
Log("BuildTrackScenePath hook: mprotect target failed: %s", strerror(errno));
return false;
}
target32[0] = 0xE51FF004; // LDR PC, [PC, #-4]
// &Hook_BuildTrackScenePath has bit0 set (Thumb-compiled mpcore code),
// triggering the ARM->Thumb interworking switch on load into PC.
target32[1] = (uint32_t)(uintptr_t)&Hook_BuildTrackScenePath;
__builtin___clear_cache((char*)target, (char*)(target + 8));
__builtin___clear_cache((char*)tramp, (char*)tramp + 16);
Log("Installed RaceLoaderTask_BuildTrackScenePath hook at %p, trampoline=%p", (void*)target, tramp);
return true;
}
// Flip to false to run the game completely unmodified (e.g. to capture a
// baseline/"before" comparison) - true installs the track-substitution hook.
// Just edit this and rebuild, no need to touch anything else.
static constexpr bool kEnableTrackSubstitutionHook = true;
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
get_libapp_base();
if (get_libapp_base()) {
if (kEnableTrackSubstitutionHook) {
InstallBuildTrackScenePathHook();
}
}
return JNI_VERSION_1_6;
}