mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-23 09:00:54 +02:00
Release the GameMode request when the daemon narrows the CPU mask; the pin concentrates Roblox's worker pool and is re-applied every few seconds, so ending the request is the only thing that lifts it. `gamemode: on` keeps it. Emit texture uploads on worker threads instead of serially inside vkQueueSubmit, in two passes so mip 0 lands before the levels scaled from it. Decode ETC2 through a persistent pool and lower the parallel floor to one 128x128 image, so small batches stop running single-threaded. Document all three in docs/PERFORMANCE.md.
1152 lines
51 KiB
C++
1152 lines
51 KiB
C++
// Modified by vii from komaruworld/mocktail. See README "About this fork".
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <condition_variable>
|
|
#include <cstddef>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <iostream>
|
|
#include <memory>
|
|
#include <mutex>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <thread>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "compat/elf_build_id.h"
|
|
#include "compat/host_abi_profile.h"
|
|
#include "compat/payload_compatibility.h"
|
|
#include "legacy/legacy_runtime.h"
|
|
#include "libc_shim/libc_shim.h"
|
|
#include "mocktail/audio/fmod_jni_audio_bridge.h"
|
|
#include "mocktail/audio/webrtc_jni_audio_bridge.h"
|
|
#include "mocktail/audio/roblox_output_device_bridge.h"
|
|
#include "runtime/auth_runtime_composition.h"
|
|
#include "runtime/command_line.h"
|
|
#include "runtime/crash_report_policy.h"
|
|
#include "runtime/environment.h"
|
|
#include "runtime/external_launch_broker.h"
|
|
#include "runtime/failure_dialog.h"
|
|
#include "runtime/fleasion.h"
|
|
#include "runtime/game_mode.h"
|
|
#include "runtime/graphics_launch_policy.h"
|
|
#include "runtime/memory_limit.h"
|
|
#include "runtime/payload_update_preflight.h"
|
|
#include "runtime/performance_policy.h"
|
|
#include "runtime/platform_cache_migration.h"
|
|
#include "runtime/process_diagnostics.h"
|
|
#include "runtime/process_launch_policy.h"
|
|
#include "runtime/roblox_desktop_app_policy.h"
|
|
#include "runtime/roblox_experience_launch_bridge.h"
|
|
#include "runtime/roblox_fullscreen_runtime_bridge.h"
|
|
#include "runtime/runtime_config_bootstrap.h"
|
|
#include "runtime/runtime_config_file.h"
|
|
#include "runtime/runtime_paths.h"
|
|
#include "runtime/session_log.h"
|
|
#include "runtime/single_instance_lock.h"
|
|
#include "runtime/support_bundle.h"
|
|
#include "runtime/supported_launch_policy.h"
|
|
#include "runtime/system_proxy.h"
|
|
#include "runtime/webview_helper_launcher.h"
|
|
#include "services/auth_service.h"
|
|
#include "services/browser_tracker_service.h"
|
|
#include "services/client_settings_service.h"
|
|
#include "services/http_client.h"
|
|
#include "window/window.h"
|
|
|
|
namespace {
|
|
|
|
struct AndroidWindowBridgeContext {};
|
|
|
|
class ExternalLaunchBrokerScope final {
|
|
public:
|
|
~ExternalLaunchBrokerScope() { (void)Shutdown(); }
|
|
|
|
std::shared_ptr<mocktail::runtime::ExternalLaunchBroker>& broker() {
|
|
return broker_;
|
|
}
|
|
|
|
mocktail::Status Shutdown() {
|
|
if (broker_ == nullptr) {
|
|
return mocktail::Status::Ok();
|
|
}
|
|
mocktail::runtime::ClearActiveExternalLaunchBroker(broker_.get());
|
|
mocktail::Status status = broker_->Shutdown();
|
|
broker_.reset();
|
|
return status;
|
|
}
|
|
|
|
private:
|
|
std::shared_ptr<mocktail::runtime::ExternalLaunchBroker> broker_;
|
|
};
|
|
|
|
void SecureErase(char* data, std::size_t size) {
|
|
volatile char* bytes = data;
|
|
for (std::size_t index = 0; bytes != nullptr && index < size; ++index) {
|
|
bytes[index] = '\0';
|
|
}
|
|
}
|
|
|
|
void SecureEraseArguments(std::vector<std::string>* arguments) {
|
|
if (arguments == nullptr) return;
|
|
for (std::string& argument : *arguments) {
|
|
SecureErase(argument.data(), argument.size());
|
|
}
|
|
arguments->clear();
|
|
}
|
|
|
|
bool QueueAndroidWindowFlags(void*, int flags, int mask) {
|
|
return mocktail::window::RequestFullscreenFromAndroidWindowFlags(flags, mask);
|
|
}
|
|
|
|
mocktail::Status ShutdownPlatformBridges(jnivm::VM* vm) {
|
|
if (vm == nullptr) {
|
|
return mocktail::Status::Error(mocktail::StatusCode::kInvalidArgument,
|
|
"platform bridge shutdown requires a VM");
|
|
}
|
|
vm->ClearAndroidWindowCallbacks();
|
|
const mocktail::Status voice_status =
|
|
mocktail::audio::ShutdownWebRtcJniAudioBridge(vm);
|
|
const mocktail::Status playback_status =
|
|
mocktail::audio::ShutdownFmodJniAudioBridge(vm);
|
|
return !voice_status.ok() ? voice_status : playback_status;
|
|
}
|
|
|
|
void PromptFirstLaunchSignIn(
|
|
const mocktail::runtime::ProcessEnvironment& environment,
|
|
const mocktail::runtime::RuntimePaths& paths,
|
|
mocktail::services::AuthService& auth_service,
|
|
const std::shared_ptr<mocktail::services::HttpClient>& http_client,
|
|
mocktail::runtime::AuthRuntimeComposition* composition) {
|
|
if (composition == nullptr ||
|
|
composition->status != mocktail::runtime::AuthRuntimeStatus::kGuest ||
|
|
environment.Get("MOCKTAIL_GUEST") == "1" ||
|
|
environment.Get("MOCKTAIL_SKIP_FIRST_LAUNCH_LOGIN") == "1") {
|
|
return;
|
|
}
|
|
if (environment.Get("MOCKTAIL_NATIVE_LOGIN") != "0") {
|
|
std::cout << " [auth] native sign-in selected; opening Roblox welcome screen\n";
|
|
return;
|
|
}
|
|
|
|
std::filesystem::path helper;
|
|
const char* helper_override = std::getenv("MOCKTAIL_WEBVIEW_HELPER");
|
|
if (helper_override != nullptr && helper_override[0] != '\0') {
|
|
helper = helper_override;
|
|
} else {
|
|
std::error_code error;
|
|
const std::filesystem::path executable =
|
|
std::filesystem::read_symlink("/proc/self/exe", error);
|
|
if (!error && !executable.empty()) {
|
|
helper = executable.parent_path() / "mocktail_webview_helper";
|
|
}
|
|
}
|
|
if (helper.empty() || !std::filesystem::exists(helper)) {
|
|
return;
|
|
}
|
|
|
|
std::cout << "\n======================================================\n"
|
|
<< " First-Time Setup: Roblox Sign-In\n"
|
|
<< "======================================================\n"
|
|
<< " [auth] no saved session found; opening desktop sign-in window...\n"
|
|
<< " [auth] tip: you can use Quick Log In (QR code) or username/password\n"
|
|
<< " [auth] (close the sign-in window to play as guest)\n\n";
|
|
|
|
struct FirstLaunchContext {
|
|
std::mutex mutex;
|
|
std::string captured_cookie;
|
|
bool finished = false;
|
|
};
|
|
auto context = std::make_shared<FirstLaunchContext>();
|
|
|
|
mocktail::runtime::WebViewHelperExitObserver exit_observer;
|
|
exit_observer.context = context;
|
|
exit_observer.on_exit = [](void* ctx) {
|
|
auto* c = static_cast<FirstLaunchContext*>(ctx);
|
|
std::lock_guard<std::mutex> lock(c->mutex);
|
|
c->finished = true;
|
|
};
|
|
|
|
const auto launched = mocktail::runtime::LaunchWebViewHelper(
|
|
helper, "https://www.roblox.com/login", exit_observer);
|
|
if (!launched || launched.process == nullptr) {
|
|
return;
|
|
}
|
|
if (!launched.process->WaitUntilReady(std::chrono::milliseconds(5000))) {
|
|
(void)launched.process->RequestClose();
|
|
return;
|
|
}
|
|
(void)launched.process->SetRobloxCookie("");
|
|
(void)launched.process->SetTitle("Roblox sign in");
|
|
(void)launched.process->SetVisible(true);
|
|
|
|
std::vector<mocktail::runtime::WebViewHelperEvent> events;
|
|
while (true) {
|
|
{
|
|
std::lock_guard<std::mutex> lock(context->mutex);
|
|
if (context->finished) {
|
|
break;
|
|
}
|
|
}
|
|
if (launched.process->DrainEvents(&events)) {
|
|
for (const auto& event : events) {
|
|
if (event.type ==
|
|
mocktail::runtime::WebViewHelperEventType::kRobloxCookie) {
|
|
std::lock_guard<std::mutex> lock(context->mutex);
|
|
context->captured_cookie = event.payload;
|
|
(void)launched.process->RequestClose();
|
|
break;
|
|
}
|
|
}
|
|
events.clear();
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(30));
|
|
}
|
|
|
|
if (!context->captured_cookie.empty()) {
|
|
if (mocktail::runtime::PersistRobloxCookie(paths.cookie_file(),
|
|
context->captured_cookie)) {
|
|
mocktail::runtime::AuthRuntimeComposition new_comp =
|
|
mocktail::runtime::ComposeAuthRuntime(environment, paths,
|
|
auth_service, http_client);
|
|
if (new_comp.status ==
|
|
mocktail::runtime::AuthRuntimeStatus::kAuthenticated) {
|
|
*composition = std::move(new_comp);
|
|
std::cout << " [auth] desktop sign-in successful; launching authenticated session\n";
|
|
}
|
|
}
|
|
} else {
|
|
std::cout << " [auth] desktop sign-in window closed; continuing as guest\n";
|
|
}
|
|
}
|
|
|
|
void ConfigureHostDriverEnvironment() {
|
|
auto set_if_unset = [](const char* name, const char* value) {
|
|
if (std::getenv(name) == nullptr) {
|
|
setenv(name, value, 1);
|
|
}
|
|
};
|
|
// Bypass X11 compositor redirection to eliminate presentation latency on X11/XWayland.
|
|
set_if_unset("SDL_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR", "1");
|
|
// Allow Adaptive Sync / Variable Refresh Rate (G-Sync/FreeSync) where supported.
|
|
set_if_unset("__GL_VRR_ALLOWED", "1");
|
|
// Expand shader disk cache to 2GB so compiled Vulkan/GL shaders are preserved across runs.
|
|
set_if_unset("__GL_SHADER_DISK_CACHE", "1");
|
|
set_if_unset("__GL_SHADER_DISK_CACHE_SIZE", "2147483648");
|
|
// Avoid busy-spin vblank waits on NVIDIA Linux driver.
|
|
set_if_unset("__GL_YIELD", "USLEEP");
|
|
set_if_unset("__GL_THREADED_OPTIMIZATIONS", "1");
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char* argv[]) {
|
|
ConfigureHostDriverEnvironment();
|
|
const auto process_started_at = std::chrono::system_clock::now();
|
|
mocktail::runtime::CommandLineParseResult command_line =
|
|
mocktail::runtime::ParseCommandLine(argc, argv);
|
|
if (!command_line) {
|
|
std::cerr << command_line.error << "\n\n"
|
|
<< mocktail::runtime::CommandLineUsage(
|
|
command_line.options.program_name);
|
|
return EXIT_FAILURE;
|
|
}
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kHelp) {
|
|
std::cout << mocktail::runtime::CommandLineUsage(
|
|
command_line.options.program_name);
|
|
return EXIT_SUCCESS;
|
|
}
|
|
// Normalize inherited process state before bootstrap or helpers can create
|
|
// threads. Keep research/canary resource limits under their caller's control.
|
|
const std::string process_launch_diagnostics =
|
|
command_line.options.mode == mocktail::runtime::CommandMode::kRun
|
|
? mocktail::runtime::ApplyInteractiveProcessLaunchPolicy()
|
|
: std::string{};
|
|
std::string command_line_error;
|
|
if (!mocktail::runtime::ApplySupportedLaunchPolicy(
|
|
command_line.options.mode == mocktail::runtime::CommandMode::kRun,
|
|
&command_line_error)) {
|
|
std::cerr << "[FATAL] " << command_line_error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
if (!mocktail::runtime::ApplyCommandLineEnvironment(command_line.options,
|
|
&command_line_error)) {
|
|
std::cerr << command_line_error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
|
|
const mocktail::runtime::ProcessEnvironment environment;
|
|
const std::string built_in_compatibility_manifest = environment.GetOr(
|
|
"MOCKTAIL_UPDATE_COMPATIBILITY_PATH",
|
|
environment.GetOr("MOCKTAIL_COMPATIBILITY_MANIFEST",
|
|
MOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST));
|
|
const mocktail::runtime::RuntimePaths paths =
|
|
mocktail::runtime::RuntimePaths::FromEnvironment(environment);
|
|
if (command_line.options.force_run_latest) {
|
|
std::cerr << "[runtime] WARNING: latest Roblox will run once without "
|
|
"approval and will not become the active payload\n";
|
|
const mocktail::runtime::PayloadUpdatePreflightResult force_latest =
|
|
mocktail::runtime::RunPayloadUpdatePreflight(environment, paths, true);
|
|
if (!force_latest) {
|
|
std::cerr << "[FATAL] " << force_latest.error;
|
|
if (!force_latest.details.empty()) {
|
|
std::cerr << ": " << force_latest.details;
|
|
}
|
|
std::cerr << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
return EXIT_SUCCESS;
|
|
}
|
|
std::optional<mocktail::runtime::RobloxExperienceLaunchRequest>
|
|
external_launch_request;
|
|
if (!command_line.options.launch_request_json.empty()) {
|
|
external_launch_request.emplace();
|
|
const mocktail::Status launch_status =
|
|
mocktail::runtime::ParseRobloxExperienceLaunchJson(
|
|
command_line.options.launch_request_json,
|
|
&*external_launch_request);
|
|
if (!launch_status.ok()) {
|
|
std::cerr << "[FATAL] Invalid controlled website launch request\n";
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
std::vector<std::string> cgroup_reexec_arguments;
|
|
if (!mocktail::runtime::BuildCommandLineReexecArguments(
|
|
command_line.options, argc, argv, &cgroup_reexec_arguments,
|
|
&command_line_error)) {
|
|
std::cerr << "[FATAL] " << command_line_error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
mocktail::runtime::ScrubCommandLineLaunchArguments(&command_line.options,
|
|
argc, argv);
|
|
mocktail::runtime::SessionLog session_log;
|
|
mocktail::runtime::FailureSupportBundleGuard support_bundle_guard(
|
|
environment, paths,
|
|
command_line.options.mode == mocktail::runtime::CommandMode::kRun);
|
|
std::optional<mocktail::runtime::SingleInstanceLock> instance_lock;
|
|
ExternalLaunchBrokerScope external_launch_broker;
|
|
mocktail::runtime::ExternalLaunchBrokerOptions broker_options;
|
|
const bool isolated_canary =
|
|
environment.GetOr("MOCKTAIL_ISOLATED_CANARY", "0") == "1";
|
|
if (isolated_canary && external_launch_request.has_value()) {
|
|
std::cerr << "[FATAL] Website launches are unavailable inside isolated "
|
|
"canary\n";
|
|
return EXIT_FAILURE;
|
|
}
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
|
instance_lock.emplace(
|
|
mocktail::runtime::SingleInstanceLock::AcquireForLaunch(environment,
|
|
paths));
|
|
if (!instance_lock->acquired()) {
|
|
if (!isolated_canary && instance_lock->already_running() &&
|
|
external_launch_request.has_value()) {
|
|
const mocktail::Status forward_status =
|
|
mocktail::runtime::ExternalLaunchBroker::ForwardToOwner(
|
|
broker_options, *external_launch_request);
|
|
if (forward_status.ok()) {
|
|
std::cout << " [launch] sent website join to running Mocktail\n";
|
|
support_bundle_guard.SetExitCode(EXIT_SUCCESS);
|
|
return EXIT_SUCCESS;
|
|
}
|
|
std::cerr << "[FATAL] Cannot send website join to running Mocktail: "
|
|
<< forward_status.message() << '\n';
|
|
(void)mocktail::runtime::ShowFailureDialog(
|
|
environment,
|
|
"Mocktail is already running, but the requested experience "
|
|
"could not be sent to it.");
|
|
} else if (instance_lock->already_running()) {
|
|
std::cerr << "[FATAL] Mocktail is already running for this user\n";
|
|
(void)mocktail::runtime::ShowFailureDialog(
|
|
environment, "An instance of Mocktail is already running.");
|
|
} else {
|
|
std::cerr << "[FATAL] " << instance_lock->error() << '\n';
|
|
(void)mocktail::runtime::ShowFailureDialog(
|
|
environment, "Mocktail could not acquire its launch lock.");
|
|
}
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
const mocktail::runtime::RuntimeConfigBootstrapResult config_bootstrap =
|
|
mocktail::runtime::EnsureRuntimeConfigFile(paths.config_file());
|
|
if (!config_bootstrap) {
|
|
std::cerr << "[FATAL] Cannot prepare " << paths.config_file() << ": "
|
|
<< config_bootstrap.error << '\n';
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
|
(void)mocktail::runtime::ShowFailureDialog(
|
|
environment, "Mocktail could not prepare its configuration.");
|
|
}
|
|
return EXIT_FAILURE;
|
|
}
|
|
mocktail::runtime::RuntimeConfigLoadResult runtime_config =
|
|
mocktail::runtime::LoadRuntimeConfig(environment, paths.config_file());
|
|
if (!runtime_config) {
|
|
std::cerr << "[FATAL] Cannot load " << paths.config_file() << ": "
|
|
<< runtime_config.error << '\n';
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
|
(void)mocktail::runtime::ShowFailureDialog(
|
|
environment, "Mocktail could not load its configuration.");
|
|
}
|
|
return EXIT_FAILURE;
|
|
}
|
|
if (runtime_config.config.use_system_proxy()) {
|
|
const mocktail::runtime::SystemProxyResult system_proxy =
|
|
mocktail::runtime::ResolveSystemProxy();
|
|
const std::string proxy_host = system_proxy.proxy.has_value()
|
|
? system_proxy.proxy->host
|
|
: std::string();
|
|
const std::string proxy_port =
|
|
system_proxy.proxy.has_value()
|
|
? std::to_string(system_proxy.proxy->port)
|
|
: std::string();
|
|
const std::string proxy_scheme = system_proxy.proxy.has_value()
|
|
? system_proxy.proxy->scheme
|
|
: std::string();
|
|
if (!system_proxy ||
|
|
setenv("MOCKTAIL_HTTP_PROXY_HOST", proxy_host.c_str(), 1) != 0 ||
|
|
setenv("MOCKTAIL_HTTP_PROXY_PORT", proxy_port.c_str(), 1) != 0 ||
|
|
setenv("MOCKTAIL_HTTP_PROXY_SCHEME", proxy_scheme.c_str(), 1) != 0) {
|
|
std::cerr << "[FATAL] Cannot resolve host system proxy";
|
|
if (!system_proxy.error.empty()) {
|
|
std::cerr << ": " << system_proxy.error;
|
|
}
|
|
std::cerr << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
runtime_config = mocktail::runtime::LoadRuntimeConfig(
|
|
environment, paths.config_file());
|
|
if (!runtime_config) {
|
|
std::cerr << "[FATAL] Cannot apply host system proxy: "
|
|
<< runtime_config.error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun &&
|
|
runtime_config.config.has_unsafe_detached_thread_overrides()) {
|
|
std::cerr << "[FATAL] Unsupported detached legacy thread overrides:\n";
|
|
for (const std::string& name :
|
|
runtime_config.config.unsafe_detached_thread_overrides()) {
|
|
std::cerr << " - " << name << '\n';
|
|
}
|
|
std::cerr << " Supported runtime requires synchronous or owned worker "
|
|
"execution.\n";
|
|
(void)mocktail::runtime::ShowFailureDialog(
|
|
environment,
|
|
"Mocktail cannot start with unsupported legacy thread "
|
|
"overrides enabled.");
|
|
return EXIT_FAILURE;
|
|
}
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun &&
|
|
!mocktail::runtime::ApplyGraphicsLaunchPolicy(runtime_config.config,
|
|
&command_line_error)) {
|
|
std::cerr << "[FATAL] " << command_line_error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
mocktail::runtime::FailureDialogMonitor failure_dialog;
|
|
mocktail::runtime::MemoryLimitWatchdog memory_limit_watchdog;
|
|
const bool use_memory_limit =
|
|
command_line.options.mode == mocktail::runtime::CommandMode::kRun &&
|
|
runtime_config.config.performance().memory_limit_enabled();
|
|
const std::uint64_t memory_limit_bytes =
|
|
use_memory_limit
|
|
? runtime_config.config.performance().memory_limit_bytes()
|
|
: 0;
|
|
mocktail::runtime::CgroupMemoryLimitResult cgroup_limit;
|
|
if (use_memory_limit) {
|
|
cgroup_limit = mocktail::runtime::MaybeReexecWithCgroupMemoryLimit(
|
|
argc, argv, memory_limit_bytes, &cgroup_reexec_arguments);
|
|
}
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
|
session_log = mocktail::runtime::SessionLog::Start(
|
|
environment, paths, process_started_at);
|
|
if (session_log) {
|
|
std::cout << session_log.Header(
|
|
environment, paths,
|
|
runtime_config.config.graphics_backend_name())
|
|
<< std::flush;
|
|
} else if (session_log.attempted()) {
|
|
std::cerr << " [session] automatic logging unavailable: "
|
|
<< session_log.error() << '\n';
|
|
}
|
|
std::cout << process_launch_diagnostics << std::flush;
|
|
mocktail::runtime::InstallCpuLimitDiagnostics();
|
|
mocktail::runtime::LogProcessDiagnostics(
|
|
mocktail::runtime::ProcessDiagnosticStage::kStartup);
|
|
}
|
|
if (runtime_config.config.fleasion_enabled()) {
|
|
const auto fleasion = mocktail::runtime::PrepareFleasion(
|
|
runtime_config.config, environment, paths);
|
|
if (!fleasion ||
|
|
setenv("MOCKTAIL_CA_BUNDLE", fleasion.bundle.c_str(), 1) != 0 ||
|
|
setenv("MOCKTAIL_FLEASION_BASE_CA_BUNDLE", fleasion.base_bundle.c_str(), 1) != 0 ||
|
|
setenv("MOCKTAIL_FLEASION_GENERATED_BUNDLE", fleasion.bundle.c_str(), 1) != 0 ||
|
|
setenv("MOCKTAIL_FLEASION_CA_CERTIFICATE", fleasion.certificate.c_str(), 1) != 0) {
|
|
std::cerr << "[FATAL] Cannot prepare Fleasion: "
|
|
<< (!fleasion ? fleasion.error : "cannot export certificate paths") << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
if (const auto& proxy = runtime_config.config.network_proxy(); proxy &&
|
|
(setenv("MOCKTAIL_HTTP_PROXY_HOST", proxy->host.c_str(), 1) != 0 ||
|
|
setenv("MOCKTAIL_HTTP_PROXY_PORT", std::to_string(proxy->port).c_str(), 1) != 0 ||
|
|
setenv("MOCKTAIL_HTTP_PROXY_SCHEME", proxy->scheme.c_str(), 1) != 0)) {
|
|
std::cerr << "[FATAL] Cannot export Fleasion proxy\n";
|
|
return EXIT_FAILURE;
|
|
}
|
|
runtime_config = mocktail::runtime::LoadRuntimeConfig(environment, paths.config_file());
|
|
if (!runtime_config) {
|
|
std::cerr << "[FATAL] Cannot apply Fleasion: " << runtime_config.error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
std::cout << " [fleasion] mode=" << runtime_config.config.fleasion_proxy_mode()
|
|
<< " certificate=" << fleasion.certificate
|
|
<< " trust_bundle=" << fleasion.bundle << '\n';
|
|
if (runtime_config.config.network_proxy()) {
|
|
std::cout << " [fleasion] proxy=" << mocktail::runtime::BuildNetworkProxyUrl(
|
|
*runtime_config.config.network_proxy()) << "; start Fleasion before Roblox\n";
|
|
}
|
|
}
|
|
if (config_bootstrap.created()) {
|
|
std::cout << " [runtime] created first-run configuration: "
|
|
<< paths.config_file() << '\n';
|
|
}
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun &&
|
|
runtime_config.config.use_system_proxy()) {
|
|
if (runtime_config.config.network_proxy().has_value()) {
|
|
std::cout << " [network] system proxy="
|
|
<< mocktail::runtime::BuildNetworkProxyUrl(
|
|
*runtime_config.config.network_proxy())
|
|
<< '\n';
|
|
} else {
|
|
std::cout << " [network] system proxy=direct\n";
|
|
}
|
|
}
|
|
if (use_memory_limit) {
|
|
if (cgroup_limit.active()) {
|
|
std::cout << " [memory] hard process-tree limit active: "
|
|
<< runtime_config.config.performance().memory_limit_mb
|
|
<< " MiB RAM, swap disabled\n";
|
|
}
|
|
}
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
|
std::cout << " [runtime] graphics backend="
|
|
<< runtime_config.config.graphics_backend_name()
|
|
<< (runtime_config.config.graphics_backend() ==
|
|
mocktail::runtime::GraphicsBackend::kSystem
|
|
? " (EGL/OpenGL ES)"
|
|
: "")
|
|
<< '\n';
|
|
}
|
|
|
|
// Register before starting helper processes or loading the Android payload.
|
|
// libgamemode caches its shared
|
|
// D-Bus connection, so the first use must happen in the final game process.
|
|
const mocktail::runtime::GameModePolicy game_mode_policy =
|
|
runtime_config.config.performance().game_mode;
|
|
mocktail::runtime::GameModeSession game_mode_session =
|
|
command_line.options.mode == mocktail::runtime::CommandMode::kRun
|
|
? mocktail::runtime::GameModeSession::Start(game_mode_policy)
|
|
: mocktail::runtime::GameModeSession();
|
|
switch (game_mode_session.state()) {
|
|
case mocktail::runtime::GameModeSessionState::kActive:
|
|
std::cout << " [gamemode] performance request active\n";
|
|
break;
|
|
case mocktail::runtime::GameModeSessionState::kDeclinedCorePinning:
|
|
std::cout << " [gamemode] released: it pinned this process to a subset "
|
|
"of the CPUs, which concentrates the worker pool. Set "
|
|
"pin_cores=no in ~/.config/gamemode.ini to keep GameMode, "
|
|
"or performance.gamemode: on to accept the pinning\n";
|
|
break;
|
|
case mocktail::runtime::GameModeSessionState::kAlreadyActive:
|
|
std::cout << " [gamemode] already active for this process\n";
|
|
break;
|
|
case mocktail::runtime::GameModeSessionState::kUnavailable:
|
|
case mocktail::runtime::GameModeSessionState::kRequestFailed:
|
|
if (game_mode_policy == mocktail::runtime::GameModePolicy::kOn) {
|
|
std::cerr << " [gamemode] requested but unavailable: "
|
|
<< game_mode_session.detail()
|
|
<< "; continuing without GameMode\n";
|
|
} else {
|
|
std::cout << " [gamemode] unavailable; continuing normally\n";
|
|
}
|
|
break;
|
|
case mocktail::runtime::GameModeSessionState::kDisabled:
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
|
std::cout << " [gamemode] disabled by runtime policy\n";
|
|
}
|
|
break;
|
|
case mocktail::runtime::GameModeSessionState::kStopped:
|
|
case mocktail::runtime::GameModeSessionState::kStopFailed:
|
|
break;
|
|
}
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
|
failure_dialog = mocktail::runtime::FailureDialogMonitor::Start(
|
|
environment,
|
|
"Mocktail could not start Roblox because of an internal error.");
|
|
}
|
|
if (use_memory_limit) {
|
|
if (!memory_limit_watchdog.Start(memory_limit_bytes, &command_line_error)) {
|
|
failure_dialog.SetMessage(
|
|
"Mocktail could not start its memory safety monitor.");
|
|
std::cerr << "[FATAL] " << command_line_error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
std::cout << " [memory] RSS+swap watchdog active at "
|
|
<< runtime_config.config.performance().memory_limit_mb
|
|
<< " MiB\n";
|
|
}
|
|
SecureEraseArguments(&cgroup_reexec_arguments);
|
|
const bool uses_managed_payload =
|
|
!environment.HasNonEmpty("ROBLOX_LIB_PATH") &&
|
|
runtime_config.config.roblox_library_path() ==
|
|
std::filesystem::path("rbx_bin/libroblox.so");
|
|
if (!uses_managed_payload &&
|
|
!environment.HasNonEmpty("MOCKTAIL_ASSET_PATH")) {
|
|
const std::filesystem::path adjacent_assets =
|
|
mocktail::runtime::ResolveAdjacentRobloxAssetPath(
|
|
runtime_config.config.roblox_library_path(),
|
|
paths.working_directory());
|
|
if (adjacent_assets.empty() ||
|
|
setenv("MOCKTAIL_ASSET_PATH", adjacent_assets.c_str(), 1) != 0) {
|
|
std::cerr << "[FATAL] Cannot bind assets to explicit Roblox library\n";
|
|
return EXIT_FAILURE;
|
|
}
|
|
std::cout << " [runtime] explicit Roblox library uses adjacent assets: "
|
|
<< adjacent_assets << '\n';
|
|
}
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun &&
|
|
uses_managed_payload) {
|
|
failure_dialog.SetMessage(
|
|
"Mocktail could not update or verify the Roblox installation.");
|
|
const mocktail::runtime::PayloadUpdatePreflightResult update_preflight =
|
|
mocktail::runtime::RunPayloadUpdatePreflight(environment, paths);
|
|
if (!update_preflight) {
|
|
std::cerr << "[FATAL] " << update_preflight.error;
|
|
if (!update_preflight.details.empty()) {
|
|
// Without this the dialog only ever said "could not update or verify",
|
|
// which is indistinguishable between a provider outage, a rejected
|
|
// signature, and a full disk.
|
|
std::cerr << ": " << update_preflight.details;
|
|
failure_dialog.SetMessage(
|
|
"Mocktail could not update or verify the Roblox installation.\n\n" +
|
|
update_preflight.details + "\n\nSession log: " +
|
|
(paths.logs_root() / "sessions").string());
|
|
}
|
|
std::cerr << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
failure_dialog.SetMessage(
|
|
"Mocktail could not finish starting Roblox because of an internal "
|
|
"error.");
|
|
}
|
|
if (!mocktail::runtime::ExportRuntimePathEnvironment(paths,
|
|
&command_line_error)) {
|
|
std::cerr << "[FATAL] " << command_line_error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
|
const mocktail::Status window_state_status =
|
|
mocktail::window::ConfigureWindowStatePersistence(paths.state_root() /
|
|
"window-state.json");
|
|
if (!window_state_status.ok()) {
|
|
std::cerr << "[FATAL] Cannot configure window state persistence: "
|
|
<< window_state_status.message() << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
std::filesystem::path app_storage_file;
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
|
const mocktail::runtime::InputCapabilityConfig& input =
|
|
runtime_config.config.input_capabilities();
|
|
const std::string platform_profile =
|
|
mocktail::runtime::BuildPlatformProfileRevision(
|
|
runtime_config.config.device_profile().cache_key,
|
|
input.touch_enabled, input.mouse_enabled, input.keyboard_enabled);
|
|
const mocktail::runtime::PlatformCacheMigrationResult cache_migration =
|
|
mocktail::runtime::MigratePlatformProfileCaches(environment, paths,
|
|
platform_profile);
|
|
if (!cache_migration) {
|
|
std::cerr << "[FATAL] Platform cache migration failed: "
|
|
<< cache_migration.error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
app_storage_file = cache_migration.app_storage_file;
|
|
setenv("MOCKTAIL_APP_STORAGE_FILE_INTERNAL", app_storage_file.c_str(), 1);
|
|
if (cache_migration.transitioned) {
|
|
std::cout << " [runtime] platform cache profile transitioned; "
|
|
<< "refreshable identity and policy caches invalidated="
|
|
<< (cache_migration.app_storage_updated ? 1 : 0) << '\n';
|
|
}
|
|
}
|
|
const bool needs_active_library =
|
|
!environment.HasNonEmpty("ROBLOX_LIB_PATH") &&
|
|
runtime_config.config.roblox_library_path() ==
|
|
std::filesystem::path("rbx_bin/libroblox.so");
|
|
const bool needs_active_assets =
|
|
!environment.HasNonEmpty("MOCKTAIL_ASSET_PATH");
|
|
if (needs_active_library || needs_active_assets) {
|
|
const mocktail::runtime::ActivePayloadPaths active =
|
|
paths.ResolveActivePayload();
|
|
if (!active) {
|
|
std::cerr << "[FATAL] Cannot resolve " << paths.active_payload_manifest()
|
|
<< ": " << active.error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
if (active.active) {
|
|
if (uses_managed_payload &&
|
|
!mocktail::runtime::PrepareManagedPayloadWorkingDirectory(
|
|
paths, active, &command_line_error)) {
|
|
std::cerr << "[FATAL] " << command_line_error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
if ((needs_active_library &&
|
|
setenv("ROBLOX_LIB_PATH", active.roblox_library.c_str(), 1) != 0) ||
|
|
(needs_active_assets &&
|
|
setenv("MOCKTAIL_ASSET_PATH", active.assets_content.c_str(), 1) !=
|
|
0)) {
|
|
std::cerr << "[FATAL] Cannot activate resolved Roblox payload\n";
|
|
return EXIT_FAILURE;
|
|
}
|
|
if (uses_managed_payload) {
|
|
bool use_external_profile = !active.compatibility_manifest.empty();
|
|
if (use_external_profile) {
|
|
const mocktail::compat::BuildIdResult active_build_id =
|
|
mocktail::compat::ReadElfBuildId(active.roblox_library.string());
|
|
if (!active_build_id) {
|
|
std::cerr << "[FATAL] Cannot identify active Roblox payload: "
|
|
<< active_build_id.error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
// Built-in profiles supersede stale probation receipts and never
|
|
// use external overrides.
|
|
use_external_profile = mocktail::compat::FindHostAbiProfile(
|
|
active_build_id.build_id) == nullptr;
|
|
}
|
|
if (use_external_profile) {
|
|
if (setenv("MOCKTAIL_COMPATIBILITY_MANIFEST",
|
|
active.compatibility_manifest.c_str(), 1) != 0 ||
|
|
setenv("MOCKTAIL_HOST_ABI_PROFILE_FILE",
|
|
active.host_abi_profile.c_str(), 1) != 0 ||
|
|
setenv("MOCKTAIL_HOST_ABI_APPROVAL_RECEIPT",
|
|
active.host_abi_approval_receipt.c_str(), 1) != 0) {
|
|
std::cerr
|
|
<< "[FATAL] Cannot activate approved Roblox ABI profile\n";
|
|
return EXIT_FAILURE;
|
|
}
|
|
} else if (setenv("MOCKTAIL_COMPATIBILITY_MANIFEST",
|
|
built_in_compatibility_manifest.c_str(), 1) != 0 ||
|
|
unsetenv("MOCKTAIL_HOST_ABI_PROFILE_FILE") != 0 ||
|
|
unsetenv("MOCKTAIL_HOST_ABI_APPROVAL_RECEIPT") != 0) {
|
|
std::cerr << "[FATAL] Cannot activate built-in Roblox ABI profile\n";
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
runtime_config = mocktail::runtime::LoadRuntimeConfig(
|
|
environment, paths.config_file());
|
|
if (!runtime_config) {
|
|
std::cerr << "[FATAL] Cannot resolve runtime configuration: "
|
|
<< runtime_config.error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
}
|
|
if (!mocktail::runtime::ExportRuntimeConfigEnvironment(runtime_config.config,
|
|
&command_line_error)) {
|
|
std::cerr << command_line_error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun &&
|
|
!environment.HasNonEmpty("MOCKTAIL_NATIVE_SET_DEFAULT_POLICY_FILE") &&
|
|
setenv("MOCKTAIL_NATIVE_SET_DEFAULT_POLICY_FILE", "1", 1) != 0) {
|
|
std::cerr << "[FATAL] Cannot enable Roblox default app-policy file\n";
|
|
return EXIT_FAILURE;
|
|
}
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
|
std::cout << " [runtime] device profile="
|
|
<< runtime_config.config.device_profile().name << " class="
|
|
<< mocktail::runtime::DeviceClassName(
|
|
runtime_config.config.device_profile().device_class)
|
|
<< " model=\""
|
|
<< runtime_config.config.device_profile().display_name << "\"\n";
|
|
}
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
|
const std::filesystem::path fflags_path =
|
|
paths.config_root() / "fflags.json";
|
|
const auto fflags = mocktail::services::LoadAndMergeFflagsFile(
|
|
fflags_path,
|
|
environment.GetOr("MOCKTAIL_CLIENT_SETTINGS_OVERRIDES_JSON", "{}"));
|
|
if (!fflags.error.empty()) {
|
|
std::cerr << "[FATAL] Cannot load FFlag overrides from " << fflags_path
|
|
<< ": " << fflags.error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
if (fflags.loaded) {
|
|
std::cout << " [runtime] loaded " << fflags.count
|
|
<< " FFlag overrides from " << fflags_path << '\n';
|
|
}
|
|
std::string client_settings_overrides;
|
|
std::vector<std::string> kept_fflag_overrides;
|
|
if (!mocktail::runtime::MergeRuntimeClientSettingsOverrides(
|
|
runtime_config.config.frame_rate(),
|
|
runtime_config.config.performance(), fflags.json,
|
|
&client_settings_overrides, &command_line_error,
|
|
&kept_fflag_overrides)) {
|
|
std::cerr << "[FATAL] Cannot apply runtime client-settings policy: "
|
|
<< command_line_error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
for (const std::string& name : kept_fflag_overrides) {
|
|
std::cout << " [runtime] keeping " << name << " from " << fflags_path
|
|
<< " over the performance preset\n";
|
|
}
|
|
std::string audio_capture_overrides;
|
|
if (!mocktail::runtime::MergeAudioCaptureClientSettingsOverrides(
|
|
runtime_config.config.microphone_enabled(),
|
|
client_settings_overrides, &audio_capture_overrides,
|
|
&command_line_error)) {
|
|
std::cerr << "[FATAL] Cannot apply microphone permission policy: "
|
|
<< command_line_error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
if (setenv("MOCKTAIL_CLIENT_SETTINGS_OVERRIDES_JSON",
|
|
audio_capture_overrides.c_str(), 1) != 0) {
|
|
std::cerr << "[FATAL] Cannot export runtime client-settings policy\n";
|
|
return EXIT_FAILURE;
|
|
}
|
|
std::cout << " [runtime] HttpClient compatibility mode enabled: "
|
|
"generic LuaApp retry and RuntimeMutexRv disabled\n";
|
|
std::string fast_flags_overrides;
|
|
if (!mocktail::runtime::MergeCrashReportFastFlagsOverrides(
|
|
environment.GetOr("MOCKTAIL_FAST_FLAGS_JSON", "{}"),
|
|
&fast_flags_overrides, &command_line_error)) {
|
|
std::cerr << "[FATAL] Cannot apply crash-report fast-flags policy: "
|
|
<< command_line_error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
if (setenv("MOCKTAIL_FAST_FLAGS_JSON", fast_flags_overrides.c_str(), 1) !=
|
|
0) {
|
|
std::cerr << "[FATAL] Cannot export crash-report fast-flags policy\n";
|
|
return EXIT_FAILURE;
|
|
}
|
|
std::cout << " [runtime] Roblox crash-report uploads disabled by "
|
|
"mandatory policy\n";
|
|
const auto physics_worker_mode =
|
|
runtime_config.config.performance().physics_worker_mode;
|
|
const int physical_core_count =
|
|
runtime_config.config.performance().physical_core_count;
|
|
const int engine_worker_count =
|
|
physics_worker_mode == mocktail::runtime::PhysicsWorkerMode::kThroughput
|
|
? mocktail::runtime::CalculateThroughputWorkerCount(
|
|
physical_core_count)
|
|
: physical_core_count;
|
|
if (physics_worker_mode == mocktail::runtime::PhysicsWorkerMode::kLatency) {
|
|
std::cout << " [runtime] physics worker mode=latency: Roblox-managed "
|
|
"worker pools, midphase batch=128\n";
|
|
} else if (physics_worker_mode ==
|
|
mocktail::runtime::PhysicsWorkerMode::kThroughput) {
|
|
std::cout << " [runtime] physics worker mode=throughput: "
|
|
<< engine_worker_count << " scheduler workers, async minimum="
|
|
<< std::min(engine_worker_count, 3) << ", midphase batch=128\n";
|
|
}
|
|
if ((runtime_config.config.performance().multithreaded_rendering ||
|
|
physics_worker_mode ==
|
|
mocktail::runtime::PhysicsWorkerMode::kThroughput) &&
|
|
physics_worker_mode != mocktail::runtime::PhysicsWorkerMode::kLatency) {
|
|
std::cout << " [runtime] full multithreaded engine queues enabled: "
|
|
<< engine_worker_count << " scheduler workers, "
|
|
<< std::max(1, engine_worker_count / 2)
|
|
<< " occlusion workers\n";
|
|
} else if (runtime_config.config.performance().multithreaded_rendering &&
|
|
physics_worker_mode ==
|
|
mocktail::runtime::PhysicsWorkerMode::kLatency) {
|
|
std::cout << " [runtime] latency mode leaves scheduler/render worker "
|
|
"counts under Roblox client-settings control\n";
|
|
}
|
|
}
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun &&
|
|
runtime_config.config.frame_rate().mode ==
|
|
mocktail::runtime::FrameRateLimitMode::kUnlimited) {
|
|
std::cout << " [runtime] frame-rate mode=unlimited scheduler_target="
|
|
<< mocktail::runtime::kMaximumSupportedRobloxSchedulerFps
|
|
<< " vsync=" << runtime_config.config.vsync_mode() << '\n';
|
|
}
|
|
|
|
const libc_shim::HostCaBundleResolution ca_bundle =
|
|
libc_shim::ResolveHostCaBundle();
|
|
if (ca_bundle.status == libc_shim::HostCaBundleStatus::kInvalidOverride) {
|
|
std::cerr << "Invalid MOCKTAIL_CA_BUNDLE: expected a readable, non-empty "
|
|
"absolute regular file\n";
|
|
return 2;
|
|
}
|
|
|
|
mocktail::runtime::RobloxFullscreenRuntimeBridge fullscreen_bridge;
|
|
mocktail::audio::RobloxOutputDeviceBridge output_device_bridge;
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
|
const std::string compatibility_manifest =
|
|
environment.GetOr("MOCKTAIL_COMPATIBILITY_MANIFEST",
|
|
MOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST);
|
|
const mocktail::compat::PayloadCompatibilityResult compatibility =
|
|
mocktail::compat::CheckPayloadCompatibility(
|
|
runtime_config.config.roblox_library_path().string(),
|
|
compatibility_manifest,
|
|
command_line.options.allow_unverified_build);
|
|
if (!compatibility) {
|
|
std::cerr << "[FATAL] " << compatibility.error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
const mocktail::Status fullscreen_status =
|
|
fullscreen_bridge.Install(compatibility.profile);
|
|
if (!fullscreen_status.ok()) {
|
|
std::cerr << "[FATAL] Cannot install fullscreen runtime bridge: "
|
|
<< fullscreen_status.message() << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
if (compatibility.profile.fmod_output_device_bridge &&
|
|
compatibility.profile.fmod_output_device_bridge->has_input_devices()) {
|
|
std::string menu_settings;
|
|
const char *settings =
|
|
std::getenv("MOCKTAIL_CLIENT_SETTINGS_OVERRIDES_JSON");
|
|
if (!mocktail::runtime::MergeAudioDeviceMenuClientSettingsOverrides(
|
|
settings ? settings : "{}", &menu_settings,
|
|
&command_line_error) ||
|
|
setenv("MOCKTAIL_CLIENT_SETTINGS_OVERRIDES_JSON",
|
|
menu_settings.c_str(), 1) != 0) {
|
|
std::cerr << "[FATAL] Cannot configure host audio device menu: "
|
|
<< command_line_error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
const mocktail::Status output_device_status =
|
|
output_device_bridge.Install(compatibility.profile);
|
|
if (!output_device_status.ok()) {
|
|
std::cerr << "[FATAL] Cannot install Roblox output-device bridge: "
|
|
<< output_device_status.message() << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
|
|
mocktail::legacy::RuntimeDependencies dependencies;
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
|
auto http_client = std::make_shared<mocktail::services::CurlHttpClient>();
|
|
mocktail::services::BrowserTrackerService browser_tracker(*http_client);
|
|
std::filesystem::path tracker_cookie_file =
|
|
paths.data_root() / "browser_tracker.cookie";
|
|
bool tracker_cookie_owned = true;
|
|
if (environment.HasNonEmpty("MOCKTAIL_COOKIE_FILE")) {
|
|
tracker_cookie_file = environment.GetOr("MOCKTAIL_COOKIE_FILE", "");
|
|
tracker_cookie_owned = false;
|
|
} else if (mocktail::runtime::RuntimePaths::Exists(paths.cookie_file())) {
|
|
tracker_cookie_file = paths.cookie_file();
|
|
}
|
|
const mocktail::services::BrowserTrackerResult browser_tracker_result =
|
|
browser_tracker.EnsureInitialized(app_storage_file, tracker_cookie_file,
|
|
tracker_cookie_owned);
|
|
if (!browser_tracker_result) {
|
|
std::cerr << " [runtime] BrowserTracker bootstrap skipped: "
|
|
<< browser_tracker_result.error;
|
|
if (browser_tracker_result.http_status != 0) {
|
|
std::cerr << " (HTTP " << browser_tracker_result.http_status << ')';
|
|
}
|
|
std::cerr << '\n';
|
|
} else {
|
|
std::cout << " [runtime] BrowserTracker identity "
|
|
<< (browser_tracker_result.status ==
|
|
mocktail::services::BrowserTrackerStatus::kCreated
|
|
? "initialized"
|
|
: "loaded")
|
|
<< " before engine startup\n";
|
|
}
|
|
mocktail::services::AuthService auth_service(*http_client);
|
|
mocktail::runtime::AuthRuntimeComposition composition =
|
|
mocktail::runtime::ComposeAuthRuntime(environment, paths, auth_service,
|
|
http_client);
|
|
if (!external_launch_request.has_value() &&
|
|
command_line.options.window_mode !=
|
|
mocktail::runtime::WindowMode::kHeadless) {
|
|
PromptFirstLaunchSignIn(environment, paths, auth_service, http_client,
|
|
&composition);
|
|
}
|
|
if (!composition) {
|
|
std::cerr << "[FATAL] Typed Roblox authentication preflight failed: "
|
|
<< composition.error;
|
|
if (composition.http_status != 0) {
|
|
std::cerr << " (HTTP " << composition.http_status << ')';
|
|
}
|
|
std::cerr << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
composition.jni_vm->SetPlatformIdentity(jnivm::PlatformIdentity{
|
|
runtime_config.config.input_capabilities().touch_enabled,
|
|
runtime_config.config.input_capabilities().mouse_enabled,
|
|
runtime_config.config.input_capabilities().keyboard_enabled,
|
|
runtime_config.config.device_profile().pc_hardware,
|
|
std::string(runtime_config.config.device_profile().platform_name),
|
|
std::string(runtime_config.config.device_profile().display_name),
|
|
std::string(runtime_config.config.device_profile().manufacturer),
|
|
std::string(runtime_config.config.device_profile().model),
|
|
std::string(runtime_config.config.device_profile().brand),
|
|
std::string(runtime_config.config.device_profile().device_code),
|
|
std::string(runtime_config.config.device_profile().device_sku),
|
|
std::string(runtime_config.config.device_profile().soc_model),
|
|
});
|
|
if (composition.rejected_credential_retired) {
|
|
constexpr std::string_view kSignedOutMessage =
|
|
"Your saved Roblox session is no longer valid. Sign in again to "
|
|
"continue.";
|
|
std::cout << " [auth] saved Roblox session expired; continuing with "
|
|
"native sign-in\n";
|
|
(void)mocktail::runtime::ShowWarningDialog(environment,
|
|
kSignedOutMessage);
|
|
}
|
|
if (composition.status ==
|
|
mocktail::runtime::AuthRuntimeStatus::kAuthenticated) {
|
|
if (setenv("MOCKTAIL_NATIVE_SET_USER_ID", "1", 1) != 0) {
|
|
std::cerr << "[FATAL] Cannot enable authenticated NativeSettings "
|
|
"identity\n";
|
|
return EXIT_FAILURE;
|
|
}
|
|
std::cout
|
|
<< " [auth] typed Roblox identity resolved for production VM\n";
|
|
} else {
|
|
std::cout
|
|
<< " [auth] explicit guest identity selected for production VM\n";
|
|
}
|
|
if (runtime_config.config.desktop_playability()) {
|
|
std::filesystem::path assets = paths.DefaultAssetPath();
|
|
if (environment.HasNonEmpty("MOCKTAIL_ASSET_PATH")) {
|
|
assets = environment.GetOr("MOCKTAIL_ASSET_PATH", "");
|
|
}
|
|
const std::filesystem::path default_app_policy =
|
|
assets / "guac/defaultConfigs/GuacDefaultPolicy-GlobalDist.json";
|
|
const std::int64_t app_policy_user_id =
|
|
composition.status ==
|
|
mocktail::runtime::AuthRuntimeStatus::kAuthenticated
|
|
? composition.account_identity.user_id
|
|
: -1;
|
|
const mocktail::runtime::DesktopAppPolicyResult desktop_policy =
|
|
mocktail::runtime::ApplyDesktopAppPolicy(
|
|
app_storage_file, default_app_policy, app_policy_user_id,
|
|
runtime_config.config.theme_mode());
|
|
if (!desktop_policy) {
|
|
std::cerr << "[FATAL] Desktop Roblox app-policy failed: "
|
|
<< desktop_policy.error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
std::string desktop_client_settings;
|
|
if (!mocktail::runtime::MergeDesktopAppPolicyClientSettingsOverride(
|
|
desktop_policy.policy_json,
|
|
environment.GetOr("MOCKTAIL_CLIENT_SETTINGS_OVERRIDES_JSON",
|
|
"{}"),
|
|
&desktop_client_settings, &command_line_error)) {
|
|
std::cerr << "[FATAL] Cannot activate desktop Roblox app-policy: "
|
|
<< command_line_error << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
if (setenv("MOCKTAIL_CLIENT_SETTINGS_OVERRIDES_JSON",
|
|
desktop_client_settings.c_str(), 1) != 0) {
|
|
std::cerr << "[FATAL] Cannot export desktop Roblox app-policy\n";
|
|
return EXIT_FAILURE;
|
|
}
|
|
std::cout << " [runtime] desktop app-policy ready: normalized="
|
|
<< desktop_policy.normalized_policy_count
|
|
<< " updated=" << (desktop_policy.updated ? 1 : 0)
|
|
<< " runtime_override=1\n";
|
|
}
|
|
auto android_window_context =
|
|
std::make_shared<AndroidWindowBridgeContext>();
|
|
jnivm::AndroidWindowCallbacks android_window_callbacks;
|
|
android_window_callbacks.set_flags = &QueueAndroidWindowFlags;
|
|
composition.jni_vm->SetAndroidWindowCallbacks(
|
|
std::move(android_window_context), android_window_callbacks);
|
|
const mocktail::Status audio_status =
|
|
mocktail::audio::InstallFmodJniAudioBridge(composition.jni_vm.get());
|
|
if (!audio_status.ok()) {
|
|
std::cerr << "[FATAL] Typed FMOD Java audio composition failed: "
|
|
<< audio_status.message() << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
const mocktail::Status voice_status =
|
|
mocktail::audio::InstallWebRtcJniAudioBridge(
|
|
composition.jni_vm.get());
|
|
if (!voice_status.ok()) {
|
|
(void)mocktail::audio::ShutdownFmodJniAudioBridge(
|
|
composition.jni_vm.get());
|
|
std::cerr << "[FATAL] WebRTC voice audio composition failed: "
|
|
<< voice_status.message() << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
dependencies = mocktail::legacy::RuntimeDependencies(
|
|
std::move(composition), &ShutdownPlatformBridges);
|
|
}
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun &&
|
|
!isolated_canary) {
|
|
// Listen only after re-exec, preflight, and bridge setup, so an ACK cannot
|
|
// precede a known startup failure. Isolated canaries expose no endpoint.
|
|
mocktail::Status broker_status =
|
|
mocktail::runtime::ExternalLaunchBroker::StartOwnerAfterLockAcquired(
|
|
broker_options, &external_launch_broker.broker(),
|
|
external_launch_request.has_value() ? &*external_launch_request
|
|
: nullptr);
|
|
if (broker_status.ok()) external_launch_request.reset();
|
|
if (broker_status.ok()) {
|
|
broker_status = mocktail::runtime::InstallActiveExternalLaunchBroker(
|
|
external_launch_broker.broker());
|
|
}
|
|
if (!broker_status.ok()) {
|
|
std::cerr << "[FATAL] Cannot activate website launch bridge: "
|
|
<< broker_status.message() << '\n';
|
|
return EXIT_FAILURE;
|
|
}
|
|
}
|
|
failure_dialog.SetMessage(
|
|
"Roblox closed unexpectedly because of an internal error.");
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
|
mocktail::runtime::LogProcessDiagnostics(
|
|
mocktail::runtime::ProcessDiagnosticStage::kNativeRuntime);
|
|
}
|
|
int runtime_status =
|
|
mocktail::legacy::Run(command_line.options, std::move(dependencies));
|
|
if (command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
|
mocktail::runtime::LogProcessDiagnostics(
|
|
mocktail::runtime::ProcessDiagnosticStage::kShutdown);
|
|
std::cerr << "[diagnostic] native runtime exit_status=" << runtime_status
|
|
<< '\n';
|
|
}
|
|
const mocktail::Status launch_broker_shutdown_status =
|
|
external_launch_broker.Shutdown();
|
|
if (!launch_broker_shutdown_status.ok()) {
|
|
std::cerr << " [launch] website bridge shutdown failed: "
|
|
<< launch_broker_shutdown_status.message() << '\n';
|
|
runtime_status = EXIT_FAILURE;
|
|
}
|
|
if (runtime_status != EXIT_SUCCESS) {
|
|
failure_dialog.SetMessage("Roblox stopped with error code " +
|
|
std::to_string(runtime_status) + ".");
|
|
}
|
|
support_bundle_guard.SetExitCode(runtime_status);
|
|
output_device_bridge.Shutdown();
|
|
fullscreen_bridge.Shutdown();
|
|
memory_limit_watchdog.Stop();
|
|
const mocktail::Status game_mode_stop_status = game_mode_session.Stop();
|
|
if (!game_mode_stop_status.ok()) {
|
|
std::cerr << " [gamemode] release failed: "
|
|
<< game_mode_stop_status.message() << '\n';
|
|
}
|
|
if (runtime_status == EXIT_SUCCESS &&
|
|
command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
|
failure_dialog.MarkSuccessful();
|
|
support_bundle_guard.Disarm();
|
|
// Guest atexit handlers target workers that cannot be joined. Host
|
|
// shutdown is already complete here, so do not re-enter guest teardown.
|
|
std::cout.flush();
|
|
std::cerr.flush();
|
|
std::_Exit(EXIT_SUCCESS);
|
|
}
|
|
if (runtime_status == EXIT_SUCCESS) {
|
|
failure_dialog.MarkSuccessful();
|
|
support_bundle_guard.Disarm();
|
|
}
|
|
return runtime_status;
|
|
}
|