Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ Release notes on GitHub are taken from the matching section here.
- The asset overlay folder now also replaces files Roblox downloads and no longer ships, such as the shift
lock and text cursors. Put the file under `content/`, at the path Roblox uses. Thanks to @dim-ghub for the
idea.
- `input.system_cursor` in `config.yaml`, or System cursors in the settings window, shows your desktop's
own arrow, hand and text cursors instead of the ones Roblox draws. A game's custom cursor and the shift
lock cursor are still drawn by Roblox. If a Roblox update changes the code this relies on, Nightcap logs
that and keeps Roblox's cursors. `MOCKTAIL_CURSOR_TRACE=1` prints each cursor Roblox draws. On Wayland,
the desktop cursor also stays at the press point while you hold the right mouse button to turn the camera.
- `MOCKTAIL_ASSET_TRACE=1` now also prints the files Roblox asks for and does not get, as `[asset] miss`.
- Arch Linux: `nightcap-git` is on the AUR and builds the current `main` branch. Thanks to @dim-ghub, who
maintains it.
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@ add_library(mocktail_runtime STATIC
src/runtime/process_launch_policy.cc
src/runtime/process_diagnostics.cc
src/runtime/asset_overlay_names.cc
src/runtime/roblox_cursor_hook.cc
src/runtime/runtime_config.cc
src/runtime/runtime_config_bootstrap.cc
src/runtime/runtime_config_editor.cc
Expand Down
4 changes: 4 additions & 0 deletions config/mocktail.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ input:
# the two disagree. Turn this on when first-person aiming feels too slow or
# too fast under fractional scaling. The pointer position is unaffected.
raw_mouse: false
# Boolean (default: false): show the desktop's own arrow, hand and text
# cursors instead of the ones Roblox draws. A game's custom cursor and the
# shift lock cursor are still drawn by Roblox.
system_cursor: false

integrations:
fleasion:
Expand Down
82 changes: 82 additions & 0 deletions include/runtime/roblox_cursor_hook.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#ifndef MOCKTAIL_RUNTIME_ROBLOX_CURSOR_HOOK_H_
#define MOCKTAIL_RUNTIME_ROBLOX_CURSOR_HOOK_H_

#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

namespace mocktail {
namespace runtime {

// What Roblox draws as the mouse cursor, by the system cursor that can stand
// in for it. kOther is a game's own cursor, the shift lock cursor, or none:
// Roblox keeps drawing those itself.
enum class RobloxCursorKind : std::uint8_t {
kOther,
kArrow,
kPointer,
kText,
};

// Classifies the content ID of the cursor Roblox draws, such as
// rbxasset://textures/Cursors/KeyboardMouse/ArrowFarCursor.png. Only the
// stock keyboard-and-mouse cursors have a system equivalent.
RobloxCursorKind ClassifyRobloxCursorContent(std::string_view content);

// True for an asset path that names one of those stock cursor textures, with
// or without a scheme or a leading directory, such as
// android/textures/Cursors/KeyboardMouse/IBeamCursor.png.
bool IsStockSystemCursorAsset(std::string_view path);

// The rbxasset:// names of the stock cursor textures. Unmapping them makes
// Roblox open the local file for one it would otherwise download.
std::vector<std::string> StockSystemCursorAssetNames();

// A fully transparent 64x64 PNG that replaces each stock cursor texture, so
// Roblox draws nothing under the system cursor.
const std::uint8_t* TransparentCursorPng(std::size_t* size);

// One loaded ELF segment, addressed as the code that runs from it sees it.
struct CursorHookSegment {
std::uintptr_t address = 0;
const std::uint8_t* data = nullptr;
std::size_t size = 0;
bool executable = false;
};

// Where the hooks go in Roblox's per-frame cursor draw.
struct CursorRenderSite {
// Entry of the function that picks and draws the cursor.
std::uintptr_t render_function = 0;
// The `call` that reads the type of the cursor content just before the
// draw. Its argument points at that content.
std::uintptr_t content_type_call = 0;
// What that call reaches.
std::uintptr_t content_type_function = 0;
};

// Finds the draw function through the one instruction that loads its
// "[FLog::CursorCache] Invalid asset ID" message, then checks the code shape
// both hooks rely on. Fails rather than guess when any part does not match.
std::optional<CursorRenderSite> LocateCursorRenderSite(
const std::vector<CursorHookSegment>& segments, std::string* error);

using RobloxCursorObserver = void (*)(RobloxCursorKind kind);

// Hooks the cursor draw of the libroblox.so loaded at `image_base`, which no
// code may have run from yet. The observer is called from Roblox's render
// thread each time Roblox draws or skips the cursor. Returns false, changing
// nothing, when the code does not match.
bool InstallRobloxCursorHooks(std::uintptr_t image_base,
RobloxCursorObserver observer,
std::string* error);

bool RobloxCursorHooksInstalled();

} // namespace runtime
} // namespace mocktail

#endif // MOCKTAIL_RUNTIME_ROBLOX_CURSOR_HOOK_H_
4 changes: 4 additions & 0 deletions include/runtime/runtime_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ struct InputCapabilityConfig {
// surface units. The two differ only when the desktop display scale and the
// window's pixel density disagree, as under fractional scaling.
bool raw_mouse = false;
// Show the desktop's arrow, hand and text cursors in place of the stock
// ones Roblox draws. Custom game cursors and the mouse-lock cursor are
// still drawn by Roblox.
bool system_cursor = false;
};

struct NetworkProxyConfig {
Expand Down
42 changes: 41 additions & 1 deletion src/legacy/legacy_runtime.cc
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
#include "runtime/owned_pthread.h"
#include "runtime/platform_cache_migration.h"
#include "runtime/asset_overlay_names.h"
#include "runtime/roblox_cursor_hook.h"
#include "runtime/roblox_app_lifecycle.h"
#include "runtime/roblox_capability_resolver.h"
#include "runtime/roblox_platform_web_symbols.h"
Expand Down Expand Up @@ -98,6 +99,8 @@
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};
// input.system_cursor, read before libroblox.so loads.
std::atomic<bool> g_system_cursor_requested{false};
std::atomic<const mocktail::compat::HostAbiProfile*>
g_active_host_abi_profile{nullptr};

Expand Down Expand Up @@ -1970,8 +1973,17 @@ extern "C" void MocktailConstructorWrapper() {
// 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(
std::vector<std::string> names = mocktail::runtime::OverlayRbxAssetNames(
CurrentRuntimePaths().asset_overlay_dir());
// Stock cursors become transparent local files under the system cursor.
if (mocktail::runtime::RobloxCursorHooksInstalled()) {
for (std::string& name :
mocktail::runtime::StockSystemCursorAssetNames()) {
if (std::find(names.begin(), names.end(), name) == names.end()) {
names.push_back(std::move(name));
}
}
}
const long page_size = sysconf(_SC_PAGESIZE);
if (names.empty() || page_size <= 0) {
return;
Expand Down Expand Up @@ -2007,6 +2019,31 @@ void DisableOverlaidAssetMappings(uintptr_t base) {
<< std::flush;
}

void PublishRobloxCursor(mocktail::runtime::RobloxCursorKind kind) {
using mocktail::runtime::RobloxCursorKind;
using mocktail::window::CursorShape;
mocktail::window::PublishRobloxCursor(
kind == RobloxCursorKind::kArrow ? CursorShape::kArrow
: kind == RobloxCursorKind::kPointer ? CursorShape::kPointer
: kind == RobloxCursorKind::kText ? CursorShape::kText
: CursorShape::kHidden);
}

// Must run before any libroblox code does: it patches the cursor draw.
void InstallSystemCursorHooks(uintptr_t base) {
if (!g_system_cursor_requested.load(std::memory_order_acquire)) {
return;
}
std::string error;
if (!mocktail::runtime::InstallRobloxCursorHooks(base, &PublishRobloxCursor,
&error)) {
std::cerr << " [cursor] system cursors unavailable for this Roblox "
"version, keeping Roblox's own: "
<< error << '\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 ||
Expand All @@ -2015,6 +2052,7 @@ extern "C" void mocktail_before_soinfo_constructors(const char* realpath,
}

g_libroblox_base = base;
InstallSystemCursorHooks(base);
DisableOverlaidAssetMappings(base);
g_mocktail_abort_libroblox_base = base;
g_libroblox_base_static = base;
Expand Down Expand Up @@ -4691,6 +4729,8 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
"or dark\n";
return EXIT_FAILURE;
}
g_system_cursor_requested.store(input_capabilities.system_cursor,
std::memory_order_release);
setenv("MOCKTAIL_TOUCH_ENABLED_INTERNAL",
input_capabilities.touch_enabled ? "1" : "0", 1);
setenv("MOCKTAIL_MOUSE_ENABLED_INTERNAL",
Expand Down
Loading