diff --git a/config/mocktail.example.yaml b/config/mocktail.example.yaml index 5bdc7e1..986bb65 100644 --- a/config/mocktail.example.yaml +++ b/config/mocktail.example.yaml @@ -32,7 +32,9 @@ runtime: # roblox_library: /absolute/path/to/libroblox.so appearance: - theme: system + # String (default: roblox): use Roblox's saved account theme. Supported + # overrides: dark, light, or system (follow the desktop color scheme). + theme: roblox graphics: # String (default: direct-vulkan): graphics backend. Use opengl on hardware diff --git a/include/runtime/platform_cache_migration.h b/include/runtime/platform_cache_migration.h index c2f4a7a..b9cebbe 100644 --- a/include/runtime/platform_cache_migration.h +++ b/include/runtime/platform_cache_migration.h @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -38,6 +39,20 @@ PlatformCacheMigrationResult MigratePlatformProfileCaches( const Environment& environment, const RuntimePaths& paths, std::string_view desired_revision); +struct RobloxThemeCacheResult { + std::optional dark_theme; + std::string error; + + explicit operator bool() const { return error.empty(); } +}; + +// Reads the current account's Roblox theme without changing appStorage. A +// missing value leaves dark_theme empty so the caller can use Roblox's own +// startup default. +RobloxThemeCacheResult ReadRobloxThemeCache( + const std::filesystem::path& app_storage_file, + std::int64_t authenticated_user_id); + // Synchronizes only Roblox's local theme keys with the resolved host theme. bool ApplyRobloxThemeCacheOverride( const std::filesystem::path& app_storage_file, diff --git a/include/runtime/roblox_desktop_app_policy.h b/include/runtime/roblox_desktop_app_policy.h index d283823..1a89e30 100644 --- a/include/runtime/roblox_desktop_app_policy.h +++ b/include/runtime/roblox_desktop_app_policy.h @@ -20,14 +20,15 @@ struct DesktopAppPolicyResult { explicit operator bool() const { return error.empty(); } }; -// Normalizes only Roblox UniversalApp layout coordinates for a PC profile. -// Account eligibility and feature-entitlement fields remain server-owned. +// Normalizes Roblox UniversalApp layout coordinates for a PC profile and +// applies a fixed theme only when configured. Account eligibility and +// feature-entitlement fields remain server-owned. // The guest and authenticated cache keys are both populated so user-switch // startup cannot fall back to the Android Unknown/mobile layout. DesktopAppPolicyResult ApplyDesktopAppPolicy( const std::filesystem::path& app_storage_file, const std::filesystem::path& default_policy_file, - std::int64_t authenticated_user_id); + std::int64_t authenticated_user_id, std::string_view theme_mode); // Routes the normalized policy through Roblox's supported runtime override so // a later GUAC response cannot replace the in-memory desktop layout with the diff --git a/include/runtime/runtime_config.h b/include/runtime/runtime_config.h index c788ded..6c4ddbd 100644 --- a/include/runtime/runtime_config.h +++ b/include/runtime/runtime_config.h @@ -87,8 +87,8 @@ class RuntimeConfig { const WindowConfig& window() const { return window_; } const std::string& theme_mode() const { return theme_mode_; } bool theme_mode_valid() const { - return theme_mode_ == "system" || theme_mode_ == "light" || - theme_mode_ == "dark"; + return theme_mode_ == "roblox" || theme_mode_ == "system" || + theme_mode_ == "light" || theme_mode_ == "dark"; } const InputCapabilityConfig& input_capabilities() const { return input_capabilities_; @@ -139,7 +139,7 @@ class RuntimeConfig { GraphicsBackend graphics_backend_ = GraphicsBackend::kVulkan; std::string graphics_backend_name_ = "direct-vulkan"; WindowConfig window_; - std::string theme_mode_ = "system"; + std::string theme_mode_ = "roblox"; InputCapabilityConfig input_capabilities_; DeviceProfile device_profile_ = *FindDeviceProfile(kDefaultDeviceProfileName); bool device_profile_valid_ = true; diff --git a/src/legacy/legacy_runtime.cc b/src/legacy/legacy_runtime.cc index fe23d00..f38e514 100644 --- a/src/legacy/legacy_runtime.cc +++ b/src/legacy/legacy_runtime.cc @@ -4430,8 +4430,9 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options, return EXIT_FAILURE; } if (!runtime_config.theme_mode_valid()) { - std::cerr << "[FATAL] Invalid MOCKTAIL_THEME; expected system, light, or " - "dark\n"; + std::cerr + << "[FATAL] Invalid MOCKTAIL_THEME; expected roblox, system, light, " + "or dark\n"; return EXIT_FAILURE; } setenv("MOCKTAIL_TOUCH_ENABLED_INTERNAL", @@ -4713,13 +4714,35 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options, 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 bool dark_theme = - runtime_config.theme_mode() == "dark" || - (runtime_config.theme_mode() == "system" && system_dark_theme); - setenv("MOCKTAIL_RESOLVED_THEME_INTERNAL", dark_theme ? "Dark" : "Light", 1); const char* app_storage_file = std::getenv("MOCKTAIL_APP_STORAGE_FILE_INTERNAL"); - if (app_storage_file != nullptr) { + 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 << "[FATAL] Cannot read Roblox theme cache: " + << saved_theme.error << '\n'; + return EXIT_FAILURE; + } + 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 missing; 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, diff --git a/src/main.cc b/src/main.cc index 751509c..c3899af 100644 --- a/src/main.cc +++ b/src/main.cc @@ -949,7 +949,8 @@ int main(int argc, char* argv[]) { : -1; const mocktail::runtime::DesktopAppPolicyResult desktop_policy = mocktail::runtime::ApplyDesktopAppPolicy( - app_storage_file, default_app_policy, app_policy_user_id); + 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'; diff --git a/src/runtime/platform_cache_migration.cc b/src/runtime/platform_cache_migration.cc index 57d11d0..219d29a 100644 --- a/src/runtime/platform_cache_migration.cc +++ b/src/runtime/platform_cache_migration.cc @@ -341,6 +341,68 @@ PlatformCacheMigrationResult MigratePlatformProfileCaches( return result; } +RobloxThemeCacheResult ReadRobloxThemeCache( + const std::filesystem::path& app_storage_file, + std::int64_t authenticated_user_id) { + RobloxThemeCacheResult result; + if (!app_storage_file.is_absolute() || authenticated_user_id < -1) { + result.error = "Roblox theme cache path or user ID is invalid"; + return result; + } + + const JsonReadResult storage = ReadJson(app_storage_file); + if (storage.status == ReadStatus::kInvalid) { + result.error = storage.error; + return result; + } + if (storage.status == ReadStatus::kMissing) { + return result; + } + + const auto read_theme = [&result](const nlohmann::json& value, + std::string_view name) { + if (!value.is_string()) { + result.error = std::string(name) + " is not a string"; + return; + } + const std::string& theme = value.get_ref(); + if (theme != "dark" && theme != "light") { + result.error = std::string(name) + " must be dark or light"; + return; + } + result.dark_theme = theme == "dark"; + }; + + if (authenticated_user_id >= 0) { + const auto encoded_device_themes = storage.value.find("DeviceLevelTheme"); + if (encoded_device_themes != storage.value.end()) { + if (!encoded_device_themes->is_string()) { + result.error = "Roblox DeviceLevelTheme is not an encoded object"; + return result; + } + const nlohmann::json device_themes = nlohmann::json::parse( + encoded_device_themes->get_ref(), nullptr, false, + true); + if (device_themes.is_discarded() || !device_themes.is_object()) { + result.error = "Roblox DeviceLevelTheme encoding is invalid"; + return result; + } + const auto account_theme = + device_themes.find(std::to_string(authenticated_user_id)); + if (account_theme != device_themes.end()) { + read_theme(*account_theme, "Roblox account theme"); + return result; + } + } + } + + const auto authenticated_theme = storage.value.find("AuthenticatedTheme"); + if (authenticated_theme != storage.value.end()) { + read_theme(*authenticated_theme, "Roblox authenticated theme"); + } + return result; +} + bool ApplyRobloxThemeCacheOverride( const std::filesystem::path& app_storage_file, std::int64_t authenticated_user_id, bool dark_theme, std::string* error) { diff --git a/src/runtime/roblox_desktop_app_policy.cc b/src/runtime/roblox_desktop_app_policy.cc index 052093b..fa717ce 100644 --- a/src/runtime/roblox_desktop_app_policy.cc +++ b/src/runtime/roblox_desktop_app_policy.cc @@ -206,13 +206,18 @@ bool DecodePolicy(const nlohmann::json& encoded, nlohmann::json* policy) { DesktopAppPolicyResult ApplyDesktopAppPolicy( const std::filesystem::path& app_storage_file, const std::filesystem::path& default_policy_file, - std::int64_t authenticated_user_id) { + std::int64_t authenticated_user_id, std::string_view theme_mode) { DesktopAppPolicyResult result; if (!app_storage_file.is_absolute() || !default_policy_file.is_absolute() || - authenticated_user_id < -1) { - result.error = "desktop app-policy paths or user ID are invalid"; + authenticated_user_id < -1 || + (theme_mode != "roblox" && theme_mode != "system" && + theme_mode != "light" && theme_mode != "dark")) { + result.error = "desktop app-policy paths, user ID, or theme are invalid"; return result; } + const std::string forced_theme = + theme_mode == "light" || theme_mode == "dark" ? std::string(theme_mode) + : std::string(); JsonReadResult storage = ReadJsonObject(app_storage_file, "Roblox appStorage"); @@ -259,6 +264,7 @@ DesktopAppPolicyResult ApplyDesktopAppPolicy( return result; } NormalizeDesktopLayout(&policy); + policy["ForceTheme"] = forced_theme; entry.value() = policy.dump(); if (!template_policy.is_object()) { template_policy = policy; @@ -289,6 +295,7 @@ DesktopAppPolicyResult ApplyDesktopAppPolicy( } template_policy = fallback.value; NormalizeDesktopLayout(&template_policy); + template_policy["ForceTheme"] = forced_theme; } const std::string encoded_policy = template_policy.dump(); diff --git a/src/runtime/runtime_config.cc b/src/runtime/runtime_config.cc index f48bad2..f3e9d9a 100644 --- a/src/runtime/runtime_config.cc +++ b/src/runtime/runtime_config.cc @@ -185,7 +185,8 @@ RuntimeConfig RuntimeConfig::FromEnvironment(const Environment& environment) { config.window_.high_dpi = ReadBoolean(environment, "MOCKTAIL_WIN_HIGH_DPI", config.window_.high_dpi, &config.window_.high_dpi_valid); - config.theme_mode_ = environment.GetOr("MOCKTAIL_THEME", "system"); + config.theme_mode_ = + environment.GetOr("MOCKTAIL_THEME", config.theme_mode_); const std::optional configured_device = environment.Get("MOCKTAIL_DEVICE_PROFILE"); const bool has_explicit_device = diff --git a/src/runtime/runtime_config_bootstrap.cc b/src/runtime/runtime_config_bootstrap.cc index 6389354..f0d5386 100644 --- a/src/runtime/runtime_config_bootstrap.cc +++ b/src/runtime/runtime_config_bootstrap.cc @@ -50,7 +50,9 @@ runtime: # roblox_library: /absolute/path/to/libroblox.so appearance: - theme: system + # String (default: roblox): use Roblox's saved account theme. Supported + # overrides: dark, light, or system (follow the desktop color scheme). + theme: roblox graphics: # String (default: direct-vulkan): graphics backend. Use opengl on hardware diff --git a/src/runtime/runtime_config_file.cc b/src/runtime/runtime_config_file.cc index 10f8aa1..faee55b 100644 --- a/src/runtime/runtime_config_file.cc +++ b/src/runtime/runtime_config_file.cc @@ -301,8 +301,9 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment, (*environment)["ROBLOX_LIB_PATH"] = *library; } if (const auto theme = value("appearance.theme"); theme.has_value()) { - if (*theme != "system" && *theme != "light" && *theme != "dark") { - *error = "appearance.theme must be system, light, or dark"; + if (*theme != "roblox" && *theme != "system" && *theme != "light" && + *theme != "dark") { + *error = "appearance.theme must be roblox, system, light, or dark"; return false; } (*environment)["MOCKTAIL_THEME"] = *theme; diff --git a/src/window/window.cc b/src/window/window.cc index 668a498..0de594d 100644 --- a/src/window/window.cc +++ b/src/window/window.cc @@ -1052,7 +1052,11 @@ bool Init(int width, int height, const char* title) { if (g_state.native_window == nullptr) { g_state.native_window = g_state.sdl_window; } - // Wayland reports undefined currentExtent until the window is mapped. + // Wayland reports undefined currentExtent until the window is mapped and + // receives its initial configure. X11 has no equivalent requirement, and + // SDL_SyncWindow can time out there while waiting for an unrelated WM + // state request (notably restored maximization) even though the native + // window is already valid for Vulkan WSI. if (!SDL_ShowWindow(g_state.sdl_window)) { fprintf(stderr, " [window] SDL_ShowWindow failed: %s\n", SDL_GetError()); SDL_DestroyWindow(g_state.sdl_window); @@ -1062,8 +1066,13 @@ bool Init(int width, int height, const char* title) { g_state.initialised = false; return false; } - if (!SDL_SyncWindow(g_state.sdl_window)) { - fprintf(stderr, " [window] SDL_SyncWindow failed: %s\n", SDL_GetError()); + const char* active_video_driver = SDL_GetCurrentVideoDriver(); + if (DirectVulkanWindowRequiresInitialSync( + active_video_driver != nullptr ? active_video_driver : "") && + !SDL_SyncWindow(g_state.sdl_window)) { + fprintf(stderr, + " [window] SDL_SyncWindow timed out waiting for the initial " + "Wayland configure\n"); SDL_DestroyWindow(g_state.sdl_window); g_state.sdl_window = nullptr; SDL_Quit(); diff --git a/src/window/window_creation_policy.cc b/src/window/window_creation_policy.cc index 2d91c17..4bf9444 100644 --- a/src/window/window_creation_policy.cc +++ b/src/window/window_creation_policy.cc @@ -11,5 +11,9 @@ SDL_WindowFlags ApplyHighPixelDensityWindowFlag(SDL_WindowFlags flags, return flags & ~SDL_WINDOW_HIGH_PIXEL_DENSITY; } +bool DirectVulkanWindowRequiresInitialSync(std::string_view video_driver) { + return video_driver == "wayland"; +} + } // namespace window } // namespace mocktail diff --git a/src/window/window_creation_policy.h b/src/window/window_creation_policy.h index a2b8c17..b12d04f 100644 --- a/src/window/window_creation_policy.h +++ b/src/window/window_creation_policy.h @@ -3,12 +3,21 @@ #include +#include + namespace mocktail { namespace window { SDL_WindowFlags ApplyHighPixelDensityWindowFlag(SDL_WindowFlags flags, bool high_dpi); +// Native Wayland surfaces must receive their initial configure before Vulkan +// can reliably query the surface extent. X11/XWayland windows do not have +// that requirement, and waiting for an asynchronous window-manager state +// change (for example, restored maximization) can time out even though the +// X11 window is already usable. +bool DirectVulkanWindowRequiresInitialSync(std::string_view video_driver); + } // namespace window } // namespace mocktail diff --git a/tests/platform_cache_migration_test.cc b/tests/platform_cache_migration_test.cc index 7e543e9..7ba2eee 100644 --- a/tests/platform_cache_migration_test.cc +++ b/tests/platform_cache_migration_test.cc @@ -301,6 +301,45 @@ TEST(PlatformCacheMigrationTest, EXPECT_EQ(storage["UnrelatedPreference"], "keep"); } +TEST(PlatformCacheMigrationTest, + ReadsAccountThemeWithoutChangingRobloxStorage) { + TemporaryDirectory temporary; + FixturePaths fixture = PathsFor(temporary); + const nlohmann::json device_themes = { + {"42", "light"}, + {"99", "dark"}, + }; + ASSERT_TRUE(WriteJson(fixture.app_storage, + {{"AuthenticatedTheme", "dark"}, + {"DeviceLevelTheme", device_themes.dump()}, + {"UnrelatedPreference", "keep"}})); + const std::string original = ReadFile(fixture.app_storage); + + const RobloxThemeCacheResult account = + ReadRobloxThemeCache(fixture.app_storage, 42); + ASSERT_TRUE(account) << account.error; + ASSERT_TRUE(account.dark_theme.has_value()); + EXPECT_FALSE(*account.dark_theme); + + const RobloxThemeCacheResult fallback = + ReadRobloxThemeCache(fixture.app_storage, 7); + ASSERT_TRUE(fallback) << fallback.error; + ASSERT_TRUE(fallback.dark_theme.has_value()); + EXPECT_TRUE(*fallback.dark_theme); + EXPECT_EQ(ReadFile(fixture.app_storage), original); +} + +TEST(PlatformCacheMigrationTest, MissingRobloxThemeUsesCallerDefault) { + TemporaryDirectory temporary; + FixturePaths fixture = PathsFor(temporary); + + const RobloxThemeCacheResult result = + ReadRobloxThemeCache(fixture.app_storage, -1); + ASSERT_TRUE(result) << result.error; + EXPECT_FALSE(result.dark_theme.has_value()); + EXPECT_FALSE(std::filesystem::exists(fixture.app_storage)); +} + } // namespace } // namespace runtime } // namespace mocktail diff --git a/tests/roblox_desktop_app_policy_test.cc b/tests/roblox_desktop_app_policy_test.cc index 3d94a9c..233fb0d 100644 --- a/tests/roblox_desktop_app_policy_test.cc +++ b/tests/roblox_desktop_app_policy_test.cc @@ -98,7 +98,7 @@ TEST(RobloxDesktopAppPolicyTest, ASSERT_TRUE(WriteJson(default_path, {{"PlatformGroup", "Unknown"}})); const DesktopAppPolicyResult result = - ApplyDesktopAppPolicy(storage_path, default_path, 42); + ApplyDesktopAppPolicy(storage_path, default_path, 42, "dark"); ASSERT_TRUE(result) << result.error; EXPECT_TRUE(result.updated); EXPECT_EQ(result.normalized_policy_count, 1U); @@ -115,10 +115,11 @@ TEST(RobloxDesktopAppPolicyTest, ExpectDesktopLayout(user_policy); EXPECT_EQ(user_policy["EligibleForVideoCapture"], false); EXPECT_EQ(user_policy["AccountOwnedMarker"], "preserve"); + EXPECT_EQ(user_policy["ForceTheme"], "dark"); ExpectDesktopLayout(DecodePolicy(updated, "GUAC:-1:app-policy")); const DesktopAppPolicyResult repeated = - ApplyDesktopAppPolicy(storage_path, default_path, 42); + ApplyDesktopAppPolicy(storage_path, default_path, 42, "dark"); ASSERT_TRUE(repeated) << repeated.error; EXPECT_FALSE(repeated.updated); EXPECT_EQ(repeated.normalized_policy_count, 2U); @@ -134,7 +135,7 @@ TEST(RobloxDesktopAppPolicyTest, SeedsMissingStorageFromPayloadDefault) { {"PayloadDefaultMarker", 7}})); const DesktopAppPolicyResult result = - ApplyDesktopAppPolicy(storage_path, default_path, -1); + ApplyDesktopAppPolicy(storage_path, default_path, -1, "dark"); ASSERT_TRUE(result) << result.error; EXPECT_TRUE(result.app_storage_created); EXPECT_TRUE(result.updated); @@ -145,11 +146,26 @@ TEST(RobloxDesktopAppPolicyTest, SeedsMissingStorageFromPayloadDefault) { const nlohmann::json policy = DecodePolicy(configurations, "GUAC:-1:app-policy"); ExpectDesktopLayout(policy); + EXPECT_EQ(policy["ForceTheme"], "dark"); EXPECT_EQ(policy["PayloadDefaultMarker"], 7); EXPECT_EQ(nlohmann::json::parse(result.policy_json)["PayloadDefaultMarker"], 7); } +TEST(RobloxDesktopAppPolicyTest, RobloxThemeDoesNotForceCachedPolicy) { + TemporaryDirectory temporary; + const std::filesystem::path storage_path = temporary.path() / "storage.json"; + const std::filesystem::path default_path = temporary.path() / "default.json"; + ASSERT_TRUE(WriteJson(default_path, + {{"PlatformGroup", "Unknown"}, + {"ForceTheme", "dark"}})); + + const DesktopAppPolicyResult result = + ApplyDesktopAppPolicy(storage_path, default_path, 42, "roblox"); + ASSERT_TRUE(result) << result.error; + EXPECT_EQ(nlohmann::json::parse(result.policy_json)["ForceTheme"], ""); +} + TEST(RobloxDesktopAppPolicyTest, SelectsAuthenticatedEntitlementsAndSynchronizesCacheKeys) { TemporaryDirectory temporary; @@ -169,11 +185,12 @@ TEST(RobloxDesktopAppPolicyTest, ASSERT_TRUE(WriteJson(default_path, {{"PlatformGroup", "Unknown"}})); const DesktopAppPolicyResult result = - ApplyDesktopAppPolicy(storage_path, default_path, 42); + ApplyDesktopAppPolicy(storage_path, default_path, 42, "system"); ASSERT_TRUE(result) << result.error; const nlohmann::json selected = nlohmann::json::parse(result.policy_json); ExpectDesktopLayout(selected); EXPECT_EQ(selected["Entitlement"], "authenticated"); + EXPECT_EQ(selected["ForceTheme"], ""); const nlohmann::json updated = DecodeConfigurations(ReadJson(storage_path)); EXPECT_EQ(DecodePolicy(updated, "GUAC:-1:app-policy"), selected); @@ -210,14 +227,16 @@ TEST(RobloxDesktopAppPolicyTest, RejectsMalformedOrSymlinkedStorage) { std::ofstream output(malformed); output << "not-json"; } - EXPECT_FALSE(ApplyDesktopAppPolicy(malformed, default_path, 42)); + EXPECT_FALSE(ApplyDesktopAppPolicy(malformed, default_path, 42, "dark")); const std::filesystem::path outside = temporary.path() / "outside.json"; const std::filesystem::path linked = temporary.path() / "linked.json"; ASSERT_TRUE(WriteJson(outside, {{"Preserve", true}})); ASSERT_EQ(symlink(outside.c_str(), linked.c_str()), 0); - EXPECT_FALSE(ApplyDesktopAppPolicy(linked, default_path, 42)); + EXPECT_FALSE(ApplyDesktopAppPolicy(linked, default_path, 42, "dark")); EXPECT_EQ(ReadJson(outside)["Preserve"], true); + + EXPECT_FALSE(ApplyDesktopAppPolicy(outside, default_path, 42, "invalid")); } } // namespace diff --git a/tests/runtime_config_file_test.cc b/tests/runtime_config_file_test.cc index 504ef3e..cb4b555 100644 --- a/tests/runtime_config_file_test.cc +++ b/tests/runtime_config_file_test.cc @@ -100,6 +100,7 @@ TEST(RuntimeConfigBootstrapTest, CreatesCompletePrivateFirstRunFile) { ASSERT_TRUE(loaded) << loaded.error; EXPECT_TRUE(loaded.file_loaded); EXPECT_EQ(loaded.config.graphics_backend(), GraphicsBackend::kVulkan); + EXPECT_EQ(loaded.config.theme_mode(), "roblox"); EXPECT_EQ(loaded.config.frame_rate().mode, FrameRateLimitMode::kUnmanaged); EXPECT_EQ(loaded.config.vsync_mode(), "auto"); EXPECT_FALSE(loaded.config.performance().multithreaded_rendering); @@ -130,6 +131,9 @@ TEST(RuntimeConfigBootstrapTest, "profile.\ndevice: pc-windows-11", "# Boolean (default: false): start without a visible SDL " "window.\n headless: false", + "# String (default: roblox): use Roblox's saved account theme. " + "Supported\n # overrides: dark, light, or system (follow the " + "desktop color scheme).\n theme: roblox", "# Recommended values: direct-vulkan, opengl, system, " "angle-vulkan.\n backend: direct-vulkan", "# DFIntTaskSchedulerTargetFps without a whitelist.\n " @@ -257,6 +261,7 @@ TEST(RuntimeConfigFileTest, UsesDefaultsWhenFileDoesNotExist) { ASSERT_TRUE(loaded) << loaded.error; EXPECT_FALSE(loaded.file_loaded); EXPECT_EQ(loaded.config.graphics_backend(), GraphicsBackend::kVulkan); + EXPECT_EQ(loaded.config.theme_mode(), "roblox"); EXPECT_EQ(loaded.config.roblox_library_path(), "rbx_bin/libroblox.so"); } diff --git a/tests/runtime_config_test.cc b/tests/runtime_config_test.cc index f9345ae..357f224 100644 --- a/tests/runtime_config_test.cc +++ b/tests/runtime_config_test.cc @@ -45,6 +45,7 @@ TEST(RuntimeConfigTest, UsesSupportedDefaults) { EXPECT_EQ(config.window().title, "Roblox"); EXPECT_FALSE(config.window().high_dpi); EXPECT_TRUE(config.window().high_dpi_valid); + EXPECT_EQ(config.theme_mode(), "roblox"); EXPECT_FALSE(config.input_capabilities().touch_enabled); EXPECT_TRUE(config.input_capabilities().mouse_enabled); EXPECT_TRUE(config.input_capabilities().keyboard_enabled); @@ -97,6 +98,7 @@ TEST(RuntimeConfigTest, ReadsTypedRuntimeValues) { {"MOCKTAIL_WIN_HEIGHT", "1080"}, {"MOCKTAIL_WIN_TITLE", "Mocktail Test"}, {"MOCKTAIL_WIN_HIGH_DPI", "true"}, + {"MOCKTAIL_THEME", "system"}, {"MOCKTAIL_TOUCH_MODE", "on"}, {"MOCKTAIL_DESKTOP_PLAYABILITY", "0"}, {"MOCKTAIL_FRAME_RATE_LIMIT", "144"}, @@ -127,6 +129,7 @@ TEST(RuntimeConfigTest, ReadsTypedRuntimeValues) { EXPECT_EQ(config.window().title, "Mocktail Test"); EXPECT_TRUE(config.window().high_dpi); EXPECT_TRUE(config.window().high_dpi_valid); + EXPECT_EQ(config.theme_mode(), "system"); EXPECT_TRUE(config.input_capabilities().touch_enabled); EXPECT_FALSE(config.desktop_playability()); EXPECT_FALSE(config.roblox_http_user_agent().has_value()); diff --git a/tests/window_creation_policy_test.cc b/tests/window_creation_policy_test.cc index d9606fd..8687d62 100644 --- a/tests/window_creation_policy_test.cc +++ b/tests/window_creation_policy_test.cc @@ -32,6 +32,16 @@ TEST(WindowCreationPolicyTest, EnablesHighDensityForEveryWindowBackend) { } } +TEST(WindowCreationPolicyTest, SynchronizesInitialDirectVulkanWaylandMap) { + EXPECT_TRUE(DirectVulkanWindowRequiresInitialSync("wayland")); +} + +TEST(WindowCreationPolicyTest, DoesNotSynchronizeInitialX11Map) { + EXPECT_FALSE(DirectVulkanWindowRequiresInitialSync("x11")); + EXPECT_FALSE(DirectVulkanWindowRequiresInitialSync("dummy")); + EXPECT_FALSE(DirectVulkanWindowRequiresInitialSync("")); +} + } // namespace } // namespace window } // namespace mocktail