Files
InstanceBrick--nightcap/src/legacy/legacy_runtime.cc
T
InstanceBrick 7dc4f55642 Add native ARM64 Steam Frame VR and eye-tracked rendering
Integrate Frame OpenXR input, SteamVR video controls, gaze foveation, native audio, frame pacing and source-based packaging.
2026-10-16 15:31:49 -04:00

7556 lines
310 KiB
C++

// Modified by vii from komaruworld/mocktail. See README "About this fork".
#include <algorithm>
#include <atomic>
#include <cctype>
#include <cerrno>
#include <chrono>
#include <cmath>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <dlfcn.h>
#include <link.h>
#include <iomanip>
#include <iostream>
#include <csignal>
#include <netdb.h>
#include <memory>
#include <mutex>
#include <new>
#include <poll.h>
#include <unordered_map>
#include <pthread.h>
#include <setjmp.h>
#include <signal.h>
#include <string>
#include <string_view>
#include <arpa/inet.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <time.h>
#include <ucontext.h>
#include <elf.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/prctl.h>
#if defined(__x86_64__)
#include <asm/prctl.h>
#endif
#include <sys/syscall.h>
#include <vector>
#include <jni.h>
#include <SDL3/SDL_video.h>
#include "compat/bionic_abi_exports.h"
#include "compat/bionic_prctl_runtime.h"
#include "compat/bionic_pthread_create_runtime.h"
#include "compat/bionic_socket_runtime.h"
#include "compat/build_profile.h"
#include "compat/elf_build_id.h"
#include "compat/guest_abi.h"
#include "compat/host_abi_experiment.h"
#include "compat/host_abi_profile.h"
#include "compat/host_allocator_bridge.h"
#include "jnivm/jnivm.h"
#include "legacy/legacy_runtime.h"
#include "libc_shim/libc_shim.h"
#include "linker/linker.h"
#include "mocktail/graphics/bionic_egl_bridge.h"
#include "mocktail/graphics/chrome_trace_writer.h"
#include "mocktail/vr/roblox_vr_device_bridge.h"
#include "runtime/engine_pump_rest.h"
#include "runtime/device_memory_profile.h"
#include "runtime/display_size.h"
#include "runtime/environment.h"
#include "runtime/discord_rpc.h"
#include "runtime/luau_codegen_switch.h"
#include "runtime/jnivm_platform_web_callbacks.h"
#include "runtime/owned_pthread.h"
#include "runtime/platform_cache_migration.h"
#include "runtime/asset_overlay_names.h"
#include "runtime/roblox_app_lifecycle.h"
#include "runtime/roblox_capability_resolver.h"
#include "runtime/roblox_platform_web_symbols.h"
#include "runtime/roblox_experience_composition.h"
#include "runtime/roblox_haptic_protocol_bridge.h"
#include "runtime/roblox_game_session_native_adapter.h"
#include "runtime/roblox_window_input_runtime.h"
#include "runtime/roblox_text_input_jni_bridge.h"
#include "runtime/runtime_config.h"
#include "runtime/runtime_paths.h"
#include "runtime/texture_memory_policy.h"
#include "services/client_settings_service.h"
#include "services/http_client.h"
#include "window/window.h"
#include "window/window_game_surface_bridge.h"
#ifdef MOCKTAIL_USE_BIONIC_LINKER
#include <mcpelauncher/linker.h>
#endif
#ifndef MOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST
#define MOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST \
"config/roblox_compatibility.json"
#endif
std::atomic<bool> g_allow_legacy_binary_patches{false};
std::atomic<bool> g_allow_host_abi_bridges{false};
std::atomic<bool> g_allow_host_constructor_replay{false};
std::atomic<const mocktail::compat::HostAbiProfile*>
g_active_host_abi_profile{nullptr};
namespace {
void* ResolveRobloxCapabilitySymbol(void* context, const char* symbol_name) {
if (context == nullptr || symbol_name == nullptr) {
return nullptr;
}
auto* handle = static_cast<linker::LibraryHandle*>(context);
return linker::ResolveSymbol(*handle, symbol_name);
}
void RestoreGameSessionJniEnvironment(void* context) {
auto* vm = static_cast<jnivm::VM*>(context);
if (vm != nullptr) {
vm->RestoreFunctions();
}
}
volatile uintptr_t g_stage6_jni_env = 0;
volatile uintptr_t g_libroblox_base = 0;
volatile uintptr_t g_game_activity_native_handle = 0;
jobject g_saved_game_activity = nullptr;
extern "C" void* mocktail_gameactivity_on_trim_memory_native;
volatile sig_atomic_t g_jni_onload_in_progress = 0;
volatile sig_atomic_t g_jni_onload_timings_printed = 0;
volatile sig_atomic_t g_jni_onload_soft_timeout = 0;
volatile sig_atomic_t g_jni_onload_jmp_armed = 0;
thread_local sigjmp_buf g_jni_onload_jmp_buf;
thread_local sigjmp_buf g_init_with_params_jmp_buf;
thread_local volatile sig_atomic_t g_init_with_params_recovery_in_progress = 0;
thread_local sigjmp_buf g_start_app_with_params_jmp_buf;
thread_local volatile sig_atomic_t g_start_app_with_params_recovery_in_progress = 0;
thread_local sigjmp_buf g_start_lua_app_dm_jmp_buf;
thread_local volatile sig_atomic_t g_start_lua_app_dm_recovery_in_progress = 0;
thread_local sigjmp_buf g_send_app_ready_jmp_buf;
thread_local volatile sig_atomic_t g_send_app_ready_recovery_in_progress = 0;
thread_local sigjmp_buf g_send_game_loaded_jmp_buf;
thread_local volatile sig_atomic_t g_send_game_loaded_recovery_in_progress = 0;
thread_local sigjmp_buf g_set_asset_path_jmp_buf;
thread_local volatile sig_atomic_t g_set_asset_path_recovery_in_progress = 0;
thread_local sigjmp_buf g_game_global_init_jmp_buf;
thread_local volatile sig_atomic_t g_game_global_init_recovery_in_progress = 0;
thread_local sigjmp_buf g_init_client_settings_jmp_buf;
thread_local volatile sig_atomic_t g_init_client_settings_recovery_in_progress = 0;
thread_local sigjmp_buf g_post_client_settings_jmp_buf;
thread_local volatile sig_atomic_t g_post_client_settings_recovery_in_progress = 0;
thread_local sigjmp_buf g_initialize_native_flags_jmp_buf;
thread_local volatile sig_atomic_t
g_initialize_native_flags_recovery_in_progress = 0;
thread_local sigjmp_buf g_cookie_setter_jmp_buf;
thread_local volatile sig_atomic_t g_cookie_setter_recovery_in_progress = 0;
thread_local sigjmp_buf g_native_settings_jmp_buf;
thread_local volatile sig_atomic_t g_native_settings_recovery_in_progress = 0;
thread_local const char* g_native_settings_recovery_name = nullptr;
thread_local sigjmp_buf g_app_bridge_app_start_jmp_buf;
thread_local volatile sig_atomic_t g_app_bridge_app_start_recovery_in_progress =
0;
thread_local sigjmp_buf g_game_activity_init_jmp_buf;
thread_local volatile sig_atomic_t g_game_activity_init_recovery_in_progress =
0;
thread_local sigjmp_buf g_game_activity_surface_jmp_buf;
thread_local volatile sig_atomic_t g_game_activity_surface_recovery_in_progress = 0;
thread_local sigjmp_buf g_activity_lifecycle_jmp_buf;
thread_local volatile sig_atomic_t g_activity_lifecycle_recovery_in_progress = 0;
thread_local sigjmp_buf g_update_screen_orientation_jmp_buf;
thread_local volatile sig_atomic_t
g_update_screen_orientation_recovery_in_progress = 0;
thread_local sigjmp_buf g_update_surface_app_jmp_buf;
thread_local volatile sig_atomic_t g_update_surface_app_recovery_in_progress = 0;
thread_local sigjmp_buf g_native_fragment_start_jmp_buf;
thread_local volatile sig_atomic_t
g_native_fragment_start_recovery_in_progress = 0;
thread_local sigjmp_buf g_display_refresh_rate_jmp_buf;
thread_local volatile sig_atomic_t
g_display_refresh_rate_recovery_in_progress = 0;
constexpr sig_atomic_t kStage6RecoveryInactive = 0;
constexpr sig_atomic_t kStage6RecoveryInline = 1;
void PrintBacktraceNoSig(const char* prefix);
void PrintContextBacktrace(ucontext_t* ucontext, const char* prefix);
static uintptr_t NullVtableStub();
using JniOnLoadFn = jint (*)(JavaVM*, void*);
using NativeGameGlobalInitFn = void (*)(JNIEnv*, jclass);
using NativeInitClientSettingsFn = jint (*)(JNIEnv*, jclass, jstring, jstring,
jstring);
using NativeInitClientSettingsSignedFn = jint (*)(JNIEnv*, jclass, jstring,
jstring, jstring, jstring);
using NativeInitClientSettingsCachedFn = jint (*)(JNIEnv*, jclass, jstring,
jstring, jstring, jstring,
jlong);
using NativeInitClientSettingsCachedCompressedFn =
jint (*)(JNIEnv*, jclass, jbyteArray, jstring, jstring, jstring, jlong,
jboolean);
using NativePostClientSettingsFn = void (*)(JNIEnv*, jclass, jobject);
using NativeInitializeNativeFlagsFn = jobject (*)(JNIEnv*, jclass,
jobjectArray);
using NativeAppBridgeAppStartFn = void (*)(JNIEnv*, jclass, jstring, jstring,
jboolean, jstring, jstring,
jstring);
using NativeSetIsFirstInstallFn = void (*)(JNIEnv*, jclass, jboolean);
using NativeAppBridgeObjectParamsFn = void (*)(JNIEnv*, jclass, jobject);
using NativeAppBridgeSetInitParamsFn = void (*)(JNIEnv*, jclass, jobject);
using NativeUpdateSurfaceAppFn = void (*)(JNIEnv*, jclass, jobject, jobject);
using NativeUpdateAppUiSizesFn = void (*)(JNIEnv*, jclass, jint, jint, jint,
jint, jint);
using NativeUpdateScreenOrientationFn = void (*)(JNIEnv*, jclass, jint);
using NativeSendAppReadyFn = void (*)(JNIEnv*, jclass, jstring, jstring,
jstring, jstring);
using NativeSendGameLoadedFn = void (*)(JNIEnv*, jclass, jstring, jstring,
jstring);
using NativePassSupportedRefreshRatesFn = void (*)(JNIEnv*, jclass,
jfloatArray);
using NativePassCurrentDisplayRefreshRateFn = void (*)(JNIEnv*, jclass, jfloat);
using NativeUpdateMobileHapticsStateFn = void (*)(JNIEnv*, jclass, jboolean, jstring);
using NativeSetStringParamFn = void (*)(JNIEnv*, jclass, jstring);
using NativeSetBaseUrlFn = void (*)(JNIEnv*, jclass, jstring, jstring);
using NativeSetTaskSchedulerBackgroundModeFn = void (*)(JNIEnv*, jclass,
jboolean, jstring);
using NativeObjectInitFn = void (*)(JNIEnv*, jclass, jobject);
using NativeSetTwoStringParamsFn = void (*)(JNIEnv*, jclass, jstring, jstring);
using NativeSetThreeStringParamsFn = void (*)(JNIEnv*, jclass, jstring,
jstring, jstring);
using NativeSetHttpClientProxyFn = void (*)(JNIEnv*, jclass, jstring, jlong);
using NativeInitStorageManagerFn = void (*)(JNIEnv*, jobject, jobject, jstring,
jstring);
using NativeSetPlatformImplFn = jobject (*)(JNIEnv*, jclass, jobject);
using NativeActivityLifecycleStringFn = void (*)(JNIEnv*, jobject, jstring);
using NativeNoArgFn = void (*)(JNIEnv*, jclass);
using NativeDirectNoArgFn = void (*)();
using NativeGameActivityInitFn = jlong (*)(JNIEnv*, jobject, jstring, jstring,
jstring, jobject, jbyteArray,
jobject);
using GameActivityLifecycleFn = void (*)(JNIEnv*, jobject, jlong);
using GameActivitySurfaceCreatedFn = void (*)(JNIEnv*, jobject, jlong, jobject);
using GameActivitySurfaceChangedFn = void (*)(JNIEnv*, jobject, jlong, jobject,
jint, jint, jint);
struct NativeActivityLifecycleCallbacks {
NativeActivityLifecycleStringFn on_pre_created;
NativeActivityLifecycleStringFn on_created;
NativeActivityLifecycleStringFn on_post_created;
NativeActivityLifecycleStringFn on_pre_started;
NativeActivityLifecycleStringFn on_started;
NativeActivityLifecycleStringFn on_post_started;
NativeActivityLifecycleStringFn on_pre_resumed;
NativeActivityLifecycleStringFn on_resumed;
NativeActivityLifecycleStringFn on_post_resumed;
};
NativeNoArgFn g_native_call_messages_from_main_thread = nullptr;
NativeNoArgFn g_pending_main_thread_start_lua_app_dm = nullptr;
jclass g_native_gl_class_for_main_thread = nullptr;
jnivm::VM* g_vm_for_main_thread_pump = nullptr;
std::atomic<int> g_main_thread_message_pump_ready{0};
uint64_t g_pending_main_thread_start_lua_due_ms = 0;
bool g_pending_main_thread_start_lua_started = false;
NativeSetTaskSchedulerBackgroundModeFn
g_pending_main_thread_task_scheduler_background_mode = nullptr;
jclass g_pending_main_thread_task_scheduler_class = nullptr;
std::atomic<int> g_pending_main_thread_task_scheduler_state{0};
std::atomic<int> g_pending_main_thread_task_scheduler_recovered{0};
constexpr int kMainThreadTaskSchedulerIdle = 0;
constexpr int kMainThreadTaskSchedulerPending = 1;
constexpr int kMainThreadTaskSchedulerRunning = 2;
constexpr int kMainThreadTaskSchedulerComplete = 3;
constexpr int kMainThreadTaskSchedulerTimedOut = 4;
void PublishCurrentJniEnv(JNIEnv* env);
struct JniOnLoadAsyncContext {
JniOnLoadFn fn;
jint result = JNI_ERR;
jnivm::VM* vm;
};
void* RunJniOnLoadWorker(void* arg) {
auto* context = static_cast<JniOnLoadAsyncContext*>(arg);
if (context == nullptr || context->vm == nullptr) {
return nullptr;
}
auto context_class = context->vm->RegisterClass("android/content/Context");
auto activity_class =
context->vm->RegisterClass("com/roblox/client/RobloxActivity");
auto lifecycle_callbacks_class = context->vm->RegisterClass(
"com/roblox/universalapp/activitylifecyclecallbacks/"
"JNIActivityLifecycleCallbacks");
auto settings_class = context->vm->RegisterClass("rbx/JNIRobloxSettings");
settings_class->RegisterMethod(
"nativeInitClientSettings", "()V",
[](JNIEnv* /*env*/, jobject /*obj*/) {
std::cout << " [JNI callback] nativeInitClientSettings invoked\n";
});
static_cast<void>(context_class);
static_cast<void>(activity_class);
static_cast<void>(lifecycle_callbacks_class);
PublishCurrentJniEnv(context->vm->GetJNIEnv());
context->result = context->fn(context->vm->GetJavaVM(), nullptr);
return nullptr;
}
struct EngineStartupContext {
jnivm::VM* vm;
JavaVM* java_vm;
jnivm::RobloxAuthIdentity account_identity;
const mocktail::runtime::SecureRobloxCredential* roblox_credential;
mocktail::runtime::RobloxGameSessionRuntime* game_session_runtime;
mocktail::runtime::LuauCodegenMenuSwitch* luau_codegen_switch;
bool run_prepare_jni;
bool run_set_asset_path;
bool call_real_set_asset_path;
bool run_global_init;
bool run_init_client_settings;
bool run_post_client_settings;
bool run_app_bridge_app_start;
bool run_native_settings;
bool run_set_init_params;
bool run_init_with_params;
bool call_real_init_with_params;
bool run_update_screen_orientation;
bool run_update_surface_app;
bool call_real_update_surface_app;
bool run_start_app_with_params;
bool call_real_start_app_with_params;
bool run_activity_lifecycle;
bool run_game_activity_init;
bool run_game_activity_surface;
bool run_app_lifecycle_active;
bool run_native_fragment_start;
bool run_display_refresh_rate;
bool run_start_lua_app_dm;
NativeGameGlobalInitFn native_global_init;
NativeInitClientSettingsFn native_init_client_settings;
NativeInitClientSettingsSignedFn native_init_client_settings_signed;
NativeInitClientSettingsCachedFn native_init_client_settings_cached;
NativeInitClientSettingsCachedCompressedFn
native_init_client_settings_cached_compressed;
NativePostClientSettingsFn native_post_client_settings;
NativeInitializeNativeFlagsFn native_initialize_native_flags;
NativeAppBridgeAppStartFn native_app_bridge_app_start;
NativeSetIsFirstInstallFn native_set_is_first_install;
NativeSetBaseUrlFn native_set_base_url;
NativeObjectInitFn native_set_device_info;
NativeObjectInitFn native_base_url_protocol_init;
NativeSetStringParamFn native_set_roblox_channel;
NativeSetStringParamFn native_override_channel_platform_name;
NativeSetStringParamFn native_set_roblox_version;
NativeSetStringParamFn native_set_exception_reason_filename;
NativeSetTwoStringParamsFn native_set_base_data_directories;
NativeSetStringParamFn native_set_cache_directory;
NativeSetStringParamFn native_set_files_directory;
NativeSetStringParamFn native_set_external_directory;
NativeSetStringParamFn native_set_preferences_file;
NativeSetStringParamFn native_set_default_app_policy_file;
NativeSetHttpClientProxyFn native_set_http_client_proxy;
NativeNoArgFn native_init_fast_log;
NativeSetTwoStringParamsFn native_set_multiple_cookies;
NativeSetTwoStringParamsFn native_cookie_manager_set_cookie;
NativeSetThreeStringParamsFn native_set_platform_headers_with_idfa;
NativeSetStringParamFn native_set_user_id;
NativeObjectInitFn native_init_asset_manager;
NativeInitStorageManagerFn native_init_storage_manager;
NativeSetPlatformImplFn native_local_storage_set_platform_impl;
NativeAppBridgeSetInitParamsFn native_set_init_params;
NativeNoArgFn native_retry_init;
NativeAppBridgeObjectParamsFn native_init_with_params;
NativeNoArgFn native_update_adapter_init;
NativeUpdateScreenOrientationFn native_update_screen_orientation;
NativeUpdateAppUiSizesFn native_update_app_ui_sizes;
NativeSetTaskSchedulerBackgroundModeFn
native_set_task_scheduler_background_mode;
NativeUpdateSurfaceAppFn native_update_surface_app;
NativeAppBridgeObjectParamsFn native_start_app_with_params;
NativeSendAppReadyFn native_send_app_ready;
NativeSendGameLoadedFn native_send_game_loaded;
NativeSetStringParamFn native_set_asset_path;
NativeActivityLifecycleCallbacks activity_lifecycle_callbacks;
NativeGameActivityInitFn native_game_activity_init;
NativeNoArgFn native_app_lifecycle_set_active;
NativeNoArgFn native_on_fragment_start;
NativePassSupportedRefreshRatesFn native_pass_supported_refresh_rates;
NativePassCurrentDisplayRefreshRateFn native_pass_current_display_refresh_rate;
NativeNoArgFn native_start_lua_app_dm;
NativeUpdateMobileHapticsStateFn native_update_mobile_haptics_state;
};
constexpr size_t kDefaultEngineStackSize = 1024ULL * 1024 * 1024;
constexpr size_t kMinEngineStackSize = 16ULL * 1024 * 1024;
bool IsEnabled(const char* name);
bool IsDisabled(const char* name);
bool IsLegacyBinaryCompatibilityToggle(const char* name) {
if (name == nullptr) {
return false;
}
static constexpr const char* kUnsafePrefixes[] = {
"MOCKTAIL_PATCH_",
"MOCKTAIL_RECOVER_",
"MOCKTAIL_STAGE6_",
"MOCKTAIL_TRACE_STAGE6_",
"MOCKTAIL_DUMP_STAGE6_",
"MOCKTAIL_INSTALL_STAGE6_",
"MOCKTAIL_CALL_STAGE6_",
"MOCKTAIL_SEED_STAGE6_",
"MOCKTAIL_RESET_STAGE6_",
"MOCKTAIL_LIBROBLOX_CTOR_",
"MOCKTAIL_SKIP_LIBROBLOX_CTOR_",
"MOCKTAIL_ALLOW_LIBROBLOX_CTOR_",
"MOCKTAIL_QUARANTINE_LIBROBLOX_",
"MOCKTAIL_EMUTLS_",
};
for (const char* prefix : kUnsafePrefixes) {
if (std::strncmp(name, prefix, std::strlen(prefix)) == 0) {
return true;
}
}
static constexpr const char* kUnsafeExactNames[] = {
"MOCKTAIL_HEADLESS_SIGSEGV_GUARDS",
"MOCKTAIL_DEFER_RBXM_SIGNATURE_CHECK_TO_POST_TTI",
"MOCKTAIL_KEEP_CONSTRUCTOR_EMUTLS_HELPERS_PATCHED",
"MOCKTAIL_MAX_LIBROBLOX_CTORS",
"MOCKTAIL_NO_RECOVER_START_APP",
"MOCKTAIL_RESTORE_KNOWN_EMUTLS_KEYS",
"MOCKTAIL_RUN_LIBROBLOX_CTORS",
"MOCKTAIL_SKIP_CONSTRUCTOR_PATCH_OFFSETS",
};
for (const char* unsafe_name : kUnsafeExactNames) {
if (std::strcmp(name, unsafe_name) == 0) {
return true;
}
}
return false;
}
const char* CachedGetenv(const char* name) {
if (name == nullptr) {
return nullptr;
}
// Launch flags are string literals and do not change after startup. Cache by
// pointer identity so the main-thread pump does not walk environ + allocate
// on every tick.
static std::mutex mutex;
static std::unordered_map<const char*, const char*> cache;
std::lock_guard<std::mutex> lock(mutex);
const auto existing = cache.find(name);
if (existing != cache.end()) {
return existing->second;
}
const char* value = std::getenv(name);
cache.emplace(name, value);
return value;
}
bool IsEnabled(const char* name) {
if (!g_allow_legacy_binary_patches.load(std::memory_order_acquire) &&
IsLegacyBinaryCompatibilityToggle(name)) {
return false;
}
const char* value = CachedGetenv(name);
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
}
bool IsDisabled(const char* name) {
if (!g_allow_legacy_binary_patches.load(std::memory_order_acquire) &&
IsLegacyBinaryCompatibilityToggle(name)) {
return true;
}
const char* value = CachedGetenv(name);
return value != nullptr && std::strcmp(value, "0") == 0;
}
bool StartsWith(const char* value, const char* prefix) {
if (value == nullptr || prefix == nullptr) {
return false;
}
return std::strncmp(value, prefix, std::strlen(prefix)) == 0;
}
bool IsGlSymbol(const char* name) {
return StartsWith(name, "gl");
}
enum class SymbolResolveSource {
kMissing = 0,
kWindow = 1,
kRealGles = 2,
kStub = 3,
kHost = 4,
};
struct SymbolResolveResult {
void* address = nullptr;
SymbolResolveSource source = SymbolResolveSource::kMissing;
};
// dlsym searches the object and its dependency chain, so a
// stub that transitively links SDL or another host library would
// satisfy EGL/GL/Vulkan lookups with unrelated host symbols
//
// So, we should only accept the symbol if its stub owns it.
bool StubOwnsSymbolAddress(void* handle, void* address) {
if (handle == nullptr || address == nullptr) {
return false;
}
Dl_info info = {};
if (::dladdr(address, &info) == 0 || info.dli_fbase == nullptr) {
return false;
}
link_map* map = nullptr;
if (::dlinfo(handle, RTLD_DI_LINKMAP, &map) != 0 || map == nullptr) {
return false;
}
return reinterpret_cast<void*>(map->l_addr) == info.dli_fbase;
}
SymbolResolveResult ResolveSymbolForBionic(const char* name, bool has_window,
void* real_gles_handle,
const std::vector<void*>& stub_handles) {
const bool is_gl_symbol = IsGlSymbol(name);
const bool prefer_real_gles =
has_window && !IsEnabled("MOCKTAIL_GLES_FORCE_STUB") &&
!IsEnabled("MOCKTAIL_GLES_NOOP_DRAW_CALLS");
SymbolResolveResult result;
if (is_gl_symbol && prefer_real_gles) {
result.address = mocktail::window::GetGLProcAddress(name);
if (result.address != nullptr) {
result.source = SymbolResolveSource::kWindow;
return result;
}
}
if (is_gl_symbol && prefer_real_gles && real_gles_handle != nullptr) {
result.address = ::dlsym(real_gles_handle, name);
if (result.address != nullptr) {
result.source = SymbolResolveSource::kRealGles;
return result;
}
}
for (void* h : stub_handles) {
if (void *candidate = ::dlsym(h, name); candidate != nullptr && StubOwnsSymbolAddress(h, candidate)) {
result.address = candidate;
result.source = SymbolResolveSource::kStub;
return result;
}
}
result.address = ::dlsym(RTLD_DEFAULT, name);
if (result.address != nullptr) {
result.source = SymbolResolveSource::kHost;
}
return result;
}
bool TraceAllEnabled() {
return IsEnabled("MOCKTAIL_TRACE_ALL") || IsEnabled("MOCKTAIL_FULL_TRACE");
}
bool VerboseOutputEnabled() {
return IsEnabled("MOCKTAIL_VERBOSE") || TraceAllEnabled();
}
bool LibRobloxConstructorTraceEnabled() {
return IsEnabled("MOCKTAIL_TRACE_LIBROBLOX_CONSTRUCTORS") ||
TraceAllEnabled();
}
void EnableFullTraceIfRequested() {
if (!TraceAllEnabled()) {
return;
}
const char* kTraceEnvNames[] = {
"MOCKTAIL_ENGINE_TRACE",
"MOCKTAIL_JNI_TRACE",
"MOCKTAIL_JNI_VM_TRACE",
"MOCKTAIL_JNI_STRING_TRACE",
"MOCKTAIL_DNS_TRACE",
"MOCKTAIL_GL_TRACE",
"MOCKTAIL_EGL_TRACE",
"MOCKTAIL_WINDOW_TRACE",
"MOCKTAIL_ANDROID_TRACE",
"MOCKTAIL_ASSET_TRACE",
"MOCKTAIL_TRACE_POST_CLIENT_SETTINGS_JNI",
"MOCKTAIL_TRACE_START_LUA_JNI",
};
for (const char* name : kTraceEnvNames) {
setenv(name, "1", 1);
}
}
void SetEnvDefault(const char* name, const char* value) {
if (name == nullptr || value == nullptr) {
return;
}
const char* current = std::getenv(name);
if (current == nullptr || current[0] == '\0') {
setenv(name, value, 1);
}
}
bool IsMocktailStubGlesPath(const char* path) {
if (path == nullptr || path[0] == '\0') {
return true;
}
std::string value(path);
return value == "libGLESv2.so" ||
value.find("/build/libGLESv2.so") != std::string::npos;
}
void* OpenRealGlesLibrary() {
const char* env_library = std::getenv("MOCKTAIL_GLES_LIBRARY");
const char* candidates[] = {
env_library,
"/usr/lib/libGLESv2.so.2",
"/usr/lib64/libGLESv2.so.2",
"libGLESv2.so.2",
"/usr/lib/chromium/libGLESv2.so",
"/usr/lib/chromium-browser/libGLESv2.so",
"/usr/lib/electron42/libGLESv2.so",
"/usr/lib/electron41/libGLESv2.so",
"/usr/lib/electron40/libGLESv2.so",
"/usr/lib/electron39/libGLESv2.so",
"/usr/lib/cef/libGLESv2.so",
};
for (const char* candidate : candidates) {
if (IsMocktailStubGlesPath(candidate)) {
continue;
}
void* handle = ::dlopen(candidate, RTLD_LAZY | RTLD_LOCAL);
if (handle != nullptr) {
std::cout << " [gles] Using real GLES from " << candidate << '\n';
return handle;
}
}
std::cerr << " [gles] real libGLESv2 not found; using Mocktail GLES shim\n";
return nullptr;
}
void ApplyRuntimeDefaults() {
SetEnvDefault("MOCKTAIL_SOBER_MODE", "1");
SetEnvDefault("MOCKTAIL_HEADLESS", "0");
// Startup worker ownership is joined. Returning while it still references
// JNI, runtime, or stack state is never supported.
SetEnvDefault("MOCKTAIL_ENGINE_DETACH", "0");
SetEnvDefault("MOCKTAIL_INIT_CLIENT_SETTINGS", "0");
SetEnvDefault("MOCKTAIL_POST_CLIENT_SETTINGS", "0");
SetEnvDefault("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
// nativeAppBridgeAppStart runs Roblox's StartupController, which builds the
// experimentation protocol that writes ClientSettings/IxpSettings.json. It
// runs inline before V2 init; on its own thread it can block V2 init.
SetEnvDefault("MOCKTAIL_APP_BRIDGE_APP_START", "1");
SetEnvDefault("MOCKTAIL_APP_BRIDGE_APP_START_THREAD", "0");
SetEnvDefault("MOCKTAIL_START_LUA_APP_DM", "1");
SetEnvDefault("MOCKTAIL_START_LUA_APP_DM_INLINE", "0");
// The shipped APK drives these callbacks in a mostly synchronous ASMA flow.
// Keep the background worker path opt-in so the default mirrors release
// ordering and avoids racing heap-sensitive V2 startup state.
SetEnvDefault("MOCKTAIL_START_LUA_APP_DM_THREAD", "0");
SetEnvDefault("MOCKTAIL_START_LUA_APP_DM_AFTER_START_APP", "1");
SetEnvDefault("MOCKTAIL_START_LUA_APP_DM_DELAY_MS", "500");
SetEnvDefault("MOCKTAIL_APP_BRIDGE_HEADLESS_INIT_PARAMS", "0");
SetEnvDefault("MOCKTAIL_PLACE_ID", "0");
SetEnvDefault("MOCKTAIL_CALL_REAL_APP_BRIDGE_INIT", "1");
SetEnvDefault("MOCKTAIL_CALL_REAL_APP_BRIDGE_INIT_THREAD", "0");
SetEnvDefault("MOCKTAIL_APP_BRIDGE_INIT_THREAD_TIMEOUT_MS", "1500");
SetEnvDefault("MOCKTAIL_CALL_REAL_APP_BRIDGE_START", "1");
SetEnvDefault("MOCKTAIL_START_GAME_WITH_PARAM", "0");
SetEnvDefault("MOCKTAIL_SET_APP_BRIDGE_NOTIFICATION_LISTENER", "1");
// APK ASMA calls nativeAppBridgeV2StartAppWithParams synchronously before
// driving post-start surface/Lua callbacks. Keep the worker path opt-in so
// those follow-up callbacks cannot race native StartApp construction.
SetEnvDefault("MOCKTAIL_CALL_REAL_APP_BRIDGE_START_THREAD", "0");
SetEnvDefault("MOCKTAIL_ASMA_START_TASK_SCHEDULER_FOREGROUND", "1");
SetEnvDefault("MOCKTAIL_TASK_SCHEDULER_FOREGROUND_ON_MAIN_THREAD", "1");
SetEnvDefault("MOCKTAIL_NATIVE_FRAGMENT_START", "1");
SetEnvDefault("MOCKTAIL_PASS_CURRENT_DISPLAY_REFRESH_RATE", "1");
SetEnvDefault("MOCKTAIL_PASS_SUPPORTED_REFRESH_RATES", "1");
SetEnvDefault("MOCKTAIL_PASS_ACTIVITY_TO_GAME_SURFACE_PARAMS", "0");
SetEnvDefault("MOCKTAIL_SYNC_START_APP_WITH_GAME", "1");
// APK ASMA publishes queued ready events after the Lua app startup path has
// run. Keep this inline by default; the detached worker can outlive teardown.
SetEnvDefault("MOCKTAIL_SEND_APP_READY", "1");
SetEnvDefault("MOCKTAIL_SEND_APP_READY_THREAD", "0");
SetEnvDefault("MOCKTAIL_SEND_GAME_LOADED", "0");
SetEnvDefault("MOCKTAIL_SEND_GAME_LOADED_THREAD", "0");
SetEnvDefault("MOCKTAIL_UPDATE_SCREEN_ORIENTATION", "0");
SetEnvDefault("MOCKTAIL_STEP_UPDATE_SURFACE_APP", "0");
SetEnvDefault("MOCKTAIL_CALL_REAL_APP_BRIDGE_UPDATE_SURFACE", "0");
SetEnvDefault("MOCKTAIL_CALL_REAL_APP_BRIDGE_UPDATE_SURFACE_THREAD", "0");
// Keep the ASMA/V2 NativeGL path as the default. GameActivity's native app
// glue currently stalls V2 init on surface flags; leave it available opt-in.
SetEnvDefault("MOCKTAIL_STEP_GAME_ACTIVITY_INIT", "0");
SetEnvDefault("MOCKTAIL_STEP_GAME_ACTIVITY_SURFACE", "0");
// GameActivity lifecycle callbacks can block on android_app_set_activity_state
// in some Linux shims; keep them opt-in to avoid startup dead-ends.
SetEnvDefault("MOCKTAIL_GAME_ACTIVITY_LIFECYCLE_CALLBACKS", "0");
SetEnvDefault("MOCKTAIL_NATIVE_SET_USER_ID", "0");
SetEnvDefault("MOCKTAIL_MAIN_THREAD_MESSAGE_PUMP", "0");
SetEnvDefault("MOCKTAIL_IGNORE_WINDOW_CLOSE", "0");
SetEnvDefault("MOCKTAIL_WIN_TITLE", "Roblox");
SetEnvDefault("MOCKTAIL_GRAPHICS_BACKEND", "angle-vulkan");
SetEnvDefault("MOCKTAIL_REQUIRE_REAL_GRAPHICS", "0");
}
bool IsHeadlessMode() {
return IsEnabled("MOCKTAIL_HEADLESS");
}
bool ShouldRunStartupStep(const char* step_env, bool default_value) {
if (IsEnabled(step_env)) {
return true;
}
if (IsDisabled(step_env)) {
return false;
}
return default_value;
}
int GetEnvInt(const char* name, int default_value) {
const char* value = CachedGetenv(name);
if (value == nullptr || value[0] == '\0') {
return default_value;
}
char* end = nullptr;
long parsed = std::strtol(value, &end, 10);
if (end == value) {
return default_value;
}
return static_cast<int>(parsed);
}
jlong GetEnvLong(const char* name, jlong default_value) {
const char* value = CachedGetenv(name);
if (value == nullptr || value[0] == '\0') {
return default_value;
}
char* end = nullptr;
long long parsed = std::strtoll(value, &end, 10);
if (end == value) {
return default_value;
}
return static_cast<jlong>(parsed);
}
uint64_t MonotonicMillis() {
timespec ts{};
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) {
return 0;
}
return static_cast<uint64_t>(ts.tv_sec) * 1000ULL +
static_cast<uint64_t>(ts.tv_nsec) / 1000000ULL;
}
uint64_t MonotonicNanos() {
timespec ts{};
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) {
return 0;
}
return static_cast<uint64_t>(ts.tv_sec) * 1000000000ULL +
static_cast<uint64_t>(ts.tv_nsec);
}
// `attached` reports whether AttachCurrentThread itself succeeded, so a caller
// that caches the env can tell a real attach from the GetJNIEnv fallback.
JNIEnv* AttachMainThreadJniEnv(bool* attached = nullptr) {
if (attached != nullptr) {
*attached = false;
}
if (g_vm_for_main_thread_pump == nullptr) {
return nullptr;
}
JNIEnv* env = nullptr;
JavaVM* java_vm = g_vm_for_main_thread_pump->GetJavaVM();
if (java_vm != nullptr) {
void* raw_env = nullptr;
jint attach_result = java_vm->AttachCurrentThread(&raw_env, nullptr);
if (attach_result == JNI_OK && raw_env != nullptr) {
env = static_cast<JNIEnv*>(raw_env);
if (attached != nullptr) {
*attached = true;
}
} else if (IsEnabled("MOCKTAIL_TRACE_MAIN_THREAD_PUMP")) {
std::cerr << " [main] AttachCurrentThread failed in pump: "
<< attach_result << '\n'
<< std::flush;
}
}
if (env == nullptr) {
env = g_vm_for_main_thread_pump->GetJNIEnv();
}
g_vm_for_main_thread_pump->RestoreFunctions();
PublishCurrentJniEnv(env);
return env;
}
// The pseudo-VM attach is idempotent: once this thread holds a live env for
// the VM, re-attaching hands back the same thread-local pointer. Only the
// main thread calls this, so the cache needs no synchronization. The game
// session installs RestoreGameSessionJniEnvironment, which restores the JNI
// function tables when a guest JNI_OnLoad replaces them.
JNIEnv* MainThreadPumpJniEnv() {
static JNIEnv* cached_env = nullptr;
static const jnivm::VM* cached_vm = nullptr;
if (cached_env != nullptr && cached_vm == g_vm_for_main_thread_pump) {
// Only the attach is cached. The table restore and the env publish run on
// every tick: a guest JNI_OnLoad can replace the function tables, and any
// other thread that publishes its own env overwrites the single global
// slot this one shares.
g_vm_for_main_thread_pump->RestoreFunctions();
PublishCurrentJniEnv(cached_env);
return cached_env;
}
// Only a real attach is worth caching. The GetJNIEnv fallback means this
// thread is not attached to the VM, and the next tick has to retry: a VM
// swap can make AttachCurrentThread fail for one tick and succeed after.
bool attached = false;
JNIEnv* env = AttachMainThreadJniEnv(&attached);
if (env != nullptr && attached) {
cached_env = env;
cached_vm = g_vm_for_main_thread_pump;
}
return env;
}
bool InvokeTaskSchedulerForeground(
JNIEnv* env, jclass native_gl_class,
NativeSetTaskSchedulerBackgroundModeFn native_set_task_scheduler_background_mode,
const char* log_scope) {
if (env == nullptr || native_gl_class == nullptr ||
native_set_task_scheduler_background_mode == nullptr) {
std::cerr << " [" << log_scope
<< "] NativeGLInterface.setTaskSchedulerBackgroundMode skipped: "
<< "missing JNI state\n"
<< std::flush;
return false;
}
jstring reason = env->NewStringUTF("ASMA.start");
std::cout << " [" << log_scope
<< "] NativeGLInterface.setTaskSchedulerBackgroundMode(false, "
<< "ASMA.start)\n"
<< std::flush;
if (sigsetjmp(g_update_surface_app_jmp_buf, 0) == 0) {
g_update_surface_app_recovery_in_progress = kStage6RecoveryInline;
native_set_task_scheduler_background_mode(env, native_gl_class, JNI_FALSE,
reason);
g_update_surface_app_recovery_in_progress = kStage6RecoveryInactive;
std::cout << " [" << log_scope
<< "] NativeGLInterface.setTaskSchedulerBackgroundMode returned\n"
<< std::flush;
return true;
}
g_update_surface_app_recovery_in_progress = kStage6RecoveryInactive;
std::cerr << " [" << log_scope
<< "] setTaskSchedulerBackgroundMode recovered from crash\n"
<< std::flush;
return false;
}
void RunPendingMainThreadTaskSchedulerForeground() {
int expected = kMainThreadTaskSchedulerPending;
if (!g_pending_main_thread_task_scheduler_state.compare_exchange_strong(
expected, kMainThreadTaskSchedulerRunning,
std::memory_order_acq_rel)) {
return;
}
JNIEnv* env = AttachMainThreadJniEnv();
const bool invoked = InvokeTaskSchedulerForeground(
env, g_pending_main_thread_task_scheduler_class,
g_pending_main_thread_task_scheduler_background_mode, "main");
g_pending_main_thread_task_scheduler_recovered.store(invoked ? 0 : 1,
std::memory_order_release);
g_pending_main_thread_task_scheduler_state.store(
kMainThreadTaskSchedulerComplete, std::memory_order_release);
}
bool RunTaskSchedulerForegroundOnMainThread(
NativeSetTaskSchedulerBackgroundModeFn native_set_task_scheduler_background_mode,
jclass native_gl_class) {
if (IsDisabled("MOCKTAIL_TASK_SCHEDULER_FOREGROUND_ON_MAIN_THREAD") ||
IsEnabled("MOCKTAIL_ENGINE_INLINE") || !mocktail::window::IsInitialised()) {
return false;
}
if (native_set_task_scheduler_background_mode == nullptr ||
native_gl_class == nullptr) {
return false;
}
g_pending_main_thread_task_scheduler_background_mode =
native_set_task_scheduler_background_mode;
g_pending_main_thread_task_scheduler_class = native_gl_class;
g_pending_main_thread_task_scheduler_recovered.store(
0, std::memory_order_release);
g_pending_main_thread_task_scheduler_state.store(
kMainThreadTaskSchedulerPending, std::memory_order_release);
std::cout << " [engine] NativeGLInterface.setTaskSchedulerBackgroundMode "
<< "scheduled on main thread\n"
<< std::flush;
const int timeout_ms = GetEnvInt(
"MOCKTAIL_TASK_SCHEDULER_FOREGROUND_MAIN_THREAD_TIMEOUT_MS", 3000);
const uint64_t start_ms = MonotonicMillis();
while (true) {
const int state = g_pending_main_thread_task_scheduler_state.load(
std::memory_order_acquire);
if (state == kMainThreadTaskSchedulerComplete) {
const bool recovered =
g_pending_main_thread_task_scheduler_recovered.load(
std::memory_order_acquire) != 0;
g_pending_main_thread_task_scheduler_state.store(
kMainThreadTaskSchedulerIdle, std::memory_order_release);
if (recovered) {
std::cerr << " [engine] main-thread "
<< "setTaskSchedulerBackgroundMode recovered from crash\n"
<< std::flush;
} else {
std::cout << " [engine] main-thread "
<< "setTaskSchedulerBackgroundMode returned\n"
<< std::flush;
}
return true;
}
if (timeout_ms >= 0) {
const uint64_t now_ms = MonotonicMillis();
if (now_ms >= start_ms &&
now_ms - start_ms >= static_cast<uint64_t>(timeout_ms)) {
g_pending_main_thread_task_scheduler_state.store(
kMainThreadTaskSchedulerTimedOut, std::memory_order_release);
std::cerr << " [engine] main-thread "
<< "setTaskSchedulerBackgroundMode timed out after "
<< timeout_ms << " ms\n"
<< std::flush;
return true;
}
}
usleep(1000);
}
}
// The profile trace writer is compiled into the Vulkan adapter, which the host
// dlopens RTLD_GLOBAL, so the pump slice goes through symbols resolved from it.
// Until the adapter is loaded there is no writer and nothing is recorded.
struct PumpTraceHooks {
using ActiveFn = mocktail::graphics::ChromeTraceWriter* (*)();
using ClockFn = std::uint64_t (*)();
using SliceFn = void (*)(mocktail::graphics::ChromeTraceWriter*, const char*,
const char*, std::uint64_t, std::uint64_t,
const mocktail::graphics::TraceArg*, std::size_t);
ActiveFn active = nullptr;
ClockFn clock = nullptr;
SliceFn slice = nullptr;
};
const PumpTraceHooks& PumpTrace() {
static PumpTraceHooks hooks;
// The adapter loads during startup, so a few hundred ticks cover it. After
// that the lookups stop and the pump stays untraced.
static int attempts = 0;
if (hooks.active == nullptr && attempts++ < 1000) {
PumpTraceHooks resolved;
resolved.active = reinterpret_cast<PumpTraceHooks::ActiveFn>(
::dlsym(RTLD_DEFAULT, "_ZN8mocktail8graphics18ActiveProfileTraceEv"));
resolved.clock = reinterpret_cast<PumpTraceHooks::ClockFn>(
::dlsym(RTLD_DEFAULT, "_ZN8mocktail8graphics15TraceClockNanosEv"));
resolved.slice = reinterpret_cast<PumpTraceHooks::SliceFn>(::dlsym(
RTLD_DEFAULT,
"_ZN8mocktail8graphics17ChromeTraceWriter5SliceEPKcS3_mmPKNS0_"
"8TraceArgEm"));
if (resolved.active != nullptr && resolved.clock != nullptr &&
resolved.slice != nullptr) {
hooks = resolved;
}
}
return hooks;
}
void PumpRobloxMainThreadMessagesOnce() {
static const bool force_early =
IsEnabled("MOCKTAIL_FORCE_EARLY_MAIN_THREAD_MESSAGE_PUMP");
static const bool pump_disabled =
IsDisabled("MOCKTAIL_MAIN_THREAD_MESSAGE_PUMP");
static const bool trace_pump =
IsEnabled("MOCKTAIL_TRACE_MAIN_THREAD_PUMP");
static const int pump_limit =
GetEnvInt("MOCKTAIL_MAIN_THREAD_MESSAGE_PUMP_LIMIT", 0);
if (g_main_thread_message_pump_ready.load() == 0 && !force_early) {
static bool logged_not_ready = false;
if (!logged_not_ready && trace_pump) {
logged_not_ready = true;
std::cerr << " [main] nativeCallMessagesFromMainThread pump not ready\n"
<< std::flush;
}
return;
}
if (g_native_call_messages_from_main_thread == nullptr ||
g_vm_for_main_thread_pump == nullptr ||
g_native_gl_class_for_main_thread == nullptr || pump_disabled) {
static bool logged_unavailable = false;
if (!logged_unavailable && trace_pump) {
logged_unavailable = true;
std::cerr << " [main] nativeCallMessagesFromMainThread pump unavailable"
<< " fn="
<< reinterpret_cast<const void*>(
g_native_call_messages_from_main_thread)
<< " vm=" << static_cast<void*>(g_vm_for_main_thread_pump)
<< " class="
<< reinterpret_cast<const void*>(
g_native_gl_class_for_main_thread)
<< " disabled=" << pump_disabled << '\n'
<< std::flush;
}
return;
}
JNIEnv* env = MainThreadPumpJniEnv();
if (env == nullptr) {
static bool logged_missing_env = false;
if (!logged_missing_env && trace_pump) {
logged_missing_env = true;
std::cerr << " [main] nativeCallMessagesFromMainThread pump has no JNIEnv\n"
<< std::flush;
}
return;
}
static int pump_count = 0;
++pump_count;
if (pump_limit > 0 && pump_count > pump_limit) {
return;
}
if (__builtin_expect(trace_pump, 0)) {
if (pump_count <= 10 || pump_count % 100 == 0) {
std::cerr << " [main] nativeCallMessagesFromMainThread pump #"
<< pump_count << " env=" << static_cast<void*>(env) << '\n'
<< std::flush;
}
}
const PumpTraceHooks& hooks = PumpTrace();
mocktail::graphics::ChromeTraceWriter* trace =
hooks.active != nullptr ? hooks.active() : nullptr;
const std::uint64_t pump_start_ns = trace != nullptr ? hooks.clock() : 0;
// CPU time separates a step that computes from one that waits.
const auto thread_cpu_ns = [] {
timespec now{};
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &now);
return static_cast<std::uint64_t>(now.tv_sec) * 1'000'000'000ULL +
static_cast<std::uint64_t>(now.tv_nsec);
};
const std::uint64_t pump_start_cpu_ns =
trace != nullptr ? thread_cpu_ns() : 0;
g_native_call_messages_from_main_thread(
env, g_native_gl_class_for_main_thread);
if (trace != nullptr) {
// Empty polls run at kilohertz rates and would swamp the trace, so only
// calls that ran a message are recorded.
const std::uint64_t pump_end_ns = hooks.clock();
if (pump_end_ns - pump_start_ns >= 20'000) {
const mocktail::graphics::TraceArg args[] = {
{"cpu_us", static_cast<std::int64_t>(
(thread_cpu_ns() - pump_start_cpu_ns) / 1000)}};
hooks.slice(trace, "nativeCallMessagesFromMainThread", "pump",
pump_start_ns, pump_end_ns, args, 1);
}
}
if (__builtin_expect(trace_pump, 0)) {
if (pump_count <= 10 || pump_count % 100 == 0) {
std::cerr << " [main] nativeCallMessagesFromMainThread returned #"
<< pump_count << '\n'
<< std::flush;
}
}
}
void PumpStartupOwnerThread(void* /*context*/) {
RunPendingMainThreadTaskSchedulerForeground();
PumpRobloxMainThreadMessagesOnce();
}
std::string GetEnvString(const char* name, const char* default_value) {
const char* value = CachedGetenv(name);
if (value == nullptr || value[0] == '\0') {
return default_value ? default_value : "";
}
return value;
}
bool HasEnvValue(const char* name) {
const char* value = CachedGetenv(name);
return value != nullptr && value[0] != '\0';
}
struct BionicAddrInfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
socklen_t ai_addrlen;
char* ai_canonname;
struct sockaddr* ai_addr;
BionicAddrInfo* ai_next;
};
static_assert(sizeof(BionicAddrInfo) == 48,
"unexpected bionic addrinfo size");
bool DnsTraceEnabled() {
return IsEnabled("MOCKTAIL_DNS_TRACE");
}
addrinfo HostHintsFromBionic(const BionicAddrInfo* hints) {
addrinfo host_hints{};
if (!hints) {
return host_hints;
}
host_hints.ai_flags = hints->ai_flags;
host_hints.ai_family = hints->ai_family;
host_hints.ai_socktype = hints->ai_socktype;
host_hints.ai_protocol = hints->ai_protocol;
return host_hints;
}
BionicAddrInfo* BionicAddrInfoFromHost(const addrinfo* host) {
BionicAddrInfo* head = nullptr;
BionicAddrInfo* tail = nullptr;
for (const addrinfo* current = host; current; current = current->ai_next) {
auto* node =
static_cast<BionicAddrInfo*>(std::calloc(1, sizeof(BionicAddrInfo)));
if (!node) {
break;
}
node->ai_flags = current->ai_flags;
node->ai_family = current->ai_family;
node->ai_socktype = current->ai_socktype;
node->ai_protocol = current->ai_protocol;
node->ai_addrlen = current->ai_addrlen;
if (current->ai_canonname) {
node->ai_canonname = ::strdup(current->ai_canonname);
}
if (current->ai_addr && current->ai_addrlen > 0) {
node->ai_addr = static_cast<sockaddr*>(std::malloc(current->ai_addrlen));
if (node->ai_addr) {
std::memcpy(node->ai_addr, current->ai_addr, current->ai_addrlen);
}
}
if (!head) {
head = node;
} else {
tail->ai_next = node;
}
tail = node;
}
return head;
}
extern "C" int mocktail_getaddrinfo(const char* node, const char* service,
const BionicAddrInfo* hints,
BionicAddrInfo** result) {
if (!result) {
return EAI_FAIL;
}
*result = nullptr;
addrinfo host_hints = HostHintsFromBionic(hints);
addrinfo* host_result = nullptr;
int rc = ::getaddrinfo(node, service, hints ? &host_hints : nullptr,
&host_result);
if (DnsTraceEnabled()) {
std::cout << " [dns] getaddrinfo node=" << (node ? node : "(null)")
<< " service=" << (service ? service : "(null)")
<< " family=" << (hints ? hints->ai_family : 0)
<< " socktype=" << (hints ? hints->ai_socktype : 0)
<< " rc=" << rc
<< " message=" << (rc == 0 ? "ok" : ::gai_strerror(rc))
<< '\n';
}
if (rc != 0) {
return rc;
}
*result = BionicAddrInfoFromHost(host_result);
::freeaddrinfo(host_result);
return *result ? 0 : EAI_MEMORY;
}
extern "C" void mocktail_freeaddrinfo(BionicAddrInfo* info) {
while (info) {
BionicAddrInfo* next = info->ai_next;
std::free(info->ai_canonname);
std::free(info->ai_addr);
std::free(info);
info = next;
}
}
extern "C" hostent* mocktail_gethostbyname(const char* name) {
hostent* result = ::gethostbyname(name);
if (DnsTraceEnabled()) {
std::cout << " [dns] gethostbyname name=" << (name ? name : "(null)")
<< " result=" << reinterpret_cast<void*>(result) << '\n';
}
return result;
}
extern "C" int mocktail_mprotect(void* addr, size_t len, int prot) {
if (addr == nullptr || len == 0) {
errno = EINVAL;
return -1;
}
long page_size = sysconf(_SC_PAGESIZE);
if (page_size <= 0) {
errno = EINVAL;
return -1;
}
uintptr_t start = reinterpret_cast<uintptr_t>(addr);
uintptr_t page_mask = static_cast<uintptr_t>(page_size) - 1;
uintptr_t page_start = start & ~page_mask;
uintptr_t end = start + len;
if (end < start) {
errno = EINVAL;
return -1;
}
uintptr_t page_end = (end + page_mask) & ~page_mask;
if (page_end < page_start) {
errno = EINVAL;
return -1;
}
return ::mprotect(reinterpret_cast<void*>(page_start),
static_cast<size_t>(page_end - page_start), prot);
}
void RegisterBionicDnsWrappers() {
linker::RegisterSymbol("getaddrinfo",
reinterpret_cast<void*>(mocktail_getaddrinfo));
linker::RegisterSymbol("freeaddrinfo",
reinterpret_cast<void*>(mocktail_freeaddrinfo));
linker::RegisterSymbol("gethostbyname",
reinterpret_cast<void*>(mocktail_gethostbyname));
}
void RegisterBionicPathWrappers() {
linker::RegisterSymbol("open", reinterpret_cast<void*>(mocktail_open));
linker::RegisterSymbol("__open_2", reinterpret_cast<void*>(mocktail___open_2));
linker::RegisterSymbol("fopen", reinterpret_cast<void*>(mocktail_fopen));
linker::RegisterSymbol("access", reinterpret_cast<void*>(mocktail_access));
linker::RegisterSymbol("stat", reinterpret_cast<void*>(mocktail_stat));
linker::RegisterSymbol("lstat", reinterpret_cast<void*>(mocktail_lstat));
linker::RegisterSymbol("statvfs", reinterpret_cast<void*>(mocktail_statvfs));
linker::RegisterSymbol("statfs", reinterpret_cast<void*>(mocktail_statfs));
linker::RegisterSymbol("mkdir", reinterpret_cast<void*>(mocktail_mkdir));
linker::RegisterSymbol("opendir", reinterpret_cast<void*>(mocktail_opendir));
linker::RegisterSymbol("rename", reinterpret_cast<void*>(mocktail_rename));
linker::RegisterSymbol("unlink", reinterpret_cast<void*>(mocktail_unlink));
linker::RegisterSymbol("rmdir", reinterpret_cast<void*>(mocktail_rmdir));
linker::RegisterSymbol("realpath", reinterpret_cast<void*>(mocktail_realpath));
linker::RegisterSymbol("readlink", reinterpret_cast<void*>(mocktail_readlink));
linker::RegisterSymbol("__readlink_chk",
reinterpret_cast<void*>(mocktail___readlink_chk));
}
mocktail::runtime::RuntimePaths CurrentRuntimePaths() {
const mocktail::runtime::ProcessEnvironment environment;
return mocktail::runtime::RuntimePaths::FromEnvironment(environment);
}
std::string SoberDataRoot() {
return CurrentRuntimePaths().sober_data_root().string();
}
std::string SoberCacheRoot() {
return CurrentRuntimePaths().sober_cache_root().string();
}
std::string MocktailCacheRoot() {
return CurrentRuntimePaths().cache_root().string();
}
std::string MocktailConfigRoot() {
return CurrentRuntimePaths().config_root().string();
}
std::string DefaultSoberAwarePath(const char* sober_path,
const char* fallback_path) {
return CurrentRuntimePaths()
.DefaultSoberAwarePath(sober_path ? sober_path : "",
fallback_path ? fallback_path : "")
.string();
}
std::string DefaultAssetPath() {
return CurrentRuntimePaths().DefaultAssetPath().string();
}
bool RegisterFreshGamePresentObserver(
void* context, mocktail::runtime::FreshLaunchPresentObserver observer,
void* observer_context) {
if (context == nullptr) {
return false;
}
return static_cast<mocktail::window::ScopedPresentObserver*>(context)
->Register(observer, observer_context);
}
void ClearFreshGamePresentObserver(void* context) {
if (context != nullptr) {
static_cast<mocktail::window::ScopedPresentObserver*>(context)->Reset();
}
}
jobject CreateExperienceRawCallback(
void* context, std::shared_ptr<void> callback_context,
void (*run)(void*, JNIEnv*, jstring)) {
if (context == nullptr || run == nullptr) {
return nullptr;
}
return static_cast<jnivm::VM*>(context)->CreateMessageBusRawCallback(
std::move(callback_context), jnivm::MessageBusRawCallbacks{run});
}
void ClearExperienceRawCallback(void* context, jobject callback) {
if (context != nullptr) {
static_cast<jnivm::VM*>(context)->ClearMessageBusRawCallback(callback);
}
}
jobject CreateMessageBusRequestHandler(void *context,
std::shared_ptr<void> callback_context,
std::string (*run)(void *, JNIEnv *,
jstring)) {
if (context == nullptr || run == nullptr) {
return nullptr;
}
return static_cast<jnivm::VM *>(context)->CreateMessageBusRequestHandler(
std::move(callback_context),
jnivm::MessageBusRequestHandlerCallbacks{run});
}
void ClearMessageBusRequestHandler(void *context, jobject handler) {
if (context != nullptr) {
static_cast<jnivm::VM *>(context)->ClearMessageBusRequestHandler(handler);
}
}
jobject CreateBrowserServiceMemStorageCallback(
void *context, std::shared_ptr<void> callback_context,
void (*on_item_set)(void *, JNIEnv *, jstring)) {
if (context == nullptr || on_item_set == nullptr) {
return nullptr;
}
return static_cast<jnivm::VM *>(context)->CreateMemStorageCallback(
std::move(callback_context),
jnivm::MemStorageCallbackCallbacks{on_item_set});
}
void ClearBrowserServiceMemStorageCallback(void *context, jobject callback) {
if (context != nullptr) {
static_cast<jnivm::VM *>(context)->ClearMemStorageCallback(callback);
}
}
jobject CreateAsyncMessageBusRequestHandler(
void* context, std::shared_ptr<void> callback_context,
void (*run)(void*, JNIEnv*, jstring, jstring)) {
return context ? static_cast<jnivm::VM*>(context)
->CreateMessageBusAsyncRequestHandler(
std::move(callback_context),
jnivm::MessageBusAsyncRequestHandlerCallbacks{run})
: nullptr;
}
void ClearAsyncMessageBusRequestHandler(void* context, jobject handler) {
if (context) {
static_cast<jnivm::VM*>(context)->ClearMessageBusAsyncRequestHandler(
handler);
}
}
struct ExperienceLifecycleTarget {
std::weak_ptr<mocktail::runtime::RobloxExperienceComposition> composition;
};
void NotifyLuaAppDidReturn(void* context) {
if (context == nullptr) {
return;
}
const std::shared_ptr<mocktail::runtime::RobloxExperienceComposition>
composition =
static_cast<ExperienceLifecycleTarget*>(context)->composition.lock();
if (composition != nullptr) {
composition->NotifyLuaAppDidReturn();
}
}
mocktail::runtime::GameSessionUpdateResult ExperienceSurfaceCreated(
void* context, uint64_t generation) {
return static_cast<mocktail::runtime::RobloxExperienceComposition*>(context)
->SurfaceCreated(generation);
}
mocktail::runtime::GameSessionUpdateResult ExperienceSurfaceChanged(
void* context, mocktail::runtime::GameSurface surface) {
return static_cast<mocktail::runtime::RobloxExperienceComposition*>(context)
->SurfaceChanged(std::move(surface));
}
mocktail::runtime::GameSessionUpdateResult ExperienceSurfaceDestroyed(
void* context, uint64_t generation) {
return static_cast<mocktail::runtime::RobloxExperienceComposition*>(context)
->SurfaceDestroyed(generation);
}
std::string GetEnvStringDefaultPath(const char* name,
const std::string& default_value) {
const char* value = std::getenv(name);
if (value == nullptr || value[0] == '\0') {
return default_value;
}
return value;
}
bool EnsureDirectory(const std::string& path) {
return mocktail::runtime::RuntimePaths::EnsureDirectory(path);
}
void EnsureAndroidDirectory(const std::string& android_path) {
std::string host_path = libc_shim::TranslatePath(android_path);
if (EnsureDirectory(host_path) && host_path != android_path &&
IsEnabled("MOCKTAIL_ENGINE_TRACE")) {
std::cerr << " [engine] directory " << android_path << " -> "
<< host_path << '\n';
}
}
size_t GetEngineStackSize() {
const char* value = std::getenv("MOCKTAIL_ENGINE_STACK_MB");
if (value == nullptr || value[0] == '\0') {
return kDefaultEngineStackSize;
}
char* end = nullptr;
unsigned long long stack_mb = std::strtoull(value, &end, 10);
if (end == value || stack_mb == 0) {
return kDefaultEngineStackSize;
}
size_t stack_size = static_cast<size_t>(stack_mb) * 1024 * 1024;
return stack_size < kMinEngineStackSize ? kMinEngineStackSize : stack_size;
}
void PrintStage(int stage, const char* description) {
std::cout << "[stage " << stage << "] " << description << '\n'
<< std::flush;
}
bool IsUnsafeSoftTimeoutModule(void* rip) {
Dl_info dlinfo;
if (rip == nullptr || dladdr(rip, &dlinfo) == 0 || dlinfo.dli_fname == nullptr) {
return true;
}
const char* module = dlinfo.dli_fname;
return std::strstr(module, "libc.so") != nullptr ||
std::strstr(module, "libpthread.so") != nullptr ||
std::strstr(module, "ld-linux") != nullptr ||
std::strstr(module, "libstdc++") != nullptr ||
std::strstr(module, "libgcc_s") != nullptr;
}
void JniOnLoadTimeoutAlarm(int, siginfo_t* info, void* context) {
static_cast<void>(info);
auto* uc = static_cast<ucontext_t*>(context);
#if defined(__x86_64__)
auto rip = uc ? static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RIP]) : 0;
auto rsp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RSP]) : 0;
auto rbp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RBP]) : 0;
auto rax = uc ? static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RAX]) : 0;
const char* regs_format =
" [timeout] RIP=0x%016llx RSP=0x%016llx RBP=0x%016llx "
"RAX=0x%016llx\n";
#elif defined(__aarch64__)
auto rip = uc ? static_cast<uintptr_t>(uc->uc_mcontext.pc) : 0;
auto rsp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.sp) : 0;
auto rbp = uc ? static_cast<uintptr_t>(uc->uc_mcontext.regs[29]) : 0;
auto rax = uc ? static_cast<uintptr_t>(uc->uc_mcontext.regs[0]) : 0;
const char* regs_format =
" [timeout] PC=0x%016llx SP=0x%016llx FP=0x%016llx "
"X0=0x%016llx\n";
#else
uintptr_t rip = 0, rsp = 0, rbp = 0, rax = 0;
const char* regs_format =
" [timeout] PC=0x%016llx SP=0x%016llx FP=0x%016llx "
"X0=0x%016llx\n";
#endif
char regs_msg[192];
int len = std::snprintf(
regs_msg, sizeof(regs_msg), regs_format,
static_cast<unsigned long long>(rip), static_cast<unsigned long long>(rsp),
static_cast<unsigned long long>(rbp), static_cast<unsigned long long>(rax));
write(2, regs_msg, static_cast<size_t>(len));
if (g_jni_onload_in_progress == 0 || g_jni_onload_timings_printed != 0) {
return;
}
g_jni_onload_timings_printed = 1;
const char prefix[] =
" [timeout] JNI_OnLoad still in progress; stack snapshot:\n";
write(2, prefix, sizeof(prefix) - 1);
PrintContextBacktrace(uc, " ");
PrintBacktraceNoSig(" ");
if (g_jni_onload_soft_timeout != 0 && g_jni_onload_jmp_armed != 0 &&
!IsUnsafeSoftTimeoutModule(reinterpret_cast<void*>(rip))) {
g_jni_onload_jmp_armed = 0;
g_jni_onload_in_progress = 0;
siglongjmp(g_jni_onload_jmp_buf, 1);
return;
}
if (g_jni_onload_soft_timeout != 0 && g_jni_onload_jmp_armed != 0) {
const char skip_msg[] =
" [timeout] soft timeout skipped for unsafe module; waiting\n";
write(2, skip_msg, sizeof(skip_msg) - 1);
return;
}
_exit(124);
}
void PublishCurrentJniEnv(JNIEnv* env) {
using SetCurrentJniEnvFn = void (*)(void*);
// RTLD_DEFAULT resolves this to the runtime's own exported setter, so the
// address is fixed for the life of the process.
static SetCurrentJniEnvFn set_current_jni_env =
reinterpret_cast<SetCurrentJniEnvFn>(
::dlsym(RTLD_DEFAULT, "mocktail_set_current_jni_env"));
if (set_current_jni_env) {
set_current_jni_env(env);
}
}
bool EngineTraceEnabled() {
return IsEnabled("MOCKTAIL_ENGINE_TRACE");
}
void PreloadPthreadSymbols() {
using PreloadPthreadSymbolsFn = void (*)();
auto* preload_pthread_symbols = reinterpret_cast<PreloadPthreadSymbolsFn>(
::dlsym(RTLD_DEFAULT, "mocktail_preload_pthread_symbols"));
if (preload_pthread_symbols) {
preload_pthread_symbols();
}
}
void EngineLog(const char* message) {
if (EngineTraceEnabled()) {
std::cerr << " [engine] " << message << '\n';
}
}
void EngineLogPtr(const char* name, const void* ptr) {
if (EngineTraceEnabled()) {
std::cerr << " [engine] " << name << "=0x"
<< std::hex << reinterpret_cast<uintptr_t>(ptr) << std::dec
<< '\n';
}
}
void PrintStepDecision(const char* name, bool enabled) {
if (!VerboseOutputEnabled()) {
return;
}
std::cout << " [engine] " << (enabled ? "run " : "skip ") << name << '\n'
<< std::flush;
}
void PrintNativeBypass(const char* name, const char* flag) {
if (!VerboseOutputEnabled()) {
return;
}
std::cout << " [engine] bypass native " << name << " (set " << flag
<< "=1 to call Roblox entrypoint)\n"
<< std::flush;
}
// Free counterpart for host-owned allocations. Pointers without the Mocktail
// alloc header are ignored so native freelist walks do not touch uninit state
// after constructors are skipped on the host.
mocktail::compat::HostAbiExperimentResult g_host_abi_install_result;
bool g_host_abi_install_attempted = false;
bool HostAbiExperimentRequested() {
return g_allow_host_abi_bridges.load(std::memory_order_acquire) &&
!IsDisabled("MOCKTAIL_HOST_ABI_BRIDGES");
}
bool InstallActiveHostAbiExperiment(uintptr_t libroblox_base) {
if (g_host_abi_install_attempted) {
return static_cast<bool>(g_host_abi_install_result);
}
g_host_abi_install_attempted = true;
if (!HostAbiExperimentRequested()) {
std::cout << " [compat] host ABI profile disabled by policy\n"
<< std::flush;
return false;
}
const mocktail::compat::HostAbiProfile* profile =
g_active_host_abi_profile.load(std::memory_order_acquire);
if (libroblox_base == 0 || profile == nullptr) {
std::cerr << " [compat] host ABI install has no active profile/base\n"
<< std::flush;
return false;
}
const mocktail::compat::HostAbiBridgeTargets targets{
reinterpret_cast<void*>(&mocktail::compat::HostAllocate),
reinterpret_cast<void*>(&mocktail::compat::HostReallocate),
reinterpret_cast<void*>(&mocktail::compat::HostAlignedAllocate),
reinterpret_cast<void*>(&mocktail::compat::HostFree),
reinterpret_cast<void*>(&mocktail::compat::HostUsableSize),
reinterpret_cast<void*>(&mocktail::compat::HostAllocatorObjectAllocate),
reinterpret_cast<void*>(&NullVtableStub),
};
const char* allocator_bridge_override =
std::getenv("MOCKTAIL_HOST_ALLOCATOR_BRIDGES");
const mocktail::compat::HostAllocatorStrategy allocator_strategy =
profile->ResolveAllocatorStrategy(
allocator_bridge_override != nullptr,
IsEnabled("MOCKTAIL_HOST_ALLOCATOR_BRIDGES"));
const bool install_allocator_bridges =
allocator_strategy ==
mocktail::compat::HostAllocatorStrategy::kHostBridges;
const mocktail::compat::HostAbiExperimentOptions options{
install_allocator_bridges,
install_allocator_bridges &&
!IsDisabled("MOCKTAIL_HOST_ALLOCATOR_OBJECT"),
install_allocator_bridges && !IsDisabled("MOCKTAIL_HOST_EMPTY_STRING"),
install_allocator_bridges &&
!IsDisabled("MOCKTAIL_HOST_ALLOC_ARENA_INIT"),
install_allocator_bridges &&
!IsDisabled("MOCKTAIL_HOST_JNI_SINGLETON_SEED"),
};
g_host_abi_install_result = mocktail::compat::InstallHostAbiExperiment(
libroblox_base, *profile, targets, options);
return static_cast<bool>(g_host_abi_install_result);
}
bool InitializeActiveHostAbiThread() {
if (!HostAbiExperimentRequested()) {
return true;
}
const mocktail::compat::HostAbiProfile* profile =
g_active_host_abi_profile.load(std::memory_order_acquire);
if (profile == nullptr || g_libroblox_base == 0) {
return false;
}
return mocktail::compat::InitializeHostAbiThread(g_libroblox_base, *profile);
}
static uintptr_t NullVtableStub() { return 0; }
void PrintBacktraceNoSig(const char* prefix) { (void)prefix; }
void PrintContextBacktrace(ucontext_t* ucontext, const char* prefix) {
(void)ucontext;
(void)prefix;
}
bool EnvOffsetListContains(const char* name, uintptr_t offset) {
const char* skip_list = std::getenv(name);
if (skip_list == nullptr || *skip_list == '\0') {
return false;
}
const char* cursor = skip_list;
while (*cursor != '\0') {
while (*cursor == ',' || std::isspace(static_cast<unsigned char>(*cursor))) {
++cursor;
}
if (*cursor == '\0') {
break;
}
char* end = nullptr;
errno = 0;
unsigned long long parsed = std::strtoull(cursor, &end, 0);
if (end == cursor) {
break;
}
if (errno == 0 && static_cast<uintptr_t>(parsed) == offset) {
return true;
}
cursor = end;
}
return false;
}
extern "C" int mocktail_recover_stack_chk_fail() {
if (g_init_with_params_recovery_in_progress != 0) {
g_init_with_params_recovery_in_progress = 0;
siglongjmp(g_init_with_params_jmp_buf, 1);
}
if (g_start_app_with_params_recovery_in_progress != 0) {
g_start_app_with_params_recovery_in_progress = 0;
siglongjmp(g_start_app_with_params_jmp_buf, 1);
}
if (g_start_lua_app_dm_recovery_in_progress != 0) {
g_start_lua_app_dm_recovery_in_progress = 0;
siglongjmp(g_start_lua_app_dm_jmp_buf, 1);
}
if (g_game_global_init_recovery_in_progress != 0) {
g_game_global_init_recovery_in_progress = 0;
siglongjmp(g_game_global_init_jmp_buf, 1);
}
if (g_init_client_settings_recovery_in_progress != 0) {
g_init_client_settings_recovery_in_progress = 0;
siglongjmp(g_init_client_settings_jmp_buf, 1);
}
if (g_post_client_settings_recovery_in_progress != 0) {
g_post_client_settings_recovery_in_progress = 0;
siglongjmp(g_post_client_settings_jmp_buf, 1);
}
const char msg[] =
" [patch] stack check bridge had no active recovery target\n";
write(2, msg, sizeof(msg) - 1);
return 0;
}
bool EnvIndexRangeListContains(const char* name, size_t index) {
const char* range_list = std::getenv(name);
if (range_list == nullptr || *range_list == '\0') {
return false;
}
const char* cursor = range_list;
while (*cursor != '\0') {
while (*cursor == ',' || std::isspace(static_cast<unsigned char>(*cursor))) {
++cursor;
}
if (*cursor == '\0') {
break;
}
char* end = nullptr;
unsigned long long start = std::strtoull(cursor, &end, 0);
if (end == cursor) {
break;
}
unsigned long long stop = start;
cursor = end;
if (*cursor == '-') {
++cursor;
char* range_end = nullptr;
stop = std::strtoull(cursor, &range_end, 0);
if (range_end == cursor) {
break;
}
cursor = range_end;
}
if (start <= index && index <= stop) {
return true;
}
while (*cursor != '\0' && *cursor != ',') {
++cursor;
}
}
return false;
}
bool LibRobloxConstructorOffsetSkipped(uintptr_t offset) {
return EnvOffsetListContains("MOCKTAIL_SKIP_LIBROBLOX_CTOR_OFFSETS", offset);
}
bool LibRobloxConstructorIndexSkipped(size_t index) {
return EnvIndexRangeListContains("MOCKTAIL_SKIP_LIBROBLOX_CTOR_INDEX_RANGES",
index);
}
bool LibRobloxConstructorIndexAllowed(size_t index) {
return EnvIndexRangeListContains("MOCKTAIL_ALLOW_LIBROBLOX_CTOR_INDEX_RANGES",
index);
}
static size_t g_current_ctor_index = 0;
static std::vector<uintptr_t> g_original_ctors;
static uintptr_t g_libroblox_base_static = 0;
static size_t g_libroblox_ctor_start_index = 0;
static size_t g_libroblox_ctor_end_index = 0;
static size_t g_libroblox_ctor_executed_count = 0;
static size_t g_libroblox_ctor_skipped_range_count = 0;
static size_t g_libroblox_ctor_skipped_default_count = 0;
static size_t g_libroblox_ctor_skipped_index_count = 0;
static size_t g_libroblox_ctor_skipped_env_count = 0;
bool LibRobloxConstructorDefaultQuarantineEnabled() {
// The Build-ID profile already supplies the final allowlisted range.
// Applying the legacy "only index 0" quarantine on top would contradict it.
if (g_allow_host_constructor_replay.load(std::memory_order_acquire)) {
return false;
}
if (!IsEnabled("MOCKTAIL_RUN_LIBROBLOX_CTORS")) {
return false;
}
if (IsDisabled("MOCKTAIL_QUARANTINE_LIBROBLOX_UNSAFE_CTORS")) {
return false;
}
const char* policy = std::getenv("MOCKTAIL_LIBROBLOX_CTOR_POLICY");
if (policy != nullptr && (std::strcmp(policy, "all") == 0 ||
std::strcmp(policy, "unsafe") == 0)) {
return false;
}
return true;
}
bool LibRobloxConstructorDefaultQuarantineSkipped(size_t index) {
if (!LibRobloxConstructorDefaultQuarantineEnabled()) {
return false;
}
if (index == 0 || LibRobloxConstructorIndexAllowed(index)) {
return false;
}
return true;
}
size_t GetCtorIndexEnv(const char* name, size_t default_value,
size_t max_value) {
int value = GetEnvInt(name, -1);
if (value < 0) {
return default_value;
}
return std::min(static_cast<size_t>(value), max_value);
}
void MaybePrintLibRobloxConstructorSummary(size_t idx) {
if (idx + 1 != g_original_ctors.size()) {
return;
}
std::cout << " [ctor] Summary: executed="
<< g_libroblox_ctor_executed_count
<< " skipped_by_range=" << g_libroblox_ctor_skipped_range_count
<< " skipped_by_default=" << g_libroblox_ctor_skipped_default_count
<< " skipped_by_index=" << g_libroblox_ctor_skipped_index_count
<< " skipped_by_env=" << g_libroblox_ctor_skipped_env_count
<< '\n'
<< std::flush;
}
extern "C" void MocktailConstructorWrapper() {
if (g_current_ctor_index >= g_original_ctors.size()) {
std::cerr << " [ctor] Warning: constructor index " << g_current_ctor_index
<< " out of bounds!\n" << std::flush;
return;
}
size_t idx = g_current_ctor_index++;
uintptr_t orig = g_original_ctors[idx];
uintptr_t offset = orig - g_libroblox_base_static;
const bool trace_constructor = LibRobloxConstructorTraceEnabled();
if (trace_constructor) {
std::cout << " [ctor] [" << idx << "/" << g_original_ctors.size()
<< "] offset 0x" << std::hex << offset << std::dec << '\n'
<< std::flush;
}
if (idx < g_libroblox_ctor_start_index ||
idx >= g_libroblox_ctor_end_index) {
++g_libroblox_ctor_skipped_range_count;
if (trace_constructor) {
std::cout << " [ctor] [" << idx << "] Skipped by ctor index range.\n"
<< std::flush;
}
MaybePrintLibRobloxConstructorSummary(idx);
return;
}
const mocktail::compat::HostAbiProfile* active_host_profile =
g_active_host_abi_profile.load(std::memory_order_acquire);
const bool use_native_mimalloc =
g_host_abi_install_attempted &&
g_host_abi_install_result.uses_native_mimalloc;
if (g_allow_host_constructor_replay.load(std::memory_order_acquire) &&
active_host_profile != nullptr &&
!(use_native_mimalloc
? active_host_profile->AllowsNativeMimallocConstructor(idx)
: active_host_profile->AllowsConstructor(idx))) {
++g_libroblox_ctor_skipped_range_count;
if (trace_constructor) {
std::cout << " [ctor] [" << idx << "] Skipped by typed "
<< (use_native_mimalloc ? "native Mimalloc" : "host bridge")
<< " run ranges.\n"
<< std::flush;
}
MaybePrintLibRobloxConstructorSummary(idx);
return;
}
if (LibRobloxConstructorDefaultQuarantineSkipped(idx)) {
++g_libroblox_ctor_skipped_default_count;
if (trace_constructor) {
std::cout << " [ctor] [" << idx
<< "] Skipped by default unsafe ctor quarantine.\n"
<< std::flush;
}
MaybePrintLibRobloxConstructorSummary(idx);
return;
}
if (LibRobloxConstructorIndexSkipped(idx)) {
++g_libroblox_ctor_skipped_index_count;
if (trace_constructor) {
std::cout << " [ctor] [" << idx << "] Skipped by ctor index env.\n"
<< std::flush;
}
MaybePrintLibRobloxConstructorSummary(idx);
return;
}
if (LibRobloxConstructorOffsetSkipped(offset)) {
++g_libroblox_ctor_skipped_env_count;
if (trace_constructor) {
std::cout << " [ctor] [" << idx << "] Skipped by env.\n"
<< std::flush;
}
MaybePrintLibRobloxConstructorSummary(idx);
return;
}
if (trace_constructor) {
std::cout << " [ctor] [" << idx << "] Executing.\n" << std::flush;
}
void (*fn)() = reinterpret_cast<void(*)()>(orig);
fn();
if (use_native_mimalloc && active_host_profile != nullptr) {
const mocktail::compat::NativeMimallocBootstrapStatus bootstrap_status =
mocktail::compat::CompleteNativeMimallocConstructor(
g_libroblox_base_static, *active_host_profile, idx);
if (bootstrap_status ==
mocktail::compat::NativeMimallocBootstrapStatus::kInitialized) {
std::cout << " [ctor] Native mimalloc thread state initialized after ["
<< idx << "]\n"
<< std::flush;
} else if (bootstrap_status ==
mocktail::compat::NativeMimallocBootstrapStatus::kFailed) {
std::cerr << " [ctor] Native mimalloc thread state initialization "
"failed after ["
<< idx << "]\n"
<< std::flush;
}
}
++g_libroblox_ctor_executed_count;
if (trace_constructor) {
std::cout << " [ctor] [" << idx << "] Done.\n" << std::flush;
}
MaybePrintLibRobloxConstructorSummary(idx);
}
// Unmaps the rbxasset:// names that have a file in the asset overlay folder, so
// Roblox opens those files instead of downloading the stock asset. Must run
// before any libroblox code does: the segment loses PROT_EXEC while it is
// written.
void DisableOverlaidAssetMappings(uintptr_t base) {
const std::vector<std::string> names = mocktail::runtime::OverlayRbxAssetNames(
CurrentRuntimePaths().asset_overlay_dir());
const long page_size = sysconf(_SC_PAGESIZE);
if (names.empty() || page_size <= 0) {
return;
}
const auto* header = reinterpret_cast<const Elf64_Ehdr*>(base);
const auto* segments =
reinterpret_cast<const Elf64_Phdr*>(base + header->e_phoff);
std::size_t changed = 0;
for (std::size_t index = 0; index < header->e_phnum; ++index) {
const Elf64_Phdr& segment = segments[index];
if (segment.p_type != PT_LOAD || (segment.p_flags & PF_W) != 0 ||
segment.p_filesz == 0) {
continue;
}
const uintptr_t begin = base + segment.p_vaddr;
const uintptr_t page_mask = static_cast<uintptr_t>(page_size) - 1;
const uintptr_t page_begin = begin & ~page_mask;
const std::size_t span =
((begin + segment.p_filesz + page_mask) & ~page_mask) - page_begin;
const int original =
PROT_READ | ((segment.p_flags & PF_X) != 0 ? PROT_EXEC : 0);
if (mprotect(reinterpret_cast<void*>(page_begin), span,
PROT_READ | PROT_WRITE) != 0) {
continue;
}
changed += mocktail::runtime::DisableAssetNameMappings(
reinterpret_cast<std::uint8_t*>(begin), segment.p_filesz, names);
mprotect(reinterpret_cast<void*>(page_begin), span, original);
}
std::cout << " [asset] overlay holds " << names.size()
<< " rbxasset file(s); unmapped " << changed
<< " download name(s)\n"
<< std::flush;
}
extern "C" void mocktail_before_soinfo_constructors(const char* realpath,
uintptr_t base) {
if (realpath == nullptr || std::strstr(realpath, "libroblox.so") == nullptr ||
base == 0) {
return;
}
g_libroblox_base = base;
DisableOverlaidAssetMappings(base);
g_mocktail_abort_libroblox_base = base;
g_libroblox_base_static = base;
const mocktail::compat::HostAbiProfile* host_abi =
g_active_host_abi_profile.load(std::memory_order_acquire);
// Host alloc bridges must be in place before any constructor executes.
if (HostAbiExperimentRequested() && !InstallActiveHostAbiExperiment(base)) {
std::cerr << " [compat] host ABI install failed before constructors\n"
<< std::flush;
return;
}
const bool force_run_ctors =
(std::getenv("MOCKTAIL_SKIP_LIBROBLOX_CTORS") != nullptr &&
std::strcmp(std::getenv("MOCKTAIL_SKIP_LIBROBLOX_CTORS"), "0") == 0) ||
IsEnabled("MOCKTAIL_RUN_LIBROBLOX_CTORS");
const bool want_light_wrap =
IsEnabled("MOCKTAIL_WRAP_LIBROBLOX_CTORS") || force_run_ctors;
const bool use_native_mimalloc =
g_host_abi_install_attempted &&
g_host_abi_install_result.uses_native_mimalloc;
mocktail::compat::NativeThreadInitializer thread_initializer = nullptr;
if (use_native_mimalloc && host_abi != nullptr &&
host_abi->data_seeds.allocator_thread_initializer != 0) {
thread_initializer =
reinterpret_cast<mocktail::compat::NativeThreadInitializer>(
base + host_abi->data_seeds.allocator_thread_initializer);
}
mocktail::compat::ConfigureBionicPthreadThreadInitializer(
thread_initializer);
libc_shim::GuestAllocator guest_allocator = nullptr;
if (use_native_mimalloc && host_abi != nullptr &&
host_abi->native_allocator.IsValid()) {
guest_allocator = reinterpret_cast<libc_shim::GuestAllocator>(
base + host_abi->native_allocator.allocate);
}
libc_shim::ConfigureGuestAllocator(guest_allocator);
const bool has_selected_constructor_ranges =
host_abi != nullptr &&
(use_native_mimalloc
? host_abi->HasValidNativeMimallocConstructorRanges()
: host_abi->HasValidConstructorRanges());
const bool have_init_array =
g_allow_host_constructor_replay.load(std::memory_order_acquire) &&
HostAbiExperimentRequested() &&
host_abi != nullptr && host_abi->init_array_offset != 0 &&
has_selected_constructor_ranges;
if (!(want_light_wrap && have_init_array)) {
std::cout << " [compat] leaving libroblox constructors untouched for "
"this Build-ID profile\n"
<< std::flush;
return;
}
g_current_ctor_index = 0;
g_libroblox_ctor_executed_count = 0;
g_libroblox_ctor_skipped_range_count = 0;
g_libroblox_ctor_skipped_default_count = 0;
g_libroblox_ctor_skipped_index_count = 0;
g_libroblox_ctor_skipped_env_count = 0;
size_t kCtorCount = 0;
uintptr_t init_array_offset = 0;
if (have_init_array) {
init_array_offset = host_abi->init_array_offset;
kCtorCount = host_abi->init_array_count;
}
const size_t profile_ctor_start =
have_init_array
? (use_native_mimalloc
? host_abi->NativeMimallocConstructorRangeBegin()
: host_abi->ConstructorRangeBegin())
: 0;
const size_t profile_ctor_end =
have_init_array
? (use_native_mimalloc
? host_abi->NativeMimallocConstructorRangeEndExclusive()
: host_abi->ConstructorRangeEndExclusive())
: kCtorCount;
g_libroblox_ctor_start_index = std::max(
profile_ctor_start,
GetCtorIndexEnv("MOCKTAIL_LIBROBLOX_CTOR_START_INDEX",
profile_ctor_start, kCtorCount));
g_libroblox_ctor_end_index = std::min(
profile_ctor_end,
GetCtorIndexEnv("MOCKTAIL_LIBROBLOX_CTOR_END_INDEX", profile_ctor_end,
kCtorCount));
int max_ctors = GetEnvInt("MOCKTAIL_MAX_LIBROBLOX_CTORS", -1);
if (max_ctors >= 0) {
g_libroblox_ctor_end_index =
std::min(g_libroblox_ctor_end_index,
g_libroblox_ctor_start_index +
static_cast<size_t>(max_ctors));
}
if (g_libroblox_ctor_end_index < g_libroblox_ctor_start_index) {
g_libroblox_ctor_end_index = g_libroblox_ctor_start_index;
}
uintptr_t* init_array =
reinterpret_cast<uintptr_t*>(base + init_array_offset);
long ctor_page_size = sysconf(_SC_PAGESIZE);
if (ctor_page_size > 0) {
uintptr_t addr = reinterpret_cast<uintptr_t>(init_array);
uintptr_t page = addr & ~(static_cast<uintptr_t>(ctor_page_size) - 1);
const size_t span =
(kCtorCount * sizeof(uintptr_t)) + static_cast<size_t>(ctor_page_size);
mprotect(reinterpret_cast<void*>(page), span, PROT_READ | PROT_WRITE);
}
g_original_ctors.clear();
g_original_ctors.reserve(kCtorCount);
for (size_t i = 0; i < kCtorCount; ++i) {
g_original_ctors.push_back(init_array[i]);
init_array[i] = reinterpret_cast<uintptr_t>(MocktailConstructorWrapper);
}
std::cout << " [compat] wrapping .init_array @+0x" << std::hex
<< init_array_offset << std::dec << " count=" << kCtorCount
<< '\n'
<< std::flush;
std::cout << " [compat] Wrapped " << kCtorCount
<< " constructors for typed replay"
<< (LibRobloxConstructorTraceEnabled() ? " with trace" : "")
<< '\n'
<< std::flush;
std::cout << " [ctor] Active index range ["
<< g_libroblox_ctor_start_index << ", "
<< g_libroblox_ctor_end_index << ")\n"
<< std::flush;
if (use_native_mimalloc) {
std::cout << " [ctor] Native mimalloc integration probe selected; "
"host allocator bridges are disabled\n"
<< std::flush;
}
if (LibRobloxConstructorDefaultQuarantineEnabled()) {
std::cout
<< " [ctor] Default unsafe ctor quarantine enabled: only index 0 "
"runs unless MOCKTAIL_ALLOW_LIBROBLOX_CTOR_INDEX_RANGES allows more\n"
<< std::flush;
} else {
std::cout << " [ctor] Default unsafe ctor quarantine disabled\n"
<< std::flush;
}
}
extern "C" bool mocktail_should_skip_soinfo_constructors(const char* realpath) {
if (realpath == nullptr ||
std::strstr(realpath, "libroblox.so") == nullptr) {
return false;
}
const bool typed_replay_allowed =
g_allow_host_constructor_replay.load(std::memory_order_acquire) &&
HostAbiExperimentRequested();
if (!typed_replay_allowed) {
std::cout << " [compat] skipping libroblox constructors: no exact "
"Build-ID replay policy\n"
<< std::flush;
return true;
}
// Explicit host ABI policy, independent of fixed-offset binary patches.
// Native libroblox .init_array currently aborts in emutls growth on Linux
// hosts (caller off≈0x28bae15 on 2.725.1142). Skip by default for
// non-patched profiles so name-based JNI startup remains reachable.
//
// Controls:
// MOCKTAIL_SKIP_LIBROBLOX_CTORS=1 force skip
// MOCKTAIL_SKIP_LIBROBLOX_CTORS=0 force run
// MOCKTAIL_RUN_LIBROBLOX_CTORS=1 force run (legacy name)
const char* skip_constructors = std::getenv("MOCKTAIL_SKIP_LIBROBLOX_CTORS");
if (skip_constructors != nullptr) {
if (std::strcmp(skip_constructors, "0") == 0) {
std::cout << " [compat] running libroblox static constructors "
"(MOCKTAIL_SKIP_LIBROBLOX_CTORS=0)\n"
<< std::flush;
return false;
}
std::cout << " [compat] skipping libroblox static constructors "
"(MOCKTAIL_SKIP_LIBROBLOX_CTORS)\n"
<< std::flush;
return true;
}
const char* run_constructors = std::getenv("MOCKTAIL_RUN_LIBROBLOX_CTORS");
if (run_constructors != nullptr && std::strcmp(run_constructors, "0") != 0) {
std::cout << " [compat] running libroblox static constructors "
"(MOCKTAIL_RUN_LIBROBLOX_CTORS)\n"
<< std::flush;
return false;
}
if (typed_replay_allowed) {
std::cout
<< " [compat] skipping libroblox static constructors for host load "
"(set MOCKTAIL_SKIP_LIBROBLOX_CTORS=0 to force native .init_array)\n"
<< std::flush;
return true;
}
std::cout << " [patch] skipping libroblox static constructors"
<< " (set MOCKTAIL_RUN_LIBROBLOX_CTORS=1 to enable)\n"
<< std::flush;
return true;
}
std::string JStringToString(JNIEnv* env, jstring value) {
if (!env || !value) {
return {};
}
const char* chars = env->GetStringUTFChars(value, nullptr);
if (!chars) {
return {};
}
std::string result(chars);
env->ReleaseStringUTFChars(value, chars);
return result;
}
void MocktailSetAssetPath(JNIEnv* env, jstring asset_path) {
std::string path = JStringToString(env, asset_path);
if (path.empty()) {
path = DefaultAssetPath();
}
std::filesystem::path root_path(path);
if (root_path.filename() == "content") {
root_path = root_path.parent_path();
}
setenv("MOCKTAIL_ASSET_ROOT", root_path.c_str(), 1);
std::cout << " [engine] asset root set to " << root_path.string() << '\n' << std::flush;
}
void MocktailAppBridgeInit(JNIEnv* env, jstring app_params) {
std::string params = JStringToString(env, app_params);
if (params.empty()) {
params = "{}";
}
setenv("MOCKTAIL_APP_BRIDGE_PARAMS", params.c_str(), 1);
setenv("MOCKTAIL_APP_BRIDGE_INIT", "1", 1);
std::cout << " [engine] app bridge params staged\n" << std::flush;
}
void MocktailAppBridgeStart(JNIEnv* env, jstring app_params) {
std::string params = JStringToString(env, app_params);
if (params.empty()) {
params = "{}";
}
setenv("MOCKTAIL_APP_BRIDGE_START_PARAMS", params.c_str(), 1);
setenv("MOCKTAIL_APP_STARTED", "1", 1);
std::cout << " [engine] app bridge marked started\n" << std::flush;
}
jstring NewStringFromEnvDefault(JNIEnv* env, const char* env_name,
const char* default_value) {
const char* value = std::getenv(env_name);
if (!value || value[0] == '\0') {
value = default_value;
}
return env->NewStringUTF(value);
}
std::string MocktailCookiePath() {
return MocktailConfigRoot() + "/cookie";
}
bool CookieHasAttribute(const std::string& cookie, const char* attribute) {
std::string lower_cookie = cookie;
std::string lower_attribute = attribute ? attribute : "";
std::transform(lower_cookie.begin(), lower_cookie.end(),
lower_cookie.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
std::transform(lower_attribute.begin(), lower_attribute.end(),
lower_attribute.begin(), [](unsigned char ch) {
return static_cast<char>(std::tolower(ch));
});
return !lower_attribute.empty() &&
lower_cookie.find(lower_attribute) != std::string::npos;
}
std::string CookieForJNICookieManager(std::string_view cookie_header,
const std::string& domain) {
if (cookie_header.empty()) {
return {};
}
std::string cookie(cookie_header);
if (!CookieHasAttribute(cookie, "domain=")) {
if (!cookie.empty() && cookie.back() != ';') {
cookie += "; ";
} else if (!cookie.empty()) {
cookie += ' ';
}
cookie += "Domain=";
cookie += domain.empty() ? "roblox.com" : domain;
}
return cookie;
}
void ApplyAuthStartupDefaults(bool credential_available,
bool user_overrode_start_lua_app_dm,
bool user_overrode_start_lua_step,
bool user_overrode_start_app_step,
bool user_overrode_call_start_app) {
if (credential_available) {
std::cout << " [auth] typed Roblox credential ready for native startup\n";
return;
}
if (IsEnabled("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP")) {
std::cout << " [auth] no Roblox cookie found; continuing in no-cookie mode\n"
<< std::flush;
return;
}
std::cerr << " [auth] no Roblox cookie found; continuing in strict mode\n"
<< std::flush;
if (!user_overrode_start_lua_app_dm) {
setenv("MOCKTAIL_START_LUA_APP_DM", "0", 1);
}
if (!user_overrode_start_lua_step) {
setenv("MOCKTAIL_STEP_START_LUA_APP_DM", "0", 1);
}
if (!user_overrode_start_app_step) {
setenv("MOCKTAIL_STEP_START_APP_WITH_PARAMS", "0", 1);
}
if (!user_overrode_call_start_app) {
setenv("MOCKTAIL_CALL_REAL_APP_BRIDGE_START", "0", 1);
}
}
std::string ResolveClientSettingsJson() {
mocktail::services::ClientSettingsOptions options;
options.explicit_json =
GetEnvString("MOCKTAIL_CLIENT_SETTINGS_JSON", "");
options.explicit_file =
GetEnvString("MOCKTAIL_CLIENT_SETTINGS_JSON_FILE", "");
options.use_bundled = IsEnabled("MOCKTAIL_USE_BUNDLED_CLIENT_SETTINGS");
options.sober_mode = IsEnabled("MOCKTAIL_SOBER_MODE");
options.fetch = IsEnabled("MOCKTAIL_FETCH_CLIENT_SETTINGS");
options.auto_update =
!IsDisabled("MOCKTAIL_CLIENT_SETTINGS_AUTO_UPDATE");
options.application =
GetEnvString("MOCKTAIL_CLIENT_SETTINGS_APP", "GoogleAndroidApp");
options.url = GetEnvString("MOCKTAIL_CLIENT_SETTINGS_URL", "");
options.cache_file = GetEnvString(
"MOCKTAIL_CLIENT_SETTINGS_CACHE_FILE",
(MocktailCacheRoot() + "/clientsettings/" + options.application +
".json")
.c_str());
static mocktail::services::CurlHttpClient http_client;
static mocktail::services::ClientSettingsService settings_service(
http_client);
const mocktail::services::ClientSettingsResult result =
settings_service.Resolve(options);
using mocktail::services::ClientSettingsSource;
switch (result.source) {
case ClientSettingsSource::kExplicitJson:
break;
case ClientSettingsSource::kExplicitFile:
std::cout << " [settings] using explicit client settings file "
<< options.explicit_file << " bytes=" << result.json.size()
<< '\n';
break;
case ClientSettingsSource::kBundledFile:
std::cout << " [settings] using bundled client settings "
<< options.bundled_file << " bytes=" << result.json.size()
<< '\n';
break;
case ClientSettingsSource::kSafeDefaults:
std::cout << " [settings] using safe inline client settings\n";
break;
case ClientSettingsSource::kDownloaded:
std::cout << " [settings] flags "
<< (result.cache_updated ? "updated" : "unchanged") << '\n';
break;
case ClientSettingsSource::kCache:
if (!result.error.empty()) {
std::cerr
<< " [settings] CDN fetch failed; using cached flags if present: "
<< result.error << '\n';
}
std::cout << " [settings] using cached flags\n";
break;
case ClientSettingsSource::kEmptyDefaults:
if (!result.error.empty()) {
std::cerr << " [settings] CDN fetch failed: " << result.error << '\n';
}
std::cerr
<< " [settings] no client settings available; using empty defaults\n";
break;
}
std::cout << std::flush;
std::cerr << std::flush;
return result.json;
}
jstring NewClientSettingsString(JNIEnv* env) {
std::string content = ResolveClientSettingsJson();
return env->NewStringUTF(content.c_str());
}
void SetObjectField(JNIEnv* env, jobject object, const char* name,
const char* signature, jobject value) {
if (!env || !object || !name || !signature) {
return;
}
jclass object_class = env->GetObjectClass(object);
jfieldID field_id = env->GetFieldID(object_class, name, signature);
env->SetObjectField(object, field_id, value);
}
void SetStringField(JNIEnv* env, jobject object, const char* name,
const char* value) {
SetObjectField(env, object, name, "Ljava/lang/String;",
env->NewStringUTF(value ? value : ""));
}
void SetJStringField(JNIEnv* env, jobject object, const char* name,
jstring value) {
if (!env || !object || !name) {
return;
}
jclass object_class = env->GetObjectClass(object);
jfieldID field_id = env->GetFieldID(object_class, name, "Ljava/lang/String;");
env->SetObjectField(object, field_id, value);
}
void SetIntField(JNIEnv* env, jobject object, const char* name, jint value) {
if (!env || !object || !name) {
return;
}
jclass object_class = env->GetObjectClass(object);
jfieldID field_id = env->GetFieldID(object_class, name, "I");
env->SetIntField(object, field_id, value);
}
void SetLongField(JNIEnv* env, jobject object, const char* name, jlong value) {
if (!env || !object || !name) {
return;
}
jclass object_class = env->GetObjectClass(object);
jfieldID field_id = env->GetFieldID(object_class, name, "J");
env->SetLongField(object, field_id, value);
}
void SetFloatField(JNIEnv* env, jobject object, const char* name,
jfloat value) {
if (!env || !object || !name) {
return;
}
jclass object_class = env->GetObjectClass(object);
jfieldID field_id = env->GetFieldID(object_class, name, "F");
env->SetFloatField(object, field_id, value);
}
void SetBooleanField(JNIEnv* env, jobject object, const char* name,
jboolean value) {
if (!env || !object || !name) {
return;
}
jclass object_class = env->GetObjectClass(object);
jfieldID field_id = env->GetFieldID(object_class, name, "Z");
env->SetBooleanField(object, field_id, value);
}
void SetRobloxServiceUrlFields(JNIEnv* env, jobject object) {
if (!env || !object) {
return;
}
constexpr const char* kBaseUrl = "https://www.roblox.com";
constexpr const char* kApiUrl = "https://apis.roblox.com";
constexpr const char* kClientSettingsUrl =
"https://clientsettingscdn.roblox.com/v2/settings-compressed/"
"application/AndroidApp.zst";
constexpr const char* kClientSettingsBaseUrl =
"https://clientsettingscdn.roblox.com";
constexpr const char* kEcsv2ClientUrl =
"https://ecsv2.roblox.com/client/pbe";
constexpr const char* kEcsv2TimespentUrl =
"https://ecsv2.roblox.com/timespent/pbe";
constexpr const char* kTelemetryUrl =
"https://apis.roblox.com/experience-signals-ingest/public/v1/"
"events/single";
SetStringField(env, object, "baseUrl", kBaseUrl);
SetStringField(env, object, "baseURL", kBaseUrl);
SetStringField(env, object, "wwwBaseUrl", kBaseUrl);
SetStringField(env, object, "apiBaseUrl", kApiUrl);
SetStringField(env, object, "apiGatewayUrl", kApiUrl);
SetStringField(env, object, "clientSettingsUrl", kClientSettingsUrl);
SetStringField(env, object, "settingsUrl", kClientSettingsUrl);
SetStringField(env, object, "clientSettingsBaseUrl", kClientSettingsBaseUrl);
SetStringField(env, object, "clientSettingsHost", kClientSettingsBaseUrl);
SetStringField(env, object, "ecsv2Url", kEcsv2ClientUrl);
SetStringField(env, object, "ecsUrl", kEcsv2ClientUrl);
SetStringField(env, object, "eventStreamUrl", kEcsv2ClientUrl);
SetStringField(env, object, "telemetryUrl", kTelemetryUrl);
SetStringField(env, object, "telemetryEndpoint", kTelemetryUrl);
SetStringField(env, object, "timeSpentUrl", kEcsv2TimespentUrl);
}
jobject NewObject(JNIEnv* env, const char* class_name) {
if (!env || !class_name) {
return nullptr;
}
jclass clazz = env->FindClass(class_name);
return env->AllocObject(clazz);
}
void InstallNativeGlJavaImplementation(JNIEnv* env, jclass native_gl_java_class) {
if (IsDisabled("MOCKTAIL_SET_NATIVE_GL_JAVA_IMPLEMENTATION")) {
return;
}
if (env == nullptr || native_gl_java_class == nullptr) {
std::cerr << " [engine] NativeGLJavaInterface.setImplementation skipped: "
<< "missing JNI state\n"
<< std::flush;
return;
}
jobject engine_java_callback =
NewObject(env, "com/roblox/engine/jni/EngineJavaCallback2");
if (engine_java_callback == nullptr) {
std::cerr << " [engine] NativeGLJavaInterface.setImplementation skipped: "
<< "EngineJavaCallback2 allocation failed\n"
<< std::flush;
return;
}
jmethodID set_implementation = env->GetStaticMethodID(
native_gl_java_class, "setImplementation",
"(Lcom/roblox/engine/jni/EngineJavaCallback2;)V");
if (set_implementation == nullptr) {
std::cerr << " [engine] NativeGLJavaInterface.setImplementation skipped: "
<< "method not found\n"
<< std::flush;
return;
}
std::cout << " [engine] NativeGLJavaInterface.setImplementation\n"
<< std::flush;
env->CallStaticVoidMethod(native_gl_java_class, set_implementation,
engine_java_callback);
std::cout << " [engine] NativeGLJavaInterface.setImplementation returned\n"
<< std::flush;
}
jobject BuildPlatformParams(JNIEnv* env, jobject surface, bool is_headless);
mocktail::runtime::DisplaySize HostDisplaySize() {
return mocktail::runtime::ParseDisplaySize(
std::getenv(mocktail::runtime::kDisplaySizeEnvironment));
}
// Live window pixels once the host window exists; the initial window size
// handoff otherwise.
mocktail::runtime::DisplaySize HostWindowPixelSize() {
const mocktail::window::WindowViewportSnapshot viewport =
mocktail::window::GetWindowViewportSnapshot();
if (viewport.valid()) {
return {viewport.pixel_width, viewport.pixel_height};
}
return mocktail::runtime::ParseDisplaySize(
std::getenv(mocktail::runtime::kWindowSizeEnvironment));
}
jobject BuildDeviceParams(JNIEnv* env) {
jobject params = NewObject(env, "com/roblox/engine/jni/model/DeviceParams");
if (!params) {
return nullptr;
}
const std::string device_name =
GetEnvString("MOCKTAIL_DEVICE_NAME", "Mocktail Linux");
const std::string manufacturer =
GetEnvString("MOCKTAIL_DEVICE_MANUFACTURER", "Mocktail");
const std::string device_sku = GetEnvString(
"MOCKTAIL_DEVICE_SKU",
("mocktail-" + std::string(mocktail::compat::kGuestCpuName)).c_str());
const std::string soc_model =
GetEnvString("MOCKTAIL_DEVICE_SOC_MODEL",
std::string(mocktail::compat::kGuestCpuName).c_str());
SetStringField(env, params, "osVersion", "33");
SetStringField(env, params, "deviceName", device_name.c_str());
const std::string app_version =
GetEnvString("MOCKTAIL_ROBLOX_VERSION", "unknown");
SetStringField(env, params, "appVersion", app_version.c_str());
SetStringField(env, params, "country", "US");
SetStringField(env, params, "manufacturer", manufacturer.c_str());
const mocktail::runtime::DisplaySize display_size =
mocktail::runtime::ParseDisplaySize(
std::getenv(mocktail::runtime::kDisplaySizeEnvironment));
const std::string display_resolution = std::to_string(display_size.width) +
"x" +
std::to_string(display_size.height);
SetStringField(env, params, "displayResolution", display_resolution.c_str());
SetStringField(env, params, "networkType", "wifi");
SetStringField(env, params, "deviceSku", device_sku.c_str());
SetStringField(env, params, "socModel", soc_model.c_str());
SetStringField(env, params, "appBuildVariant", "googleProdRelease");
SetStringField(env, params, "testDeviceName", device_name.c_str());
// MOCKTAIL_DISABLE_LOW_RAM_DEVICE forces the normal-RAM profile even when
// the host total cannot be read or is below 4 GiB.
std::uint64_t host_memory_bytes = mocktail::runtime::DetectHostMemoryBytes();
if (IsEnabled("MOCKTAIL_DISABLE_LOW_RAM_DEVICE")) {
host_memory_bytes =
std::max<std::uint64_t>(host_memory_bytes, 4096ULL * 1024ULL * 1024ULL);
}
const mocktail::runtime::DeviceMemoryProfile memory =
mocktail::runtime::BuildDeviceMemoryProfile(host_memory_bytes);
SetIntField(env, params, "deviceTotalMemoryMB", memory.total_memory_mb);
SetIntField(env, params, "displayPhysicalWidthPixels", display_size.width);
SetIntField(env, params, "displayPhysicalHeightPixels",
display_size.height);
SetIntField(env, params, "memoryClass", memory.memory_class_mb);
SetIntField(env, params, "largeMemoryClass", memory.large_memory_class_mb);
SetLongField(env, params, "lowMemoryKillerBackgroundAppThreshold",
memory.low_memory_killer_background_threshold);
SetLongField(env, params, "lowMemoryKillerForegroundAppThreshold",
memory.low_memory_killer_foreground_threshold);
SetBooleanField(env, params, "cpu64Bit", JNI_TRUE);
SetBooleanField(env, params, "isChrome", JNI_FALSE);
SetBooleanField(env, params, "isLowRamDevice",
memory.low_ram_device ? JNI_TRUE : JNI_FALSE);
return params;
}
jobject BuildAppBridgeInitParams(JNIEnv* env, jstring client_settings,
jstring fast_flags, jstring app_params,
jstring asset_path, bool is_headless) {
jobject params =
NewObject(env, "com/roblox/engine/jni/autovalue/InitParams");
if (!params) {
return nullptr;
}
jobject activity = NewObject(env, "com/roblox/client/RobloxActivity");
jobject context = NewObject(env, "android/content/Context");
jobject asset_manager = NewObject(env, "android/content/res/AssetManager");
jobject resources = NewObject(env, "android/content/res/Resources");
jobject class_loader = NewObject(env, "java/lang/ClassLoader");
jobject package_manager = NewObject(env, "android/content/pm/PackageManager");
jobject window = NewObject(env, "android/view/Window");
jobject display = NewObject(env, "android/view/Display");
jobject surface = NewObject(env, "android/view/Surface");
jobject platform_params = BuildPlatformParams(env, surface, is_headless);
jobject device_params = BuildDeviceParams(env);
jobject surface_holder = NewObject(env, "android/view/SurfaceHolder");
jobject view = NewObject(env, "android/view/View");
SetObjectField(env, params, "platformParams",
"Lcom/roblox/engine/jni/model/PlatformParams;",
platform_params);
jobject start_game_device_params =
IsEnabled("MOCKTAIL_START_GAME_DEVICE_PARAMS_NON_NULL") ? device_params
: nullptr;
SetObjectField(env, params, "deviceParams",
"Lcom/roblox/engine/jni/model/DeviceParams;",
start_game_device_params);
SetStringField(env, params, "baseURL", "https://www.roblox.com");
const std::string user_agent = GetEnvString(
"MOCKTAIL_USER_AGENT", "Roblox/unknown (Linux; Android 33; Mocktail)");
SetStringField(env, params, "userAgent", user_agent.c_str());
SetBooleanField(env, params, "isTablet", JNI_FALSE);
SetBooleanField(env, params, "isPotato", JNI_FALSE);
SetBooleanField(env, params, "isVrDevice",
IsEnabled("MOCKTAIL_VR_ENABLED") ? JNI_TRUE : JNI_FALSE);
SetStringField(env, params, "buildVariant", "googleProdRelease");
SetObjectField(env, params, "vrContext", "Landroid/app/Activity;",
activity);
SetObjectField(env, params, "activity", "Lcom/roblox/client/RobloxActivity;",
activity);
SetObjectField(env, params, "robloxActivity",
"Lcom/roblox/client/RobloxActivity;", activity);
SetObjectField(env, params, "mainActivity",
"Lcom/roblox/client/RobloxActivity;", activity);
SetObjectField(env, params, "context", "Landroid/content/Context;", context);
SetObjectField(env, params, "applicationContext",
"Landroid/content/Context;", context);
SetObjectField(env, params, "assetManager",
"Landroid/content/res/AssetManager;", asset_manager);
SetObjectField(env, params, "assets",
"Landroid/content/res/AssetManager;", asset_manager);
SetObjectField(env, params, "resources",
"Landroid/content/res/Resources;", resources);
SetObjectField(env, params, "classLoader", "Ljava/lang/ClassLoader;",
class_loader);
SetObjectField(env, params, "packageManager",
"Landroid/content/pm/PackageManager;", package_manager);
SetObjectField(env, params, "window", "Landroid/view/Window;", window);
SetObjectField(env, params, "display", "Landroid/view/Display;", display);
SetObjectField(env, params, "surface", "Landroid/view/Surface;", surface);
SetObjectField(env, params, "surfaceHolder", "Landroid/view/SurfaceHolder;",
surface_holder);
SetObjectField(env, params, "view", "Landroid/view/View;", view);
SetObjectField(env, params, "decorView", "Landroid/view/View;", view);
SetObjectField(env, params, "rootView", "Landroid/view/View;", view);
SetObjectField(env, params, "clientSettingsJson", "Ljava/lang/String;",
client_settings);
SetObjectField(env, params, "clientSettings", "Ljava/lang/String;",
client_settings);
SetObjectField(env, params, "fastFlagsJson", "Ljava/lang/String;",
fast_flags);
SetObjectField(env, params, "fastFlags", "Ljava/lang/String;", fast_flags);
SetObjectField(env, params, "appParamsJson", "Ljava/lang/String;",
app_params);
SetObjectField(env, params, "appParams", "Ljava/lang/String;", app_params);
SetObjectField(env, params, "launchParams", "Ljava/lang/String;",
app_params);
SetObjectField(env, params, "assetPath", "Ljava/lang/String;", asset_path);
SetStringField(env, params, "packageName", "com.roblox.client");
SetStringField(env, params, "platformName", "Android");
const std::string init_app_version =
GetEnvString("MOCKTAIL_ROBLOX_VERSION", "unknown");
SetStringField(env, params, "appVersion", init_app_version.c_str());
const std::string init_device_name =
GetEnvString("MOCKTAIL_DEVICE_NAME", "Mocktail Headless");
SetStringField(env, params, "deviceName", init_device_name.c_str());
SetStringField(env, params, "locale", "en_us");
SetRobloxServiceUrlFields(env, params);
const mocktail::runtime::DisplaySize init_display = HostDisplaySize();
SetIntField(env, params, "screenWidth", init_display.width);
SetIntField(env, params, "screenHeight", init_display.height);
SetIntField(env, params, "densityDpi", 160);
SetIntField(env, params, "sdkVersion", 33);
jboolean headless = is_headless ? JNI_TRUE : JNI_FALSE;
SetBooleanField(env, params, "headless", headless);
SetBooleanField(env, params, "isHeadless", headless);
SetBooleanField(env, params, "isFirstInstall", JNI_FALSE);
SetBooleanField(env, params, "isLowMemoryDevice", JNI_FALSE);
return params;
}
jobject BuildApplicationExitInfoList(JNIEnv* env) {
jobject list = NewObject(env, "java/util/ArrayList");
if (!list) {
list = NewObject(env, "java/util/List");
}
return list;
}
jobject BuildPlatformParams(JNIEnv* env, jobject surface, bool is_headless) {
jobject params =
NewObject(env, "com/roblox/engine/jni/model/PlatformParams");
if (!params) {
return nullptr;
}
SetObjectField(env, params, "surface", "Landroid/view/Surface;", surface);
SetStringField(env, params, "platform", "Android");
const std::string platform_device_name =
GetEnvString("MOCKTAIL_DEVICE_NAME", "Mocktail Headless");
SetStringField(env, params, "deviceName", platform_device_name.c_str());
SetStringField(env, params, "locale", "en_us");
SetStringField(env, params, "assetFolderPath", DefaultAssetPath().c_str());
const mocktail::runtime::DisplaySize platform_window = HostWindowPixelSize();
const mocktail::runtime::DisplaySize platform_display = HostDisplaySize();
SetIntField(env, params, "width", platform_window.width);
SetIntField(env, params, "height", platform_window.height);
SetIntField(env, params, "screenWidth", platform_display.width);
SetIntField(env, params, "screenHeight", platform_display.height);
SetIntField(env, params, "densityDpi", 160);
SetIntField(env, params, "viewportWidthMm",
mocktail::runtime::PixelsToMillimetersAt160Dpi(
platform_window.width));
SetIntField(env, params, "viewportHeightMm",
mocktail::runtime::PixelsToMillimetersAt160Dpi(
platform_window.height));
const mocktail::window::WindowViewportSnapshot viewport =
mocktail::window::GetWindowViewportSnapshot();
SetFloatField(env, params, "dpiScale", viewport.dpi_scale);
SetBooleanField(
env, params, "isKeyboardDevice",
IsEnabled("MOCKTAIL_KEYBOARD_ENABLED_INTERNAL") ? JNI_TRUE : JNI_FALSE);
SetBooleanField(
env, params, "isMouseDevice",
IsEnabled("MOCKTAIL_MOUSE_ENABLED_INTERNAL") ? JNI_TRUE : JNI_FALSE);
SetBooleanField(
env, params, "isTouchDevice",
IsEnabled("MOCKTAIL_TOUCH_ENABLED_INTERNAL") ? JNI_TRUE : JNI_FALSE);
SetBooleanField(env, params, "isLuaHomePageEnabled", JNI_TRUE);
SetBooleanField(env, params, "isLuaGamesPageEnabled", JNI_TRUE);
SetBooleanField(env, params, "isLuaChatEnabled", JNI_TRUE);
SetBooleanField(env, params, "isTablet", JNI_FALSE);
jboolean headless = is_headless ? JNI_TRUE : JNI_FALSE;
SetBooleanField(env, params, "headless", headless);
SetBooleanField(env, params, "isHeadless", headless);
return params;
}
jobject BuildStartAppParams(JNIEnv* env, jstring app_params,
jobject platform_params, jobject surface,
bool is_headless, const char* launch_mode,
const jnivm::RobloxAuthIdentity& identity) {
jobject params =
NewObject(env, "com/roblox/engine/jni/autovalue/StartAppParams");
if (!params) {
return nullptr;
}
jobject activity = NewObject(env, "com/roblox/client/RobloxActivity");
jobject context = NewObject(env, "android/content/Context");
jobject asset_manager = NewObject(env, "android/content/res/AssetManager");
jobject resources = NewObject(env, "android/content/res/Resources");
jobject class_loader = NewObject(env, "java/lang/ClassLoader");
jobject package_manager = NewObject(env, "android/content/pm/PackageManager");
jobject window = NewObject(env, "android/view/Window");
jobject display = NewObject(env, "android/view/Display");
jobject surface_holder = NewObject(env, "android/view/SurfaceHolder");
jobject view = NewObject(env, "android/view/View");
SetObjectField(env, params, "surface", "Landroid/view/Surface;", surface);
SetObjectField(env, params, "platformParams",
"Lcom/roblox/engine/jni/model/PlatformParams;",
platform_params);
SetStringField(env, params, "appStarterPlace",
"rbxasset://places/Mobile.rbxl");
SetStringField(env, params, "appStarterScript", "LuaAppStarterScript");
SetLongField(env, params, "appUserId", identity.user_id);
SetBooleanField(env, params, "isUnder13", JNI_FALSE);
SetStringField(env, params, "username", identity.username.c_str());
SetIntField(env, params, "membershipType",
GetEnvInt("MOCKTAIL_ROBLOX_MEMBERSHIP_TYPE", 0));
const char* theme = std::getenv("MOCKTAIL_RESOLVED_THEME_INTERNAL");
SetStringField(env, params, "selectedTheme",
theme != nullptr ? theme : "Dark");
SetObjectField(env, params, "vrContext", "Landroid/app/Activity;",
activity);
SetObjectField(env, params, "appParams", "Ljava/lang/String;", app_params);
SetObjectField(env, params, "appParamsJson", "Ljava/lang/String;",
app_params);
SetObjectField(env, params, "launchParams", "Ljava/lang/String;",
app_params);
SetObjectField(env, params, "activity", "Lcom/roblox/client/RobloxActivity;",
activity);
SetObjectField(env, params, "robloxActivity",
"Lcom/roblox/client/RobloxActivity;", activity);
SetObjectField(env, params, "mainActivity",
"Lcom/roblox/client/RobloxActivity;", activity);
SetObjectField(env, params, "context", "Landroid/content/Context;", context);
SetObjectField(env, params, "applicationContext",
"Landroid/content/Context;", context);
SetObjectField(env, params, "assetManager",
"Landroid/content/res/AssetManager;", asset_manager);
SetObjectField(env, params, "assets",
"Landroid/content/res/AssetManager;", asset_manager);
SetObjectField(env, params, "resources",
"Landroid/content/res/Resources;", resources);
SetObjectField(env, params, "classLoader", "Ljava/lang/ClassLoader;",
class_loader);
SetObjectField(env, params, "packageManager",
"Landroid/content/pm/PackageManager;", package_manager);
SetObjectField(env, params, "window", "Landroid/view/Window;", window);
SetObjectField(env, params, "display", "Landroid/view/Display;", display);
SetObjectField(env, params, "surfaceHolder", "Landroid/view/SurfaceHolder;",
surface_holder);
SetObjectField(env, params, "view", "Landroid/view/View;", view);
SetObjectField(env, params, "decorView", "Landroid/view/View;", view);
SetObjectField(env, params, "rootView", "Landroid/view/View;", view);
const char* mode = launch_mode;
if (mode == nullptr || mode[0] == '\0') {
mode = is_headless ? "headless" : "normal";
}
SetStringField(env, params, "launchMode", mode);
SetStringField(env, params, "joinData", "{}");
SetStringField(env, params, "packageName", "com.roblox.client");
SetStringField(env, params, "platformName", "Android");
const std::string start_app_version =
GetEnvString("MOCKTAIL_ROBLOX_VERSION", "unknown");
SetStringField(env, params, "appVersion", start_app_version.c_str());
const std::string start_device_name =
GetEnvString("MOCKTAIL_DEVICE_NAME", "Mocktail Linux");
SetStringField(env, params, "deviceName", start_device_name.c_str());
SetStringField(env, params, "locale", "en_us");
SetRobloxServiceUrlFields(env, params);
const mocktail::runtime::DisplaySize start_window = HostWindowPixelSize();
const mocktail::runtime::DisplaySize start_display = HostDisplaySize();
SetIntField(env, params, "width", start_window.width);
SetIntField(env, params, "height", start_window.height);
SetIntField(env, params, "screenWidth", start_display.width);
SetIntField(env, params, "screenHeight", start_display.height);
SetIntField(env, params, "densityDpi", 160);
SetIntField(env, params, "sdkVersion", 33);
SetIntField(env, params, "placeId", 0);
jboolean headless = is_headless ? JNI_TRUE : JNI_FALSE;
SetBooleanField(env, params, "headless", headless);
SetBooleanField(env, params, "isHeadless", headless);
SetBooleanField(env, params, "isFirstInstall", JNI_FALSE);
SetBooleanField(env, params, "isLowMemoryDevice", JNI_FALSE);
return params;
}
jobject BuildStartGameParams(JNIEnv* env, jobject platform_params,
jobject device_params, jobject surface,
jobject vr_context, const char* launch_data_env,
const jnivm::RobloxAuthIdentity& identity) {
std::cout << " [engine] BuildStartGameParams begin\n" << std::flush;
if (!env) {
std::cerr << " [engine] BuildStartGameParams: env is null\n" << std::flush;
return nullptr;
}
jclass start_game_params_class =
env->FindClass("com/roblox/engine/jni/autovalue/StartGameParams");
if (!start_game_params_class) {
std::cerr << " [engine] BuildStartGameParams: StartGameParams class not found\n"
<< std::flush;
return nullptr;
}
const jlong place_id = GetEnvLong("MOCKTAIL_PLACE_ID", 0);
const jlong join_target_user_id =
GetEnvLong("MOCKTAIL_GAME_JOIN_USER_ID", 0);
const jlong conversation_id =
GetEnvLong("MOCKTAIL_GAME_CONVERSATION_ID", 0);
const jlong referred_by_player_id =
GetEnvLong("MOCKTAIL_REFERRED_BY_PLAYER_ID", 0);
const jint join_request_type =
GetEnvInt("MOCKTAIL_GAME_JOIN_REQUEST_TYPE", -1);
const std::string access_code_value =
GetEnvString("MOCKTAIL_GAME_ACCESS_CODE", "");
const std::string link_code_value =
GetEnvString("MOCKTAIL_GAME_LINK_CODE", "");
const std::string reserved_server_access_code_value =
GetEnvString("MOCKTAIL_GAME_RESERVED_SERVER_ACCESS_CODE", "");
const std::string call_id_value = GetEnvString("MOCKTAIL_GAME_CALL_ID", "");
const std::string event_id_value = GetEnvString("MOCKTAIL_GAME_EVENT_ID", "");
const std::string join_attempt_id_value =
GetEnvString("MOCKTAIL_GAME_JOIN_ATTEMPT_ID", "");
const std::string join_attempt_origin_value =
GetEnvString("MOCKTAIL_GAME_JOIN_ATTEMPT_ORIGIN", "");
const std::string iso_context_value =
GetEnvString("MOCKTAIL_GAME_ISO_CONTEXT", "");
const std::string& username_value = identity.username;
const std::string launch_data_value =
GetEnvString(launch_data_env != nullptr ? launch_data_env
: "MOCKTAIL_GAME_PARAMS_JSON",
"");
const std::string game_id_value =
GetEnvString("MOCKTAIL_GAME_ID", "");
const std::string referral_page_value =
GetEnvString("MOCKTAIL_REFERRAL_PAGE", "");
const std::string game_join_context_value =
GetEnvString("MOCKTAIL_GAME_JOIN_CONTEXT", "");
if (EngineTraceEnabled()) {
std::cout << " [engine] StartGame params: place_id=" << place_id
<< " join_target_user_id=" << join_target_user_id
<< " game_id=" << game_id_value
<< " join_request_type=" << join_request_type << '\n'
<< std::flush;
}
const jstring access_code = env->NewStringUTF(access_code_value.c_str());
const jstring link_code = env->NewStringUTF(link_code_value.c_str());
const jstring reserved_server_access_code =
env->NewStringUTF(reserved_server_access_code_value.c_str());
const jstring call_id = env->NewStringUTF(call_id_value.c_str());
const jstring event_id = env->NewStringUTF(event_id_value.c_str());
const jstring join_attempt_id = env->NewStringUTF(join_attempt_id_value.c_str());
const jstring join_attempt_origin =
env->NewStringUTF(join_attempt_origin_value.c_str());
const jstring iso_context = env->NewStringUTF(iso_context_value.c_str());
jstring username = env->NewStringUTF(username_value.c_str());
jstring launch_data = env->NewStringUTF(launch_data_value.c_str());
jstring game_id = env->NewStringUTF(game_id_value.c_str());
jstring referral_page = env->NewStringUTF(referral_page_value.c_str());
jstring game_join_context = env->NewStringUTF(game_join_context_value.c_str());
jobject params = NewObject(env, "com/roblox/engine/jni/autovalue/StartGameParams");
if (!params) {
return nullptr;
}
SetObjectField(env, params, "surface", "Landroid/view/Surface;", surface);
SetObjectField(env, params, "platformParams",
"Lcom/roblox/engine/jni/model/PlatformParams;",
platform_params);
SetObjectField(env, params, "deviceParams",
"Lcom/roblox/engine/jni/model/DeviceParams;",
nullptr);
SetLongField(env, params, "placeId", place_id);
SetLongField(env, params, "userId", join_target_user_id);
SetJStringField(env, params, "accessCode", access_code);
SetJStringField(env, params, "callId", call_id);
SetJStringField(env, params, "linkCode", link_code);
SetJStringField(env, params, "reservedServerAccessCode",
reserved_server_access_code);
SetLongField(env, params, "conversationId", conversation_id);
SetIntField(env, params, "joinRequestType", join_request_type);
SetJStringField(env, params, "gameId", game_id);
SetBooleanField(env, params, "isUnder13", JNI_FALSE);
SetJStringField(env, params, "username", username);
SetJStringField(env, params, "referralPage", referral_page);
SetJStringField(env, params, "launchData", launch_data);
SetJStringField(env, params, "gameJoinContext", game_join_context);
SetJStringField(env, params, "eventId", event_id);
SetJStringField(env, params, "joinAttemptId", join_attempt_id);
SetJStringField(env, params, "joinAttemptOrigin", join_attempt_origin);
SetJStringField(env, params, "isoContext", iso_context);
SetLongField(env, params, "referredByPlayerId", referred_by_player_id);
SetObjectField(env, params, "vrContext", "Landroid/app/Activity;", vr_context);
return params;
}
jobject BuildMockSurface(JNIEnv* env) {
jobject surface = NewObject(env, "android/view/Surface");
if (!surface) {
return nullptr;
}
const mocktail::runtime::DisplaySize surface_size = HostWindowPixelSize();
SetIntField(env, surface, "width", surface_size.width);
SetIntField(env, surface, "height", surface_size.height);
SetBooleanField(env, surface, "valid", JNI_TRUE);
SetBooleanField(env, surface, "isValid", JNI_TRUE);
return surface;
}
jobject BuildConfiguration(JNIEnv* env) {
return jnivm::CreateAndroidConfiguration(env);
}
void ConfigureNativeSettings(JNIEnv* env, jclass settings_class,
const EngineStartupContext* context) {
if (!env || !settings_class || !context) {
return;
}
const std::string sober_data_root = SoberDataRoot();
const std::string sober_cache_root = SoberCacheRoot();
std::string data_dir = GetEnvStringDefaultPath(
"MOCKTAIL_ANDROID_DATA_DIR",
DefaultSoberAwarePath(sober_data_root.c_str(),
"/data/user/0/com.roblox.client"));
std::string files_dir = GetEnvStringDefaultPath(
"MOCKTAIL_ANDROID_FILES_DIR",
DefaultSoberAwarePath((sober_data_root + "/files").c_str(),
"/data/user/0/com.roblox.client/files"));
std::string settings_cache_dir = GetEnvStringDefaultPath(
"MOCKTAIL_ANDROID_SETTINGS_CACHE_DIR",
DefaultSoberAwarePath(sober_cache_root.c_str(),
"/data/user/0/com.roblox.client"));
std::string cache_dir = GetEnvStringDefaultPath(
"MOCKTAIL_ANDROID_CACHE_DIR",
DefaultSoberAwarePath((sober_cache_root + "/cache").c_str(),
"/data/user/0/com.roblox.client/cache"));
std::string external_base = GetEnvStringDefaultPath(
"MOCKTAIL_ANDROID_EXTERNAL_BASE_DIR",
DefaultSoberAwarePath((sober_data_root + "/sdcard/Android/data/"
"com.roblox.client")
.c_str(),
"/sdcard/Android/data/com.roblox.client"));
std::string external_dir = GetEnvStringDefaultPath(
"MOCKTAIL_ANDROID_EXTERNAL_DIR",
DefaultSoberAwarePath((external_base + "/files").c_str(),
"/sdcard/Android/data/com.roblox.client/files"));
std::string preferences_file = GetEnvString("MOCKTAIL_ANDROID_PREFERENCES_FILE",
"prefs");
std::string default_policy_file = GetEnvString(
"MOCKTAIL_DEFAULT_APP_POLICY_FILE",
"content/guac/defaultConfigs/GuacDefaultPolicy-GlobalDist.json");
std::string base_url =
GetEnvString("MOCKTAIL_BASE_URL", "https://www.roblox.com/");
std::string api_url =
GetEnvString("MOCKTAIL_API_URL", "https://api.roblox.com/");
std::string roblox_channel =
GetEnvString("MOCKTAIL_ROBLOX_CHANNEL", "production");
std::string channel_platform_name =
GetEnvString("MOCKTAIL_CHANNEL_PLATFORM_NAME", "GoogleAndroidApp");
std::string roblox_version =
GetEnvString("MOCKTAIL_ROBLOX_VERSION", "unknown");
std::string exception_reason_filename =
GetEnvString("MOCKTAIL_EXCEPTION_REASON_FILENAME",
"exception_reason.txt");
std::string http_proxy_host =
GetEnvString("MOCKTAIL_HTTP_PROXY_HOST", "");
jlong http_proxy_port =
static_cast<jlong>(GetEnvInt("MOCKTAIL_HTTP_PROXY_PORT", 0));
std::string cookie_base_url =
GetEnvString("MOCKTAIL_COOKIE_BASE_URL", "https://www.roblox.com/");
std::string cookie_domain =
GetEnvString("MOCKTAIL_COOKIE_DOMAIN", "roblox.com");
const mocktail::runtime::SecureRobloxCredential* credential =
context->roblox_credential;
const std::string_view roblox_cookies =
credential != nullptr ? credential->view() : std::string_view();
std::string cookie_manager_cookies =
CookieForJNICookieManager(roblox_cookies, cookie_domain);
if (!roblox_cookies.empty()) {
std::cout << " [engine] typed credential prepared bytes="
<< roblox_cookies.size() << '\n'
<< std::flush;
} else if (IsEnabled("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP")) {
std::cout << " [engine] no Roblox cookie found; proceeding without login\n"
<< std::flush;
} else {
std::cout << " [engine] WARNING: no Roblox cookie found at "
<< MocktailCookiePath() << '\n'
<< std::flush;
}
std::string android_id =
GetEnvString("MOCKTAIL_ANDROID_ID", "0000000000000000");
std::string advertising_id =
GetEnvString("MOCKTAIL_ADVERTISING_ID", "");
const jnivm::RobloxAuthIdentity& account_identity =
context->account_identity;
std::string account_user_id = std::to_string(account_identity.user_id);
EnsureAndroidDirectory(data_dir);
EnsureAndroidDirectory(files_dir);
EnsureAndroidDirectory(settings_cache_dir);
EnsureAndroidDirectory(cache_dir);
EnsureAndroidDirectory(external_base);
EnsureAndroidDirectory(external_dir);
EnsureAndroidDirectory(data_dir + "/rbx-storage");
EnsureAndroidDirectory(data_dir + "/appData");
EnsureAndroidDirectory(data_dir + "/appData/LocalStorage");
EnsureAndroidDirectory(data_dir + "/appData/rbx-storage");
EnsureAndroidDirectory(files_dir + "/rbx-storage");
EnsureAndroidDirectory(files_dir + "/appData");
EnsureAndroidDirectory(files_dir + "/appData/LocalStorage");
EnsureAndroidDirectory(files_dir + "/appData/OTAPatchBackups");
EnsureAndroidDirectory(files_dir + "/appData/rbx-storage");
EnsureAndroidDirectory(cache_dir + "/ContentProvider_2");
EnsureAndroidDirectory(cache_dir + "/rbx-storage");
EnsureAndroidDirectory(cache_dir + "/sounds");
jstring data_dir_string = env->NewStringUTF(data_dir.c_str());
jstring files_dir_string = env->NewStringUTF(files_dir.c_str());
jstring settings_cache_dir_string =
env->NewStringUTF(settings_cache_dir.c_str());
jstring external_base_string = env->NewStringUTF(external_base.c_str());
jstring external_dir_string = env->NewStringUTF(external_dir.c_str());
jstring preferences_file_string = env->NewStringUTF(preferences_file.c_str());
jstring default_policy_file_string =
env->NewStringUTF(default_policy_file.c_str());
jstring base_url_string = env->NewStringUTF(base_url.c_str());
jstring api_url_string = env->NewStringUTF(api_url.c_str());
jstring roblox_channel_string = env->NewStringUTF(roblox_channel.c_str());
jstring channel_platform_name_string =
env->NewStringUTF(channel_platform_name.c_str());
jstring roblox_version_string = env->NewStringUTF(roblox_version.c_str());
jstring exception_reason_filename_string =
env->NewStringUTF(exception_reason_filename.c_str());
jstring http_proxy_host_string = env->NewStringUTF(http_proxy_host.c_str());
jstring cookie_base_url_string = env->NewStringUTF(cookie_base_url.c_str());
jstring cookie_domain_string = env->NewStringUTF(cookie_domain.c_str());
jstring roblox_cookies_string = env->NewStringUTF(
credential != nullptr ? credential->c_str() : "");
jstring cookie_manager_cookies_string =
env->NewStringUTF(cookie_manager_cookies.c_str());
jstring android_id_string = env->NewStringUTF(android_id.c_str());
jstring advertising_id_string = env->NewStringUTF(advertising_id.c_str());
jstring googleplay_string = env->NewStringUTF("googleplay");
jstring user_id_string = env->NewStringUTF(account_user_id.c_str());
auto run_native_setting = [](const char* name, auto call) -> bool {
if (sigsetjmp(g_native_settings_jmp_buf, 1) == 0) {
g_native_settings_recovery_name = name;
g_native_settings_recovery_in_progress = 1;
call();
g_native_settings_recovery_in_progress = 0;
g_native_settings_recovery_name = nullptr;
return true;
}
g_native_settings_recovery_in_progress = 0;
std::cerr << " [engine] NativeSettings " << name
<< " recovered from crash\n"
<< std::flush;
g_native_settings_recovery_name = nullptr;
return false;
};
if (context->native_set_http_client_proxy &&
ShouldRunStartupStep("MOCKTAIL_NATIVE_SET_HTTP_CLIENT_PROXY", false)) {
std::cout << " [engine] NativeSettings httpClientProxy host="
<< http_proxy_host << " port=" << http_proxy_port << '\n'
<< std::flush;
run_native_setting("httpClientProxy", [&]() {
context->native_set_http_client_proxy(env, settings_class,
http_proxy_host_string,
http_proxy_port);
});
}
if (context->native_set_exception_reason_filename &&
ShouldRunStartupStep("MOCKTAIL_NATIVE_SET_EXCEPTION_REASON_FILENAME",
true)) {
std::cout << " [engine] NativeSettings exceptionReasonFilename="
<< exception_reason_filename << '\n'
<< std::flush;
run_native_setting("exceptionReasonFilename", [&]() {
context->native_set_exception_reason_filename(
env, settings_class, exception_reason_filename_string);
});
}
if (context->native_set_base_url &&
ShouldRunStartupStep("MOCKTAIL_NATIVE_SET_BASE_URL", true)) {
std::cout << " [engine] NativeSettings baseUrl=" << base_url
<< " apiUrl=" << api_url << '\n' << std::flush;
run_native_setting("baseUrl", [&]() {
context->native_set_base_url(env, settings_class, base_url_string,
api_url_string);
});
}
if (context->native_set_roblox_channel &&
ShouldRunStartupStep("MOCKTAIL_NATIVE_SET_ROBLOX_CHANNEL", true)) {
std::cout << " [engine] NativeSettings robloxChannel=" << roblox_channel
<< '\n'
<< std::flush;
run_native_setting("robloxChannel", [&]() {
context->native_set_roblox_channel(env, settings_class,
roblox_channel_string);
});
}
if (context->native_override_channel_platform_name &&
ShouldRunStartupStep("MOCKTAIL_NATIVE_OVERRIDE_CHANNEL_PLATFORM_NAME",
true)) {
std::cout << " [engine] NativeSettings channelPlatformName="
<< channel_platform_name << '\n' << std::flush;
run_native_setting("channelPlatformName", [&]() {
context->native_override_channel_platform_name(
env, settings_class, channel_platform_name_string);
});
}
if (context->native_set_roblox_version &&
ShouldRunStartupStep("MOCKTAIL_NATIVE_SET_ROBLOX_VERSION", true)) {
std::cout << " [engine] NativeSettings robloxVersion=" << roblox_version
<< '\n' << std::flush;
run_native_setting("robloxVersion", [&]() {
context->native_set_roblox_version(env, settings_class,
roblox_version_string);
});
}
if (context->native_set_device_info &&
ShouldRunStartupStep("MOCKTAIL_NATIVE_SET_DEVICE_INFO", true)) {
std::cout << " [engine] NativeSettings deviceInfo\n" << std::flush;
jobject device_params = BuildDeviceParams(env);
run_native_setting("deviceInfo", [&]() {
context->native_set_device_info(env, settings_class, device_params);
});
}
if (ShouldRunStartupStep("MOCKTAIL_NATIVE_SET_BASE_DATA_DIRS", true)) {
run_native_setting("baseDataDirectories", [&]() {
context->native_set_base_data_directories(env, settings_class,
data_dir_string,
external_base_string);
});
}
if (ShouldRunStartupStep("MOCKTAIL_NATIVE_SET_CACHE_DIR", true)) {
run_native_setting("cacheDirectory", [&]() {
context->native_set_cache_directory(env, settings_class,
settings_cache_dir_string);
});
}
if (ShouldRunStartupStep("MOCKTAIL_NATIVE_SET_FILES_DIR", true)) {
run_native_setting("filesDirectory", [&]() {
context->native_set_files_directory(env, settings_class,
files_dir_string);
});
}
if (ShouldRunStartupStep("MOCKTAIL_NATIVE_SET_EXTERNAL_DIR", true)) {
run_native_setting("externalDirectory", [&]() {
context->native_set_external_directory(env, settings_class,
external_dir_string);
});
}
if (ShouldRunStartupStep("MOCKTAIL_NATIVE_SET_PREFERENCES_FILE", true)) {
run_native_setting("preferencesFile", [&]() {
context->native_set_preferences_file(env, settings_class,
preferences_file_string);
});
}
if (ShouldRunStartupStep("MOCKTAIL_NATIVE_SET_DEFAULT_POLICY_FILE", false)) {
run_native_setting("defaultPolicyFile", [&]() {
context->native_set_default_app_policy_file(env, settings_class,
default_policy_file_string);
});
}
if (context->native_init_fast_log &&
ShouldRunStartupStep("MOCKTAIL_NATIVE_INIT_FAST_LOG", true)) {
std::cout << " [engine] NativeSettings initFastLog\n" << std::flush;
run_native_setting("initFastLog", [&]() {
context->native_init_fast_log(env, settings_class);
});
}
jclass cookie_manager_class = nullptr;
auto find_cookie_manager_class = [&]() -> jclass {
if (cookie_manager_class == nullptr) {
cookie_manager_class =
env->FindClass("com/roblox/universalapp/cookie/JNICookieManager");
}
return cookie_manager_class;
};
if (context->native_cookie_manager_set_cookie &&
!cookie_manager_cookies.empty() &&
// This native path expects Roblox's full cookie backend singleton. In
// Mocktail it currently reaches heap function pointers, so keep it opt-in.
ShouldRunStartupStep("MOCKTAIL_JNI_COOKIE_MANAGER_SET_COOKIE", false)) {
jclass cls = find_cookie_manager_class();
if (cls != nullptr) {
std::cout << " [engine] JNICookieManager.setCookie domain="
<< cookie_domain << " bytes=" << cookie_manager_cookies.size()
<< '\n' << std::flush;
if (IsEnabled("MOCKTAIL_UNSAFE_NATIVE_COOKIE_SETTER")) {
if (sigsetjmp(g_cookie_setter_jmp_buf, 1) == 0) {
g_cookie_setter_recovery_in_progress = 1;
context->native_cookie_manager_set_cookie(
env, cls, cookie_domain_string, cookie_manager_cookies_string);
g_cookie_setter_recovery_in_progress = 0;
std::cout << " [engine] JNICookieManager.setCookie returned\n"
<< std::flush;
} else {
g_cookie_setter_recovery_in_progress = 0;
std::cerr << " [engine] JNICookieManager.setCookie recovered; "
<< "native cookie singleton is not ready\n"
<< std::flush;
}
} else {
std::cerr << " [engine] JNICookieManager.setCookie blocked; set "
<< "MOCKTAIL_UNSAFE_NATIVE_COOKIE_SETTER=1 to run the "
<< "known-crashing native path\n"
<< std::flush;
}
}
} else if (!cookie_manager_cookies.empty() &&
context->native_cookie_manager_set_cookie != nullptr) {
std::cout << " [engine] JNICookieManager.setCookie skipped; set "
<< "MOCKTAIL_JNI_COOKIE_MANAGER_SET_COOKIE=1 to force\n"
<< std::flush;
} else if (!cookie_manager_cookies.empty() &&
context->native_cookie_manager_set_cookie == nullptr) {
std::cout << " [engine] WARNING: JNICookieManager.setCookie unavailable\n"
<< std::flush;
}
if (context->native_set_multiple_cookies && !roblox_cookies.empty() &&
ShouldRunStartupStep("MOCKTAIL_NATIVE_SET_MULTIPLE_COOKIES", true)) {
std::cout << " [engine] NativeSettings multipleCookies base="
<< cookie_base_url << " bytes=" << roblox_cookies.size() << '\n'
<< std::flush;
run_native_setting("multipleCookies", [&]() {
context->native_set_multiple_cookies(env, settings_class,
cookie_base_url_string,
roblox_cookies_string);
});
std::cout << " [engine] NativeSettings multipleCookies returned\n"
<< std::flush;
}
if (context->native_set_platform_headers_with_idfa &&
ShouldRunStartupStep("MOCKTAIL_NATIVE_SET_PLATFORM_HEADERS", false)) {
std::cout << " [engine] NativeSettings platformHeaders androidId="
<< android_id << '\n'
<< std::flush;
run_native_setting("platformHeaders", [&]() {
context->native_set_platform_headers_with_idfa(
env, settings_class, android_id_string, googleplay_string,
advertising_id_string);
});
}
if (context->native_set_user_id &&
ShouldRunStartupStep("MOCKTAIL_NATIVE_SET_USER_ID", false)) {
std::cout << " [engine] NativeSettings userId=" << account_user_id << '\n'
<< std::flush;
run_native_setting("userId", [&]() {
context->native_set_user_id(env, settings_class, user_id_string);
});
}
mocktail::runtime::SecurelyClearString(&cookie_manager_cookies);
}
void ConfigureLocalStorage(JNIEnv* env, const EngineStartupContext* context) {
if (!env || !context) {
return;
}
const std::string sober_data_root = SoberDataRoot();
const std::string sober_cache_root = SoberCacheRoot();
std::string data_dir = GetEnvStringDefaultPath(
"MOCKTAIL_ANDROID_DATA_DIR",
DefaultSoberAwarePath(sober_data_root.c_str(),
"/data/user/0/com.roblox.client"));
std::string files_dir = GetEnvStringDefaultPath(
"MOCKTAIL_ANDROID_FILES_DIR",
DefaultSoberAwarePath((sober_data_root + "/files").c_str(),
"/data/user/0/com.roblox.client/files"));
std::string cache_dir = GetEnvStringDefaultPath(
"MOCKTAIL_ANDROID_CACHE_DIR",
DefaultSoberAwarePath((sober_cache_root + "/cache").c_str(),
"/data/user/0/com.roblox.client/cache"));
EnsureAndroidDirectory(files_dir + "/appData");
EnsureAndroidDirectory(files_dir + "/appData/LocalStorage");
EnsureAndroidDirectory(files_dir + "/appData/OTAPatchBackups");
EnsureAndroidDirectory(files_dir + "/appData/rbx-storage");
EnsureAndroidDirectory(data_dir + "/rbx-storage");
EnsureAndroidDirectory(data_dir + "/shared_prefs");
EnsureAndroidDirectory(cache_dir);
EnsureAndroidDirectory(cache_dir + "/ContentProvider_2");
EnsureAndroidDirectory(cache_dir + "/rbx-storage");
EnsureAndroidDirectory(cache_dir + "/sounds");
jobject asset_manager = nullptr;
auto get_asset_manager = [&]() -> jobject {
if (asset_manager == nullptr) {
asset_manager = NewObject(env, "android/content/res/AssetManager");
}
return asset_manager;
};
if (context->native_init_asset_manager &&
ShouldRunStartupStep("MOCKTAIL_JNI_ASSET_MANAGER_SETUP", true)) {
jclass asset_manager_setup_class =
env->FindClass("com/roblox/client/JNIAAssetManagerSetup");
std::cout << " [engine] JNIAAssetManagerSetup.initNative\n"
<< std::flush;
context->native_init_asset_manager(env, asset_manager_setup_class,
get_asset_manager());
std::cout << " [engine] JNIAAssetManagerSetup.initNative returned\n"
<< std::flush;
}
if (context->native_local_storage_set_platform_impl &&
ShouldRunStartupStep("MOCKTAIL_LOCAL_STORAGE_SET_PLATFORM_IMPL", false)) {
jobject platform_handler = NewObject(
env,
"com/roblox/protocols/localstorageplatforminterface/generated/"
"IPlatformLocalStorageHandler");
jobject shared_preferences =
NewObject(env, "android/content/SharedPreferences");
const std::string shared_prefs_file =
data_dir + "/shared_prefs/LOCAL_STORAGE_SHARED_PREFS.xml";
SetObjectField(env, platform_handler, "sharedPreferences",
"Landroid/content/SharedPreferences;", shared_preferences);
SetStringField(env, platform_handler, "sharedPrefsFile",
shared_prefs_file.c_str());
SetStringField(env, platform_handler, "dataDir", data_dir.c_str());
jclass core_class = env->FindClass(
"com/roblox/protocols/localstorageplatforminterface/generated/"
"ILocalStorageHandlerCore");
std::cout << " [engine] LocalStorage setPlatformImpl sharedPrefs="
<< shared_prefs_file << '\n'
<< std::flush;
jobject core = context->native_local_storage_set_platform_impl(
env, core_class, platform_handler);
std::cout << " [engine] LocalStorage setPlatformImpl returned " << core
<< '\n'
<< std::flush;
}
if (!context->native_init_storage_manager ||
!ShouldRunStartupStep("MOCKTAIL_LOCAL_STORAGE_INIT_STORAGE_MANAGER",
true)) {
return;
}
jobject local_storage_manager =
NewObject(env, "com/roblox/client/LocalStorageManager");
jstring files_dir_string = env->NewStringUTF(files_dir.c_str());
jstring cache_dir_string = env->NewStringUTF(cache_dir.c_str());
std::cout << " [engine] LocalStorageManager.initStorageManagerNativeV3 files="
<< files_dir << " cache=" << cache_dir << '\n'
<< std::flush;
context->native_init_storage_manager(env, local_storage_manager,
get_asset_manager(), files_dir_string,
cache_dir_string);
std::cout << " [engine] LocalStorageManager.initStorageManagerNativeV3 returned\n"
<< std::flush;
}
// Resolve Android adapters from the runtime bundle even when a launcher
// relocates the executable. Host libraries with the same unversioned SONAME
// do not implement the Android ABI (notably VK_KHR_android_surface).
static std::string RuntimeBionicAdapterPath(const char* name) {
const char* override_dir = std::getenv("MOCKTAIL_RUNTIME_LIBRARY_DIR");
std::string directory;
if (override_dir != nullptr && override_dir[0] != '\0') {
directory = override_dir;
} else {
char executable_path[4097] = {};
const ssize_t length =
::readlink("/proc/self/exe", executable_path,
sizeof(executable_path) - 1U);
if (length <= 0) return {};
executable_path[length] = '\0';
std::string path(executable_path);
const std::string::size_type separator = path.find_last_of('/');
if (separator == std::string::npos) return {};
directory = separator == 0 ? "/" : path.substr(0, separator);
}
if (directory.empty()) return {};
return directory + "/" + name;
}
void* EngineStartupThread(void* arg) {
auto* context = static_cast<EngineStartupContext*>(arg);
if (context == nullptr) {
std::cerr << " [engine] invalid startup context\n" << std::flush;
return nullptr;
}
std::cout << " [engine] EngineStartupThread entered, context=" << context
<< "\n" << std::flush;
std::cout << " [engine] context flags: prepare=" << context->run_prepare_jni
<< " setAsset=" << context->run_set_asset_path
<< " initWithParams=" << context->run_init_with_params << '\n'
<< std::flush;
EngineLog("thread entered");
EngineLogPtr("context", context);
EngineLogPtr("JavaVM", context->java_vm);
std::cout << " [engine] thread java_vm=" << context->java_vm
<< " shared_vm=" << context->vm << '\n' << std::flush;
bool attached_to_thread = false;
JNIEnv* env = nullptr;
const bool skip_attach = IsEnabled("MOCKTAIL_ENGINE_SKIP_ATTACH");
if (skip_attach) {
std::cout << " [engine] skipping AttachCurrentThread (MOCKTAIL_ENGINE_SKIP_ATTACH)\n"
<< std::flush;
EngineLog("AttachCurrentThread skipped");
} else if (context->java_vm) {
std::cout << " [engine] entering AttachCurrentThread\n" << std::flush;
EngineLog("AttachCurrentThread");
void* raw_env = nullptr;
jint attach_result =
context->java_vm->AttachCurrentThread(&raw_env, nullptr);
std::cout << " [engine] AttachCurrentThread result=" << attach_result
<< " raw_env=" << raw_env << '\n' << std::flush;
if (attach_result == JNI_OK) {
env = static_cast<JNIEnv*>(raw_env);
g_stage6_jni_env = reinterpret_cast<uintptr_t>(env);
attached_to_thread = true;
}
EngineLog("AttachCurrentThread returned");
EngineLogPtr("attached JNIEnv", env);
}
if (!env) {
EngineLog("fallback GetJNIEnv");
}
if (!env) {
env = context->vm->GetJNIEnv();
g_stage6_jni_env = reinterpret_cast<uintptr_t>(env);
}
auto ensure_env = [&]() -> JNIEnv* {
if (!env) {
env = context->vm->GetJNIEnv();
}
return env;
};
if (!env) {
std::cerr << " [engine] failed to acquire JNIEnv\n";
return nullptr;
}
if (!InitializeActiveHostAbiThread()) {
std::cerr << " [engine] allocator TLS init failed on startup thread\n"
<< std::flush;
return nullptr;
}
std::cout << " [engine] native allocator TLS initialized\n"
<< std::flush;
EngineLogPtr("reset JNIEnv", env);
EngineLog("PublishCurrentJniEnv");
PublishCurrentJniEnv(env);
// Keep EGL unbound by default so the worker that runs real Roblox surface
// calls can become the first owner of the context. Binding here makes later
// worker MakeCurrent fail with EGL_BAD_ACCESS on Mesa/Wayland.
if (mocktail::window::IsInitialised() &&
IsEnabled("MOCKTAIL_BIND_EGL_ON_STARTUP_THREAD")) {
std::cout << " [engine] binding EGL context on engine thread\n"
<< std::flush;
mocktail::window::MakeCurrentOnThread();
}
// JNI_OnLoad replaces env->functions; Stage 6 needs the pseudo-VM table.
if (env && context->vm) {
context->vm->RestoreFunctions();
env = context->vm->GetJNIEnv();
std::cerr << " [engine] env->functions restored: " << (void*)env->functions << "\n" << std::flush;
}
EngineLogPtr("JNIEnv", env);
EngineLogPtr("JNIEnv.functions", env ? env->functions : nullptr);
if (!context->run_prepare_jni) {
EngineLog("JNI prep disabled");
if (attached_to_thread) {
EngineLog("DetachCurrentThread");
jint detach_result = context->java_vm->DetachCurrentThread();
if (detach_result != JNI_OK) {
std::cerr << " [engine] DetachCurrentThread failed: " << detach_result
<< '\n' << std::flush;
}
attached_to_thread = false;
}
return nullptr;
}
EngineLog("FindClass NativeGLInterface");
jclass native_gl_class =
env->FindClass("com/roblox/engine/jni/NativeGLInterface");
jclass native_input_class =
env->FindClass("com/roblox/engine/jni/NativeInputInterface");
if (!native_input_class) {
native_input_class = native_gl_class;
}
jclass native_settings_class =
env->FindClass("com/roblox/engine/jni/NativeSettingsInterface");
jclass native_app_bridge_class =
env->FindClass("com/roblox/engine/jni/NativeAppBridgeInterface");
jclass native_gl_java_class =
env->FindClass("com/roblox/engine/jni/NativeGLJavaInterface");
if (!native_app_bridge_class) {
native_app_bridge_class = native_gl_class;
}
jclass startup_activity_class =
env->FindClass("com/roblox/client/startup/MainGameActivity");
if (!startup_activity_class) {
startup_activity_class = native_gl_class;
}
EngineLog("FindClass returned");
EngineLog("NewStringUTF args");
jstring empty_string = env->NewStringUTF("");
jstring base_url = env->NewStringUTF("https://www.roblox.com/");
jstring user_agent = NewStringFromEnvDefault(
env, "MOCKTAIL_USER_AGENT",
"Roblox/unknown (Linux; Android 33; Mocktail)");
jstring android_id =
NewStringFromEnvDefault(env, "MOCKTAIL_ANDROID_ID", "0000000000000000");
jstring launch_source =
NewStringFromEnvDefault(env, "MOCKTAIL_LAUNCH_SOURCE", "AppAndroidV");
jstring client_settings = NewClientSettingsString(env);
jstring client_settings_overrides =
NewStringFromEnvDefault(env, "MOCKTAIL_CLIENT_SETTINGS_OVERRIDES_JSON",
"{}");
jstring client_settings_signature =
NewStringFromEnvDefault(env, "MOCKTAIL_CLIENT_SETTINGS_SIGNATURE", "");
jstring client_settings_group =
NewStringFromEnvDefault(env, "MOCKTAIL_CLIENT_SETTINGS_GROUP",
"GoogleAndroidApp");
jstring fast_flags =
NewStringFromEnvDefault(env, "MOCKTAIL_FAST_FLAGS_JSON", "{}");
const bool is_headless = IsHeadlessMode();
const char* launch_mode = std::getenv("MOCKTAIL_LAUNCH_MODE");
const char* default_launch_mode = is_headless ? "headless" : "normal";
std::string default_app_params =
std::string("{\"launchMode\":\"") + (launch_mode != nullptr &&
launch_mode[0] != '\0'
? launch_mode
: default_launch_mode) +
"\",\"placeId\":0}";
jstring app_params = NewStringFromEnvDefault(
env, "MOCKTAIL_APP_PARAMS_JSON", default_app_params.c_str());
EngineLogPtr("JNIEnv.functions", env ? env->functions : nullptr);
jstring asset_path =
env->NewStringUTF(GetEnvStringDefaultPath("MOCKTAIL_ASSET_PATH",
DefaultAssetPath())
.c_str());
std::string asset_path_value = JStringToString(env, asset_path);
if (asset_path_value.empty()) {
asset_path_value = DefaultAssetPath();
asset_path = env->NewStringUTF(asset_path_value.c_str());
}
const bool app_bridge_init_headless =
is_headless || IsEnabled("MOCKTAIL_APP_BRIDGE_HEADLESS_INIT_PARAMS");
if (app_bridge_init_headless != is_headless) {
std::cout << " [engine] AppBridge init uses headless params while "
<< "windowed surface/start stay enabled\n"
<< std::flush;
}
if (IsEnabled("MOCKTAIL_ENGINE_TRACE")) {
std::cout << " [engine] asset path prepared: " << asset_path_value
<< '\n'
<< std::flush;
}
jobject surface = BuildMockSurface(env);
jobject game_activity = NewObject(env, "com/roblox/client/startup/MainGameActivity");
jobject game_surface_activity =
IsEnabled("MOCKTAIL_PASS_ACTIVITY_TO_GAME_SURFACE_PARAMS")
? game_activity
: nullptr;
jobject app_bridge_notification_listener =
NewObject(env,
"com/roblox/engine/jni/OnAppBridgeNotificationListener");
jobject game_activity_asset_manager =
NewObject(env, "android/content/res/AssetManager");
jobject game_activity_config = BuildConfiguration(env);
jobject platform_params = BuildPlatformParams(env, surface, is_headless);
jobject device_params = BuildDeviceParams(env);
const jnivm::RobloxAuthIdentity& account_identity =
context->account_identity;
jobject start_game_params =
BuildStartGameParams(env, platform_params, device_params, surface,
game_activity, "MOCKTAIL_GAME_PARAMS_JSON",
account_identity);
jobject start_app_params = BuildStartAppParams(env, app_params, platform_params,
surface, is_headless,
launch_mode, account_identity);
InstallNativeGlJavaImplementation(env, native_gl_java_class);
EngineLog("NewStringUTF returned");
jlong game_activity_handle = 0;
bool game_activity_init_attempted = false;
auto run_game_activity_initialize = [&]() -> jlong {
if (!context->run_game_activity_init ||
context->native_game_activity_init == nullptr) {
return 0;
}
if (game_activity_init_attempted) {
return game_activity_handle;
}
game_activity_init_attempted = true;
env = ensure_env();
std::cout << " [engine] GameActivity.initializeNativeCode\n"
<< std::flush;
jstring internal_data_dir =
env->NewStringUTF("/data/user/0/com.roblox.client/files");
jstring obb_dir =
env->NewStringUTF("/sdcard/Android/obb/com.roblox.client");
jstring external_data_dir =
env->NewStringUTF("/sdcard/Android/data/com.roblox.client/files");
if (sigsetjmp(g_game_activity_init_jmp_buf, 1) == 0) {
g_game_activity_init_recovery_in_progress = 1;
g_saved_game_activity = game_activity;
game_activity_handle = context->native_game_activity_init(
env, game_activity, internal_data_dir, obb_dir, external_data_dir,
game_activity_asset_manager, nullptr, game_activity_config);
g_game_activity_init_recovery_in_progress = 0;
} else {
g_game_activity_init_recovery_in_progress = 0;
game_activity_handle = 0;
std::cerr << " [engine] GameActivity.initializeNativeCode recovered\n"
<< std::flush;
}
g_game_activity_native_handle =
static_cast<uintptr_t>(game_activity_handle);
std::cout << " [engine] GameActivity.initializeNativeCode returned "
<< game_activity_handle << '\n'
<< std::flush;
if (game_activity_handle != 0 &&
IsEnabled("MOCKTAIL_DUMP_GAME_ACTIVITY_HANDLE")) {
auto** game_activity_slots = reinterpret_cast<void**>(
static_cast<uintptr_t>(game_activity_handle));
std::cout << " [engine] GameActivity handle slots:";
for (int i = 0; i < 40; ++i) {
std::cout << " [" << i << "]=" << game_activity_slots[i];
}
std::cout << '\n' << std::flush;
}
return game_activity_handle;
};
if (context->run_game_activity_init) {
run_game_activity_initialize();
}
if (context->run_set_asset_path) {
env = ensure_env();
std::cout << " [engine] nativeSetAssetPath\n" << std::flush;
if (context->call_real_set_asset_path) {
if (sigsetjmp(g_set_asset_path_jmp_buf, 1) == 0) {
g_set_asset_path_recovery_in_progress = 1;
if (EngineTraceEnabled()) {
std::cerr << " [engine] nativeSetAssetPath env=" << (void*)env
<< " class=" << (void*)startup_activity_class
<< " path=" << asset_path_value << '\n'
<< std::flush;
}
context->native_set_asset_path(env, startup_activity_class, asset_path);
g_set_asset_path_recovery_in_progress = 0;
} else {
std::cerr << " [engine] nativeSetAssetPath recovered\n" << std::flush;
MocktailSetAssetPath(env, asset_path);
}
} else {
MocktailSetAssetPath(env, asset_path);
}
std::cout << " [engine] nativeSetAssetPath returned\n" << std::flush;
}
if (context->run_native_settings) {
env = ensure_env();
std::cout << " [engine] NativeSettings directories\n" << std::flush;
ConfigureNativeSettings(env, native_settings_class, context);
ConfigureLocalStorage(env, context);
if (context->native_base_url_protocol_init != nullptr) {
jclass base_url_protocol_class =
env->FindClass("com/roblox/universalapp/linking/JNIBaseUrlProtocol");
std::cout << " [engine] JNIBaseUrlProtocol.init\n" << std::flush;
context->native_base_url_protocol_init(env, base_url_protocol_class,
game_activity);
std::cout << " [engine] JNIBaseUrlProtocol.init returned\n"
<< std::flush;
}
std::cout << " [engine] NativeSettings directories returned\n"
<< std::flush;
}
if (context->run_global_init) {
env = ensure_env();
std::cout << " [engine] nativeGameGlobalInit\n" << std::flush;
if (sigsetjmp(g_game_global_init_jmp_buf, 1) == 0) {
g_game_global_init_recovery_in_progress = 1;
context->native_global_init(env, native_gl_class);
g_game_global_init_recovery_in_progress = 0;
} else {
g_game_global_init_recovery_in_progress = 0;
const char msg[] = " [engine] nativeGameGlobalInit recovered\n";
write(2, msg, sizeof(msg) - 1);
}
const char msg[] = " [engine] nativeGameGlobalInit returned\n";
write(1, msg, sizeof(msg) - 1);
}
if (context->native_update_adapter_init &&
!IsDisabled("MOCKTAIL_UPDATE_ADAPTER_INIT")) {
env = ensure_env();
std::cout << " [engine] nativeUpdateAdapterInit\n" << std::flush;
context->native_update_adapter_init(env, native_gl_class);
std::cout << " [engine] nativeUpdateAdapterInit returned\n" << std::flush;
}
if (context->run_update_screen_orientation &&
context->native_update_screen_orientation != nullptr) {
env = ensure_env();
if (sigsetjmp(g_update_screen_orientation_jmp_buf, 1) == 0) {
const jint orientation =
static_cast<jint>(GetEnvInt("MOCKTAIL_SCREEN_ORIENTATION", 2));
std::cout << " [engine] nativeUpdateScreenOrientation orientation="
<< orientation << '\n'
<< std::flush;
g_update_screen_orientation_recovery_in_progress = 1;
context->native_update_screen_orientation(env, native_input_class,
orientation);
g_update_screen_orientation_recovery_in_progress = 0;
std::cout << " [engine] nativeUpdateScreenOrientation returned\n"
<< std::flush;
} else {
g_update_screen_orientation_recovery_in_progress = 0;
std::cerr << " [engine] nativeUpdateScreenOrientation recovered\n"
<< std::flush;
}
}
if (context->run_init_client_settings) {
env = ensure_env();
std::cout << " [engine] nativeInitClientSettings\n" << std::flush;
if (sigsetjmp(g_init_client_settings_jmp_buf, 1) == 0) {
g_init_client_settings_recovery_in_progress = 1;
const char* variant = std::getenv("MOCKTAIL_INIT_CLIENT_SETTINGS_VARIANT");
if (variant == nullptr || variant[0] == '\0') {
variant = "classic";
}
jint settings_result = 0;
const jlong client_settings_timestamp_seconds =
static_cast<jlong>(time(nullptr));
if (std::strcmp(variant, "compressed") == 0 &&
context->native_init_client_settings_cached_compressed != nullptr) {
jbyteArray empty_compressed_settings = env->NewByteArray(0);
settings_result =
context->native_init_client_settings_cached_compressed(
env, native_gl_class, empty_compressed_settings,
client_settings_overrides, client_settings_group, empty_string,
client_settings_timestamp_seconds, JNI_FALSE);
} else if (std::strcmp(variant, "cached") == 0 &&
context->native_init_client_settings_cached != nullptr) {
settings_result = context->native_init_client_settings_cached(
env, native_gl_class, client_settings, client_settings_overrides,
client_settings_group, empty_string,
client_settings_timestamp_seconds);
} else if (std::strcmp(variant, "signed") == 0 &&
context->native_init_client_settings_signed != nullptr) {
settings_result = context->native_init_client_settings_signed(
env, native_gl_class, client_settings, client_settings_signature,
client_settings_overrides, client_settings_group);
} else if (context->native_init_client_settings != nullptr) {
variant = "classic";
settings_result = context->native_init_client_settings(
env, native_gl_class, client_settings, client_settings_overrides,
client_settings_group);
} else {
std::cerr << " [engine] nativeInitClientSettings has no usable "
<< "variant\n"
<< std::flush;
}
g_init_client_settings_recovery_in_progress = 0;
std::cout << " [engine] nativeInitClientSettings returned "
<< settings_result << " via " << variant << '\n'
<< std::flush;
if (context->run_native_settings &&
ShouldRunStartupStep("MOCKTAIL_REAPPLY_NATIVE_SETTINGS_AFTER_INIT",
true)) {
std::cout << " [engine] reapplying NativeSettings after "
<< "nativeInitClientSettings\n"
<< std::flush;
ConfigureNativeSettings(env, native_settings_class, context);
}
} else {
g_init_client_settings_recovery_in_progress = 0;
std::cerr << " [engine] nativeInitClientSettings recovered\n" << std::flush;
}
}
if (context->run_post_client_settings) {
env = ensure_env();
std::cout << " [engine] nativePostClientSettingsLoadedInitialization3\n"
<< std::flush;
if (IsEnabled("MOCKTAIL_TRACE_POST_CLIENT_SETTINGS_JNI")) {
setenv("MOCKTAIL_JNI_TRACE", "1", 1);
}
if (sigsetjmp(g_post_client_settings_jmp_buf, 1) == 0) {
g_post_client_settings_recovery_in_progress = 1;
jobject application_exit_info_list = BuildApplicationExitInfoList(env);
context->native_post_client_settings(env, native_gl_class,
application_exit_info_list);
g_post_client_settings_recovery_in_progress = 0;
} else {
g_post_client_settings_recovery_in_progress = 0;
std::cerr << " [engine] nativePostClientSettingsLoadedInitialization3 recovered\n"
<< std::flush;
}
std::cout
<< " [engine] nativePostClientSettingsLoadedInitialization3 returned\n"
<< std::flush;
}
if (context->native_initialize_native_flags != nullptr &&
ShouldRunStartupStep("MOCKTAIL_INITIALIZE_NATIVE_FLAGS", false)) {
env = ensure_env();
std::cout << " [engine] nativeInitializeNativeFlags\n" << std::flush;
jclass flag_jni_class =
env->FindClass("com/roblox/client/flags/FlagJniInterface");
jclass string_class = env->FindClass("java/lang/String");
jobjectArray native_flag_keys =
env->NewObjectArray(0, string_class, nullptr);
if (sigsetjmp(g_initialize_native_flags_jmp_buf, 1) == 0) {
g_initialize_native_flags_recovery_in_progress = 1;
jobject init_result = context->native_initialize_native_flags(
env, flag_jni_class, native_flag_keys);
g_initialize_native_flags_recovery_in_progress = 0;
std::cout << " [engine] nativeInitializeNativeFlags returned "
<< init_result << '\n'
<< std::flush;
} else {
g_initialize_native_flags_recovery_in_progress = 0;
std::cerr << " [engine] nativeInitializeNativeFlags recovered\n"
<< std::flush;
}
}
// All client-setting initialization is complete. Verify the native switch
// before any LuaApp startup can compile menu scripts. Failure leaves the
// client's own codegen policy unchanged.
if (context->luau_codegen_switch != nullptr) {
context->luau_codegen_switch->PrepareForMenus();
}
if (context->run_set_init_params) {
env = ensure_env();
if (!context->run_native_settings && context->native_set_device_info &&
ShouldRunStartupStep("MOCKTAIL_NATIVE_SET_DEVICE_INFO", true)) {
std::cout << " [engine] NativeSettings deviceInfo\n" << std::flush;
jobject device_params = BuildDeviceParams(env);
context->native_set_device_info(env, native_settings_class,
device_params);
std::cout << " [engine] NativeSettings deviceInfo returned\n"
<< std::flush;
}
std::cout << " [engine] nativeAppBridgeSetInitParams\n" << std::flush;
jobject init_params =
BuildAppBridgeInitParams(env, client_settings, fast_flags, app_params,
asset_path, is_headless);
context->native_set_init_params(env, startup_activity_class, init_params);
std::cout << " [engine] nativeAppBridgeSetInitParams returned\n"
<< std::flush;
if (context->native_retry_init && IsEnabled("MOCKTAIL_RETRY_INIT")) {
std::cout << " [engine] nativeRetryInit\n" << std::flush;
context->native_retry_init(env, startup_activity_class);
std::cout << " [engine] nativeRetryInit returned\n" << std::flush;
}
}
if (!context->run_app_bridge_app_start &&
IsEnabled("MOCKTAIL_SET_APP_BRIDGE_NOTIFICATION_LISTENER") &&
native_gl_java_class && app_bridge_notification_listener) {
env = ensure_env();
std::cout << " [engine] NativeGLJavaInterface."
<< "setAppBridgeNotificationListener\n"
<< std::flush;
jmethodID set_listener = env->GetStaticMethodID(
native_gl_java_class, "setAppBridgeNotificationListener",
"(Lcom/roblox/engine/jni/OnAppBridgeNotificationListener;)V");
env->CallStaticVoidMethod(native_gl_java_class, set_listener,
app_bridge_notification_listener);
std::cout << " [engine] NativeGLJavaInterface."
<< "setAppBridgeNotificationListener returned\n"
<< std::flush;
}
if (context->run_app_bridge_app_start &&
context->native_app_bridge_app_start) {
env = ensure_env();
if (IsEnabled("MOCKTAIL_TRACE_APP_BRIDGE_APP_START_JNI")) {
setenv("MOCKTAIL_JNI_TRACE", "1", 1);
}
if (context->native_set_is_first_install) {
std::cout << " [engine] NativeAppBridgeInterface.setIsFirstInstall\n"
<< std::flush;
context->native_set_is_first_install(env, native_app_bridge_class,
JNI_FALSE);
std::cout << " [engine] NativeAppBridgeInterface.setIsFirstInstall "
<< "returned\n"
<< std::flush;
}
if (native_gl_java_class && app_bridge_notification_listener &&
ShouldRunStartupStep("MOCKTAIL_SET_APP_BRIDGE_NOTIFICATION_LISTENER",
true)) {
std::cout << " [engine] NativeGLJavaInterface."
<< "setAppBridgeNotificationListener\n"
<< std::flush;
jmethodID set_listener = env->GetStaticMethodID(
native_gl_java_class, "setAppBridgeNotificationListener",
"(Lcom/roblox/engine/jni/OnAppBridgeNotificationListener;)V");
env->CallStaticVoidMethod(native_gl_java_class, set_listener,
app_bridge_notification_listener);
std::cout << " [engine] NativeGLJavaInterface."
<< "setAppBridgeNotificationListener returned\n"
<< std::flush;
}
std::cout << " [engine] nativeAppBridgeAppStart\n" << std::flush;
if (sigsetjmp(g_app_bridge_app_start_jmp_buf, 1) == 0) {
g_app_bridge_app_start_recovery_in_progress = 1;
context->native_app_bridge_app_start(
env, native_app_bridge_class, base_url, user_agent, JNI_FALSE,
android_id, launch_source, empty_string);
g_app_bridge_app_start_recovery_in_progress = 0;
} else {
g_app_bridge_app_start_recovery_in_progress = 0;
std::cerr << " [engine] nativeAppBridgeAppStart recovered\n"
<< std::flush;
}
std::cout << " [engine] nativeAppBridgeAppStart returned\n"
<< std::flush;
}
if (context->run_init_with_params) {
env = ensure_env();
std::cout << " [engine] nativeAppBridgeV2InitWithParams\n" << std::flush;
if (context->call_real_init_with_params) {
jobject init_params =
BuildAppBridgeInitParams(env, client_settings, fast_flags, app_params,
asset_path, app_bridge_init_headless);
if (sigsetjmp(g_init_with_params_jmp_buf, 1) == 0) {
g_init_with_params_recovery_in_progress = 1;
context->native_init_with_params(env, native_gl_class, init_params);
g_init_with_params_recovery_in_progress = 0;
} else {
std::cerr << " [engine] nativeAppBridgeV2InitWithParams recovered\n"
<< std::flush;
MocktailAppBridgeInit(env, app_params);
}
} else {
MocktailAppBridgeInit(env, app_params);
}
std::cout << " [engine] nativeAppBridgeV2InitWithParams returned\n"
<< std::flush;
g_main_thread_message_pump_ready.store(1);
if (IsEnabled("MOCKTAIL_TRACE_MAIN_THREAD_PUMP")) {
std::cerr << " [engine] nativeCallMessagesFromMainThread pump ready\n"
<< std::flush;
}
}
if (context->run_game_activity_init && context->native_game_activity_init) {
env = ensure_env();
jlong handle = run_game_activity_initialize();
if (context->run_game_activity_surface && handle != 0) {
if (!IsDisabled("MOCKTAIL_GAME_ACTIVITY_CLEAR_APP_CMD_SLOT")) {
auto** game_activity_slots = reinterpret_cast<void**>(
static_cast<uintptr_t>(handle));
std::cout << " [engine] GameActivity slot[1] before clear="
<< game_activity_slots[1] << '\n'
<< std::flush;
game_activity_slots[1] = nullptr;
}
auto* on_start = reinterpret_cast<GameActivityLifecycleFn>(
mocktail_gameactivity_on_start_native);
auto* on_resume = reinterpret_cast<GameActivityLifecycleFn>(
mocktail_gameactivity_on_resume_native);
auto* on_surface_created = reinterpret_cast<GameActivitySurfaceCreatedFn>(
mocktail_gameactivity_on_surface_created_native);
auto* on_surface_changed = reinterpret_cast<GameActivitySurfaceChangedFn>(
mocktail_gameactivity_on_surface_changed_native);
auto* on_surface_redraw_needed =
reinterpret_cast<GameActivitySurfaceCreatedFn>(
mocktail_gameactivity_on_surface_redraw_needed_native);
std::cout << " [engine] GameActivity callbacks:"
<< " start=" << reinterpret_cast<void*>(on_start)
<< " resume=" << reinterpret_cast<void*>(on_resume)
<< " created=" << reinterpret_cast<void*>(on_surface_created)
<< " changed=" << reinterpret_cast<void*>(on_surface_changed)
<< " redraw="
<< reinterpret_cast<void*>(on_surface_redraw_needed) << '\n'
<< std::flush;
const bool run_lifecycle_callbacks =
IsEnabled("MOCKTAIL_GAME_ACTIVITY_LIFECYCLE_CALLBACKS");
if (sigsetjmp(g_game_activity_surface_jmp_buf, 1) == 0) {
g_game_activity_surface_recovery_in_progress = 1;
if (run_lifecycle_callbacks && on_start) {
on_start(env, game_activity, handle);
}
if (run_lifecycle_callbacks && on_resume) {
on_resume(env, game_activity, handle);
}
if (run_lifecycle_callbacks && on_surface_created) {
on_surface_created(env, game_activity, handle, surface);
}
if (run_lifecycle_callbacks && on_surface_changed) {
const mocktail::runtime::DisplaySize changed_size =
HostWindowPixelSize();
on_surface_changed(env, game_activity, handle, surface, 4,
changed_size.width, changed_size.height);
}
if (run_lifecycle_callbacks && on_surface_redraw_needed) {
on_surface_redraw_needed(env, game_activity, handle, surface);
}
g_game_activity_surface_recovery_in_progress = 0;
std::cout << " [engine] GameActivity surface callbacks returned\n"
<< std::flush;
} else {
g_game_activity_surface_recovery_in_progress = 0;
std::cerr << " [engine] GameActivity surface callbacks recovered\n"
<< std::flush;
}
}
}
if (context->run_activity_lifecycle) {
env = ensure_env();
if (context->vm != nullptr) {
context->vm->RestoreFunctions();
env = context->vm->GetJNIEnv();
PublishCurrentJniEnv(env);
}
jobject lifecycle_callbacks =
NewObject(env,
"com/roblox/universalapp/activitylifecyclecallbacks/"
"JNIActivityLifecycleCallbacks");
const char* activity_name_env =
std::getenv("MOCKTAIL_ACTIVITY_LIFECYCLE_ACTIVITY_NAME");
const char* activity_name =
activity_name_env != nullptr && activity_name_env[0] != '\0'
? activity_name_env
: "MainGameActivity";
jstring activity_name_string = env->NewStringUTF(activity_name);
std::cout << " [engine] activity lifecycle for " << activity_name << '\n'
<< std::flush;
auto invoke_lifecycle_callback =
[&](const char* label, NativeActivityLifecycleStringFn callback) {
if (callback == nullptr) {
return;
}
std::cout << " [engine] JNIActivityLifecycleCallbacks." << label
<< '\n'
<< std::flush;
if (sigsetjmp(g_activity_lifecycle_jmp_buf, 1) == 0) {
g_activity_lifecycle_recovery_in_progress = 1;
callback(env, lifecycle_callbacks, activity_name_string);
g_activity_lifecycle_recovery_in_progress = 0;
std::cout << " [engine] JNIActivityLifecycleCallbacks." << label
<< " returned\n"
<< std::flush;
} else {
g_activity_lifecycle_recovery_in_progress = 0;
std::cerr << " [engine] JNIActivityLifecycleCallbacks." << label
<< " recovered from crash\n"
<< std::flush;
}
};
invoke_lifecycle_callback(
"nativeOnPreCreated",
context->activity_lifecycle_callbacks.on_pre_created);
invoke_lifecycle_callback(
"nativeOnCreated", context->activity_lifecycle_callbacks.on_created);
invoke_lifecycle_callback(
"nativeOnPostCreated",
context->activity_lifecycle_callbacks.on_post_created);
invoke_lifecycle_callback(
"nativeOnPreStarted",
context->activity_lifecycle_callbacks.on_pre_started);
invoke_lifecycle_callback(
"nativeOnStarted", context->activity_lifecycle_callbacks.on_started);
invoke_lifecycle_callback(
"nativeOnPostStarted",
context->activity_lifecycle_callbacks.on_post_started);
invoke_lifecycle_callback(
"nativeOnPreResumed",
context->activity_lifecycle_callbacks.on_pre_resumed);
invoke_lifecycle_callback(
"nativeOnResumed", context->activity_lifecycle_callbacks.on_resumed);
invoke_lifecycle_callback(
"nativeOnPostResumed",
context->activity_lifecycle_callbacks.on_post_resumed);
std::cout << " [engine] activity lifecycle returned\n" << std::flush;
}
if (context->run_app_lifecycle_active &&
context->native_app_lifecycle_set_active) {
env = ensure_env();
std::cout << " [engine] JNIAppLifecycleNativeAdapter.setActive\n"
<< std::flush;
context->native_app_lifecycle_set_active(env, native_gl_class);
std::cout << " [engine] JNIAppLifecycleNativeAdapter.setActive returned\n"
<< std::flush;
}
if (context->run_native_fragment_start &&
context->native_on_fragment_start) {
env = ensure_env();
std::cout << " [engine] NativeGLInterface.nativeOnFragmentStart\n"
<< std::flush;
if (sigsetjmp(g_native_fragment_start_jmp_buf, 1) == 0) {
g_native_fragment_start_recovery_in_progress = kStage6RecoveryInline;
context->native_on_fragment_start(env, native_gl_class);
g_native_fragment_start_recovery_in_progress = kStage6RecoveryInactive;
std::cout << " [engine] NativeGLInterface.nativeOnFragmentStart returned\n"
<< std::flush;
} else {
g_native_fragment_start_recovery_in_progress = kStage6RecoveryInactive;
std::cerr << " [engine] NativeGLInterface.nativeOnFragmentStart recovered\n"
<< std::flush;
}
}
const mocktail::platform::DisplayRefreshCapabilities display_refresh =
mocktail::window::GetDisplayRefreshCapabilities();
if (context->run_display_refresh_rate && display_refresh.valid() &&
context->native_pass_current_display_refresh_rate) {
env = ensure_env();
std::cout << " [engine] NativeGLInterface.nativePassCurrentDisplayRefreshRate "
<< display_refresh.current_hz << '\n'
<< std::flush;
if (sigsetjmp(g_display_refresh_rate_jmp_buf, 1) == 0) {
g_display_refresh_rate_recovery_in_progress = kStage6RecoveryInline;
context->native_pass_current_display_refresh_rate(env, native_gl_class,
display_refresh.current_hz);
g_display_refresh_rate_recovery_in_progress = kStage6RecoveryInactive;
std::cout
<< " [engine] NativeGLInterface.nativePassCurrentDisplayRefreshRate returned\n"
<< std::flush;
} else {
g_display_refresh_rate_recovery_in_progress = kStage6RecoveryInactive;
std::cerr
<< " [engine] NativeGLInterface.nativePassCurrentDisplayRefreshRate recovered\n"
<< std::flush;
}
}
if (context->run_display_refresh_rate && display_refresh.valid() &&
context->native_pass_supported_refresh_rates &&
IsEnabled("MOCKTAIL_PASS_SUPPORTED_REFRESH_RATES")) {
env = ensure_env();
std::cout << " [engine] NativeGLInterface.nativePassSupportedRefreshRates"
<< " count=" << display_refresh.supported_hz.size() << '\n'
<< std::flush;
if (sigsetjmp(g_display_refresh_rate_jmp_buf, 1) == 0) {
g_display_refresh_rate_recovery_in_progress = kStage6RecoveryInline;
const jsize count =
static_cast<jsize>(display_refresh.supported_hz.size());
jfloatArray refresh_rates = env->NewFloatArray(count);
if (refresh_rates != nullptr && count > 0) {
env->SetFloatArrayRegion(refresh_rates, 0, count,
display_refresh.supported_hz.data());
context->native_pass_supported_refresh_rates(env, native_gl_class,
refresh_rates);
}
g_display_refresh_rate_recovery_in_progress = kStage6RecoveryInactive;
std::cout
<< " [engine] NativeGLInterface.nativePassSupportedRefreshRates returned\n"
<< std::flush;
} else {
g_display_refresh_rate_recovery_in_progress = kStage6RecoveryInactive;
std::cerr
<< " [engine] NativeGLInterface.nativePassSupportedRefreshRates recovered\n"
<< std::flush;
}
}
if (context->native_update_app_ui_sizes &&
ShouldRunStartupStep("MOCKTAIL_UPDATE_APP_UI_SIZES", false)) {
env = ensure_env();
std::cout << " [engine] updateAppUISizes\n" << std::flush;
const mocktail::runtime::DisplaySize ui_size = HostWindowPixelSize();
context->native_update_app_ui_sizes(env, native_gl_class, ui_size.width,
ui_size.height, 0, 0, 0);
std::cout << " [engine] updateAppUISizes returned\n" << std::flush;
}
if (context->native_update_mobile_haptics_state) {
env = ensure_env();
std::cout << " [engine] NativeGLInterface.nativeUpdateMobileHapticsState\n"
<< std::flush;
const std::string motors_json =
mocktail::runtime::RobloxHapticProtocolBridge::GetMotorsJson();
jstring motors_str = env->NewStringUTF(motors_json.c_str());
if (motors_str != nullptr) {
context->native_update_mobile_haptics_state(env, native_gl_class,
JNI_TRUE, motors_str);
env->DeleteLocalRef(motors_str);
}
std::cout
<< " [engine] NativeGLInterface.nativeUpdateMobileHapticsState returned\n"
<< std::flush;
}
int init_delay_ms = GetEnvInt("MOCKTAIL_APPBRIDGE_INIT_DELAY_MS", 0);
if (init_delay_ms > 0) {
std::cout << " [engine] wait after AppBridge init: " << init_delay_ms
<< " ms\n"
<< std::flush;
usleep(static_cast<useconds_t>(init_delay_ms) * 1000);
}
if (context->run_start_lua_app_dm && context->native_start_lua_app_dm &&
!IsEnabled("MOCKTAIL_START_LUA_APP_DM_AFTER_START_APP")) {
int delay_ms = GetEnvInt("MOCKTAIL_START_LUA_APP_DM_DELAY_MS", 1500);
if (IsEnabled("MOCKTAIL_START_LUA_APP_DM_INLINE")) {
if (delay_ms > 0) {
std::cout << " [engine] nativeAppBridgeStartLuaAppDM wait "
<< delay_ms << " ms\n"
<< std::flush;
usleep(static_cast<useconds_t>(delay_ms) * 1000);
}
env = ensure_env();
if (IsEnabled("MOCKTAIL_TRACE_START_LUA_JNI")) {
setenv("MOCKTAIL_JNI_TRACE", "1", 1);
}
std::cout << " [engine] nativeAppBridgeStartLuaAppDM\n"
<< std::flush;
if (sigsetjmp(g_start_lua_app_dm_jmp_buf, 1) == 0) {
g_start_lua_app_dm_recovery_in_progress = kStage6RecoveryInline;
context->native_start_lua_app_dm(env, native_gl_class);
g_start_lua_app_dm_recovery_in_progress = kStage6RecoveryInactive;
} else {
g_start_lua_app_dm_recovery_in_progress = kStage6RecoveryInactive;
std::cerr << " [engine] nativeAppBridgeStartLuaAppDM recovered\n"
<< std::flush;
}
std::cout << " [engine] nativeAppBridgeStartLuaAppDM returned\n"
<< std::flush;
} else {
g_pending_main_thread_start_lua_app_dm =
context->native_start_lua_app_dm;
g_pending_main_thread_start_lua_due_ms =
MonotonicMillis() + static_cast<uint64_t>(delay_ms);
g_pending_main_thread_start_lua_started = false;
std::cout
<< " [engine] delayed nativeAppBridgeStartLuaAppDM scheduled on "
<< "main thread\n"
<< std::flush;
}
}
if (context->run_update_surface_app) {
env = ensure_env();
std::cout << " [engine] nativeAppBridgeV2UpdateSurfaceAppWithPlatformParams\n"
<< std::flush;
if (context->call_real_update_surface_app) {
context->native_update_surface_app(env, native_gl_class, surface,
platform_params);
std::cout
<< " [engine] nativeAppBridgeV2UpdateSurfaceAppWithPlatformParams returned\n"
<< std::flush;
} else {
std::cout
<< " [engine] nativeAppBridgeV2UpdateSurfaceAppWithPlatformParams bypassed\n"
<< std::flush;
}
}
if (IsEnabled("MOCKTAIL_ASMA_START_TASK_SCHEDULER_FOREGROUND") &&
context->native_set_task_scheduler_background_mode) {
env = ensure_env();
if (!RunTaskSchedulerForegroundOnMainThread(
context->native_set_task_scheduler_background_mode,
native_gl_class)) {
InvokeTaskSchedulerForeground(
env, native_gl_class,
context->native_set_task_scheduler_background_mode, "engine");
}
}
const bool effective_run_start_app_with_params =
HasEnvValue("MOCKTAIL_STEP_START_APP_WITH_PARAMS")
? IsEnabled("MOCKTAIL_STEP_START_APP_WITH_PARAMS")
: context->run_start_app_with_params;
const bool effective_call_real_start_app_with_params =
effective_run_start_app_with_params &&
(HasEnvValue("MOCKTAIL_CALL_REAL_APP_BRIDGE_START")
? IsEnabled("MOCKTAIL_CALL_REAL_APP_BRIDGE_START")
: context->call_real_start_app_with_params);
const bool effective_start_game_with_params =
IsEnabled("MOCKTAIL_START_GAME_WITH_PARAM") &&
context->game_session_runtime != nullptr;
const bool force_inline_start_app_with_params =
effective_start_game_with_params &&
IsEnabled("MOCKTAIL_SYNC_START_APP_WITH_GAME");
if (context->run_start_app_with_params != effective_run_start_app_with_params ||
context->call_real_start_app_with_params !=
effective_call_real_start_app_with_params) {
std::cerr << " [engine] start-app flags drifted: context_run_start_app_with_params="
<< (context->run_start_app_with_params ? 1 : 0)
<< " effective=" << (effective_run_start_app_with_params ? 1 : 0)
<< " context_call_real_start_app_with_params="
<< (context->call_real_start_app_with_params ? 1 : 0)
<< " effective_call_real_start_app_with_params="
<< (effective_call_real_start_app_with_params ? 1 : 0) << '\n'
<< std::flush;
}
if (effective_run_start_app_with_params) {
env = ensure_env();
std::cout << " [engine] nativeAppBridgeV2StartAppWithParams\n"
<< std::flush;
volatile sig_atomic_t start_app_recovered = 0;
if (effective_call_real_start_app_with_params) {
if (force_inline_start_app_with_params) {
std::cout << " [engine] forcing inline StartAppWithParams because "
"StartGameWithParam is enabled\n"
<< std::flush;
}
if (sigsetjmp(g_start_app_with_params_jmp_buf, 1) == 0) {
g_start_app_with_params_recovery_in_progress =
kStage6RecoveryInline;
context->native_start_app_with_params(env, native_gl_class,
start_app_params);
g_start_app_with_params_recovery_in_progress =
kStage6RecoveryInactive;
} else {
start_app_recovered = 1;
g_start_app_with_params_recovery_in_progress =
kStage6RecoveryInactive;
std::cerr <<
" [engine] nativeAppBridgeV2StartAppWithParams recovered\n"
<< std::flush;
}
} else {
MocktailAppBridgeStart(env, app_params);
}
std::cout << " [engine] nativeAppBridgeV2StartAppWithParams returned\n"
<< std::flush;
if (start_app_recovered) {
std::cerr
<< " [engine] startup path cannot continue after start_app recovery\n"
<< std::flush;
pthread_exit(nullptr);
}
}
if (IsEnabled("MOCKTAIL_START_LUA_APP_DM_AFTER_START_APP") &&
context->run_start_lua_app_dm && context->native_start_lua_app_dm) {
int delay_ms = GetEnvInt("MOCKTAIL_START_LUA_APP_DM_DELAY_MS", 0);
if (delay_ms > 0) {
std::cout << " [engine] post-StartApp nativeAppBridgeStartLuaAppDM wait "
<< delay_ms << " ms\n"
<< std::flush;
usleep(static_cast<useconds_t>(delay_ms) * 1000);
}
env = ensure_env();
if (IsEnabled("MOCKTAIL_TRACE_START_LUA_JNI")) {
setenv("MOCKTAIL_JNI_TRACE", "1", 1);
}
std::cout << " [engine] post-StartApp nativeAppBridgeStartLuaAppDM\n"
<< std::flush;
if (sigsetjmp(g_start_lua_app_dm_jmp_buf, 1) == 0) {
g_start_lua_app_dm_recovery_in_progress = kStage6RecoveryInline;
context->native_start_lua_app_dm(env, native_gl_class);
g_start_lua_app_dm_recovery_in_progress = kStage6RecoveryInactive;
} else {
g_start_lua_app_dm_recovery_in_progress = kStage6RecoveryInactive;
std::cerr
<< " [engine] post-StartApp nativeAppBridgeStartLuaAppDM recovered\n"
<< std::flush;
}
std::cout << " [engine] post-StartApp nativeAppBridgeStartLuaAppDM returned\n"
<< std::flush;
}
if (context->game_session_runtime != nullptr) {
env = ensure_env();
mocktail::runtime::GameSessionPrincipal principal;
principal.kind = context->account_identity.user_id > 0
? mocktail::runtime::GameSessionPrincipalKind::
kAuthenticated
: mocktail::runtime::GameSessionPrincipalKind::
kLocalGuest;
principal.generation = 1;
principal.principal_id = context->account_identity.user_id > 0
? std::to_string(
context->account_identity.user_id)
: std::string();
principal.base_url = "https://www.roblox.com/";
std::string launch_parameters =
GetEnvString("MOCKTAIL_GAME_PARAMS_JSON", "{}");
if (launch_parameters.empty()) {
launch_parameters = "{}";
}
mocktail::runtime::GameJoinRequest request{
1, GetEnvLong("MOCKTAIL_PLACE_ID", 0), std::move(launch_parameters)};
const mocktail::window::WindowSurfaceSnapshot window_surface =
mocktail::window::GetWindowSurfaceSnapshot();
if (!window_surface.available) {
std::cerr << " [game-session] initial typed window surface is "
"unavailable\n";
return nullptr;
}
mocktail::runtime::GameSurface game_surface{
window_surface.generation, window_surface.native_window,
window_surface.width, window_surface.height, window_surface.dpi_scale};
mocktail::runtime::RobloxGameSessionBinding binding{
{native_gl_class, surface, platform_params, game_surface_activity,
start_game_params},
principal,
request,
game_surface};
const mocktail::Status status =
context->game_session_runtime->InitializeAndStart(
binding, std::move(principal), std::move(request), game_surface);
if (!status.ok()) {
std::cerr << " [game-session] typed lifecycle startup failed: "
<< status.message() << '\n'
<< std::flush;
return nullptr;
}
std::cout << " [game-session] typed lifecycle startup completed: "
<< mocktail::runtime::GameSessionStateName(
context->game_session_runtime->Snapshot().state)
<< '\n'
<< std::flush;
}
if (context->native_send_app_ready &&
ShouldRunStartupStep("MOCKTAIL_SEND_APP_READY", false)) {
env = ensure_env();
volatile sig_atomic_t send_app_ready_recovered = 0;
std::cout << " [engine] nativeAppBridgeV2SendAppEventOnAppReady\n"
<< std::flush;
if (sigsetjmp(g_send_app_ready_jmp_buf, 1) == 0) {
g_send_app_ready_recovery_in_progress = kStage6RecoveryInline;
jstring empty_ready_arg = env->NewStringUTF("");
jstring home_feature = env->NewStringUTF("Home");
context->native_send_app_ready(env, native_gl_class, empty_ready_arg,
empty_ready_arg, empty_ready_arg,
home_feature);
g_send_app_ready_recovery_in_progress = kStage6RecoveryInactive;
} else {
send_app_ready_recovered = 1;
g_send_app_ready_recovery_in_progress = kStage6RecoveryInactive;
std::cerr
<< " [engine] nativeAppBridgeV2SendAppEventOnAppReady recovered\n"
<< std::flush;
}
if (send_app_ready_recovered == 0) {
std::cout
<< " [engine] nativeAppBridgeV2SendAppEventOnAppReady returned\n"
<< std::flush;
}
}
if (context->native_send_game_loaded &&
ShouldRunStartupStep("MOCKTAIL_SEND_GAME_LOADED", false)) {
env = ensure_env();
volatile sig_atomic_t send_game_loaded_recovered = 0;
std::cout << " [engine] nativeAppBridgeV2SendAppEventOnGameLoaded\n"
<< std::flush;
if (sigsetjmp(g_send_game_loaded_jmp_buf, 1) == 0) {
g_send_game_loaded_recovery_in_progress = kStage6RecoveryInline;
jstring empty_game_loaded_arg = env->NewStringUTF("");
jstring home_feature = env->NewStringUTF("Home");
context->native_send_game_loaded(env, native_gl_class, home_feature,
empty_game_loaded_arg,
empty_game_loaded_arg);
g_send_game_loaded_recovery_in_progress = kStage6RecoveryInactive;
} else {
send_game_loaded_recovered = 1;
g_send_game_loaded_recovery_in_progress = kStage6RecoveryInactive;
std::cerr
<< " [engine] nativeAppBridgeV2SendAppEventOnGameLoaded recovered\n"
<< std::flush;
}
if (send_game_loaded_recovered == 0) {
std::cout
<< " [engine] nativeAppBridgeV2SendAppEventOnGameLoaded returned\n"
<< std::flush;
if (context->native_update_mobile_haptics_state) {
const std::string motors_json =
mocktail::runtime::RobloxHapticProtocolBridge::GetMotorsJson();
jstring motors_str = env->NewStringUTF(motors_json.c_str());
if (motors_str != nullptr) {
context->native_update_mobile_haptics_state(env, native_gl_class,
JNI_TRUE, motors_str);
env->DeleteLocalRef(motors_str);
}
}
}
}
int keepalive_ms = GetEnvInt("MOCKTAIL_KEEPALIVE_MS", 0);
if (keepalive_ms > 0) {
std::cout << " [engine] keepalive: " << keepalive_ms << " ms\n"
<< std::flush;
usleep(static_cast<useconds_t>(keepalive_ms) * 1000);
std::cout << " [engine] keepalive returned\n" << std::flush;
}
if (IsEnabled("MOCKTAIL_KEEPALIVE")) {
std::cout << " [engine] keepalive: forever\n" << std::flush;
while (true) {
sleep(1);
}
}
if (attached_to_thread) {
EngineLog("DetachCurrentThread");
jint detach_result = context->java_vm->DetachCurrentThread();
if (detach_result != JNI_OK) {
std::cerr << " [engine] DetachCurrentThread failed: " << detach_result
<< '\n' << std::flush;
}
attached_to_thread = false;
}
return nullptr;
}
} // namespace
int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
RuntimeDependencies dependencies) {
const bool user_overrode_start_lua_app_dm =
HasEnvValue("MOCKTAIL_START_LUA_APP_DM");
const bool user_overrode_start_lua_step =
HasEnvValue("MOCKTAIL_STEP_START_LUA_APP_DM");
const bool user_overrode_start_app_step =
HasEnvValue("MOCKTAIL_STEP_START_APP_WITH_PARAMS");
const bool user_overrode_call_start_app =
HasEnvValue("MOCKTAIL_CALL_REAL_APP_BRIDGE_START");
const bool user_overrode_call_update_surface =
HasEnvValue("MOCKTAIL_CALL_REAL_APP_BRIDGE_UPDATE_SURFACE");
const bool user_overrode_main_thread_message_pump =
HasEnvValue("MOCKTAIL_MAIN_THREAD_MESSAGE_PUMP");
std::cout << "======================================================\n"
<< " Mocktail Roblox Compatibility Layer\n"
<< "======================================================\n"
<< std::flush;
ApplyRuntimeDefaults();
const mocktail::runtime::ProcessEnvironment process_environment;
const mocktail::runtime::RuntimeConfig runtime_config =
mocktail::runtime::RuntimeConfig::FromEnvironment(process_environment);
mocktail::runtime::DiscordRpcSession discord_rpc(
runtime_config.discord_rpc());
if (runtime_config.discord_rpc().enabled) {
std::string discord_rpc_detail;
if (discord_rpc.Start(&discord_rpc_detail)) {
std::cout << " [discord-rpc] enabled; browsing activity queued\n";
} else {
std::cerr << " [discord-rpc] unavailable: " << discord_rpc_detail
<< "; continuing without Rich Presence\n";
}
}
mocktail::runtime::LuauCodegenMenuSwitch luau_codegen_switch(
[&process_environment]() -> std::uint8_t* {
if (process_environment.GetOr("MOCKTAIL_LUAU_CODEGEN_MENU_SWITCH",
"0") != "1") {
return nullptr;
}
const std::uintptr_t base = g_libroblox_base;
std::uint8_t* flag = mocktail::runtime::FindFastFlagValue(
mocktail::runtime::LoadedImageSegments(base),
mocktail::runtime::kLuauCodegenAllFastFlagName);
if (flag == nullptr) {
std::cerr << " [luau-codegen] DebugLuauCodegenAll not found; "
"automatic menu codegen disabled\n";
} else {
std::cout << " [luau-codegen] menus-only switch at +0x" << std::hex
<< (reinterpret_cast<std::uintptr_t>(flag) - base)
<< std::dec << '\n';
}
return flag;
},
discord_rpc.observer());
const mocktail::runtime::InputCapabilityConfig& input_capabilities =
runtime_config.input_capabilities();
if (!runtime_config.frame_rate().valid()) {
std::cerr << "[FATAL] Invalid MOCKTAIL_FRAME_RATE_LIMIT; expected "
"-1, display, unlimited, or a positive integer\n";
return EXIT_FAILURE;
}
if (!runtime_config.vr_frame_rate().valid()) {
std::cerr << "[FATAL] Invalid MOCKTAIL_VR_FRAME_RATE_LIMIT; expected "
"-1, display, unlimited, or a positive integer\n";
return EXIT_FAILURE;
}
if (!runtime_config.theme_mode_valid()) {
std::cerr
<< "[FATAL] Invalid MOCKTAIL_THEME; expected roblox, system, light, "
"or dark\n";
return EXIT_FAILURE;
}
setenv("MOCKTAIL_TOUCH_ENABLED_INTERNAL",
input_capabilities.touch_enabled ? "1" : "0", 1);
setenv("MOCKTAIL_MOUSE_ENABLED_INTERNAL",
input_capabilities.mouse_enabled ? "1" : "0", 1);
setenv("MOCKTAIL_KEYBOARD_ENABLED_INTERNAL",
input_capabilities.keyboard_enabled ? "1" : "0", 1);
if (runtime_config.has_unsafe_detached_thread_overrides()) {
std::cerr << "[FATAL] Unsupported detached legacy thread overrides:\n";
for (const std::string& name :
runtime_config.unsafe_detached_thread_overrides()) {
std::cerr << " - " << name << '\n';
}
std::cerr << " Supported runtime requires synchronous or owned worker "
"execution.\n";
return EXIT_FAILURE;
}
const runtime::ScopedRobloxCredentialBinding credential_binding(
dependencies.jni_vm().get(), dependencies.roblox_credential());
if (!credential_binding.bound()) {
std::cerr << "[FATAL] Typed Pseudo-JVM credential provider is missing\n";
return EXIT_FAILURE;
}
const std::string library_path =
runtime_config.roblox_library_path().string();
const mocktail::compat::BuildIdResult build_id_result =
mocktail::compat::ReadElfBuildId(library_path);
if (!build_id_result) {
std::cerr << "[FATAL] Cannot identify Roblox library '" << library_path
<< "': " << build_id_result.error << '\n';
return EXIT_FAILURE;
}
const char* manifest_override =
std::getenv("MOCKTAIL_COMPATIBILITY_MANIFEST");
const std::string compatibility_manifest =
manifest_override != nullptr && manifest_override[0] != '\0'
? manifest_override
: MOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST;
const mocktail::compat::ProfileLookupResult profile_result =
mocktail::compat::FindBuildProfile(compatibility_manifest,
build_id_result.build_id);
if (!profile_result) {
std::cerr << "[FATAL] Cannot read Roblox compatibility profile: "
<< profile_result.error << '\n';
return EXIT_FAILURE;
}
if (!profile_result.profile.has_value()) {
std::cerr << "[FATAL] Unsupported Roblox Build ID "
<< build_id_result.build_id << ".\n"
<< " Add and validate a profile in " << compatibility_manifest
<< " before starting native code.\n";
return EXIT_FAILURE;
}
const mocktail::compat::BuildProfile& build_profile = *profile_result.profile;
if (IsEnabled("MOCKTAIL_VR_ENABLED") &&
mocktail::vr::FindRobloxVrDeviceProfile(build_profile.elf_build_id) ==
nullptr) {
std::cerr << "[FATAL] Experimental Roblox VR has no verified profile for "
<< build_profile.elf_build_id << "\n";
return EXIT_FAILURE;
}
const mocktail::compat::HostAbiProfile* host_abi_profile =
mocktail::compat::FindHostAbiProfile(build_profile.elf_build_id);
const bool experiment_allowed =
build_profile.default_allowed || options.allow_unverified_build;
g_allow_legacy_binary_patches.store(
build_profile.allow_legacy_binary_patches, std::memory_order_release);
const bool allow_host_abi_bridges =
build_profile.allow_host_abi_bridges && experiment_allowed &&
host_abi_profile != nullptr &&
host_abi_profile->bridge_entry_count > 0;
const bool allow_host_constructor_replay =
build_profile.allow_host_constructor_replay &&
allow_host_abi_bridges && host_abi_profile->init_array_offset != 0 &&
host_abi_profile->HasValidConstructorRanges();
g_allow_host_abi_bridges.store(allow_host_abi_bridges,
std::memory_order_release);
g_allow_host_constructor_replay.store(allow_host_constructor_replay,
std::memory_order_release);
g_active_host_abi_profile.store(host_abi_profile,
std::memory_order_release);
g_host_abi_install_attempted = false;
g_host_abi_install_result = {};
SetEnvDefault("MOCKTAIL_ROBLOX_VERSION",
build_profile.version_name.c_str());
const std::string roblox_version_code =
std::to_string(build_profile.version_code);
SetEnvDefault("MOCKTAIL_ROBLOX_VERSION_CODE",
roblox_version_code.c_str());
const std::string default_user_agent =
"Roblox/" + build_profile.version_name +
" (Linux; Android 33; Mocktail)";
SetEnvDefault("MOCKTAIL_USER_AGENT", default_user_agent.c_str());
mocktail::compat::SetLegacyBionicDiagnosticsEnabled(
build_profile.allow_legacy_binary_patches);
std::cout << " [compat] Roblox " << build_profile.version_name
<< " Build ID " << build_profile.elf_build_id << " ("
<< mocktail::compat::BuildStatusName(build_profile.status) << ")\n";
std::cout << " [compat] legacy binary patches: "
<< (build_profile.allow_legacy_binary_patches ? "enabled"
: "disabled")
<< '\n';
std::cout << " [compat] Build-ID host ABI profile: "
<< (allow_host_abi_bridges ? "allowed" : "denied")
<< '\n';
std::cout << " [compat] Build-ID constructor replay: "
<< (allow_host_constructor_replay ? "allowed" : "denied")
<< '\n'
<< std::flush;
if (!build_profile.default_allowed && !options.allow_unverified_build) {
std::cerr << "[FATAL] This Roblox build is not enabled for normal runs: "
<< build_profile.reason << '\n'
<< " Use --allow-unverified-build only for an explicit "
"compatibility check.\n";
return EXIT_FAILURE;
}
ApplyAuthStartupDefaults(!dependencies.roblox_credential().empty(),
user_overrode_start_lua_app_dm,
user_overrode_start_lua_step,
user_overrode_start_app_step,
user_overrode_call_start_app);
EnableFullTraceIfRequested();
if (TraceAllEnabled()) {
std::cout << " [trace] full tracing enabled\n" << std::flush;
}
// Roblox internal threads trigger SI_KERNEL traps from CET shadow-stack
// return mismatches. Unsupported kernels ignore this request.
#if defined(__x86_64__)
{
long r = syscall(SYS_arch_prctl, ARCH_SHSTK_DISABLE, ARCH_SHSTK_SHSTK);
if (r == 0) {
std::cout << " [cet] shadow-stack (SHSTK) disabled\n";
}
}
#endif
if (build_profile.allow_legacy_binary_patches) {
std::cerr
<< "[FATAL] Legacy binary patches and signal recovery were removed.\n"
<< " Build ID " << build_profile.elf_build_id
<< " is not supported on this runtime.\n";
return EXIT_FAILURE;
}
std::cout << " [compat] signal-recovery handler disabled for this Build "
"ID\n";
const bool is_headless = runtime_config.headless();
if (!HasEnvValue("MOCKTAIL_APP_BRIDGE_HEADLESS_INIT_PARAMS")) {
setenv("MOCKTAIL_APP_BRIDGE_HEADLESS_INIT_PARAMS", is_headless ? "1" : "0",
1);
}
PrintStage(1, "Initialising Bionic linker + Pseudo-JVM");
#ifdef MOCKTAIL_USE_BIONIC_LINKER
::linker::init();
std::cout << " mcpelauncher Bionic linker initialised.\n";
#endif
auto jni_vm = dependencies.jni_vm();
if (jni_vm == nullptr) {
std::cerr << "[FATAL] Typed Pseudo-JVM dependency is missing\n";
return EXIT_FAILURE;
}
std::cout << " Pseudo-JVM instance created successfully.\n";
// FindClass needs these descriptors before JNI_OnLoad.
PrintStage(2, "Registering Android SDK JNI classes");
auto context_class = jni_vm->RegisterClass("android/content/Context");
auto activity_class =
jni_vm->RegisterClass("com/roblox/client/RobloxActivity");
auto settings_class = jni_vm->RegisterClass("rbx/JNIRobloxSettings");
settings_class->RegisterMethod(
"nativeInitClientSettings", "()V",
[](JNIEnv* /*env*/, jobject /*obj*/) {
std::cout << " [JNI callback] nativeInitClientSettings invoked\n";
});
auto native_gl_interface_class =
jni_vm->RegisterClass("com/roblox/engine/jni/NativeGLInterface");
g_native_gl_class_for_main_thread =
reinterpret_cast<jclass>(native_gl_interface_class.get());
g_vm_for_main_thread_pump = jni_vm.get();
jni_vm->RegisterClass("com/roblox/engine/jni/NativeInputInterface");
jni_vm->RegisterClass("com/roblox/engine/jni/NativeSettingsInterface");
jni_vm->RegisterClass("com/roblox/engine/jni/NativeAppBridgeInterface");
jni_vm->RegisterClass("com/roblox/engine/jni/NativeGLJavaInterface");
jni_vm->RegisterClass("com/roblox/engine/jni/EngineJavaCallback2");
jni_vm->RegisterClass("com/roblox/engine/jni/OnAppBridgeNotificationListener");
jni_vm->RegisterClass("com/roblox/client/flags/FlagJniInterface");
jni_vm->RegisterClass("com/roblox/client/flags/NativeFlagsInitResult");
jni_vm->RegisterClass("com/roblox/engine/jni/model/DeviceStaticParams");
jni_vm->RegisterClass("com/roblox/engine/jni/model/DeviceParams");
jni_vm->RegisterClass("com/roblox/engine/jni/model/PlatformParams");
jni_vm->RegisterClass("com/roblox/engine/jni/model/NativeTextBoxInfo");
jni_vm->RegisterClass("com/roblox/engine/jni/autovalue/InitParams");
jni_vm->RegisterClass("com/roblox/engine/jni/autovalue/StartAppParams");
jni_vm->RegisterClass("com/roblox/client/startup/MainGameActivity");
jni_vm->RegisterClass("com/roblox/client/startup/NativeHelper");
jni_vm->RegisterClass(
"com/roblox/universalapp/activitylifecyclecallbacks/"
"JNIActivityLifecycleCallbacks");
jni_vm->RegisterClass("com/roblox/universalapp/messagebus/MessageBus");
jni_vm->RegisterClass("com/roblox/universalapp/messagebus/Connection");
jni_vm->RegisterClass(
"com/roblox/universalapp/systemtheme/SystemThemeProtocol");
jni_vm->RegisterClass("com/roblox/universalapp/cookie/JNICookieManager");
jni_vm->RegisterClass("com/roblox/universalapp/cookie/JNICookieProtocol");
jni_vm->RegisterClass(
"com/roblox/universalapp/cookie/JNICookieProtocol$OnSetCookieHandler");
jni_vm->RegisterClass("com/roblox/client/JNIAAssetManagerSetup");
jni_vm->RegisterClass("org/fmod/FMOD");
jni_vm->RegisterClass("android/content/res/AssetManager");
jni_vm->RegisterClass("android/media/AudioManager");
jni_vm->RegisterClass(
"com/roblox/protocols/systemdialog/PlatformSystemDialogHandler");
jni_vm->RegisterClass(
"com/roblox/protocols/systemdialogplatforminterface/generated/"
"SystemDialogRequest");
jni_vm->RegisterClass(
"com/roblox/protocols/systemdialogplatforminterface/generated/"
"ISystemDialogCallback");
jni_vm->RegisterClass("com/google/androidgamesdk/GameActivity");
jni_vm->RegisterClass("com/google/androidgamesdk/gametextinput/InputConnection");
jni_vm->RegisterClass("com/google/androidgamesdk/gametextinput/State");
jni_vm->RegisterClass("com/roblox/client/LocalStorageManager");
jni_vm->RegisterClass(
"com/roblox/protocols/localstorageplatforminterface/generated/"
"ILocalStorageHandlerCore");
jni_vm->RegisterClass(
"com/roblox/protocols/localstorageplatforminterface/generated/"
"ILocalStorageHandlerCore$CppProxy");
jni_vm->RegisterClass(
"com/roblox/protocols/localstorageplatforminterface/generated/"
"IPlatformLocalStorageHandler");
jni_vm->RegisterClass(
"com/roblox/protocols/localstorageplatforminterface/generated/"
"IPlatformLocalStorageHandler$CppProxy");
jni_vm->RegisterClass("java/util/HashSet");
jni_vm->RegisterClass("java/util/Iterator");
jni_vm->RegisterClass("android/os/LocaleList");
jni_vm->RegisterClass("java/util/Locale");
jni_vm->RegisterClass("androidx/core/graphics/Insets");
jni_vm->RegisterClass("androidx/core/view/WindowInsetsCompat$Type");
jni_vm->RegisterClass("android/view/MotionEvent");
jni_vm->RegisterClass("android/view/KeyEvent");
jni_vm->RegisterClass("android/view/Surface");
jni_vm->RegisterClass("java/lang/String");
jni_vm->RegisterClass("java/lang/Object");
std::cout << " Registered " << jni_vm->GetClassCount()
<< " JNI class(es)\n";
// Create SDL/EGL before loading Android stubs so SDL binds the host graphics
// backend. The window stays hidden until the first real frame.
bool window_initialised = false;
if (!is_headless) {
const int win_w = runtime_config.window().width;
const int win_h = runtime_config.window().height;
const char* win_title = runtime_config.window().title.c_str();
std::cout << " [window] Creating " << win_w << "x" << win_h
<< " window...\n"
<< std::flush;
window_initialised = mocktail::window::Init(win_w, win_h, win_title);
if (!window_initialised) {
if (IsEnabled("MOCKTAIL_SOBER_MODE")) {
std::cerr << " [window] FATAL: Sober-style startup requires a "
<< "working SDL video device. Check DISPLAY/WAYLAND_DISPLAY "
<< "and SDL3.\n"
<< std::flush;
return EXIT_FAILURE;
}
std::cerr
<< " [window] WARNING: window init failed, continuing headless\n"
<< std::flush;
} else {
std::cout << " [window] Window ready; waiting for Roblox frames\n"
<< std::flush;
}
}
if (window_initialised) {
auto* sdl_window =
static_cast<SDL_Window*>(mocktail::window::GetBackendWindow());
const SDL_DisplayID display =
sdl_window != nullptr ? SDL_GetDisplayForWindow(sdl_window) : 0;
const SDL_DisplayMode* mode =
display != 0 ? SDL_GetCurrentDisplayMode(display) : nullptr;
if (mode != nullptr && mode->w > 0 && mode->h > 0) {
// SDL display modes are in desktop units; pixel_density maps them to
// physical pixels.
const float density = mode->pixel_density > 0.0f ? mode->pixel_density
: 1.0f;
const std::string display_size =
std::to_string(std::lround(mode->w * density)) + "x" +
std::to_string(std::lround(mode->h * density));
setenv(mocktail::runtime::kDisplaySizeEnvironment, display_size.c_str(),
1);
std::cout << " [window] host display " << display_size << " pixels\n"
<< std::flush;
}
}
if (window_initialised) {
const mocktail::window::WindowViewportSnapshot viewport =
mocktail::window::GetWindowViewportSnapshot();
if (viewport.valid()) {
const std::string window_size = std::to_string(viewport.pixel_width) +
"x" +
std::to_string(viewport.pixel_height);
setenv(mocktail::runtime::kWindowSizeEnvironment, window_size.c_str(),
1);
}
}
const SDL_SystemTheme system_theme =
window_initialised ? SDL_GetSystemTheme() : SDL_SYSTEM_THEME_UNKNOWN;
const bool system_dark_theme = system_theme == SDL_SYSTEM_THEME_DARK;
const char* app_storage_file =
std::getenv("MOCKTAIL_APP_STORAGE_FILE_INTERNAL");
bool dark_theme = runtime_config.theme_mode() != "light";
bool roblox_theme_found = false;
if (runtime_config.theme_mode() == "system") {
dark_theme = system_dark_theme;
} else if (runtime_config.theme_mode() == "roblox" &&
app_storage_file != nullptr) {
const mocktail::runtime::RobloxThemeCacheResult saved_theme =
mocktail::runtime::ReadRobloxThemeCache(
app_storage_file, dependencies.account_identity().user_id);
if (!saved_theme) {
std::cerr << " [theme] Cannot read Roblox theme cache; using default: "
<< saved_theme.error << '\n';
}
if (saved_theme.dark_theme.has_value()) {
dark_theme = *saved_theme.dark_theme;
roblox_theme_found = true;
}
}
setenv("MOCKTAIL_RESOLVED_THEME_INTERNAL", dark_theme ? "Dark" : "Light", 1);
if (runtime_config.theme_mode() == "roblox") {
std::cout << " [theme] "
<< (roblox_theme_found ? "Roblox saved theme="
: "Roblox theme unavailable; default=")
<< (dark_theme ? "dark" : "light") << '\n'
<< std::flush;
} else if (app_storage_file != nullptr) {
std::string theme_error;
if (!mocktail::runtime::ApplyRobloxThemeCacheOverride(
app_storage_file, dependencies.account_identity().user_id,
dark_theme, &theme_error)) {
std::cerr << " [theme] Cannot persist Roblox theme override: "
<< theme_error << '\n';
}
std::cout << " [theme] Roblox local theme="
<< (dark_theme ? "dark" : "light") << '\n'
<< std::flush;
}
const bool has_window = window_initialised;
if (!has_window) {
std::cout << " [window] GUI-bound startup steps disabled (no SDL window)\n"
<< std::flush;
} else {
if (!HasEnvValue("MOCKTAIL_APP_LIFECYCLE_ACTIVE") &&
!HasEnvValue("MOCKTAIL_STEP_APP_LIFECYCLE_ACTIVE")) {
setenv("MOCKTAIL_APP_LIFECYCLE_ACTIVE", "1", 1);
std::cout << " [window] windowed startup: auto-enabled "
<< "AppLifecycleNativeAdapter.setActive\n"
<< std::flush;
}
if (!user_overrode_main_thread_message_pump) {
setenv("MOCKTAIL_MAIN_THREAD_MESSAGE_PUMP", "1", 1);
std::cout << " [window] windowed startup: auto-enabled main-thread "
<< "message pump\n"
<< std::flush;
}
if (!user_overrode_call_update_surface) {
std::cout << " [window] windowed startup: leaving real UpdateSurfaceAppWithPlatformParams opt-in\n"
<< std::flush;
}
if (!user_overrode_call_start_app &&
IsEnabled("MOCKTAIL_STEP_START_APP_WITH_PARAMS")) {
setenv("MOCKTAIL_CALL_REAL_APP_BRIDGE_START", "1", 1);
std::cout << " [window] windowed startup: auto-enabled real "
<< "StartAppWithParams when auth allows it\n"
<< std::flush;
}
}
PrintStage(3, "Building Bionic symbol table from stubs");
libc_shim::Install();
// Android unwinders must see mcpelauncher-linker's soinfo list. Stage the
// exact per-SONAME exports before any synthetic library snapshots the legacy
// map, and keep the global compatibility map consistent for unresolved ELF
// imports discovered below.
void* bionic_dl_iterate_phdr = linker::BionicDlIteratePhdrAddress();
linker::RegisterSyntheticSymbol("libc.so", "dl_iterate_phdr",
bionic_dl_iterate_phdr);
linker::RegisterSyntheticSymbol("libdl.so", "dl_iterate_phdr",
bionic_dl_iterate_phdr);
linker::RegisterSymbol("dl_iterate_phdr", bionic_dl_iterate_phdr);
// SDL owns the host window and surface. Android libroblox must instead see
// Mocktail's libEGL adapter, which reuses that surface. Do not resolve this
// by SONAME: Chromium ANGLE also calls itself libEGL.so and would otherwise
// win once the SDL backend has loaded it.
mocktail::graphics::BionicEglBridge bionic_egl_bridge;
if (has_window) {
if (!bionic_egl_bridge.Load()) {
std::cerr << " [graphics] exact Bionic EGL adapter unavailable: "
<< bionic_egl_bridge.error() << '\n';
if (IsEnabled("MOCKTAIL_REQUIRE_REAL_GRAPHICS")) {
std::cerr << "[FATAL] Windowed mode requires Mocktail's exact "
"Bionic EGL adapter.\n";
return EXIT_FAILURE;
}
} else {
for (const auto& [name, address] : bionic_egl_bridge.exports()) {
linker::RegisterSyntheticSymbol("libEGL.so", name, address);
linker::RegisterSymbol(name, address);
}
std::cout << " [graphics] Bionic libEGL adapter loaded from "
<< bionic_egl_bridge.library_path() << " ("
<< bionic_egl_bridge.exports().size() << " exports)\n";
}
}
std::vector<const char*> stub_names = {
"libc.so", "libdl.so", "libm.so", "libz.so",
"libandroid.so", "liblog.so", "libmediandk.so",
"libOpenSLES.so", "libOpenMAXAL.so",
"libEGL.so", "libGLESv2.so",
};
if (IsEnabled("MOCKTAIL_PRELOAD_VULKAN_SHIM")) {
stub_names.push_back("libvulkan.so");
}
std::vector<void*> stub_handles;
void* bionic_vulkan_adapter_handle = nullptr;
for (const char* name : stub_names) {
void* h = nullptr;
bool exact_adapter = false;
if (std::strcmp(name, "libEGL.so") == 0 &&
bionic_egl_bridge.IsLoaded()) {
h = bionic_egl_bridge.handle();
exact_adapter = true;
} else {
const std::string adapter = RuntimeBionicAdapterPath(name);
if (!adapter.empty()) {
h = ::dlopen(adapter.c_str(), RTLD_LAZY | RTLD_GLOBAL);
exact_adapter = h != nullptr;
}
}
if (h == nullptr) {
h = ::dlopen(name, RTLD_LAZY | RTLD_GLOBAL | RTLD_NOLOAD);
if (!h) h = ::dlopen(name, RTLD_LAZY | RTLD_GLOBAL);
}
if (h) {
std::cout << " [stubs] Preloaded " << name;
if (exact_adapter) {
std::cout << " via exact Mocktail adapter";
}
std::cout << '\n';
stub_handles.push_back(h);
if (std::strcmp(name, "libvulkan.so") == 0) {
bionic_vulkan_adapter_handle = h;
}
} else {
std::cerr << " [warn] Could not preload " << name
<< ": " << ::dlerror() << '\n';
}
}
void* real_gles_handle = nullptr;
if (has_window && !IsEnabled("MOCKTAIL_GLES_FORCE_STUB") &&
!IsEnabled("MOCKTAIL_GLES_NOOP_DRAW_CALLS")) {
real_gles_handle = OpenRealGlesLibrary();
}
// Prefer Bionic shims over glibc for guest layouts such as pthread and FILE.
static const char* kSymbolsToRegister[] = {
// Symbols exported by the Android compatibility stubs.
"AAsset_close",
"AAsset_getBuffer",
"AAsset_getLength",
"AAsset_getLength64",
"AAsset_getRemainingLength",
"AAsset_getRemainingLength64",
"AAssetManager_fromJava",
"AAssetManager_open",
"AAsset_openFileDescriptor",
"AAsset_openFileDescriptor64",
"AAsset_read",
"AAsset_seek",
"AAsset_seek64",
"AConfiguration_delete",
"AConfiguration_fromAssetManager",
"AConfiguration_getCountry",
"AConfiguration_getLanguage",
"AConfiguration_getNavHidden",
"AConfiguration_getScreenHeightDp",
"AConfiguration_getScreenSize",
"AConfiguration_getScreenWidthDp",
"AConfiguration_new",
"alCreateEngine",
"ALooper_acquire",
"ALooper_addFd",
"ALooper_forThread",
"ALooper_pollOnce",
"ALooper_prepare",
"ALooper_release",
"ALooper_removeFd",
"AMediaCodec_configure",
"AMediaCodec_createDecoderByType",
"AMediaCodec_createEncoderByType",
"AMediaCodec_delete",
"AMediaCodec_dequeueInputBuffer",
"AMediaCodec_dequeueOutputBuffer",
"AMediaCodec_flush",
"AMediaCodec_getInputBuffer",
"AMediaCodec_getOutputBuffer",
"AMediaCodec_getOutputFormat",
"AMediaCodec_queueInputBuffer",
"AMediaCodec_releaseOutputBuffer",
"AMediaCodec_start",
"AMediaCodec_stop",
"AMediaFormat_delete",
"AMediaFormat_getBuffer",
"AMediaFormat_getInt32",
"AMEDIAFORMAT_KEY_BIT_RATE",
"AMEDIAFORMAT_KEY_CHANNEL_COUNT",
"AMEDIAFORMAT_KEY_COLOR_FORMAT",
"AMEDIAFORMAT_KEY_FRAME_RATE",
"AMEDIAFORMAT_KEY_HEIGHT",
"AMEDIAFORMAT_KEY_I_FRAME_INTERVAL",
"AMEDIAFORMAT_KEY_MIME",
"AMEDIAFORMAT_KEY_SAMPLE_RATE",
"AMEDIAFORMAT_KEY_STRIDE",
"AMEDIAFORMAT_KEY_WIDTH",
"AMediaFormat_new",
"AMediaFormat_setBuffer",
"AMediaFormat_setFloat",
"AMediaFormat_setInt32",
"AMediaFormat_setString",
"AMediaFormat_toString",
"ANativeWindow_acquire",
"ANativeWindow_fromSurface",
"ANativeWindow_getHeight",
"ANativeWindow_getWidth",
"ANativeWindow_release",
"eglBindAPI",
"eglChooseConfig",
"eglCreateContext",
"eglCreatePbufferSurface",
"eglCreateWindowSurface",
"eglDestroyContext",
"eglDestroySurface",
"eglGetConfigAttrib",
"eglGetCurrentContext",
"eglGetDisplay",
"eglGetError",
"eglGetProcAddress",
"eglInitialize",
"eglMakeCurrent",
"eglQueryString",
"eglQuerySurface",
"eglSwapBuffers",
"eglSwapInterval",
"eglTerminate",
"glActiveTexture",
"glAttachShader",
"glBindAttribLocation",
"glBindBuffer",
"glBindFramebuffer",
"glBindRenderbuffer",
"glBindTexture",
"glBlendFunc",
"glBlendFuncSeparate",
"glBufferData",
"glBufferSubData",
"glCheckFramebufferStatus",
"glClear",
"glClearColor",
"glClearDepthf",
"glClearStencil",
"glColorMask",
"glCompileShader",
"glCompressedTexImage2D",
"glCompressedTexSubImage2D",
"glCopyTexSubImage2D",
"glCreateProgram",
"glCreateShader",
"glCullFace",
"glDeleteBuffers",
"glDeleteFramebuffers",
"glDeleteProgram",
"glDeleteRenderbuffers",
"glDeleteShader",
"glDeleteTextures",
"glDepthFunc",
"glDepthMask",
"glDisable",
"glDisableVertexAttribArray",
"glDrawArrays",
"glDrawElements",
"glEnable",
"glEnableVertexAttribArray",
"glFramebufferRenderbuffer",
"glFramebufferTexture2D",
"glGenBuffers",
"glGenFramebuffers",
"glGenRenderbuffers",
"glGenTextures",
"glGenerateMipmap",
"glGetActiveUniform",
"glGetError",
"glGetIntegerv",
"glGetProgramInfoLog",
"glGetProgramiv",
"glGetShaderInfoLog",
"glGetShaderiv",
"glGetString",
"glGetUniformLocation",
"glLinkProgram",
"glPixelStorei",
"glPolygonOffset",
"glReadPixels",
"glReleaseShaderCompiler",
"glRenderbufferStorage",
"glScissor",
"glShaderSource",
"glStencilFunc",
"glStencilMask",
"glStencilOp",
"glTexImage2D",
"glTexParameterf",
"glTexParameterfv",
"glTexParameteri",
"glTexSubImage2D",
"glUniform1i",
"glUseProgram",
"glVertexAttribPointer",
"glViewport",
"vkCreateAndroidSurfaceKHR",
"vkCreateInstance",
"vkDestroySurfaceKHR",
"vkEnumerateInstanceExtensionProperties",
"vkEnumerateInstanceLayerProperties",
"vkGetDeviceProcAddr",
"vkGetInstanceProcAddr",
"__android_log_assert",
"__android_log_buf_write",
"__android_log_print",
"__android_log_write",
"__assert",
"__assert2",
"__ctype_get_mb_cur_max",
"__errno",
"__FD_CLR_chk",
"__FD_ISSET_chk",
"__FD_SET_chk",
"fread",
"__fread_chk",
"fwrite",
"__fwrite_chk",
"fflush",
"open",
"fopen",
"access",
"stat",
"lstat",
"statvfs",
"statfs",
"mkdir",
"opendir",
"rename",
"unlink",
"rmdir",
"realpath",
"readlink",
"__gnu_strerror_r",
"__open_2",
"__poll_chk",
"pthread_attr_destroy",
"pthread_attr_getstack",
"pthread_attr_init",
"pthread_attr_setdetachstate",
"pthread_attr_setschedparam",
"pthread_attr_setstacksize",
"pthread_condattr_destroy",
"pthread_condattr_init",
"pthread_condattr_setclock",
"pthread_cond_broadcast",
"pthread_cond_destroy",
"pthread_cond_init",
"pthread_cond_signal",
"pthread_cond_timedwait",
"pthread_cond_wait",
"pthread_create",
"pthread_getattr_np",
"pthread_mutexattr_destroy",
"pthread_mutexattr_init",
"pthread_mutexattr_settype",
"pthread_mutex_destroy",
"pthread_mutex_init",
"pthread_mutex_lock",
"pthread_mutex_trylock",
"pthread_mutex_unlock",
"pthread_rwlock_destroy",
"pthread_rwlock_init",
"pthread_rwlock_rdlock",
"pthread_rwlock_unlock",
"pthread_rwlock_wrlock",
"__read_chk",
"__readlink_chk",
"__sendto_chk",
"__sF",
// stdio stream pointers — Bionic clients access these as FILE* globals
"stdin",
"stdout",
"stderr",
"slCreateEngine",
"SL_IID_ANDROIDCONFIGURATION",
"SL_IID_ANDROIDSIMPLEBUFFERQUEUE",
"SL_IID_BUFFERQUEUE",
"SL_IID_ENGINE",
"SL_IID_PLAY",
"SL_IID_RECORD",
"SL_IID_VOLUME",
"mocktail_recover_stack_chk_fail",
"__stack_chk_fail",
"__stack_chk_guard",
"__strchr_chk",
"__strlen_chk",
"__strncpy_chk2",
"sysconf",
"__system_property_get",
"__write_chk",
// pthread functions whose host ABI is already compatible.
"pthread_once",
"pthread_self",
"pthread_equal",
"pthread_join",
"pthread_detach",
"pthread_kill",
"pthread_exit",
"pthread_getschedparam",
"pthread_key_create",
"pthread_key_delete",
"pthread_getspecific",
"pthread_setspecific",
"pthread_sigmask",
"pthread_setname_np",
// Standard C library functions resolved from host libc.
"strcmp",
"strncmp",
"strcpy",
"strncpy",
"strlen",
"strcat",
"strncat",
"strchr",
"strrchr",
"strstr",
"strtol",
"strtoul",
"strtod",
"strtof",
"atoi",
"atof",
"atol",
"memcpy",
"memmove",
"memset",
"memcmp",
"memchr",
"malloc",
"calloc",
"realloc",
"free",
"abort",
"exit",
"getenv",
"setenv",
"putenv",
"sprintf",
"snprintf",
"sscanf",
"printf",
"fprintf",
"vprintf",
"vfprintf",
"vsprintf",
"vsnprintf",
"fclose",
"feof",
"ferror",
"fgets",
"fgetc",
"fputc",
"fputs",
"fseek",
"ftell",
"rewind",
"close",
"read",
"write",
"lseek",
"lseek64",
"pread",
"pwrite",
"pread64",
"pwrite64",
"getcwd",
"chdir",
"dup",
"dup2",
"pipe",
"socket",
"connect",
"bind",
"listen",
"accept",
"setsockopt",
"getsockopt",
"send",
"recv",
"sendto",
"recvfrom",
"getaddrinfo",
"freeaddrinfo",
"getnameinfo",
"inet_ntop",
"inet_pton",
"htons",
"htonl",
"ntohs",
"ntohl",
"clock_gettime",
"clock_getres",
"gettimeofday",
"nanosleep",
"usleep",
"sleep",
"time",
"localtime",
"gmtime",
"mktime",
"strftime",
"mmap",
"munmap",
"mprotect",
"msync",
"mlock",
"munlock",
"madvise",
"sigaction",
"signal",
"raise",
"kill",
"getpid",
"getuid",
"geteuid",
"getgid",
"getegid",
"waitpid",
"fork",
"execve",
"dlopen",
"dlclose",
"dlsym",
"dlerror",
"prctl",
"ioctl",
"fcntl",
"isatty",
"isalpha",
"isdigit",
"isspace",
"isupper",
"islower",
"toupper",
"tolower",
"rand",
"srand",
"rand_r",
"qsort",
"bsearch",
"abs",
"labs",
"llabs",
"ceil",
"floor",
"round",
"fabs",
"pow",
"sqrt",
"log",
"log2",
"log10",
"exp",
"sin",
"cos",
"tan",
"asin",
"acos",
"atan",
"atan2",
"strerror",
"perror",
"readdir",
"closedir",
"pthread_barrier_init",
"pthread_barrier_destroy",
"pthread_barrier_wait",
"pthread_spin_init",
"pthread_spin_destroy",
"pthread_spin_lock",
"pthread_spin_trylock",
"pthread_spin_unlock",
};
int registered = 0;
int total_symbols = 0;
int gl_symbol_count = 0;
int gl_from_window = 0;
int gl_from_real_gles = 0;
int gl_from_stub = 0;
int gl_from_host = 0;
int gl_unresolved = 0;
for (const char* sym : kSymbolsToRegister) {
++total_symbols;
auto result = ResolveSymbolForBionic(sym, has_window, real_gles_handle,
stub_handles);
void* addr = result.address;
const bool is_gl_symbol = IsGlSymbol(sym);
if (addr == nullptr && is_gl_symbol) {
++gl_unresolved;
}
if (is_gl_symbol) {
++gl_symbol_count;
switch (result.source) {
case SymbolResolveSource::kWindow:
++gl_from_window;
break;
case SymbolResolveSource::kRealGles:
++gl_from_real_gles;
break;
case SymbolResolveSource::kStub:
++gl_from_stub;
break;
case SymbolResolveSource::kHost:
++gl_from_host;
break;
case SymbolResolveSource::kMissing:
default:
break;
}
}
if (addr) {
linker::RegisterSymbol(sym, addr);
++registered;
}
}
if (bionic_vulkan_adapter_handle != nullptr) {
static constexpr const char* kVulkanAdapterExports[] = {
"vkCreateAndroidSurfaceKHR",
"vkCreateDevice",
"vkCreateInstance",
"vkCreateSwapchainKHR",
"vkDestroyDevice",
"vkDestroyInstance",
"vkDestroySurfaceKHR",
"vkDestroySwapchainKHR",
"vkEnumerateInstanceExtensionProperties",
"vkEnumerateInstanceLayerProperties",
"vkGetDeviceProcAddr",
"vkGetDeviceQueue",
"vkGetDeviceQueue2",
"vkGetInstanceProcAddr",
"vkQueuePresentKHR",
};
size_t vulkan_exports = 0;
for (const char* name : kVulkanAdapterExports) {
void* address = ::dlsym(bionic_vulkan_adapter_handle, name);
if (address == nullptr ||
!StubOwnsSymbolAddress(bionic_vulkan_adapter_handle, address)) {
continue;
}
linker::RegisterSyntheticSymbol("libvulkan.so", name, address);
linker::RegisterSyntheticSymbol("libvulkan.so.1", name, address);
linker::RegisterSymbol(name, address);
++vulkan_exports;
}
std::cout << " [vulkan] registered exact Android loader adapter exports: "
<< vulkan_exports << '\n';
}
RegisterBionicDnsWrappers();
RegisterBionicPathWrappers();
linker::RegisterSymbol("prctl",
reinterpret_cast<void*>(mocktail_bionic_prctl));
linker::RegisterSymbol(
"setsockopt", reinterpret_cast<void*>(mocktail_bionic_setsockopt));
linker::RegisterSymbol("sendmsg",
reinterpret_cast<void*>(mocktail_bionic_sendmsg));
linker::RegisterSymbol("mprotect", reinterpret_cast<void*>(mocktail_mprotect));
linker::RegisterSymbol("pthread_condattr_init",
reinterpret_cast<void*>(mocktail_pthread_condattr_init));
linker::RegisterSymbol("pthread_condattr_destroy",
reinterpret_cast<void*>(mocktail_pthread_condattr_destroy));
linker::RegisterSymbol("pthread_condattr_setclock",
reinterpret_cast<void*>(mocktail_pthread_condattr_setclock));
linker::RegisterSymbol("pthread_cond_init",
reinterpret_cast<void*>(mocktail_pthread_cond_init));
linker::RegisterSymbol("pthread_cond_destroy",
reinterpret_cast<void*>(mocktail_pthread_cond_destroy));
linker::RegisterSymbol("pthread_cond_signal",
reinterpret_cast<void*>(mocktail_pthread_cond_signal));
linker::RegisterSymbol("pthread_cond_broadcast",
reinterpret_cast<void*>(mocktail_pthread_cond_broadcast));
linker::RegisterSymbol("pthread_cond_wait",
reinterpret_cast<void*>(mocktail_pthread_cond_wait));
linker::RegisterSymbol("pthread_cond_timedwait",
reinterpret_cast<void*>(mocktail_pthread_cond_timedwait));
linker::RegisterSymbol("pthread_mutexattr_init",
reinterpret_cast<void*>(mocktail_pthread_mutexattr_init));
linker::RegisterSymbol("pthread_mutexattr_destroy",
reinterpret_cast<void*>(mocktail_pthread_mutexattr_destroy));
linker::RegisterSymbol("pthread_mutexattr_settype",
reinterpret_cast<void*>(mocktail_pthread_mutexattr_settype));
linker::RegisterSymbol("pthread_mutex_init",
reinterpret_cast<void*>(mocktail_pthread_mutex_init));
linker::RegisterSymbol("pthread_mutex_destroy",
reinterpret_cast<void*>(mocktail_pthread_mutex_destroy));
linker::RegisterSymbol("pthread_mutex_lock",
reinterpret_cast<void*>(mocktail_pthread_mutex_lock));
linker::RegisterSymbol("pthread_mutex_trylock",
reinterpret_cast<void*>(mocktail_pthread_mutex_trylock));
linker::RegisterSymbol("pthread_mutex_unlock",
reinterpret_cast<void*>(mocktail_pthread_mutex_unlock));
linker::RegisterSymbol("pthread_once",
reinterpret_cast<void*>(mocktail_pthread_once));
linker::RegisterSymbol("pthread_spin_init",
reinterpret_cast<void*>(mocktail_pthread_spin_init));
linker::RegisterSymbol("pthread_spin_destroy",
reinterpret_cast<void*>(mocktail_pthread_spin_destroy));
linker::RegisterSymbol("pthread_spin_lock",
reinterpret_cast<void*>(mocktail_pthread_spin_lock));
linker::RegisterSymbol("pthread_spin_trylock",
reinterpret_cast<void*>(mocktail_pthread_spin_trylock));
linker::RegisterSymbol("pthread_spin_unlock",
reinterpret_cast<void*>(mocktail_pthread_spin_unlock));
linker::RegisterSymbol(
"pthread_attr_init",
reinterpret_cast<void*>(mocktail_pthread_attr_init));
linker::RegisterSymbol(
"pthread_attr_destroy",
reinterpret_cast<void*>(mocktail_pthread_attr_destroy));
linker::RegisterSymbol(
"pthread_attr_setstacksize",
reinterpret_cast<void*>(mocktail_pthread_attr_setstacksize));
linker::RegisterSymbol(
"pthread_attr_setdetachstate",
reinterpret_cast<void*>(mocktail_pthread_attr_setdetachstate));
linker::RegisterSymbol(
"pthread_attr_setschedparam",
reinterpret_cast<void*>(mocktail_pthread_attr_setschedparam));
linker::RegisterSymbol(
"pthread_getattr_np",
reinterpret_cast<void*>(mocktail_pthread_getattr_np));
linker::RegisterSymbol(
"pthread_attr_getstack",
reinterpret_cast<void*>(mocktail_pthread_attr_getstack));
linker::RegisterSymbol("pthread_barrier_init",
reinterpret_cast<void*>(mocktail_pthread_barrier_init));
linker::RegisterSymbol(
"pthread_barrier_destroy",
reinterpret_cast<void*>(mocktail_pthread_barrier_destroy));
linker::RegisterSymbol("pthread_barrier_wait",
reinterpret_cast<void*>(mocktail_pthread_barrier_wait));
linker::RegisterSymbol(
"pthread_create",
reinterpret_cast<void*>(mocktail_bionic_pthread_create));
linker::RegisterSymbol(
"pthread_setschedparam",
reinterpret_cast<void*>(mocktail_bionic_pthread_setschedparam));
linker::RegisterSymbol(
"sched_setscheduler",
reinterpret_cast<void*>(mocktail_bionic_sched_setscheduler));
linker::RegisterSymbol("abort", reinterpret_cast<void*>(mocktail_abort));
linker::RegisterSymbol("__stack_chk_fail", reinterpret_cast<void*>(mocktail_recover_stack_chk_fail));
(void)registered;
std::cout << " [linker] Registered " << registered << " / " << total_symbols
<< " known symbols.\n";
std::cout << " [linker] GL symbol resolution: total=" << gl_symbol_count
<< ", window=" << gl_from_window
<< ", real_gles=" << gl_from_real_gles
<< ", stub=" << gl_from_stub << ", host=" << gl_from_host
<< ", unresolved=" << gl_unresolved << '\n';
if (IsEnabled("MOCKTAIL_REQUIRE_REAL_GRAPHICS") && has_window &&
!IsEnabled("MOCKTAIL_GLES_FORCE_STUB") &&
!IsEnabled("MOCKTAIL_GLES_NOOP_DRAW_CALLS")) {
if (gl_from_stub > 0 || gl_unresolved > 0 || (gl_from_window == 0 &&
gl_from_real_gles == 0 &&
gl_from_host == 0)) {
std::cerr << "[FATAL] Mocktail cannot guarantee real GL symbols for "
"windowed mode.\n"
<< " Set MOCKTAIL_GLES_FORCE_STUB=1 or "
"MOCKTAIL_GLES_NOOP_DRAW_CALLS=1 to continue with stubs.\n"
" Or clear MOCKTAIL_REQUIRE_REAL_GRAPHICS and inspect "
"logs above.\n"
<< " Consider setting MOCKTAIL_GRAPHICS_BACKEND=angle-vulkan and "
"MOCKTAIL_GLES_LIBRARY=<path-to-real-libGLESv2.so>.\n";
return EXIT_FAILURE;
}
}
// Register imports before loading synthetic SONAMEs.
{
int fd = ::open(library_path.c_str(), O_RDONLY);
if (fd >= 0) {
struct stat st;
if (::fstat(fd, &st) == 0) {
void* map = ::mmap(nullptr, static_cast<size_t>(st.st_size),
PROT_READ, MAP_PRIVATE, fd, 0);
if (map != MAP_FAILED) {
auto* ehdr = reinterpret_cast<Elf64_Ehdr*>(map);
auto* shdr = reinterpret_cast<Elf64_Shdr*>(
reinterpret_cast<char*>(map) + ehdr->e_shoff);
Elf64_Shdr* dynsym_hdr = nullptr;
Elf64_Shdr* dynstr_hdr = nullptr;
for (int si = 0; si < ehdr->e_shnum; ++si) {
if (shdr[si].sh_type == SHT_DYNSYM) dynsym_hdr = &shdr[si];
else if (shdr[si].sh_type == SHT_STRTAB && si != ehdr->e_shstrndx)
dynstr_hdr = &shdr[si];
}
if (dynsym_hdr && dynstr_hdr) {
auto* syms = reinterpret_cast<Elf64_Sym*>(
reinterpret_cast<char*>(map) + dynsym_hdr->sh_offset);
const char* strtab = reinterpret_cast<const char*>(map)
+ dynstr_hdr->sh_offset;
size_t num_syms = dynsym_hdr->sh_size / sizeof(Elf64_Sym);
int auto_registered = 0;
for (size_t si = 0; si < num_syms; ++si) {
if (syms[si].st_shndx != SHN_UNDEF) continue;
const char* name = strtab + syms[si].st_name;
if (!name || name[0] == '\0') continue;
if (linker::GetBionicSymbols().count(name)) continue;
auto result = ResolveSymbolForBionic(name, has_window,
real_gles_handle,
stub_handles);
if (result.address != nullptr) {
linker::RegisterSymbol(name, result.address);
++auto_registered;
}
}
std::cout << " [linker] Auto-registered " << auto_registered
<< " additional ELF symbols from " << library_path
<< '\n';
}
::munmap(map, static_cast<size_t>(st.st_size));
}
}
::close(fd);
}
}
// Load synthetic libraries only after the import map is complete.
linker::RegisterBionicPthreadKeyRuntimeForLibc();
linker::RegisterBionicSysconfRuntimeForLibc();
linker::RegisterBionicSignalRuntimeForLibc();
linker::RegisterBionicStdioRuntimeForLibc();
linker::RegisterBionicDynamicLoaderForLibdl();
std::cout << " [linker] Registered typed Bionic pthread/sysconf/signal/"
"stdio ABI for libc.so\n";
std::cout << " [linker] dl_iterate_phdr routed to Bionic linker before "
"SONAME snapshots\n"
<< std::flush;
for (const char* name : stub_names) {
linker::LoadLibrary(name, name);
}
if (bionic_vulkan_adapter_handle != nullptr) {
linker::LoadLibrary("libvulkan.so.1", "libvulkan.so.1");
}
PreloadPthreadSymbols();
std::cout << " Library path: " << library_path << '\n';
linker::LibraryHandle roblox_handle =
linker::LoadLibrary(library_path, "libroblox");
if (roblox_handle == nullptr) {
std::cerr << "\n[FATAL] Could not load '" << library_path << "'.\n"
<< " Extract lib/" << compat::kGuestAbi
<< "/libroblox.so from a Roblox APK and\n"
<< " place it at the path above (or set ROBLOX_LIB_PATH).\n";
return EXIT_FAILURE;
}
const linker::ProgramHeaderValidation program_headers =
linker::ValidateBionicProgramHeaders(roblox_handle);
if (!program_headers) {
std::cerr << "\n[FATAL] Bionic program-header visibility failed: "
<< program_headers.error << '\n';
return EXIT_FAILURE;
}
std::cout << " [linker] Bionic unwind metadata validated for libroblox\n"
<< std::flush;
// The experimental VR bridge is armed only for --vr on an exact supported
// Build ID. It validates its machine contracts and interposes the
// DebugDeviceVR vtable slots after relocations and before guest startup
// can create the object. A no-op success while no bridge is armed.
if (const mocktail::Status vr_activation =
mocktail::vr::NotifyRobloxVrImageLoaded(
linker::FindLoadedAndroidLibraryBase("libroblox"));
!vr_activation.ok()) {
std::cerr << "\n[FATAL] Experimental Roblox VR bridge activation "
"failed: "
<< vr_activation.message() << '\n';
return EXIT_FAILURE;
}
// The pre-constructor hook owns installation; repeating arena setup here is
// not idempotent.
if (HostAbiExperimentRequested() &&
(!g_host_abi_install_attempted || !g_host_abi_install_result)) {
std::cerr << "\n[FATAL] Host ABI profile did not install before "
"libroblox constructors: "
<< (g_host_abi_install_result.error != nullptr
? g_host_abi_install_result.error
: "linker pre-constructor hook was not observed")
<< '\n';
return EXIT_FAILURE;
}
PrintStage(5, "Invoking JNI_OnLoad in libroblox.so");
auto* jni_onload =
reinterpret_cast<JniOnLoadFn>(
linker::ResolveSymbol(roblox_handle, "JNI_OnLoad"));
if (jni_onload == nullptr) {
std::cerr << "\n[FATAL] Symbol 'JNI_OnLoad' not found in libroblox.so.\n";
return EXIT_FAILURE;
}
const mocktail::compat::HostAbiProfile* stage5_host_profile =
g_active_host_abi_profile.load(std::memory_order_acquire);
if (g_host_abi_install_attempted &&
g_host_abi_install_result.uses_native_mimalloc &&
stage5_host_profile != nullptr) {
const mocktail::compat::NativePreJniBootstrapStatus bootstrap_status =
mocktail::compat::InitializeNativePreJniRegistry(
g_libroblox_base_static, *stage5_host_profile);
if (bootstrap_status ==
mocktail::compat::NativePreJniBootstrapStatus::kInitialized) {
std::cout << " [compat] native pre-JNI registry initialized\n"
<< std::flush;
} else if (bootstrap_status ==
mocktail::compat::NativePreJniBootstrapStatus::kFailed) {
std::cerr << "\n[FATAL] native pre-JNI registry initialization failed\n";
return EXIT_FAILURE;
}
}
JavaVM* raw_vm = jni_vm->GetJavaVM();
PublishCurrentJniEnv(jni_vm->GetJNIEnv());
int jni_timeout_ms =
GetEnvInt("MOCKTAIL_JNI_ONLOAD_TIMEOUT_MS", 3000);
bool soft_timeout_enabled = IsEnabled("MOCKTAIL_JNI_ONLOAD_SOFT_TIMEOUT");
bool soft_timeout_forced =
IsEnabled("MOCKTAIL_JNI_ONLOAD_SOFT_TIMEOUT_FORCE");
g_stage6_jni_env =
reinterpret_cast<uintptr_t>(jni_vm->GetJNIEnv());
if (soft_timeout_enabled && !soft_timeout_forced) {
std::cerr
<< " [jvm] JNI_OnLoad soft timeout disabled in safe mode (set "
<< "MOCKTAIL_JNI_ONLOAD_SOFT_TIMEOUT_FORCE=1 to enable).\n"
<< std::flush;
}
g_jni_onload_soft_timeout = soft_timeout_enabled && soft_timeout_forced;
jint jni_version = JNI_ERR;
if (jni_timeout_ms > 0 && g_jni_onload_soft_timeout != 0) {
std::cout << " [jvm] JNI_OnLoad soft timeout watcher armed: "
<< jni_timeout_ms << "ms\n"
<< std::flush;
JniOnLoadAsyncContext async_context{jni_onload, JNI_ERR, jni_vm.get()};
mocktail::runtime::OwnedPthread onload_thread;
const int create_result =
onload_thread.Start(&RunJniOnLoadWorker, &async_context, 0);
if (create_result != 0) {
std::cout << " [jvm] JNI_OnLoad worker thread creation failed: "
<< create_result << "\n"
<< std::flush;
jni_version = jni_onload(raw_vm, nullptr);
} else {
const mocktail::runtime::OwnedPthreadWaitResult wait_result =
onload_thread.WaitFor(jni_timeout_ms, 10);
if (wait_result.joined()) {
jni_version = async_context.result;
} else {
std::cerr << " [jvm] JNI_OnLoad soft timeout exceeded; cancelling "
"without allowing runtime unwind\n"
<< std::flush;
const int cancel_join_grace_ms = std::clamp(
GetEnvInt("MOCKTAIL_JNI_ONLOAD_CANCEL_JOIN_GRACE_MS", 5000), 100,
60000);
const mocktail::runtime::OwnedPthreadCancelResult cancel_result =
onload_thread.CancelAndJoinFor(cancel_join_grace_ms, 10);
std::cerr << "[FATAL] JNI_OnLoad did not complete before its timeout; "
<< "cancel=" << cancel_result.cancel_error << " join="
<< mocktail::runtime::OwnedPthreadWaitStatusName(
cancel_result.wait.status)
<< " error=" << cancel_result.wait.platform_error
<< "; terminating without RAII unwind\n"
<< std::flush;
std::_Exit(EXIT_FAILURE);
}
}
} else {
if (jni_timeout_ms > 0) {
g_jni_onload_in_progress = 1;
g_jni_onload_timings_printed = 0;
std::cout << " [jvm] JNI_OnLoad timeout watchdog armed: "
<< jni_timeout_ms << "ms\n"
<< std::flush;
struct sigaction action;
action.sa_sigaction = JniOnLoadTimeoutAlarm;
action.sa_flags = SA_SIGINFO | SA_RESETHAND;
sigemptyset(&action.sa_mask);
sigaction(SIGALRM, &action, nullptr);
alarm((jni_timeout_ms + 999) / 1000);
}
jni_version = jni_onload(raw_vm, nullptr);
if (jni_timeout_ms > 0) {
g_jni_onload_in_progress = 0;
g_jni_onload_jmp_armed = 0;
alarm(0);
}
}
std::cout << " JNI_OnLoad returned. Requested JNI version: 0x"
<< std::hex << std::setw(8) << std::setfill('0') << jni_version
<< std::dec << '\n';
// libroblox owns this tagged-pointer segment table. Keep it intact by
// default; replacing it with the pseudo-JNI table makes native Roblox
// scheduler objects resolve to Java object stubs with the wrong layout.
{
uintptr_t base_addr = g_libroblox_base;
#ifdef MOCKTAIL_USE_BIONIC_LINKER
const uintptr_t loader_base =
static_cast<uintptr_t>(::linker::get_library_base(roblox_handle));
if (loader_base != 0) {
base_addr = loader_base;
}
#endif
if (base_addr == 0) {
Dl_info symbol_info{};
if (::dladdr(reinterpret_cast<void*>(jni_onload), &symbol_info) != 0) {
base_addr = reinterpret_cast<uintptr_t>(symbol_info.dli_fbase);
}
}
if (base_addr == 0) {
std::cerr << "[FATAL] Bionic linker did not report libroblox base\n";
return EXIT_FAILURE;
}
g_libroblox_base = base_addr;
g_mocktail_abort_libroblox_base = base_addr;
std::cout << " [compat] skipped all fixed-offset post-JNI patches\n"
<< std::flush;
}
// Restore the pseudo-JNI table if JNI_OnLoad replaced it.
{
JNIEnv* main_env = jni_vm->GetJNIEnv();
if (main_env) {
std::cerr << " [jvm] restoring env->functions after JNI_OnLoad"
<< " (was " << (void*)main_env->functions << ")\n" << std::flush;
jni_vm->RestoreFunctions();
std::cerr << " [jvm] env->functions now " << (void*)main_env->functions
<< "\n"
<< std::flush;
}
}
std::unique_ptr<mocktail::runtime::RobloxGameSessionRuntime>
game_session_runtime;
std::unique_ptr<mocktail::runtime::RobloxGameSessionSymbols>
experience_game_symbols;
std::shared_ptr<mocktail::runtime::RobloxExperienceComposition>
experience_composition;
std::unique_ptr<mocktail::runtime::RobloxPermissionsBridge>
eager_permissions_bridge;
std::unique_ptr<mocktail::runtime::RobloxCallProtocolBridge>
eager_call_protocol_bridge;
std::unique_ptr<mocktail::runtime::RobloxHapticProtocolBridge>
eager_haptic_protocol_bridge;
std::shared_ptr<ExperienceLifecycleTarget> experience_lifecycle_target;
std::shared_ptr<mocktail::runtime::RobloxWindowInputRuntime>
window_input_runtime;
std::unique_ptr<mocktail::runtime::RobloxTextInputJniBridge>
text_input_bridge;
mocktail::window::ScopedPresentObserver game_present_observer;
if (!IsDisabled("MOCKTAIL_START_ENGINE")) {
PrintStage(6, is_headless ? "Invoking headless Roblox startup entry points"
: "Invoking Roblox startup entry points");
const mocktail::runtime::RobloxSymbolLookup capability_lookup(
&ResolveRobloxCapabilitySymbol, &roblox_handle);
const mocktail::runtime::RobloxCapabilityResolution capability_resolution =
mocktail::runtime::ResolveRobloxCapabilities(
capability_lookup,
mocktail::runtime::RobloxCapabilityRequirements{has_window});
if (!capability_resolution.ok()) {
std::cerr << "\n[FATAL] Roblox startup capability resolution failed: "
<< mocktail::runtime::RobloxCapabilityResolutionStatusName(
capability_resolution.status())
<< '\n';
for (const std::string& symbol_name :
capability_resolution.missing_required_symbols()) {
std::cerr << " - " << symbol_name << '\n';
}
return EXIT_FAILURE;
}
const mocktail::runtime::RobloxCapabilities& roblox_capabilities =
*capability_resolution.capabilities();
const mocktail::runtime::RobloxStartupSymbols& startup_symbols =
roblox_capabilities.startup;
if (has_window) {
const mocktail::runtime::RobloxGameSessionResolution game_resolution =
mocktail::runtime::ResolveRobloxGameSessionSymbols(capability_lookup);
if (!game_resolution.ok()) {
std::cerr << "\n[FATAL] GAME lifecycle symbol resolution failed: "
<< mocktail::runtime::RobloxGameSessionResolutionStatusName(
game_resolution.status())
<< '\n';
for (const std::string& symbol_name :
game_resolution.missing_required_symbols()) {
std::cerr << " - " << symbol_name << '\n';
}
return EXIT_FAILURE;
}
if (IsEnabled("MOCKTAIL_START_GAME_WITH_PARAM")) {
mocktail::runtime::JniEnvironmentProvider environment{
raw_vm, jni_vm.get(), &RestoreGameSessionJniEnvironment};
mocktail::runtime::RobloxGameSurfaceJniConfig surface_config;
surface_config.asset_folder_path = DefaultAssetPath();
surface_config.dpi_scale =
mocktail::window::GetWindowViewportSnapshot().dpi_scale;
surface_config.is_touch_device = input_capabilities.touch_enabled;
surface_config.is_mouse_device = input_capabilities.mouse_enabled;
surface_config.is_keyboard_device =
input_capabilities.keyboard_enabled;
game_session_runtime =
std::make_unique<mocktail::runtime::RobloxGameSessionRuntime>(
environment, *game_resolution.symbols(),
std::move(surface_config));
std::cout << " [game-session] eager typed GAME symbols ready\n"
<< std::flush;
} else {
experience_game_symbols =
std::make_unique<mocktail::runtime::RobloxGameSessionSymbols>(
*game_resolution.symbols());
std::cout << " [experience] dynamic GAME symbols ready\n"
<< std::flush;
}
}
NativeGameGlobalInitFn native_global_init =
startup_symbols.game_global_init;
NativeNoArgFn native_update_adapter_init =
startup_symbols.update_adapter_init;
NativeAppBridgeObjectParamsFn native_init_with_params =
startup_symbols.app_bridge_v2_init_with_params;
NativeNoArgFn native_start_lua_app_dm =
startup_symbols.app_bridge_start_lua_app_dm;
NativeAppBridgeObjectParamsFn native_start_app_with_params =
startup_symbols.app_bridge_v2_start_app_with_params;
NativeUpdateSurfaceAppFn native_update_surface_app =
startup_symbols.app_bridge_v2_update_surface_app_with_platform_params;
NativeNoArgFn native_call_messages_from_main_thread =
startup_symbols.call_messages_from_main_thread;
auto* native_init_client_settings =
reinterpret_cast<NativeInitClientSettingsFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeGLInterface_"
"nativeInitClientSettings"));
auto* native_init_client_settings_signed =
reinterpret_cast<NativeInitClientSettingsSignedFn>(
linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeGLInterface_"
"nativeInitClientSettingsSigned"));
auto* native_init_client_settings_cached =
reinterpret_cast<NativeInitClientSettingsCachedFn>(
linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeGLInterface_"
"nativeInitClientSettingsCached"));
auto* native_init_client_settings_cached_compressed =
reinterpret_cast<NativeInitClientSettingsCachedCompressedFn>(
linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeGLInterface_"
"nativeInitClientSettingsCachedCompressed"));
auto* native_post_client_settings =
reinterpret_cast<NativePostClientSettingsFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeGLInterface_"
"nativePostClientSettingsLoadedInitialization3"));
auto* native_initialize_native_flags =
reinterpret_cast<NativeInitializeNativeFlagsFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_client_flags_FlagJniInterface_"
"nativeInitializeNativeFlags"));
auto* native_app_bridge_app_start =
reinterpret_cast<NativeAppBridgeAppStartFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeAppBridgeInterface_"
"nativeAppBridgeAppStart__Ljava_lang_String_2Ljava_lang_String_2Z"
"Ljava_lang_String_2Ljava_lang_String_2Ljava_lang_String_2"));
auto* native_set_is_first_install =
reinterpret_cast<NativeSetIsFirstInstallFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeAppBridgeInterface_"
"setIsFirstInstall"));
auto* native_set_base_url =
reinterpret_cast<NativeSetBaseUrlFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeSettingsInterface_"
"nativeSetBaseUrl"));
auto* native_set_device_info =
reinterpret_cast<NativeObjectInitFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeSettingsInterface_"
"nativeSetDeviceInfo"));
auto* native_base_url_protocol_init =
reinterpret_cast<NativeObjectInitFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_universalapp_linking_JNIBaseUrlProtocol_init"));
auto* native_set_roblox_channel =
reinterpret_cast<NativeSetStringParamFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeSettingsInterface_"
"nativeSetRobloxChannel"));
auto* native_override_channel_platform_name =
reinterpret_cast<NativeSetStringParamFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeSettingsInterface_"
"nativeOverrideChannelPlatformName"));
auto* native_set_roblox_version =
reinterpret_cast<NativeSetStringParamFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeSettingsInterface_"
"nativeSetRobloxVersion"));
auto* native_set_exception_reason_filename =
reinterpret_cast<NativeSetStringParamFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeSettingsInterface_"
"nativeSetExceptionReasonFilename"));
auto* native_set_base_data_directories =
reinterpret_cast<NativeSetTwoStringParamsFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeSettingsInterface_"
"nativeSetBaseDataDirectories"));
auto* native_set_cache_directory =
reinterpret_cast<NativeSetStringParamFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeSettingsInterface_"
"nativeSetCacheDirectory"));
auto* native_set_files_directory =
reinterpret_cast<NativeSetStringParamFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeSettingsInterface_"
"nativeSetFilesDirectory"));
auto* native_set_external_directory =
reinterpret_cast<NativeSetStringParamFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeSettingsInterface_"
"nativeSetExternalDirectory"));
auto* native_set_preferences_file =
reinterpret_cast<NativeSetStringParamFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeSettingsInterface_"
"nativeSetPreferencesFile"));
auto* native_set_default_app_policy_file =
reinterpret_cast<NativeSetStringParamFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeSettingsInterface_"
"nativeSetDefaultAppPolicyFile"));
auto* native_set_http_client_proxy =
reinterpret_cast<NativeSetHttpClientProxyFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeSettingsInterface_"
"nativeSetHttpClientProxy"));
auto* native_init_fast_log =
reinterpret_cast<NativeNoArgFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeSettingsInterface_"
"nativeInitFastLog"));
auto* native_set_multiple_cookies =
reinterpret_cast<NativeSetTwoStringParamsFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeSettingsInterface_"
"nativeSetMultipleCookies"));
auto* native_get_cookies_for_domain =
reinterpret_cast<jnivm::RobloxCookieGetter>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeSettingsInterface_"
"nativeGetCookiesForDomain"));
jni_vm->SetRobloxCookieGetter(native_get_cookies_for_domain);
auto* native_cookie_manager_set_cookie =
reinterpret_cast<NativeSetTwoStringParamsFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_universalapp_cookie_JNICookieManager_setCookie"));
auto* native_set_platform_headers_with_idfa =
reinterpret_cast<NativeSetThreeStringParamsFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeSettingsInterface_"
"nativeSetPlatformHeadersWithIdfa"));
auto* native_set_user_id =
reinterpret_cast<NativeSetStringParamFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeSettingsInterface_"
"nativeSetUserId"));
auto* native_init_asset_manager =
reinterpret_cast<NativeObjectInitFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_client_JNIAAssetManagerSetup_initNative"));
auto* native_init_storage_manager =
reinterpret_cast<NativeInitStorageManagerFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_client_LocalStorageManager_"
"initStorageManagerNativeV3"));
auto* native_local_storage_set_platform_impl =
reinterpret_cast<NativeSetPlatformImplFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_protocols_localstorageplatforminterface_"
"generated_ILocalStorageHandlerCore_setPlatformImpl"));
auto* native_set_init_params =
reinterpret_cast<NativeAppBridgeSetInitParamsFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_client_startup_MainGameActivity_"
"nativeAppBridgeSetInitParams"));
auto* native_retry_init =
reinterpret_cast<NativeNoArgFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_client_startup_MainGameActivity_"
"nativeRetryInit"));
auto* native_update_screen_orientation =
reinterpret_cast<NativeUpdateScreenOrientationFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeInputInterface_"
"nativeUpdateScreenOrientation"));
auto* native_update_app_ui_sizes =
reinterpret_cast<NativeUpdateAppUiSizesFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeGLInterface_updateAppUISizes"));
auto* native_on_fragment_start =
reinterpret_cast<NativeNoArgFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeGLInterface_"
"nativeOnFragmentStart"));
auto* native_pass_current_display_refresh_rate =
reinterpret_cast<NativePassCurrentDisplayRefreshRateFn>(
linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeGLInterface_"
"nativePassCurrentDisplayRefreshRate"));
auto* native_pass_supported_refresh_rates =
reinterpret_cast<NativePassSupportedRefreshRatesFn>(
linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeGLInterface_"
"nativePassSupportedRefreshRates"));
auto* native_set_task_scheduler_background_mode =
reinterpret_cast<NativeSetTaskSchedulerBackgroundModeFn>(
linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeGLInterface_"
"setTaskSchedulerBackgroundMode"));
auto* native_send_app_ready =
reinterpret_cast<NativeSendAppReadyFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeGLInterface_"
"nativeAppBridgeV2SendAppEventOnAppReady"));
auto* native_send_game_loaded =
reinterpret_cast<NativeSendGameLoadedFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeGLInterface_"
"nativeAppBridgeV2SendAppEventOnGameLoaded"));
auto* native_update_mobile_haptics_state =
mocktail::runtime::RobloxHapticProtocolBridge::EnabledForThisSession()
? reinterpret_cast<NativeUpdateMobileHapticsStateFn>(
linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_engine_jni_NativeGLInterface_"
"nativeUpdateMobileHapticsState"))
: nullptr;
auto* native_set_asset_path =
reinterpret_cast<NativeSetStringParamFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_client_startup_MainGameActivity_"
"nativeSetAssetPath"));
auto resolve_activity_lifecycle =
[&](const char* method_name) -> NativeActivityLifecycleStringFn {
std::string symbol =
"Java_com_roblox_universalapp_activitylifecyclecallbacks_"
"JNIActivityLifecycleCallbacks_";
symbol += method_name;
return reinterpret_cast<NativeActivityLifecycleStringFn>(
linker::ResolveSymbol(roblox_handle, symbol.c_str()));
};
NativeActivityLifecycleCallbacks activity_lifecycle_callbacks = {
resolve_activity_lifecycle("nativeOnPreCreated"),
resolve_activity_lifecycle("nativeOnCreated"),
resolve_activity_lifecycle("nativeOnPostCreated"),
resolve_activity_lifecycle("nativeOnPreStarted"),
resolve_activity_lifecycle("nativeOnStarted"),
resolve_activity_lifecycle("nativeOnPostStarted"),
resolve_activity_lifecycle("nativeOnPreResumed"),
resolve_activity_lifecycle("nativeOnResumed"),
resolve_activity_lifecycle("nativeOnPostResumed"),
};
auto* native_game_activity_init =
reinterpret_cast<NativeGameActivityInitFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_google_androidgamesdk_GameActivity_initializeNativeCode"));
auto* native_app_lifecycle_set_active =
reinterpret_cast<NativeNoArgFn>(linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_universalapp_applifecyclenativeadapter_"
"JNIAppLifecycleNativeAdapter_setActive"));
g_native_call_messages_from_main_thread =
native_call_messages_from_main_thread;
std::cout << " [engine] Stage6 symbol resolve done" << std::endl;
std::cout << " native_init_with_params="
<< reinterpret_cast<const void*>(native_init_with_params) << '\n'
<< " native_init_client_settings="
<< reinterpret_cast<const void*>(native_init_client_settings)
<< '\n'
<< " native_init_client_settings_signed="
<< reinterpret_cast<const void*>(
native_init_client_settings_signed)
<< '\n'
<< " native_init_client_settings_cached="
<< reinterpret_cast<const void*>(
native_init_client_settings_cached)
<< '\n'
<< " native_init_client_settings_cached_compressed="
<< reinterpret_cast<const void*>(
native_init_client_settings_cached_compressed)
<< '\n'
<< " native_initialize_native_flags="
<< reinterpret_cast<const void*>(
native_initialize_native_flags)
<< '\n'
<< " native_app_bridge_app_start="
<< reinterpret_cast<const void*>(native_app_bridge_app_start)
<< '\n'
<< " native_set_is_first_install="
<< reinterpret_cast<const void*>(native_set_is_first_install)
<< '\n'
<< " native_update_adapter_init="
<< reinterpret_cast<const void*>(native_update_adapter_init)
<< '\n'
<< " native_update_app_ui_sizes="
<< reinterpret_cast<const void*>(native_update_app_ui_sizes)
<< '\n'
<< " native_update_surface_app="
<< reinterpret_cast<const void*>(native_update_surface_app) << '\n'
<< " native_start_app_with_params="
<< reinterpret_cast<const void*>(native_start_app_with_params) << '\n'
<< " native_send_app_ready="
<< reinterpret_cast<const void*>(native_send_app_ready) << '\n'
<< " native_send_game_loaded="
<< reinterpret_cast<const void*>(native_send_game_loaded) << '\n'
<< " native_update_mobile_haptics_state="
<< reinterpret_cast<const void*>(
native_update_mobile_haptics_state)
<< '\n'
<< " native_start_lua_app_dm="
<< reinterpret_cast<const void*>(native_start_lua_app_dm) << '\n'
<< " native_call_messages_from_main_thread="
<< reinterpret_cast<const void*>(
native_call_messages_from_main_thread)
<< '\n'
<< " native_base_url_protocol_init="
<< reinterpret_cast<const void*>(native_base_url_protocol_init)
<< '\n'
<< " native_set_device_info="
<< reinterpret_cast<const void*>(native_set_device_info)
<< '\n'
<< " native_override_channel_platform_name="
<< reinterpret_cast<const void*>(
native_override_channel_platform_name)
<< '\n'
<< " native_set_roblox_version="
<< reinterpret_cast<const void*>(native_set_roblox_version)
<< '\n'
<< " native_set_http_client_proxy="
<< reinterpret_cast<const void*>(native_set_http_client_proxy)
<< '\n'
<< " native_init_fast_log="
<< reinterpret_cast<const void*>(native_init_fast_log) << '\n'
<< " native_set_multiple_cookies="
<< reinterpret_cast<const void*>(native_set_multiple_cookies)
<< '\n'
<< " native_cookie_manager_set_cookie="
<< reinterpret_cast<const void*>(
native_cookie_manager_set_cookie)
<< '\n'
<< " native_set_platform_headers_with_idfa="
<< reinterpret_cast<const void*>(
native_set_platform_headers_with_idfa)
<< '\n'
<< " native_local_storage_set_platform_impl="
<< reinterpret_cast<const void*>(
native_local_storage_set_platform_impl)
<< '\n'
<< " native_retry_init="
<< reinterpret_cast<const void*>(native_retry_init) << '\n'
<< " native_set_asset_path="
<< reinterpret_cast<const void*>(native_set_asset_path) << '\n'
<< " activity_on_pre_created="
<< reinterpret_cast<const void*>(
activity_lifecycle_callbacks.on_pre_created)
<< '\n'
<< " activity_on_created="
<< reinterpret_cast<const void*>(
activity_lifecycle_callbacks.on_created)
<< '\n'
<< " activity_on_post_created="
<< reinterpret_cast<const void*>(
activity_lifecycle_callbacks.on_post_created)
<< '\n'
<< " activity_on_pre_started="
<< reinterpret_cast<const void*>(
activity_lifecycle_callbacks.on_pre_started)
<< '\n'
<< " activity_on_started="
<< reinterpret_cast<const void*>(
activity_lifecycle_callbacks.on_started)
<< '\n'
<< " activity_on_post_started="
<< reinterpret_cast<const void*>(
activity_lifecycle_callbacks.on_post_started)
<< '\n'
<< " activity_on_pre_resumed="
<< reinterpret_cast<const void*>(
activity_lifecycle_callbacks.on_pre_resumed)
<< '\n'
<< " activity_on_resumed="
<< reinterpret_cast<const void*>(
activity_lifecycle_callbacks.on_resumed)
<< '\n'
<< " activity_on_post_resumed="
<< reinterpret_cast<const void*>(
activity_lifecycle_callbacks.on_post_resumed)
<< '\n'
<< " native_game_activity_init="
<< reinterpret_cast<const void*>(native_game_activity_init)
<< '\n'
<< " native_app_lifecycle_set_active="
<< reinterpret_cast<const void*>(native_app_lifecycle_set_active)
<< '\n'
<< " native_on_fragment_start="
<< reinterpret_cast<const void*>(native_on_fragment_start)
<< '\n'
<< " native_pass_supported_refresh_rates="
<< reinterpret_cast<const void*>(
native_pass_supported_refresh_rates)
<< '\n'
<< " native_pass_current_display_refresh_rate="
<< reinterpret_cast<const void*>(
native_pass_current_display_refresh_rate)
<< '\n'
<< std::flush;
void* message_bus_publish_response_raw = linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_universalapp_messagebus_MessageBus_"
"publishProtocolMethodResponseRaw");
if ((native_init_client_settings == nullptr &&
native_init_client_settings_signed == nullptr &&
native_init_client_settings_cached == nullptr &&
native_init_client_settings_cached_compressed == nullptr) ||
native_post_client_settings == nullptr ||
native_set_base_url == nullptr || native_set_device_info == nullptr ||
native_set_roblox_channel == nullptr ||
native_set_exception_reason_filename == nullptr ||
native_set_base_data_directories == nullptr ||
native_set_cache_directory == nullptr ||
native_set_files_directory == nullptr ||
native_set_external_directory == nullptr ||
native_set_preferences_file == nullptr ||
native_set_default_app_policy_file == nullptr ||
native_set_http_client_proxy == nullptr ||
native_init_fast_log == nullptr ||
native_set_multiple_cookies == nullptr ||
native_set_platform_headers_with_idfa == nullptr ||
native_init_storage_manager == nullptr ||
native_set_init_params == nullptr || native_set_asset_path == nullptr ||
activity_lifecycle_callbacks.on_created == nullptr ||
activity_lifecycle_callbacks.on_started == nullptr ||
activity_lifecycle_callbacks.on_resumed == nullptr ||
message_bus_publish_response_raw == nullptr) {
std::cerr << "\n[FATAL] One or more NativeGL startup symbols were not "
<< "found.\n";
return EXIT_FAILURE;
}
std::cout << " [compat] skipped fixed-offset Stage 6 patch set\n"
<< std::flush;
// Real startup calls run by default; signal recovery falls back to the
// Mocktail implementations.
const bool requested_set_asset_path =
ShouldRunStartupStep("MOCKTAIL_STEP_SET_ASSET_PATH", true);
const bool requested_init_with_params =
ShouldRunStartupStep("MOCKTAIL_STEP_INIT_WITH_PARAMS", true);
const bool requested_start_app_with_params =
ShouldRunStartupStep("MOCKTAIL_STEP_START_APP_WITH_PARAMS", true);
const bool run_set_asset_path = requested_set_asset_path;
const bool call_real_set_asset_path =
run_set_asset_path &&
!IsDisabled("MOCKTAIL_CALL_REAL_NATIVE_SET_ASSET_PATH");
const bool run_global_init =
ShouldRunStartupStep("MOCKTAIL_STEP_GAME_GLOBAL_INIT",
!IsDisabled("MOCKTAIL_GAME_GLOBAL_INIT"));
const bool run_init_client_settings =
ShouldRunStartupStep("MOCKTAIL_STEP_INIT_CLIENT_SETTINGS",
!IsDisabled("MOCKTAIL_INIT_CLIENT_SETTINGS"));
const bool run_post_client_settings =
ShouldRunStartupStep("MOCKTAIL_STEP_POST_CLIENT_SETTINGS",
IsEnabled("MOCKTAIL_POST_CLIENT_SETTINGS"));
const bool run_app_bridge_app_start =
ShouldRunStartupStep("MOCKTAIL_STEP_APP_BRIDGE_APP_START",
IsEnabled("MOCKTAIL_APP_BRIDGE_APP_START")) &&
native_app_bridge_app_start != nullptr;
const bool run_init_with_params = requested_init_with_params;
const bool call_real_init_with_params =
run_init_with_params &&
!IsDisabled("MOCKTAIL_CALL_REAL_APP_BRIDGE_INIT");
const bool run_update_screen_orientation =
ShouldRunStartupStep("MOCKTAIL_STEP_UPDATE_SCREEN_ORIENTATION",
IsEnabled("MOCKTAIL_UPDATE_SCREEN_ORIENTATION")) &&
native_update_screen_orientation != nullptr;
const bool run_update_surface_app =
ShouldRunStartupStep("MOCKTAIL_STEP_UPDATE_SURFACE_APP",
IsEnabled("MOCKTAIL_UPDATE_SURFACE_APP"));
const bool call_real_update_surface_app =
run_update_surface_app &&
IsEnabled("MOCKTAIL_CALL_REAL_APP_BRIDGE_UPDATE_SURFACE");
const bool run_activity_lifecycle =
ShouldRunStartupStep("MOCKTAIL_STEP_ACTIVITY_LIFECYCLE",
IsEnabled("MOCKTAIL_ACTIVITY_LIFECYCLE"));
const bool run_game_activity_init =
ShouldRunStartupStep("MOCKTAIL_STEP_GAME_ACTIVITY_INIT",
has_window || IsEnabled("MOCKTAIL_GAME_ACTIVITY_INIT"));
const bool run_set_init_params =
ShouldRunStartupStep("MOCKTAIL_STEP_SET_INIT_PARAMS",
IsEnabled("MOCKTAIL_SET_INIT_PARAMS") ||
(run_game_activity_init &&
!run_init_with_params));
const bool run_game_activity_surface =
ShouldRunStartupStep(
"MOCKTAIL_STEP_GAME_ACTIVITY_SURFACE",
has_window && run_game_activity_init &&
IsEnabled("MOCKTAIL_GAME_ACTIVITY_SURFACE"));
const bool run_app_lifecycle_active =
ShouldRunStartupStep("MOCKTAIL_STEP_APP_LIFECYCLE_ACTIVE",
has_window && IsEnabled("MOCKTAIL_APP_LIFECYCLE_ACTIVE"));
const bool run_native_fragment_start =
ShouldRunStartupStep("MOCKTAIL_STEP_NATIVE_FRAGMENT_START",
has_window &&
IsEnabled("MOCKTAIL_NATIVE_FRAGMENT_START")) &&
native_on_fragment_start != nullptr;
const bool run_display_refresh_rate =
ShouldRunStartupStep(
"MOCKTAIL_STEP_PASS_CURRENT_DISPLAY_REFRESH_RATE",
has_window &&
IsEnabled("MOCKTAIL_PASS_CURRENT_DISPLAY_REFRESH_RATE")) &&
(native_pass_current_display_refresh_rate != nullptr ||
native_pass_supported_refresh_rates != nullptr);
const bool run_start_app_with_params = requested_start_app_with_params;
// Keep the real call opt-in because it can block before rendering starts.
const bool call_real_start_app_with_params =
run_start_app_with_params &&
IsEnabled("MOCKTAIL_CALL_REAL_APP_BRIDGE_START");
const bool run_start_lua_app_dm =
ShouldRunStartupStep("MOCKTAIL_STEP_START_LUA_APP_DM",
IsEnabled("MOCKTAIL_START_LUA_APP_DM"));
const bool run_native_settings =
ShouldRunStartupStep("MOCKTAIL_STEP_NATIVE_SETTINGS",
run_init_with_params ||
run_start_app_with_params);
const bool run_prepare_jni =
ShouldRunStartupStep("MOCKTAIL_STEP_PREP_JNI",
run_set_asset_path || run_global_init ||
run_init_client_settings ||
run_post_client_settings ||
run_app_bridge_app_start ||
run_native_settings ||
run_set_init_params ||
run_init_with_params ||
run_update_screen_orientation ||
run_activity_lifecycle ||
run_game_activity_init ||
run_game_activity_surface ||
run_app_lifecycle_active ||
run_native_fragment_start ||
run_display_refresh_rate ||
run_update_surface_app ||
run_start_app_with_params ||
run_start_lua_app_dm);
PrintStepDecision("prepare JNI args", run_prepare_jni);
PrintStepDecision("nativeSetAssetPath", run_set_asset_path);
PrintStepDecision("nativeGameGlobalInit", run_global_init);
PrintStepDecision("nativeInitClientSettings", run_init_client_settings);
PrintStepDecision("nativePostClientSettingsLoadedInitialization3",
run_post_client_settings);
PrintStepDecision("GameActivity.initializeNativeCode",
run_game_activity_init);
PrintStepDecision("NativeSettings directories", run_native_settings);
PrintStepDecision("nativeAppBridgeSetInitParams", run_set_init_params);
PrintStepDecision("nativeAppBridgeV2InitWithParams", run_init_with_params);
PrintStepDecision("nativeUpdateScreenOrientation",
run_update_screen_orientation);
PrintStepDecision("activity lifecycle", run_activity_lifecycle);
PrintStepDecision("nativeAppBridgeAppStart", run_app_bridge_app_start);
PrintStepDecision("GameActivity surface callbacks",
run_game_activity_surface);
PrintStepDecision("JNIAppLifecycleNativeAdapter.setActive",
run_app_lifecycle_active);
PrintStepDecision("NativeGLInterface.nativeOnFragmentStart",
run_native_fragment_start);
PrintStepDecision("NativeGLInterface.nativePassCurrentDisplayRefreshRate",
run_display_refresh_rate);
PrintStepDecision("nativeAppBridgeStartLuaAppDM", run_start_lua_app_dm);
PrintStepDecision("nativeAppBridgeV2UpdateSurfaceAppWithPlatformParams",
run_update_surface_app);
PrintStepDecision("nativeAppBridgeV2StartAppWithParams",
run_start_app_with_params);
if (run_set_asset_path && !call_real_set_asset_path) {
PrintNativeBypass("nativeSetAssetPath",
"MOCKTAIL_CALL_REAL_NATIVE_SET_ASSET_PATH");
}
if (run_init_with_params && !call_real_init_with_params) {
PrintNativeBypass("nativeAppBridgeV2InitWithParams",
"MOCKTAIL_CALL_REAL_APP_BRIDGE_INIT");
}
if (run_update_surface_app && !call_real_update_surface_app) {
PrintNativeBypass("nativeAppBridgeV2UpdateSurfaceAppWithPlatformParams",
"MOCKTAIL_CALL_REAL_APP_BRIDGE_UPDATE_SURFACE");
}
if (run_start_app_with_params && !call_real_start_app_with_params) {
PrintNativeBypass("nativeAppBridgeV2StartAppWithParams",
"MOCKTAIL_CALL_REAL_APP_BRIDGE_START");
}
std::cout << " [engine] startup decisions resolved. "
<< "run_prepare_jni=" << (run_prepare_jni ? 1 : 0) << '\n'
<< " run_init_with_params=" << (run_init_with_params ? 1 : 0)
<< " run_start_app_with_params="
<< (run_start_app_with_params ? 1 : 0) << '\n' << std::flush;
// The direct GAME path still runs CoreScripts that query app protocols.
// Install these independently of ExperienceProtocol's dynamic launch path.
if (game_session_runtime != nullptr) {
mocktail::runtime::JniEnvironmentProvider environment{
raw_vm, jni_vm.get(), &RestoreGameSessionJniEnvironment};
JNIEnv* env = nullptr;
auto status = environment.Acquire(&env);
if (!status.ok()) {
std::cerr << "[FATAL] GAME protocol JNI environment unavailable\n";
return EXIT_FAILURE;
}
const auto symbols = mocktail::runtime::ResolveRobloxPlatformWebSymbols(
roblox_handle,
reinterpret_cast<mocktail::runtime::SubscribeExperienceLaunchRawFn>(
linker::ResolveSymbol(roblox_handle,
"Java_com_roblox_universalapp_messagebus_"
"MessageBus_doSubscribeRaw")),
reinterpret_cast<mocktail::runtime::DeleteMessageBusConnectionFn>(
linker::ResolveSymbol(roblox_handle,
"Java_com_roblox_universalapp_messagebus_"
"Connection_deleteSharedPtr")));
jclass bus_class =
env->FindClass("com/roblox/universalapp/messagebus/MessageBus");
jmethodID singleton =
bus_class ? env->GetStaticMethodID(
bus_class, "f",
"()Lcom/roblox/universalapp/messagebus/MessageBus;")
: nullptr;
jobject bus = singleton
? env->CallStaticObjectMethod(bus_class, singleton)
: nullptr;
if (bus_class)
env->DeleteLocalRef(bus_class);
if (!bus || env->ExceptionCheck()) {
if (env->ExceptionCheck())
env->ExceptionClear();
if (bus)
env->DeleteLocalRef(bus);
std::cerr << "[FATAL] GAME platform MessageBus is unavailable\n";
return EXIT_FAILURE;
}
const mocktail::runtime::RobloxMessageBusObjects objects{
bus,
jni_vm.get(),
&CreateExperienceRawCallback,
&ClearExperienceRawCallback,
&CreateAsyncMessageBusRequestHandler,
&ClearAsyncMessageBusRequestHandler};
eager_permissions_bridge =
std::make_unique<mocktail::runtime::RobloxPermissionsBridge>(
environment, symbols.permissions, objects,
runtime_config.microphone_enabled());
eager_call_protocol_bridge =
std::make_unique<mocktail::runtime::RobloxCallProtocolBridge>(
environment, symbols.permissions, objects);
if (mocktail::runtime::RobloxHapticProtocolBridge::
EnabledForThisSession()) {
eager_haptic_protocol_bridge =
std::make_unique<mocktail::runtime::RobloxHapticProtocolBridge>(
environment, symbols.permissions, objects);
}
status = eager_permissions_bridge->Initialize();
if (status.ok())
status = eager_call_protocol_bridge->Initialize();
if (status.ok() && eager_haptic_protocol_bridge != nullptr)
status = eager_haptic_protocol_bridge->Initialize();
env->DeleteLocalRef(bus);
if (!status.ok()) {
std::cerr << "[FATAL] GAME platform protocols did not initialize: "
<< status.message() << '\n';
return EXIT_FAILURE;
}
std::cout << " [platform] direct GAME PermissionsProtocol and "
"CallProtocol ready\n"
<< std::flush;
}
if (experience_game_symbols != nullptr) {
mocktail::runtime::RobloxExperienceMessageBusSymbols message_bus_symbols;
message_bus_symbols.get_launch_id =
reinterpret_cast<mocktail::runtime::GetExperienceLaunchIdFn>(
linker::ResolveSymbol(roblox_handle,
"Java_com_roblox_universalapp_experience_"
"JNIExperienceProtocol_getLaunchId"));
message_bus_symbols.subscribe_raw =
reinterpret_cast<mocktail::runtime::SubscribeExperienceLaunchRawFn>(
linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_universalapp_messagebus_MessageBus_"
"doSubscribeRaw"));
message_bus_symbols.delete_connection =
reinterpret_cast<mocktail::runtime::DeleteMessageBusConnectionFn>(
linker::ResolveSymbol(
roblox_handle,
"Java_com_roblox_universalapp_messagebus_Connection_"
"deleteSharedPtr"));
const mocktail::runtime::RobloxPlatformWebSymbols platform_web_symbols =
mocktail::runtime::ResolveRobloxPlatformWebSymbols(
roblox_handle, message_bus_symbols.subscribe_raw,
message_bus_symbols.delete_connection);
mocktail::runtime::RobloxExperienceJniFactory jni_factory{
jni_vm.get(),
&CreateExperienceRawCallback,
&ClearExperienceRawCallback,
&CreateMessageBusRequestHandler,
&ClearMessageBusRequestHandler,
&CreateBrowserServiceMemStorageCallback,
&ClearBrowserServiceMemStorageCallback,
&mocktail::runtime::SetJnivmPlatformWebCallbacks,
&mocktail::runtime::ClearJnivmPlatformWebCallbacks,
&CreateAsyncMessageBusRequestHandler,
&ClearAsyncMessageBusRequestHandler};
mocktail::runtime::RobloxFreshLaunchPresentBoundary present_boundary{
&game_present_observer, &RegisterFreshGamePresentObserver,
&ClearFreshGamePresentObserver};
mocktail::runtime::RobloxGameSurfaceJniConfig surface_config;
surface_config.asset_folder_path = DefaultAssetPath();
surface_config.dpi_scale =
mocktail::window::GetWindowViewportSnapshot().dpi_scale;
surface_config.is_touch_device = input_capabilities.touch_enabled;
surface_config.is_mouse_device = input_capabilities.mouse_enabled;
surface_config.is_keyboard_device = input_capabilities.keyboard_enabled;
mocktail::runtime::JniEnvironmentProvider environment{
raw_vm, jni_vm.get(), &RestoreGameSessionJniEnvironment};
experience_composition =
std::make_shared<mocktail::runtime::RobloxExperienceComposition>(
environment, message_bus_symbols, platform_web_symbols.web_view,
platform_web_symbols.browser_service,
platform_web_symbols.permissions, *experience_game_symbols,
jni_factory, present_boundary, std::move(surface_config),
&dependencies.roblox_credential(),
mocktail::runtime::RobloxExperienceSurfaceProvider{},
luau_codegen_switch.observer(),
dependencies.clear_persisted_web_view_cookie(),
runtime_config.microphone_enabled(),
platform_web_symbols.account);
const mocktail::Status platform_protocol_status =
experience_composition->InitializePlatformProtocols();
if (!platform_protocol_status.ok()) {
std::cerr << "[FATAL] Roblox platform protocols did not initialize: "
<< platform_protocol_status.message() << '\n';
return EXIT_FAILURE;
}
std::cout
<< " [platform] WebView, BrowserService and Permissions protocols "
"initialized before native bootstrap\n"
<< std::flush;
}
EngineStartupContext startup_context_value = {
jni_vm.get(),
raw_vm,
dependencies.account_identity(),
&dependencies.roblox_credential(),
game_session_runtime.get(),
&luau_codegen_switch,
run_prepare_jni,
run_set_asset_path,
call_real_set_asset_path,
run_global_init,
run_init_client_settings,
run_post_client_settings,
run_app_bridge_app_start,
run_native_settings,
run_set_init_params,
run_init_with_params,
call_real_init_with_params,
run_update_screen_orientation,
run_update_surface_app,
call_real_update_surface_app,
run_start_app_with_params,
call_real_start_app_with_params,
run_activity_lifecycle,
run_game_activity_init,
run_game_activity_surface,
run_app_lifecycle_active,
run_native_fragment_start,
run_display_refresh_rate,
run_start_lua_app_dm,
native_global_init,
native_init_client_settings,
native_init_client_settings_signed,
native_init_client_settings_cached,
native_init_client_settings_cached_compressed,
native_post_client_settings,
native_initialize_native_flags,
native_app_bridge_app_start,
native_set_is_first_install,
native_set_base_url,
native_set_device_info,
native_base_url_protocol_init,
native_set_roblox_channel,
native_override_channel_platform_name,
native_set_roblox_version,
native_set_exception_reason_filename,
native_set_base_data_directories,
native_set_cache_directory,
native_set_files_directory,
native_set_external_directory,
native_set_preferences_file,
native_set_default_app_policy_file,
native_set_http_client_proxy,
native_init_fast_log,
native_set_multiple_cookies,
native_cookie_manager_set_cookie,
native_set_platform_headers_with_idfa,
native_set_user_id,
native_init_asset_manager,
native_init_storage_manager,
native_local_storage_set_platform_impl,
native_set_init_params,
native_retry_init,
native_init_with_params,
native_update_adapter_init,
native_update_screen_orientation,
native_update_app_ui_sizes,
native_set_task_scheduler_background_mode,
native_update_surface_app,
native_start_app_with_params,
native_send_app_ready,
native_send_game_loaded,
native_set_asset_path,
activity_lifecycle_callbacks,
native_game_activity_init,
native_app_lifecycle_set_active,
native_on_fragment_start,
native_pass_supported_refresh_rates,
native_pass_current_display_refresh_rate,
native_start_lua_app_dm,
native_update_mobile_haptics_state};
if (game_session_runtime != nullptr) {
if (!game_present_observer.Register(
&mocktail::runtime::RobloxGameSessionRuntime::
SuccessfulPresentCallback,
game_session_runtime.get())) {
std::cerr << "[FATAL] Could not register the GAME host present "
"observer\n";
return EXIT_FAILURE;
}
std::cout << " [game-session] host present observer registered\n"
<< std::flush;
}
if (IsEnabled("MOCKTAIL_ENGINE_DETACH")) {
std::cerr << "[FATAL] Detached startup workers are unsupported because "
"their runtime ownership cannot be proven\n";
return EXIT_FAILURE;
}
EngineStartupContext* startup_context = &startup_context_value;
if (IsEnabled("MOCKTAIL_ENGINE_INLINE")) {
std::cout << " [engine] starting inline startup path\n" << std::flush;
EngineStartupThread(startup_context);
} else {
std::cout << " [engine] starting pthread startup path\n" << std::flush;
std::cout << " [engine] creating startup thread\n" << std::flush;
mocktail::runtime::OwnedPthread startup_thread;
const int create_result = startup_thread.Start(
&EngineStartupThread, startup_context, GetEngineStackSize());
std::cout << " [engine] startup thread create result=" << create_result
<< '\n' << std::flush;
if (create_result != 0) {
std::cerr << "\n[FATAL] Could not create engine startup thread: "
<< create_result << '\n';
return EXIT_FAILURE;
}
const int startup_timeout_ms =
GetEnvInt("MOCKTAIL_STARTUP_THREAD_TIMEOUT_MS", 0);
if (startup_timeout_ms <= 0) {
std::cout << " [engine] joining startup thread\n" << std::flush;
} else {
std::cout << " [engine] joining startup thread with timeout "
<< startup_timeout_ms << "ms\n"
<< std::flush;
}
const mocktail::runtime::OwnedPthreadWaitResult wait_result =
startup_thread.WaitFor(startup_timeout_ms > 0 ? startup_timeout_ms
: -1,
10, &PumpStartupOwnerThread);
if (!wait_result.joined()) {
if (wait_result.status ==
mocktail::runtime::OwnedPthreadWaitStatus::kTimedOut) {
std::cerr << " [engine] startup thread timed out after "
<< startup_timeout_ms
<< "ms, cancelling and requiring a physical join\n"
<< std::flush;
for (int dump_index = 0; dump_index < 3; ++dump_index) {
startup_thread.Signal(SIGUSR1);
usleep(100 * 1000);
}
} else {
std::cerr << " [engine] startup thread join failed: "
<< wait_result.platform_error << '\n'
<< std::flush;
}
const int cancel_join_grace_ms = std::clamp(
GetEnvInt("MOCKTAIL_STARTUP_CANCEL_JOIN_GRACE_MS", 5000), 100,
60000);
const mocktail::runtime::OwnedPthreadCancelResult cancel_result =
startup_thread.CancelAndJoinFor(
cancel_join_grace_ms, 10, &PumpStartupOwnerThread);
if (cancel_result.cancel_error != 0) {
std::cerr << " [engine] startup thread cancel failed: "
<< cancel_result.cancel_error << '\n'
<< std::flush;
}
std::cerr << "[FATAL] Startup worker did not complete normally; "
<< cancel_join_grace_ms << "ms cancel/join grace result: "
<< mocktail::runtime::OwnedPthreadWaitStatusName(
cancel_result.wait.status)
<< " error=" << cancel_result.wait.platform_error
<< ". Native rollback is unproven even after a physical "
"join; terminating without RAII unwind\n"
<< std::flush;
std::_Exit(EXIT_FAILURE);
}
}
if (game_session_runtime != nullptr &&
!game_session_runtime->startup_status().ok()) {
std::cerr << "[FATAL] Typed GAME lifecycle did not start: "
<< game_session_runtime->startup_status().message() << '\n';
return EXIT_FAILURE;
}
if (experience_composition != nullptr &&
dependencies.account_identity().user_id > 0) {
const mocktail::window::WindowSurfaceSnapshot window_surface =
mocktail::window::GetWindowSurfaceSnapshot();
mocktail::runtime::RobloxLuaAppExperienceReadiness readiness;
readiness.principal.kind =
mocktail::runtime::GameSessionPrincipalKind::kAuthenticated;
readiness.principal.generation = 1;
readiness.principal.principal_id =
std::to_string(dependencies.account_identity().user_id);
readiness.principal.base_url = "https://www.roblox.com/";
readiness.surface = {window_surface.generation,
window_surface.native_window, window_surface.width,
window_surface.height, window_surface.dpi_scale};
readiness.username = dependencies.account_identity().username;
const mocktail::Status experience_status =
experience_composition->OnLuaAppReady(std::move(readiness));
if (!experience_status.ok()) {
std::cerr << "[FATAL] Dynamic ExperienceProtocol did not initialize: "
<< experience_status.message() << '\n';
return EXIT_FAILURE;
}
experience_lifecycle_target = std::make_shared<ExperienceLifecycleTarget>(
ExperienceLifecycleTarget{experience_composition});
jni_vm->SetRobloxExperienceLifecycleCallbacks(
experience_lifecycle_target,
jnivm::RobloxExperienceLifecycleCallbacks{&NotifyLuaAppDidReturn});
std::cout << " [experience] subscribed to dynamic launch requests\n"
<< std::flush;
} else if (experience_composition != nullptr) {
std::cout << " [experience] dynamic launch subscription awaits an "
"authenticated identity\n"
<< std::flush;
}
if (mocktail::window::IsInitialised()) {
mocktail::runtime::JniEnvironmentProvider input_environment{
raw_vm, jni_vm.get(), &RestoreGameSessionJniEnvironment};
window_input_runtime =
std::make_shared<mocktail::runtime::RobloxWindowInputRuntime>(
input_environment, roblox_capabilities.input);
window_input_runtime->SetRawMouseEnabled(input_capabilities.raw_mouse);
const mocktail::Status input_status = window_input_runtime->Initialize();
if (!input_status.ok()) {
std::cerr << "[FATAL] Typed production input did not initialize: "
<< input_status.message() << '\n';
return EXIT_FAILURE;
}
const mocktail::Status text_input_status =
mocktail::runtime::RobloxTextInputJniBridge::Create(
jni_vm.get(), window_input_runtime, &text_input_bridge);
if (!text_input_status.ok()) {
std::cerr << "[FATAL] Typed Roblox text input did not initialize: "
<< text_input_status.message() << '\n';
return EXIT_FAILURE;
}
}
std::cout << " [engine] legacy startup call sequence returned\n"
<< std::flush;
}
std::cout << "\n======================================================\n"
<< " Bootstrap sequence returned; validating readiness.\n"
<< "======================================================\n";
bool real_frame_presented = false;
bool lifecycle_shutdown_completed = false;
bool game_surface_events_completed = true;
bool input_shutdown_completed = window_input_runtime == nullptr;
bool resize_readiness_completed = true;
if (mocktail::window::IsInitialised()) {
std::cout << " [main] engine pump rest: "
<< mocktail::runtime::EnginePumpRestModeName(
mocktail::runtime::ActiveEnginePumpRestMode())
<< " (MOCKTAIL_ENGINE_PUMP_REST=sleep|tpause|spin)\n";
std::cout << " [main] entering SDL event loop (close the window to quit)\n"
<< std::flush;
std::unique_ptr<mocktail::window::WindowGameSurfaceBridge> surface_bridge;
if (game_session_runtime != nullptr) {
surface_bridge =
std::make_unique<mocktail::window::WindowGameSurfaceBridge>(
mocktail::window::MakeWindowGameSurfaceEventSource(),
mocktail::window::MakeWindowGameSurfaceConsumer(
game_session_runtime.get()),
mocktail::window::MakeWindowResizeReadinessCommitObserver());
} else if (experience_composition != nullptr &&
experience_composition->subscribed()) {
mocktail::window::WindowGameSurfaceConsumer experience_consumer{
experience_composition.get(), &ExperienceSurfaceCreated,
&ExperienceSurfaceChanged, &ExperienceSurfaceDestroyed};
surface_bridge =
std::make_unique<mocktail::window::WindowGameSurfaceBridge>(
mocktail::window::MakeWindowGameSurfaceEventSource(),
experience_consumer,
mocktail::window::MakeWindowResizeReadinessCommitObserver());
}
const auto drain_game_surface_events = [&]() {
if (surface_bridge == nullptr) {
return true;
}
const mocktail::window::WindowGameSurfaceDrainResult result =
surface_bridge->Drain();
if (!result.ok()) {
std::cerr << "[FATAL] Typed GAME surface bridge rejected an event: "
<< result.status.message() << '\n';
return false;
}
if (result.drained_events != 0) {
const mocktail::window::WindowGameSurfaceBridgeSnapshot snapshot =
surface_bridge->Snapshot();
std::cout << " [game-session] typed window surface events committed="
<< result.drained_events
<< " last_generation=" << snapshot.last_generation << '\n'
<< std::flush;
}
return true;
};
int main_loop_ticks = 0;
const bool fps_trace = IsEnabled("MOCKTAIL_TRACE_FPS");
const bool trace_main_loop = IsEnabled("MOCKTAIL_TRACE_MAIN_LOOP");
uint64_t fps_window_start_ns = fps_trace ? MonotonicNanos() : 0;
uint64_t fps_samples = 0;
uint64_t fps_tick_ns = 0;
uint64_t fps_sdl_ns = 0;
uint64_t fps_platform_ns = 0;
uint64_t fps_engine_ns = 0;
uint64_t fps_launch_ns = 0;
uint64_t fps_pace_sleep_ns = 0;
while (true) {
const uint64_t fps_tick_start_ns = fps_trace ? MonotonicNanos() : 0;
const bool keep_window_open = mocktail::window::PumpEvents();
// Deliver XR tracked-controller input on the same thread and through the
// same router as SDL input, once per loop iteration after the SDL pump.
// Inert in non-VR builds or while no OpenXR controller layer is active.
if (window_input_runtime != nullptr) {
window_input_runtime->DrainXrControllers();
}
game_surface_events_completed = drain_game_surface_events();
const uint64_t fps_after_sdl_ns = fps_trace ? MonotonicNanos() : 0;
if (!game_surface_events_completed || !keep_window_open) {
break;
}
++main_loop_ticks;
if (trace_main_loop &&
(main_loop_ticks <= 10 || main_loop_ticks % 100 == 0)) {
std::cerr << " [main] SDL event loop tick #" << main_loop_ticks
<< '\n'
<< std::flush;
}
RunPendingMainThreadTaskSchedulerForeground();
if (g_pending_main_thread_start_lua_app_dm &&
!g_pending_main_thread_start_lua_started &&
g_vm_for_main_thread_pump &&
g_native_gl_class_for_main_thread &&
MonotonicMillis() >= g_pending_main_thread_start_lua_due_ms) {
g_pending_main_thread_start_lua_started = true;
JNIEnv* env = AttachMainThreadJniEnv();
if (env == nullptr) {
continue;
}
if (IsEnabled("MOCKTAIL_TRACE_START_LUA_JNI")) {
setenv("MOCKTAIL_JNI_TRACE", "1", 1);
}
std::cout << " [main] delayed nativeAppBridgeStartLuaAppDM\n"
<< std::flush;
g_pending_main_thread_start_lua_app_dm(
env, g_native_gl_class_for_main_thread);
std::cout << " [main] delayed nativeAppBridgeStartLuaAppDM returned\n"
<< std::flush;
}
if (experience_composition != nullptr) {
const mocktail::Status platform_event_status =
experience_composition->DrainPlatformEvents();
if (!platform_event_status.ok()) {
std::cerr << "[FATAL] Roblox platform event delivery failed: "
<< platform_event_status.message() << '\n';
game_surface_events_completed = false;
break;
}
}
const uint64_t fps_after_platform_ns =
fps_trace ? MonotonicNanos() : 0;
PumpRobloxMainThreadMessagesOnce();
const uint64_t fps_after_engine_ns = fps_trace ? MonotonicNanos() : 0;
if (experience_composition != nullptr &&
experience_composition->subscribed()) {
const mocktail::Status launch_status =
experience_composition->DrainLaunchRequests();
if (!launch_status.ok()) {
std::cerr << "[FATAL] Dynamic experience launch failed: "
<< launch_status.message() << '\n';
game_surface_events_completed = false;
break;
}
}
const uint64_t fps_after_launch_ns = fps_trace ? MonotonicNanos() : 0;
// The pacer sleeps only under throttled presentation. Unthrottled, the
// engine pump above is a poll, and the rest between polls is what keeps
// it from spinning.
uint64_t fps_pace_ns = mocktail::window::PaceInputPump();
if (fps_pace_ns == 0 &&
mocktail::window::UnthrottledPresentationRequested()) {
fps_pace_ns = mocktail::runtime::RestAfterEnginePump();
}
if (fps_trace) {
const uint64_t fps_tick_end_ns = MonotonicNanos();
++fps_samples;
fps_tick_ns += fps_tick_end_ns - fps_tick_start_ns;
fps_sdl_ns += fps_after_sdl_ns - fps_tick_start_ns;
fps_platform_ns += fps_after_platform_ns - fps_after_sdl_ns;
fps_engine_ns += fps_after_engine_ns - fps_after_platform_ns;
fps_launch_ns += fps_after_launch_ns - fps_after_engine_ns;
fps_pace_sleep_ns += fps_pace_ns;
if (fps_tick_end_ns - fps_window_start_ns >= 1000000000ULL &&
fps_samples != 0) {
std::fprintf(stderr,
" [fps] main n=%llu tick=%llu us sdl=%llu us "
"drain=%llu us engine=%llu us launch=%llu us "
"pace_sleep=%llu us\n",
static_cast<unsigned long long>(fps_samples),
static_cast<unsigned long long>(fps_tick_ns /
fps_samples / 1000ULL),
static_cast<unsigned long long>(fps_sdl_ns /
fps_samples / 1000ULL),
static_cast<unsigned long long>(fps_platform_ns /
fps_samples / 1000ULL),
static_cast<unsigned long long>(fps_engine_ns /
fps_samples / 1000ULL),
static_cast<unsigned long long>(fps_launch_ns /
fps_samples / 1000ULL),
static_cast<unsigned long long>(fps_pace_sleep_ns /
fps_samples / 1000ULL));
fps_window_start_ns = fps_tick_end_ns;
fps_samples = 0;
fps_tick_ns = 0;
fps_sdl_ns = 0;
fps_platform_ns = 0;
fps_engine_ns = 0;
fps_launch_ns = 0;
fps_pace_sleep_ns = 0;
}
}
}
std::cout << " [main] window closed, shutting down\n" << std::flush;
real_frame_presented = mocktail::window::HasPresentedFrame();
if (text_input_bridge != nullptr) {
input_shutdown_completed = text_input_bridge->Shutdown().ok();
}
if (window_input_runtime != nullptr) {
input_shutdown_completed = window_input_runtime->Shutdown().ok() &&
input_shutdown_completed;
}
if (eager_haptic_protocol_bridge != nullptr) {
(void)eager_haptic_protocol_bridge->Shutdown();
eager_haptic_protocol_bridge.reset();
}
if (eager_call_protocol_bridge != nullptr) {
(void)eager_call_protocol_bridge->Shutdown();
eager_call_protocol_bridge.reset();
}
if (eager_permissions_bridge != nullptr) {
(void)eager_permissions_bridge->Shutdown();
eager_permissions_bridge.reset();
}
bool experience_destroyed_app = false;
if (experience_composition != nullptr) {
jni_vm->ClearRobloxExperienceLifecycleCallbacks();
experience_lifecycle_target.reset();
const mocktail::runtime::GameSessionSnapshot snapshot =
experience_composition->Snapshot();
experience_destroyed_app =
snapshot.game_running || snapshot.game_paused ||
snapshot.game_present_pending;
const mocktail::Status experience_shutdown =
experience_composition->Shutdown();
if (experience_destroyed_app) {
lifecycle_shutdown_completed = experience_shutdown.ok();
}
if (!experience_shutdown.ok()) {
std::cerr << " [main] ExperienceProtocol shutdown failed: "
<< experience_shutdown.message() << '\n';
}
}
if (game_session_runtime != nullptr) {
const mocktail::runtime::GameSessionUpdateResult shutdown_result =
game_session_runtime->Shutdown();
lifecycle_shutdown_completed =
shutdown_result.ok() &&
shutdown_result.state == mocktail::runtime::GameSessionState::kStopped;
std::cout << " [main] Roblox lifecycle shutdown: "
<< mocktail::runtime::GameSessionStateName(shutdown_result.state)
<< " (" << shutdown_result.message << ")\n"
<< std::flush;
game_present_observer.Reset();
} else if (!experience_destroyed_app) {
const mocktail::runtime::RobloxSymbolLookup lifecycle_lookup(
&ResolveRobloxCapabilitySymbol, &roblox_handle);
const mocktail::runtime::RobloxAppLifecycleResolution
lifecycle_resolution =
mocktail::runtime::ResolveRobloxAppLifecycleSymbols(
lifecycle_lookup);
if (lifecycle_resolution.ok()) {
JNIEnv* shutdown_env = AttachMainThreadJniEnv();
mocktail::runtime::RobloxAppLifecycle lifecycle(
*lifecycle_resolution.symbols());
const mocktail::runtime::RobloxAppShutdownResult shutdown_result =
lifecycle.Shutdown(shutdown_env,
g_native_gl_class_for_main_thread);
lifecycle_shutdown_completed = shutdown_result.ok();
std::cout << " [main] Roblox lifecycle shutdown: "
<< mocktail::runtime::RobloxAppShutdownStatusName(
shutdown_result.status)
<< " (" << shutdown_result.message << ")\n"
<< std::flush;
} else {
std::cerr << " [main] Roblox lifecycle resolution failed: "
<< mocktail::runtime::RobloxAppLifecycleResolutionStatusName(
lifecycle_resolution.status())
<< '\n'
<< std::flush;
}
}
const mocktail::Status resize_stop_status =
lifecycle_shutdown_completed
? mocktail::window::StopResizeReadiness()
: mocktail::window::ResizeReadinessCompletionStatus();
const mocktail::Status resize_completion_status =
mocktail::window::ResizeReadinessCompletionStatus();
resize_readiness_completed =
resize_stop_status.ok() && resize_completion_status.ok();
if (!resize_readiness_completed) {
const mocktail::Status& failure =
resize_stop_status.ok() ? resize_completion_status
: resize_stop_status;
std::cerr << "[FATAL] Real resize readiness failed: "
<< failure.message() << '\n';
}
const mocktail::Status audio_shutdown_status =
dependencies.ShutdownBeforePlatform();
if (!audio_shutdown_status.ok()) {
std::cerr << " [main] audio shutdown blocked SDL teardown: "
<< audio_shutdown_status.message() << '\n';
lifecycle_shutdown_completed = false;
} else {
mocktail::window::Shutdown();
}
} else {
int keepalive_ms = GetEnvInt("MOCKTAIL_KEEPALIVE_MS", 0);
if (keepalive_ms > 0) {
std::cout << " [main] headless keepalive: " << keepalive_ms << " ms\n"
<< std::flush;
if (IsEnabled("MOCKTAIL_MAIN_THREAD_MESSAGE_PUMP") ||
IsEnabled("MOCKTAIL_HEADLESS_KEEPALIVE_PUMP")) {
int waited_ms = 0;
constexpr int kHeadlessKeepalivePumpIntervalMs = 10;
while (waited_ms < keepalive_ms) {
RunPendingMainThreadTaskSchedulerForeground();
PumpRobloxMainThreadMessagesOnce();
const int chunk_ms = std::min(kHeadlessKeepalivePumpIntervalMs,
keepalive_ms - waited_ms);
usleep(static_cast<useconds_t>(chunk_ms) * 1000);
waited_ms += chunk_ms;
}
} else {
usleep(static_cast<useconds_t>(keepalive_ms) * 1000);
}
std::cout << " [main] headless keepalive returned\n" << std::flush;
} else if (IsEnabled("MOCKTAIL_KEEPALIVE")) {
std::cout << " [main] headless keepalive: forever\n" << std::flush;
while (true) {
pause();
}
}
}
if (!build_profile.default_allowed) {
std::cerr
<< "[FATAL] Compatibility run returned, but Build ID "
<< build_profile.elf_build_id
<< " has not passed the readiness gates; refusing a success exit.\n";
return EXIT_FAILURE;
}
if (is_headless) {
std::cerr << "[FATAL] Headless LuaApp readiness evidence is not wired into "
"the supported runtime yet.\n";
return EXIT_FAILURE;
}
if (!real_frame_presented) {
std::cerr << "[FATAL] Windowed runtime exited without a real presented "
"frame.\n";
return EXIT_FAILURE;
}
if (!lifecycle_shutdown_completed) {
std::cerr << "[FATAL] Windowed runtime exited without completing the "
"Roblox lifecycle shutdown.\n";
return EXIT_FAILURE;
}
if (!game_surface_events_completed) {
std::cerr << "[FATAL] Windowed runtime rejected a typed GAME surface "
"lifecycle event.\n";
return EXIT_FAILURE;
}
if (!input_shutdown_completed) {
std::cerr << "[FATAL] Windowed runtime exited without completing typed input shutdown.\n";
return EXIT_FAILURE;
}
if (!resize_readiness_completed) {
std::cerr << "[FATAL] Windowed runtime exited without completing the real "
"resize/rebind readiness sequence.\n";
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}