mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-17 17:00:55 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
220c083e83 | ||
|
|
d778a5bc8a | ||
|
|
0a039628db | ||
|
|
b40fd5d2d6 | ||
|
|
5fb44c66e6 | ||
|
|
5d636749fa | ||
|
|
88b7382577 | ||
|
|
3ce39c1f39 | ||
|
|
6aa22c257d | ||
|
|
e5cf5e3de4 | ||
|
|
36733cfb75 | ||
|
|
ca760f111b | ||
|
|
bfb27b4104 | ||
|
|
5663fe7d25 |
No files matched your search
+20
-115
@@ -3,7 +3,27 @@ name: CI
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- '.github/workflows/**'
|
||||
- 'CMakeLists.txt'
|
||||
- 'Makefile'
|
||||
- 'README.md'
|
||||
- 'config/**'
|
||||
- 'packaging/**'
|
||||
- 'scripts/**'
|
||||
- 'stubs/**'
|
||||
- 'tests/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/**'
|
||||
- 'CMakeLists.txt'
|
||||
- 'Makefile'
|
||||
- 'README.md'
|
||||
- 'config/**'
|
||||
- 'packaging/**'
|
||||
- 'scripts/**'
|
||||
- 'stubs/**'
|
||||
- 'tests/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -38,118 +58,3 @@ jobs:
|
||||
bash tests/managed_payload_build_contract_test.sh .
|
||||
bash tests/flatpak_packaging_test.sh .
|
||||
bash tests/native_packaging_test.sh .
|
||||
|
||||
flatpak:
|
||||
name: Flatpak build
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Flatpak builder
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes flatpak flatpak-builder gnupg
|
||||
flatpak remote-add --user --if-not-exists flathub \
|
||||
https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
|
||||
- name: Build Flatpak repository and bundle
|
||||
shell: bash
|
||||
env:
|
||||
FLATPAK_GPG_PRIVATE_KEY: ${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}
|
||||
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
git update-ref refs/heads/main "$GITHUB_SHA"
|
||||
builder_signing=()
|
||||
bundle_keys=()
|
||||
publish=false
|
||||
if [[ "$GITHUB_REF" == refs/heads/main && \
|
||||
"$GITHUB_EVENT_NAME" != pull_request ]]; then
|
||||
publish=true
|
||||
[[ -n "$FLATPAK_GPG_PRIVATE_KEY" && \
|
||||
-n "$FLATPAK_GPG_KEY_ID" ]]
|
||||
install -d -m 0700 "$RUNNER_TEMP/mocktail-gnupg"
|
||||
printf '%s' "$FLATPAK_GPG_PRIVATE_KEY" | \
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" --import
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" \
|
||||
--export "$FLATPAK_GPG_KEY_ID" >mocktail-flatpak.gpg
|
||||
builder_signing=(
|
||||
"--gpg-sign=$FLATPAK_GPG_KEY_ID"
|
||||
"--gpg-homedir=$RUNNER_TEMP/mocktail-gnupg"
|
||||
)
|
||||
bundle_keys=(--gpg-keys=mocktail-flatpak.gpg)
|
||||
fi
|
||||
flatpak-builder \
|
||||
--arch=x86_64 \
|
||||
--jobs=4 \
|
||||
--user \
|
||||
--install-deps-from=flathub \
|
||||
--disable-rofiles-fuse \
|
||||
--force-clean \
|
||||
--default-branch=stable \
|
||||
--repo=repo \
|
||||
"${builder_signing[@]}" \
|
||||
build-flatpak-ci \
|
||||
packaging/flatpak/space.bigrat.mocktail.json
|
||||
if [[ "$publish" == true ]]; then
|
||||
flatpak build-update-repo \
|
||||
--generate-static-deltas \
|
||||
--static-delta-jobs=2 \
|
||||
--prune \
|
||||
--gpg-sign="$FLATPAK_GPG_KEY_ID" \
|
||||
--gpg-homedir="$RUNNER_TEMP/mocktail-gnupg" \
|
||||
repo
|
||||
fi
|
||||
flatpak build-bundle \
|
||||
--arch=x86_64 \
|
||||
"${bundle_keys[@]}" \
|
||||
repo \
|
||||
Mocktail-x86_64.flatpak \
|
||||
space.bigrat.mocktail \
|
||||
stable
|
||||
if [[ "$publish" == true ]]; then
|
||||
./scripts/assemble_flatpak_pages.sh \
|
||||
repo Mocktail-x86_64.flatpak mocktail-flatpak.gpg public
|
||||
fi
|
||||
|
||||
- name: Upload Flatpak
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-x86_64-flatpak
|
||||
path: Mocktail-x86_64.flatpak
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
- name: Configure GitHub Pages
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
||||
|
||||
- name: Upload GitHub Pages repository
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
||||
with:
|
||||
path: public
|
||||
|
||||
deploy-pages:
|
||||
name: Publish Flatpak repository
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
needs: flatpak
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
|
||||
@@ -0,0 +1,168 @@
|
||||
name: Flatpak
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- '.github/workflows/flatpak.yml'
|
||||
- '.gitmodules'
|
||||
- 'CMakeLists.txt'
|
||||
- 'LICENSE'
|
||||
- 'assets/screenshots/**'
|
||||
- 'cmake/**'
|
||||
- 'config/**'
|
||||
- 'include/**'
|
||||
- 'packaging/flatpak/**'
|
||||
- 'packaging/discord-join.html'
|
||||
- 'packaging/icons/**'
|
||||
- 'packaging/space.bigrat.mocktail.desktop'
|
||||
- 'packaging/space.bigrat.mocktail.metainfo.xml'
|
||||
- 'packaging/space.bigrat.mocktail.svg'
|
||||
- 'scripts/assemble_flatpak_pages.sh'
|
||||
- 'src/**'
|
||||
- 'stubs/**'
|
||||
- 'third_party/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/flatpak.yml'
|
||||
- '.gitmodules'
|
||||
- 'CMakeLists.txt'
|
||||
- 'LICENSE'
|
||||
- 'assets/screenshots/**'
|
||||
- 'cmake/**'
|
||||
- 'config/**'
|
||||
- 'include/**'
|
||||
- 'packaging/flatpak/**'
|
||||
- 'packaging/discord-join.html'
|
||||
- 'packaging/icons/**'
|
||||
- 'packaging/space.bigrat.mocktail.desktop'
|
||||
- 'packaging/space.bigrat.mocktail.metainfo.xml'
|
||||
- 'packaging/space.bigrat.mocktail.svg'
|
||||
- 'scripts/assemble_flatpak_pages.sh'
|
||||
- 'src/**'
|
||||
- 'stubs/**'
|
||||
- 'third_party/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: flatpak-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
flatpak:
|
||||
name: Flatpak build
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install Flatpak builder
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes flatpak flatpak-builder gnupg
|
||||
flatpak remote-add --user --if-not-exists flathub \
|
||||
https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
|
||||
- name: Build Flatpak repository and bundle
|
||||
shell: bash
|
||||
env:
|
||||
FLATPAK_GPG_PRIVATE_KEY: ${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}
|
||||
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
git update-ref refs/heads/main "$GITHUB_SHA"
|
||||
builder_signing=()
|
||||
bundle_keys=()
|
||||
publish=false
|
||||
if [[ "$GITHUB_REF" == refs/heads/main && \
|
||||
"$GITHUB_EVENT_NAME" != pull_request ]]; then
|
||||
publish=true
|
||||
[[ -n "$FLATPAK_GPG_PRIVATE_KEY" && \
|
||||
-n "$FLATPAK_GPG_KEY_ID" ]]
|
||||
install -d -m 0700 "$RUNNER_TEMP/mocktail-gnupg"
|
||||
printf '%s' "$FLATPAK_GPG_PRIVATE_KEY" | \
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" --import
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" \
|
||||
--export "$FLATPAK_GPG_KEY_ID" >mocktail-flatpak.gpg
|
||||
builder_signing=(
|
||||
"--gpg-sign=$FLATPAK_GPG_KEY_ID"
|
||||
"--gpg-homedir=$RUNNER_TEMP/mocktail-gnupg"
|
||||
)
|
||||
bundle_keys=(--gpg-keys=mocktail-flatpak.gpg)
|
||||
fi
|
||||
flatpak-builder \
|
||||
--arch=x86_64 \
|
||||
--jobs=4 \
|
||||
--user \
|
||||
--install-deps-from=flathub \
|
||||
--disable-rofiles-fuse \
|
||||
--force-clean \
|
||||
--default-branch=stable \
|
||||
--repo=repo \
|
||||
"${builder_signing[@]}" \
|
||||
build-flatpak-ci \
|
||||
packaging/flatpak/space.bigrat.mocktail.json
|
||||
if [[ "$publish" == true ]]; then
|
||||
flatpak build-update-repo \
|
||||
--generate-static-deltas \
|
||||
--static-delta-jobs=2 \
|
||||
--prune \
|
||||
--gpg-sign="$FLATPAK_GPG_KEY_ID" \
|
||||
--gpg-homedir="$RUNNER_TEMP/mocktail-gnupg" \
|
||||
repo
|
||||
fi
|
||||
flatpak build-bundle \
|
||||
--arch=x86_64 \
|
||||
"${bundle_keys[@]}" \
|
||||
repo \
|
||||
Mocktail-x86_64.flatpak \
|
||||
space.bigrat.mocktail \
|
||||
stable
|
||||
if [[ "$publish" == true ]]; then
|
||||
./scripts/assemble_flatpak_pages.sh \
|
||||
repo Mocktail-x86_64.flatpak mocktail-flatpak.gpg public
|
||||
fi
|
||||
|
||||
- name: Upload Flatpak
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-x86_64-flatpak
|
||||
path: Mocktail-x86_64.flatpak
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
- name: Configure GitHub Pages
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
||||
|
||||
- name: Upload GitHub Pages repository
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
||||
with:
|
||||
path: public
|
||||
|
||||
deploy-pages:
|
||||
name: Publish Flatpak repository
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
needs: flatpak
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
|
||||
@@ -3,7 +3,31 @@ name: Native packages
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- '.github/workflows/ci.yml'
|
||||
- '.github/workflows/flatpak.yml'
|
||||
- '.gitignore'
|
||||
- 'README.md'
|
||||
- 'assets/screenshots/**'
|
||||
- 'docs/**'
|
||||
- 'packaging/aur/**'
|
||||
- 'packaging/flatpak/**'
|
||||
- 'scripts/assemble_flatpak_pages.sh'
|
||||
- 'scripts/build_flatpak.sh'
|
||||
- 'tests/**'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '.github/workflows/ci.yml'
|
||||
- '.github/workflows/flatpak.yml'
|
||||
- '.gitignore'
|
||||
- 'README.md'
|
||||
- 'assets/screenshots/**'
|
||||
- 'docs/**'
|
||||
- 'packaging/aur/**'
|
||||
- 'packaging/flatpak/**'
|
||||
- 'scripts/assemble_flatpak_pages.sh'
|
||||
- 'scripts/build_flatpak.sh'
|
||||
- 'tests/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
+17
-1
@@ -8,7 +8,7 @@
|
||||
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(Mocktail
|
||||
VERSION 0.1.0
|
||||
VERSION 1.0.1
|
||||
DESCRIPTION "Android x86-64 Roblox compatibility runtime for Linux"
|
||||
LANGUAGES CXX
|
||||
)
|
||||
@@ -26,6 +26,18 @@ set(MOCKTAIL_DEFAULT_SIGNING_TRUST_MANIFEST
|
||||
CACHE FILEPATH
|
||||
"Fallback signing trust manifest used outside an installed runtime"
|
||||
)
|
||||
set(MOCKTAIL_DISCORD_APPLICATION_ID "1537088975720812655" CACHE STRING
|
||||
"Discord application ID baked into Mocktail Rich Presence releases"
|
||||
)
|
||||
string(LENGTH "${MOCKTAIL_DISCORD_APPLICATION_ID}"
|
||||
MOCKTAIL_DISCORD_APPLICATION_ID_LENGTH)
|
||||
if(NOT MOCKTAIL_DISCORD_APPLICATION_ID STREQUAL "" AND
|
||||
(NOT MOCKTAIL_DISCORD_APPLICATION_ID MATCHES "^[0-9]+$" OR
|
||||
MOCKTAIL_DISCORD_APPLICATION_ID_LENGTH LESS 17 OR
|
||||
MOCKTAIL_DISCORD_APPLICATION_ID_LENGTH GREATER 20))
|
||||
message(FATAL_ERROR
|
||||
"MOCKTAIL_DISCORD_APPLICATION_ID must be empty or a Discord snowflake")
|
||||
endif()
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
@@ -221,6 +233,7 @@ target_include_directories(mocktail_legacy_jnivm PUBLIC
|
||||
add_library(mocktail_runtime STATIC
|
||||
src/runtime/command_line.cc
|
||||
src/runtime/crash_report_policy.cc
|
||||
src/runtime/discord_rpc.cc
|
||||
src/runtime/device_profile.cc
|
||||
src/runtime/environment.cc
|
||||
src/runtime/external_launch_broker.cc
|
||||
@@ -256,6 +269,9 @@ target_include_directories(mocktail_runtime PUBLIC include)
|
||||
target_include_directories(mocktail_runtime PUBLIC
|
||||
${MOCKTAIL_JNI_INCLUDE_DIRS}
|
||||
)
|
||||
target_compile_definitions(mocktail_runtime PRIVATE
|
||||
"MOCKTAIL_DISCORD_APPLICATION_ID=\"${MOCKTAIL_DISCORD_APPLICATION_ID}\""
|
||||
)
|
||||
mocktail_apply_compile_options(mocktail_runtime)
|
||||
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND
|
||||
CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 16)
|
||||
|
||||
@@ -134,7 +134,7 @@ run-angle: ## Run with ANGLE/Vulkan compatibility backend
|
||||
update-roblox: ## Validate and import the current x86_64 Roblox bundle cached by Sober
|
||||
@./scripts/update_roblox_payload.sh
|
||||
|
||||
update-auto: build ## Download latest Roblox, derive its ABI, canary twice and activate safely
|
||||
update-auto: build ## Download latest Roblox, derive its ABI, run two canaries, then activate
|
||||
@./$(BUILD_DIR)/mocktail_updater update
|
||||
|
||||
update-auto-launch: build ## Update Roblox and launch the verified current payload
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
[](LICENSE)
|
||||
|
||||
Mocktail runs the Android `x86_64` Roblox client on Linux. It provides the
|
||||
Android ABI and JNI pieces the client expects, then uses SDL3 and Vulkan or
|
||||
OpenGL for the Linux side.
|
||||
Android ABI and JNI pieces the client expects, then connects them to SDL3 and
|
||||
Vulkan or OpenGL on the Linux side.
|
||||
|
||||
Mocktail is still experimental and may break after a Roblox update. It is not
|
||||
affiliated with Roblox Corporation or VinegarHQ.
|
||||
Mocktail is an independent community project. It is not affiliated with Roblox
|
||||
Corporation or VinegarHQ and does not distribute the Roblox client.
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -25,7 +25,7 @@ an update fails.
|
||||
## Install with Flatpak
|
||||
|
||||
```bash
|
||||
flatpak install --user https://komaruworld.github.io/mocktail/mocktail.flatpakref
|
||||
flatpak install --user https://mocktail.bigrat.space/mocktail.flatpakref
|
||||
flatpak run space.bigrat.mocktail
|
||||
```
|
||||
|
||||
@@ -44,16 +44,14 @@ Use `mocktail` instead of `mocktail-git` to build the pinned release from
|
||||
source, or install the stable prebuilt package with `paru -S mocktail-bin` or
|
||||
`yay -S mocktail-bin`.
|
||||
|
||||
## Screenshots
|
||||
|
||||
<details>
|
||||
<summary>Flatpak build</summary>
|
||||
<summary>Screenshots</summary>
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
</details>
|
||||
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -5,7 +5,7 @@ include_guard(GLOBAL)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(UTF8PROC REQUIRED IMPORTED_TARGET libutf8proc)
|
||||
pkg_check_modules(SDL3_TTF REQUIRED IMPORTED_TARGET sdl3-ttf)
|
||||
find_package(SDL3_ttf REQUIRED CONFIG)
|
||||
pkg_check_modules(FONTCONFIG REQUIRED IMPORTED_TARGET fontconfig)
|
||||
|
||||
get_filename_component(MOCKTAIL_INPUT_ROOT "${CMAKE_CURRENT_LIST_DIR}/.."
|
||||
@@ -35,7 +35,7 @@ target_link_libraries(mocktail_input_runtime PUBLIC
|
||||
PkgConfig::UTF8PROC
|
||||
PRIVATE
|
||||
PkgConfig::FONTCONFIG
|
||||
PkgConfig::SDL3_TTF
|
||||
SDL3_ttf::SDL3_ttf
|
||||
)
|
||||
target_compile_features(mocktail_input_runtime PUBLIC cxx_std_17)
|
||||
mocktail_apply_compile_options(mocktail_input_runtime)
|
||||
|
||||
@@ -64,6 +64,32 @@ audio:
|
||||
# To pin output, copy an exact SDL device name printed during startup.
|
||||
output_device: default
|
||||
|
||||
integrations:
|
||||
discord_rpc:
|
||||
# Boolean (default: false): publish Mocktail activity to Discord Desktop.
|
||||
# This never signs in to Discord and never reads an account token.
|
||||
enabled: false
|
||||
# Boolean (default: true): show the current Roblox experience name.
|
||||
show_place_name: true
|
||||
# Boolean (default: true): show how long the current session has run.
|
||||
show_elapsed_time: true
|
||||
join:
|
||||
# Boolean (default: true): let friends open the current experience. When
|
||||
# Roblox provides a public server ID, the button targets that server.
|
||||
enabled: true
|
||||
# Boolean (default: true): never expose private or reserved joins.
|
||||
public_servers_only: true
|
||||
# button_label: Join Server
|
||||
# English defaults. Uncomment only the lines you want to customize.
|
||||
# text:
|
||||
# browsing: Browsing experiences
|
||||
# joining: Joining an experience
|
||||
# playing: "{place_name}"
|
||||
# state: Playing Roblox
|
||||
# unknown_place: Unknown experience
|
||||
# Advanced: overrides the application bundled by the package maintainer.
|
||||
# application_id: 123456789012345678
|
||||
|
||||
window:
|
||||
# Integer (default: 1280): initial window width in pixels.
|
||||
width: 1280
|
||||
|
||||
@@ -19,16 +19,8 @@ namespace mocktail::compat {
|
||||
|
||||
using BionicAtForkCallback = void (*)(void);
|
||||
|
||||
// Registers Android's __register_atfork callback contract with the host
|
||||
// pthread runtime. Bionic associates registrations with dso_handle and
|
||||
// removes them when that DSO unloads. POSIX pthread_atfork has no matching
|
||||
// unregister operation, so the host bridge accepts dso_handle for ABI
|
||||
// compatibility but cannot use it for cleanup.
|
||||
//
|
||||
// Callback code must remain mapped for every later fork. In practice, a guest
|
||||
// DSO that has registered callbacks must stay loaded until process exit;
|
||||
// unloading is safe only when the process guarantees that it will never fork
|
||||
// again.
|
||||
// POSIX cannot unregister by dso_handle, so registered guest callbacks must
|
||||
// remain mapped for every later fork.
|
||||
int RegisterBionicAtFork(BionicAtForkCallback prepare,
|
||||
BionicAtForkCallback parent,
|
||||
BionicAtForkCallback child, void* dso_handle) noexcept;
|
||||
@@ -37,8 +29,6 @@ int RegisterBionicAtFork(BionicAtForkCallback prepare,
|
||||
|
||||
extern "C" {
|
||||
|
||||
// Exact LP64 Android/Bionic __register_atfork signature. The prefixed host
|
||||
// symbol is exported to the guest as "__register_atfork" by synthetic libc.so.
|
||||
int mocktail_bionic_register_atfork(
|
||||
mocktail::compat::BionicAtForkCallback prepare,
|
||||
mocktail::compat::BionicAtForkCallback parent,
|
||||
|
||||
@@ -22,11 +22,8 @@
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
// Android x86-64's malloc.h declares ten size_t fields in this order. This is
|
||||
// not the host's struct mallinfo: glibc's legacy type uses int,
|
||||
// and musl exposes no allocator-telemetry API. On hosts without a compatible
|
||||
// size_t-based telemetry API, BionicMallinfo returns an all-zero "unknown"
|
||||
// snapshot. Callers must not interpret that fallback as proof of zero usage.
|
||||
// Android uses size_t fields here; glibc uses int and musl has no equivalent.
|
||||
// An all-zero fallback means unknown, not zero usage.
|
||||
struct BionicMallinfoSnapshot {
|
||||
size_t arena = 0;
|
||||
size_t ordblks = 0;
|
||||
@@ -44,10 +41,7 @@ struct BionicLocaleState;
|
||||
using BionicLocale = BionicLocaleState*;
|
||||
using BionicThreadDestructor = void (*)(void*);
|
||||
|
||||
// Do not use the host socket structs here. Although glibc happens to match
|
||||
// Android LP64, musl declares cmsg_len and msg_controllen as 32-bit socklen_t.
|
||||
// Bionic x86-64 uses size_t for both, as audited from the vendored
|
||||
// bionic/libc/include/sys/socket.h.
|
||||
// Bionic uses size_t where musl uses 32-bit socklen_t.
|
||||
struct BionicMessageHeader {
|
||||
void* msg_name = nullptr;
|
||||
uint32_t msg_namelen = 0;
|
||||
@@ -64,35 +58,24 @@ struct BionicControlMessageHeader {
|
||||
int cmsg_type = 0;
|
||||
};
|
||||
|
||||
// Implements Android's exported CMSG_NXTHDR helper over the explicit Bionic
|
||||
// x86-64 msghdr/cmsghdr layout above. Malformed or truncated control buffers
|
||||
// fail closed with nullptr.
|
||||
// Malformed or truncated control buffers return nullptr.
|
||||
BionicControlMessageHeader* BionicCmsgNextHeader(
|
||||
BionicMessageHeader* message, BionicControlMessageHeader* current) noexcept;
|
||||
|
||||
// Registers one destructor for the current thread using libstdc++'s Itanium
|
||||
// C++ ABI implementation. That implementation is present on both supported
|
||||
// glibc and musl toolchains and preserves destructor execution at thread exit.
|
||||
// Uses libstdc++'s Itanium ABI on both supported libc implementations.
|
||||
int BionicCxaThreadAtExit(BionicThreadDestructor destructor, void* argument,
|
||||
void* dso_handle) noexcept;
|
||||
|
||||
// Fills the complete buffer from the Linux kernel RNG. The implementation
|
||||
// falls back to /dev/urandom only when getrandom is unavailable or blocked and
|
||||
// aborts rather than returning predictable or partially initialized bytes.
|
||||
// Falls back to /dev/urandom; never returns partial or predictable bytes.
|
||||
void BionicArc4RandomBuffer(void* buffer, size_t size) noexcept;
|
||||
|
||||
BionicMallinfoSnapshot BionicMallinfo() noexcept;
|
||||
bool BionicMallinfoHasHostTelemetry() noexcept;
|
||||
|
||||
// Formats one Linux errno value using Bionic's POSIX strerror_r contract.
|
||||
// Unlike glibc's GNU variant, success is reported as zero; truncation returns
|
||||
// -1 and sets errno to ERANGE while preserving the caller's errno on success.
|
||||
// POSIX strerror_r contract: zero on success, -1/ERANGE on truncation.
|
||||
int BionicStrError(int error_number, char* buffer, size_t buffer_size) noexcept;
|
||||
|
||||
// Bionic supports only C and C.UTF-8, whose integer parsing is identical. A
|
||||
// host C locale is installed only for the duration of each call, so ambient
|
||||
// process locale cannot change guest parsing and a Bionic locale pointer is
|
||||
// never mistaken for an incompatible host locale_t.
|
||||
// Parses under host C locale without passing an incompatible Bionic locale_t.
|
||||
long long BionicStrToLongLongLocale(const char* text, char** end, int base,
|
||||
BionicLocale locale) noexcept;
|
||||
unsigned long long BionicStrToUnsignedLongLongLocale(
|
||||
|
||||
@@ -23,30 +23,21 @@ namespace mocktail::compat {
|
||||
|
||||
using NativeThreadInitializer = void (*)();
|
||||
|
||||
// Pinned Bionic LP64 pthread_internal.h keeps total per-thread reservation
|
||||
// near 1 MiB by subtracting its 32 KiB alternate signal stack from the normal
|
||||
// thread stack. Host defaults are not ABI substitutes: musl's default is only
|
||||
// 128 KiB on the supported Void runtime.
|
||||
// Bionic reserves 32 KiB of its 1 MiB thread allocation for the signal stack.
|
||||
inline constexpr size_t kBionicLp64DefaultThreadStackSize =
|
||||
(1U * 1024U * 1024U) - (32U * 1024U);
|
||||
static_assert(kBionicLp64DefaultThreadStackSize == 1015808U);
|
||||
|
||||
// Used only after a Bionic-sized stack is rejected by the host at thread
|
||||
// creation time. The retry takes the largest of this floor, the requested
|
||||
// guest size, and the host default. That retains enough stack on musl without
|
||||
// shrinking glibc's default.
|
||||
// Retry floor when the host rejects the Bionic-sized stack.
|
||||
inline constexpr size_t kBionicLp64FallbackThreadStackSize = 2U * 1024U * 1024U;
|
||||
|
||||
// Installs the payload-owned per-thread initializer used by subsequently
|
||||
// created guest threads. Passing nullptr restores ordinary host behavior.
|
||||
// nullptr restores host behavior.
|
||||
void ConfigureBionicPthreadThreadInitializer(
|
||||
NativeThreadInitializer initializer) noexcept;
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
// Creates a host thread after rebuilding the supported POSIX attributes.
|
||||
// Rebuilding avoids passing ABI-private pthread_attr_t state from the Android
|
||||
// payload directly into glibc.
|
||||
// Rebuilds attributes instead of passing Android pthread_attr_t to glibc.
|
||||
extern "C" int mocktail_bionic_pthread_create(pthread_t* thread,
|
||||
const pthread_attr_t* attr,
|
||||
void* (*start_routine)(void*),
|
||||
|
||||
@@ -31,9 +31,8 @@ constexpr uint32_t kBionicPthreadKeyValidFlag = uint32_t{1} << 31;
|
||||
|
||||
using BionicPthreadKeyDestructor = void (*)(void*);
|
||||
|
||||
// implementation of Bionic's pthread_key_* ABI. It owns only guest
|
||||
// keys registered through the synthetic Android libc.so, while ordinary host
|
||||
// code continues to use glibc pthread keys.
|
||||
// Implements Bionic's pthread_key_* ABI for keys registered through the
|
||||
// synthetic libc.so. Host code continues to use glibc pthread keys.
|
||||
class BionicPthreadKeyRuntime {
|
||||
public:
|
||||
static BionicPthreadKeyRuntime& Instance() noexcept;
|
||||
|
||||
@@ -62,7 +62,7 @@ enum class NativePreJniBootstrapStatus {
|
||||
kFailed,
|
||||
};
|
||||
|
||||
// Installs one exact-Build-ID experiment before libroblox constructors run.
|
||||
// Installs one exact-Build-ID profile before libroblox constructors run.
|
||||
// The caller owns policy: this function never enables itself from environment
|
||||
// variables and must only receive a profile selected from the loaded ELF ID.
|
||||
HostAbiExperimentResult
|
||||
|
||||
@@ -81,7 +81,7 @@ struct NativeAllocatorProfile {
|
||||
inline constexpr size_t kMaxHostBridgeEntries = 8;
|
||||
inline constexpr size_t kMaxConstructorRunRanges = 26;
|
||||
|
||||
// Build-ID-scoped native ABI knowledge for supported and experimental payload
|
||||
// Build-ID-scoped native ABI knowledge for supported and candidate payload
|
||||
// profiles. Keeping these addresses outside the legacy startup graph makes the
|
||||
// version boundary reviewable and independently testable.
|
||||
struct HostAbiProfile {
|
||||
|
||||
@@ -28,11 +28,8 @@ struct FileSha256Result {
|
||||
explicit operator bool() const noexcept { return error.empty(); }
|
||||
};
|
||||
|
||||
// Computes a lowercase SHA-256 digest without invoking a shell or an external
|
||||
// utility. The external ABI approval receipt binds exact profile,
|
||||
// compatibility-manifest, and payload bytes through this implementation; the
|
||||
// runtime itself is bound by its stable GNU Build ID because installation may
|
||||
// rewrite RPATH bytes.
|
||||
// Approval receipts bind the profile, manifest, and payload to this digest.
|
||||
// Runtime identity uses its Build ID because installation may rewrite RPATH.
|
||||
FileSha256Result ComputeFileSha256(const std::string& path);
|
||||
|
||||
struct ExternalHostAbiProfileRequest {
|
||||
@@ -55,27 +52,19 @@ struct ExternalHostAbiProfileResult {
|
||||
explicit operator bool() const noexcept { return error.empty(); }
|
||||
};
|
||||
|
||||
// Loads one exact, payload-bound ABI profile into the process registry. A
|
||||
// candidate is accepted only by an explicitly authorized canary process. A
|
||||
// normal launch instead requires a separate approval receipt produced after
|
||||
// repeated Tier C readiness. Built-in profiles are immutable and always win.
|
||||
// Candidates require canary authorization; normal launches require an
|
||||
// approval receipt. Built-in profiles always win.
|
||||
ExternalHostAbiProfileResult LoadExternalHostAbiProfile(
|
||||
const ExternalHostAbiProfileRequest& request);
|
||||
|
||||
// Environment integration used by the pre-native compatibility gate. An
|
||||
// unset MOCKTAIL_HOST_ABI_PROFILE_FILE is a successful no-op. If it is set,
|
||||
// candidate authorization requires MOCKTAIL_HOST_ABI_CANARY=1,
|
||||
// MOCKTAIL_ALLOW_CANDIDATE_HOST_ABI=1, and the explicit research command-line
|
||||
// marker. Approved profiles require MOCKTAIL_HOST_ABI_APPROVAL_RECEIPT and
|
||||
// neither canary environment marker.
|
||||
// An unset profile is a no-op. Candidates require both canary flags and the
|
||||
// research CLI marker; approved profiles require a receipt and no canary flag.
|
||||
ExternalHostAbiProfileResult InitializeHostAbiProfileFromEnvironment(
|
||||
const std::string& payload_path,
|
||||
const std::string& compatibility_manifest_path,
|
||||
const std::string& expected_build_id,
|
||||
bool explicit_unverified_authorization);
|
||||
|
||||
// Registry lookup used by FindHostAbiProfile after its immutable built-in
|
||||
// table. Callers should use FindHostAbiProfile rather than this function.
|
||||
const HostAbiProfile* FindLoadedExternalHostAbiProfile(
|
||||
std::string_view build_id) noexcept;
|
||||
|
||||
|
||||
+15
-98
@@ -12,20 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// jnivm/jnivm.h — Pseudo Java Native Interface Virtual Machine.
|
||||
//
|
||||
// Provides a lightweight C++ implementation of a JVM stub for use with
|
||||
// Android-native shared libraries (e.g., libroblox.so) running on Linux.
|
||||
// No real Java bytecode is executed; instead, all JNI calls are intercepted
|
||||
// and dispatched to registered C++ callbacks.
|
||||
//
|
||||
// Usage:
|
||||
// auto vm = std::make_shared<jnivm::VM>();
|
||||
// auto cls = vm->RegisterClass("android/content/Context");
|
||||
// cls->RegisterMethod("getSystemService",
|
||||
// "(Ljava/lang/String;)Ljava/lang/Object;",
|
||||
// [](JNIEnv*, jobject, ...) { return nullptr; });
|
||||
|
||||
#ifndef SOBER_TEST_JNIVM_JNIVM_H_
|
||||
#define SOBER_TEST_JNIVM_JNIVM_H_
|
||||
|
||||
@@ -42,29 +28,20 @@
|
||||
|
||||
namespace jnivm {
|
||||
|
||||
// Forward declarations
|
||||
|
||||
class VM;
|
||||
class Class;
|
||||
|
||||
// Object — base type for all pseudo-Java objects managed by the pseudo-JVM.
|
||||
// Analogous to java.lang.Object in the real JVM.
|
||||
|
||||
class Object {
|
||||
public:
|
||||
explicit Object(std::shared_ptr<Class> klass) : klass_(std::move(klass)) {}
|
||||
virtual ~Object() = default;
|
||||
|
||||
// Returns the pseudo-class descriptor of this object.
|
||||
std::shared_ptr<Class> GetClass() const { return klass_; }
|
||||
|
||||
private:
|
||||
std::shared_ptr<Class> klass_;
|
||||
};
|
||||
|
||||
// MethodCallback — signature for registered JNI method stubs.
|
||||
// The callback receives the raw JNIEnv pointer and the receiver jobject.
|
||||
|
||||
using MethodCallback = std::function<void(JNIEnv *, jobject)>;
|
||||
|
||||
// Resolved Roblox account identity supplied by the host runtime. This
|
||||
@@ -75,8 +52,6 @@ struct RobloxAuthIdentity {
|
||||
std::string display_name;
|
||||
};
|
||||
|
||||
// Host device capabilities exposed through the pseudo Android framework.
|
||||
// The guest ABI remains Android, while the default profile // describes a Linux desktop with physical mouse and keyboard input.
|
||||
struct PlatformIdentity {
|
||||
bool touch_enabled = false;
|
||||
bool mouse_enabled = true;
|
||||
@@ -99,23 +74,16 @@ struct RobloxCredentialView {
|
||||
|
||||
using RobloxCredentialProvider = RobloxCredentialView (*)(const void *context);
|
||||
|
||||
// Creates a Configuration object consistent with the VM's immutable platform
|
||||
// identity snapshot. This is the replacement for legacy construction of
|
||||
// contradictory Android keyboard/touch fields.
|
||||
jobject CreateAndroidConfiguration(JNIEnv *env);
|
||||
|
||||
// Narrow write-only boundary for credentials produced by Roblox after the
|
||||
// native runtime has started. A successfully persisted value supersedes the
|
||||
// bound provider for the remainder of the current VM lifetime so native sign-in
|
||||
// can become authoritative without reopening an untrusted credential source.
|
||||
// A stored credential replaces the bound provider for the rest of this VM's
|
||||
// lifetime.
|
||||
struct RobloxCredentialSinkCallbacks {
|
||||
bool (*store)(void *context, const char *data, std::size_t size) = nullptr;
|
||||
};
|
||||
|
||||
// Narrow host callback boundary for org/fmod/AudioDevice. The pseudo-JVM owns
|
||||
// only an opaque shared context and never depends on the SDL audio runtime.
|
||||
// Each Java receiver is passed as a stable identity so the host can keep
|
||||
// independent device lifecycles without retaining a JNI local reference.
|
||||
// The VM keeps the host context opaque. Receiver identities let the backend
|
||||
// track devices without retaining JNI local references.
|
||||
struct FmodAudioDeviceCallbacks {
|
||||
bool (*init)(void *context, const void *identity, int channels,
|
||||
int sample_rate_hz, int block_size_frames,
|
||||
@@ -126,17 +94,12 @@ struct FmodAudioDeviceCallbacks {
|
||||
void (*shutdown)(void *context) = nullptr;
|
||||
};
|
||||
|
||||
// Narrow host callback boundary for the activity.setWindowFlags(II)V contract.
|
||||
// Production LuaApp retains its activity through the android.app.Activity
|
||||
// interface, while GameActivity startup uses the concrete subclass. Guest JNI
|
||||
// threads only publish Android flags; the platform callback must marshal any
|
||||
// SDL work onto the SDL main thread.
|
||||
// Guest JNI threads only publish Android window flags. The host callback must
|
||||
// marshal SDL work onto the main thread.
|
||||
struct AndroidWindowCallbacks {
|
||||
bool (*set_flags)(void *context, int flags, int mask) = nullptr;
|
||||
};
|
||||
|
||||
// Immutable snapshot of the Android NativeTextBoxInfo payload supplied by
|
||||
// Roblox when a native text field receives focus.
|
||||
struct RobloxTextBoxInfo {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
@@ -161,8 +124,7 @@ struct RobloxTextInputShowRequest {
|
||||
RobloxTextBoxInfo info;
|
||||
};
|
||||
|
||||
// Narrow callback boundary for the Roblox native text-input callbacks. The
|
||||
// pseudo-JVM snapshots all JNI-owned arguments before entering this boundary.
|
||||
// JNI-owned arguments are copied before the host callback runs.
|
||||
struct RobloxTextInputCallbacks {
|
||||
void (*show)(void *context,
|
||||
const RobloxTextInputShowRequest &request) = nullptr;
|
||||
@@ -172,9 +134,8 @@ struct RobloxTextInputCallbacks {
|
||||
void (*shutdown)(void *context) = nullptr;
|
||||
};
|
||||
|
||||
// Narrow boundary for a native MessageBus RawCallback object. The VM
|
||||
// retains context while a callback is in flight and dispatches only the exact
|
||||
// run(Ljava/lang/String;)V method on the exact created receiver.
|
||||
// Dispatch accepts run(String) only on a receiver created by this VM. The
|
||||
// context stays alive while the callback runs.
|
||||
struct MessageBusRawCallbacks {
|
||||
void (*run)(void *context, JNIEnv *env, jstring message) = nullptr;
|
||||
};
|
||||
@@ -183,18 +144,14 @@ struct MessageBusRequestHandlerCallbacks {
|
||||
std::string (*run)(void *context, JNIEnv *env, jstring message) = nullptr;
|
||||
};
|
||||
|
||||
// Narrow boundary for the current APK's legacy BrowserService transport.
|
||||
// Dispatch accepts only Callback.onItemSet(Ljava/lang/String;)V on the exact
|
||||
// pseudo receiver created by CreateMemStorageCallback.
|
||||
// Dispatch accepts Callback.onItemSet(String) only on a receiver created by
|
||||
// this VM. The context stays alive while the callback runs.
|
||||
struct MemStorageCallbackCallbacks {
|
||||
void (*on_item_set)(void *context, JNIEnv *env, jstring value) = nullptr;
|
||||
};
|
||||
|
||||
// Narrow boundary for the exact APK callbacks that update host web
|
||||
// state: NativeGLJavaInterface notifications, web activities, cookie sync,
|
||||
// and JNICookieProtocol.OnSetCookieHandler. The VM retains the shared callback
|
||||
// context while a callback is in flight; consumers must copy JNI strings
|
||||
// before returning.
|
||||
// Web callbacks share a retained context while in flight. Consumers must copy
|
||||
// JNI strings before returning.
|
||||
struct RobloxDataModelNotificationCallbacks {
|
||||
void (*on_notification)(void *context, JNIEnv *env, jstring type,
|
||||
jstring data) = nullptr;
|
||||
@@ -209,37 +166,22 @@ struct RobloxDataModelNotificationCallbacks {
|
||||
jstring url) = nullptr;
|
||||
};
|
||||
|
||||
// Narrow boundary for the exact NativeHelper lifecycle notification
|
||||
// emitted when Roblox returns from an experience to LuaApp.
|
||||
struct RobloxExperienceLifecycleCallbacks {
|
||||
void (*on_lua_app_did_return)(void *context) = nullptr;
|
||||
};
|
||||
|
||||
// Class — represents a pseudo Java class registered in the pseudo-JVM.
|
||||
// Stores the class name (JNI descriptor format) and a map of method stubs.
|
||||
|
||||
class Class {
|
||||
public:
|
||||
explicit Class(std::string name) : name_(std::move(name)) {}
|
||||
|
||||
// Returns the JNI-style class descriptor, e.g. "android/content/Context".
|
||||
const std::string &GetName() const { return name_; }
|
||||
|
||||
// Registers a JNI method stub.
|
||||
//
|
||||
// Args:
|
||||
// method_name: Simple method name (e.g. "getSystemService").
|
||||
// signature: JNI type descriptor (e.g. "(Ljava/lang/String;)V").
|
||||
// callback: C++ lambda invoked when the method is called via JNI.
|
||||
void RegisterMethod(const std::string &method_name,
|
||||
const std::string &signature, MethodCallback callback);
|
||||
|
||||
// Looks up a method stub by name and signature.
|
||||
// Returns nullptr if not found.
|
||||
const MethodCallback *FindMethod(const std::string &method_name,
|
||||
const std::string &signature) const;
|
||||
|
||||
// Returns all registered methods (name → callback).
|
||||
const std::unordered_map<std::string, MethodCallback> &GetMethods() const {
|
||||
return methods_;
|
||||
}
|
||||
@@ -247,57 +189,37 @@ public:
|
||||
private:
|
||||
std::string name_;
|
||||
|
||||
// Key format: "methodName:descriptor"
|
||||
std::unordered_map<std::string, MethodCallback> methods_;
|
||||
};
|
||||
|
||||
// VM — the top-level pseudo Java Virtual Machine.
|
||||
//
|
||||
// Owns a registry of pseudo-classes and exposes a JavaVM* compatible
|
||||
// pointer for passing to JNI_OnLoad in the loaded shared library.
|
||||
|
||||
class VM {
|
||||
public:
|
||||
VM();
|
||||
~VM();
|
||||
|
||||
// Disallow copy and assign.
|
||||
VM(const VM &) = delete;
|
||||
VM &operator=(const VM &) = delete;
|
||||
|
||||
// Registers a new pseudo-class by its JNI descriptor.
|
||||
// Repeated registration returns the existing instance.
|
||||
//
|
||||
// Args:
|
||||
// class_name: JNI-style descriptor, e.g.
|
||||
// "com/roblox/client/RobloxActivity".
|
||||
//
|
||||
// Returns:
|
||||
// Shared pointer to the registered (or pre-existing) Class object.
|
||||
// Repeated registration returns the existing class.
|
||||
std::shared_ptr<Class> RegisterClass(const std::string &class_name);
|
||||
|
||||
// Finds a previously registered class.
|
||||
// Returns nullptr if the class has not been registered.
|
||||
std::shared_ptr<Class> FindClass(const std::string &class_name) const;
|
||||
|
||||
// Returns the raw JavaVM pointer compatible with JNI_OnLoad signature.
|
||||
JavaVM *GetJavaVM() { return java_vm_; }
|
||||
|
||||
// Resolves the process pseudo-VM that owns an exact JavaVM pointer. Returns
|
||||
// null for foreign VMs and after the owner is destroyed.
|
||||
static VM *FromJavaVM(JavaVM *java_vm);
|
||||
|
||||
// Returns the attached JNIEnv pointer for the current host thread.
|
||||
JNIEnv *GetJNIEnv();
|
||||
|
||||
// Returns the number of registered classes.
|
||||
std::size_t GetClassCount() const { return class_registry_.size(); }
|
||||
|
||||
// Replaces the resolved account identity with an independent copy. Invalid
|
||||
// (non-positive) identities are normalized to the unresolved state.
|
||||
void SetRobloxAuthIdentity(const RobloxAuthIdentity &identity);
|
||||
|
||||
// Returns the VM to its unresolved account state.
|
||||
void ClearRobloxAuthIdentity();
|
||||
|
||||
// Returns an immutable-by-value snapshot safe for use by JNI callbacks on
|
||||
@@ -405,12 +327,10 @@ public:
|
||||
void ClearRobloxExperienceLifecycleCallbacks();
|
||||
bool DispatchRobloxExperienceLuaAppDidReturn();
|
||||
|
||||
// Restores env->functions to &native_interface_ in case libroblox replaced
|
||||
// it.
|
||||
// Restores the pseudo-JNI table after guest code replaces env->functions.
|
||||
void RestoreFunctions();
|
||||
|
||||
private:
|
||||
// Internal JNI function tables populated with stub implementations.
|
||||
JNIInvokeInterface_ invoke_interface_ = {};
|
||||
JavaVM java_vm_storage_ = {};
|
||||
JavaVM *java_vm_ = nullptr;
|
||||
@@ -419,7 +339,6 @@ private:
|
||||
JNIEnv jni_env_storage_ = {};
|
||||
JNIEnv *jni_env_ = nullptr;
|
||||
|
||||
// Registry of pseudo-classes indexed by their JNI descriptor.
|
||||
std::unordered_map<std::string, std::shared_ptr<Class>> class_registry_;
|
||||
|
||||
mutable std::mutex roblox_auth_identity_mutex_;
|
||||
@@ -498,8 +417,6 @@ private:
|
||||
std::shared_ptr<RobloxExperienceLifecycleBinding>
|
||||
roblox_experience_lifecycle_binding_;
|
||||
|
||||
// Initialises the JNIInvokeInterface_ and JNINativeInterface_ function tables
|
||||
// with stub implementations that forward calls to this VM instance.
|
||||
void InitJNIFunctionTables();
|
||||
};
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ class RuntimeDependencies final {
|
||||
ShutdownBeforePlatformCallback shutdown_before_platform_ = nullptr;
|
||||
};
|
||||
|
||||
// Transitional reverse-engineering runtime. New subsystems must expose // interfaces and be composed outside this implementation.
|
||||
// Transitional runtime kept for the reverse-engineered startup path.
|
||||
int Run(const runtime::CommandLineOptions& options,
|
||||
RuntimeDependencies dependencies);
|
||||
|
||||
|
||||
@@ -12,17 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// libc_shim/libc_shim.h — Bionic libc → Glibc call translation layer.
|
||||
//
|
||||
// Intercepts calls made by Bionic-compiled code (libroblox.so) to
|
||||
// Bionic-specific libc functions and forwards them to their Glibc equivalents
|
||||
// on the host Linux system.
|
||||
//
|
||||
// Key responsibilities:
|
||||
// - pthread_mutex_t size/layout differences between Bionic and Glibc.
|
||||
// - Virtual filesystem path mapping (APK sandbox → Flatpak paths).
|
||||
// - Bionic-only syscall wrappers missing in Glibc.
|
||||
|
||||
#ifndef SOBER_TEST_LIBC_SHIM_LIBC_SHIM_H_
|
||||
#define SOBER_TEST_LIBC_SHIM_LIBC_SHIM_H_
|
||||
|
||||
@@ -57,10 +46,8 @@ struct HostCaBundleResolution {
|
||||
// boundary as the caller that will later free them.
|
||||
void ConfigureGuestAllocator(GuestAllocator allocator) noexcept;
|
||||
|
||||
// Installs all shim hooks. Must be called before the target .so is loaded.
|
||||
//
|
||||
// After this call, dlopen(3) symbol resolution for known Bionic symbols will
|
||||
// be intercepted and redirected to the shim implementations.
|
||||
// Must run before loading the target library so its Bionic imports resolve to
|
||||
// these shims.
|
||||
void Install();
|
||||
|
||||
// Resolves the host TLS trust bundle without weakening certificate
|
||||
@@ -76,29 +63,13 @@ HostCaBundleResolution ConfigureHostCaBundlePathMappings();
|
||||
|
||||
const char* HostCaBundleStatusName(HostCaBundleStatus status);
|
||||
|
||||
// Maps an Android-style virtual path to its Flatpak sandbox equivalent.
|
||||
//
|
||||
// Example:
|
||||
// "/data/user/0/com.roblox.client/files/logs" →
|
||||
// "/home/user/.var/app/org.vinegarhq.Sober/data/sober/logs"
|
||||
//
|
||||
// Args:
|
||||
// android_path: Absolute path as seen by the Bionic library.
|
||||
//
|
||||
// Returns:
|
||||
// Translated host path. Returns android_path unchanged if no mapping
|
||||
// is registered for it.
|
||||
// Applies the longest matching Android-to-host path prefix. Unmapped paths are
|
||||
// returned unchanged.
|
||||
std::string TranslatePath(const std::string& android_path);
|
||||
|
||||
// Registers a path prefix mapping used by TranslatePath.
|
||||
//
|
||||
// Args:
|
||||
// android_prefix: Android path prefix to intercept.
|
||||
// host_prefix: Replacement prefix on the host filesystem.
|
||||
void RegisterPathMapping(const std::string& android_prefix,
|
||||
const std::string& host_prefix);
|
||||
|
||||
// Removes all registered path mappings. Primarily for use in tests.
|
||||
void ClearPathMappings();
|
||||
|
||||
} // namespace libc_shim
|
||||
|
||||
+5
-67
@@ -12,18 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// linker/linker.h — ELF Bionic-compatible shared library loader.
|
||||
//
|
||||
// Provides a minimal interface for loading Android ELF shared libraries
|
||||
// (.so files compiled for Bionic libc / Android ABI) into a Linux process
|
||||
// address space, resolving dependencies and exporting a symbol table.
|
||||
//
|
||||
// Design notes:
|
||||
// - Uses dlopen(3) under the hood for system libraries.
|
||||
// - For Bionic-ABI libraries (libroblox.so), manual ELF parsing and
|
||||
// mmap(2)-based loading is required; this header exposes that interface.
|
||||
// - All paths follow POSIX conventions.
|
||||
|
||||
#ifndef MOCKTAIL_LINKER_LINKER_H_
|
||||
#define MOCKTAIL_LINKER_LINKER_H_
|
||||
|
||||
@@ -34,9 +22,7 @@
|
||||
|
||||
namespace linker {
|
||||
|
||||
// Opaque handle returned by LoadLibrary. Callers must not interpret the
|
||||
// pointer value; it is only valid as an argument to ResolveSymbol or
|
||||
// UnloadLibrary.
|
||||
// Opaque; valid only with this linker's resolve/unload functions.
|
||||
using LibraryHandle = void*;
|
||||
using SymbolMap = std::unordered_map<std::string, void*>;
|
||||
using AndroidLibraryLoadObserver = bool (*)(void* context,
|
||||
@@ -50,59 +36,27 @@ struct ProgramHeaderValidation {
|
||||
explicit operator bool() const noexcept { return valid; }
|
||||
};
|
||||
|
||||
// AddSyntheticLibrary — registers one Android SONAME and only the exports
|
||||
// owned by that library. Dependencies of subsequently loaded Android ELF
|
||||
// files resolve these symbols through mcpelauncher-linker.
|
||||
LibraryHandle AddSyntheticLibrary(const std::string& soname,
|
||||
const SymbolMap& exports);
|
||||
|
||||
// RegisterSyntheticSymbol — stages or adds one export to a specific Android
|
||||
// SONAME. If the synthetic library is already loaded, its symbol table is
|
||||
// updated in place.
|
||||
// Updates an already loaded library in place.
|
||||
void RegisterSyntheticSymbol(const std::string& soname,
|
||||
const std::string& name, void* address);
|
||||
|
||||
// Registers Android Bionic thread-local and synchronization ABIs only
|
||||
// for synthetic libc.so. Host code keeps its glibc implementations.
|
||||
// These adapters belong to their synthetic SONAME, never the host namespace.
|
||||
void RegisterBionicPthreadKeyRuntimeForLibc();
|
||||
|
||||
// Registers Android's DSO-aware __register_atfork entrypoint only for
|
||||
// synthetic libc.so. The host pthread bridge preserves callback order but
|
||||
// cannot unregister callbacks when a guest DSO unloads.
|
||||
void RegisterBionicAtForkRuntimeForLibc();
|
||||
|
||||
// Registers Android LP64 positioned I/O and seek entrypoints only for
|
||||
// synthetic libc.so. The bridge relies on the x86-64 host's 64-bit
|
||||
// off_t contract and never exposes host large-file feature-macro aliases.
|
||||
void RegisterBionicLargeFileRuntimeForLibc();
|
||||
|
||||
// Registers host-independent implementations of Android libc exports that
|
||||
// are not uniformly supplied by glibc and musl. Every symbol remains owned by
|
||||
// synthetic libc.so and preserves the Android x86-64 ABI.
|
||||
void RegisterBionicHostLibcRuntimeForLibc();
|
||||
|
||||
// Registers Bionic sysconf name translation only for synthetic libc.so.
|
||||
// Bionic and glibc use different integer values for sysconf names.
|
||||
void RegisterBionicSysconfRuntimeForLibc();
|
||||
|
||||
// Registers the Android LP64 sigaction layout adapter only for libc.so.
|
||||
void RegisterBionicSignalRuntimeForLibc();
|
||||
|
||||
// Registers the LP64 Bionic FILE/__sF ABI and host stdio translation only for
|
||||
// synthetic libc.so.
|
||||
void RegisterBionicStdioRuntimeForLibc();
|
||||
|
||||
// Restores mcpelauncher-linker's own libdl exports after legacy host-symbol
|
||||
// discovery. Android dlopen/dlsym must never escape into glibc.
|
||||
// Android dlopen/dlsym must not escape into glibc.
|
||||
void RegisterBionicDynamicLoaderForLibdl();
|
||||
|
||||
// GetSyntheticLibrarySymbols — returns a snapshot of one SONAME's exports.
|
||||
// The copy is safe to inspect while other libraries are being registered.
|
||||
SymbolMap GetSyntheticLibrarySymbols(const std::string& soname);
|
||||
|
||||
// UpdateAndroidLibraryPath — configures the Bionic linker's dependency search
|
||||
// path. Multiple directories use the same colon-separated syntax as
|
||||
// LD_LIBRARY_PATH.
|
||||
void UpdateAndroidLibraryPath(const std::string& search_path);
|
||||
|
||||
// Installs the single process owner that validates or adapts a real Android
|
||||
@@ -112,18 +66,12 @@ bool SetAndroidLibraryLoadObserver(AndroidLibraryLoadObserver observer,
|
||||
void* context);
|
||||
void ClearAndroidLibraryLoadObserver(void* context);
|
||||
|
||||
// OpenAndroidLibrary — loads a real Android ELF and registers its handle under
|
||||
// logical_name. The containing directory is added to the Bionic dependency
|
||||
// search path before loading.
|
||||
LibraryHandle OpenAndroidLibrary(const std::string& real_path,
|
||||
const std::string& logical_name);
|
||||
|
||||
// Legacy compatibility bridge. New code should use AddSyntheticLibrary or
|
||||
// OpenAndroidLibrary so real files and synthetic SONAMEs cannot be confused.
|
||||
LibraryHandle LoadLibrary(const std::string& real_path,
|
||||
const std::string& host_alias);
|
||||
|
||||
// ResolveSymbol — looks up an exported symbol in a loaded library.
|
||||
void* ResolveSymbol(LibraryHandle handle, const std::string& symbol_name);
|
||||
|
||||
// Crashpad execution is intentionally outside Mocktail's supported JNI
|
||||
@@ -132,24 +80,15 @@ void* ResolveSymbol(LibraryHandle handle, const std::string& symbol_name);
|
||||
bool IsBlockedRobloxCrashReportNativeSymbol(
|
||||
std::string_view symbol_name) noexcept;
|
||||
|
||||
// UnloadLibrary — releases resources associated with a loaded library.
|
||||
void UnloadLibrary(LibraryHandle handle);
|
||||
|
||||
// Legacy compatibility bridge used by the current monolithic startup path.
|
||||
// New code should use RegisterSyntheticSymbol and assign every export to its
|
||||
// owning SONAME.
|
||||
void RegisterSymbol(const std::string& name, void* addr);
|
||||
|
||||
// Returns the legacy accumulated symbol table. It remains available until the
|
||||
// existing startup path is migrated to per-SONAME maps.
|
||||
const SymbolMap& GetBionicSymbols();
|
||||
|
||||
// Returns mcpelauncher-linker's Bionic dl_iterate_phdr entrypoint. Android
|
||||
// unwinders must not receive glibc's host implementation/layout.
|
||||
// Android unwinders must not receive glibc's implementation or layout.
|
||||
void* BionicDlIteratePhdrAddress();
|
||||
|
||||
// Confirms that the Bionic program-header iterator can see a manually loaded
|
||||
// Android ELF, including the unwind metadata needed for C++ exceptions.
|
||||
ProgramHeaderValidation ValidateBionicProgramHeaders(LibraryHandle handle);
|
||||
|
||||
// Returns the mapped ELF base for a loaded Android library alias. The lookup
|
||||
@@ -157,7 +96,6 @@ ProgramHeaderValidation ValidateBionicProgramHeaders(LibraryHandle handle);
|
||||
// alias or a host-dlopen library without Bionic image metadata.
|
||||
std::uintptr_t FindLoadedAndroidLibraryBase(const std::string& logical_name);
|
||||
|
||||
// SymbolTable — global registry of alias → handle mappings.
|
||||
const std::unordered_map<std::string, LibraryHandle>& GetSymbolTable();
|
||||
|
||||
} // namespace linker
|
||||
|
||||
@@ -22,9 +22,7 @@
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
// Interleaved PCM formats accepted at the host audio boundary. Endianness is
|
||||
// explicit because Android x86-64 producers use little-endian PCM regardless
|
||||
// of the host implementation chosen by SDL.
|
||||
// Android producers supply little-endian interleaved PCM.
|
||||
enum class PcmSampleFormat {
|
||||
kUnsigned8,
|
||||
kSigned16LittleEndian,
|
||||
@@ -41,11 +39,9 @@ struct PcmSpec {
|
||||
using AudioBufferReleaseCallback = void (*)(void* context, const void* data,
|
||||
std::size_t size_bytes);
|
||||
|
||||
// A PCM submission. With no release callback, Enqueue() copies the bytes
|
||||
// before returning. With a release callback, the sink may borrow the buffer;
|
||||
// the caller must keep it valid until the callback runs. A successful
|
||||
// submission invokes the callback exactly once, including when Clear() or
|
||||
// Shutdown() discards queued audio. A failed submission never invokes it.
|
||||
// Without a callback Enqueue copies the bytes. With one, a successful Enqueue
|
||||
// borrows them until exactly one release callback, including on Clear or
|
||||
// Shutdown. On failure the callback is not invoked.
|
||||
struct PcmBuffer {
|
||||
const void* data = nullptr;
|
||||
std::size_t size_bytes = 0;
|
||||
@@ -57,9 +53,7 @@ Status ValidatePcmSpec(const PcmSpec& spec);
|
||||
std::size_t BytesPerSample(PcmSampleFormat format);
|
||||
std::size_t BytesPerFrame(const PcmSpec& spec);
|
||||
|
||||
// Thread-safe PCM playback contract. Implementations perform device format
|
||||
// conversion and resampling through their host audio library; callers always
|
||||
// submit data in source_spec().
|
||||
// Thread-safe; callers submit source_spec(), and the backend converts it.
|
||||
class AudioSink {
|
||||
public:
|
||||
virtual ~AudioSink() = default;
|
||||
@@ -68,15 +62,14 @@ class AudioSink {
|
||||
virtual Status Enqueue(const PcmBuffer& buffer) = 0;
|
||||
virtual Status Pause() = 0;
|
||||
virtual Status Resume() = 0;
|
||||
// Linear gain is delegated to the host audio library. Zero mutes, one is
|
||||
// unity gain, and negative or non-finite values are rejected.
|
||||
// Zero mutes; one is unity; negative and non-finite values are invalid.
|
||||
virtual Status SetGain(float linear_gain) = 0;
|
||||
virtual Status Flush() = 0;
|
||||
virtual Status Clear() = 0;
|
||||
virtual Status GetQueuedBytes(std::size_t* size_bytes) const = 0;
|
||||
|
||||
// Idempotent. It waits for in-flight API calls and releases all borrowed
|
||||
// buffers before returning. Calls started after shutdown fail closed.
|
||||
// Idempotent. Waits for in-flight calls and borrowed buffers; later calls
|
||||
// fail closed.
|
||||
virtual void Shutdown() = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -23,14 +23,10 @@
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
// org.fmod.AudioDevice is an ordinary Java object. The JNI boundary passes
|
||||
// its jobject value through this opaque key without making the audio layer
|
||||
// depend on JNI headers or retain a JNI reference.
|
||||
// Opaque jobject identity; the audio layer never retains a JNI reference.
|
||||
using FmodJavaAudioDeviceIdentity = const void*;
|
||||
|
||||
// A factory returns a paused sink with exactly source_spec. Production uses
|
||||
// SDL3; tests can inject a deterministic sink without opening a host device.
|
||||
// factory_context must remain valid until the runtime is shut down.
|
||||
// Returns a paused sink. factory_context must outlive the runtime.
|
||||
using FmodJavaAudioSinkFactory = Status (*)(
|
||||
void* factory_context, const PcmSpec& source_spec,
|
||||
std::unique_ptr<AudioSink>* sink);
|
||||
@@ -44,8 +40,7 @@ struct FmodJavaAudioRuntimeOptions {
|
||||
std::size_t max_buffer_bytes_per_device = 64U * 1024U * 1024U;
|
||||
};
|
||||
|
||||
// Content-free counters suitable for readiness logs. They never expose PCM,
|
||||
// JNI identities, or guest pointers.
|
||||
// Contains no PCM, JNI identities, or guest pointers.
|
||||
struct FmodJavaAudioRuntimeStats {
|
||||
std::uint64_t init_attempts = 0;
|
||||
std::uint64_t initialized_devices = 0;
|
||||
@@ -65,18 +60,9 @@ struct FmodJavaAudioRuntimeStats {
|
||||
std::size_t pending_bytes = 0;
|
||||
};
|
||||
|
||||
// Thread-safe implementation of the APK's exact Java playback contract:
|
||||
//
|
||||
// AudioDevice.init(channels, sampleRate, blockSize, blockCount)Z
|
||||
// AudioDevice.write(byte[], length)V
|
||||
// AudioDevice.close()V
|
||||
//
|
||||
// Init() allocates blockCount fixed slots of blockSize frames. Every successful
|
||||
// Write() copies guest bytes synchronously into one slot, then lends that stable
|
||||
// storage to AudioSink without a second PCM copy. FmodJavaAudioRuntime performs
|
||||
// no per-write allocation for buffer storage or release ownership. A producer
|
||||
// waits with AudioTrack-style blocking backpressure when all slots are borrowed
|
||||
// instead of growing memory or dropping a block.
|
||||
// Fixed slots avoid per-write allocation. Write copies into a slot, then lends
|
||||
// it to AudioSink; a full pool applies blocking backpressure instead of
|
||||
// growing memory or dropping audio.
|
||||
class FmodJavaAudioRuntime final {
|
||||
public:
|
||||
explicit FmodJavaAudioRuntime(
|
||||
@@ -94,8 +80,7 @@ class FmodJavaAudioRuntime final {
|
||||
|
||||
FmodJavaAudioRuntimeStats GetStats() const;
|
||||
|
||||
// Idempotent. Prevents new devices/writes, shuts down every sink, and waits
|
||||
// for every borrowed owned buffer to be released before returning.
|
||||
// Waits for every borrowed buffer to be released.
|
||||
void Shutdown();
|
||||
|
||||
private:
|
||||
|
||||
@@ -25,9 +25,7 @@
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
// SDL documents subsystem initialization and shutdown as main-thread-only.
|
||||
// The process composition root owns this lifetime; audio workers only create
|
||||
// streams while the subsystem is active.
|
||||
// SDL requires subsystem startup and shutdown on the main thread.
|
||||
Status InitializeSdlAudioSubsystem();
|
||||
Status ShutdownSdlAudioSubsystem();
|
||||
|
||||
@@ -36,50 +34,37 @@ struct SdlPlaybackDevice {
|
||||
std::string name;
|
||||
};
|
||||
|
||||
// Lists physical SDL playback devices. The abstract `default` target is not a
|
||||
// physical entry and is therefore intentionally absent from this vector.
|
||||
// The virtual `default` target is not returned.
|
||||
Status ListSdlPlaybackDevices(std::vector<SdlPlaybackDevice>* devices);
|
||||
|
||||
// Resolves `default` or one exact physical device name. This pure selection
|
||||
// step is separate from SDL enumeration so configuration behavior is directly
|
||||
// testable.
|
||||
Status ResolveSdlPlaybackDevice(
|
||||
std::string_view requested,
|
||||
const std::vector<SdlPlaybackDevice>& available_devices,
|
||||
std::uint32_t* playback_device_id, std::string* resolved_name);
|
||||
|
||||
// Sets the process playback target used by every subsequently created FMOD
|
||||
// Java and OpenSL sink. It must run after SDL audio initialization and before
|
||||
// the first sink is opened.
|
||||
// Must run after SDL audio initialization and before the first sink opens.
|
||||
Status ConfigureSdlPlaybackDevice(
|
||||
std::string_view requested,
|
||||
std::vector<SdlPlaybackDevice>* available_devices,
|
||||
std::string* resolved_name);
|
||||
|
||||
// Returns the process playback target currently used by new sinks. The
|
||||
// physical ID is zero for SDL's system-default route.
|
||||
// Device ID zero means the system default.
|
||||
Status GetConfiguredSdlPlaybackDevice(std::uint32_t* playback_device_id,
|
||||
std::string* resolved_name);
|
||||
|
||||
// Atomically migrates every live FMOD Java and OpenSL stream to one physical
|
||||
// SDL device, or to the system default when playback_device_id is zero. New
|
||||
// sinks inherit the same target. Existing queued PCM stays in its stream.
|
||||
// Migrates live streams without dropping queued PCM. New sinks inherit it.
|
||||
Status SwitchSdlPlaybackDevice(std::uint32_t playback_device_id,
|
||||
std::string* resolved_name);
|
||||
|
||||
struct SdlAudioSinkOptions {
|
||||
PcmSpec source_spec;
|
||||
|
||||
// Zero selects the process playback target configured above (the SDL
|
||||
// default until explicitly configured). A nonzero ID bypasses that target.
|
||||
// Zero selects the configured process target.
|
||||
std::uint32_t playback_device_id = 0;
|
||||
bool start_paused = true;
|
||||
};
|
||||
|
||||
// Opens a real SDL 3.4+ playback device and its bound SDL_AudioStream. The SDL
|
||||
// audio subsystem must already be owned by InitializeSdlAudioSubsystem(). SDL
|
||||
// owns all format conversion, resampling, and borrowed-buffer consumption.
|
||||
// On failure, *sink remains null.
|
||||
// SDL owns conversion, resampling, and borrowed-buffer consumption.
|
||||
Status CreateSdlAudioSink(const SdlAudioSinkOptions& options,
|
||||
std::unique_ptr<AudioSink>* sink);
|
||||
|
||||
|
||||
@@ -26,9 +26,7 @@
|
||||
namespace mocktail {
|
||||
namespace graphics {
|
||||
|
||||
// Vulkan VkResult values are a stable signed 32-bit ABI. Keeping the Android
|
||||
// boundary independent of the optional Vulkan SDK lets capability-only builds
|
||||
// continue to compile with SDL's opaque Vulkan handles.
|
||||
// Stable signed 32-bit ABI, independent of the optional Vulkan SDK.
|
||||
using AndroidVulkanResult = VkResult;
|
||||
|
||||
inline constexpr AndroidVulkanResult kAndroidVulkanSuccess =
|
||||
@@ -43,43 +41,29 @@ inline constexpr std::uint64_t kHostImageAcquireWatchdogTimeoutNs =
|
||||
inline constexpr char kAndroidSurfaceExtension[] =
|
||||
"VK_KHR_android_surface";
|
||||
|
||||
// Desktop compositors can report a valid acquired/presented image as
|
||||
// suboptimal while a fullscreen configure settles. The Android client path
|
||||
// used by Roblox treats that positive success code as a hard error, so the
|
||||
// compatibility boundary exposes it as the equivalent VK_SUCCESS result.
|
||||
// Roblox treats VK_SUBOPTIMAL_KHR as a hard error, so expose it as success.
|
||||
VkResult NormalizeAndroidSwapchainResult(VkResult result);
|
||||
|
||||
// Android requests an infinite image-acquire wait, but a lost Wayland/X11
|
||||
// compositor wakeup would then block Roblox's RenderJob forever. Only the
|
||||
// infinite guest request is bounded; explicit finite Vulkan timeouts retain
|
||||
// their original contract.
|
||||
// Bounds only infinite acquire waits so a lost compositor wakeup cannot hang.
|
||||
std::uint64_t BoundHostImageAcquireTimeout(std::uint64_t requested_timeout);
|
||||
bool IsHostImageAcquireWatchdogTimeout(std::uint64_t requested_timeout,
|
||||
VkResult host_result);
|
||||
VkResult NormalizeHostImageAcquireResult(std::uint64_t requested_timeout,
|
||||
VkResult host_result);
|
||||
|
||||
// Infinite fence/timeline waits retain their guest-visible semantics, but are
|
||||
// issued to the host in finite slices so a stalled renderer produces evidence
|
||||
// while it remains blocked. VK_TIMEOUT from a slice is never returned to the
|
||||
// Android client as a completed wait.
|
||||
// Infinite guest waits use finite host slices without exposing slice timeouts.
|
||||
std::uint64_t BoundHostSynchronizationWaitTimeout(
|
||||
std::uint64_t requested_timeout);
|
||||
bool ShouldContinueHostSynchronizationWait(std::uint64_t requested_timeout,
|
||||
VkResult host_result);
|
||||
|
||||
// Rewrites an Android VkInstance extension request for the host WSI. The
|
||||
// returned storage owns its strings and can be used to build the
|
||||
// ppEnabledExtensionNames array passed to the host vkCreateInstance call.
|
||||
// Returned storage owns the translated extension names.
|
||||
Status TranslateAndroidVulkanInstanceExtensions(
|
||||
const std::vector<std::string>& android_extensions,
|
||||
const std::vector<std::string>& host_wsi_extensions,
|
||||
std::vector<std::string>* host_extensions);
|
||||
|
||||
// Android WSI boundary. vkCreateAndroidSurfaceKHR-compatible exports can
|
||||
// delegate here after vkCreateInstance has used the translated extension list.
|
||||
// The Android native-window member is intentionally ignored: SDL owns the real
|
||||
// host window and is the only component allowed to create its VkSurfaceKHR.
|
||||
// Ignores the guest native-window field; SDL alone creates the host surface.
|
||||
class AndroidVulkanWsiAdapter final {
|
||||
public:
|
||||
using PresentCallback = void (*)(void* user_data);
|
||||
|
||||
@@ -12,14 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Exact-path loader for Mocktail's Bionic libEGL adapter.
|
||||
//
|
||||
// A host EGL implementation may have the Android SONAME "libEGL.so" too
|
||||
// (Chromium ANGLE does). Loading by SONAME after SDL/ANGLE starts can then
|
||||
// return the host implementation and make Android code create a second EGL
|
||||
// surface for one native window. This adapter opens the Mocktail
|
||||
// bridge beside the executable and exposes only its EGL exports for the
|
||||
// synthetic Bionic libEGL.so dependency.
|
||||
// Loading by SONAME can select host ANGLE instead of the Bionic adapter. This
|
||||
// bridge therefore opens the library beside the executable by exact path.
|
||||
|
||||
#ifndef MOCKTAIL_GRAPHICS_BIONIC_EGL_BRIDGE_H_
|
||||
#define MOCKTAIL_GRAPHICS_BIONIC_EGL_BRIDGE_H_
|
||||
@@ -37,9 +31,7 @@ public:
|
||||
BionicEglBridge(const BionicEglBridge &) = delete;
|
||||
BionicEglBridge &operator=(const BionicEglBridge &) = delete;
|
||||
|
||||
// Loads $ORIGIN/libEGL.so by its exact absolute path. On success, the
|
||||
// handle intentionally remains live for the process lifetime because the
|
||||
// Bionic linker retains the exported function addresses.
|
||||
// The handle stays live because the Bionic linker retains its exports.
|
||||
bool Load();
|
||||
|
||||
bool IsLoaded() const { return handle_ != nullptr; }
|
||||
|
||||
@@ -30,9 +30,8 @@
|
||||
namespace mocktail {
|
||||
namespace graphics {
|
||||
|
||||
// Thin host WSI scaffold. It does not create a VkInstance, device, swapchain,
|
||||
// renderer, or allocator. A future Android Vulkan adapter may translate
|
||||
// vkCreateAndroidSurfaceKHR into CreateSurface after rewriting extensions.
|
||||
// Provides SDL's host WSI entry points. Instance, device, swapchain, renderer,
|
||||
// and allocator ownership stays with the caller.
|
||||
class SdlVulkanWsi final {
|
||||
public:
|
||||
SdlVulkanWsi() = default;
|
||||
|
||||
@@ -18,19 +18,15 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
// Stable process-local ABI between the host input runtime in the executable
|
||||
// and libvulkan.so, which is loaded as a separate Android compatibility DSO.
|
||||
// Pixels are tightly packed RGBA8. The producer owns and securely clears its
|
||||
// storage; consumers must copy a matching revision before returning.
|
||||
// Process-local ABI with libvulkan.so. Pixels are RGBA8; consumers must copy a
|
||||
// matching revision before the producer clears its storage.
|
||||
struct MocktailTextOverlayFrameInfo {
|
||||
static constexpr std::uint32_t kAbiVersion = 2;
|
||||
|
||||
std::uint32_t abi_version = kAbiVersion;
|
||||
std::uint32_t visible = 0;
|
||||
std::uint64_t revision = 0;
|
||||
// x/y/width/height use the host's logical input coordinate space. These
|
||||
// dimensions let a Vulkan consumer map that space to the swapchain's pixel
|
||||
// extent without assuming a 1:1 high-DPI scale.
|
||||
// Coordinates are logical, not swapchain pixels.
|
||||
std::uint32_t coordinate_width = 0;
|
||||
std::uint32_t coordinate_height = 0;
|
||||
std::int32_t x = 0;
|
||||
@@ -43,9 +39,7 @@ struct MocktailTextOverlayFrameInfo {
|
||||
|
||||
extern "C" {
|
||||
|
||||
// Lock-free conservative hint for the Vulkan present fast path. A false
|
||||
// result guarantees that no drawable overlay is currently published. A true
|
||||
// result only means that the synchronized query/copy path must decide.
|
||||
// false guarantees no overlay; true still requires a synchronized query.
|
||||
bool mocktail_text_overlay_may_present();
|
||||
bool mocktail_text_overlay_query(MocktailTextOverlayFrameInfo* frame);
|
||||
bool mocktail_text_overlay_copy(std::uint64_t revision, void* rgba,
|
||||
|
||||
@@ -22,13 +22,8 @@
|
||||
namespace mocktail {
|
||||
namespace graphics {
|
||||
|
||||
// Composites the process-local RGBA text surface into application-owned
|
||||
// swapchain images. The adapter retains ownership of every Vulkan object it
|
||||
// registers. DestroySwapchain and DestroyDevice must be called before the
|
||||
// corresponding host Vulkan destroy function.
|
||||
//
|
||||
// An unsupported device or swapchain remains a valid registration failure:
|
||||
// QueuePresent then forwards the unmodified VkPresentInfoKHR to fallback.
|
||||
// Registered Vulkan objects remain caller-owned and must be unregistered
|
||||
// before host destruction. Unsupported registrations fall back cleanly.
|
||||
class VulkanTextOverlayCompositor final {
|
||||
public:
|
||||
VulkanTextOverlayCompositor();
|
||||
@@ -38,10 +33,8 @@ class VulkanTextOverlayCompositor final {
|
||||
VulkanTextOverlayCompositor& operator=(const VulkanTextOverlayCompositor&) =
|
||||
delete;
|
||||
|
||||
// enabled_features describes the features actually enabled by
|
||||
// vkCreateDevice, not merely the features supported by the physical device.
|
||||
// The supplied proc-address callbacks must bypass the Android-facing Vulkan
|
||||
// adapter and resolve host functions directly.
|
||||
// enabled_features must match vkCreateDevice. Proc callbacks resolve host
|
||||
// functions directly, bypassing the Android adapter.
|
||||
bool RegisterDevice(VkInstance instance, VkPhysicalDevice physical_device,
|
||||
VkDevice device, std::uint32_t api_version,
|
||||
const char* const* enabled_extensions,
|
||||
@@ -61,10 +54,7 @@ class VulkanTextOverlayCompositor final {
|
||||
const VkImage* images, std::uint32_t image_count);
|
||||
void DestroySwapchain(VkDevice device, VkSwapchainKHR swapchain);
|
||||
|
||||
// Serializes adapter-mediated submit, sparse-bind, present, and idle
|
||||
// operations on the exact queue range imported by libplacebo. Other
|
||||
// VkQueues remain independent, including the inactive-overlay fast path on
|
||||
// a non-imported queue.
|
||||
// Adapter operations serialize only queues imported by libplacebo.
|
||||
VkResult QueueSubmit(VkQueue queue, std::uint32_t submit_count,
|
||||
const VkSubmitInfo* submits, VkFence fence,
|
||||
PFN_vkQueueSubmit fallback);
|
||||
@@ -77,10 +67,7 @@ class VulkanTextOverlayCompositor final {
|
||||
VkResult QueueWaitIdle(VkQueue queue, PFN_vkQueueWaitIdle fallback);
|
||||
VkResult DeviceWaitIdle(VkDevice device, PFN_vkDeviceWaitIdle fallback);
|
||||
|
||||
// Always invokes fallback when it is non-null. The original wait semaphores
|
||||
// are forwarded unchanged until the compositor successfully submits the
|
||||
// wait-collapse bridge; after that point only compositor-owned semaphores
|
||||
// are passed to fallback.
|
||||
// After wait collapse, fallback receives only compositor-owned semaphores.
|
||||
VkResult QueuePresent(VkQueue queue, const VkPresentInfoKHR* present_info,
|
||||
PFN_vkQueuePresentKHR fallback);
|
||||
|
||||
|
||||
@@ -40,9 +40,7 @@ enum class AuthRuntimeStatus {
|
||||
kUnavailable,
|
||||
};
|
||||
|
||||
// Process-local ownership for the canonical .ROBLOSECURITY header. The class
|
||||
// is move-only and clears its backing allocation before release.
|
||||
// Callers must never log the value returned by view() or c_str().
|
||||
// Move-only credential storage that clears its allocation. Never log its view.
|
||||
class SecureRobloxCredential final {
|
||||
public:
|
||||
SecureRobloxCredential() = default;
|
||||
@@ -64,14 +62,10 @@ class SecureRobloxCredential final {
|
||||
std::vector<char> bytes_;
|
||||
};
|
||||
|
||||
// Clears a transient string that contained derived credential material.
|
||||
void SecurelyClearString(std::string* value);
|
||||
|
||||
// Binds the production pseudo-VM to the exact credential resolved by the
|
||||
// authentication preflight. The binding is intentionally non-movable:
|
||||
// it must be created only after its credential reaches its final address and
|
||||
// destroyed before that credential or the VM. Guest credentials block legacy
|
||||
// env/disk fallback until the sink durably accepts a native sign-in value.
|
||||
// Must be created after the credential reaches its final address and destroyed
|
||||
// before the credential or VM. Guest bindings block legacy env/disk fallback.
|
||||
class ScopedRobloxCredentialBinding final {
|
||||
public:
|
||||
ScopedRobloxCredentialBinding(jnivm::VM* jni_vm,
|
||||
@@ -93,9 +87,7 @@ class ScopedRobloxCredentialBinding final {
|
||||
jnivm::VM* jni_vm_ = nullptr;
|
||||
};
|
||||
|
||||
// Result of resolving host credentials before the Android runtime starts.
|
||||
// Credential ownership is retained only so the exact credential validated by
|
||||
// AuthService can be moved into native startup without reopening its source.
|
||||
// Retains the validated credential without reopening its source.
|
||||
struct AuthRuntimeComposition {
|
||||
AuthRuntimeStatus status = AuthRuntimeStatus::kUnavailable;
|
||||
std::shared_ptr<jnivm::VM> jni_vm;
|
||||
@@ -107,21 +99,14 @@ struct AuthRuntimeComposition {
|
||||
explicit operator bool() const { return jni_vm != nullptr; }
|
||||
};
|
||||
|
||||
// Resolves the saved Roblox session and constructs the production pseudo-VM.
|
||||
// A VM is returned only for an authenticated session or when guest startup is
|
||||
// explicitly enabled with MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP. An exact HTTP 401
|
||||
// from a managed Mocktail file clears only the rejected .ROBLOSECURITY. A
|
||||
// rejected default Sober credential is suppressed by a private fingerprint
|
||||
// without modifying Sober's file. The guest policy then allows native sign-in
|
||||
// to continue. Explicit overrides, unsafe recovery, other invalid credentials,
|
||||
// and unavailable authentication still fail closed before native startup.
|
||||
// A VM requires authentication or explicit guest mode. HTTP 401 clears only a
|
||||
// managed credential; rejected Sober credentials are suppressed without
|
||||
// changing Sober's file.
|
||||
AuthRuntimeComposition ComposeAuthRuntime(const Environment& environment,
|
||||
const RuntimePaths& paths,
|
||||
services::AuthService& auth_service);
|
||||
|
||||
// Production overload retaining the HTTP transport for credentials delivered
|
||||
// by native sign-in after startup. A persisted credential is validated before
|
||||
// its account identity is promoted into the running pseudo-VM.
|
||||
// Retains HTTP transport for native sign-in and validates persisted credentials.
|
||||
AuthRuntimeComposition ComposeAuthRuntime(
|
||||
const Environment& environment, const RuntimePaths& paths,
|
||||
services::AuthService& auth_service,
|
||||
|
||||
@@ -21,19 +21,12 @@
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Applies Mocktail's mandatory privacy boundary to the flat Roblox
|
||||
// client-settings override object. The policy disables native Crashpad, Java
|
||||
// crash/ANR uploads, Lua Backtrace errors, and inferred-crash submission while
|
||||
// preserving unrelated caller overrides. Unsafe values are replaced rather
|
||||
// than treated as conflicts because crash upload is not a user-configurable
|
||||
// runtime capability.
|
||||
// Disables Roblox crash uploads while preserving unrelated overrides.
|
||||
bool MergeCrashReportClientSettingsOverrides(std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
|
||||
// Applies the same mandatory values to Roblox's separate AppBridge
|
||||
// fastFlagsJson channel. Only FastVariable names are inserted here;
|
||||
// Java-only client settings remain on the client-settings boundary above.
|
||||
// AppBridge accepts only the FastVariable subset of this policy.
|
||||
bool MergeCrashReportFastFlagsOverrides(std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
|
||||
@@ -28,9 +28,7 @@ enum class DeviceClass {
|
||||
kConsole,
|
||||
};
|
||||
|
||||
// One immutable profile keeps Roblox admission, pseudo-Android hardware
|
||||
// identity, and built-in input capabilities consistent. The guest ABI and
|
||||
// client-settings group remain Android/GoogleAndroidApp.
|
||||
// Admission changes do not change the Android guest ABI.
|
||||
struct DeviceProfile {
|
||||
std::string name;
|
||||
std::string cache_key;
|
||||
@@ -51,18 +49,14 @@ struct DeviceProfile {
|
||||
};
|
||||
|
||||
inline constexpr std::string_view kDefaultDeviceProfileName = "pc-windows-11";
|
||||
// These identifiers select Roblox server-side admission only. They never
|
||||
// replace the Android APK ABI or its GoogleAndroidApp client-settings group.
|
||||
// Transport classifier only; the guest remains GoogleAndroidApp.
|
||||
inline constexpr std::string_view kRobloxDesktopHttpUserAgent =
|
||||
"Roblox/WinInet";
|
||||
// The current Android-host playability endpoint recognizes this as console.
|
||||
// PlayStation-labelled user agents currently fall back to desktop admission,
|
||||
// so the PS5 marketing profile keeps this transport-only classifier.
|
||||
// The playability endpoint recognizes this classifier as console.
|
||||
inline constexpr std::string_view kRobloxConsoleAdmissionUserAgent =
|
||||
"Roblox/XboxOne";
|
||||
|
||||
// Accepts canonical names and the convenient pc/mobile/console aliases.
|
||||
// Returned profiles have static lifetime.
|
||||
// Accepts canonical names and pc/mobile/console aliases; result is static.
|
||||
const DeviceProfile* FindDeviceProfile(std::string_view name);
|
||||
bool IsValidDeviceProfileValue(std::string_view value, std::size_t maximum);
|
||||
std::string BuildCustomDeviceProfileCacheKey(const DeviceProfile& profile);
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_DISCORD_RPC_H_
|
||||
#define MOCKTAIL_RUNTIME_DISCORD_RPC_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "runtime/roblox_experience_presence.h"
|
||||
#include "runtime/runtime_config.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
struct DiscordRpcActivity {
|
||||
std::string details;
|
||||
std::string state;
|
||||
std::optional<int64_t> start_timestamp;
|
||||
std::string large_image;
|
||||
std::string large_text;
|
||||
std::string button_label;
|
||||
std::string button_url;
|
||||
};
|
||||
|
||||
bool IsPublicDiscordJoin(const RobloxExperienceLaunchRequest& request);
|
||||
std::string BuildDiscordJoinUrl(const RobloxExperienceLaunchRequest& request);
|
||||
DiscordRpcActivity BuildDiscordRpcActivity(
|
||||
const DiscordRpcConfig& config, RobloxExperiencePresencePhase phase,
|
||||
const RobloxExperienceLaunchRequest* request, std::string place_name,
|
||||
int64_t session_started_at, std::string place_icon_url = {});
|
||||
|
||||
// Maintains a direct, unauthenticated Discord Desktop RPC connection. The
|
||||
// worker owns all network and IPC work; lifecycle notifications only replace
|
||||
// a small in-memory snapshot and never block the Roblox main thread.
|
||||
class DiscordRpcSession final {
|
||||
public:
|
||||
explicit DiscordRpcSession(DiscordRpcConfig config);
|
||||
~DiscordRpcSession();
|
||||
|
||||
DiscordRpcSession(const DiscordRpcSession&) = delete;
|
||||
DiscordRpcSession& operator=(const DiscordRpcSession&) = delete;
|
||||
|
||||
bool Start(std::string* detail = nullptr);
|
||||
void Stop();
|
||||
RobloxExperiencePresenceObserver observer();
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
static void Notify(void* context, RobloxExperiencePresencePhase phase,
|
||||
const RobloxExperienceLaunchRequest* request);
|
||||
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_DISCORD_RPC_H_
|
||||
@@ -28,14 +28,10 @@ namespace runtime {
|
||||
inline constexpr std::size_t kMaximumPendingExternalLaunches = 8;
|
||||
|
||||
struct ExternalLaunchBrokerOptions {
|
||||
// Empty selects the current user's protected runtime endpoint. Tests and
|
||||
// isolated launchers may provide an explicit socket under a private
|
||||
// directory owned by the current user.
|
||||
// Empty selects the current user's protected endpoint.
|
||||
std::filesystem::path socket_path;
|
||||
std::size_t maximum_pending_launches = kMaximumPendingExternalLaunches;
|
||||
// The owner crosses configuration/bootstrap and a possible cgroup
|
||||
// re-exec before it can safely ACK. A concurrent browser click waits through
|
||||
// that bounded startup window instead of racing a not-yet-created socket.
|
||||
// Covers bootstrap and a possible cgroup re-exec.
|
||||
int forwarding_timeout_ms = 30000;
|
||||
};
|
||||
|
||||
@@ -49,16 +45,11 @@ struct ExternalLaunchSink {
|
||||
bool valid() const { return dispatch != nullptr; }
|
||||
};
|
||||
|
||||
// Resolves the per-user, per-installation endpoint. Owner resolution may
|
||||
// create only the private fallback directory; it never removes a stale
|
||||
// socket. Socket cleanup belongs exclusively to StartOwnerAfterLockAcquired
|
||||
// after the caller owns the matching Mocktail single-instance lock.
|
||||
// Resolution never removes stale sockets; only a lock-owning broker may do so.
|
||||
Status ResolveExternalLaunchSocketPath(bool owner,
|
||||
std::filesystem::path* socket_path);
|
||||
|
||||
// Owns a bounded process-local launch queue and its same-user AF_UNIX endpoint.
|
||||
// Wire requests contain only already-normalized ExperienceProtocol JSON; the
|
||||
// raw browser URI and its gameinfo ticket never cross this boundary.
|
||||
// The wire format contains normalized requests, never raw browser tickets.
|
||||
class ExternalLaunchBroker final {
|
||||
public:
|
||||
~ExternalLaunchBroker();
|
||||
@@ -68,25 +59,20 @@ class ExternalLaunchBroker final {
|
||||
ExternalLaunchBroker(ExternalLaunchBroker&&) = delete;
|
||||
ExternalLaunchBroker& operator=(ExternalLaunchBroker&&) = delete;
|
||||
|
||||
// The caller must hold the single-instance lock. Only this operation may
|
||||
// remove a stale socket left by a crashed owner. An optional initial request
|
||||
// is normalized and queued before the listener worker can accept later
|
||||
// browser clicks, preserving click order without a startup race.
|
||||
// Requires the single-instance lock. The initial request is queued before
|
||||
// the listener starts, preserving click order.
|
||||
static Status StartOwnerAfterLockAcquired(
|
||||
ExternalLaunchBrokerOptions options,
|
||||
std::shared_ptr<ExternalLaunchBroker>* broker,
|
||||
const RobloxExperienceLaunchRequest* initial_request = nullptr);
|
||||
|
||||
// Forwards one normalized request and waits for an explicit owner ACK. The
|
||||
// operation never starts a second runtime and never logs request contents.
|
||||
// Waits for an owner ACK and never logs the request.
|
||||
static Status ForwardToOwner(const ExternalLaunchBrokerOptions& options,
|
||||
const RobloxExperienceLaunchRequest& request);
|
||||
|
||||
Status QueueInitialRequest(RobloxExperienceLaunchRequest request);
|
||||
|
||||
// Dispatches at most maximum_requests in FIFO order. A failed dispatch is
|
||||
// returned to the front of the queue, so temporary downstream backpressure
|
||||
// never drops a browser launch.
|
||||
// A failed dispatch returns to the front of the FIFO queue.
|
||||
Status Drain(const ExternalLaunchSink& sink, std::size_t maximum_requests);
|
||||
|
||||
Status Shutdown();
|
||||
@@ -100,9 +86,7 @@ class ExternalLaunchBroker final {
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
// Process-global attachment used by the composition root and the dynamic
|
||||
// ExperienceProtocol runtime. shared_ptr acquisition keeps the broker alive
|
||||
// while an external request is being drained.
|
||||
// shared_ptr acquisition keeps the active broker alive during a drain.
|
||||
Status InstallActiveExternalLaunchBroker(
|
||||
const std::shared_ptr<ExternalLaunchBroker>& broker);
|
||||
void ClearActiveExternalLaunchBroker(const ExternalLaunchBroker* broker);
|
||||
|
||||
@@ -22,19 +22,14 @@
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Returns whether an interactive launch may present a desktop failure dialog.
|
||||
// Readiness canaries and headless sessions remain strictly non-interactive.
|
||||
// Canaries and headless sessions remain non-interactive.
|
||||
bool FailureDialogsEnabled(const Environment& environment);
|
||||
|
||||
// Presents one synchronous GTK failure dialog. This is intended for startup
|
||||
// failures that happen before the crash monitor can be armed.
|
||||
bool ShowFailureDialog(const Environment& environment,
|
||||
std::string_view message);
|
||||
|
||||
// Owns a small out-of-process GTK monitor. The helper is started before guest
|
||||
// threads exist and receives an explicit success disposition. If Mocktail
|
||||
// returns an error, calls _Exit(), or dies from a fatal signal, the helper
|
||||
// remains able to present the last safe user-facing message.
|
||||
// The helper starts before guest threads, so it can report errors and fatal
|
||||
// exits even when the main process can no longer show a dialog.
|
||||
class FailureDialogMonitor final {
|
||||
public:
|
||||
FailureDialogMonitor() = default;
|
||||
|
||||
@@ -87,9 +87,9 @@ using ResumeGameFn = Status (*)(void* context, const GameSurface& surface);
|
||||
using LeaveGameFn = Status (*)(void* context);
|
||||
using ShutdownGameRuntimeFn = Status (*)(void* context);
|
||||
|
||||
// native boundary. All callbacks are synchronous and non-throwing. The
|
||||
// coordinator serializes lifecycle invocation, matching MainGameActivity's
|
||||
// single-thread executor without embedding JNI details in the policy layer.
|
||||
// These callbacks form the native boundary. They are synchronous and
|
||||
// non-throwing; the coordinator serializes lifecycle calls to match
|
||||
// MainGameActivity's single-thread executor.
|
||||
struct GameSessionCapabilities {
|
||||
void* context = nullptr;
|
||||
SetGameForegroundFn set_game_foreground = nullptr;
|
||||
@@ -171,16 +171,11 @@ struct GameSessionSnapshot {
|
||||
std::string failure_reason;
|
||||
};
|
||||
|
||||
// Fail-closed policy for the MainGameActivity game-session branch.
|
||||
//
|
||||
// SurfaceCreated must prepare a generation before SurfaceChanged can supply its
|
||||
// native window and dimensions. QueueJoinRequest and the prepared surface are
|
||||
// independent initial prerequisites; whichever completes last starts the game
|
||||
// with the current candidate surface. Initial start never calls ResumeGame.
|
||||
// Once running, SurfaceChanged updates the active surface, SurfaceDestroyed
|
||||
// pauses, and SurfaceCreated(new generation) plus SurfaceChanged resumes.
|
||||
// Candidate surfaces become active only after the corresponding native call
|
||||
// succeeds.
|
||||
// SurfaceCreated must open a generation before its matching SurfaceChanged can
|
||||
// supply a window and size. A join request and the first surface may arrive in
|
||||
// either order; initial startup calls StartGame, never ResumeGame. Once running,
|
||||
// new generations pause and resume the session. A candidate surface becomes
|
||||
// active only after its native callback succeeds.
|
||||
class GameSessionCoordinator final {
|
||||
public:
|
||||
explicit GameSessionCoordinator(GameSessionCapabilities capabilities,
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// adapter kept outside the research runtime. The opaque context must be
|
||||
// the production jnivm::VM owned by the startup composition.
|
||||
// The opaque context must be the jnivm::VM owned by startup composition.
|
||||
bool SetJnivmPlatformWebCallbacks(
|
||||
void* context, std::shared_ptr<void> callback_context,
|
||||
void (*on_data_model_notification)(void*, JNIEnv*, jstring, jstring),
|
||||
|
||||
@@ -34,8 +34,7 @@ enum class PhysicsWorkerMode {
|
||||
bool ParsePhysicsWorkerMode(std::string_view value, PhysicsWorkerMode* mode);
|
||||
std::string_view PhysicsWorkerModeName(PhysicsWorkerMode mode);
|
||||
|
||||
// Makes every available physical core eligible for Roblox scheduler work.
|
||||
// At least one scheduler worker remains on malformed topology input.
|
||||
// Malformed topology still yields one worker.
|
||||
int CalculateThroughputWorkerCount(int available_physical_cores);
|
||||
|
||||
struct PerformancePolicy {
|
||||
@@ -61,25 +60,16 @@ PerformancePolicy ParsePerformancePolicy(
|
||||
std::string_view memory_limit_mb = "0", std::string_view game_mode = "auto",
|
||||
std::string_view physics_worker_mode = "auto");
|
||||
|
||||
// Counts unique physical cores available to the current process. Linux CPU
|
||||
// affinity is respected; systems without readable topology fall back to the
|
||||
// available logical CPU count.
|
||||
// Respects CPU affinity and falls back to the available logical count.
|
||||
int DetectAvailablePhysicalCoreCount();
|
||||
|
||||
// Merges the supported Roblox scheduler/rendering mode into the flat
|
||||
// client-settings override object. Disabled policy leaves existing overrides
|
||||
// intact. Conflicting explicit values fail closed instead of silently
|
||||
// replacing the caller's settings.
|
||||
// Disabled policy preserves overrides; conflicting values fail.
|
||||
bool MergePerformanceClientSettingsOverrides(const PerformancePolicy& policy,
|
||||
std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
|
||||
// Applies every runtime policy in a stable order before the resulting
|
||||
// override object crosses the legacy startup boundary. The mandatory
|
||||
// HttpClient compatibility policy is applied before the mandatory crash-report
|
||||
// upload block. Neither policy can be overridden by a caller-provided client
|
||||
// setting.
|
||||
// Applies mandatory HTTP and crash policies after caller settings.
|
||||
bool MergeRuntimeClientSettingsOverrides(const FrameRatePolicy& frame_rate,
|
||||
const PerformancePolicy& performance,
|
||||
std::string_view base_json,
|
||||
|
||||
@@ -143,11 +143,10 @@ struct RobloxBrowserServiceCallbackFactory {
|
||||
}
|
||||
};
|
||||
|
||||
// equivalent of the current APK's yg.c and gi.a BrowserService bridges.
|
||||
// It binds the four exact MemStorage keys and owns every Callback and
|
||||
// Connection until a disconnect-and-release shutdown has completed. Outgoing
|
||||
// WebView events are queued without JNI and must be drained by the platform
|
||||
// main thread.
|
||||
// Host counterpart to the APK's yg.c and gi.a BrowserService bridges. It binds
|
||||
// the four expected MemStorage keys. Shutdown disconnects before releasing its
|
||||
// callbacks and connections. The platform main thread drains outgoing WebView
|
||||
// events without holding JNI references.
|
||||
class RobloxBrowserServiceBridge final {
|
||||
public:
|
||||
RobloxBrowserServiceBridge(JniEnvironmentProvider environment,
|
||||
|
||||
@@ -21,12 +21,14 @@
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/owned_pthread.h"
|
||||
#include "runtime/roblox_browser_service_bridge.h"
|
||||
#include "runtime/roblox_experience_launch_bridge.h"
|
||||
#include "runtime/roblox_experience_presence.h"
|
||||
#include "runtime/roblox_fresh_game_launch_controller.h"
|
||||
#include "runtime/roblox_web_view_bridge.h"
|
||||
#include "runtime/webview_roblox_cookie.h"
|
||||
@@ -120,7 +122,8 @@ class RobloxExperienceComposition final {
|
||||
RobloxFreshLaunchPresentBoundary present_boundary,
|
||||
RobloxGameSurfaceJniConfig surface_config = {},
|
||||
const SecureRobloxCredential* initial_web_view_credential = nullptr,
|
||||
RobloxExperienceSurfaceProvider surface_provider = {});
|
||||
RobloxExperienceSurfaceProvider surface_provider = {},
|
||||
RobloxExperiencePresenceObserver presence_observer = {});
|
||||
~RobloxExperienceComposition();
|
||||
|
||||
RobloxExperienceComposition(const RobloxExperienceComposition&) = delete;
|
||||
@@ -185,6 +188,7 @@ class RobloxExperienceComposition final {
|
||||
void* context, const RobloxBrowserServiceExecuteRequest& request);
|
||||
static void WebSurfaceExited(void* context);
|
||||
static void NotifyLateLuaAppDidReturn(void* context);
|
||||
static void GamePresented(void* context, uint64_t frame_serial);
|
||||
static void* RunLaunchWorker(void* context);
|
||||
static Status RouteWebSurfaceEvent(WebSurfaceRoute route,
|
||||
const WebViewHelperEvent& event,
|
||||
@@ -205,6 +209,7 @@ class RobloxExperienceComposition final {
|
||||
Status Dispatch(const RobloxExperienceLaunchRequest& request);
|
||||
Status PromoteAuthenticatedSession();
|
||||
Status RefreshLateSurface();
|
||||
void PublishPresentedPresence(uint64_t frame_serial);
|
||||
void RunActiveLaunch();
|
||||
Status BuildPlatformGlobalObjects();
|
||||
Status BuildLuaAppGlobalObjects();
|
||||
@@ -218,6 +223,8 @@ class RobloxExperienceComposition final {
|
||||
Status RestartLuaAppSurface(const GameSurface& surface);
|
||||
Status ReleaseLuaAppGlobalObjects();
|
||||
Status ReleaseGlobalObjects();
|
||||
void NotifyPresence(RobloxExperiencePresencePhase phase,
|
||||
const RobloxExperienceLaunchRequest* request) const;
|
||||
|
||||
const JniEnvironmentProvider environment_;
|
||||
const RobloxExperienceMessageBusSymbols message_bus_symbols_;
|
||||
@@ -229,6 +236,7 @@ class RobloxExperienceComposition final {
|
||||
const RobloxGameSurfaceJniConfig surface_config_;
|
||||
const bool consumes_window_surface_events_;
|
||||
const RobloxExperienceSurfaceProvider surface_provider_;
|
||||
const RobloxExperiencePresenceObserver presence_observer_;
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
std::mutex surface_operation_mutex_;
|
||||
@@ -253,6 +261,7 @@ class RobloxExperienceComposition final {
|
||||
bool web_view_cookie_synchronized_ = false;
|
||||
std::deque<RobloxExperienceLaunchRequest> pending_launch_requests_;
|
||||
std::unique_ptr<LaunchTask> active_launch_;
|
||||
std::optional<RobloxExperienceLaunchRequest> presence_request_;
|
||||
std::string active_game_canonical_json_;
|
||||
OwnedPthread launch_worker_;
|
||||
uint64_t next_request_id_ = 1;
|
||||
@@ -260,6 +269,7 @@ class RobloxExperienceComposition final {
|
||||
bool subscribed_ = false;
|
||||
bool launch_in_progress_ = false;
|
||||
bool game_active_ = false;
|
||||
bool playing_presence_published_ = false;
|
||||
bool lua_app_return_pending_ = false;
|
||||
bool controlled_switch_waiting_for_return_ = false;
|
||||
bool lua_app_surface_recreation_pending_ = false;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_EXPERIENCE_PRESENCE_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_EXPERIENCE_PRESENCE_H_
|
||||
|
||||
#include "runtime/roblox_experience_launch_bridge.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
enum class RobloxExperiencePresencePhase {
|
||||
kBrowsing,
|
||||
kJoining,
|
||||
kPlaying,
|
||||
};
|
||||
|
||||
using NotifyRobloxExperiencePresenceFn =
|
||||
void (*)(void* context, RobloxExperiencePresencePhase phase,
|
||||
const RobloxExperienceLaunchRequest* request);
|
||||
|
||||
// Non-owning observer retained by RobloxExperienceComposition. Its owner must
|
||||
// outlive the composition. Browsing notifications carry a null request.
|
||||
struct RobloxExperiencePresenceObserver {
|
||||
void* context = nullptr;
|
||||
NotifyRobloxExperiencePresenceFn notify = nullptr;
|
||||
|
||||
bool valid() const { return notify != nullptr; }
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_ROBLOX_EXPERIENCE_PRESENCE_H_
|
||||
@@ -86,7 +86,8 @@ class RobloxFreshGameLaunchController final {
|
||||
RobloxFreshGameLaunchController(
|
||||
JniEnvironmentProvider environment, RobloxGameSessionSymbols symbols,
|
||||
RobloxFreshLaunchPresentBoundary present_boundary,
|
||||
RobloxGameSurfaceJniConfig surface_config = {});
|
||||
RobloxGameSurfaceJniConfig surface_config = {},
|
||||
RobloxGamePresentedObserver presented_observer = {});
|
||||
~RobloxFreshGameLaunchController();
|
||||
|
||||
RobloxFreshGameLaunchController(const RobloxFreshGameLaunchController&) =
|
||||
@@ -114,6 +115,7 @@ class RobloxFreshGameLaunchController final {
|
||||
const RobloxGameSessionSymbols symbols_;
|
||||
const RobloxFreshLaunchPresentBoundary present_boundary_;
|
||||
const RobloxGameSurfaceJniConfig surface_config_;
|
||||
const RobloxGamePresentedObserver presented_observer_;
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
std::unique_ptr<RobloxGameSessionRuntime> runtime_;
|
||||
|
||||
@@ -221,6 +221,16 @@ class RobloxGameSessionNativeAdapter final {
|
||||
bool initialized_ = false;
|
||||
};
|
||||
|
||||
using RobloxGamePresentedCallback = void (*)(void* context,
|
||||
uint64_t frame_serial);
|
||||
|
||||
struct RobloxGamePresentedObserver {
|
||||
void* context = nullptr;
|
||||
RobloxGamePresentedCallback notify = nullptr;
|
||||
|
||||
bool valid() const { return notify != nullptr; }
|
||||
};
|
||||
|
||||
// Small production owner that binds the JNI adapter to the policy coordinator.
|
||||
// Initial startup always follows the APK sequence:
|
||||
// principal -> join request -> SurfaceCreated(foreground) ->
|
||||
@@ -229,7 +239,8 @@ class RobloxGameSessionRuntime final {
|
||||
public:
|
||||
RobloxGameSessionRuntime(JniEnvironmentProvider environment,
|
||||
RobloxGameSessionSymbols symbols,
|
||||
RobloxGameSurfaceJniConfig surface_config = {});
|
||||
RobloxGameSurfaceJniConfig surface_config = {},
|
||||
RobloxGamePresentedObserver presented_observer = {});
|
||||
|
||||
RobloxGameSessionRuntime(const RobloxGameSessionRuntime&) = delete;
|
||||
RobloxGameSessionRuntime& operator=(const RobloxGameSessionRuntime&) = delete;
|
||||
@@ -256,6 +267,7 @@ class RobloxGameSessionRuntime final {
|
||||
private:
|
||||
RobloxGameSessionNativeAdapter adapter_;
|
||||
GameSessionCoordinator coordinator_;
|
||||
const RobloxGamePresentedObserver presented_observer_;
|
||||
// Makes the process-level pause/leave/destroy sequence atomic with respect
|
||||
// to surface updates. Present evidence bypasses this mutex
|
||||
// because StartGame can report it synchronously from the render callback.
|
||||
|
||||
@@ -28,7 +28,8 @@ inline constexpr std::size_t kMaximumRobloxLaunchUriBytes = 64 * 1024;
|
||||
|
||||
// Converts the two Roblox website launch protocols into the owned
|
||||
// ExperienceProtocol contract used by the supported game-session pipeline.
|
||||
// Browser authentication tickets and tracker identifiers are // discarded: the runtime uses its already validated account identity.
|
||||
// Authentication tickets and tracker IDs are discarded; the runtime already
|
||||
// has a validated account identity.
|
||||
Status ParseRobloxLaunchUri(std::string_view uri,
|
||||
RobloxExperienceLaunchRequest* request);
|
||||
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// host copy of the current APK's NativeTextBoxInfo Java value. The copy
|
||||
// contains no text and remains valid after the JNI local reference is released.
|
||||
// Host-owned NativeTextBoxInfo copy that outlives the JNI local reference.
|
||||
struct RobloxNativeTextBoxInfoSnapshot {
|
||||
float x = 0.0F;
|
||||
float y = 0.0F;
|
||||
|
||||
@@ -55,11 +55,30 @@ struct NetworkProxyConfig {
|
||||
int port = 0;
|
||||
};
|
||||
|
||||
struct DiscordRpcTextConfig {
|
||||
std::string browsing = "Browsing experiences";
|
||||
std::string joining = "Joining an experience";
|
||||
std::string playing = "{place_name}";
|
||||
std::string state = "Playing Roblox";
|
||||
std::string unknown_place = "Unknown experience";
|
||||
};
|
||||
|
||||
struct DiscordRpcConfig {
|
||||
bool enabled = false;
|
||||
bool show_place_name = true;
|
||||
bool show_elapsed_time = true;
|
||||
bool join_enabled = true;
|
||||
bool public_servers_only = true;
|
||||
std::string join_button_label = "Join Server";
|
||||
std::string application_id;
|
||||
DiscordRpcTextConfig text;
|
||||
};
|
||||
|
||||
std::optional<NetworkProxyConfig> ParseNetworkProxyConfig(
|
||||
std::string_view host, std::string_view port);
|
||||
std::string BuildNetworkProxyUrl(const NetworkProxyConfig& proxy);
|
||||
|
||||
// Immutable, supported runtime options. Experimental MOCKTAIL_PATCH_* knobs
|
||||
// Immutable, supported runtime options. Advanced MOCKTAIL_PATCH_* controls
|
||||
// intentionally do not belong here; they remain isolated in the legacy path.
|
||||
class RuntimeConfig {
|
||||
public:
|
||||
@@ -95,6 +114,8 @@ class RuntimeConfig {
|
||||
const std::optional<NetworkProxyConfig>& network_proxy() const {
|
||||
return network_proxy_;
|
||||
}
|
||||
const DiscordRpcConfig& discord_rpc() const { return discord_rpc_; }
|
||||
bool discord_rpc_valid() const { return discord_rpc_valid_; }
|
||||
|
||||
// These legacy opt-ins create workers that do not own their VM/JNI state.
|
||||
// Empty and "0" values are disabled; every other non-empty value is unsafe.
|
||||
@@ -123,6 +144,8 @@ class RuntimeConfig {
|
||||
std::string audio_output_device_ = "default";
|
||||
bool audio_output_device_valid_ = true;
|
||||
std::optional<NetworkProxyConfig> network_proxy_;
|
||||
DiscordRpcConfig discord_rpc_;
|
||||
bool discord_rpc_valid_ = true;
|
||||
std::vector<std::string> unsafe_detached_thread_overrides_;
|
||||
};
|
||||
|
||||
|
||||
@@ -52,7 +52,6 @@ CandidateApprovalResult CreateCandidateApproval(
|
||||
bool ValidateCandidateApproval(const std::filesystem::path& store_root,
|
||||
std::string_view activation_json,
|
||||
const PayloadIntegrityResult& payload,
|
||||
const std::filesystem::path& runtime_binary,
|
||||
CandidateApprovalResult* approval,
|
||||
std::string* error);
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ struct HostAbiDerivationResult {
|
||||
}
|
||||
};
|
||||
|
||||
// Derives an exact, experimental HostAbi profile by matching normalized x86-64
|
||||
// Derives an exact candidate HostAbi profile by matching normalized x86-64
|
||||
// instruction signatures against one already approved reference payload. This
|
||||
// is static analysis only; the result still requires two real Tier C canaries
|
||||
// before normal runtime activation.
|
||||
|
||||
@@ -53,7 +53,7 @@ class PayloadStore final {
|
||||
const std::array<std::filesystem::path, 2>& canary_logs);
|
||||
PayloadStoreResult Import(const std::filesystem::path& prepared_payload);
|
||||
PayloadStoreResult InspectCurrent() const;
|
||||
PayloadStoreResult VerifyCurrent() const;
|
||||
PayloadStoreResult VerifyCurrent();
|
||||
PayloadStoreResult Rollback();
|
||||
std::string StatusJson(std::string* error = nullptr) const;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Maintainer: komaruworld <rtysus228@gmail.com>
|
||||
|
||||
pkgname=mocktail
|
||||
pkgver=0.1.0
|
||||
pkgver=1.0.1
|
||||
pkgrel=1
|
||||
pkgdesc='Android x86-64 Roblox compatibility runtime for Linux'
|
||||
arch=('x86_64')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pkgbase = mocktail-bin
|
||||
pkgdesc = Android x86-64 Roblox compatibility runtime for Linux (prebuilt)
|
||||
pkgver = 0.1.0
|
||||
pkgver = 1.0.1
|
||||
pkgrel = 1
|
||||
url = https://github.com/komaruworld/mocktail
|
||||
arch = x86_64
|
||||
@@ -31,10 +31,10 @@ pkgbase = mocktail-bin
|
||||
provides = mocktail
|
||||
conflicts = mocktail
|
||||
conflicts = mocktail-git
|
||||
noextract = mocktail-0.1.0-1-x86_64.pkg.tar.zst
|
||||
noextract = mocktail-1.0.1-1-x86_64.pkg.tar.zst
|
||||
options = !debug
|
||||
options = !strip
|
||||
source = mocktail-0.1.0-1-x86_64.pkg.tar.zst::https://github.com/komaruworld/mocktail/releases/download/0.1.0/mocktail-0.1.0-1-x86_64.pkg.tar.zst
|
||||
sha256sums = f8a44ca7bd35526035ce7d1902ff7a4b1981a7c36e54d82ce4fc655db6fdf0f3
|
||||
source = mocktail-1.0.1-1-x86_64.pkg.tar.zst::https://github.com/komaruworld/mocktail/releases/download/1.0.1/mocktail-1.0.1-1-x86_64.pkg.tar.zst
|
||||
sha256sums = 63072fa49b3776cb164b71067696cd195e77470be7e0374444d4e8548d1de35c
|
||||
|
||||
pkgname = mocktail-bin
|
||||
@@ -1,7 +1,7 @@
|
||||
# Maintainer: komaruworld <rtysus228 at gmail dot com>
|
||||
|
||||
pkgname=mocktail-bin
|
||||
pkgver=0.1.0
|
||||
pkgver=1.0.1
|
||||
pkgrel=1
|
||||
pkgdesc='Android x86-64 Roblox compatibility runtime for Linux (prebuilt)'
|
||||
arch=('x86_64')
|
||||
@@ -38,7 +38,7 @@ options=('!debug' '!strip')
|
||||
_archive="mocktail-${pkgver}-${pkgrel}-${CARCH}.pkg.tar.zst"
|
||||
source=("${_archive}::https://github.com/komaruworld/mocktail/releases/download/${pkgver}/${_archive}")
|
||||
noextract=("${_archive}")
|
||||
sha256sums=('f8a44ca7bd35526035ce7d1902ff7a4b1981a7c36e54d82ce4fc655db6fdf0f3')
|
||||
sha256sums=('63072fa49b3776cb164b71067696cd195e77470be7e0374444d4e8548d1de35c')
|
||||
|
||||
package() {
|
||||
bsdtar -xf "${srcdir}/${_archive}" -C "${pkgdir}" usr
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pkgbase = mocktail-git
|
||||
pkgdesc = Android x86-64 Roblox compatibility runtime for Linux
|
||||
pkgver = 0.1.0.r1.g4e78c7d
|
||||
pkgver = 1.0.1.r15.g0a03962
|
||||
pkgrel = 1
|
||||
url = https://github.com/komaruworld/mocktail
|
||||
arch = x86_64
|
||||
@@ -10,6 +10,7 @@ pkgbase = mocktail-git
|
||||
makedepends = ninja
|
||||
makedepends = nlohmann-json
|
||||
makedepends = pkgconf
|
||||
makedepends = vulkan-headers
|
||||
depends = capstone
|
||||
depends = curl
|
||||
depends = fontconfig
|
||||
@@ -38,10 +39,6 @@ pkgbase = mocktail-git
|
||||
conflicts = mocktail-bin
|
||||
options = !debug
|
||||
source = mocktail::git+https://github.com/komaruworld/mocktail.git#branch=main
|
||||
source = libjnivm::git+https://github.com/ChristopherHX/libjnivm.git
|
||||
source = vulkan-headers::git+https://github.com/KhronosGroup/Vulkan-Headers.git
|
||||
sha256sums = SKIP
|
||||
sha256sums = SKIP
|
||||
sha256sums = SKIP
|
||||
|
||||
pkgname = mocktail-git
|
||||
@@ -1,7 +1,7 @@
|
||||
# Maintainer: komaruworld <rtysus228 at gmail dot com>
|
||||
|
||||
pkgname=mocktail-git
|
||||
pkgver=0.1.0.r1.g4e78c7d
|
||||
pkgver=1.0.1.r15.g0a03962
|
||||
pkgrel=1
|
||||
pkgdesc='Android x86-64 Roblox compatibility runtime for Linux'
|
||||
arch=('x86_64')
|
||||
@@ -37,6 +37,7 @@ makedepends=(
|
||||
'ninja'
|
||||
'nlohmann-json'
|
||||
'pkgconf'
|
||||
'vulkan-headers'
|
||||
)
|
||||
optdepends=('gamemode: Feral GameMode integration')
|
||||
provides=('mocktail')
|
||||
@@ -44,13 +45,9 @@ conflicts=('mocktail' 'mocktail-bin')
|
||||
options=('!debug')
|
||||
source=(
|
||||
'mocktail::git+https://github.com/komaruworld/mocktail.git#branch=main'
|
||||
'libjnivm::git+https://github.com/ChristopherHX/libjnivm.git'
|
||||
'vulkan-headers::git+https://github.com/KhronosGroup/Vulkan-Headers.git'
|
||||
)
|
||||
sha256sums=(
|
||||
'SKIP'
|
||||
'SKIP'
|
||||
'SKIP'
|
||||
)
|
||||
|
||||
pkgver() {
|
||||
@@ -69,11 +66,9 @@ pkgver() {
|
||||
prepare() {
|
||||
cd mocktail
|
||||
|
||||
git submodule init
|
||||
git config submodule.third_party/libjnivm.url "${srcdir}/libjnivm"
|
||||
git config submodule.third_party/Vulkan-Headers.url \
|
||||
"${srcdir}/vulkan-headers"
|
||||
git -c protocol.file.allow=always submodule update
|
||||
sed -i \
|
||||
's|add_subdirectory(third_party/Vulkan-Headers EXCLUDE_FROM_ALL)|find_package(VulkanHeaders CONFIG REQUIRED)|' \
|
||||
CMakeLists.txt
|
||||
}
|
||||
|
||||
build() {
|
||||
@@ -83,6 +78,7 @@ build() {
|
||||
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||
-DMOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST=/usr/share/mocktail/metadata/roblox_compatibility.json \
|
||||
-DMOCKTAIL_DEFAULT_SIGNING_TRUST_MANIFEST=/usr/share/mocktail/metadata/roblox_signing_certificates.json \
|
||||
-DMOCKTAIL_ENABLE_UPSTREAM_JNIVM=OFF \
|
||||
-DBUILD_TESTING=OFF
|
||||
cmake --build build
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pkgbase = mocktail
|
||||
pkgdesc = Android x86-64 Roblox compatibility runtime for Linux
|
||||
pkgver = 0.1.0
|
||||
pkgver = 1.0.1
|
||||
pkgrel = 1
|
||||
url = https://github.com/komaruworld/mocktail
|
||||
arch = x86_64
|
||||
@@ -10,6 +10,7 @@ pkgbase = mocktail
|
||||
makedepends = ninja
|
||||
makedepends = nlohmann-json
|
||||
makedepends = pkgconf
|
||||
makedepends = vulkan-headers
|
||||
depends = capstone
|
||||
depends = curl
|
||||
depends = fontconfig
|
||||
@@ -36,11 +37,7 @@ pkgbase = mocktail
|
||||
conflicts = mocktail-bin
|
||||
conflicts = mocktail-git
|
||||
options = !debug
|
||||
source = mocktail::git+https://github.com/komaruworld/mocktail.git#tag=0.1.0
|
||||
source = libjnivm::git+https://github.com/ChristopherHX/libjnivm.git
|
||||
source = vulkan-headers::git+https://github.com/KhronosGroup/Vulkan-Headers.git
|
||||
sha256sums = SKIP
|
||||
sha256sums = SKIP
|
||||
source = mocktail::git+https://github.com/komaruworld/mocktail.git#tag=1.0.1
|
||||
sha256sums = SKIP
|
||||
|
||||
pkgname = mocktail
|
||||
@@ -1,7 +1,7 @@
|
||||
# Maintainer: komaruworld <rtysus228 at gmail dot com>
|
||||
|
||||
pkgname=mocktail
|
||||
pkgver=0.1.0
|
||||
pkgver=1.0.1
|
||||
pkgrel=1
|
||||
pkgdesc='Android x86-64 Roblox compatibility runtime for Linux'
|
||||
arch=('x86_64')
|
||||
@@ -37,29 +37,24 @@ makedepends=(
|
||||
'ninja'
|
||||
'nlohmann-json'
|
||||
'pkgconf'
|
||||
'vulkan-headers'
|
||||
)
|
||||
optdepends=('gamemode: Feral GameMode integration')
|
||||
conflicts=('mocktail-bin' 'mocktail-git')
|
||||
options=('!debug')
|
||||
source=(
|
||||
"mocktail::git+https://github.com/komaruworld/mocktail.git#tag=${pkgver}"
|
||||
'libjnivm::git+https://github.com/ChristopherHX/libjnivm.git'
|
||||
'vulkan-headers::git+https://github.com/KhronosGroup/Vulkan-Headers.git'
|
||||
)
|
||||
sha256sums=(
|
||||
'SKIP'
|
||||
'SKIP'
|
||||
'SKIP'
|
||||
)
|
||||
|
||||
prepare() {
|
||||
cd mocktail
|
||||
|
||||
git submodule init
|
||||
git config submodule.third_party/libjnivm.url "${srcdir}/libjnivm"
|
||||
git config submodule.third_party/Vulkan-Headers.url \
|
||||
"${srcdir}/vulkan-headers"
|
||||
git -c protocol.file.allow=always submodule update
|
||||
sed -i \
|
||||
's|add_subdirectory(third_party/Vulkan-Headers EXCLUDE_FROM_ALL)|find_package(VulkanHeaders CONFIG REQUIRED)|' \
|
||||
CMakeLists.txt
|
||||
}
|
||||
|
||||
build() {
|
||||
@@ -69,6 +64,7 @@ build() {
|
||||
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||
-DMOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST=/usr/share/mocktail/metadata/roblox_compatibility.json \
|
||||
-DMOCKTAIL_DEFAULT_SIGNING_TRUST_MANIFEST=/usr/share/mocktail/metadata/roblox_signing_certificates.json \
|
||||
-DMOCKTAIL_ENABLE_UPSTREAM_JNIVM=OFF \
|
||||
-DBUILD_TESTING=OFF
|
||||
cmake --build build
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<meta name="referrer" content="no-referrer">
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'">
|
||||
<title>Join a Roblox server with Mocktail</title>
|
||||
<style>
|
||||
:root { font: 17px/1.55 system-ui, sans-serif; color-scheme: dark; }
|
||||
body { margin: 0; background: #0d1117; color: #e6edf3; }
|
||||
main { max-width: 560px; margin: 15vh auto; padding: 32px; }
|
||||
h1 { font-size: 2rem; }
|
||||
p { color: #9da7b3; }
|
||||
a { display: inline-block; padding: 12px 18px; border-radius: 9px;
|
||||
background: #238636; color: white; text-decoration: none;
|
||||
font-weight: 700; }
|
||||
[hidden] { display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body><main>
|
||||
<h1>Join Server</h1>
|
||||
<p id="status">Checking the Discord invitation…</p>
|
||||
<a id="launch" hidden>Open Mocktail</a>
|
||||
<noscript>This invitation requires JavaScript to open Mocktail.</noscript>
|
||||
</main>
|
||||
<script>
|
||||
'use strict';
|
||||
const status = document.querySelector('#status');
|
||||
const launch = document.querySelector('#launch');
|
||||
const query = new URLSearchParams(location.hash.slice(1));
|
||||
const placeId = query.get('placeId') || '';
|
||||
const gameInstanceId = query.get('gameInstanceId') || '';
|
||||
const validPlace = /^[1-9][0-9]{0,18}$/.test(placeId);
|
||||
const validInstance = gameInstanceId.length === 0 ||
|
||||
(gameInstanceId.length <= 128 &&
|
||||
!/[\u0000-\u001f\u007f]/.test(gameInstanceId));
|
||||
if (!validPlace || !validInstance) {
|
||||
status.textContent = 'This server invitation is invalid or incomplete.';
|
||||
} else {
|
||||
let target = 'roblox://experiences/start?placeId=' + placeId;
|
||||
if (gameInstanceId.length > 0) {
|
||||
target += '&gameInstanceId=' + encodeURIComponent(gameInstanceId);
|
||||
}
|
||||
launch.href = target;
|
||||
launch.hidden = false;
|
||||
status.textContent = gameInstanceId.length > 0
|
||||
? 'Opening this public Roblox server in Mocktail…'
|
||||
: 'Opening this Roblox experience in Mocktail…';
|
||||
location.replace(target);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -12,7 +12,17 @@
|
||||
"--socket=fallback-x11",
|
||||
"--socket=pulseaudio",
|
||||
"--device=dri",
|
||||
"--talk-name=com.feralinteractive.GameMode"
|
||||
"--talk-name=com.feralinteractive.GameMode",
|
||||
"--filesystem=xdg-run/discord-ipc-0:rw",
|
||||
"--filesystem=xdg-run/discord-ipc-1:rw",
|
||||
"--filesystem=xdg-run/discord-ipc-2:rw",
|
||||
"--filesystem=xdg-run/discord-ipc-3:rw",
|
||||
"--filesystem=xdg-run/discord-ipc-4:rw",
|
||||
"--filesystem=xdg-run/discord-ipc-5:rw",
|
||||
"--filesystem=xdg-run/discord-ipc-6:rw",
|
||||
"--filesystem=xdg-run/discord-ipc-7:rw",
|
||||
"--filesystem=xdg-run/discord-ipc-8:rw",
|
||||
"--filesystem=xdg-run/discord-ipc-9:rw"
|
||||
],
|
||||
"build-options": {
|
||||
"strip": true,
|
||||
@@ -166,6 +176,9 @@
|
||||
"buildsystem": "cmake-ninja",
|
||||
"config-opts": [
|
||||
"-DCMAKE_BUILD_TYPE=Release",
|
||||
"-DCMAKE_INSTALL_LIBDIR=lib",
|
||||
"-DMOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST=/app/share/mocktail/metadata/roblox_compatibility.json",
|
||||
"-DMOCKTAIL_DEFAULT_SIGNING_TRUST_MANIFEST=/app/share/mocktail/metadata/roblox_signing_certificates.json",
|
||||
"-DBUILD_TESTING=OFF"
|
||||
],
|
||||
"sources": [
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<component type="desktop-application">
|
||||
<id>space.bigrat.mocktail</id>
|
||||
<name>Mocktail</name>
|
||||
<summary>Experimental Roblox Android compatibility runtime for Linux</summary>
|
||||
<summary>Play Roblox on Linux</summary>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>Apache-2.0</project_license>
|
||||
<developer id="space.bigrat">
|
||||
@@ -13,29 +13,25 @@
|
||||
<url type="homepage">https://github.com/komaruworld/mocktail</url>
|
||||
<description>
|
||||
<p>
|
||||
Mocktail is an experimental compatibility runtime for running the
|
||||
Android x86-64 Roblox client on a native Linux desktop. It provides the
|
||||
pseudo-JVM, Android ABI adapters, SDL window/input/audio integration and
|
||||
Vulkan presentation needed by the client.
|
||||
Sign in to Roblox, browse experiences, and play them on Linux.
|
||||
</p>
|
||||
<p>
|
||||
The project is a prototype and is not affiliated with or endorsed by
|
||||
Roblox Corporation. The verified Roblox payload is downloaded on first
|
||||
launch and is not distributed with Mocktail.
|
||||
Mocktail is an independent community project. It is not affiliated with
|
||||
Roblox Corporation or VinegarHQ, and the Roblox client is not included.
|
||||
</p>
|
||||
</description>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/0.1.0/docs/screenshots/flatpak-home.png</image>
|
||||
<caption>Browse Roblox experiences in the desktop client</caption>
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/88b7382577aa138a2d16619b091f69abcdf7da15/assets/screenshots/flatpak-home.png</image>
|
||||
<caption>Browse Roblox experiences</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/0.1.0/docs/screenshots/flatpak-gameplay-tower.png</image>
|
||||
<caption>Roblox gameplay running through Mocktail</caption>
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/88b7382577aa138a2d16619b091f69abcdf7da15/assets/screenshots/flatpak-gameplay-tower.png</image>
|
||||
<caption>Play Tower of Hell on Linux</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/0.1.0/docs/screenshots/flatpak-gameplay-lobby.png</image>
|
||||
<caption>A Roblox experience running in the Flatpak build</caption>
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/88b7382577aa138a2d16619b091f69abcdf7da15/assets/screenshots/flatpak-gameplay-lobby.png</image>
|
||||
<caption>Play DOORS on Linux</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<launchable type="desktop-id">space.bigrat.mocktail.desktop</launchable>
|
||||
@@ -44,6 +40,14 @@
|
||||
<binary>mocktail</binary>
|
||||
</provides>
|
||||
<releases>
|
||||
<release version="1.0.1" date="2026-08-13">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Fix issues with GameMode</li>
|
||||
<li>Add Discord RPC support</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.1.0" date="2026-08-12"/>
|
||||
</releases>
|
||||
</component>
|
||||
@@ -59,7 +59,7 @@ VERSION_MARKER_PATTERN = re.compile(
|
||||
|
||||
|
||||
class ProviderError(RuntimeError):
|
||||
"""A safe, user-facing provider failure."""
|
||||
"""Raised when a provider response cannot be downloaded or validated."""
|
||||
|
||||
|
||||
def _has_version_record(metadata: bytes, version: str) -> bool:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
"""Download a provenance-pinned Uptodown XAPK bootstrap archive."""
|
||||
"""Download a pinned Uptodown XAPK for Mocktail's first-run setup."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -47,7 +47,7 @@ DOWNLOAD_ORIGIN = "https://dw.uptodown.com/dwn/"
|
||||
|
||||
|
||||
class ProviderError(RuntimeError):
|
||||
"""A safe, user-facing provider failure."""
|
||||
"""Raised when a provider response cannot be downloaded or validated."""
|
||||
|
||||
|
||||
class DownloadPageParser(HTMLParser):
|
||||
|
||||
@@ -9,7 +9,7 @@ readonly REPOSITORY="${1:?Flatpak repository path is required}"
|
||||
readonly BUNDLE="${2:?Flatpak bundle path is required}"
|
||||
readonly PUBLIC_KEY="${3:?GPG public key path is required}"
|
||||
readonly OUTPUT="${4:?Pages output path is required}"
|
||||
readonly BASE_URL="https://komaruworld.github.io/mocktail"
|
||||
readonly BASE_URL="https://mocktail.bigrat.space"
|
||||
|
||||
[[ -d "${REPOSITORY}" && ! -L "${REPOSITORY}" ]] || {
|
||||
printf 'Flatpak repository is missing or unsafe: %s\n' "${REPOSITORY}" >&2
|
||||
@@ -35,6 +35,9 @@ install -m 0644 -- "${PUBLIC_KEY}" "${OUTPUT}/mocktail-flatpak.gpg"
|
||||
install -m 0644 -- \
|
||||
"$(dirname -- "${BASH_SOURCE[0]}")/../packaging/space.bigrat.mocktail.svg" \
|
||||
"${OUTPUT}/mocktail.svg"
|
||||
install -m 0644 -- \
|
||||
"$(dirname -- "${BASH_SOURCE[0]}")/../packaging/discord-join.html" \
|
||||
"${OUTPUT}/join.html"
|
||||
|
||||
readonly GPG_KEY="$(base64 --wrap=0 "${PUBLIC_KEY}")"
|
||||
|
||||
@@ -57,7 +60,7 @@ Branch=stable
|
||||
Url=${BASE_URL}/repo/
|
||||
RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
Homepage=https://github.com/komaruworld/mocktail
|
||||
Comment=Experimental Linux compatibility runtime
|
||||
Comment=Play Roblox on Linux
|
||||
Icon=${BASE_URL}/mocktail.svg
|
||||
GPGKey=${GPG_KEY}
|
||||
IsRuntime=false
|
||||
@@ -92,8 +95,8 @@ cat >"${OUTPUT}/index.html" <<'EOF'
|
||||
<p>Signed development Flatpak builds for Linux x86_64.</p>
|
||||
<a class="button" href="mocktail.flatpakref">Install with Flatpak</a>
|
||||
<a class="button secondary" href="https://github.com/komaruworld/mocktail">Source</a>
|
||||
<code>flatpak install --user https://komaruworld.github.io/mocktail/mocktail.flatpakref</code>
|
||||
<p>Mocktail is experimental and is not affiliated with Roblox Corporation.</p>
|
||||
<code>flatpak install --user https://mocktail.bigrat.space/mocktail.flatpakref</code>
|
||||
<p>Mocktail is an independent community project and is not affiliated with Roblox Corporation.</p>
|
||||
</main></body>
|
||||
</html>
|
||||
EOF
|
||||
@@ -6,47 +6,9 @@
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# scripts/build.sh — Mocktail one-shot native build script.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/build.sh [OPTIONS]
|
||||
#
|
||||
# Options:
|
||||
# --apk <path> Explicitly extract a local Roblox x86_64 APK.
|
||||
# --apk-url <url> Explicitly download and extract an APK URL.
|
||||
# --skip-apk Compatibility alias for the default managed-payload mode.
|
||||
# --build-dir <path> CMake build directory. Relative paths use project root.
|
||||
# Default: build.
|
||||
# --cmake-toolchain <file>
|
||||
# Explicit CMake toolchain file.
|
||||
# --cmake-sysroot <dir>
|
||||
# Explicit compiler/CMake sysroot.
|
||||
# --build-type <type> CMake build type: Debug | Release | RelWithDebInfo.
|
||||
# Default: Release.
|
||||
# --jobs <n> Parallel build jobs. Default: nproc.
|
||||
# --run-tests Run CTest after build.
|
||||
# --clean Remove the build/ directory before configuring.
|
||||
# --help Show this help and exit.
|
||||
#
|
||||
# Examples:
|
||||
# # Build with a local APK file:
|
||||
# ./scripts/build.sh --apk ~/Downloads/roblox-x86_64.apk
|
||||
#
|
||||
# # Build with a direct download URL:
|
||||
# ./scripts/build.sh --apk-url "https://example.com/roblox-x86_64.apk"
|
||||
#
|
||||
# # Rebuild the C++ project; the first launch obtains the managed payload:
|
||||
# ./scripts/build.sh --skip-apk --build-type Debug --run-tests
|
||||
#
|
||||
# With no APK option, Mocktail builds only the native runtime. Its first normal
|
||||
# launch downloads, validates, canaries, and activates a supported x86_64
|
||||
# payload in the user's XDG data directory. ROBLOX_LIB_PATH remains an explicit
|
||||
# research override and disables APK extraction when already set.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colour helpers
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
@@ -91,7 +53,6 @@ Examples:
|
||||
EOF
|
||||
}
|
||||
|
||||
# Default configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
BUILD_DIR="${PROJECT_ROOT}/build"
|
||||
@@ -107,7 +68,6 @@ CLEAN=false
|
||||
CMAKE_TOOLCHAIN_FILE=""
|
||||
CMAKE_SYSROOT=""
|
||||
|
||||
# Argument parsing
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--apk)
|
||||
@@ -180,7 +140,6 @@ info "Mocktail build: ${BUILD_TYPE}, -j${JOBS}"
|
||||
info "Project root: ${PROJECT_ROOT}"
|
||||
info "Build directory: ${BUILD_DIR}"
|
||||
|
||||
# Step 1 — Check required tools
|
||||
step "1/5 checking dependencies"
|
||||
|
||||
check_tool() {
|
||||
@@ -208,7 +167,6 @@ if [[ "${EXTRACT_APK}" == true && -n "${APK_URL}" ]] &&
|
||||
check_tool wget "sudo apt install wget"
|
||||
fi
|
||||
|
||||
# Step 2 — Download APK (if URL provided)
|
||||
if [[ "${EXTRACT_APK}" == true && -n "${APK_URL}" ]]; then
|
||||
step "2/5 downloading Roblox APK"
|
||||
APK_PATH="${PROJECT_ROOT}/rbx_bin/roblox-x86_64.apk"
|
||||
@@ -223,7 +181,6 @@ if [[ "${EXTRACT_APK}" == true && -n "${APK_URL}" ]]; then
|
||||
success "APK downloaded."
|
||||
fi
|
||||
|
||||
# Step 3 — Extract libroblox.so from APK
|
||||
LIBROBLOX_DST="${RBX_BIN_DIR}/libroblox.so"
|
||||
|
||||
if [[ -n "${ROBLOX_LIB_PATH:-}" ]]; then
|
||||
@@ -251,7 +208,6 @@ else
|
||||
success "No APK or libroblox.so is required at build time."
|
||||
fi
|
||||
|
||||
# Step 4 — Init git submodules
|
||||
step "4/5 initialising git submodules"
|
||||
cd "${PROJECT_ROOT}"
|
||||
if [[ -f ".gitmodules" ]]; then
|
||||
@@ -262,7 +218,6 @@ else
|
||||
info "To add submodules run: ./scripts/add_submodules.sh"
|
||||
fi
|
||||
|
||||
# Step 5 — CMake configure
|
||||
step "5/5 configuring CMake"
|
||||
|
||||
if [[ "${CLEAN}" == true && -d "${BUILD_DIR}" ]]; then
|
||||
@@ -293,21 +248,18 @@ cmake "${cmake_arguments[@]}"
|
||||
|
||||
success "CMake configuration complete."
|
||||
|
||||
# Step 6 — Build
|
||||
step "building Mocktail"
|
||||
|
||||
cmake --build "${BUILD_DIR}" -j"${JOBS}"
|
||||
|
||||
success "Build complete → ${BUILD_DIR}/mocktail"
|
||||
|
||||
# Step 7 — Run tests (optional)
|
||||
if [[ "${RUN_TESTS}" == true ]]; then
|
||||
step "running unit tests"
|
||||
ctest --test-dir "${BUILD_DIR}" --output-on-failure -j"${JOBS}"
|
||||
success "All tests passed."
|
||||
fi
|
||||
|
||||
# Done
|
||||
echo
|
||||
success "Build successful."
|
||||
echo "Run:"
|
||||
|
||||
@@ -13,12 +13,11 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Derive a fail-closed HostAbi profile from one exact approved payload.
|
||||
"""Build a HostAbi profile for one approved payload without running it.
|
||||
|
||||
The analyzer never executes guest code and never marks a payload supported.
|
||||
It accepts only an exact reference sidecar bound to the reference ELF digest,
|
||||
matches native boundaries structurally, and emits an experimental manifest for
|
||||
an isolated updater probation run.
|
||||
The reference sidecar must match the reference ELF digest. The resulting
|
||||
manifest is meant for an updater probation run; it does not mark the payload as
|
||||
supported.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -40,7 +39,7 @@ from typing import Any, Iterator, Sequence
|
||||
try:
|
||||
import capstone
|
||||
from capstone import x86 as capstone_x86
|
||||
except ImportError: # The CLI turns this into one explicit fail-closed error.
|
||||
except ImportError:
|
||||
capstone = None
|
||||
capstone_x86 = None
|
||||
|
||||
@@ -107,7 +106,7 @@ PRIMARY_TOOLCHAIN_MARKERS = (
|
||||
|
||||
|
||||
class AnalyzerError(RuntimeError):
|
||||
"""One deterministic fail-closed analysis error."""
|
||||
"""Raised when HostAbi analysis rejects an input."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -1636,9 +1635,7 @@ def analyze(arguments: argparse.Namespace) -> tuple[dict[str, Any], dict[str, An
|
||||
|
||||
def parse_arguments(argv: Sequence[str]) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description=(
|
||||
"Derive an exact experimental Roblox HostAbi sidecar for isolated probation"
|
||||
)
|
||||
description="Derive a probationary Roblox HostAbi sidecar"
|
||||
)
|
||||
parser.add_argument("--reference-lib", required=True, metavar="REF")
|
||||
parser.add_argument("--reference-profile", required=True, metavar="REFJSON")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# Progressive real-startup harness for Mocktail.
|
||||
# Tiers A, B, and LEGACY are research experiments. Tier C verifies the
|
||||
# Tiers A, B, and LEGACY are diagnostic modes. Tier C verifies the
|
||||
# supported LuaApp graphics path. GAME verifies the local UGCGame graphics
|
||||
# path. INPUT adds a real SDL-to-JNI click to that gate. NETWORK verifies an
|
||||
# authenticated public-place join without exposing
|
||||
@@ -360,7 +360,7 @@ SUPPORT_BUNDLE_DISABLED="${MOCKTAIL_DISABLE_SUPPORT_BUNDLE:-0}"
|
||||
export MOCKTAIL_DISABLE_SUPPORT_BUNDLE=1
|
||||
set +e
|
||||
# Roblox may ignore SIGTERM while a native engine thread is
|
||||
# active. Bound the smoke experiment even in that state so the caller gets a
|
||||
# active. Bound the smoke run even in that state so the caller gets a
|
||||
# deterministic result and no stale window/process is left behind. Tier C with
|
||||
# an explicitly disabled present timer is the supported interactive launch,
|
||||
# however, and must remain alive until the user closes it.
|
||||
|
||||
@@ -200,7 +200,7 @@ InstallHostAbiExperiment(uintptr_t library_base, const HostAbiProfile &profile,
|
||||
result.uses_native_mimalloc = !options.install_allocator_bridges;
|
||||
if (library_base == 0 || profile.bridge_entry_count == 0 ||
|
||||
profile.bridge_entry_count > profile.bridge_entries.size()) {
|
||||
result.error = "invalid host ABI experiment profile";
|
||||
result.error = "invalid host ABI profile";
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ InstallHostAbiExperiment(uintptr_t library_base, const HostAbiProfile &profile,
|
||||
std::cout << std::flush;
|
||||
result.installed = ok;
|
||||
if (!ok) {
|
||||
result.error = "one or more host ABI experiment actions failed";
|
||||
result.error = "one or more host ABI actions failed";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -953,21 +953,14 @@ bool ValidateReceipt(const std::string& receipt_path,
|
||||
return false;
|
||||
}
|
||||
|
||||
const BuildIdResult runtime_build_id = ReadElfBuildId("/proc/self/exe");
|
||||
const FileSha256Result runtime_hash = ComputeFileSha256("/proc/self/exe");
|
||||
const FileSha256Result manifest_hash =
|
||||
ComputeFileSha256(compatibility_manifest_path);
|
||||
if (!runtime_build_id || !runtime_hash || !manifest_hash) {
|
||||
*error =
|
||||
"cannot identify runtime or hash compatibility manifest for approval "
|
||||
"receipt";
|
||||
if (!manifest_hash) {
|
||||
*error = "cannot hash compatibility manifest for approval receipt";
|
||||
return false;
|
||||
}
|
||||
if (receipt_runtime_build_id != runtime_build_id.build_id ||
|
||||
receipt_runtime_sha256 != runtime_hash.sha256 ||
|
||||
receipt_manifest_sha256 != manifest_hash.sha256) {
|
||||
*error =
|
||||
"approval receipt belongs to different runtime or compatibility bytes";
|
||||
if (receipt_manifest_sha256 != manifest_hash.sha256) {
|
||||
*error = "approval receipt belongs to different compatibility bytes";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -990,7 +983,7 @@ bool ValidateReceipt(const std::string& receipt_path,
|
||||
if (!ValidateCanaryAttestation(
|
||||
*canonical_attestation, index + 1, payload_id, payload_fingerprint,
|
||||
profile_sha256, manifest_hash.sha256, canary_runtime_sha256,
|
||||
runtime_build_id.build_id, &canary_evidence[index], error)) {
|
||||
receipt_runtime_build_id, &canary_evidence[index], error)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1018,7 +1011,7 @@ bool ValidateReceipt(const std::string& receipt_path,
|
||||
|
||||
std::string generation_evidence;
|
||||
generation_evidence.append("runtime_build_id=")
|
||||
.append(runtime_build_id.build_id)
|
||||
.append(receipt_runtime_build_id)
|
||||
.append("\n");
|
||||
generation_evidence.append("payload=")
|
||||
.append(payload_fingerprint)
|
||||
|
||||
@@ -62,7 +62,7 @@ PayloadCompatibilityResult CheckPayloadCompatibility(
|
||||
result.error = "This Roblox build is not enabled for normal runs: " +
|
||||
result.profile.reason +
|
||||
"\n Use --allow-unverified-build only for an explicit "
|
||||
"compatibility experiment.";
|
||||
"compatibility run.";
|
||||
result.profile = {};
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@ BackendSelection SelectGraphicsBackend(
|
||||
reason};
|
||||
}
|
||||
|
||||
// Fallback order remains Vulkan-first and deterministic.
|
||||
constexpr GraphicsBackendKind kFallbackOrder[] = {
|
||||
GraphicsBackendKind::kAngleVulkan,
|
||||
GraphicsBackendKind::kDirectVulkan,
|
||||
|
||||
+6
-24
@@ -12,8 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// src/jnivm/jnivm.cc — Implementation of the Pseudo Java Native Interface VM.
|
||||
|
||||
#include "jnivm/jnivm.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -35,8 +33,6 @@
|
||||
|
||||
namespace jnivm {
|
||||
|
||||
// Class implementation
|
||||
|
||||
void Class::RegisterMethod(const std::string& method_name,
|
||||
const std::string& signature,
|
||||
MethodCallback callback) {
|
||||
@@ -71,7 +67,6 @@ thread_local JNIEnv* g_thread_local_env = nullptr;
|
||||
thread_local JNIEnv g_thread_env_storage = {};
|
||||
thread_local VM* g_thread_vm_instance = nullptr;
|
||||
|
||||
// Thread-local pointer to the active VM instance for JNI callback resolution.
|
||||
VM* g_vm_instance = nullptr;
|
||||
std::recursive_mutex g_jni_state_mutex;
|
||||
|
||||
@@ -495,8 +490,7 @@ RobloxAuthIdentity ResearchRobloxIdentityFromEnvironment() {
|
||||
return identity;
|
||||
}
|
||||
|
||||
// Temporary research-only compatibility path. Production startup should
|
||||
// inject a resolved identity through VM::SetRobloxAuthIdentity instead.
|
||||
// Normal startup injects identity through VM::SetRobloxAuthIdentity.
|
||||
identity.user_id = static_cast<int64_t>(parsed);
|
||||
const char* username = std::getenv("MOCKTAIL_ROBLOX_USERNAME");
|
||||
if (username && username[0] != '\0') {
|
||||
@@ -557,8 +551,7 @@ std::shared_ptr<Class> ClassFromJClass(jclass clazz) {
|
||||
return FallbackClassForName(cls->GetName());
|
||||
}
|
||||
|
||||
// Stores a shared_ptr into my_segment, keeping it alive in g_segment_owners.
|
||||
// Returns the encoded segment-table handle cast to jclass.
|
||||
// g_segment_owners keeps the encoded class handle alive.
|
||||
static jclass StoreClass(std::shared_ptr<Class> cls) {
|
||||
std::lock_guard<std::recursive_mutex> lock(g_jni_state_mutex);
|
||||
Class* raw_ptr = cls.get();
|
||||
@@ -593,9 +586,8 @@ PseudoJavaObject* PseudoObjectFromRef(jobject obj) {
|
||||
raw_ptr = reinterpret_cast<Object*>(obj);
|
||||
}
|
||||
if (!raw_ptr) return nullptr;
|
||||
// Perform a static cast instead of dynamic_cast to avoid calling the host
|
||||
// __dynamic_cast against potentially-Bionic RTTI structures. All objects
|
||||
// stored in our segment table are PseudoJavaObjects by construction.
|
||||
// Avoid host __dynamic_cast on potentially Bionic RTTI; segment entries are
|
||||
// PseudoJavaObjects by construction.
|
||||
return static_cast<PseudoJavaObject*>(raw_ptr);
|
||||
}
|
||||
|
||||
@@ -840,8 +832,7 @@ std::string JavaStringUtf8Bytes(const std::vector<jchar>& utf16) {
|
||||
code_point = 0x10000 + ((code_point - 0xd800) << 10) +
|
||||
(utf16[++index] - 0xdc00);
|
||||
} else if (code_point >= 0xd800 && code_point <= 0xdfff) {
|
||||
// java.nio.charset.CharsetEncoder replaces each malformed UTF-16 input
|
||||
// unit with its configured UTF-8 replacement, the single byte '?'.
|
||||
// CharsetEncoder replaces each malformed UTF-16 unit with '?'.
|
||||
output.push_back('?');
|
||||
continue;
|
||||
}
|
||||
@@ -5082,9 +5073,7 @@ JNIEnv* VM::GetJNIEnv() {
|
||||
}
|
||||
|
||||
void VM::RestoreFunctions() {
|
||||
// Force env->functions back to our pseudo-VM table on ALL known JNIEnv*.
|
||||
// libroblox.so replaces env->functions with a Bionic JNI table during
|
||||
// JNI_OnLoad; we need to restore it before Stage 6 calls.
|
||||
// JNI_OnLoad replaces env->functions; restore all known environments.
|
||||
java_vm_storage_.functions = &invoke_interface_;
|
||||
java_vm_ = &java_vm_storage_;
|
||||
if (jni_env_) {
|
||||
@@ -5121,10 +5110,6 @@ std::shared_ptr<Class> VM::FindClass(const std::string& class_name) const {
|
||||
}
|
||||
|
||||
void VM::InitJNIFunctionTables() {
|
||||
// JNIInvokeInterface_ — the JavaVM function table.
|
||||
// Most functions are no-ops in this pseudo-VM; only AttachCurrentThread
|
||||
// is meaningful for single-threaded prototype use.
|
||||
|
||||
invoke_interface_.AttachCurrentThread =
|
||||
[](JavaVM* vm, void** env, void* /*args*/) -> jint {
|
||||
if (JniVmTraceEnabled()) {
|
||||
@@ -5207,9 +5192,6 @@ void VM::InitJNIFunctionTables() {
|
||||
java_vm_storage_.functions = &invoke_interface_;
|
||||
java_vm_ = &java_vm_storage_;
|
||||
|
||||
// JNINativeInterface_ — the JNIEnv function table.
|
||||
// Only the minimal subset required by Roblox's JNI_OnLoad is stubbed here.
|
||||
|
||||
native_interface_.FindClass =
|
||||
[](JNIEnv* /*env*/, const char* name) -> jclass {
|
||||
if (JniVmTraceEnabled()) {
|
||||
|
||||
+60
-136
@@ -12,18 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Transitional Sober compatibility runtime. This file preserves the current
|
||||
// reverse-engineered startup path while upstream-backed modules replace
|
||||
// it one boundary at a time.
|
||||
//
|
||||
// Implements the preserved 5-stage initialisation pipeline:
|
||||
//
|
||||
// Stage 1: Initialise Pseudo-JVM (libjnivm).
|
||||
// Stage 2: Register mandatory Android SDK JNI classes.
|
||||
// Stage 3: Load host system libraries under Bionic alias names.
|
||||
// Stage 4: Map libroblox.so into process address space via the linker.
|
||||
// Stage 5: Invoke JNI_OnLoad and hand control to the game engine.
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
@@ -76,6 +64,7 @@
|
||||
#include "linker/linker.h"
|
||||
#include "mocktail/graphics/bionic_egl_bridge.h"
|
||||
#include "runtime/environment.h"
|
||||
#include "runtime/discord_rpc.h"
|
||||
#include "runtime/jnivm_platform_web_callbacks.h"
|
||||
#include "runtime/owned_pthread.h"
|
||||
#include "runtime/roblox_app_lifecycle.h"
|
||||
@@ -399,7 +388,6 @@ extern "C" int mocktail_recover_stack_chk_fail() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// JNI_OnLoad function pointer type as specified by the JNI standard.
|
||||
using JniOnLoadFn = jint (*)(JavaVM*, void*);
|
||||
using NativeGameGlobalInitFn = void (*)(JNIEnv*, jclass);
|
||||
using NativeInitClientSettingsFn = jint (*)(JNIEnv*, jclass, jstring, jstring,
|
||||
@@ -2518,10 +2506,7 @@ static void* GetThreadScratchBuffer(pid_t tid) {
|
||||
return g_thread_scratch_pool[0].buffer;
|
||||
}
|
||||
|
||||
// Fallback vtable used when a null/invalid object pointer is dereferenced
|
||||
// during a vtable dispatch in Stage 6. Each slot is a no-op stub that
|
||||
// returns 0 so that the caller can continue execution. The table covers
|
||||
// all vtable offsets seen in libroblox (currently up to +0xf8 = slot 31).
|
||||
// Covers every libroblox vtable offset observed through slot 31 (+0xf8).
|
||||
static uintptr_t NullVtableStub() { return 0; }
|
||||
static const uintptr_t kFallbackVtable[32] = {
|
||||
reinterpret_cast<uintptr_t>(&NullVtableStub),
|
||||
@@ -2557,8 +2542,7 @@ static const uintptr_t kFallbackVtable[32] = {
|
||||
reinterpret_cast<uintptr_t>(&NullVtableStub),
|
||||
reinterpret_cast<uintptr_t>(&NullVtableStub),
|
||||
};
|
||||
// Fake object whose first 8 bytes point to kFallbackVtable.
|
||||
// Used to satisfy `mov rax,[rsi]; call [rax+offset]` when RSI=null.
|
||||
// Lets a recovered `mov rax,[rsi]` continue through the fallback vtable.
|
||||
static const uintptr_t kFallbackObject[4] = {
|
||||
reinterpret_cast<uintptr_t>(kFallbackVtable), 0, 0, 0,
|
||||
};
|
||||
@@ -2809,12 +2793,10 @@ void ApplyRuntimeDefaults() {
|
||||
SetEnvDefault("MOCKTAIL_ENGINE_DETACH", "0");
|
||||
SetEnvDefault("MOCKTAIL_INIT_CLIENT_SETTINGS", "0");
|
||||
SetEnvDefault("MOCKTAIL_POST_CLIENT_SETTINGS", "0");
|
||||
// Sober-compatible fallback: allow unauthenticated startup (LuaApp path)
|
||||
// unless explicitly disabled.
|
||||
SetEnvDefault("MOCKTAIL_ALLOW_NO_COOKIE_LUA_APP", "1");
|
||||
// Current Roblox Android builds drive app startup through NativeGLInterface's
|
||||
// V2 app bridge path. The legacy NativeAppBridgeInterface entry point stays
|
||||
// available for experiments, but running it in parallel can block V2 init.
|
||||
// available by opt-in, but running it in parallel can block V2 init.
|
||||
SetEnvDefault("MOCKTAIL_APP_BRIDGE_APP_START", "0");
|
||||
SetEnvDefault("MOCKTAIL_APP_BRIDGE_APP_START_THREAD", "0");
|
||||
SetEnvDefault("MOCKTAIL_START_LUA_APP_DM", "1");
|
||||
@@ -5823,8 +5805,8 @@ extern "C" void* mocktail_roblox_allocator_object_alloc_bridge(void*,
|
||||
return mocktail_roblox_aligned_alloc_bridge(size, align);
|
||||
}
|
||||
|
||||
// Retained only for the legacy fixed-offset allocator patch below. Current
|
||||
// Build-ID experiments own their seed storage in compat/host_abi_experiment.
|
||||
// Retained for the legacy fixed-offset allocator patch. Build-ID profiles keep
|
||||
// their seed storage in compat/host_abi_experiment.
|
||||
uintptr_t g_mocktail_roblox_allocator_object_vtable[4] = {};
|
||||
uintptr_t g_mocktail_roblox_allocator_object[1] = {
|
||||
reinterpret_cast<uintptr_t>(g_mocktail_roblox_allocator_object_vtable)};
|
||||
@@ -5901,7 +5883,7 @@ bool InstallActiveHostAbiExperiment(uintptr_t libroblox_base) {
|
||||
}
|
||||
g_host_abi_install_attempted = true;
|
||||
if (!HostAbiExperimentRequested()) {
|
||||
std::cout << " [compat] host ABI experiment disabled by policy\n"
|
||||
std::cout << " [compat] host ABI profile disabled by policy\n"
|
||||
<< std::flush;
|
||||
return false;
|
||||
}
|
||||
@@ -5909,7 +5891,7 @@ bool InstallActiveHostAbiExperiment(uintptr_t libroblox_base) {
|
||||
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 experiment has no active profile/base\n"
|
||||
std::cerr << " [compat] host ABI install has no active profile/base\n"
|
||||
<< std::flush;
|
||||
return false;
|
||||
}
|
||||
@@ -6533,7 +6515,7 @@ extern "C" void mocktail_before_soinfo_constructors(const char* realpath,
|
||||
|
||||
// Host alloc bridges must be in place before any constructor executes.
|
||||
if (HostAbiExperimentRequested() && !InstallActiveHostAbiExperiment(base)) {
|
||||
std::cerr << " [compat] host ABI experiment failed before constructors\n"
|
||||
std::cerr << " [compat] host ABI install failed before constructors\n"
|
||||
<< std::flush;
|
||||
return;
|
||||
}
|
||||
@@ -6633,7 +6615,6 @@ extern "C" void mocktail_before_soinfo_constructors(const char* realpath,
|
||||
uintptr_t* init_array =
|
||||
reinterpret_cast<uintptr_t*>(base + init_array_offset);
|
||||
|
||||
// Make the page writable just in case
|
||||
long ctor_page_size = sysconf(_SC_PAGESIZE);
|
||||
if (ctor_page_size > 0) {
|
||||
uintptr_t addr = reinterpret_cast<uintptr_t>(init_array);
|
||||
@@ -6647,7 +6628,6 @@ extern "C" void mocktail_before_soinfo_constructors(const char* realpath,
|
||||
g_original_ctors.reserve(kCtorCount);
|
||||
for (size_t i = 0; i < kCtorCount; ++i) {
|
||||
g_original_ctors.push_back(init_array[i]);
|
||||
// Skip the first constructor if env flag is set, otherwise wrap it
|
||||
if (i == 0 && IsEnabled("MOCKTAIL_PATCH_FIRST_CONSTRUCTOR")) {
|
||||
static void (*noop_fn)() = []() {};
|
||||
g_original_ctors[i] = reinterpret_cast<uintptr_t>(noop_fn);
|
||||
@@ -26925,26 +26905,21 @@ void HeadlessSegvHandler(int signo, siginfo_t* info, void* context) {
|
||||
return;
|
||||
}
|
||||
|
||||
// libGLdispatch crash recovery (handles Bionic TLS incompatibility in Glibc threads).
|
||||
// Matches any SIGSEGV occurring inside libGLdispatch.so.0.
|
||||
// libGLdispatch may access Bionic TLS keys through glibc thread state.
|
||||
if (g_current_stage >= 6 && module_name && std::strstr(module_name, "libGLdispatch") != nullptr) {
|
||||
// If the instruction is a 64-bit store (mov [reg], reg) or load (mov reg, [reg]):
|
||||
if (instruction[0] == 0x48 && (instruction[1] == 0x89 || instruction[1] == 0x8b)) {
|
||||
int rm = instruction[2] & 7;
|
||||
int reg_map[] = { REG_RAX, REG_RCX, REG_RDX, REG_RBX, -1, -1, REG_RSI, REG_RDI };
|
||||
if (rm < 8 && reg_map[rm] != -1) {
|
||||
uintptr_t old_val = ucontext->uc_mcontext.gregs[reg_map[rm]];
|
||||
// Only redirect if the register is currently NULL, very low, or points to a read-only stub/vtable.
|
||||
if (old_val < kStage5LowAddressThreshold) {
|
||||
// Get or allocate a thread-specific writable buffer from our pool.
|
||||
// This avoids calling malloc (which corrupts the heap if interrupted)
|
||||
// and avoids sharing the same static buffer across multiple threads (which causes corruption).
|
||||
// Signal handlers cannot call malloc; each thread gets fixed scratch
|
||||
// storage to avoid sharing writes across recovered threads.
|
||||
pid_t tid = static_cast<pid_t>(syscall(SYS_gettid));
|
||||
void* thread_scratch = GetThreadScratchBuffer(tid);
|
||||
ucontext->uc_mcontext.gregs[reg_map[rm]] = reinterpret_cast<greg_t>(thread_scratch);
|
||||
} else if (old_val == reinterpret_cast<uintptr_t>(&NullVtableStub) ||
|
||||
old_val == reinterpret_cast<uintptr_t>(kFallbackVtable)) {
|
||||
// If it was redirected to a read-only stub/vtable, redirect it to a thread-specific writable buffer.
|
||||
pid_t tid = static_cast<pid_t>(syscall(SYS_gettid));
|
||||
void* thread_scratch = GetThreadScratchBuffer(tid);
|
||||
ucontext->uc_mcontext.gregs[reg_map[rm]] = reinterpret_cast<greg_t>(thread_scratch);
|
||||
@@ -27015,7 +26990,7 @@ void HeadlessSegvHandler(int signo, siginfo_t* info, void* context) {
|
||||
}
|
||||
skip_stage6_null_object_immediate_store:
|
||||
|
||||
// Broader: any `48 89 ??` store with low dest-addr (catches all register combos).
|
||||
// Catch the remaining `48 89 ??` stores to low addresses.
|
||||
if (g_current_stage >= 6 && info &&
|
||||
reinterpret_cast<uintptr_t>(info->si_addr) < kStage5LowAddressThreshold &&
|
||||
instruction[0] == 0x48 && instruction[1] == 0x89) {
|
||||
@@ -27025,10 +27000,9 @@ skip_stage6_null_object_immediate_store:
|
||||
write(2, msg, sizeof(msg) - 1);
|
||||
}
|
||||
++g_skipped_headless_null_writes;
|
||||
// Determine instruction length (modrm) — skip the store.
|
||||
// 48 89 /r — 3 or 4 bytes depending on displacement.
|
||||
uint8_t modrm = instruction[2];
|
||||
uint8_t mod = (modrm >> 6) & 0x3;
|
||||
// The ModR/M displacement determines how many instruction bytes to skip.
|
||||
int instr_len = (mod == 1) ? 4 : (mod == 2) ? 7 : 3;
|
||||
ucontext->uc_mcontext.gregs[REG_RIP] += instr_len;
|
||||
return;
|
||||
@@ -27053,9 +27027,7 @@ skip_stage6_null_object_immediate_store:
|
||||
reinterpret_cast<uintptr_t>(info->si_addr) == 0 &&
|
||||
instruction[0] == 0x48 && instruction[1] == 0x8b &&
|
||||
instruction[2] == 0x06) {
|
||||
// Pattern: mov rax,[rsi] with RSI=null, often followed by call [rax+N].
|
||||
// Point RSI at a fake object whose vtable is kFallbackVtable so that the
|
||||
// load succeeds and subsequent indirect calls land in NullVtableStub.
|
||||
// Redirect the null receiver so a following vtable call reaches the stub.
|
||||
if (g_skipped_headless_null_writes == 0) {
|
||||
const char msg[] =
|
||||
" [patch] redirected Stage6 mov rax,[rsi] (RSI=null) to fallback object\n";
|
||||
@@ -27064,13 +27036,11 @@ skip_stage6_null_object_immediate_store:
|
||||
++g_skipped_headless_null_writes;
|
||||
ucontext->uc_mcontext.gregs[REG_RSI] =
|
||||
reinterpret_cast<greg_t>(kFallbackObject);
|
||||
// Do NOT advance RIP — re-execute the mov against the now-valid RSI.
|
||||
// Re-run the load against the repaired receiver.
|
||||
return;
|
||||
}
|
||||
|
||||
// Broader fallback: any Stage6 null/low-address load through RSI into RAX
|
||||
// that is the exact 3-byte form `48 8b 06` — recover by pointing RSI at
|
||||
// fallback vtable object so the call chain can continue.
|
||||
// The same recovery applies to nonzero addresses below the valid threshold.
|
||||
if (g_current_stage >= 6 && info &&
|
||||
reinterpret_cast<uintptr_t>(info->si_addr) < kStage5LowAddressThreshold &&
|
||||
instruction[0] == 0x48 && instruction[1] == 0x8b &&
|
||||
@@ -27106,8 +27076,7 @@ skip_stage6_null_object_immediate_store:
|
||||
return;
|
||||
}
|
||||
|
||||
// Also handle `mov rax,[rax]` (48 8b 00) / `mov rax,[rax+N]` (48 8b 40 NN)
|
||||
// with low RAX — redirect RAX to kFallbackVtable.
|
||||
// Redirect low `mov rax,[rax+N]` bases to the fallback vtable.
|
||||
if (g_current_stage >= 6 && info &&
|
||||
reinterpret_cast<uintptr_t>(info->si_addr) < kStage5LowAddressThreshold &&
|
||||
instruction[0] == 0x48 && instruction[1] == 0x8b &&
|
||||
@@ -27279,12 +27248,8 @@ skip_stage6_null_object_immediate_store:
|
||||
return;
|
||||
}
|
||||
|
||||
// SI_KERNEL SIGTRAP (si_code=128): kernel-generated trap, commonly a
|
||||
// shadow-stack (CET/SHSTK) return-address mismatch on Roblox's internal
|
||||
// threads. The instruction at RIP is not an int3 — it's the instruction
|
||||
// that would have executed after the bad return. We cannot safely continue
|
||||
// from here; if a recovery jmp_buf is armed on this thread use it, otherwise
|
||||
// just exit the offending thread so the rest of the engine keeps running.
|
||||
// SI_KERNEL SIGTRAP commonly follows a CET shadow-stack return mismatch.
|
||||
// Use a thread-local recovery target when one is armed.
|
||||
if (signo == SIGTRAP && info && info->si_code == SI_KERNEL &&
|
||||
g_current_stage >= 6) {
|
||||
if (libroblox_base != 0 &&
|
||||
@@ -27323,17 +27288,11 @@ skip_stage6_null_object_immediate_store:
|
||||
write(2, msg, sizeof(msg) - 1);
|
||||
siglongjmp(g_start_app_with_params_jmp_buf, 1);
|
||||
}
|
||||
// No recovery target on this thread — skip the trap by advancing RIP past
|
||||
// the current instruction and returning. Killing the thread via
|
||||
// pthread_exit would hang the engine since Roblox waits on these threads.
|
||||
// SI_KERNEL SIGTRAP here is a shadow-stack (CET) mismatch on Roblox's
|
||||
// internal threads; skipping it lets execution continue from the instruction
|
||||
// after the bad return address.
|
||||
// Roblox may wait on this thread, so exiting it can hang the engine.
|
||||
const char msg[] =
|
||||
" [patch] SI_KERNEL SIGTRAP: no recovery, skipping instruction\n";
|
||||
write(2, msg, sizeof(msg) - 1);
|
||||
// Advance past the current instruction — for SI_KERNEL traps the faulting
|
||||
// instruction is still at RIP (not executed yet), so +1 moves past it.
|
||||
// SI_KERNEL leaves RIP on the instruction that has not run yet.
|
||||
ucontext->uc_mcontext.gregs[REG_RIP] += 1;
|
||||
return;
|
||||
}
|
||||
@@ -28487,9 +28446,8 @@ bool PatchStartAppDebugTrap(void* native_start_app_with_params) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// NOTE: We intentionally do NOT patch the function entry point with `ret`.
|
||||
// Doing so would silently kill the real startup path. Only the known-bad
|
||||
// diagnostic tail block inside the function is bypassed here.
|
||||
// Patch only the diagnostic tail; replacing the entry point would disable
|
||||
// real startup.
|
||||
constexpr uintptr_t kNativeStartAppOffset = 0x250436d;
|
||||
constexpr uintptr_t kTailPatchOffset = 0x2504567;
|
||||
constexpr unsigned char kTailJumpPatch[] = {
|
||||
@@ -30532,9 +30490,7 @@ void* EngineStartupThread(void* arg) {
|
||||
<< std::flush;
|
||||
mocktail::window::MakeCurrentOnThread();
|
||||
}
|
||||
// Force our pseudo-VM function table on the env that EngineStartupThread will use.
|
||||
// libroblox.so replaces env->functions during JNI_OnLoad; we restore it here so
|
||||
// that our NewStringUTF / GetStringUTFChars helpers work correctly in Stage 6.
|
||||
// JNI_OnLoad replaces env->functions; Stage 6 needs the pseudo-VM table.
|
||||
if (env && context->vm) {
|
||||
context->vm->RestoreFunctions();
|
||||
env = context->vm->GetJNIEnv();
|
||||
@@ -31922,6 +31878,17 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
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";
|
||||
}
|
||||
}
|
||||
const mocktail::runtime::InputCapabilityConfig& input_capabilities =
|
||||
runtime_config.input_capabilities();
|
||||
if (!runtime_config.frame_rate().valid()) {
|
||||
@@ -32038,7 +32005,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
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 experiment.\n";
|
||||
"compatibility check.\n";
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
@@ -32052,10 +32019,8 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
std::cout << " [trace] full tracing enabled\n" << std::flush;
|
||||
}
|
||||
|
||||
// Disable shadow-stack (CET/SHSTK) for this process before loading
|
||||
// libroblox. Roblox's internal threads trigger SIGTRAP SI_KERNEL from
|
||||
// return-address mismatches on the shadow stack; disabling it here prevents
|
||||
// those spurious traps. This is a no-op on kernels that don't support CET.
|
||||
// Roblox internal threads trigger SI_KERNEL traps from CET shadow-stack
|
||||
// return mismatches. Unsupported kernels ignore this request.
|
||||
{
|
||||
long r = syscall(SYS_arch_prctl, ARCH_SHSTK_DISABLE, ARCH_SHSTK_SHSTK);
|
||||
if (r == 0) {
|
||||
@@ -32074,7 +32039,6 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
setenv("MOCKTAIL_APP_BRIDGE_HEADLESS_INIT_PARAMS", is_headless ? "1" : "0",
|
||||
1);
|
||||
}
|
||||
// Stage 1: Initialise the Bionic-compatible ELF linker + Pseudo-JVM.
|
||||
PrintStage(1, "Initialising Bionic linker + Pseudo-JVM");
|
||||
|
||||
#ifdef MOCKTAIL_USE_BIONIC_LINKER
|
||||
@@ -32089,8 +32053,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
}
|
||||
std::cout << " Pseudo-JVM instance created successfully.\n";
|
||||
|
||||
// Stage 2: Register mandatory Android SDK JNI classes.
|
||||
// Roblox's JNI_OnLoad will call FindClass for these at startup.
|
||||
// FindClass needs these descriptors before JNI_OnLoad.
|
||||
PrintStage(2, "Registering Android SDK JNI classes");
|
||||
|
||||
auto context_class = jni_vm->RegisterClass("android/content/Context");
|
||||
@@ -32104,7 +32067,6 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
std::cout << " [JNI callback] nativeInitClientSettings invoked\n";
|
||||
});
|
||||
|
||||
// Classes required by Stage 6 startup entry points.
|
||||
auto native_gl_interface_class =
|
||||
jni_vm->RegisterClass("com/roblox/engine/jni/NativeGLInterface");
|
||||
g_native_gl_class_for_main_thread =
|
||||
@@ -32178,10 +32140,8 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
std::cout << " Registered " << jni_vm->GetClassCount()
|
||||
<< " JNI class(es)\n";
|
||||
|
||||
// Window init (non-headless mode): create the SDL3 + EGL window before
|
||||
// Android/EGL stubs are preloaded, so SDL resolves the real host graphics
|
||||
// backend. By default the window stays hidden until Roblox presents a real
|
||||
// frame.
|
||||
// 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;
|
||||
@@ -32238,15 +32198,6 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 3: Populate the Bionic symbol table with all stub + system
|
||||
// symbols so that mcpelauncher-linker can resolve libroblox.so's
|
||||
// DT_NEEDED entries without calling the host Glibc dynamic linker.
|
||||
//
|
||||
// Strategy:
|
||||
// 1. dlopen each stub .so with RTLD_GLOBAL (host linker finds them
|
||||
// via RPATH=$ORIGIN).
|
||||
// 2. Iterate over known stub symbols and register them into the
|
||||
// Bionic symbol table via linker::RegisterSymbol().
|
||||
PrintStage(3, "Building Bionic symbol table from stubs");
|
||||
|
||||
libc_shim::Install();
|
||||
@@ -32287,7 +32238,6 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
}
|
||||
}
|
||||
|
||||
// Stub libraries to preload. All located via RPATH=$ORIGIN.
|
||||
std::vector<const char*> stub_names = {
|
||||
"libc.so", "libdl.so", "libm.so", "libz.so",
|
||||
"libandroid.so", "liblog.so", "libmediandk.so",
|
||||
@@ -32331,15 +32281,9 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
real_gles_handle = OpenRealGlesLibrary();
|
||||
}
|
||||
|
||||
// Register every known Android NDK / system symbol into the Bionic
|
||||
// symbol table so mcpelauncher-linker resolves them correctly.
|
||||
//
|
||||
// We prioritize resolving from our loaded stub libraries first.
|
||||
// This ensures Bionic shims (like custom pthread structure handlers
|
||||
// or __sF stdio compatibility) are preferred over host glibc ones,
|
||||
// preventing layout incompatibility and crashes.
|
||||
// Prefer Bionic shims over glibc for guest layouts such as pthread and FILE.
|
||||
static const char* kSymbolsToRegister[] = {
|
||||
// Exported stub functions and structures
|
||||
// Symbols exported by the Android compatibility stubs.
|
||||
"AAsset_close",
|
||||
"AAsset_getBuffer",
|
||||
"AAsset_getLength",
|
||||
@@ -32597,7 +32541,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
"__system_property_get",
|
||||
"__write_chk",
|
||||
|
||||
// Binary-compatible standard pthread symbols not explicitly shimmed
|
||||
// pthread functions whose host ABI is already compatible.
|
||||
"pthread_once",
|
||||
"pthread_self",
|
||||
"pthread_equal",
|
||||
@@ -32614,7 +32558,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
"pthread_sigmask",
|
||||
"pthread_setname_np",
|
||||
|
||||
// Standard C library functions needed by libroblox.so
|
||||
// Standard C library functions resolved from host libc.
|
||||
"strcmp",
|
||||
"strncmp",
|
||||
"strcpy",
|
||||
@@ -32926,13 +32870,8 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 4: Load libroblox.so via the Bionic-compatible linker.
|
||||
|
||||
// Auto-register ALL undefined symbols from the target library BEFORE
|
||||
// loading stub libraries into the Bionic linker. This ensures the stubs'
|
||||
// injected symbol maps include every symbol libroblox.so will need at
|
||||
// relocation time. Auto-registration must run first because load_library
|
||||
// takes a snapshot of g_bionic_symbols at call time.
|
||||
// Register imports before loading synthetic SONAMEs; load_library snapshots
|
||||
// g_bionic_symbols.
|
||||
{
|
||||
int fd = ::open(library_path.c_str(), O_RDONLY);
|
||||
if (fd >= 0) {
|
||||
@@ -32942,7 +32881,6 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
PROT_READ, MAP_PRIVATE, fd, 0);
|
||||
if (map != MAP_FAILED) {
|
||||
auto* ehdr = reinterpret_cast<Elf64_Ehdr*>(map);
|
||||
// Walk section headers to find .dynsym and .dynstr
|
||||
auto* shdr = reinterpret_cast<Elf64_Shdr*>(
|
||||
reinterpret_cast<char*>(map) + ehdr->e_shoff);
|
||||
Elf64_Shdr* dynsym_hdr = nullptr;
|
||||
@@ -32963,7 +32901,6 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
if (syms[si].st_shndx != SHN_UNDEF) continue;
|
||||
const char* name = strtab + syms[si].st_name;
|
||||
if (!name || name[0] == '\0') continue;
|
||||
// Skip if already registered
|
||||
if (linker::GetBionicSymbols().count(name)) continue;
|
||||
auto result = ResolveSymbolForBionic(name, has_window,
|
||||
real_gles_handle,
|
||||
@@ -32984,9 +32921,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
}
|
||||
}
|
||||
|
||||
// Register stub libraries inside Bionic linker. This must happen AFTER
|
||||
// auto-registration so that every symbol libroblox needs is already in
|
||||
// g_bionic_symbols when load_library snapshots the map.
|
||||
// Load synthetic libraries only after the import map is complete.
|
||||
linker::RegisterBionicPthreadKeyRuntimeForLibc();
|
||||
linker::RegisterBionicSysconfRuntimeForLibc();
|
||||
linker::RegisterBionicSignalRuntimeForLibc();
|
||||
@@ -33026,12 +32961,11 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
std::cout << " [linker] Bionic unwind metadata validated for libroblox\n"
|
||||
<< std::flush;
|
||||
|
||||
// The pre-constructor linker hook is the only installation phase. A second
|
||||
// arena initialization here would not be idempotent; post-load only verifies
|
||||
// that the experiment completed.
|
||||
// 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 experiment did not install before "
|
||||
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
|
||||
@@ -33040,7 +32974,6 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
// Stage 5: Invoke JNI_OnLoad — hand control to the game engine.
|
||||
PrintStage(5, "Invoking JNI_OnLoad in libroblox.so");
|
||||
|
||||
auto* jni_onload =
|
||||
@@ -33218,9 +33151,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
}
|
||||
}
|
||||
|
||||
// libroblox.so may have replaced env->functions during JNI_OnLoad with a
|
||||
// Bionic JNI table. Restore our pseudo-VM function table so that our Stage 6
|
||||
// helpers (NewStringUTF, GetStringUTFChars, etc.) work correctly.
|
||||
// Restore the pseudo-JNI table if JNI_OnLoad replaced it.
|
||||
{
|
||||
JNIEnv* main_env = jni_vm->GetJNIEnv();
|
||||
if (main_env) {
|
||||
@@ -33843,13 +33774,8 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
std::cout << " [compat] skipped fixed-offset Stage 6 patch set\n"
|
||||
<< std::flush;
|
||||
}
|
||||
// By default, all startup steps are enabled when MOCKTAIL_START_ENGINE=1.
|
||||
// Real (non-mock) calls to nativeSetAssetPath,
|
||||
// nativeAppBridgeV2InitWithParams and nativeAppBridgeV2StartAppWithParams
|
||||
// are now the default path — the mock/bypass path only activates when the
|
||||
// real call crashes and recovery fires via siglongjmp. The old
|
||||
// MOCKTAIL_UNSAFE_NATIVE_STARTUP guard is removed so users do not need to
|
||||
// set it explicitly.
|
||||
// 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 =
|
||||
@@ -33857,8 +33783,6 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
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;
|
||||
// call_real_set_asset_path: enabled by default unless explicitly disabled.
|
||||
// The signal handler will catch any crash and fall back to MocktailSetAssetPath.
|
||||
const bool call_real_set_asset_path =
|
||||
run_set_asset_path &&
|
||||
!IsDisabled("MOCKTAIL_CALL_REAL_NATIVE_SET_ASSET_PATH");
|
||||
@@ -33876,7 +33800,6 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
IsEnabled("MOCKTAIL_APP_BRIDGE_APP_START")) &&
|
||||
native_app_bridge_app_start != nullptr;
|
||||
const bool run_init_with_params = requested_init_with_params;
|
||||
// call_real_init_with_params: real call is the default; mock only on crash.
|
||||
const bool call_real_init_with_params =
|
||||
run_init_with_params &&
|
||||
!IsDisabled("MOCKTAIL_CALL_REAL_APP_BRIDGE_INIT");
|
||||
@@ -33922,8 +33845,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
(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;
|
||||
// call_real_start_app_with_params: opt-in because the real call can block
|
||||
// startup before any rendering path becomes visible.
|
||||
// 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");
|
||||
@@ -34052,7 +33974,9 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
environment, message_bus_symbols, platform_web_symbols.web_view,
|
||||
platform_web_symbols.browser_service, *experience_game_symbols,
|
||||
jni_factory, present_boundary, std::move(surface_config),
|
||||
&dependencies.roblox_credential());
|
||||
&dependencies.roblox_credential(),
|
||||
mocktail::runtime::RobloxExperienceSurfaceProvider{},
|
||||
discord_rpc.observer());
|
||||
const mocktail::Status platform_protocol_status =
|
||||
experience_composition->InitializePlatformProtocols();
|
||||
if (!platform_protocol_status.ok()) {
|
||||
@@ -34533,7 +34457,7 @@ int mocktail::legacy::Run(const runtime::CommandLineOptions& options,
|
||||
|
||||
if (!build_profile.default_allowed) {
|
||||
std::cerr
|
||||
<< "[FATAL] Compatibility experiment returned, but Build ID "
|
||||
<< "[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;
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// src/libc_shim/libc_shim.cc — Bionic libc → Glibc shim implementation.
|
||||
|
||||
#include "libc_shim/libc_shim.h"
|
||||
|
||||
#include <array>
|
||||
@@ -33,7 +31,6 @@ namespace libc_shim {
|
||||
|
||||
namespace {
|
||||
|
||||
// Registered prefix mappings: android_prefix → host_prefix.
|
||||
std::unordered_map<std::string, std::string> g_path_mappings;
|
||||
|
||||
bool g_installed = false;
|
||||
@@ -504,10 +501,8 @@ extern "C" ssize_t mocktail_readlink(const char* path, char* buf,
|
||||
|
||||
extern "C" ssize_t mocktail___readlink_chk(const char* path, char* buf,
|
||||
size_t len, size_t buf_len) {
|
||||
// Match Bionic's fortify.cpp contract: both an impossible ssize_t result
|
||||
// and a compiler-proven destination overrun are fatal programming errors,
|
||||
// not recoverable readlink failures. Returning EINVAL here used to let the
|
||||
// guest continue after a detected buffer overflow.
|
||||
// Bionic treats impossible ssize_t results and proven overruns as fatal;
|
||||
// returning EINVAL would continue after a detected buffer overflow.
|
||||
if (len > static_cast<size_t>(SSIZE_MAX)) {
|
||||
std::fprintf(stderr,
|
||||
"FORTIFY: readlink: size %zu > SSIZE_MAX\n", len);
|
||||
|
||||
+9
-25
@@ -6,11 +6,7 @@
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// src/linker/linker.cc — ELF loader implementation.
|
||||
//
|
||||
// Stage A (no mcpelauncher-linker): delegates to host dlopen/dlsym.
|
||||
// Stage B (MOCKTAIL_USE_BIONIC_LINKER=1): delegates to mcpelauncher-linker
|
||||
// which loads Bionic-ABI ELF files without Glibc version conflicts.
|
||||
// Without the Bionic linker, loading falls back to host dlopen/dlsym.
|
||||
|
||||
#include "linker/linker.h"
|
||||
|
||||
@@ -50,16 +46,13 @@ namespace linker {
|
||||
|
||||
namespace {
|
||||
|
||||
// Global alias → handle map maintained across all LoadLibrary calls.
|
||||
std::unordered_map<std::string, LibraryHandle> g_symbol_table;
|
||||
|
||||
// New registrations retain symbol ownership per Android SONAME. The legacy
|
||||
// global map remains isolated below until src/main.cc is migrated.
|
||||
// Synthetic exports retain ownership per Android SONAME.
|
||||
std::unordered_map<std::string, SymbolMap> g_synthetic_symbols;
|
||||
std::unordered_map<std::string, LibraryHandle> g_synthetic_libraries;
|
||||
std::unordered_set<LibraryHandle> g_synthetic_handles;
|
||||
|
||||
// Compatibility symbol map used only by RegisterSymbol/LoadLibrary.
|
||||
SymbolMap g_legacy_bionic_symbols;
|
||||
|
||||
// Keep string memory alive for Bionic linker sonames to avoid dangling pointers.
|
||||
@@ -121,11 +114,8 @@ int BionicDlcloseBoundary(void* handle) {
|
||||
char* BionicDlerrorBoundary() { return ::linker::dlerror(); }
|
||||
#endif
|
||||
|
||||
// The legacy startup path still discovers most imports in one untyped map.
|
||||
// Bionic libc APIs with host-incompatible signatures or integer values must
|
||||
// have exactly one owner, synthetic libc.so. Re-exporting those host functions
|
||||
// from an earlier DT_NEEDED SONAME lets the guest bind to glibc before it
|
||||
// reaches libc.so.
|
||||
// Host-incompatible Bionic APIs belong only to synthetic libc.so. Exporting
|
||||
// them from an earlier dependency could bind the guest to glibc instead.
|
||||
bool IsBionicLibcOnlySymbol(std::string_view symbol) {
|
||||
static constexpr std::string_view kStdioSymbols[] = {
|
||||
"__fread_chk", "__fwrite_chk", "__sF", "clearerr",
|
||||
@@ -189,8 +179,6 @@ void RemoveHandleFromRegistry(LibraryHandle handle) {
|
||||
|
||||
} // namespace
|
||||
|
||||
// Public API
|
||||
|
||||
void RegisterSymbol(const std::string& name, void* addr) {
|
||||
std::lock_guard<std::mutex> lock(g_registry_mutex);
|
||||
g_legacy_bionic_symbols[name] = addr;
|
||||
@@ -351,9 +339,8 @@ void RegisterBionicHostLibcRuntimeForLibc() {
|
||||
RegisterSyntheticSymbol(
|
||||
"libc.so", "gethostbyname",
|
||||
reinterpret_cast<void*>(mocktail_bionic_gethostbyname));
|
||||
// The preserved startup path publishes older DNS wrappers through its
|
||||
// legacy global map. Replace those addresses before libc.so snapshots that
|
||||
// map so every supported payload crosses the upload deny boundary.
|
||||
// Replace legacy DNS wrappers before libc.so snapshots the global map, so
|
||||
// every payload crosses the upload deny boundary.
|
||||
RegisterSymbol("getaddrinfo",
|
||||
reinterpret_cast<void*>(mocktail_bionic_getaddrinfo));
|
||||
RegisterSymbol("freeaddrinfo",
|
||||
@@ -375,9 +362,8 @@ void RegisterBionicHostLibcRuntimeForLibc() {
|
||||
reinterpret_cast<void*>(mocktail_bionic_mallinfo));
|
||||
RegisterSyntheticSymbol("libc.so", "strerror_r",
|
||||
reinterpret_cast<void*>(mocktail_bionic_strerror_r));
|
||||
// The preserved startup path scans libroblox imports through the legacy
|
||||
// global map before relocation. Publish the same address there so it
|
||||
// cannot auto-register glibc's GNU char*-returning strerror_r instead.
|
||||
// Publish this before relocation to prevent binding glibc's incompatible
|
||||
// GNU char*-returning strerror_r.
|
||||
RegisterSymbol("strerror_r",
|
||||
reinterpret_cast<void*>(mocktail_bionic_strerror_r));
|
||||
RegisterSyntheticSymbol("libc.so", "strtoll_l",
|
||||
@@ -643,9 +629,7 @@ LibraryHandle LoadLibrary(const std::string& real_path,
|
||||
return OpenAndroidLibrary(real_path, host_alias);
|
||||
}
|
||||
|
||||
// The legacy composition already reaches this per-SONAME boundary for
|
||||
// every Android dependency. Register host adapters here so libc.so owns the
|
||||
// guest ABI without adding calls to the preserved legacy startup TU.
|
||||
// Register adapters here so libc.so owns the guest ABI per SONAME.
|
||||
if (real_path == "libc.so") {
|
||||
RegisterBionicAtForkRuntimeForLibc();
|
||||
RegisterBionicHostLibcRuntimeForLibc();
|
||||
|
||||
+42
-49
@@ -6,10 +6,6 @@
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
// Mocktail's composition root stays intentionally small. During the
|
||||
// upstream-first migration it delegates to the isolated legacy runtime; new
|
||||
// platform, JNI, loader, and service modules are linked as explicit targets.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -292,6 +288,42 @@ int main(int argc, char* argv[]) {
|
||||
: "")
|
||||
<< '\n';
|
||||
}
|
||||
|
||||
// Register immediately after any cgroup re-exec and 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::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,
|
||||
@@ -418,10 +450,8 @@ int main(int argc, char* argv[]) {
|
||||
<< active_build_id.error << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
// An old local probation receipt can outlive the point where the
|
||||
// exact Build ID becomes shipped support. Built-in profiles are
|
||||
// immutable and must then use the repository manifest instead of
|
||||
// attempting an external override.
|
||||
// Built-in profiles supersede stale probation receipts and never
|
||||
// use external overrides.
|
||||
use_external_profile = mocktail::compat::FindHostAbiProfile(
|
||||
active_build_id.build_id) == nullptr;
|
||||
}
|
||||
@@ -721,45 +751,10 @@ int main(int argc, char* argv[]) {
|
||||
dependencies = mocktail::legacy::RuntimeDependencies(
|
||||
std::move(composition), &ShutdownPlatformBridges);
|
||||
}
|
||||
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::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 &&
|
||||
!isolated_canary) {
|
||||
// Start accepting browser requests only after cgroup re-exec, payload
|
||||
// preflight and host bridge setup have succeeded. An ACK therefore cannot
|
||||
// be invalidated by a known host-side
|
||||
// startup or exec boundary. Isolated canaries cannot receive browser
|
||||
// launches and own no public per-user endpoint.
|
||||
// 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(),
|
||||
@@ -804,10 +799,8 @@ int main(int argc, char* argv[]) {
|
||||
command_line.options.mode == mocktail::runtime::CommandMode::kRun) {
|
||||
failure_dialog.MarkSuccessful();
|
||||
support_bundle_guard.Disarm();
|
||||
// The embedded Android payload registers process-global atexit handlers
|
||||
// for workers that it does not expose for joining. The supported runtime
|
||||
// reaches this boundary only after explicit lifecycle, audio, input, and
|
||||
// SDL shutdown; do not re-enter guest teardown from the host C runtime.
|
||||
// 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);
|
||||
|
||||
@@ -264,8 +264,8 @@ std::string CommandLineUsage(const std::string& program_name) {
|
||||
"and\n"
|
||||
<< " uses its native sign-in flow. New credentials are stored "
|
||||
"privately.\n\n"
|
||||
<< "Debug and experimental toggles remain available as MOCKTAIL_* env "
|
||||
"vars.\n";
|
||||
<< "Additional runtime options are available as MOCKTAIL_* environment "
|
||||
"variables.\n";
|
||||
return usage.str();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,907 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
#include "runtime/discord_rpc.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "runtime/owned_pthread.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
namespace {
|
||||
|
||||
using Json = nlohmann::json;
|
||||
|
||||
constexpr std::size_t kMaximumDiscordFrameBytes = 1024U * 1024U;
|
||||
constexpr std::size_t kMaximumRobloxApiBytes = 512U * 1024U;
|
||||
constexpr std::size_t kDiscordWorkerStackBytes = 16U * 1024U * 1024U;
|
||||
constexpr std::chrono::milliseconds kReconnectDelay{5000};
|
||||
constexpr std::chrono::seconds kDiscordActivityRateWindow{20};
|
||||
constexpr std::size_t kDiscordActivityRateLimit = 5;
|
||||
constexpr std::chrono::seconds kPlaceMetadataRetryBase{10};
|
||||
constexpr std::chrono::seconds kPlaceMetadataRetryMaximum{300};
|
||||
constexpr char kJoinPage[] = "https://komaruworld.github.io/mocktail/join.html";
|
||||
|
||||
const char* PresencePhaseName(RobloxExperiencePresencePhase phase) {
|
||||
switch (phase) {
|
||||
case RobloxExperiencePresencePhase::kBrowsing:
|
||||
return "browsing";
|
||||
case RobloxExperiencePresencePhase::kJoining:
|
||||
return "joining";
|
||||
case RobloxExperiencePresencePhase::kPlaying:
|
||||
return "playing";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
enum class DiscordOpcode : uint32_t {
|
||||
kHandshake = 0,
|
||||
kFrame = 1,
|
||||
kClose = 2,
|
||||
kPing = 3,
|
||||
kPong = 4,
|
||||
};
|
||||
|
||||
struct DiscordFrame {
|
||||
DiscordOpcode opcode = DiscordOpcode::kClose;
|
||||
std::string payload;
|
||||
};
|
||||
|
||||
std::string TruncateUtf8(std::string value, std::size_t maximum) {
|
||||
if (value.size() <= maximum) {
|
||||
return value;
|
||||
}
|
||||
std::size_t boundary = maximum;
|
||||
while (boundary != 0 &&
|
||||
(static_cast<unsigned char>(value[boundary]) & 0xc0U) == 0x80U) {
|
||||
--boundary;
|
||||
}
|
||||
value.resize(boundary);
|
||||
return value;
|
||||
}
|
||||
|
||||
std::string DumpJson(const Json& value) {
|
||||
return value.dump(-1, ' ', false, Json::error_handler_t::replace);
|
||||
}
|
||||
|
||||
std::string RenderPlaceTemplate(std::string text, std::string_view place_name) {
|
||||
constexpr std::string_view placeholder = "{place_name}";
|
||||
std::size_t offset = 0;
|
||||
while ((offset = text.find(placeholder, offset)) != std::string::npos) {
|
||||
text.replace(offset, placeholder.size(), place_name);
|
||||
offset += place_name.size();
|
||||
}
|
||||
return TruncateUtf8(std::move(text), 128);
|
||||
}
|
||||
|
||||
std::string PercentEncode(std::string_view value) {
|
||||
constexpr char hex[] = "0123456789ABCDEF";
|
||||
std::string encoded;
|
||||
encoded.reserve(value.size() * 3);
|
||||
for (const unsigned char byte : value) {
|
||||
if ((byte >= 'a' && byte <= 'z') || (byte >= 'A' && byte <= 'Z') ||
|
||||
(byte >= '0' && byte <= '9') || byte == '-' || byte == '_' ||
|
||||
byte == '.' || byte == '~') {
|
||||
encoded.push_back(static_cast<char>(byte));
|
||||
} else {
|
||||
encoded.push_back('%');
|
||||
encoded.push_back(hex[byte >> 4U]);
|
||||
encoded.push_back(hex[byte & 0x0fU]);
|
||||
}
|
||||
}
|
||||
return encoded;
|
||||
}
|
||||
|
||||
bool HasDiscordJoinTarget(const RobloxExperienceLaunchRequest& request) {
|
||||
return request.place_id > 0 &&
|
||||
(request.game_instance_id.empty() ||
|
||||
(request.game_instance_id.size() <= 128 &&
|
||||
std::none_of(request.game_instance_id.begin(),
|
||||
request.game_instance_id.end(),
|
||||
[](unsigned char byte) {
|
||||
return byte < 0x20 || byte == 0x7f;
|
||||
})));
|
||||
}
|
||||
|
||||
bool WriteAll(int descriptor, const void* bytes, std::size_t size) {
|
||||
const auto* next = static_cast<const unsigned char*>(bytes);
|
||||
while (size != 0) {
|
||||
const ssize_t written =
|
||||
send(descriptor, next, size, MSG_NOSIGNAL | MSG_DONTWAIT);
|
||||
if (written > 0) {
|
||||
next += written;
|
||||
size -= static_cast<std::size_t>(written);
|
||||
continue;
|
||||
}
|
||||
if (written < 0 && errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
if (written < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
|
||||
pollfd event{descriptor, POLLOUT, 0};
|
||||
if (poll(&event, 1, 1500) > 0 && (event.revents & POLLOUT) != 0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReadAll(int descriptor, void* bytes, std::size_t size) {
|
||||
auto* next = static_cast<unsigned char*>(bytes);
|
||||
while (size != 0) {
|
||||
const ssize_t received = recv(descriptor, next, size, 0);
|
||||
if (received > 0) {
|
||||
next += received;
|
||||
size -= static_cast<std::size_t>(received);
|
||||
continue;
|
||||
}
|
||||
if (received < 0 && errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void EncodeLittleEndian(uint32_t value, unsigned char* destination) {
|
||||
destination[0] = static_cast<unsigned char>(value & 0xffU);
|
||||
destination[1] = static_cast<unsigned char>((value >> 8U) & 0xffU);
|
||||
destination[2] = static_cast<unsigned char>((value >> 16U) & 0xffU);
|
||||
destination[3] = static_cast<unsigned char>((value >> 24U) & 0xffU);
|
||||
}
|
||||
|
||||
uint32_t DecodeLittleEndian(const unsigned char* source) {
|
||||
return static_cast<uint32_t>(source[0]) |
|
||||
(static_cast<uint32_t>(source[1]) << 8U) |
|
||||
(static_cast<uint32_t>(source[2]) << 16U) |
|
||||
(static_cast<uint32_t>(source[3]) << 24U);
|
||||
}
|
||||
|
||||
bool WriteFrame(int descriptor, DiscordOpcode opcode,
|
||||
std::string_view payload) {
|
||||
if (payload.size() > kMaximumDiscordFrameBytes ||
|
||||
payload.size() > std::numeric_limits<uint32_t>::max()) {
|
||||
return false;
|
||||
}
|
||||
std::array<unsigned char, 8> header{};
|
||||
EncodeLittleEndian(static_cast<uint32_t>(opcode), header.data());
|
||||
EncodeLittleEndian(static_cast<uint32_t>(payload.size()), header.data() + 4);
|
||||
return WriteAll(descriptor, header.data(), header.size()) &&
|
||||
WriteAll(descriptor, payload.data(), payload.size());
|
||||
}
|
||||
|
||||
bool ReadFrame(int descriptor, DiscordFrame* frame) {
|
||||
std::array<unsigned char, 8> header{};
|
||||
if (frame == nullptr || !ReadAll(descriptor, header.data(), header.size())) {
|
||||
return false;
|
||||
}
|
||||
const uint32_t length = DecodeLittleEndian(header.data() + 4);
|
||||
if (length > kMaximumDiscordFrameBytes) {
|
||||
return false;
|
||||
}
|
||||
frame->opcode = static_cast<DiscordOpcode>(DecodeLittleEndian(header.data()));
|
||||
frame->payload.resize(length);
|
||||
return length == 0 ||
|
||||
ReadAll(descriptor, frame->payload.data(), frame->payload.size());
|
||||
}
|
||||
|
||||
void LogDiscordRpcError(const DiscordFrame& frame) {
|
||||
if (frame.opcode != DiscordOpcode::kFrame) {
|
||||
return;
|
||||
}
|
||||
const Json response = Json::parse(frame.payload, nullptr, false);
|
||||
if (!response.is_object()) {
|
||||
return;
|
||||
}
|
||||
const auto event = response.find("evt");
|
||||
if (event == response.end() || !event->is_string() ||
|
||||
event->get_ref<const std::string&>() != "ERROR") {
|
||||
return;
|
||||
}
|
||||
int64_t code = 0;
|
||||
std::string message = "Discord rejected an RPC request";
|
||||
const auto data = response.find("data");
|
||||
if (data != response.end() && data->is_object()) {
|
||||
const auto code_value = data->find("code");
|
||||
if (code_value != data->end() && code_value->is_number_integer()) {
|
||||
code = code_value->get<int64_t>();
|
||||
}
|
||||
const auto message_value = data->find("message");
|
||||
if (message_value != data->end() && message_value->is_string()) {
|
||||
message = TruncateUtf8(message_value->get<std::string>(), 256);
|
||||
for (char& byte : message) {
|
||||
if (static_cast<unsigned char>(byte) < 0x20U || byte == 0x7f) {
|
||||
byte = ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
std::fprintf(stderr, " [discord-rpc] request rejected code=%lld: %s\n",
|
||||
static_cast<long long>(code), message.c_str());
|
||||
}
|
||||
|
||||
std::vector<std::string> DiscordSocketDirectories() {
|
||||
std::vector<std::string> directories;
|
||||
const auto append = [&directories](const char* path) {
|
||||
if (path != nullptr && path[0] != '\0') {
|
||||
directories.emplace_back(path);
|
||||
}
|
||||
};
|
||||
const char* runtime = std::getenv("XDG_RUNTIME_DIR");
|
||||
append(runtime);
|
||||
if (runtime != nullptr && runtime[0] != '\0') {
|
||||
directories.emplace_back(std::string(runtime) +
|
||||
"/app/com.discordapp.Discord");
|
||||
directories.emplace_back(std::string(runtime) +
|
||||
"/.flatpak/com.discordapp.Discord/xdg-run");
|
||||
directories.emplace_back(std::string(runtime) + "/snap.discord");
|
||||
}
|
||||
append(std::getenv("TMPDIR"));
|
||||
directories.emplace_back("/tmp");
|
||||
directories.emplace_back("/run/user/" + std::to_string(getuid()));
|
||||
std::unordered_set<std::string> unique;
|
||||
std::vector<std::string> result;
|
||||
for (std::string& directory : directories) {
|
||||
if (unique.insert(directory).second) {
|
||||
result.push_back(std::move(directory));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int ConnectDiscord() {
|
||||
for (const std::string& directory : DiscordSocketDirectories()) {
|
||||
for (int index = 0; index < 10; ++index) {
|
||||
const std::string path =
|
||||
directory + "/discord-ipc-" + std::to_string(index);
|
||||
sockaddr_un address{};
|
||||
address.sun_family = AF_UNIX;
|
||||
if (path.size() >= sizeof(address.sun_path)) {
|
||||
continue;
|
||||
}
|
||||
std::memcpy(address.sun_path, path.c_str(), path.size() + 1);
|
||||
const int descriptor = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||||
if (descriptor < 0) {
|
||||
continue;
|
||||
}
|
||||
timeval timeout{2, 0};
|
||||
(void)setsockopt(descriptor, SOL_SOCKET, SO_RCVTIMEO, &timeout,
|
||||
sizeof(timeout));
|
||||
(void)setsockopt(descriptor, SOL_SOCKET, SO_SNDTIMEO, &timeout,
|
||||
sizeof(timeout));
|
||||
if (connect(descriptor, reinterpret_cast<const sockaddr*>(&address),
|
||||
sizeof(address)) == 0) {
|
||||
return descriptor;
|
||||
}
|
||||
(void)close(descriptor);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool HandshakeDiscord(int descriptor, std::string_view application_id) {
|
||||
Json handshake = {{"v", 1}, {"client_id", application_id}};
|
||||
if (!WriteFrame(descriptor, DiscordOpcode::kHandshake, DumpJson(handshake))) {
|
||||
return false;
|
||||
}
|
||||
for (int attempt = 0; attempt < 3; ++attempt) {
|
||||
DiscordFrame frame;
|
||||
if (!ReadFrame(descriptor, &frame)) {
|
||||
return false;
|
||||
}
|
||||
if (frame.opcode == DiscordOpcode::kPing) {
|
||||
if (!WriteFrame(descriptor, DiscordOpcode::kPong, frame.payload)) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (frame.opcode != DiscordOpcode::kFrame) {
|
||||
return false;
|
||||
}
|
||||
const Json response = Json::parse(frame.payload, nullptr, false);
|
||||
const auto event =
|
||||
response.is_object() ? response.find("evt") : response.end();
|
||||
return event != response.end() && event->is_string() &&
|
||||
event->get_ref<const std::string&>() == "READY";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Json ActivityJson(const DiscordRpcActivity& activity) {
|
||||
Json result = {
|
||||
{"type", 0}, {"details", activity.details}, {"instance", true}};
|
||||
if (!activity.state.empty()) {
|
||||
result["state"] = activity.state;
|
||||
}
|
||||
if (activity.start_timestamp.has_value()) {
|
||||
result["timestamps"] = {{"start", *activity.start_timestamp}};
|
||||
}
|
||||
if (!activity.large_image.empty()) {
|
||||
result["assets"] = {{"large_image", activity.large_image}};
|
||||
if (!activity.large_text.empty()) {
|
||||
result["assets"]["large_text"] = activity.large_text;
|
||||
}
|
||||
}
|
||||
if (!activity.button_label.empty() && !activity.button_url.empty()) {
|
||||
result["buttons"] = Json::array(
|
||||
{{{"label", activity.button_label}, {"url", activity.button_url}}});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool SetDiscordActivity(int descriptor, const DiscordRpcActivity* activity,
|
||||
uint64_t nonce) {
|
||||
Json args = {{"pid", getpid()}};
|
||||
args["activity"] = activity != nullptr ? ActivityJson(*activity) : Json();
|
||||
const Json request = {{"cmd", "SET_ACTIVITY"},
|
||||
{"args", std::move(args)},
|
||||
{"nonce", std::to_string(nonce)}};
|
||||
return WriteFrame(descriptor, DiscordOpcode::kFrame, DumpJson(request));
|
||||
}
|
||||
|
||||
struct BoundedResponse {
|
||||
std::string body;
|
||||
bool exceeded = false;
|
||||
};
|
||||
|
||||
std::once_flag g_curl_init_once;
|
||||
CURLcode g_curl_init_status = CURLE_FAILED_INIT;
|
||||
|
||||
void InitializeCurl() {
|
||||
g_curl_init_status = curl_global_init(CURL_GLOBAL_DEFAULT);
|
||||
}
|
||||
|
||||
size_t WriteHttpResponse(char* data, size_t size, size_t count,
|
||||
void* user_data) {
|
||||
auto* response = static_cast<BoundedResponse*>(user_data);
|
||||
if (response == nullptr || data == nullptr ||
|
||||
(count != 0 && size > std::numeric_limits<size_t>::max() / count)) {
|
||||
return 0;
|
||||
}
|
||||
const std::size_t bytes = size * count;
|
||||
if (bytes > kMaximumRobloxApiBytes ||
|
||||
response->body.size() > kMaximumRobloxApiBytes - bytes) {
|
||||
response->exceeded = true;
|
||||
return 0;
|
||||
}
|
||||
response->body.append(data, bytes);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
std::optional<Json> GetRobloxJson(std::string url) {
|
||||
std::call_once(g_curl_init_once, InitializeCurl);
|
||||
if (g_curl_init_status != CURLE_OK) {
|
||||
return std::nullopt;
|
||||
}
|
||||
CURL* curl = curl_easy_init();
|
||||
if (curl == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
BoundedResponse response;
|
||||
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, "https");
|
||||
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS_STR, "https");
|
||||
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 0L);
|
||||
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
|
||||
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 2000L);
|
||||
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 3500L);
|
||||
curl_easy_setopt(curl, CURLOPT_USERAGENT, "Mocktail/0.1");
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteHttpResponse);
|
||||
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
|
||||
std::string proxy_url;
|
||||
const char* proxy_host = std::getenv("MOCKTAIL_HTTP_PROXY_HOST");
|
||||
const char* proxy_port = std::getenv("MOCKTAIL_HTTP_PROXY_PORT");
|
||||
if (proxy_host != nullptr && proxy_port != nullptr) {
|
||||
const std::optional<NetworkProxyConfig> proxy =
|
||||
ParseNetworkProxyConfig(proxy_host, proxy_port);
|
||||
if (proxy.has_value()) {
|
||||
proxy_url = BuildNetworkProxyUrl(*proxy);
|
||||
curl_easy_setopt(curl, CURLOPT_PROXY, proxy_url.c_str());
|
||||
curl_easy_setopt(curl, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
|
||||
}
|
||||
}
|
||||
const CURLcode status = curl_easy_perform(curl);
|
||||
long http_status = 0;
|
||||
if (status == CURLE_OK) {
|
||||
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_status);
|
||||
}
|
||||
curl_easy_cleanup(curl);
|
||||
if (status != CURLE_OK || http_status != 200 || response.exceeded) {
|
||||
return std::nullopt;
|
||||
}
|
||||
Json parsed = Json::parse(response.body, nullptr, false);
|
||||
if (parsed.is_discarded()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
struct ResolvedPlaceMetadata {
|
||||
std::string name;
|
||||
std::string icon_url;
|
||||
};
|
||||
|
||||
bool IsSafeExternalImageUrl(std::string_view url) {
|
||||
return url.size() <= 512 && url.substr(0, 8) == "https://" &&
|
||||
std::none_of(url.begin(), url.end(), [](unsigned char byte) {
|
||||
return byte < 0x20U || byte == 0x7fU;
|
||||
});
|
||||
}
|
||||
|
||||
ResolvedPlaceMetadata ResolvePlaceMetadata(int64_t place_id) {
|
||||
ResolvedPlaceMetadata metadata;
|
||||
if (place_id <= 0) {
|
||||
return metadata;
|
||||
}
|
||||
const std::optional<Json> universe =
|
||||
GetRobloxJson("https://apis.roblox.com/universes/v1/places/" +
|
||||
std::to_string(place_id) + "/universe");
|
||||
if (!universe.has_value() || !universe->contains("universeId")) {
|
||||
return metadata;
|
||||
}
|
||||
const Json& identifier = (*universe)["universeId"];
|
||||
int64_t universe_id = 0;
|
||||
if (identifier.is_number_unsigned()) {
|
||||
const uint64_t unsigned_id = identifier.get<uint64_t>();
|
||||
if (unsigned_id >
|
||||
static_cast<uint64_t>(std::numeric_limits<int64_t>::max())) {
|
||||
return metadata;
|
||||
}
|
||||
universe_id = static_cast<int64_t>(unsigned_id);
|
||||
} else if (identifier.is_number_integer()) {
|
||||
universe_id = identifier.get<int64_t>();
|
||||
} else {
|
||||
return metadata;
|
||||
}
|
||||
if (universe_id <= 0) {
|
||||
return metadata;
|
||||
}
|
||||
const std::optional<Json> game =
|
||||
GetRobloxJson("https://games.roblox.com/v1/games?universeIds=" +
|
||||
std::to_string(universe_id));
|
||||
if (game.has_value() && game->contains("data") &&
|
||||
(*game)["data"].is_array() && !(*game)["data"].empty()) {
|
||||
const Json& first = (*game)["data"][0];
|
||||
if (first.contains("name") && first["name"].is_string()) {
|
||||
metadata.name =
|
||||
TruncateUtf8(first["name"].get<std::string>(), 128);
|
||||
}
|
||||
}
|
||||
|
||||
const std::optional<Json> thumbnail = GetRobloxJson(
|
||||
"https://thumbnails.roblox.com/v1/games/icons?universeIds=" +
|
||||
std::to_string(universe_id) +
|
||||
"&returnPolicy=PlaceHolder&size=512x512&format=Png&isCircular=false");
|
||||
if (thumbnail.has_value() && thumbnail->contains("data") &&
|
||||
(*thumbnail)["data"].is_array() && !(*thumbnail)["data"].empty()) {
|
||||
const Json& first = (*thumbnail)["data"][0];
|
||||
if (first.contains("imageUrl") && first["imageUrl"].is_string()) {
|
||||
std::string icon_url = first["imageUrl"].get<std::string>();
|
||||
if (IsSafeExternalImageUrl(icon_url)) {
|
||||
metadata.icon_url = std::move(icon_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
|
||||
std::chrono::seconds PlaceMetadataRetryDelay(unsigned failures) {
|
||||
const unsigned shift = std::min(failures, 5U);
|
||||
return std::min(kPlaceMetadataRetryBase * (1U << shift),
|
||||
kPlaceMetadataRetryMaximum);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsPublicDiscordJoin(const RobloxExperienceLaunchRequest& request) {
|
||||
return HasDiscordJoinTarget(request) &&
|
||||
request.reserved_server_access_code.empty() &&
|
||||
request.access_code.empty() && request.link_code.empty();
|
||||
}
|
||||
|
||||
std::string BuildDiscordJoinUrl(const RobloxExperienceLaunchRequest& request) {
|
||||
if (!HasDiscordJoinTarget(request)) {
|
||||
return {};
|
||||
}
|
||||
std::string url =
|
||||
std::string(kJoinPage) + "#placeId=" + std::to_string(request.place_id);
|
||||
if (!request.game_instance_id.empty()) {
|
||||
url += "&gameInstanceId=" + PercentEncode(request.game_instance_id);
|
||||
}
|
||||
return url.size() <= 512 ? std::move(url) : std::string();
|
||||
}
|
||||
|
||||
DiscordRpcActivity BuildDiscordRpcActivity(
|
||||
const DiscordRpcConfig& config, RobloxExperiencePresencePhase phase,
|
||||
const RobloxExperienceLaunchRequest* request, std::string place_name,
|
||||
int64_t session_started_at, std::string place_icon_url) {
|
||||
DiscordRpcActivity activity;
|
||||
activity.state = TruncateUtf8(config.text.state, 128);
|
||||
switch (phase) {
|
||||
case RobloxExperiencePresencePhase::kBrowsing:
|
||||
activity.details = TruncateUtf8(config.text.browsing, 128);
|
||||
break;
|
||||
case RobloxExperiencePresencePhase::kJoining:
|
||||
activity.details = TruncateUtf8(config.text.joining, 128);
|
||||
break;
|
||||
case RobloxExperiencePresencePhase::kPlaying: {
|
||||
if (place_name.empty()) {
|
||||
place_name = config.text.unknown_place;
|
||||
}
|
||||
if (config.show_place_name) {
|
||||
activity.details = RenderPlaceTemplate(config.text.playing, place_name);
|
||||
} else {
|
||||
activity.details = activity.state;
|
||||
activity.state.clear();
|
||||
}
|
||||
if (config.show_elapsed_time && session_started_at > 0) {
|
||||
activity.start_timestamp = session_started_at;
|
||||
}
|
||||
if (IsSafeExternalImageUrl(place_icon_url)) {
|
||||
activity.large_image = std::move(place_icon_url);
|
||||
activity.large_text = TruncateUtf8(place_name, 128);
|
||||
}
|
||||
if (config.join_enabled && request != nullptr &&
|
||||
(!config.public_servers_only || IsPublicDiscordJoin(*request))) {
|
||||
activity.button_url = BuildDiscordJoinUrl(*request);
|
||||
if (!activity.button_url.empty()) {
|
||||
activity.button_label = config.join_button_label;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return activity;
|
||||
}
|
||||
|
||||
class DiscordRpcSession::Impl final {
|
||||
public:
|
||||
explicit Impl(DiscordRpcConfig config) : config_(std::move(config)) {}
|
||||
|
||||
~Impl() { Stop(); }
|
||||
|
||||
bool Start(std::string* detail) {
|
||||
if (!config_.enabled) {
|
||||
if (detail != nullptr) {
|
||||
*detail = "disabled by runtime policy";
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (config_.application_id.empty()) {
|
||||
if (detail != nullptr) {
|
||||
*detail = "no Discord application ID is bundled";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
std::call_once(g_curl_init_once, InitializeCurl);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (started_) {
|
||||
return true;
|
||||
}
|
||||
phase_ = RobloxExperiencePresencePhase::kBrowsing;
|
||||
request_ = {};
|
||||
revision_ = 1;
|
||||
stopping_ = false;
|
||||
}
|
||||
const int error =
|
||||
worker_.Start(&Impl::RunWorker, this, kDiscordWorkerStackBytes);
|
||||
if (error != 0) {
|
||||
if (detail != nullptr) {
|
||||
*detail =
|
||||
"could not start Discord RPC worker: " + std::to_string(error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
started_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Stop() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!started_ && !worker_.joinable()) {
|
||||
return;
|
||||
}
|
||||
stopping_ = true;
|
||||
condition_.notify_all();
|
||||
}
|
||||
if (worker_.joinable()) {
|
||||
(void)worker_.WaitFor(-1, 1);
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
started_ = false;
|
||||
}
|
||||
|
||||
void Update(RobloxExperiencePresencePhase phase,
|
||||
const RobloxExperienceLaunchRequest* request) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!started_ || stopping_) {
|
||||
return;
|
||||
}
|
||||
phase_ = phase;
|
||||
request_ = {};
|
||||
if (request != nullptr) {
|
||||
// Never pass access codes or canonical launch JSON to the Discord worker.
|
||||
request_.place_id = request->place_id;
|
||||
request_.game_instance_id = request->game_instance_id;
|
||||
if (!request->reserved_server_access_code.empty()) {
|
||||
request_.reserved_server_access_code = "present";
|
||||
}
|
||||
if (!request->access_code.empty()) {
|
||||
request_.access_code = "present";
|
||||
}
|
||||
if (!request->link_code.empty()) {
|
||||
request_.link_code = "present";
|
||||
}
|
||||
}
|
||||
if (phase == RobloxExperiencePresencePhase::kPlaying) {
|
||||
session_started_at_ =
|
||||
std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
} else {
|
||||
session_started_at_ = 0;
|
||||
}
|
||||
++revision_;
|
||||
std::fprintf(stderr,
|
||||
" [discord-rpc] queued phase=%s revision=%llu "
|
||||
"place_id=%lld\n",
|
||||
PresencePhaseName(phase_),
|
||||
static_cast<unsigned long long>(revision_),
|
||||
static_cast<long long>(request_.place_id));
|
||||
condition_.notify_all();
|
||||
}
|
||||
|
||||
private:
|
||||
struct Desired {
|
||||
RobloxExperiencePresencePhase phase =
|
||||
RobloxExperiencePresencePhase::kBrowsing;
|
||||
RobloxExperienceLaunchRequest request;
|
||||
int64_t started_at = 0;
|
||||
uint64_t revision = 0;
|
||||
};
|
||||
|
||||
struct CachedPlaceMetadata {
|
||||
std::string name;
|
||||
std::string icon_url;
|
||||
std::chrono::steady_clock::time_point next_attempt{};
|
||||
unsigned failures = 0;
|
||||
};
|
||||
|
||||
static void* RunWorker(void* context) {
|
||||
static_cast<Impl*>(context)->Run();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool IsStopping() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return stopping_;
|
||||
}
|
||||
|
||||
Desired Snapshot() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return {phase_, request_, session_started_at_, revision_};
|
||||
}
|
||||
|
||||
bool WaitForChange(uint64_t revision, std::chrono::milliseconds duration) {
|
||||
std::unique_lock<std::mutex> lock(mutex_);
|
||||
condition_.wait_for(lock, duration, [this, revision]() {
|
||||
return stopping_ || revision_ != revision;
|
||||
});
|
||||
return !stopping_;
|
||||
}
|
||||
|
||||
void Run() {
|
||||
int descriptor = -1;
|
||||
uint64_t published_revision = 0;
|
||||
uint64_t nonce = 1;
|
||||
std::deque<std::chrono::steady_clock::time_point> activity_updates;
|
||||
while (!IsStopping()) {
|
||||
Desired desired = Snapshot();
|
||||
std::string place_name;
|
||||
std::string place_icon_url;
|
||||
bool resolve_metadata_after_publish = false;
|
||||
if (desired.phase == RobloxExperiencePresencePhase::kPlaying &&
|
||||
desired.request.place_id > 0) {
|
||||
CachedPlaceMetadata& cached =
|
||||
place_metadata_[desired.request.place_id];
|
||||
place_name = cached.name;
|
||||
place_icon_url = cached.icon_url;
|
||||
if ((cached.name.empty() || cached.icon_url.empty()) &&
|
||||
std::chrono::steady_clock::now() >= cached.next_attempt) {
|
||||
// Publish fallback activity while metadata is in flight.
|
||||
resolve_metadata_after_publish = true;
|
||||
}
|
||||
}
|
||||
if (descriptor < 0) {
|
||||
descriptor = ConnectDiscord();
|
||||
if (descriptor < 0 ||
|
||||
!HandshakeDiscord(descriptor, config_.application_id)) {
|
||||
if (descriptor >= 0) {
|
||||
(void)close(descriptor);
|
||||
descriptor = -1;
|
||||
}
|
||||
if (!WaitForChange(desired.revision, kReconnectDelay)) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
std::fprintf(stderr, " [discord-rpc] connected to Discord Desktop\n");
|
||||
published_revision = 0;
|
||||
}
|
||||
if (published_revision != desired.revision) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
while (!activity_updates.empty() &&
|
||||
now - activity_updates.front() >= kDiscordActivityRateWindow) {
|
||||
activity_updates.pop_front();
|
||||
}
|
||||
if (activity_updates.size() >= kDiscordActivityRateLimit) {
|
||||
const auto delay =
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
activity_updates.front() + kDiscordActivityRateWindow - now);
|
||||
(void)WaitForChange(desired.revision, delay);
|
||||
continue;
|
||||
}
|
||||
const DiscordRpcActivity activity = BuildDiscordRpcActivity(
|
||||
config_, desired.phase,
|
||||
desired.phase == RobloxExperiencePresencePhase::kBrowsing
|
||||
? nullptr
|
||||
: &desired.request,
|
||||
std::move(place_name), desired.started_at,
|
||||
std::move(place_icon_url));
|
||||
if (!SetDiscordActivity(descriptor, &activity, nonce++)) {
|
||||
(void)close(descriptor);
|
||||
descriptor = -1;
|
||||
continue;
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
" [discord-rpc] sent phase=%s revision=%llu\n",
|
||||
PresencePhaseName(desired.phase),
|
||||
static_cast<unsigned long long>(desired.revision));
|
||||
activity_updates.push_back(std::chrono::steady_clock::now());
|
||||
published_revision = desired.revision;
|
||||
}
|
||||
|
||||
if (resolve_metadata_after_publish) {
|
||||
const ResolvedPlaceMetadata resolved =
|
||||
ResolvePlaceMetadata(desired.request.place_id);
|
||||
CachedPlaceMetadata& cached =
|
||||
place_metadata_[desired.request.place_id];
|
||||
bool changed = false;
|
||||
if (!resolved.name.empty() && resolved.name != cached.name) {
|
||||
cached.name = resolved.name;
|
||||
changed = true;
|
||||
}
|
||||
if (!resolved.icon_url.empty() &&
|
||||
resolved.icon_url != cached.icon_url) {
|
||||
cached.icon_url = resolved.icon_url;
|
||||
changed = true;
|
||||
}
|
||||
if (cached.name.empty() || cached.icon_url.empty()) {
|
||||
const auto delay = PlaceMetadataRetryDelay(cached.failures++);
|
||||
cached.next_attempt = std::chrono::steady_clock::now() + delay;
|
||||
std::fprintf(stderr,
|
||||
" [discord-rpc] place metadata incomplete; retry in "
|
||||
"%llds place_id=%lld\n",
|
||||
static_cast<long long>(delay.count()),
|
||||
static_cast<long long>(desired.request.place_id));
|
||||
} else {
|
||||
cached.failures = 0;
|
||||
cached.next_attempt = {};
|
||||
}
|
||||
if (Snapshot().revision != desired.revision) {
|
||||
continue;
|
||||
}
|
||||
if (changed) {
|
||||
// Publish cached metadata through the normal rate limiter.
|
||||
published_revision = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
pollfd event{descriptor, POLLIN, 0};
|
||||
const int ready = poll(&event, 1, 500);
|
||||
if (ready < 0 && errno == EINTR) {
|
||||
continue;
|
||||
}
|
||||
if (ready < 0 || (ready > 0 && (event.revents &
|
||||
(POLLERR | POLLHUP | POLLNVAL)) != 0)) {
|
||||
(void)close(descriptor);
|
||||
descriptor = -1;
|
||||
continue;
|
||||
}
|
||||
if (ready > 0 && (event.revents & POLLIN) != 0) {
|
||||
DiscordFrame frame;
|
||||
if (!ReadFrame(descriptor, &frame)) {
|
||||
(void)close(descriptor);
|
||||
descriptor = -1;
|
||||
} else if (frame.opcode == DiscordOpcode::kPing) {
|
||||
if (!WriteFrame(descriptor, DiscordOpcode::kPong, frame.payload)) {
|
||||
(void)close(descriptor);
|
||||
descriptor = -1;
|
||||
}
|
||||
} else if (frame.opcode == DiscordOpcode::kClose) {
|
||||
(void)close(descriptor);
|
||||
descriptor = -1;
|
||||
} else {
|
||||
LogDiscordRpcError(frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (descriptor >= 0) {
|
||||
(void)SetDiscordActivity(descriptor, nullptr, nonce);
|
||||
(void)close(descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
const DiscordRpcConfig config_;
|
||||
std::mutex mutex_;
|
||||
std::condition_variable condition_;
|
||||
OwnedPthread worker_;
|
||||
RobloxExperiencePresencePhase phase_ =
|
||||
RobloxExperiencePresencePhase::kBrowsing;
|
||||
RobloxExperienceLaunchRequest request_;
|
||||
std::unordered_map<int64_t, CachedPlaceMetadata> place_metadata_;
|
||||
int64_t session_started_at_ = 0;
|
||||
uint64_t revision_ = 0;
|
||||
bool started_ = false;
|
||||
bool stopping_ = false;
|
||||
};
|
||||
|
||||
DiscordRpcSession::DiscordRpcSession(DiscordRpcConfig config)
|
||||
: impl_(std::make_unique<Impl>(std::move(config))) {}
|
||||
|
||||
DiscordRpcSession::~DiscordRpcSession() { Stop(); }
|
||||
|
||||
bool DiscordRpcSession::Start(std::string* detail) {
|
||||
return impl_->Start(detail);
|
||||
}
|
||||
|
||||
void DiscordRpcSession::Stop() { impl_->Stop(); }
|
||||
|
||||
RobloxExperiencePresenceObserver DiscordRpcSession::observer() {
|
||||
return {this, &DiscordRpcSession::Notify};
|
||||
}
|
||||
|
||||
void DiscordRpcSession::Notify(void* context,
|
||||
RobloxExperiencePresencePhase phase,
|
||||
const RobloxExperienceLaunchRequest* request) {
|
||||
if (context != nullptr) {
|
||||
static_cast<DiscordRpcSession*>(context)->impl_->Update(phase, request);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
@@ -15,6 +15,9 @@
|
||||
#include "runtime/game_mode.h"
|
||||
|
||||
#include <dlfcn.h>
|
||||
#if defined(__GLIBC__)
|
||||
#include <link.h>
|
||||
#endif
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
@@ -54,6 +57,19 @@ std::string ClientError(const GameModeClientApi& api,
|
||||
return std::string(fallback);
|
||||
}
|
||||
|
||||
void* OpenClientLibrary(const char* name) {
|
||||
#if defined(__GLIBC__)
|
||||
// Mocktail intentionally exports Bionic compatibility symbols for the
|
||||
// Android payload. A normal dlopen() lets host libraries such as libdbus
|
||||
// bind to those ABI-compatible-looking exports instead of glibc, which can
|
||||
// corrupt the GameMode portal exchange. A new link-map namespace keeps the
|
||||
// host client and all of its dependencies on the host ABI.
|
||||
return dlmopen(LM_ID_NEWLM, name, RTLD_NOW | RTLD_LOCAL);
|
||||
#else
|
||||
return dlopen(name, RTLD_NOW | RTLD_LOCAL);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ParseGameModePolicy(std::string_view value, GameModePolicy* policy) {
|
||||
@@ -124,9 +140,9 @@ GameModeSession GameModeSession::Start(GameModePolicy policy) {
|
||||
return StartBound(policy, {}, nullptr);
|
||||
}
|
||||
|
||||
void* library = dlopen("libgamemode.so.0", RTLD_NOW | RTLD_LOCAL);
|
||||
void* library = OpenClientLibrary("libgamemode.so.0");
|
||||
if (library == nullptr) {
|
||||
library = dlopen("libgamemode.so", RTLD_NOW | RTLD_LOCAL);
|
||||
library = OpenClientLibrary("libgamemode.so");
|
||||
}
|
||||
if (library == nullptr) {
|
||||
GameModeSession session;
|
||||
|
||||
@@ -33,10 +33,7 @@ namespace runtime {
|
||||
namespace {
|
||||
|
||||
constexpr size_t kMaxPendingLaunchRequests = 8;
|
||||
// Loading libroblox contributes a large static-TLS image. A one-megabyte
|
||||
// pthread stack is accepted in unit tests before the guest is loaded, but
|
||||
// pthread_create returns EINVAL in production once that TLS image is present.
|
||||
// Keep this above the runtime's proven 16 MiB guest-thread floor.
|
||||
// libroblox's static TLS requires the proven 16 MiB guest-thread stack floor.
|
||||
constexpr size_t kLaunchWorkerStackSize = 64ULL * 1024 * 1024;
|
||||
constexpr std::chrono::milliseconds kWebSurfaceReadyTimeout{3000};
|
||||
constexpr char kRobloxBaseUrl[] = "https://www.roblox.com/";
|
||||
@@ -56,8 +53,7 @@ Status FailedPrecondition(std::string message) {
|
||||
GameSurface SnapshotProductionSurface(void*) {
|
||||
const window::WindowSurfaceSnapshot snapshot =
|
||||
window::GetWindowSurfaceSnapshot();
|
||||
return {snapshot.generation,
|
||||
snapshot.available ? snapshot.native_window : 0,
|
||||
return {snapshot.generation, snapshot.available ? snapshot.native_window : 0,
|
||||
snapshot.available ? snapshot.width : 0,
|
||||
snapshot.available ? snapshot.height : 0};
|
||||
}
|
||||
@@ -278,7 +274,8 @@ RobloxExperienceComposition::RobloxExperienceComposition(
|
||||
RobloxFreshLaunchPresentBoundary present_boundary,
|
||||
RobloxGameSurfaceJniConfig surface_config,
|
||||
const SecureRobloxCredential* initial_web_view_credential,
|
||||
RobloxExperienceSurfaceProvider surface_provider)
|
||||
RobloxExperienceSurfaceProvider surface_provider,
|
||||
RobloxExperiencePresenceObserver presence_observer)
|
||||
: environment_(environment),
|
||||
message_bus_symbols_(message_bus_symbols),
|
||||
web_view_symbols_(web_view_symbols),
|
||||
@@ -293,6 +290,7 @@ RobloxExperienceComposition::RobloxExperienceComposition(
|
||||
? surface_provider
|
||||
: RobloxExperienceSurfaceProvider{nullptr,
|
||||
&SnapshotProductionSurface}),
|
||||
presence_observer_(presence_observer),
|
||||
web_surface_exit_target_(std::make_shared<WebSurfaceExitTarget>()),
|
||||
lifecycle_target_(std::make_shared<LifecycleTarget>()) {
|
||||
web_surface_exit_target_->composition = this;
|
||||
@@ -426,7 +424,9 @@ Status RobloxExperienceComposition::OnLuaAppReady(
|
||||
}
|
||||
|
||||
auto controller = std::make_shared<RobloxFreshGameLaunchController>(
|
||||
environment_, game_symbols_, present_boundary_, surface_config_);
|
||||
environment_, game_symbols_, present_boundary_, surface_config_,
|
||||
RobloxGamePresentedObserver{this,
|
||||
&RobloxExperienceComposition::GamePresented});
|
||||
RobloxExperienceMessageBusObjects bridge_objects;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
@@ -509,9 +509,7 @@ Status RobloxExperienceComposition::OpenWebSurface(
|
||||
}
|
||||
}
|
||||
|
||||
// Opening and configuring the singleton is one serialized transaction.
|
||||
// Optional fields are normalized before this point, so presentation state
|
||||
// from the previous route can never leak into the new page.
|
||||
// Serialize setup so presentation state cannot leak between routes.
|
||||
if (!current->SetTitle(presentation.title) ||
|
||||
!current->SetVisible(presentation.visible) ||
|
||||
!current->SetBackNavigationDisabled(
|
||||
@@ -623,9 +621,8 @@ Status RobloxExperienceComposition::DispatchWebViewOpen(
|
||||
presentation.visible = request.is_visible.value_or(true);
|
||||
presentation.show_domain_as_title =
|
||||
request.show_domain_as_title.value_or(false);
|
||||
// The host helper has no Android toolbar. Mapping a hidden Roblox back
|
||||
// button to disabled host back navigation preserves the protocol's only
|
||||
// actionable navigation policy without inventing a replacement header.
|
||||
// The host helper has no toolbar, so a hidden Roblox back button disables
|
||||
// host back navigation too.
|
||||
presentation.back_navigation_disabled =
|
||||
request.back_button_visible.has_value() && !*request.back_button_visible;
|
||||
return composition->OpenWebSurface(request.url, "webview",
|
||||
@@ -799,11 +796,8 @@ Status RobloxExperienceComposition::RouteWebSurfaceEvent(
|
||||
RobloxWebViewBridge* web_view_bridge) {
|
||||
switch (event.type) {
|
||||
case WebViewHelperEventType::kExecuteRoblox:
|
||||
// EnableAndroidWebViewService4 installs WebViewProtocol's listener on
|
||||
// the shared gi.a fragment regardless of which protocol opened it. The
|
||||
// MemStorage callback in gi.a is only the APK's disabled legacy
|
||||
// fallback. Keeping the raw command intact is required for
|
||||
// RequestGameJob fields such as instanceId and joinAttemptId.
|
||||
// WebViewProtocol listens on the shared gi.a fragment. Preserve the raw
|
||||
// command, including RequestGameJob instance and attempt IDs.
|
||||
if (route == WebSurfaceRoute::kNone) {
|
||||
return FailedPrecondition(
|
||||
"WebView helper event has no active logical route");
|
||||
@@ -839,9 +833,7 @@ Status RobloxExperienceComposition::RouteCurrentWebSurfaceEvent(
|
||||
logical_generation == 0 ||
|
||||
logical_generation != web_surface_logical_generation_ ||
|
||||
web_surface_route_ == WebSurfaceRoute::kNone) {
|
||||
// A close, helper replacement, or logical page reuse invalidates every
|
||||
// event already buffered for the old surface. This is normal teardown,
|
||||
// not a platform delivery failure.
|
||||
// Closing or replacing the helper invalidates buffered surface events.
|
||||
return Status::Ok();
|
||||
}
|
||||
route = web_surface_route_;
|
||||
@@ -931,11 +923,10 @@ Status RobloxExperienceComposition::PromoteAuthenticatedSession() {
|
||||
return Status::Ok();
|
||||
}
|
||||
SecurelyClearString(&credential);
|
||||
const GameSurface surface = surface_provider_.snapshot(
|
||||
surface_provider_.context);
|
||||
const GameSurface surface =
|
||||
surface_provider_.snapshot(surface_provider_.context);
|
||||
RobloxLuaAppExperienceReadiness readiness;
|
||||
readiness.principal.kind =
|
||||
GameSessionPrincipalKind::kAuthenticated;
|
||||
readiness.principal.kind = GameSessionPrincipalKind::kAuthenticated;
|
||||
readiness.principal.generation = 1;
|
||||
readiness.principal.principal_id = std::to_string(identity.user_id);
|
||||
readiness.principal.base_url = kRobloxBaseUrl;
|
||||
@@ -952,8 +943,7 @@ Status RobloxExperienceComposition::PromoteAuthenticatedSession() {
|
||||
if (consumes_window_surface_events_) {
|
||||
window::WindowSurfaceEvent stale_event;
|
||||
while (window::PollWindowSurfaceEvent(&stale_event)) {
|
||||
// Readiness already captured the latest snapshot. Older events were
|
||||
// queued before a GAME consumer existed and must not replay stale state.
|
||||
// Readiness already captured the latest snapshot; discard older events.
|
||||
}
|
||||
}
|
||||
vm->SetRobloxExperienceLifecycleCallbacks(
|
||||
@@ -1070,8 +1060,7 @@ Status RobloxExperienceComposition::Dispatch(
|
||||
}
|
||||
if (!request.canonical_json.empty()) {
|
||||
const bool active_game_matches =
|
||||
game_active_ &&
|
||||
request.canonical_json == active_game_canonical_json_;
|
||||
game_active_ && request.canonical_json == active_game_canonical_json_;
|
||||
const bool active_launch_matches =
|
||||
active_launch_ != nullptr &&
|
||||
request.canonical_json == active_launch_->request.canonical_json;
|
||||
@@ -1111,9 +1100,8 @@ Status RobloxExperienceComposition::DrainExternalLaunchRequests() {
|
||||
Status status = DrainActiveExternalLaunchRequests(
|
||||
ExternalLaunchSink{this, &RobloxExperienceComposition::DispatchLaunch},
|
||||
available);
|
||||
// The MessageBus can fill the composition queue after the capacity snapshot
|
||||
// above. Keep the external request at the broker's front and retry on the
|
||||
// next drain instead of turning ordinary backpressure into runtime failure.
|
||||
// A MessageBus race can fill the queue; leave the request at the broker's
|
||||
// front and retry on the next drain.
|
||||
return status.code() == StatusCode::kUnavailable ? Status::Ok() : status;
|
||||
}
|
||||
|
||||
@@ -1143,9 +1131,16 @@ Status RobloxExperienceComposition::DrainLaunchRequests() {
|
||||
: std::string();
|
||||
active_launch_.reset();
|
||||
}
|
||||
if (!completed_result.ok()) pending_launch_requests_.clear();
|
||||
if (!completed_result.ok()) {
|
||||
pending_launch_requests_.clear();
|
||||
presence_request_.reset();
|
||||
playing_presence_published_ = false;
|
||||
}
|
||||
}
|
||||
if (!completed_result.ok()) {
|
||||
NotifyPresence(RobloxExperiencePresencePhase::kBrowsing, nullptr);
|
||||
return completed_result;
|
||||
}
|
||||
if (!completed_result.ok()) return completed_result;
|
||||
}
|
||||
|
||||
std::shared_ptr<RobloxFreshGameLaunchController> returned_controller;
|
||||
@@ -1153,29 +1148,34 @@ Status RobloxExperienceComposition::DrainLaunchRequests() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (lua_app_return_pending_ && !launch_in_progress_ &&
|
||||
active_launch_ == nullptr) {
|
||||
// Consume before invoking the controller so a concurrent callback sets
|
||||
// a new pending observation instead of being overwritten on return.
|
||||
// Consume first so a concurrent callback can set a new observation.
|
||||
lua_app_return_pending_ = false;
|
||||
returned_controller = controller_;
|
||||
}
|
||||
}
|
||||
if (returned_controller != nullptr) {
|
||||
const Status observed = returned_controller->ObserveLuaAppReturn();
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!observed.ok()) {
|
||||
lua_app_return_pending_ = true;
|
||||
pending_launch_requests_.clear();
|
||||
return observed;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!observed.ok()) {
|
||||
lua_app_return_pending_ = true;
|
||||
pending_launch_requests_.clear();
|
||||
return observed;
|
||||
}
|
||||
game_active_ = false;
|
||||
active_game_canonical_json_.clear();
|
||||
presence_request_.reset();
|
||||
playing_presence_published_ = false;
|
||||
controlled_switch_waiting_for_return_ = false;
|
||||
}
|
||||
game_active_ = false;
|
||||
active_game_canonical_json_.clear();
|
||||
controlled_switch_waiting_for_return_ = false;
|
||||
NotifyPresence(RobloxExperiencePresencePhase::kBrowsing, nullptr);
|
||||
}
|
||||
|
||||
Status external_launch_status = DrainExternalLaunchRequests();
|
||||
if (!external_launch_status.ok()) return external_launch_status;
|
||||
|
||||
bool switch_active_game = false;
|
||||
std::optional<RobloxExperienceLaunchRequest> joining_request;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!subscribed_ || controller_ == nullptr || objects_ == nullptr) {
|
||||
@@ -1184,9 +1184,8 @@ Status RobloxExperienceComposition::DrainLaunchRequests() {
|
||||
if (controlled_switch_waiting_for_return_) return Status::Ok();
|
||||
if (!launch_in_progress_ && active_launch_ == nullptr && game_active_ &&
|
||||
!pending_launch_requests_.empty()) {
|
||||
// Prevent a stale gameDidLeave callback from being applied to the next
|
||||
// controller. The queued launch starts only after the exact LuaApp
|
||||
// return callback for this controlled leave has been consumed.
|
||||
// Start the queued launch only after consuming this leave callback, so
|
||||
// it cannot reach the next controller.
|
||||
controlled_switch_waiting_for_return_ = true;
|
||||
switch_active_game = true;
|
||||
}
|
||||
@@ -1235,8 +1234,13 @@ Status RobloxExperienceComposition::DrainLaunchRequests() {
|
||||
task->context.join_request_type);
|
||||
active_launch_ = std::move(task);
|
||||
launch_in_progress_ = true;
|
||||
presence_request_ = active_launch_->request;
|
||||
playing_presence_published_ = false;
|
||||
joining_request = active_launch_->request;
|
||||
}
|
||||
|
||||
NotifyPresence(RobloxExperiencePresencePhase::kJoining, &*joining_request);
|
||||
|
||||
const int start_error =
|
||||
launch_worker_.Start(&RobloxExperienceComposition::RunLaunchWorker, this,
|
||||
kLaunchWorkerStackSize);
|
||||
@@ -1245,9 +1249,12 @@ Status RobloxExperienceComposition::DrainLaunchRequests() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
launch_in_progress_ = false;
|
||||
active_launch_.reset();
|
||||
presence_request_.reset();
|
||||
active_game_canonical_json_.clear();
|
||||
playing_presence_published_ = false;
|
||||
pending_launch_requests_.clear();
|
||||
}
|
||||
NotifyPresence(RobloxExperiencePresencePhase::kBrowsing, nullptr);
|
||||
return Status::Error(StatusCode::kPlatformError,
|
||||
"could not start experience launch worker: " +
|
||||
std::to_string(start_error));
|
||||
@@ -1260,6 +1267,34 @@ void* RobloxExperienceComposition::RunLaunchWorker(void* context) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void RobloxExperienceComposition::GamePresented(void* context,
|
||||
uint64_t frame_serial) {
|
||||
if (context != nullptr && frame_serial != 0) {
|
||||
static_cast<RobloxExperienceComposition*>(context)
|
||||
->PublishPresentedPresence(frame_serial);
|
||||
}
|
||||
}
|
||||
|
||||
void RobloxExperienceComposition::PublishPresentedPresence(
|
||||
uint64_t frame_serial) {
|
||||
std::optional<RobloxExperienceLaunchRequest> request;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!subscribed_ || playing_presence_published_ ||
|
||||
!presence_request_.has_value()) {
|
||||
return;
|
||||
}
|
||||
playing_presence_published_ = true;
|
||||
request = presence_request_;
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
" [experience] first game frame published to presence "
|
||||
"frame=%llu place_id=%lld\n",
|
||||
static_cast<unsigned long long>(frame_serial),
|
||||
static_cast<long long>(request->place_id));
|
||||
NotifyPresence(RobloxExperiencePresencePhase::kPlaying, &*request);
|
||||
}
|
||||
|
||||
void RobloxExperienceComposition::RunActiveLaunch() {
|
||||
LaunchTask* task = nullptr;
|
||||
{
|
||||
@@ -1401,6 +1436,8 @@ Status RobloxExperienceComposition::Shutdown() {
|
||||
subscribed_ = false;
|
||||
game_active_ = false;
|
||||
active_game_canonical_json_.clear();
|
||||
presence_request_.reset();
|
||||
playing_presence_published_ = false;
|
||||
lua_app_return_pending_ = false;
|
||||
controlled_switch_waiting_for_return_ = false;
|
||||
lua_app_surface_recreation_pending_ = false;
|
||||
@@ -1481,6 +1518,14 @@ GameSessionSnapshot RobloxExperienceComposition::Snapshot() const {
|
||||
return controller != nullptr ? controller->Snapshot() : GameSessionSnapshot{};
|
||||
}
|
||||
|
||||
void RobloxExperienceComposition::NotifyPresence(
|
||||
RobloxExperiencePresencePhase phase,
|
||||
const RobloxExperienceLaunchRequest* request) const {
|
||||
if (presence_observer_.valid()) {
|
||||
presence_observer_.notify(presence_observer_.context, phase, request);
|
||||
}
|
||||
}
|
||||
|
||||
Status RobloxExperienceComposition::BuildPlatformGlobalObjects() {
|
||||
JNIEnv* env = nullptr;
|
||||
Status status = environment_.Acquire(&env);
|
||||
@@ -1863,6 +1908,8 @@ Status RobloxExperienceComposition::ReleaseGlobalObjects() {
|
||||
platform_protocols_initialized_ = false;
|
||||
readiness_ = {};
|
||||
active_game_canonical_json_.clear();
|
||||
presence_request_.reset();
|
||||
playing_presence_published_ = false;
|
||||
controlled_switch_waiting_for_return_ = false;
|
||||
lua_app_surface_recreation_pending_ = false;
|
||||
pending_launch_requests_.clear();
|
||||
@@ -1894,6 +1941,8 @@ Status RobloxExperienceComposition::ReleaseLuaAppGlobalObjects() {
|
||||
}
|
||||
readiness_ = {};
|
||||
active_game_canonical_json_.clear();
|
||||
presence_request_.reset();
|
||||
playing_presence_published_ = false;
|
||||
controlled_switch_waiting_for_return_ = false;
|
||||
pending_launch_requests_.clear();
|
||||
next_request_id_ = 1;
|
||||
|
||||
@@ -361,11 +361,13 @@ Status BuildFreshStartGameParams(JNIEnv* env,
|
||||
RobloxFreshGameLaunchController::RobloxFreshGameLaunchController(
|
||||
JniEnvironmentProvider environment, RobloxGameSessionSymbols symbols,
|
||||
RobloxFreshLaunchPresentBoundary present_boundary,
|
||||
RobloxGameSurfaceJniConfig surface_config)
|
||||
RobloxGameSurfaceJniConfig surface_config,
|
||||
RobloxGamePresentedObserver presented_observer)
|
||||
: environment_(environment),
|
||||
symbols_(symbols),
|
||||
present_boundary_(present_boundary),
|
||||
surface_config_(std::move(surface_config)) {}
|
||||
surface_config_(std::move(surface_config)),
|
||||
presented_observer_(presented_observer) {}
|
||||
|
||||
RobloxFreshGameLaunchController::~RobloxFreshGameLaunchController() {
|
||||
Shutdown();
|
||||
@@ -402,8 +404,8 @@ Status RobloxFreshGameLaunchController::Launch(
|
||||
return status;
|
||||
}
|
||||
|
||||
runtime_ = std::make_unique<RobloxGameSessionRuntime>(environment_, symbols_,
|
||||
surface_config_);
|
||||
runtime_ = std::make_unique<RobloxGameSessionRuntime>(
|
||||
environment_, symbols_, surface_config_, presented_observer_);
|
||||
observer_registered_ = present_boundary_.register_observer(
|
||||
present_boundary_.context,
|
||||
&RobloxGameSessionRuntime::SuccessfulPresentCallback, runtime_.get());
|
||||
|
||||
@@ -796,9 +796,11 @@ void RobloxGameSessionNativeAdapter::ReleaseGlobalReferenceLocked(
|
||||
|
||||
RobloxGameSessionRuntime::RobloxGameSessionRuntime(
|
||||
JniEnvironmentProvider environment, RobloxGameSessionSymbols symbols,
|
||||
RobloxGameSurfaceJniConfig surface_config)
|
||||
RobloxGameSurfaceJniConfig surface_config,
|
||||
RobloxGamePresentedObserver presented_observer)
|
||||
: adapter_(environment, symbols, std::move(surface_config)),
|
||||
coordinator_(adapter_.Capabilities()) {}
|
||||
coordinator_(adapter_.Capabilities()),
|
||||
presented_observer_(presented_observer) {}
|
||||
|
||||
Status RobloxGameSessionRuntime::InitializeAndStart(
|
||||
const RobloxGameSessionBinding& binding, GameSessionPrincipal principal,
|
||||
@@ -910,14 +912,16 @@ void RobloxGameSessionRuntime::SuccessfulPresentCallback(
|
||||
if (runtime->present_evidence_recorded_.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
const GameSessionUpdateResult result =
|
||||
runtime->RecordPresent(frame_serial);
|
||||
if (result.ok()) {
|
||||
const GameSessionUpdateResult result = runtime->RecordPresent(frame_serial);
|
||||
if (result.changed()) {
|
||||
runtime->present_evidence_recorded_.store(true, std::memory_order_release);
|
||||
}
|
||||
if (result.changed() && runtime->presented_observer_.valid()) {
|
||||
runtime->presented_observer_.notify(runtime->presented_observer_.context,
|
||||
frame_serial);
|
||||
}
|
||||
if (result.changed()) {
|
||||
std::fprintf(stderr,
|
||||
" [game-session] host present accepted frame=%llu\n",
|
||||
std::fprintf(stderr, " [game-session] host present accepted frame=%llu\n",
|
||||
static_cast<unsigned long long>(frame_serial));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,7 @@ namespace runtime {
|
||||
|
||||
class RobloxWindowInputRuntime;
|
||||
|
||||
// Narrow seam between the cross-thread JNI queue and its main-thread owners.
|
||||
// Production uses RobloxWindowInputRuntime and the SDL window module. Tests
|
||||
// provide a deterministic implementation without creating an SDL window.
|
||||
// Testable boundary between the JNI queue and its main-thread owners.
|
||||
class RobloxTextInputJniBridgeBackend {
|
||||
public:
|
||||
virtual ~RobloxTextInputJniBridgeBackend() = default;
|
||||
@@ -59,19 +57,15 @@ class RobloxTextInputJniBridgeBackend {
|
||||
virtual bool RequestHideTextInput(uint64_t generation) = 0;
|
||||
};
|
||||
|
||||
// Connects pseudo-JVM keyboard callbacks to the input runtime without
|
||||
// invoking SDL or Roblox native functions from an engine callback thread.
|
||||
// Guest callbacks enqueue owned commands; the registered window pre-pump
|
||||
// callback drains them on SDL's main thread.
|
||||
// Guest callbacks enqueue commands that the SDL pre-pump drains on its main
|
||||
// thread.
|
||||
class RobloxTextInputJniBridge final {
|
||||
public:
|
||||
static Status Create(jnivm::VM* vm,
|
||||
std::shared_ptr<RobloxWindowInputRuntime> input_runtime,
|
||||
std::unique_ptr<RobloxTextInputJniBridge>* bridge);
|
||||
|
||||
// Deterministic construction seam. The backend is retained through every
|
||||
// in-flight VM callback and must obey the same main-thread contract as the
|
||||
// production window implementation.
|
||||
// The test backend remains alive through in-flight VM callbacks.
|
||||
static Status CreateForTesting(
|
||||
jnivm::VM* vm, std::shared_ptr<RobloxTextInputJniBridgeBackend> backend,
|
||||
std::unique_ptr<RobloxTextInputJniBridge>* bridge);
|
||||
@@ -81,8 +75,7 @@ class RobloxTextInputJniBridge final {
|
||||
RobloxTextInputJniBridge(const RobloxTextInputJniBridge&) = delete;
|
||||
RobloxTextInputJniBridge& operator=(const RobloxTextInputJniBridge&) = delete;
|
||||
|
||||
// Idempotently stops callbacks and clears pending sensitive text before the
|
||||
// input runtime and SDL window are destroyed.
|
||||
// Stops callbacks and clears pending sensitive text before window teardown.
|
||||
Status Shutdown();
|
||||
|
||||
private:
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
#include <cstdlib>
|
||||
#include <optional>
|
||||
|
||||
#ifndef MOCKTAIL_DISCORD_APPLICATION_ID
|
||||
#define MOCKTAIL_DISCORD_APPLICATION_ID ""
|
||||
#endif
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
namespace {
|
||||
@@ -41,6 +45,37 @@ bool LegacyEnabled(const Environment& environment, std::string_view name) {
|
||||
return value.has_value() && !value->empty() && *value != "0";
|
||||
}
|
||||
|
||||
bool IsDiscordApplicationId(std::string_view value) {
|
||||
return value.empty() ||
|
||||
(value.size() >= 17 && value.size() <= 20 &&
|
||||
std::all_of(value.begin(), value.end(), [](unsigned char byte) {
|
||||
return byte >= '0' && byte <= '9';
|
||||
}));
|
||||
}
|
||||
|
||||
bool IsDiscordText(std::string_view value, std::size_t maximum) {
|
||||
return !value.empty() && value.size() <= maximum &&
|
||||
std::none_of(value.begin(), value.end(), [](unsigned char byte) {
|
||||
return byte < 0x20 || byte == 0x7f;
|
||||
});
|
||||
}
|
||||
|
||||
bool ReadBoolean(const Environment& environment, std::string_view name,
|
||||
bool default_value, bool* valid) {
|
||||
const std::optional<std::string> value = environment.Get(name);
|
||||
if (!value.has_value()) {
|
||||
return default_value;
|
||||
}
|
||||
if (*value == "1" || *value == "true" || *value == "on") {
|
||||
return true;
|
||||
}
|
||||
if (*value == "0" || *value == "false" || *value == "off") {
|
||||
return false;
|
||||
}
|
||||
*valid = false;
|
||||
return default_value;
|
||||
}
|
||||
|
||||
std::optional<bool> InputEnabled(const Environment& environment,
|
||||
std::string_view name) {
|
||||
const std::optional<std::string> value = environment.Get(name);
|
||||
@@ -241,6 +276,47 @@ RuntimeConfig RuntimeConfig::FromEnvironment(const Environment& environment) {
|
||||
config.audio_output_device_valid_ =
|
||||
IsValidDeviceProfileValue(config.audio_output_device_, 512);
|
||||
config.network_proxy_ = ReadNetworkProxy(environment);
|
||||
bool discord_booleans_valid = true;
|
||||
config.discord_rpc_.enabled = ReadBoolean(
|
||||
environment, "MOCKTAIL_DISCORD_RPC_ENABLED", false,
|
||||
&discord_booleans_valid);
|
||||
config.discord_rpc_.show_place_name = ReadBoolean(
|
||||
environment, "MOCKTAIL_DISCORD_RPC_SHOW_PLACE_NAME", true,
|
||||
&discord_booleans_valid);
|
||||
config.discord_rpc_.show_elapsed_time = ReadBoolean(
|
||||
environment, "MOCKTAIL_DISCORD_RPC_SHOW_ELAPSED_TIME", true,
|
||||
&discord_booleans_valid);
|
||||
config.discord_rpc_.join_enabled = ReadBoolean(
|
||||
environment, "MOCKTAIL_DISCORD_RPC_JOIN_ENABLED", true,
|
||||
&discord_booleans_valid);
|
||||
config.discord_rpc_.public_servers_only = ReadBoolean(
|
||||
environment, "MOCKTAIL_DISCORD_RPC_PUBLIC_SERVERS_ONLY", true,
|
||||
&discord_booleans_valid);
|
||||
config.discord_rpc_.join_button_label = environment.GetOr(
|
||||
"MOCKTAIL_DISCORD_RPC_JOIN_BUTTON_LABEL",
|
||||
config.discord_rpc_.join_button_label);
|
||||
config.discord_rpc_.application_id = environment.GetOr(
|
||||
"MOCKTAIL_DISCORD_APPLICATION_ID", MOCKTAIL_DISCORD_APPLICATION_ID);
|
||||
config.discord_rpc_.text.browsing = environment.GetOr(
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_BROWSING",
|
||||
config.discord_rpc_.text.browsing);
|
||||
config.discord_rpc_.text.joining = environment.GetOr(
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_JOINING", config.discord_rpc_.text.joining);
|
||||
config.discord_rpc_.text.playing = environment.GetOr(
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_PLAYING", config.discord_rpc_.text.playing);
|
||||
config.discord_rpc_.text.state = environment.GetOr(
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_STATE", config.discord_rpc_.text.state);
|
||||
config.discord_rpc_.text.unknown_place = environment.GetOr(
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_UNKNOWN_PLACE",
|
||||
config.discord_rpc_.text.unknown_place);
|
||||
config.discord_rpc_valid_ = discord_booleans_valid &&
|
||||
IsDiscordApplicationId(config.discord_rpc_.application_id) &&
|
||||
IsDiscordText(config.discord_rpc_.join_button_label, 32) &&
|
||||
IsDiscordText(config.discord_rpc_.text.browsing, 128) &&
|
||||
IsDiscordText(config.discord_rpc_.text.joining, 128) &&
|
||||
IsDiscordText(config.discord_rpc_.text.playing, 128) &&
|
||||
IsDiscordText(config.discord_rpc_.text.state, 128) &&
|
||||
IsDiscordText(config.discord_rpc_.text.unknown_place, 128);
|
||||
for (const std::string_view name : kUnsafeDetachedThreadOverrides) {
|
||||
if (LegacyEnabled(environment, name)) {
|
||||
config.unsafe_detached_thread_overrides_.emplace_back(name);
|
||||
|
||||
@@ -96,6 +96,32 @@ audio:
|
||||
# To pin output, copy an exact SDL device name printed during startup.
|
||||
output_device: default
|
||||
|
||||
integrations:
|
||||
discord_rpc:
|
||||
# Boolean (default: false): publish Mocktail activity to Discord Desktop.
|
||||
# This never signs in to Discord and never reads an account token.
|
||||
enabled: false
|
||||
# Boolean (default: true): show the current Roblox experience name.
|
||||
show_place_name: true
|
||||
# Boolean (default: true): show how long the current session has run.
|
||||
show_elapsed_time: true
|
||||
join:
|
||||
# Boolean (default: true): let friends open the current experience. When
|
||||
# Roblox provides a public server ID, the button targets that server.
|
||||
enabled: true
|
||||
# Boolean (default: true): never expose private or reserved joins.
|
||||
public_servers_only: true
|
||||
# button_label: Join Server
|
||||
# English defaults. Uncomment only the lines you want to customize.
|
||||
# text:
|
||||
# browsing: Browsing experiences
|
||||
# joining: Joining an experience
|
||||
# playing: "{place_name}"
|
||||
# state: Playing Roblox
|
||||
# unknown_place: Unknown experience
|
||||
# Advanced: overrides the application bundled by the package maintainer.
|
||||
# application_id: 123456789012345678
|
||||
|
||||
window:
|
||||
# Integer (default: 1280): initial window width in pixels.
|
||||
width: 1280
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include <unistd.h>
|
||||
#include <yaml.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <charconv>
|
||||
@@ -97,6 +98,45 @@ bool ReadMapping(yaml_document_t* document, const yaml_node_t* mapping,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReadNestedMapping(yaml_document_t* document, const yaml_node_t* mapping,
|
||||
const std::string& section, int remaining_depth,
|
||||
ValueMap* values, std::string* error) {
|
||||
if (mapping == nullptr || mapping->type != YAML_MAPPING_NODE) {
|
||||
*error = section + " must be a mapping";
|
||||
return false;
|
||||
}
|
||||
std::unordered_set<std::string> keys;
|
||||
for (yaml_node_pair_t* pair = mapping->data.mapping.pairs.start;
|
||||
pair != mapping->data.mapping.pairs.top; ++pair) {
|
||||
const yaml_node_t* key_node = yaml_document_get_node(document, pair->key);
|
||||
const yaml_node_t* value_node =
|
||||
yaml_document_get_node(document, pair->value);
|
||||
const std::string key = Scalar(key_node);
|
||||
if (key.empty() || value_node == nullptr) {
|
||||
*error = section + " contains a non-scalar or empty key";
|
||||
return false;
|
||||
}
|
||||
if (!keys.insert(key).second) {
|
||||
*error = section + " contains duplicate key: " + key;
|
||||
return false;
|
||||
}
|
||||
const std::string path = section + "." + key;
|
||||
if (value_node->type == YAML_SCALAR_NODE) {
|
||||
(*values)[path] = Scalar(value_node);
|
||||
continue;
|
||||
}
|
||||
if (value_node->type != YAML_MAPPING_NODE || remaining_depth <= 0) {
|
||||
*error = path + " must be a scalar";
|
||||
return false;
|
||||
}
|
||||
if (!ReadNestedMapping(document, value_node, path, remaining_depth - 1,
|
||||
values, error)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseBoolean(std::string_view value, bool* parsed) {
|
||||
if (value == "true") {
|
||||
*parsed = true;
|
||||
@@ -157,6 +197,18 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment,
|
||||
"compatibility.desktop_playability",
|
||||
"network.proxy_host",
|
||||
"network.proxy_port",
|
||||
"integrations.discord_rpc.enabled",
|
||||
"integrations.discord_rpc.show_place_name",
|
||||
"integrations.discord_rpc.show_elapsed_time",
|
||||
"integrations.discord_rpc.application_id",
|
||||
"integrations.discord_rpc.join.enabled",
|
||||
"integrations.discord_rpc.join.public_servers_only",
|
||||
"integrations.discord_rpc.join.button_label",
|
||||
"integrations.discord_rpc.text.browsing",
|
||||
"integrations.discord_rpc.text.joining",
|
||||
"integrations.discord_rpc.text.playing",
|
||||
"integrations.discord_rpc.text.state",
|
||||
"integrations.discord_rpc.text.unknown_place",
|
||||
};
|
||||
for (const auto& [key, ignored] : yaml) {
|
||||
if (supported.find(key) == supported.end()) {
|
||||
@@ -387,6 +439,78 @@ bool ValidateAndMap(const ValueMap& yaml, ValueMap* environment,
|
||||
(*environment)["MOCKTAIL_HTTP_PROXY_HOST"] = *proxy_host;
|
||||
(*environment)["MOCKTAIL_HTTP_PROXY_PORT"] = *proxy_port;
|
||||
}
|
||||
for (const auto& [key, variable] : {
|
||||
std::pair<std::string_view, std::string_view>(
|
||||
"integrations.discord_rpc.enabled",
|
||||
"MOCKTAIL_DISCORD_RPC_ENABLED"),
|
||||
{"integrations.discord_rpc.show_place_name",
|
||||
"MOCKTAIL_DISCORD_RPC_SHOW_PLACE_NAME"},
|
||||
{"integrations.discord_rpc.show_elapsed_time",
|
||||
"MOCKTAIL_DISCORD_RPC_SHOW_ELAPSED_TIME"},
|
||||
{"integrations.discord_rpc.join.enabled",
|
||||
"MOCKTAIL_DISCORD_RPC_JOIN_ENABLED"},
|
||||
{"integrations.discord_rpc.join.public_servers_only",
|
||||
"MOCKTAIL_DISCORD_RPC_PUBLIC_SERVERS_ONLY"},
|
||||
}) {
|
||||
const std::optional<std::string> configured = value(key);
|
||||
if (!configured.has_value()) {
|
||||
continue;
|
||||
}
|
||||
bool parsed = false;
|
||||
if (!ParseBoolean(*configured, &parsed)) {
|
||||
*error = std::string(key) + " must be true or false";
|
||||
return false;
|
||||
}
|
||||
(*environment)[std::string(variable)] = parsed ? "1" : "0";
|
||||
}
|
||||
if (const auto application_id =
|
||||
value("integrations.discord_rpc.application_id");
|
||||
application_id.has_value()) {
|
||||
if (application_id->size() < 17 || application_id->size() > 20 ||
|
||||
!std::all_of(application_id->begin(), application_id->end(),
|
||||
[](unsigned char byte) {
|
||||
return byte >= '0' && byte <= '9';
|
||||
})) {
|
||||
*error = "integrations.discord_rpc.application_id must be a Discord "
|
||||
"snowflake";
|
||||
return false;
|
||||
}
|
||||
(*environment)["MOCKTAIL_DISCORD_APPLICATION_ID"] = *application_id;
|
||||
}
|
||||
struct DiscordStringField {
|
||||
std::string_view yaml;
|
||||
std::string_view variable;
|
||||
std::size_t maximum;
|
||||
};
|
||||
for (const DiscordStringField& field : {
|
||||
DiscordStringField{"integrations.discord_rpc.join.button_label",
|
||||
"MOCKTAIL_DISCORD_RPC_JOIN_BUTTON_LABEL", 32},
|
||||
{"integrations.discord_rpc.text.browsing",
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_BROWSING", 128},
|
||||
{"integrations.discord_rpc.text.joining",
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_JOINING", 128},
|
||||
{"integrations.discord_rpc.text.playing",
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_PLAYING", 128},
|
||||
{"integrations.discord_rpc.text.state",
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_STATE", 128},
|
||||
{"integrations.discord_rpc.text.unknown_place",
|
||||
"MOCKTAIL_DISCORD_RPC_TEXT_UNKNOWN_PLACE", 128},
|
||||
}) {
|
||||
const std::optional<std::string> configured = value(field.yaml);
|
||||
if (!configured.has_value()) {
|
||||
continue;
|
||||
}
|
||||
if (configured->empty() || configured->size() > field.maximum ||
|
||||
std::any_of(configured->begin(), configured->end(),
|
||||
[](unsigned char byte) {
|
||||
return byte < 0x20 || byte == 0x7f;
|
||||
})) {
|
||||
*error = std::string(field.yaml) +
|
||||
" must be non-empty, bounded, and contain no control bytes";
|
||||
return false;
|
||||
}
|
||||
(*environment)[std::string(field.variable)] = *configured;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -496,6 +620,8 @@ bool LoadYaml(const std::filesystem::path& path, ValueMap* values, bool* loaded,
|
||||
key == "performance" || key == "audio" || key == "window" ||
|
||||
key == "input" || key == "compatibility" || key == "network") {
|
||||
valid = ReadMapping(&document, value_node, key, values, error);
|
||||
} else if (key == "integrations") {
|
||||
valid = ReadNestedMapping(&document, value_node, key, 2, values, error);
|
||||
}
|
||||
// Other top-level sections are owned by subsystems such as the
|
||||
// updater. This loader ignores rather than misparses them.
|
||||
@@ -575,6 +701,8 @@ RuntimeConfigLoadResult LoadRuntimeConfig(
|
||||
result.error = "GameMode policy is invalid";
|
||||
} else if (!result.config.performance().physics_worker_mode_valid) {
|
||||
result.error = "physics worker policy is invalid";
|
||||
} else if (!result.config.discord_rpc_valid()) {
|
||||
result.error = "Discord Rich Presence configuration is invalid";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -611,6 +739,12 @@ bool ExportRuntimeConfigEnvironment(const RuntimeConfig& config,
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (!config.discord_rpc_valid()) {
|
||||
if (error != nullptr) {
|
||||
*error = "cannot export an invalid Discord Rich Presence policy";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const DeviceProfile& device = config.device_profile();
|
||||
const bool base_exported =
|
||||
SetEnvironmentValue("MOCKTAIL_HEADLESS", config.headless() ? "1" : "0",
|
||||
@@ -673,7 +807,35 @@ bool ExportRuntimeConfigEnvironment(const RuntimeConfig& config,
|
||||
GameModePolicyName(config.performance().game_mode),
|
||||
error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_AUDIO_OUTPUT_DEVICE",
|
||||
config.audio_output_device(), error);
|
||||
config.audio_output_device(), error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_DISCORD_RPC_ENABLED",
|
||||
config.discord_rpc().enabled ? "1" : "0", error) &&
|
||||
SetEnvironmentValue(
|
||||
"MOCKTAIL_DISCORD_RPC_SHOW_PLACE_NAME",
|
||||
config.discord_rpc().show_place_name ? "1" : "0", error) &&
|
||||
SetEnvironmentValue(
|
||||
"MOCKTAIL_DISCORD_RPC_SHOW_ELAPSED_TIME",
|
||||
config.discord_rpc().show_elapsed_time ? "1" : "0", error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_DISCORD_RPC_JOIN_ENABLED",
|
||||
config.discord_rpc().join_enabled ? "1" : "0",
|
||||
error) &&
|
||||
SetEnvironmentValue(
|
||||
"MOCKTAIL_DISCORD_RPC_PUBLIC_SERVERS_ONLY",
|
||||
config.discord_rpc().public_servers_only ? "1" : "0", error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_DISCORD_RPC_JOIN_BUTTON_LABEL",
|
||||
config.discord_rpc().join_button_label, error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_DISCORD_APPLICATION_ID",
|
||||
config.discord_rpc().application_id, error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_DISCORD_RPC_TEXT_BROWSING",
|
||||
config.discord_rpc().text.browsing, error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_DISCORD_RPC_TEXT_JOINING",
|
||||
config.discord_rpc().text.joining, error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_DISCORD_RPC_TEXT_PLAYING",
|
||||
config.discord_rpc().text.playing, error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_DISCORD_RPC_TEXT_STATE",
|
||||
config.discord_rpc().text.state, error) &&
|
||||
SetEnvironmentValue("MOCKTAIL_DISCORD_RPC_TEXT_UNKNOWN_PLACE",
|
||||
config.discord_rpc().text.unknown_place, error);
|
||||
if (!base_exported) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -418,7 +418,6 @@ CandidateApprovalResult CreateCandidateApproval(
|
||||
bool ValidateCandidateApproval(const std::filesystem::path& store_root,
|
||||
std::string_view activation_json,
|
||||
const PayloadIntegrityResult& payload,
|
||||
const std::filesystem::path& runtime_binary,
|
||||
CandidateApprovalResult* approval,
|
||||
std::string* error) {
|
||||
const auto activation = ParseJson(activation_json, error);
|
||||
@@ -466,15 +465,14 @@ bool ValidateCandidateApproval(const std::filesystem::path& store_root,
|
||||
}
|
||||
const std::string fingerprint = PayloadRuntimeFingerprint(
|
||||
store_root / "payloads" / payload.payload_id, payload, error);
|
||||
const std::string runtime_sha256 = HashRegularFile(runtime_binary, error);
|
||||
const compat::BuildIdResult runtime_build_id =
|
||||
compat::ReadElfBuildId(runtime_binary.string());
|
||||
const std::string profile_sha256 = HashText(profile_bytes);
|
||||
const std::string compatibility_sha256 = HashText(compatibility_bytes);
|
||||
if (!error->empty() || !runtime_build_id) {
|
||||
if (error->empty()) *error = runtime_build_id.error;
|
||||
return false;
|
||||
}
|
||||
if (!error->empty()) return false;
|
||||
const std::string canary_runtime_sha256 =
|
||||
receipt->value("canary_runtime_sha256", "");
|
||||
const std::string runtime_sha256 = receipt->value("runtime_sha256", "");
|
||||
const std::string runtime_build_id =
|
||||
receipt->value("runtime_build_id", "");
|
||||
std::error_code filesystem_error;
|
||||
const std::filesystem::path canonical_library = std::filesystem::canonical(
|
||||
store_root / "payloads" / payload.payload_id / "libroblox.so",
|
||||
@@ -494,15 +492,15 @@ bool ValidateCandidateApproval(const std::filesystem::path& store_root,
|
||||
receipt->value("profile_sha256", "") != profile_sha256 ||
|
||||
receipt->value("compatibility_manifest_sha256", "") !=
|
||||
compatibility_sha256 ||
|
||||
receipt->value("canary_runtime_sha256", "") != runtime_sha256 ||
|
||||
receipt->value("runtime_sha256", "") != runtime_sha256 ||
|
||||
receipt->value("runtime_build_id", "") != runtime_build_id.build_id ||
|
||||
!IsLowerHex(canary_runtime_sha256, 64) ||
|
||||
runtime_sha256 != canary_runtime_sha256 ||
|
||||
!IsLowerHex(runtime_build_id, 40) ||
|
||||
receipt->value("canary_tier", "") != "C" ||
|
||||
receipt->value("successful_runs", 0) != 2 ||
|
||||
!receipt->contains("canary_attestation_sha256") ||
|
||||
!(*receipt)["canary_attestation_sha256"].is_array() ||
|
||||
(*receipt)["canary_attestation_sha256"].size() != 2) {
|
||||
*error = "approval receipt does not authorize exact runtime and payload";
|
||||
*error = "approval receipt does not authorize exact payload evidence";
|
||||
return false;
|
||||
}
|
||||
std::array<std::string, 2> attestation_hashes;
|
||||
@@ -526,9 +524,9 @@ bool ValidateCandidateApproval(const std::filesystem::path& store_root,
|
||||
attestation->value("profile_sha256", "") != profile_sha256 ||
|
||||
attestation->value("compatibility_manifest_sha256", "") !=
|
||||
compatibility_sha256 ||
|
||||
attestation->value("runtime_sha256", "") != runtime_sha256 ||
|
||||
attestation->value("runtime_build_id", "") !=
|
||||
runtime_build_id.build_id ||
|
||||
attestation->value("runtime_sha256", "") !=
|
||||
canary_runtime_sha256 ||
|
||||
attestation->value("runtime_build_id", "") != runtime_build_id ||
|
||||
!IsLowerHex(attestation->value("readiness_log_sha256", ""), 64) ||
|
||||
!(*receipt)["canary_attestation_sha256"][index].is_string() ||
|
||||
(*receipt)["canary_attestation_sha256"][index]
|
||||
@@ -543,7 +541,7 @@ bool ValidateCandidateApproval(const std::filesystem::path& store_root,
|
||||
}
|
||||
std::string generation_evidence;
|
||||
generation_evidence.append("runtime_build_id=")
|
||||
.append(runtime_build_id.build_id)
|
||||
.append(runtime_build_id)
|
||||
.append("\n");
|
||||
generation_evidence.append("payload=").append(fingerprint).append("\n");
|
||||
generation_evidence.append("profile=").append(profile_sha256).append("\n");
|
||||
|
||||
+50
-15
@@ -360,6 +360,7 @@ nlohmann::json ActivationManifest(const PayloadIntegrityResult& payload) {
|
||||
{"version_name", payload.metadata.version_name},
|
||||
{"version_code", payload.metadata.version_code},
|
||||
{"elf_build_id", payload.metadata.build_id},
|
||||
{"payload_sha256", payload.metadata.library_sha256},
|
||||
{"activated_at_epoch",
|
||||
std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
@@ -398,10 +399,21 @@ bool PopulateInspectedResult(
|
||||
if (verified_payload != nullptr) *verified_payload = payload;
|
||||
const nlohmann::json document =
|
||||
nlohmann::json::parse(manifest.contents, nullptr, false, true);
|
||||
if (document.is_discarded()) {
|
||||
if (document.is_discarded() ||
|
||||
document.value("version_name", "") != payload.metadata.version_name ||
|
||||
document.value("version_code", 0ULL) != payload.metadata.version_code ||
|
||||
document.value("elf_build_id", "") != payload.metadata.build_id) {
|
||||
result->error = "active payload manifest is invalid";
|
||||
return false;
|
||||
}
|
||||
const auto cached_sha256 = document.find("payload_sha256");
|
||||
if (cached_sha256 != document.end() &&
|
||||
(!cached_sha256->is_string() ||
|
||||
cached_sha256->get_ref<const std::string&>() !=
|
||||
payload.metadata.library_sha256)) {
|
||||
result->error = "active payload manifest hash does not match payload";
|
||||
return false;
|
||||
}
|
||||
const std::string profile = document.value("host_abi_profile_path", "");
|
||||
const std::string compatibility =
|
||||
document.value("compatibility_manifest_path", "");
|
||||
@@ -418,6 +430,31 @@ bool PopulateInspectedResult(
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PersistPayloadSha256(const std::filesystem::path& root,
|
||||
const ManifestIdentity& manifest,
|
||||
const PayloadIntegrityResult& payload,
|
||||
std::string* error) {
|
||||
nlohmann::json document =
|
||||
nlohmann::json::parse(manifest.contents, nullptr, false, true);
|
||||
if (document.is_discarded() || !document.is_object()) {
|
||||
*error = "active payload manifest is invalid";
|
||||
return false;
|
||||
}
|
||||
const auto cached = document.find("payload_sha256");
|
||||
if (cached != document.end()) {
|
||||
if (!cached->is_string() ||
|
||||
cached->get_ref<const std::string&>() !=
|
||||
payload.metadata.library_sha256) {
|
||||
*error = "active payload manifest hash does not match payload";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
document["payload_sha256"] = payload.metadata.library_sha256;
|
||||
return WriteAtomic(root, root / "current.json", document.dump(2) + "\n",
|
||||
error);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
PayloadStore::PayloadStore(std::filesystem::path root,
|
||||
@@ -563,8 +600,8 @@ PayloadStoreResult PayloadStore::PromoteProbation(
|
||||
const std::string contents =
|
||||
ProbationActivationManifest(payload, approval).dump(2) + "\n";
|
||||
CandidateApprovalResult verified_approval;
|
||||
if (!ValidateCandidateApproval(root_, contents, payload, runtime_binary_,
|
||||
&verified_approval, &result.error)) {
|
||||
if (!ValidateCandidateApproval(root_, contents, payload, &verified_approval,
|
||||
&result.error)) {
|
||||
return result;
|
||||
}
|
||||
const ManifestIdentity current = ReadManifest(root_ / "current.json", true);
|
||||
@@ -596,7 +633,7 @@ PayloadStoreResult PayloadStore::Import(
|
||||
return Promote(staged.payload_id);
|
||||
}
|
||||
|
||||
PayloadStoreResult PayloadStore::VerifyCurrent() const {
|
||||
PayloadStoreResult PayloadStore::VerifyCurrent() {
|
||||
PayloadStoreResult result;
|
||||
StoreLock lock(root_, &result.error);
|
||||
if (!lock) return result;
|
||||
@@ -621,15 +658,17 @@ PayloadStoreResult PayloadStore::VerifyCurrent() const {
|
||||
std::string support_error;
|
||||
if (ExactSupported(compatibility_manifest_, payload.metadata,
|
||||
&support_error)) {
|
||||
return result;
|
||||
}
|
||||
if (runtime_binary_.empty()) {
|
||||
result.error = "approved payload verification requires runtime identity";
|
||||
if (!PersistPayloadSha256(root_, manifest, payload, &result.error)) {
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
CandidateApprovalResult approval;
|
||||
if (!ValidateCandidateApproval(root_, manifest.contents, payload,
|
||||
runtime_binary_, &approval, &result.error)) {
|
||||
if (!ValidateCandidateApproval(root_, manifest.contents, payload, &approval,
|
||||
&result.error)) {
|
||||
return result;
|
||||
}
|
||||
if (!PersistPayloadSha256(root_, manifest, payload, &result.error)) {
|
||||
return result;
|
||||
}
|
||||
result.host_abi_profile = approval.profile;
|
||||
@@ -673,13 +712,9 @@ PayloadStoreResult PayloadStore::Rollback() {
|
||||
std::string support_error;
|
||||
if (!ExactSupported(compatibility_manifest_, payload.metadata,
|
||||
&support_error)) {
|
||||
if (runtime_binary_.empty()) {
|
||||
result.error = "rollback approval requires runtime identity";
|
||||
return result;
|
||||
}
|
||||
CandidateApprovalResult approval;
|
||||
if (!ValidateCandidateApproval(root_, previous.contents, payload,
|
||||
runtime_binary_, &approval, &result.error)) {
|
||||
&approval, &result.error)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
+61
-94
@@ -12,13 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// src/window/window.cc — SDL3 window + real EGL context.
|
||||
//
|
||||
// Creates a real OS window via SDL3 and obtains a real EGL display + context
|
||||
// from the selected host EGL/GLES driver (system GLVND or ANGLE/Vulkan).
|
||||
// libroblox.so's EGL calls are intercepted in libegl_stub and redirected to
|
||||
// these handles so the game engine renders into a real window.
|
||||
|
||||
#include "window/window.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
@@ -55,7 +48,7 @@
|
||||
namespace mocktail {
|
||||
namespace window {
|
||||
|
||||
// EGL type declarations (no system EGL headers to avoid conflicts with stubs)
|
||||
// Local EGL declarations avoid conflicts with the stub headers.
|
||||
|
||||
using EGLDisplay = void*;
|
||||
using EGLConfig = void*;
|
||||
@@ -98,8 +91,6 @@ static constexpr EGLDisplay EGL_NO_DISPLAY = nullptr;
|
||||
static constexpr EGLContext EGL_NO_CONTEXT = nullptr;
|
||||
static constexpr EGLSurface EGL_NO_SURFACE = nullptr;
|
||||
|
||||
// EGL function pointer types
|
||||
|
||||
using PFN_eglGetDisplay = EGLDisplay (*)(EGLNativeDisplayType);
|
||||
using PFN_eglInitialize = EGLBoolean (*)(EGLDisplay, EGLint*, EGLint*);
|
||||
using PFN_eglBindAPI = EGLBoolean (*)(EGLenum);
|
||||
@@ -121,8 +112,6 @@ using PFN_eglDestroySurface = EGLBoolean (*)(EGLDisplay, EGLSurface);
|
||||
using PFN_eglTerminate = EGLBoolean (*)(EGLDisplay);
|
||||
using PFN_eglGetProcAddress = __eglMustCastFP (*)(const char*);
|
||||
|
||||
// Module state
|
||||
|
||||
struct WindowState {
|
||||
SDL_Window* sdl_window = nullptr;
|
||||
EGLDisplay egl_display = nullptr;
|
||||
@@ -551,10 +540,8 @@ void ConfigureGraphicsBackendBeforeSDL() {
|
||||
SDL_SetHint(SDL_HINT_VIDEO_FORCE_EGL, "1");
|
||||
|
||||
if (ShouldUseAngleVulkanBackend() || ShouldUseNativeVulkanBackend()) {
|
||||
// Mesa's implicit device-select layer is useful on desktop, but it can
|
||||
// crash when ANGLE/Roblox initialise Vulkan through the Android compat
|
||||
// path. The layer's JSON advertises NODEVICE_SELECT=1 as the supported
|
||||
// opt-out, so use the real Vulkan loader/ICDs without that implicit layer.
|
||||
// Mesa's implicit device selector can crash in the Android Vulkan path;
|
||||
// its supported opt-out is NODEVICE_SELECT=1.
|
||||
setenv("NODEVICE_SELECT", "1", 0);
|
||||
setenv("DISABLE_LAYER_MESA_ANTI_LAG", "1", 0);
|
||||
}
|
||||
@@ -563,8 +550,7 @@ void ConfigureGraphicsBackendBeforeSDL() {
|
||||
ResolveConfiguredVideoDriverChoice();
|
||||
const char* video_driver = VideoDriverChoiceName(video_driver_choice);
|
||||
if (video_driver != nullptr) {
|
||||
// ResolveConfiguredVideoDriverChoice already preserves every non-empty
|
||||
// user override. Overwrite only an inherited empty variable here.
|
||||
// Preserve any non-empty user override.
|
||||
setenv("SDL_VIDEODRIVER", video_driver, 1);
|
||||
SDL_SetHint(SDL_HINT_VIDEO_DRIVER, video_driver);
|
||||
}
|
||||
@@ -654,9 +640,7 @@ void* QueryNativeWindowHandle() {
|
||||
return wl_egl_win;
|
||||
}
|
||||
|
||||
// Direct Vulkan creates VkSurfaceKHR from SDL_Window rather than an EGL
|
||||
// window. Its SDL object is therefore the stable host-native identity when
|
||||
// the backend exposes no EGL-specific Wayland handle.
|
||||
// Direct Vulkan uses SDL_Window as its stable native identity.
|
||||
return g_state.direct_vulkan ? g_state.sdl_window : nullptr;
|
||||
}
|
||||
|
||||
@@ -865,8 +849,6 @@ bool RetryWithAutoAngleFallback(int width, int height, const char* title,
|
||||
return Init(width, height, title);
|
||||
}
|
||||
|
||||
// Init
|
||||
|
||||
Status ConfigureWindowStatePersistence(const std::filesystem::path& path) {
|
||||
if (g_state.initialised) {
|
||||
return Status::Error(
|
||||
@@ -886,16 +868,14 @@ bool Init(int width, int height, const char* title) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const Status metadata_status =
|
||||
platform::ConfigureSdlApplicationMetadata();
|
||||
const Status metadata_status = platform::ConfigureSdlApplicationMetadata();
|
||||
if (!metadata_status.ok()) {
|
||||
std::fprintf(stderr, " [window] SDL application metadata failed: %s\n",
|
||||
metadata_status.message().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// The same-surface Vulkan overlay renders SDL_EVENT_TEXT_EDITING
|
||||
// composition text. Candidate lists remain owned by the platform IME.
|
||||
// The Vulkan overlay renders preedit text; the platform IME owns candidates.
|
||||
SDL_SetHint(SDL_HINT_IME_IMPLEMENTED_UI, "composition");
|
||||
|
||||
g_state.state_persistence_active =
|
||||
@@ -968,15 +948,12 @@ bool Init(int width, int height, const char* title) {
|
||||
g_state.direct_vulkan = true;
|
||||
g_state.initialised = true;
|
||||
ResolveNativeWindowHandle();
|
||||
// The Android handle is opaque to the guest-facing adapter. Keep it
|
||||
// non-null even when SDL does not expose an EGL-specific Wayland handle;
|
||||
// SDL_Vulkan_CreateSurface uses sdl_window directly.
|
||||
// Direct Vulkan needs a non-null opaque Android handle even without an
|
||||
// EGL-specific Wayland handle.
|
||||
if (g_state.native_window == nullptr) {
|
||||
g_state.native_window = g_state.sdl_window;
|
||||
}
|
||||
// Wayland does not configure a surface (and Vulkan reports undefined
|
||||
// currentExtent) until the window is mapped. Direct Vulkan therefore
|
||||
// cannot use the EGL-era "show on first swap" policy.
|
||||
// Wayland reports undefined currentExtent until the window is mapped.
|
||||
if (!SDL_ShowWindow(g_state.sdl_window)) {
|
||||
fprintf(stderr, " [window] SDL_ShowWindow failed: %s\n", SDL_GetError());
|
||||
SDL_DestroyWindow(g_state.sdl_window);
|
||||
@@ -1006,8 +983,7 @@ bool Init(int width, int height, const char* title) {
|
||||
fprintf(stderr, " [window] SDL3 direct Vulkan window created (%dx%d)\n",
|
||||
width, height);
|
||||
if (!g_present_lifecycle.Activate()) {
|
||||
fprintf(stderr,
|
||||
" [window] host present lifecycle is already active\n");
|
||||
fprintf(stderr, " [window] host present lifecycle is already active\n");
|
||||
g_present_lifecycle.Deactivate();
|
||||
SDL_DestroyWindow(g_state.sdl_window);
|
||||
g_state.sdl_window = nullptr;
|
||||
@@ -1064,7 +1040,6 @@ bool Init(int width, int height, const char* title) {
|
||||
width, height, title ? title : "(null)");
|
||||
}
|
||||
|
||||
// ----- SDL3 init -----
|
||||
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
||||
fprintf(stderr, " [window] SDL_Init failed: %s\n", SDL_GetError());
|
||||
return false;
|
||||
@@ -1075,10 +1050,9 @@ bool Init(int width, int height, const char* title) {
|
||||
|
||||
ConfigureGraphicsBackendAfterSDLInit();
|
||||
|
||||
// Keep the hint set in case SDL rechecks it during context creation.
|
||||
// SDL may recheck this hint during context creation.
|
||||
SDL_SetHint(SDL_HINT_VIDEO_FORCE_EGL, "1");
|
||||
|
||||
// Configure SDL3 GL context for OpenGL ES 3.0 / EGL
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
|
||||
@@ -1090,7 +1064,6 @@ bool Init(int width, int height, const char* title) {
|
||||
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
|
||||
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
|
||||
|
||||
// Create the window
|
||||
SDL_WindowFlags window_flags =
|
||||
SDL_WINDOW_OPENGL | SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WINDOW_RESIZABLE;
|
||||
if (g_state.state_persistence_active && g_state.persisted_window.fullscreen) {
|
||||
@@ -1121,11 +1094,9 @@ bool Init(int width, int height, const char* title) {
|
||||
fprintf(stderr, " [window] SDL3 window created (%dx%d)\n", width, height);
|
||||
ShowWindowAccordingToStartupMode();
|
||||
|
||||
// Create SDL GL/EGL context
|
||||
SDL_GLContext sdl_context = SDL_GL_CreateContext(g_state.sdl_window);
|
||||
if (!sdl_context && IsEnabledEnv("MOCKTAIL_ALLOW_GLES2_RESEARCH")) {
|
||||
// Current production Roblox client settings reject GLES2. Retain this
|
||||
// only as an explicit research probe for old payloads.
|
||||
// Current Roblox clients reject GLES2; this path supports older payloads.
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
|
||||
sdl_context = SDL_GL_CreateContext(g_state.sdl_window);
|
||||
}
|
||||
@@ -1147,7 +1118,6 @@ bool Init(int width, int height, const char* title) {
|
||||
return CreateSoftwareWaitingWindow(width, height, title);
|
||||
}
|
||||
|
||||
// Make the context current to initialize internal structures
|
||||
if (!SDL_GL_MakeCurrent(g_state.sdl_window, sdl_context)) {
|
||||
fprintf(stderr, " [window] SDL_GL_MakeCurrent failed: %s\n",
|
||||
SDL_GetError());
|
||||
@@ -1200,7 +1170,6 @@ bool Init(int width, int height, const char* title) {
|
||||
return CreateSoftwareWaitingWindow(width, height, title);
|
||||
}
|
||||
|
||||
// Query EGL handles from SDL3
|
||||
g_state.egl_display = SDL_EGL_GetCurrentDisplay();
|
||||
g_state.egl_config = SDL_EGL_GetCurrentConfig();
|
||||
g_state.egl_surface = SDL_EGL_GetWindowSurface(g_state.sdl_window);
|
||||
@@ -1239,7 +1208,7 @@ bool Init(int width, int height, const char* title) {
|
||||
|
||||
ResolveNativeWindowHandle();
|
||||
|
||||
// Unbind context on main thread so that engine thread can bind it
|
||||
// The render thread binds the context after the main thread releases it.
|
||||
SDL_GL_MakeCurrent(g_state.sdl_window, nullptr);
|
||||
if (WindowTraceEnabled()) {
|
||||
fprintf(stderr, " [window] SDL_GL_MakeCurrent(nullptr) on main thread\n");
|
||||
@@ -1261,8 +1230,6 @@ bool Init(int width, int height, const char* title) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// MakeCurrentOnThread — call from the engine rendering thread to bind the
|
||||
// EGL context on that thread.
|
||||
bool MakeCurrentOnThread() {
|
||||
if (!g_state.initialised || !g_state.egl_context) {
|
||||
if (WindowTraceEnabled()) {
|
||||
@@ -1301,8 +1268,6 @@ bool ReleaseCurrentOnThread() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Public accessors
|
||||
|
||||
bool IsInitialised() { return g_state.initialised; }
|
||||
bool HasPresentedFrame() { return g_real_swap_count.load() > 0; }
|
||||
|
||||
@@ -1631,7 +1596,7 @@ extern "C" std::uint64_t mocktail_window_note_vulkan_call_begin(
|
||||
}
|
||||
|
||||
extern "C" void mocktail_window_note_vulkan_call_end(std::uint64_t sequence,
|
||||
std::int32_t result) {
|
||||
std::int32_t result) {
|
||||
g_vulkan_present_progress_gate.NotifyCallEnd(
|
||||
sequence, result, SDL_GetTicksNS(), SDL_GetCurrentThreadID());
|
||||
}
|
||||
@@ -1672,9 +1637,7 @@ void MaybeQueueInputReadinessSequence() {
|
||||
SDL_Event focus{};
|
||||
focus.type = SDL_EVENT_WINDOW_FOCUS_GAINED;
|
||||
focus.window.windowID = window_id;
|
||||
// A non-interactive readiness runner may not receive compositor focus. Send
|
||||
// the same focus event before its opt-in click; production runs never
|
||||
// enter this path unless MOCKTAIL_INPUT_TEST_CLICK is exactly 1.
|
||||
// An opt-in readiness click may need a synthetic focus event.
|
||||
const bool needs_focus =
|
||||
(SDL_GetWindowFlags(g_state.sdl_window) & SDL_WINDOW_INPUT_FOCUS) == 0;
|
||||
|
||||
@@ -1761,8 +1724,7 @@ bool RequestFullscreenState(bool fullscreen, const char* reason) {
|
||||
const bool current_fullscreen =
|
||||
(SDL_GetWindowFlags(g_state.sdl_window) & SDL_WINDOW_FULLSCREEN) != 0;
|
||||
if (current_fullscreen != fullscreen) {
|
||||
// Capture the normal restore rectangle before SDL replaces it with the
|
||||
// monitor extent.
|
||||
// Save the restore rectangle before SDL replaces it with monitor bounds.
|
||||
CaptureWindowState();
|
||||
if (!SDL_SetWindowFullscreen(g_state.sdl_window, fullscreen)) {
|
||||
fprintf(stderr, " [fullscreen] SDL request failed: %s\n",
|
||||
@@ -1916,7 +1878,7 @@ void MaybeReportVulkanPresentStall() {
|
||||
: 0;
|
||||
const uint64_t completed_duration_ms =
|
||||
report.last_completed_call_completed_ns >=
|
||||
report.last_completed_call_started_ns &&
|
||||
report.last_completed_call_started_ns &&
|
||||
report.last_completed_call_started_ns != 0
|
||||
? (report.last_completed_call_completed_ns -
|
||||
report.last_completed_call_started_ns) /
|
||||
@@ -1928,40 +1890,36 @@ void MaybeReportVulkanPresentStall() {
|
||||
} else if (report.stage == VulkanPresentStallStage::kInsideHostPresent) {
|
||||
stage = "inside_host_present";
|
||||
}
|
||||
fprintf(stderr,
|
||||
" [vulkan] progress watchdog: stage=%s stalled_for=%llu ms "
|
||||
"frame=%llu host_presents=%llu render_tid=%llu primary_call=%s "
|
||||
"primary_id=%llu primary_tid=%llu last_snapshot=%s last_call=%s "
|
||||
"last_id=%llu last_tid=%llu last_result=%d last_duration=%llu ms "
|
||||
"completed_ago=%llu ms secondary_call=%s secondary_id=%llu "
|
||||
"secondary_tid=%llu secondary_for=%llu ms\n",
|
||||
stage,
|
||||
static_cast<unsigned long long>(report.elapsed_ns / 1000000ULL),
|
||||
static_cast<unsigned long long>(report.frame_count),
|
||||
static_cast<unsigned long long>(report.host_present_count),
|
||||
static_cast<unsigned long long>(report.last_present_thread_id),
|
||||
report.call_name != nullptr ? report.call_name : "none",
|
||||
static_cast<unsigned long long>(report.call_sequence),
|
||||
static_cast<unsigned long long>(report.thread_id),
|
||||
report.last_completed_call_snapshot_coherent ? "coherent"
|
||||
: "unavailable",
|
||||
report.last_completed_call_name != nullptr
|
||||
? report.last_completed_call_name
|
||||
: "none",
|
||||
static_cast<unsigned long long>(
|
||||
report.last_completed_call_sequence),
|
||||
static_cast<unsigned long long>(
|
||||
report.last_completed_call_thread_id),
|
||||
report.last_completed_call_result,
|
||||
static_cast<unsigned long long>(completed_duration_ms),
|
||||
static_cast<unsigned long long>(completed_ago_ms),
|
||||
report.oldest_active_call_name != nullptr
|
||||
? report.oldest_active_call_name
|
||||
: "none",
|
||||
static_cast<unsigned long long>(report.oldest_active_call_sequence),
|
||||
static_cast<unsigned long long>(report.oldest_active_call_thread_id),
|
||||
static_cast<unsigned long long>(
|
||||
report.oldest_active_call_elapsed_ns / 1000000ULL));
|
||||
fprintf(
|
||||
stderr,
|
||||
" [vulkan] progress watchdog: stage=%s stalled_for=%llu ms "
|
||||
"frame=%llu host_presents=%llu render_tid=%llu primary_call=%s "
|
||||
"primary_id=%llu primary_tid=%llu last_snapshot=%s last_call=%s "
|
||||
"last_id=%llu last_tid=%llu last_result=%d last_duration=%llu ms "
|
||||
"completed_ago=%llu ms secondary_call=%s secondary_id=%llu "
|
||||
"secondary_tid=%llu secondary_for=%llu ms\n",
|
||||
stage, static_cast<unsigned long long>(report.elapsed_ns / 1000000ULL),
|
||||
static_cast<unsigned long long>(report.frame_count),
|
||||
static_cast<unsigned long long>(report.host_present_count),
|
||||
static_cast<unsigned long long>(report.last_present_thread_id),
|
||||
report.call_name != nullptr ? report.call_name : "none",
|
||||
static_cast<unsigned long long>(report.call_sequence),
|
||||
static_cast<unsigned long long>(report.thread_id),
|
||||
report.last_completed_call_snapshot_coherent ? "coherent" : "unavailable",
|
||||
report.last_completed_call_name != nullptr
|
||||
? report.last_completed_call_name
|
||||
: "none",
|
||||
static_cast<unsigned long long>(report.last_completed_call_sequence),
|
||||
static_cast<unsigned long long>(report.last_completed_call_thread_id),
|
||||
report.last_completed_call_result,
|
||||
static_cast<unsigned long long>(completed_duration_ms),
|
||||
static_cast<unsigned long long>(completed_ago_ms),
|
||||
report.oldest_active_call_name != nullptr ? report.oldest_active_call_name
|
||||
: "none",
|
||||
static_cast<unsigned long long>(report.oldest_active_call_sequence),
|
||||
static_cast<unsigned long long>(report.oldest_active_call_thread_id),
|
||||
static_cast<unsigned long long>(report.oldest_active_call_elapsed_ns /
|
||||
1000000ULL));
|
||||
}
|
||||
|
||||
bool PumpEvents() {
|
||||
@@ -2046,10 +2004,9 @@ bool PumpEvents() {
|
||||
g_platform_event_observer.Notify(recovered_release);
|
||||
recovered_right_button_release = true;
|
||||
if (!g_pointer_capture_owner->OnRightButton(
|
||||
false, g_text_input_owner != nullptr &&
|
||||
g_text_input_owner->active())) {
|
||||
fprintf(stderr,
|
||||
" [input] SDL lost-right-release recovery failed\n");
|
||||
false,
|
||||
g_text_input_owner != nullptr && g_text_input_owner->active())) {
|
||||
fprintf(stderr, " [input] SDL lost-right-release recovery failed\n");
|
||||
} else {
|
||||
fprintf(stderr,
|
||||
" [input] recovered missing right-button release from "
|
||||
@@ -2118,6 +2075,16 @@ bool PumpEvents() {
|
||||
dispatch_mouse_motion) {
|
||||
g_platform_event_observer.Notify(platform_event);
|
||||
}
|
||||
if (converted && !fullscreen_shortcut && event.type == SDL_EVENT_KEY_DOWN &&
|
||||
!event.key.repeat &&
|
||||
(event.key.scancode == SDL_SCANCODE_LSHIFT ||
|
||||
event.key.scancode == SDL_SCANCODE_RSHIFT) &&
|
||||
g_pointer_capture_owner != nullptr &&
|
||||
!g_pointer_capture_owner->OnShiftKeyPressed(
|
||||
g_text_input_owner != nullptr && g_text_input_owner->active())) {
|
||||
fprintf(stderr,
|
||||
" [input] SDL shift-lock pointer capture update failed\n");
|
||||
}
|
||||
if ((event.type == SDL_EVENT_MOUSE_BUTTON_DOWN ||
|
||||
event.type == SDL_EVENT_MOUSE_BUTTON_UP) &&
|
||||
event.button.button == SDL_BUTTON_RIGHT &&
|
||||
|
||||
+22
-72
@@ -12,12 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// src/window/window.h — SDL3 window + real EGL context management.
|
||||
//
|
||||
// Provides a real OS window and EGL surface that Roblox can render into.
|
||||
// EGL calls from libroblox.so are intercepted by libegl_stub and forwarded
|
||||
// to the handles created here via thread-local / global state.
|
||||
|
||||
#ifndef MOCKTAIL_WINDOW_WINDOW_H_
|
||||
#define MOCKTAIL_WINDOW_WINDOW_H_
|
||||
|
||||
@@ -38,53 +32,37 @@ namespace window {
|
||||
using PreTextInputPumpCallback = bool (*)(void* context);
|
||||
using FullscreenStateSyncCallback = bool (*)(void* context, bool fullscreen);
|
||||
|
||||
// Configures the XDG-backed state record used to restore the last windowed
|
||||
// geometry, maximized state, and F11 fullscreen state. Must be called before
|
||||
// Init(); readiness runs suppress persistence internally.
|
||||
// Configures persistent geometry and fullscreen state before Init().
|
||||
Status ConfigureWindowStatePersistence(const std::filesystem::path& path);
|
||||
|
||||
// Initialises SDL3, creates the main window and the EGL display.
|
||||
// Must be called before Stage 6 (before nativeAppBridgeV2InitWithParams).
|
||||
// Returns true on success.
|
||||
bool Init(int width, int height, const char* title);
|
||||
|
||||
// Pumps pending SDL events. Call this from a main-loop or keepalive thread.
|
||||
// Returns false when the user closes the window (quit requested).
|
||||
// Returns false after the user requests quit.
|
||||
bool PumpEvents();
|
||||
|
||||
// Paces the next main-thread SDL event drain at 240 Hz. Raw mouse/touch events
|
||||
// remain unsmoothed; this only prevents the host loop from batching them at
|
||||
// the old 60 Hz cadence.
|
||||
// Paces SDL polling at 240 Hz without smoothing raw input.
|
||||
void PaceInputPump();
|
||||
|
||||
// Accepts GameActivity.setWindowFlags from any guest JNI thread and queues the
|
||||
// relevant Android fullscreen state for SDL's next main-thread event pump.
|
||||
// Returns false when the mask contains no supported fullscreen flag.
|
||||
// Queues supported GameActivity fullscreen flags for the SDL thread.
|
||||
bool RequestFullscreenFromAndroidWindowFlags(int flags, int mask);
|
||||
|
||||
// Recognizes the current Android CoreScript's otherwise-unhandled fullscreen
|
||||
// menu request and queues one toggle for the SDL thread.
|
||||
// Converts the Android fullscreen menu request into an SDL-thread toggle.
|
||||
bool RequestFullscreenFromRobloxMenuLog(const char* tag, const char* message);
|
||||
|
||||
// Registers the exact-build adapter that mirrors successful SDL fullscreen
|
||||
// requests into UserGameSettings.InFullScreen().
|
||||
// Mirrors SDL fullscreen changes into UserGameSettings.InFullScreen().
|
||||
bool SetFullscreenStateSyncCallback(FullscreenStateSyncCallback callback,
|
||||
void* context);
|
||||
void ClearFullscreenStateSyncCallback();
|
||||
|
||||
// Returns SDL-derived surface lifetime events from the same polling loop used
|
||||
// by PumpEvents. The initial mapped surface is generation 1 and is available
|
||||
// through GetWindowSurfaceSnapshot without a synthetic Created event.
|
||||
// The initial mapped surface is generation 1 and has no synthetic Created
|
||||
// event.
|
||||
bool PollWindowSurfaceEvent(WindowSurfaceEvent* event);
|
||||
WindowSurfaceSnapshot GetWindowSurfaceSnapshot();
|
||||
|
||||
// Records a compositor-observed event only after the GAME/JNI bridge has
|
||||
// committed it. The default-off resize readiness gate uses this to distinguish
|
||||
// a real rebind from an SDL configure notification that native code rejected.
|
||||
// Records an event only after the GAME/JNI bridge commits it.
|
||||
Status RecordResizeReadinessSurfaceCommit(const WindowSurfaceEvent& event);
|
||||
|
||||
// Completes the opt-in resize gate after Roblox reaches Stopped. Disabled
|
||||
// production runs return OK without adding any work to the hot path.
|
||||
// Completes the optional resize gate after Roblox reaches Stopped.
|
||||
Status StopResizeReadiness();
|
||||
Status ResizeReadinessCompletionStatus();
|
||||
|
||||
@@ -100,81 +78,60 @@ struct WindowViewportSnapshot {
|
||||
}
|
||||
};
|
||||
|
||||
// Input uses logical SDL coordinates while the native surface uses pixels.
|
||||
// Keep both extents explicit so high-DPI touch normalization cannot mix them.
|
||||
// Keep logical input and pixel surface extents separate on high-DPI displays.
|
||||
WindowViewportSnapshot GetWindowViewportSnapshot();
|
||||
|
||||
// Returns the immutable SDL display snapshot captured on the window thread.
|
||||
platform::DisplayRefreshCapabilities GetDisplayRefreshCapabilities();
|
||||
|
||||
// Registers the sole consumer of converted SDL input/focus events.
|
||||
// Surface bookkeeping is internal and does not consume this observer slot.
|
||||
// Registers the sole consumer of converted input and focus events.
|
||||
bool SetPlatformEventObserver(PlatformEventObserver observer, void* context);
|
||||
void ClearPlatformEventObserver();
|
||||
|
||||
// Applies Roblox's exact MouseBehavior.LockCenter state through SDL relative
|
||||
// mouse mode. The callback is queried only from the SDL main thread.
|
||||
// Applies MouseBehavior.LockCenter through SDL relative mouse mode.
|
||||
bool SetMouseLockQueryCallback(MouseLockQueryCallback callback, void* context);
|
||||
void ClearMouseLockQueryCallback();
|
||||
|
||||
// Registers a generic main-thread command drain that runs immediately before
|
||||
// the SDL text-input owner. Clear waits for any invocation already in flight.
|
||||
// Clear waits for a pre-input callback already in flight.
|
||||
bool SetPreTextInputPumpCallback(PreTextInputPumpCallback callback,
|
||||
void* context);
|
||||
void ClearPreTextInputPumpCallback();
|
||||
|
||||
// Guest text callbacks only enqueue state. SDL's main-thread-only text-input
|
||||
// APIs are applied by PumpEvents. The owner remains disabled until the // focused-editor bridge explicitly enables it.
|
||||
// Guest callbacks enqueue state; PumpEvents applies it on SDL's main thread.
|
||||
void SetWindowTextInputOwnerEnabled(bool enabled);
|
||||
bool RequestShowTextInput(uint64_t generation, const TextInputArea& area,
|
||||
const TextInputOptions& options);
|
||||
bool RequestHideTextInput(uint64_t generation);
|
||||
|
||||
// Returns the native EGL display handle (EGLDisplay, opaque void*).
|
||||
void* GetEGLDisplay();
|
||||
|
||||
// Returns the native EGL surface handle (EGLSurface, opaque void*) that
|
||||
// was created from the SDL3 window. This is the surface Roblox renders into.
|
||||
void* GetEGLSurface();
|
||||
|
||||
// Returns the native EGL context handle (EGLContext, opaque void*).
|
||||
void* GetEGLContext();
|
||||
|
||||
// Returns the native EGL config handle (EGLConfig, opaque void*).
|
||||
void* GetEGLConfig();
|
||||
|
||||
// Returns the raw ANativeWindow pointer used as the Android surface.
|
||||
// This is what ANativeWindow_fromSurface() should return.
|
||||
// Raw Android surface returned by ANativeWindow_fromSurface().
|
||||
void* GetNativeWindow();
|
||||
|
||||
// Returns the SDL_Window as an opaque pointer for the Vulkan WSI
|
||||
// adapter. Android guest code must continue to use GetNativeWindow().
|
||||
// Host-only SDL_Window handle for Vulkan WSI.
|
||||
void* GetBackendWindow();
|
||||
|
||||
// True when the window was created with SDL_WINDOW_VULKAN and no EGL context.
|
||||
bool UsesDirectVulkan();
|
||||
|
||||
// Resolves an OpenGL ES function from SDL's currently loaded GL library.
|
||||
// Returns nullptr before Init() or when no real GL backend is available.
|
||||
// Returns nullptr before Init() or without a GL backend.
|
||||
void* GetGLProcAddress(const char* name);
|
||||
|
||||
// Swaps front and back buffers (calls eglSwapBuffers on the real surface).
|
||||
bool SwapBuffers();
|
||||
|
||||
// Returns true once the engine has presented at least one frame.
|
||||
bool HasPresentedFrame();
|
||||
|
||||
// Records a successful host vkQueuePresentKHR. The Vulkan loader adapter is
|
||||
// the only production caller; tests must not fabricate readiness with it.
|
||||
// Records a real vkQueuePresentKHR; tests must not fabricate readiness.
|
||||
void NoteVulkanPresent();
|
||||
|
||||
// Records VK_ERROR_OUT_OF_DATE_KHR without invoking SDL or JNI on the render
|
||||
// thread. PumpEvents coalesces this evidence into surface rebinds.
|
||||
// PumpEvents coalesces render-thread out-of-date reports into surface rebinds.
|
||||
void NoteVulkanSurfaceOutOfDate();
|
||||
|
||||
// Registers one synchronous observer for successful host presentation through
|
||||
// either vkQueuePresentKHR or SDL_GL_SwapWindow. ClearPresentObserver waits for
|
||||
// an in-flight callback, allowing its context to be destroyed safely.
|
||||
// ClearPresentObserver waits for an in-flight host-present callback.
|
||||
bool SetPresentObserver(PresentObserver observer, void* context);
|
||||
void ClearPresentObserver();
|
||||
|
||||
@@ -193,24 +150,17 @@ class ScopedPresentObserver final {
|
||||
bool registered_ = false;
|
||||
};
|
||||
|
||||
// Shows the SDL window if it was created hidden for startup.
|
||||
void ShowIfHidden();
|
||||
|
||||
// Binds the EGL context to the calling thread.
|
||||
// Must be called from the engine rendering thread before any GL calls.
|
||||
// GLdispatch (Mesa) initialises TLS on the first MakeCurrent per thread.
|
||||
// The render thread must bind EGL before GLdispatch initializes its TLS.
|
||||
bool MakeCurrentOnThread();
|
||||
|
||||
// Releases the EGL context from the calling thread.
|
||||
bool ReleaseCurrentOnThread();
|
||||
|
||||
// Destroys the window and shuts down SDL3.
|
||||
void Shutdown();
|
||||
|
||||
// Returns true if Init() was called successfully.
|
||||
bool IsInitialised();
|
||||
|
||||
// Returns current window dimensions.
|
||||
int GetWidth();
|
||||
int GetHeight();
|
||||
|
||||
|
||||
@@ -82,9 +82,8 @@ bool WindowPointerCaptureOwner::Pump(bool text_input_active) {
|
||||
wait_for_native_unlock_after_right_drag_ = false;
|
||||
}
|
||||
const bool client_active = query_ok && focused_;
|
||||
const bool native_capture_active =
|
||||
client_active && native_lock_active &&
|
||||
!wait_for_native_unlock_after_right_drag_;
|
||||
const bool native_capture_active = client_active && native_lock_active &&
|
||||
!wait_for_native_unlock_after_right_drag_;
|
||||
const bool right_drag_active = focused_ && right_button_held_;
|
||||
const bool capture_requested =
|
||||
(right_drag_active || native_capture_active) && !text_input_active;
|
||||
@@ -100,20 +99,31 @@ bool WindowPointerCaptureOwner::OnRightButton(bool pressed,
|
||||
// as Roblox receives RMB down. Remember only a lock observed before the
|
||||
// drag so a stale post-release value cannot capture SDL again.
|
||||
native_lock_was_active_before_right_drag_ =
|
||||
native_lock_observed_ &&
|
||||
!wait_for_native_unlock_after_right_drag_;
|
||||
native_lock_observed_ && !wait_for_native_unlock_after_right_drag_;
|
||||
shift_key_pressed_during_right_drag_ = false;
|
||||
discard_next_motion_after_right_drag_ = false;
|
||||
} else if (!pressed && right_button_held_) {
|
||||
const bool transient_right_drag =
|
||||
!native_lock_was_active_before_right_drag_;
|
||||
const bool native_lock_should_survive_right_drag =
|
||||
native_lock_was_active_before_right_drag_ ||
|
||||
(shift_key_pressed_during_right_drag_ && native_lock_observed_);
|
||||
const bool transient_right_drag = !native_lock_should_survive_right_drag;
|
||||
wait_for_native_unlock_after_right_drag_ = transient_right_drag;
|
||||
discard_next_motion_after_right_drag_ = transient_right_drag;
|
||||
native_lock_was_active_before_right_drag_ = false;
|
||||
shift_key_pressed_during_right_drag_ = false;
|
||||
}
|
||||
right_button_held_ = focused_ && pressed;
|
||||
return Pump(text_input_active);
|
||||
}
|
||||
|
||||
bool WindowPointerCaptureOwner::OnShiftKeyPressed(bool text_input_active) {
|
||||
if (!focused_ || !right_button_held_) {
|
||||
return true;
|
||||
}
|
||||
shift_key_pressed_during_right_drag_ = true;
|
||||
return Pump(text_input_active);
|
||||
}
|
||||
|
||||
bool WindowPointerCaptureOwner::NeedsRightButtonReleaseRecovery(
|
||||
bool observed_pressed) const {
|
||||
return right_button_held_ && !observed_pressed;
|
||||
@@ -135,6 +145,7 @@ bool WindowPointerCaptureOwner::OnFocusLost() {
|
||||
right_button_held_ = false;
|
||||
native_lock_observed_ = false;
|
||||
native_lock_was_active_before_right_drag_ = false;
|
||||
shift_key_pressed_during_right_drag_ = false;
|
||||
wait_for_native_unlock_after_right_drag_ = false;
|
||||
discard_next_motion_after_right_drag_ = false;
|
||||
return Apply(false, true);
|
||||
@@ -146,6 +157,7 @@ bool WindowPointerCaptureOwner::Shutdown() {
|
||||
right_button_held_ = false;
|
||||
native_lock_observed_ = false;
|
||||
native_lock_was_active_before_right_drag_ = false;
|
||||
shift_key_pressed_during_right_drag_ = false;
|
||||
wait_for_native_unlock_after_right_drag_ = false;
|
||||
discard_next_motion_after_right_drag_ = false;
|
||||
return Apply(false, true);
|
||||
@@ -166,11 +178,9 @@ bool WindowPointerCaptureOwner::Apply(bool capture, bool cursor_visible) {
|
||||
captured_ = capture;
|
||||
cursor_visible_ = cursor_visible;
|
||||
if (capture_changed && capture) {
|
||||
std::fprintf(stderr,
|
||||
" [input] SDL relative pointer capture enabled\n");
|
||||
std::fprintf(stderr, " [input] SDL relative pointer capture enabled\n");
|
||||
} else if (capture_changed) {
|
||||
std::fprintf(stderr,
|
||||
" [input] SDL relative pointer capture released\n");
|
||||
std::fprintf(stderr, " [input] SDL relative pointer capture released\n");
|
||||
}
|
||||
if (cursor_changed) {
|
||||
std::fprintf(stderr, " [input] SDL system cursor %s\n",
|
||||
@@ -184,8 +194,7 @@ bool WindowPointerCaptureOwner::Apply(bool capture, bool cursor_visible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SdlPointerCaptureBackend::Apply(bool relative_mode,
|
||||
bool cursor_visible) {
|
||||
bool SdlPointerCaptureBackend::Apply(bool relative_mode, bool cursor_visible) {
|
||||
auto* window = static_cast<SDL_Window*>(window_);
|
||||
if (window == nullptr) {
|
||||
return false;
|
||||
|
||||
@@ -45,6 +45,10 @@ class WindowPointerCaptureOwner final {
|
||||
// Android's lock-center query does not represent desktop RMB camera drag,
|
||||
// so the SDL owner tracks that transient capture source independently.
|
||||
bool OnRightButton(bool pressed, bool text_input_active);
|
||||
// Shift can enable Roblox's persistent mouse lock while a transient RMB
|
||||
// camera drag is active. Refresh the native state after dispatching that
|
||||
// key so the subsequent RMB release preserves the intentional lock.
|
||||
bool OnShiftKeyPressed(bool text_input_active);
|
||||
bool NeedsRightButtonReleaseRecovery(bool observed_pressed) const;
|
||||
// Mirrors Android's captured-pointer listener: the motion that releases a
|
||||
// transient capture is consumed instead of leaking one final camera delta.
|
||||
@@ -70,6 +74,7 @@ class WindowPointerCaptureOwner final {
|
||||
bool right_button_held_ = false;
|
||||
bool native_lock_observed_ = false;
|
||||
bool native_lock_was_active_before_right_drag_ = false;
|
||||
bool shift_key_pressed_during_right_drag_ = false;
|
||||
bool wait_for_native_unlock_after_right_drag_ = false;
|
||||
bool discard_next_motion_after_right_drag_ = false;
|
||||
bool captured_ = false;
|
||||
|
||||
+2
-3
@@ -1,6 +1,5 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
// Apache 2.0 License
|
||||
//
|
||||
// stubs/empty.cc — empty translation unit.
|
||||
// Used as a placeholder source file for stub .so targets that only need
|
||||
// to set SONAME and pull in a real system library via DT_NEEDED.
|
||||
// Placeholder for stub libraries that only set SONAME and add a system
|
||||
// dependency through DT_NEEDED.
|
||||
+23
-26
@@ -1,11 +1,8 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
// Apache 2.0 License
|
||||
//
|
||||
// stubs/libandroid_stub.cc — Stub implementations of Android NDK functions.
|
||||
//
|
||||
// libroblox.so links against libandroid.so which does not exist on Linux.
|
||||
// This stub satisfies the dynamic linker so dlopen() can load libroblox.so.
|
||||
// All functions are no-ops or return safe sentinel values.
|
||||
// Android NDK compatibility symbols used by libroblox.so on Linux. Asset calls
|
||||
// can read from an extracted directory or directly from an APK.
|
||||
//
|
||||
// Symbols sourced from Android NDK r28 android/native_activity.h,
|
||||
// android/asset_manager.h, android/looper.h, android/configuration.h.
|
||||
@@ -30,7 +27,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// AAssetManager / AAsset — read-only asset access
|
||||
// Read-only asset access
|
||||
|
||||
struct AAssetManager {};
|
||||
struct AAsset {
|
||||
@@ -260,7 +257,7 @@ bool MocktailUsesDirectVulkan() {
|
||||
extern "C" {
|
||||
|
||||
AAsset* AAssetManager_open(AAssetManager* mgr, const char* filename,
|
||||
int /*mode*/) {
|
||||
int) {
|
||||
if (mgr == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -418,7 +415,7 @@ int AAsset_openFileDescriptor64(AAsset* asset, off_t* outStart,
|
||||
return AAsset_openFileDescriptor(asset, outStart, outLength);
|
||||
}
|
||||
|
||||
AAssetManager* AAssetManager_fromJava(void* /*env*/, void* /*assetManager*/) {
|
||||
AAssetManager* AAssetManager_fromJava(void*, void*) {
|
||||
return &g_asset_manager;
|
||||
}
|
||||
|
||||
@@ -432,36 +429,36 @@ AConfiguration* AConfiguration_new() {
|
||||
|
||||
void AConfiguration_delete(AConfiguration* config) { free(config); }
|
||||
|
||||
void AConfiguration_fromAssetManager(AConfiguration* /*config*/,
|
||||
AAssetManager* /*am*/) {}
|
||||
void AConfiguration_fromAssetManager(AConfiguration*,
|
||||
AAssetManager*) {}
|
||||
|
||||
void AConfiguration_getLanguage(AConfiguration* /*config*/, char* out) {
|
||||
void AConfiguration_getLanguage(AConfiguration*, char* out) {
|
||||
if (out) {
|
||||
out[0] = 'e';
|
||||
out[1] = 'n';
|
||||
}
|
||||
}
|
||||
|
||||
void AConfiguration_getCountry(AConfiguration* /*config*/, char* out) {
|
||||
void AConfiguration_getCountry(AConfiguration*, char* out) {
|
||||
if (out) {
|
||||
out[0] = 'U';
|
||||
out[1] = 'S';
|
||||
}
|
||||
}
|
||||
|
||||
int32_t AConfiguration_getScreenSize(AConfiguration* /*config*/) {
|
||||
int32_t AConfiguration_getScreenSize(AConfiguration*) {
|
||||
return 2; // ACONFIGURATION_SCREENSIZE_NORMAL
|
||||
}
|
||||
|
||||
int32_t AConfiguration_getScreenWidthDp(AConfiguration* /*config*/) {
|
||||
int32_t AConfiguration_getScreenWidthDp(AConfiguration*) {
|
||||
return 360;
|
||||
}
|
||||
|
||||
int32_t AConfiguration_getScreenHeightDp(AConfiguration* /*config*/) {
|
||||
int32_t AConfiguration_getScreenHeightDp(AConfiguration*) {
|
||||
return 640;
|
||||
}
|
||||
|
||||
int32_t AConfiguration_getNavHidden(AConfiguration* /*config*/) {
|
||||
int32_t AConfiguration_getNavHidden(AConfiguration*) {
|
||||
return 1; // ACONFIGURATION_NAVHIDDEN_YES
|
||||
}
|
||||
|
||||
@@ -469,7 +466,7 @@ int32_t AConfiguration_getNavHidden(AConfiguration* /*config*/) {
|
||||
|
||||
struct ALooper {};
|
||||
|
||||
ALooper* ALooper_prepare(int /*opts*/) {
|
||||
ALooper* ALooper_prepare(int) {
|
||||
static ALooper kLooper;
|
||||
return &kLooper;
|
||||
}
|
||||
@@ -479,18 +476,18 @@ ALooper* ALooper_forThread() {
|
||||
return &kLooper;
|
||||
}
|
||||
|
||||
void ALooper_acquire(ALooper* /*looper*/) {}
|
||||
void ALooper_release(ALooper* /*looper*/) {}
|
||||
void ALooper_acquire(ALooper*) {}
|
||||
void ALooper_release(ALooper*) {}
|
||||
|
||||
int ALooper_addFd(ALooper* /*looper*/, int /*fd*/, int /*ident*/,
|
||||
int /*events*/, void* /*callback*/, void* /*data*/) {
|
||||
int ALooper_addFd(ALooper*, int, int,
|
||||
int, void*, void*) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int ALooper_removeFd(ALooper* /*looper*/, int /*fd*/) { return 1; }
|
||||
int ALooper_removeFd(ALooper*, int) { return 1; }
|
||||
|
||||
int ALooper_pollOnce(int /*timeoutMillis*/, int* /*outFd*/, int* /*outEvents*/,
|
||||
void** /*outData*/) {
|
||||
int ALooper_pollOnce(int, int*, int*,
|
||||
void**) {
|
||||
return -2; // ALOOPER_POLL_TIMEOUT
|
||||
}
|
||||
|
||||
@@ -498,8 +495,8 @@ int ALooper_pollOnce(int /*timeoutMillis*/, int* /*outFd*/, int* /*outEvents*/,
|
||||
|
||||
struct ANativeWindow {};
|
||||
|
||||
void ANativeWindow_acquire(ANativeWindow* /*window*/) {}
|
||||
void ANativeWindow_release(ANativeWindow* /*window*/) {}
|
||||
void ANativeWindow_acquire(ANativeWindow*) {}
|
||||
void ANativeWindow_release(ANativeWindow*) {}
|
||||
|
||||
ANativeWindow* ANativeWindow_fromSurface(void* env, void* surface) {
|
||||
void* native_window = MocktailNativeWindow();
|
||||
|
||||
@@ -220,7 +220,7 @@ void SetError(EGLint error) {
|
||||
|
||||
extern "C" {
|
||||
|
||||
EGLDisplay eglGetDisplay(EGLNativeDisplayType /*display_id*/) {
|
||||
EGLDisplay eglGetDisplay(EGLNativeDisplayType) {
|
||||
EGLDisplay display = ActiveDisplay();
|
||||
SetError(display != nullptr ? kEglSuccess : kEglNotInitialized);
|
||||
return display;
|
||||
@@ -250,7 +250,7 @@ EGLBoolean eglBindAPI(EGLenum api) {
|
||||
return kEglTrue;
|
||||
}
|
||||
|
||||
EGLBoolean eglChooseConfig(EGLDisplay display, const EGLint* /*attrib_list*/,
|
||||
EGLBoolean eglChooseConfig(EGLDisplay display, const EGLint*,
|
||||
EGLConfig* configs, EGLint config_size,
|
||||
EGLint* num_config) {
|
||||
if (!IsActiveDisplay(display)) {
|
||||
@@ -292,7 +292,7 @@ EGLBoolean eglGetConfigAttrib(EGLDisplay display, EGLConfig config,
|
||||
|
||||
EGLSurface eglCreateWindowSurface(EGLDisplay display, EGLConfig config,
|
||||
EGLNativeWindowType native_window,
|
||||
const EGLint* /*attrib_list*/) {
|
||||
const EGLint*) {
|
||||
if (!IsActiveDisplay(display) || config == nullptr ||
|
||||
config != ActiveConfig()) {
|
||||
SetError(kEglBadDisplay);
|
||||
@@ -314,7 +314,7 @@ EGLSurface eglCreateWindowSurface(EGLDisplay display, EGLConfig config,
|
||||
}
|
||||
|
||||
EGLSurface eglCreatePbufferSurface(EGLDisplay display, EGLConfig config,
|
||||
const EGLint* /*attrib_list*/) {
|
||||
const EGLint*) {
|
||||
if (!IsActiveDisplay(display) || config == nullptr ||
|
||||
config != ActiveConfig()) {
|
||||
SetError(kEglBadDisplay);
|
||||
@@ -329,8 +329,8 @@ EGLSurface eglCreatePbufferSurface(EGLDisplay display, EGLConfig config,
|
||||
}
|
||||
|
||||
EGLContext eglCreateContext(EGLDisplay display, EGLConfig config,
|
||||
EGLContext /*share_context*/,
|
||||
const EGLint* /*attrib_list*/) {
|
||||
EGLContext,
|
||||
const EGLint*) {
|
||||
if (!IsActiveDisplay(display) || config == nullptr ||
|
||||
config != ActiveConfig()) {
|
||||
SetError(kEglBadDisplay);
|
||||
@@ -427,7 +427,7 @@ EGLBoolean eglSwapBuffers(EGLDisplay display, EGLSurface surface) {
|
||||
return swapped ? kEglTrue : kEglFalse;
|
||||
}
|
||||
|
||||
EGLBoolean eglSwapInterval(EGLDisplay display, EGLint /*interval*/) {
|
||||
EGLBoolean eglSwapInterval(EGLDisplay display, EGLint) {
|
||||
if (!IsActiveDisplay(display) || g_current_context == nullptr) {
|
||||
SetError(kEglBadDisplay);
|
||||
return kEglFalse;
|
||||
|
||||
+96
-96
@@ -176,10 +176,10 @@ void glGenRenderbuffers(GLsizei n, GLuint* renderbuffers) {
|
||||
FillObjects(n, renderbuffers);
|
||||
}
|
||||
|
||||
GLuint glCreateShader(GLenum /*type*/) { return NextObject(); }
|
||||
GLuint glCreateShader(GLenum) { return NextObject(); }
|
||||
GLuint glCreateProgram() { return NextObject(); }
|
||||
|
||||
void glGetShaderiv(GLuint /*shader*/, GLenum pname, GLint* params) {
|
||||
void glGetShaderiv(GLuint, GLenum pname, GLint* params) {
|
||||
if (params == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -196,7 +196,7 @@ void glGetShaderiv(GLuint /*shader*/, GLenum pname, GLint* params) {
|
||||
}
|
||||
}
|
||||
|
||||
void glGetProgramiv(GLuint /*program*/, GLenum pname, GLint* params) {
|
||||
void glGetProgramiv(GLuint, GLenum pname, GLint* params) {
|
||||
if (params == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -213,7 +213,7 @@ void glGetProgramiv(GLuint /*program*/, GLenum pname, GLint* params) {
|
||||
}
|
||||
}
|
||||
|
||||
void glGetShaderInfoLog(GLuint /*shader*/, GLsizei buf_size, GLsizei* length,
|
||||
void glGetShaderInfoLog(GLuint, GLsizei buf_size, GLsizei* length,
|
||||
GLchar* info_log) {
|
||||
if (!RequireTestGraphicsStub()) {
|
||||
CopyString("real GLES backend unavailable", buf_size, length, info_log);
|
||||
@@ -222,7 +222,7 @@ void glGetShaderInfoLog(GLuint /*shader*/, GLsizei buf_size, GLsizei* length,
|
||||
CopyString("", buf_size, length, info_log);
|
||||
}
|
||||
|
||||
void glGetProgramInfoLog(GLuint /*program*/, GLsizei buf_size, GLsizei* length,
|
||||
void glGetProgramInfoLog(GLuint, GLsizei buf_size, GLsizei* length,
|
||||
GLchar* info_log) {
|
||||
if (!RequireTestGraphicsStub()) {
|
||||
CopyString("real GLES backend unavailable", buf_size, length, info_log);
|
||||
@@ -231,7 +231,7 @@ void glGetProgramInfoLog(GLuint /*program*/, GLsizei buf_size, GLsizei* length,
|
||||
CopyString("", buf_size, length, info_log);
|
||||
}
|
||||
|
||||
void glGetActiveUniform(GLuint /*program*/, GLuint /*index*/, GLsizei buf_size,
|
||||
void glGetActiveUniform(GLuint, GLuint, GLsizei buf_size,
|
||||
GLsizei* length, GLint* size, GLenum* type,
|
||||
GLchar* name) {
|
||||
if (!RequireTestGraphicsStub()) {
|
||||
@@ -253,11 +253,11 @@ void glGetActiveUniform(GLuint /*program*/, GLuint /*index*/, GLsizei buf_size,
|
||||
CopyString("", buf_size, length, name);
|
||||
}
|
||||
|
||||
GLint glGetUniformLocation(GLuint /*program*/, const GLchar* /*name*/) {
|
||||
GLint glGetUniformLocation(GLuint, const GLchar*) {
|
||||
return RequireTestGraphicsStub() ? 0 : -1;
|
||||
}
|
||||
|
||||
GLenum glCheckFramebufferStatus(GLenum /*target*/) {
|
||||
GLenum glCheckFramebufferStatus(GLenum) {
|
||||
return RequireTestGraphicsStub() ? kGlFramebufferComplete
|
||||
: kGlFramebufferUnsupported;
|
||||
}
|
||||
@@ -272,119 +272,119 @@ void glViewport(GLint x, GLint y, GLsizei width, GLsizei height) {
|
||||
g_viewport[3] = height;
|
||||
}
|
||||
|
||||
void glActiveTexture(GLenum /*texture*/) { RequireTestGraphicsStub(); }
|
||||
void glAttachShader(GLuint /*program*/, GLuint /*shader*/) {
|
||||
void glActiveTexture(GLenum) { RequireTestGraphicsStub(); }
|
||||
void glAttachShader(GLuint, GLuint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glBindAttribLocation(GLuint /*program*/, GLuint /*index*/,
|
||||
const GLchar* /*name*/) {
|
||||
void glBindAttribLocation(GLuint, GLuint,
|
||||
const GLchar*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glBindBuffer(GLenum /*target*/, GLuint /*buffer*/) {
|
||||
void glBindBuffer(GLenum, GLuint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glBindFramebuffer(GLenum /*target*/, GLuint /*framebuffer*/) {
|
||||
void glBindFramebuffer(GLenum, GLuint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glBindRenderbuffer(GLenum /*target*/, GLuint /*renderbuffer*/) {
|
||||
void glBindRenderbuffer(GLenum, GLuint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glBindTexture(GLenum /*target*/, GLuint /*texture*/) {
|
||||
void glBindTexture(GLenum, GLuint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glBlendFunc(GLenum /*sfactor*/, GLenum /*dfactor*/) {
|
||||
void glBlendFunc(GLenum, GLenum) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glBlendFuncSeparate(GLenum /*src_rgb*/, GLenum /*dst_rgb*/,
|
||||
GLenum /*src_alpha*/, GLenum /*dst_alpha*/) {
|
||||
void glBlendFuncSeparate(GLenum, GLenum,
|
||||
GLenum, GLenum) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glBufferData(GLenum /*target*/, GLsizeiptr /*size*/, const void* /*data*/,
|
||||
GLenum /*usage*/) {
|
||||
void glBufferData(GLenum, GLsizeiptr, const void*,
|
||||
GLenum) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glBufferSubData(GLenum /*target*/, GLintptr /*offset*/,
|
||||
GLsizeiptr /*size*/, const void* /*data*/) {
|
||||
void glBufferSubData(GLenum, GLintptr,
|
||||
GLsizeiptr, const void*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glClear(GLbitfield /*mask*/) { RequireTestGraphicsStub(); }
|
||||
void glClearColor(GLfloat /*red*/, GLfloat /*green*/, GLfloat /*blue*/,
|
||||
GLfloat /*alpha*/) {
|
||||
void glClear(GLbitfield) { RequireTestGraphicsStub(); }
|
||||
void glClearColor(GLfloat, GLfloat, GLfloat,
|
||||
GLfloat) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glClearDepthf(GLfloat /*depth*/) { RequireTestGraphicsStub(); }
|
||||
void glClearStencil(GLint /*s*/) { RequireTestGraphicsStub(); }
|
||||
void glColorMask(GLboolean /*red*/, GLboolean /*green*/, GLboolean /*blue*/,
|
||||
GLboolean /*alpha*/) {
|
||||
void glClearDepthf(GLfloat) { RequireTestGraphicsStub(); }
|
||||
void glClearStencil(GLint) { RequireTestGraphicsStub(); }
|
||||
void glColorMask(GLboolean, GLboolean, GLboolean,
|
||||
GLboolean) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glCompileShader(GLuint /*shader*/) { RequireTestGraphicsStub(); }
|
||||
void glCompressedTexImage2D(GLenum /*target*/, GLint /*level*/,
|
||||
GLenum /*internalformat*/, GLsizei /*width*/,
|
||||
GLsizei /*height*/, GLint /*border*/,
|
||||
GLsizei /*image_size*/, const void* /*data*/) {
|
||||
void glCompileShader(GLuint) { RequireTestGraphicsStub(); }
|
||||
void glCompressedTexImage2D(GLenum, GLint,
|
||||
GLenum, GLsizei,
|
||||
GLsizei, GLint,
|
||||
GLsizei, const void*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glCompressedTexSubImage2D(GLenum /*target*/, GLint /*level*/,
|
||||
GLint /*xoffset*/, GLint /*yoffset*/,
|
||||
GLsizei /*width*/, GLsizei /*height*/,
|
||||
GLenum /*format*/, GLsizei /*image_size*/,
|
||||
const void* /*data*/) {
|
||||
void glCompressedTexSubImage2D(GLenum, GLint,
|
||||
GLint, GLint,
|
||||
GLsizei, GLsizei,
|
||||
GLenum, GLsizei,
|
||||
const void*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glCopyTexSubImage2D(GLenum /*target*/, GLint /*level*/, GLint /*xoffset*/,
|
||||
GLint /*yoffset*/, GLint /*x*/, GLint /*y*/,
|
||||
GLsizei /*width*/, GLsizei /*height*/) {
|
||||
void glCopyTexSubImage2D(GLenum, GLint, GLint,
|
||||
GLint, GLint, GLint,
|
||||
GLsizei, GLsizei) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glCullFace(GLenum /*mode*/) { RequireTestGraphicsStub(); }
|
||||
void glDeleteBuffers(GLsizei /*n*/, const GLuint* /*buffers*/) {
|
||||
void glCullFace(GLenum) { RequireTestGraphicsStub(); }
|
||||
void glDeleteBuffers(GLsizei, const GLuint*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glDeleteFramebuffers(GLsizei /*n*/, const GLuint* /*framebuffers*/) {
|
||||
void glDeleteFramebuffers(GLsizei, const GLuint*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glDeleteProgram(GLuint /*program*/) { RequireTestGraphicsStub(); }
|
||||
void glDeleteRenderbuffers(GLsizei /*n*/, const GLuint* /*renderbuffers*/) {
|
||||
void glDeleteProgram(GLuint) { RequireTestGraphicsStub(); }
|
||||
void glDeleteRenderbuffers(GLsizei, const GLuint*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glDeleteShader(GLuint /*shader*/) { RequireTestGraphicsStub(); }
|
||||
void glDeleteTextures(GLsizei /*n*/, const GLuint* /*textures*/) {
|
||||
void glDeleteShader(GLuint) { RequireTestGraphicsStub(); }
|
||||
void glDeleteTextures(GLsizei, const GLuint*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glDepthFunc(GLenum /*func*/) { RequireTestGraphicsStub(); }
|
||||
void glDepthMask(GLboolean /*flag*/) { RequireTestGraphicsStub(); }
|
||||
void glDisable(GLenum /*cap*/) { RequireTestGraphicsStub(); }
|
||||
void glDisableVertexAttribArray(GLuint /*index*/) { RequireTestGraphicsStub(); }
|
||||
void glDrawArrays(GLenum /*mode*/, GLint /*first*/, GLsizei /*count*/) {
|
||||
void glDepthFunc(GLenum) { RequireTestGraphicsStub(); }
|
||||
void glDepthMask(GLboolean) { RequireTestGraphicsStub(); }
|
||||
void glDisable(GLenum) { RequireTestGraphicsStub(); }
|
||||
void glDisableVertexAttribArray(GLuint) { RequireTestGraphicsStub(); }
|
||||
void glDrawArrays(GLenum, GLint, GLsizei) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glDrawElements(GLenum /*mode*/, GLsizei /*count*/, GLenum /*type*/,
|
||||
const void* /*indices*/) {
|
||||
void glDrawElements(GLenum, GLsizei, GLenum,
|
||||
const void*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glEnable(GLenum /*cap*/) { RequireTestGraphicsStub(); }
|
||||
void glEnableVertexAttribArray(GLuint /*index*/) { RequireTestGraphicsStub(); }
|
||||
void glFramebufferRenderbuffer(GLenum /*target*/, GLenum /*attachment*/,
|
||||
GLenum /*renderbuffertarget*/,
|
||||
GLuint /*renderbuffer*/) {
|
||||
void glEnable(GLenum) { RequireTestGraphicsStub(); }
|
||||
void glEnableVertexAttribArray(GLuint) { RequireTestGraphicsStub(); }
|
||||
void glFramebufferRenderbuffer(GLenum, GLenum,
|
||||
GLenum,
|
||||
GLuint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glFramebufferTexture2D(GLenum /*target*/, GLenum /*attachment*/,
|
||||
GLenum /*textarget*/, GLuint /*texture*/,
|
||||
GLint /*level*/) {
|
||||
void glFramebufferTexture2D(GLenum, GLenum,
|
||||
GLenum, GLuint,
|
||||
GLint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glGenerateMipmap(GLenum /*target*/) { RequireTestGraphicsStub(); }
|
||||
void glLinkProgram(GLuint /*program*/) { RequireTestGraphicsStub(); }
|
||||
void glPixelStorei(GLenum /*pname*/, GLint /*param*/) {
|
||||
void glGenerateMipmap(GLenum) { RequireTestGraphicsStub(); }
|
||||
void glLinkProgram(GLuint) { RequireTestGraphicsStub(); }
|
||||
void glPixelStorei(GLenum, GLint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glPolygonOffset(GLfloat /*factor*/, GLfloat /*units*/) {
|
||||
void glPolygonOffset(GLfloat, GLfloat) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glReadPixels(GLint /*x*/, GLint /*y*/, GLsizei width, GLsizei height,
|
||||
GLenum /*format*/, GLenum /*type*/, void* pixels) {
|
||||
void glReadPixels(GLint, GLint, GLsizei width, GLsizei height,
|
||||
GLenum, GLenum, void* pixels) {
|
||||
if (!RequireTestGraphicsStub()) {
|
||||
return;
|
||||
}
|
||||
@@ -394,53 +394,53 @@ void glReadPixels(GLint /*x*/, GLint /*y*/, GLsizei width, GLsizei height,
|
||||
}
|
||||
}
|
||||
void glReleaseShaderCompiler() { RequireTestGraphicsStub(); }
|
||||
void glRenderbufferStorage(GLenum /*target*/, GLenum /*internalformat*/,
|
||||
GLsizei /*width*/, GLsizei /*height*/) {
|
||||
void glRenderbufferStorage(GLenum, GLenum,
|
||||
GLsizei, GLsizei) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glScissor(GLint /*x*/, GLint /*y*/, GLsizei /*width*/,
|
||||
GLsizei /*height*/) {
|
||||
void glScissor(GLint, GLint, GLsizei,
|
||||
GLsizei) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glShaderSource(GLuint /*shader*/, GLsizei /*count*/,
|
||||
const GLchar* const* /*string*/, const GLint* /*length*/) {
|
||||
void glShaderSource(GLuint, GLsizei,
|
||||
const GLchar* const*, const GLint*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glStencilFunc(GLenum /*func*/, GLint /*ref*/, GLuint /*mask*/) {
|
||||
void glStencilFunc(GLenum, GLint, GLuint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glStencilMask(GLuint /*mask*/) { RequireTestGraphicsStub(); }
|
||||
void glStencilOp(GLenum /*fail*/, GLenum /*zfail*/, GLenum /*zpass*/) {
|
||||
void glStencilMask(GLuint) { RequireTestGraphicsStub(); }
|
||||
void glStencilOp(GLenum, GLenum, GLenum) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glTexImage2D(GLenum /*target*/, GLint /*level*/, GLint /*internalformat*/,
|
||||
GLsizei /*width*/, GLsizei /*height*/, GLint /*border*/,
|
||||
GLenum /*format*/, GLenum /*type*/, const void* /*pixels*/) {
|
||||
void glTexImage2D(GLenum, GLint, GLint,
|
||||
GLsizei, GLsizei, GLint,
|
||||
GLenum, GLenum, const void*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glTexParameterf(GLenum /*target*/, GLenum /*pname*/, GLfloat /*param*/) {
|
||||
void glTexParameterf(GLenum, GLenum, GLfloat) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glTexParameterfv(GLenum /*target*/, GLenum /*pname*/,
|
||||
const GLfloat* /*params*/) {
|
||||
void glTexParameterfv(GLenum, GLenum,
|
||||
const GLfloat*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glTexParameteri(GLenum /*target*/, GLenum /*pname*/, GLint /*param*/) {
|
||||
void glTexParameteri(GLenum, GLenum, GLint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glTexSubImage2D(GLenum /*target*/, GLint /*level*/, GLint /*xoffset*/,
|
||||
GLint /*yoffset*/, GLsizei /*width*/, GLsizei /*height*/,
|
||||
GLenum /*format*/, GLenum /*type*/,
|
||||
const void* /*pixels*/) {
|
||||
void glTexSubImage2D(GLenum, GLint, GLint,
|
||||
GLint, GLsizei, GLsizei,
|
||||
GLenum, GLenum,
|
||||
const void*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glUniform1i(GLint /*location*/, GLint /*v0*/) {
|
||||
void glUniform1i(GLint, GLint) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
void glUseProgram(GLuint /*program*/) { RequireTestGraphicsStub(); }
|
||||
void glVertexAttribPointer(GLuint /*index*/, GLint /*size*/, GLenum /*type*/,
|
||||
GLboolean /*normalized*/, GLsizei /*stride*/,
|
||||
const void* /*pointer*/) {
|
||||
void glUseProgram(GLuint) { RequireTestGraphicsStub(); }
|
||||
void glVertexAttribPointer(GLuint, GLint, GLenum,
|
||||
GLboolean, GLsizei,
|
||||
const void*) {
|
||||
RequireTestGraphicsStub();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
// Apache 2.0 License
|
||||
//
|
||||
// stubs/liblog_stub.cc — Stub for Android liblog.so.
|
||||
// Redirects __android_log_print et al. to stderr on the host.
|
||||
// Redirects Android logging calls to stderr on the host.
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdarg>
|
||||
@@ -95,7 +94,7 @@ void __android_log_assert(const char* cond, const char* tag,
|
||||
__builtin_trap();
|
||||
}
|
||||
|
||||
int __android_log_buf_write(int /*bufID*/, int prio, const char* tag,
|
||||
int __android_log_buf_write(int, int prio, const char* tag,
|
||||
const char* text) {
|
||||
return __android_log_write(prio, tag, text);
|
||||
}
|
||||
|
||||
+39
-43
@@ -1,8 +1,7 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
// Apache 2.0 License
|
||||
//
|
||||
// stubs/libmediandk_stub.cc — Stub for Android libmediandk.so.
|
||||
// AMediaCodec / AMediaFormat are no-ops; Roblox uses them for video decoding.
|
||||
// Minimal libmediandk shim for video APIs imported by Roblox.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
@@ -14,7 +13,6 @@ struct AMediaFormat {
|
||||
char mime[64];
|
||||
};
|
||||
|
||||
// Forward-declared Android media status type.
|
||||
using media_status_t = int32_t;
|
||||
static constexpr media_status_t AMEDIA_OK = 0;
|
||||
static constexpr media_status_t AMEDIA_ERROR_UNKNOWN = -1;
|
||||
@@ -33,71 +31,69 @@ const char* AMEDIAFORMAT_KEY_SAMPLE_RATE = "sample-rate";
|
||||
const char* AMEDIAFORMAT_KEY_I_FRAME_INTERVAL = "i-frame-interval";
|
||||
const char* AMEDIAFORMAT_KEY_STRIDE = "stride";
|
||||
|
||||
// AMediaFormat
|
||||
AMediaFormat* AMediaFormat_new() {
|
||||
return static_cast<AMediaFormat*>(calloc(1, sizeof(AMediaFormat)));
|
||||
}
|
||||
void AMediaFormat_delete(AMediaFormat* fmt) { free(fmt); }
|
||||
bool AMediaFormat_getInt32(AMediaFormat* /*f*/, const char* /*k*/, int32_t* v) {
|
||||
bool AMediaFormat_getInt32(AMediaFormat*, const char*, int32_t* v) {
|
||||
if (v) *v = 0;
|
||||
return false;
|
||||
}
|
||||
bool AMediaFormat_getBuffer(AMediaFormat* /*f*/, const char* /*k*/,
|
||||
void** /*d*/, size_t* /*s*/) { return false; }
|
||||
void AMediaFormat_setInt32(AMediaFormat* /*f*/, const char* /*k*/,
|
||||
int32_t /*v*/) {}
|
||||
void AMediaFormat_setFloat(AMediaFormat* /*f*/, const char* /*k*/,
|
||||
float /*v*/) {}
|
||||
void AMediaFormat_setString(AMediaFormat* /*f*/, const char* k,
|
||||
bool AMediaFormat_getBuffer(AMediaFormat*, const char*,
|
||||
void**, size_t*) { return false; }
|
||||
void AMediaFormat_setInt32(AMediaFormat*, const char*,
|
||||
int32_t) {}
|
||||
void AMediaFormat_setFloat(AMediaFormat*, const char*,
|
||||
float) {}
|
||||
void AMediaFormat_setString(AMediaFormat*, const char* k,
|
||||
const char* v) {
|
||||
(void)k; (void)v;
|
||||
}
|
||||
void AMediaFormat_setBuffer(AMediaFormat* /*f*/, const char* /*k*/,
|
||||
const void* /*d*/, size_t /*s*/) {}
|
||||
const char* AMediaFormat_toString(AMediaFormat* /*f*/) { return "{}"; }
|
||||
void AMediaFormat_setBuffer(AMediaFormat*, const char*,
|
||||
const void*, size_t) {}
|
||||
const char* AMediaFormat_toString(AMediaFormat*) { return "{}"; }
|
||||
|
||||
// AMediaCodec
|
||||
AMediaCodec* AMediaCodec_createDecoderByType(const char* /*mime*/) {
|
||||
AMediaCodec* AMediaCodec_createDecoderByType(const char*) {
|
||||
return nullptr;
|
||||
}
|
||||
AMediaCodec* AMediaCodec_createEncoderByType(const char* /*mime*/) {
|
||||
AMediaCodec* AMediaCodec_createEncoderByType(const char*) {
|
||||
return nullptr;
|
||||
}
|
||||
media_status_t AMediaCodec_delete(AMediaCodec* /*codec*/) {
|
||||
media_status_t AMediaCodec_delete(AMediaCodec*) {
|
||||
return AMEDIA_OK;
|
||||
}
|
||||
media_status_t AMediaCodec_configure(AMediaCodec* /*codec*/,
|
||||
const AMediaFormat* /*format*/,
|
||||
void* /*surface*/, void* /*crypto*/,
|
||||
uint32_t /*flags*/) {
|
||||
media_status_t AMediaCodec_configure(AMediaCodec*,
|
||||
const AMediaFormat*,
|
||||
void*, void*,
|
||||
uint32_t) {
|
||||
return AMEDIA_ERROR_UNKNOWN;
|
||||
}
|
||||
media_status_t AMediaCodec_start(AMediaCodec* /*codec*/) {
|
||||
media_status_t AMediaCodec_start(AMediaCodec*) {
|
||||
return AMEDIA_ERROR_UNKNOWN;
|
||||
}
|
||||
media_status_t AMediaCodec_stop(AMediaCodec* /*codec*/) { return AMEDIA_OK; }
|
||||
media_status_t AMediaCodec_flush(AMediaCodec* /*codec*/) { return AMEDIA_OK; }
|
||||
uint8_t* AMediaCodec_getInputBuffer(AMediaCodec* /*codec*/, size_t /*idx*/,
|
||||
size_t* /*sz*/) { return nullptr; }
|
||||
uint8_t* AMediaCodec_getOutputBuffer(AMediaCodec* /*codec*/, size_t /*idx*/,
|
||||
size_t* /*sz*/) { return nullptr; }
|
||||
ssize_t AMediaCodec_dequeueInputBuffer(AMediaCodec* /*codec*/,
|
||||
int64_t /*timeoutUs*/) { return -1; }
|
||||
ssize_t AMediaCodec_dequeueOutputBuffer(AMediaCodec* /*codec*/,
|
||||
void* /*info*/,
|
||||
int64_t /*timeoutUs*/) { return -1; }
|
||||
media_status_t AMediaCodec_queueInputBuffer(AMediaCodec* /*codec*/,
|
||||
size_t /*idx*/, size_t /*offset*/,
|
||||
size_t /*size*/, uint64_t /*pts*/,
|
||||
uint32_t /*flags*/) {
|
||||
media_status_t AMediaCodec_stop(AMediaCodec*) { return AMEDIA_OK; }
|
||||
media_status_t AMediaCodec_flush(AMediaCodec*) { return AMEDIA_OK; }
|
||||
uint8_t* AMediaCodec_getInputBuffer(AMediaCodec*, size_t,
|
||||
size_t*) { return nullptr; }
|
||||
uint8_t* AMediaCodec_getOutputBuffer(AMediaCodec*, size_t,
|
||||
size_t*) { return nullptr; }
|
||||
ssize_t AMediaCodec_dequeueInputBuffer(AMediaCodec*,
|
||||
int64_t) { return -1; }
|
||||
ssize_t AMediaCodec_dequeueOutputBuffer(AMediaCodec*,
|
||||
void*,
|
||||
int64_t) { return -1; }
|
||||
media_status_t AMediaCodec_queueInputBuffer(AMediaCodec*,
|
||||
size_t, size_t,
|
||||
size_t, uint64_t,
|
||||
uint32_t) {
|
||||
return AMEDIA_ERROR_UNKNOWN;
|
||||
}
|
||||
media_status_t AMediaCodec_releaseOutputBuffer(AMediaCodec* /*codec*/,
|
||||
size_t /*idx*/,
|
||||
bool /*render*/) {
|
||||
media_status_t AMediaCodec_releaseOutputBuffer(AMediaCodec*,
|
||||
size_t,
|
||||
bool) {
|
||||
return AMEDIA_OK;
|
||||
}
|
||||
AMediaFormat* AMediaCodec_getOutputFormat(AMediaCodec* /*codec*/) {
|
||||
AMediaFormat* AMediaCodec_getOutputFormat(AMediaCodec*) {
|
||||
return AMediaFormat_new();
|
||||
}
|
||||
|
||||
|
||||
+165
-171
@@ -1,12 +1,8 @@
|
||||
// Copyright 2026 Mocktail Project Authors
|
||||
// Apache 2.0 License
|
||||
//
|
||||
// stubs/libopensl_stub.cc — test fixture for libOpenSLES.so/libOpenMAXAL.so.
|
||||
//
|
||||
// Production audio must use the SDL-backed Mocktail audio foundation. This
|
||||
// synthetic object graph is available only when the exact opt-in
|
||||
// MOCKTAIL_ENABLE_TEST_AUDIO_STUBS=1 is present; otherwise engine creation
|
||||
// fails closed. Audio capture is unsupported in both modes.
|
||||
// Test fixture for libOpenSLES.so and libOpenMAXAL.so. Engine creation requires
|
||||
// MOCKTAIL_ENABLE_TEST_AUDIO_STUBS=1; audio capture is not implemented.
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
@@ -239,37 +235,37 @@ SLresult SuccessObject(SLObjectItf* object, SLObjectItf fake_object) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
void ObjectDestroy(SLObjectItf /*self*/) {}
|
||||
void ObjectDestroy(SLObjectItf) {}
|
||||
|
||||
SLresult ObjectRealize(SLObjectItf /*self*/, SLboolean /*async*/) {
|
||||
SLresult ObjectRealize(SLObjectItf, SLboolean) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult ObjectResume(SLObjectItf /*self*/, SLboolean /*async*/) {
|
||||
SLresult ObjectResume(SLObjectItf, SLboolean) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult ObjectGetState(SLObjectItf /*self*/, SLuint32* state) {
|
||||
SLresult ObjectGetState(SLObjectItf, SLuint32* state) {
|
||||
if (state) {
|
||||
*state = SL_OBJECT_STATE_REALIZED;
|
||||
}
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult ObjectRegisterCallback(SLObjectItf /*self*/,
|
||||
SLObjectCallback /*callback*/,
|
||||
void* /*context*/) {
|
||||
SLresult ObjectRegisterCallback(SLObjectItf,
|
||||
SLObjectCallback,
|
||||
void*) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
void ObjectAbortAsyncOperation(SLObjectItf /*self*/) {}
|
||||
void ObjectAbortAsyncOperation(SLObjectItf) {}
|
||||
|
||||
SLresult ObjectSetPriority(SLObjectItf /*self*/, SLint32 /*priority*/,
|
||||
SLboolean /*preemptable*/) {
|
||||
SLresult ObjectSetPriority(SLObjectItf, SLint32,
|
||||
SLboolean) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult ObjectGetPriority(SLObjectItf /*self*/, SLint32* priority,
|
||||
SLresult ObjectGetPriority(SLObjectItf, SLint32* priority,
|
||||
SLboolean* preemptable) {
|
||||
if (priority) {
|
||||
*priority = 0;
|
||||
@@ -280,20 +276,20 @@ SLresult ObjectGetPriority(SLObjectItf /*self*/, SLint32* priority,
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult ObjectSetLossOfControlInterfaces(SLObjectItf /*self*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
SLboolean /*enabled*/) {
|
||||
SLresult ObjectSetLossOfControlInterfaces(SLObjectItf,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
SLboolean) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult ObjectGetInterface(SLObjectItf self, SLInterfaceID iid,
|
||||
void* interface_out);
|
||||
SLresult CreateUnsupportedObject(SLEngineItf /*self*/, SLObjectItf* object,
|
||||
SLuint32 /*device_id*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/);
|
||||
SLresult CreateUnsupportedObject(SLEngineItf, SLObjectItf* object,
|
||||
SLuint32,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*);
|
||||
SLresult CreateAudioPlayer(SLEngineItf self, SLObjectItf* player,
|
||||
const void* source, const void* sink,
|
||||
SLuint32 num_interfaces, const SLInterfaceID* ids,
|
||||
@@ -306,46 +302,46 @@ SLresult CreateOutputMix(SLEngineItf self, SLObjectItf* mix,
|
||||
SLuint32 num_interfaces, const SLInterfaceID* ids,
|
||||
const SLboolean* required);
|
||||
|
||||
SLresult CreateMidiPlayer(SLEngineItf /*self*/, SLObjectItf* player,
|
||||
const void* /*source*/, const void* /*sink*/,
|
||||
const void* /*bank_source*/,
|
||||
const void* /*bank_sink*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/);
|
||||
SLresult CreateListener(SLEngineItf /*self*/, SLObjectItf* listener,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/);
|
||||
SLresult Create3DGroup(SLEngineItf /*self*/, SLObjectItf* group,
|
||||
SLuint32 /*num_interfaces*/, const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/);
|
||||
SLresult CreateMetadataExtractor(SLEngineItf /*self*/, SLObjectItf* extractor,
|
||||
const void* /*source*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/);
|
||||
SLresult CreateExtensionObject(SLEngineItf /*self*/, SLObjectItf* object,
|
||||
void* /*parameters*/, SLuint32 /*object_id*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/);
|
||||
SLresult QueryNumSupportedInterfaces(SLEngineItf /*self*/,
|
||||
SLuint32 /*object_id*/, SLuint32* count);
|
||||
SLresult QuerySupportedInterfaces(SLEngineItf /*self*/, SLuint32 /*object_id*/,
|
||||
SLuint32 /*index*/, SLInterfaceID* iid);
|
||||
SLresult QueryNumSupportedExtensions(SLEngineItf /*self*/, SLuint32* count);
|
||||
SLresult QuerySupportedExtension(SLEngineItf /*self*/, SLuint32 /*index*/,
|
||||
SLresult CreateMidiPlayer(SLEngineItf, SLObjectItf* player,
|
||||
const void*, const void*,
|
||||
const void*,
|
||||
const void*,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*);
|
||||
SLresult CreateListener(SLEngineItf, SLObjectItf* listener,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*);
|
||||
SLresult Create3DGroup(SLEngineItf, SLObjectItf* group,
|
||||
SLuint32, const SLInterfaceID*,
|
||||
const SLboolean*);
|
||||
SLresult CreateMetadataExtractor(SLEngineItf, SLObjectItf* extractor,
|
||||
const void*,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*);
|
||||
SLresult CreateExtensionObject(SLEngineItf, SLObjectItf* object,
|
||||
void*, SLuint32,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*);
|
||||
SLresult QueryNumSupportedInterfaces(SLEngineItf,
|
||||
SLuint32, SLuint32* count);
|
||||
SLresult QuerySupportedInterfaces(SLEngineItf, SLuint32,
|
||||
SLuint32, SLInterfaceID* iid);
|
||||
SLresult QueryNumSupportedExtensions(SLEngineItf, SLuint32* count);
|
||||
SLresult QuerySupportedExtension(SLEngineItf, SLuint32,
|
||||
char* name, SLint32* name_length);
|
||||
SLresult IsExtensionSupported(SLEngineItf /*self*/, const char* /*name*/,
|
||||
SLresult IsExtensionSupported(SLEngineItf, const char*,
|
||||
SLboolean* supported);
|
||||
|
||||
SLresult PlaySetState(SLPlayItf /*self*/, SLuint32 state) {
|
||||
SLresult PlaySetState(SLPlayItf, SLuint32 state) {
|
||||
g_play_state = state;
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult PlayGetState(SLPlayItf /*self*/, SLuint32* state) {
|
||||
SLresult PlayGetState(SLPlayItf, SLuint32* state) {
|
||||
if (state) {
|
||||
*state = g_play_state;
|
||||
}
|
||||
@@ -359,115 +355,115 @@ SLresult ReturnZeroU32(SLuint32* value) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult PlayGetDuration(SLPlayItf /*self*/, SLuint32* duration) {
|
||||
SLresult PlayGetDuration(SLPlayItf, SLuint32* duration) {
|
||||
return ReturnZeroU32(duration);
|
||||
}
|
||||
|
||||
SLresult PlayGetPosition(SLPlayItf /*self*/, SLuint32* position) {
|
||||
SLresult PlayGetPosition(SLPlayItf, SLuint32* position) {
|
||||
return ReturnZeroU32(position);
|
||||
}
|
||||
|
||||
SLresult PlayRegisterCallback(SLPlayItf /*self*/, SLPlayCallback /*callback*/,
|
||||
void* /*context*/) {
|
||||
SLresult PlayRegisterCallback(SLPlayItf, SLPlayCallback,
|
||||
void*) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult PlaySetEventsMask(SLPlayItf /*self*/, SLuint32 /*mask*/) {
|
||||
SLresult PlaySetEventsMask(SLPlayItf, SLuint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult PlayGetEventsMask(SLPlayItf /*self*/, SLuint32* mask) {
|
||||
SLresult PlayGetEventsMask(SLPlayItf, SLuint32* mask) {
|
||||
return ReturnZeroU32(mask);
|
||||
}
|
||||
|
||||
SLresult PlaySetMarkerPosition(SLPlayItf /*self*/, SLuint32 /*position*/) {
|
||||
SLresult PlaySetMarkerPosition(SLPlayItf, SLuint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult PlayClearMarkerPosition(SLPlayItf /*self*/) {
|
||||
SLresult PlayClearMarkerPosition(SLPlayItf) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult PlayGetMarkerPosition(SLPlayItf /*self*/, SLuint32* position) {
|
||||
SLresult PlayGetMarkerPosition(SLPlayItf, SLuint32* position) {
|
||||
return ReturnZeroU32(position);
|
||||
}
|
||||
|
||||
SLresult PlaySetPositionUpdatePeriod(SLPlayItf /*self*/, SLuint32 /*period*/) {
|
||||
SLresult PlaySetPositionUpdatePeriod(SLPlayItf, SLuint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult PlayGetPositionUpdatePeriod(SLPlayItf /*self*/, SLuint32* period) {
|
||||
SLresult PlayGetPositionUpdatePeriod(SLPlayItf, SLuint32* period) {
|
||||
return ReturnZeroU32(period);
|
||||
}
|
||||
|
||||
SLresult RecordSetState(SLRecordItf /*self*/, SLuint32 state) {
|
||||
SLresult RecordSetState(SLRecordItf, SLuint32 state) {
|
||||
g_record_state = state;
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult RecordGetState(SLRecordItf /*self*/, SLuint32* state) {
|
||||
SLresult RecordGetState(SLRecordItf, SLuint32* state) {
|
||||
if (state) {
|
||||
*state = g_record_state;
|
||||
}
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult RecordSetDurationLimit(SLRecordItf /*self*/,
|
||||
SLuint32 /*milliseconds*/) {
|
||||
SLresult RecordSetDurationLimit(SLRecordItf,
|
||||
SLuint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult RecordGetPosition(SLRecordItf /*self*/, SLuint32* position) {
|
||||
SLresult RecordGetPosition(SLRecordItf, SLuint32* position) {
|
||||
return ReturnZeroU32(position);
|
||||
}
|
||||
|
||||
SLresult RecordRegisterCallback(SLRecordItf /*self*/,
|
||||
SLRecordCallback /*callback*/,
|
||||
void* /*context*/) {
|
||||
SLresult RecordRegisterCallback(SLRecordItf,
|
||||
SLRecordCallback,
|
||||
void*) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult RecordSetEventsMask(SLRecordItf /*self*/, SLuint32 /*mask*/) {
|
||||
SLresult RecordSetEventsMask(SLRecordItf, SLuint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult RecordGetEventsMask(SLRecordItf /*self*/, SLuint32* mask) {
|
||||
SLresult RecordGetEventsMask(SLRecordItf, SLuint32* mask) {
|
||||
return ReturnZeroU32(mask);
|
||||
}
|
||||
|
||||
SLresult RecordSetMarkerPosition(SLRecordItf /*self*/, SLuint32 /*position*/) {
|
||||
SLresult RecordSetMarkerPosition(SLRecordItf, SLuint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult RecordClearMarkerPosition(SLRecordItf /*self*/) {
|
||||
SLresult RecordClearMarkerPosition(SLRecordItf) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult RecordGetMarkerPosition(SLRecordItf /*self*/, SLuint32* position) {
|
||||
SLresult RecordGetMarkerPosition(SLRecordItf, SLuint32* position) {
|
||||
return ReturnZeroU32(position);
|
||||
}
|
||||
|
||||
SLresult RecordSetPositionUpdatePeriod(SLRecordItf /*self*/,
|
||||
SLuint32 /*period*/) {
|
||||
SLresult RecordSetPositionUpdatePeriod(SLRecordItf,
|
||||
SLuint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult RecordGetPositionUpdatePeriod(SLRecordItf /*self*/, SLuint32* period) {
|
||||
SLresult RecordGetPositionUpdatePeriod(SLRecordItf, SLuint32* period) {
|
||||
return ReturnZeroU32(period);
|
||||
}
|
||||
|
||||
SLresult BufferQueueEnqueue(SLBufferQueueItf /*self*/, const void* /*buffer*/,
|
||||
SLuint32 /*size*/) {
|
||||
SLresult BufferQueueEnqueue(SLBufferQueueItf, const void*,
|
||||
SLuint32) {
|
||||
++g_buffer_queue_state.count;
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult BufferQueueClear(SLBufferQueueItf /*self*/) {
|
||||
SLresult BufferQueueClear(SLBufferQueueItf) {
|
||||
g_buffer_queue_state = {};
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult BufferQueueGetState(SLBufferQueueItf /*self*/,
|
||||
SLresult BufferQueueGetState(SLBufferQueueItf,
|
||||
SLBufferQueueState* state) {
|
||||
if (state) {
|
||||
*state = g_buffer_queue_state;
|
||||
@@ -475,7 +471,7 @@ SLresult BufferQueueGetState(SLBufferQueueItf /*self*/,
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult BufferQueueRegisterCallback(SLBufferQueueItf /*self*/,
|
||||
SLresult BufferQueueRegisterCallback(SLBufferQueueItf,
|
||||
SLBufferQueueCallback callback,
|
||||
void* context) {
|
||||
g_buffer_queue_callback = callback;
|
||||
@@ -483,18 +479,18 @@ SLresult BufferQueueRegisterCallback(SLBufferQueueItf /*self*/,
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult SimpleBufferQueueEnqueue(SLAndroidSimpleBufferQueueItf /*self*/,
|
||||
const void* /*buffer*/, SLuint32 /*size*/) {
|
||||
SLresult SimpleBufferQueueEnqueue(SLAndroidSimpleBufferQueueItf,
|
||||
const void*, SLuint32) {
|
||||
++g_simple_queue_state.count;
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult SimpleBufferQueueClear(SLAndroidSimpleBufferQueueItf /*self*/) {
|
||||
SLresult SimpleBufferQueueClear(SLAndroidSimpleBufferQueueItf) {
|
||||
g_simple_queue_state = {};
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult SimpleBufferQueueGetState(SLAndroidSimpleBufferQueueItf /*self*/,
|
||||
SLresult SimpleBufferQueueGetState(SLAndroidSimpleBufferQueueItf,
|
||||
SLBufferQueueState* state) {
|
||||
if (state) {
|
||||
*state = g_simple_queue_state;
|
||||
@@ -503,40 +499,40 @@ SLresult SimpleBufferQueueGetState(SLAndroidSimpleBufferQueueItf /*self*/,
|
||||
}
|
||||
|
||||
SLresult SimpleBufferQueueRegisterCallback(
|
||||
SLAndroidSimpleBufferQueueItf /*self*/,
|
||||
SLAndroidSimpleBufferQueueItf,
|
||||
SLAndroidSimpleBufferQueueCallback callback, void* context) {
|
||||
g_simple_queue_callback = callback;
|
||||
g_simple_queue_context = context;
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult VolumeSetLevel(SLVolumeItf /*self*/, SLint32 /*level*/) {
|
||||
SLresult VolumeSetLevel(SLVolumeItf, SLint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult VolumeGetLevel(SLVolumeItf /*self*/, SLint32* level) {
|
||||
SLresult VolumeGetLevel(SLVolumeItf, SLint32* level) {
|
||||
if (level) {
|
||||
*level = 0;
|
||||
}
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult VolumeSetMute(SLVolumeItf /*self*/, SLboolean /*mute*/) {
|
||||
SLresult VolumeSetMute(SLVolumeItf, SLboolean) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult VolumeGetMute(SLVolumeItf /*self*/, SLboolean* mute) {
|
||||
SLresult VolumeGetMute(SLVolumeItf, SLboolean* mute) {
|
||||
if (mute) {
|
||||
*mute = 0;
|
||||
}
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult VolumeEnableStereoPosition(SLVolumeItf /*self*/, SLboolean /*enable*/) {
|
||||
SLresult VolumeEnableStereoPosition(SLVolumeItf, SLboolean) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult VolumeIsStereoPositionEnabled(SLVolumeItf /*self*/,
|
||||
SLresult VolumeIsStereoPositionEnabled(SLVolumeItf,
|
||||
SLboolean* enabled) {
|
||||
if (enabled) {
|
||||
*enabled = 0;
|
||||
@@ -544,23 +540,23 @@ SLresult VolumeIsStereoPositionEnabled(SLVolumeItf /*self*/,
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult VolumeSetStereoPosition(SLVolumeItf /*self*/, SLint32 /*position*/) {
|
||||
SLresult VolumeSetStereoPosition(SLVolumeItf, SLint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult VolumeGetStereoPosition(SLVolumeItf /*self*/, SLint32* position) {
|
||||
SLresult VolumeGetStereoPosition(SLVolumeItf, SLint32* position) {
|
||||
if (position) {
|
||||
*position = 0;
|
||||
}
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult ConfigSet(SLAndroidConfigurationItf /*self*/, const char* /*key*/,
|
||||
const void* /*value*/, SLuint32 /*value_size*/) {
|
||||
SLresult ConfigSet(SLAndroidConfigurationItf, const char*,
|
||||
const void*, SLuint32) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult ConfigGet(SLAndroidConfigurationItf /*self*/, const char* /*key*/,
|
||||
SLresult ConfigGet(SLAndroidConfigurationItf, const char*,
|
||||
SLuint32* value_size, void* value) {
|
||||
if (value_size) {
|
||||
if (value && *value_size > 0) {
|
||||
@@ -709,7 +705,7 @@ void* SL_IID_VOLUME = reinterpret_cast<void*>(0x1007);
|
||||
|
||||
namespace {
|
||||
|
||||
SLresult ObjectGetInterface(SLObjectItf /*self*/, SLInterfaceID iid,
|
||||
SLresult ObjectGetInterface(SLObjectItf, SLInterfaceID iid,
|
||||
void* interface_out) {
|
||||
if (!interface_out) {
|
||||
return SL_RESULT_SUCCESS;
|
||||
@@ -749,107 +745,107 @@ SLresult ObjectGetInterface(SLObjectItf /*self*/, SLInterfaceID iid,
|
||||
return SL_RESULT_FEATURE_UNSUPPORTED;
|
||||
}
|
||||
|
||||
SLresult CreateUnsupportedObject(SLEngineItf /*self*/, SLObjectItf* object,
|
||||
SLuint32 /*device_id*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/) {
|
||||
SLresult CreateUnsupportedObject(SLEngineItf, SLObjectItf* object,
|
||||
SLuint32,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*) {
|
||||
return SuccessObject(object, GenericObject());
|
||||
}
|
||||
|
||||
SLresult CreateAudioPlayer(SLEngineItf /*self*/, SLObjectItf* player,
|
||||
const void* /*source*/, const void* /*sink*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/) {
|
||||
SLresult CreateAudioPlayer(SLEngineItf, SLObjectItf* player,
|
||||
const void*, const void*,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*) {
|
||||
g_play_state = SL_PLAYSTATE_STOPPED;
|
||||
g_buffer_queue_state = {};
|
||||
g_simple_queue_state = {};
|
||||
return SuccessObject(player, PlayerObject());
|
||||
}
|
||||
|
||||
SLresult CreateAudioRecorder(SLEngineItf /*self*/, SLObjectItf* recorder,
|
||||
const void* /*source*/, const void* /*sink*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/) {
|
||||
SLresult CreateAudioRecorder(SLEngineItf, SLObjectItf* recorder,
|
||||
const void*, const void*,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*) {
|
||||
if (recorder != nullptr) {
|
||||
*recorder = nullptr;
|
||||
}
|
||||
return SL_RESULT_FEATURE_UNSUPPORTED;
|
||||
}
|
||||
|
||||
SLresult CreateMidiPlayer(SLEngineItf /*self*/, SLObjectItf* player,
|
||||
const void* /*source*/, const void* /*sink*/,
|
||||
const void* /*bank_source*/,
|
||||
const void* /*bank_sink*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/) {
|
||||
SLresult CreateMidiPlayer(SLEngineItf, SLObjectItf* player,
|
||||
const void*, const void*,
|
||||
const void*,
|
||||
const void*,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*) {
|
||||
return SuccessObject(player, PlayerObject());
|
||||
}
|
||||
|
||||
SLresult CreateListener(SLEngineItf /*self*/, SLObjectItf* listener,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/) {
|
||||
SLresult CreateListener(SLEngineItf, SLObjectItf* listener,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*) {
|
||||
return SuccessObject(listener, GenericObject());
|
||||
}
|
||||
|
||||
SLresult Create3DGroup(SLEngineItf /*self*/, SLObjectItf* group,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/) {
|
||||
SLresult Create3DGroup(SLEngineItf, SLObjectItf* group,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*) {
|
||||
return SuccessObject(group, GenericObject());
|
||||
}
|
||||
|
||||
SLresult CreateOutputMix(SLEngineItf /*self*/, SLObjectItf* mix,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/) {
|
||||
SLresult CreateOutputMix(SLEngineItf, SLObjectItf* mix,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*) {
|
||||
return SuccessObject(mix, OutputMixObject());
|
||||
}
|
||||
|
||||
SLresult CreateMetadataExtractor(SLEngineItf /*self*/, SLObjectItf* extractor,
|
||||
const void* /*source*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/) {
|
||||
SLresult CreateMetadataExtractor(SLEngineItf, SLObjectItf* extractor,
|
||||
const void*,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*) {
|
||||
return SuccessObject(extractor, GenericObject());
|
||||
}
|
||||
|
||||
SLresult CreateExtensionObject(SLEngineItf /*self*/, SLObjectItf* object,
|
||||
void* /*parameters*/, SLuint32 /*object_id*/,
|
||||
SLuint32 /*num_interfaces*/,
|
||||
const SLInterfaceID* /*ids*/,
|
||||
const SLboolean* /*required*/) {
|
||||
SLresult CreateExtensionObject(SLEngineItf, SLObjectItf* object,
|
||||
void*, SLuint32,
|
||||
SLuint32,
|
||||
const SLInterfaceID*,
|
||||
const SLboolean*) {
|
||||
return SuccessObject(object, GenericObject());
|
||||
}
|
||||
|
||||
SLresult QueryNumSupportedInterfaces(SLEngineItf /*self*/,
|
||||
SLuint32 /*object_id*/, SLuint32* count) {
|
||||
SLresult QueryNumSupportedInterfaces(SLEngineItf,
|
||||
SLuint32, SLuint32* count) {
|
||||
if (count) {
|
||||
*count = 0;
|
||||
}
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult QuerySupportedInterfaces(SLEngineItf /*self*/, SLuint32 /*object_id*/,
|
||||
SLuint32 /*index*/, SLInterfaceID* iid) {
|
||||
SLresult QuerySupportedInterfaces(SLEngineItf, SLuint32,
|
||||
SLuint32, SLInterfaceID* iid) {
|
||||
if (iid) {
|
||||
*iid = nullptr;
|
||||
}
|
||||
return SL_RESULT_FEATURE_UNSUPPORTED;
|
||||
}
|
||||
|
||||
SLresult QueryNumSupportedExtensions(SLEngineItf /*self*/, SLuint32* count) {
|
||||
SLresult QueryNumSupportedExtensions(SLEngineItf, SLuint32* count) {
|
||||
if (count) {
|
||||
*count = 0;
|
||||
}
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
SLresult QuerySupportedExtension(SLEngineItf /*self*/, SLuint32 /*index*/,
|
||||
SLresult QuerySupportedExtension(SLEngineItf, SLuint32,
|
||||
char* name, SLint32* name_length) {
|
||||
if (name_length) {
|
||||
*name_length = 0;
|
||||
@@ -860,7 +856,7 @@ SLresult QuerySupportedExtension(SLEngineItf /*self*/, SLuint32 /*index*/,
|
||||
return SL_RESULT_FEATURE_UNSUPPORTED;
|
||||
}
|
||||
|
||||
SLresult IsExtensionSupported(SLEngineItf /*self*/, const char* /*name*/,
|
||||
SLresult IsExtensionSupported(SLEngineItf, const char*,
|
||||
SLboolean* supported) {
|
||||
if (supported) {
|
||||
*supported = 0;
|
||||
@@ -872,12 +868,11 @@ SLresult IsExtensionSupported(SLEngineItf /*self*/, const char* /*name*/,
|
||||
|
||||
extern "C" {
|
||||
|
||||
// Test-only OpenSL ES entry point. Production fails closed.
|
||||
SLresult slCreateEngine(void** pEngine, uint32_t /*numOptions*/,
|
||||
const void* /*pEngineOptions*/,
|
||||
uint32_t /*numInterfaces*/,
|
||||
const void* /*pInterfaceIds*/,
|
||||
const void* /*pInterfaceRequired*/) {
|
||||
SLresult slCreateEngine(void** pEngine, uint32_t,
|
||||
const void*,
|
||||
uint32_t,
|
||||
const void*,
|
||||
const void*) {
|
||||
if (pEngine == nullptr) {
|
||||
return SL_RESULT_PARAMETER_INVALID;
|
||||
}
|
||||
@@ -890,12 +885,11 @@ SLresult slCreateEngine(void** pEngine, uint32_t /*numOptions*/,
|
||||
return SL_RESULT_SUCCESS;
|
||||
}
|
||||
|
||||
// Test-only OpenMAX AL entry point. Production fails closed.
|
||||
SLresult alCreateEngine(void** pEngine, uint32_t /*numOptions*/,
|
||||
const void* /*pEngineOptions*/,
|
||||
uint32_t /*numInterfaces*/,
|
||||
const void* /*pInterfaceIds*/,
|
||||
const void* /*pInterfaceRequired*/) {
|
||||
SLresult alCreateEngine(void** pEngine, uint32_t,
|
||||
const void*,
|
||||
uint32_t,
|
||||
const void*,
|
||||
const void*) {
|
||||
if (pEngine == nullptr) {
|
||||
return SL_RESULT_PARAMETER_INVALID;
|
||||
}
|
||||
|
||||
+15
-15
@@ -35,8 +35,8 @@ bool TestVulkanStubEnabled() {
|
||||
|
||||
extern "C" {
|
||||
|
||||
VkResult vkCreateInstance(const void* /*create_info*/,
|
||||
const VkAllocationCallbacks* /*allocator*/,
|
||||
VkResult vkCreateInstance(const void*,
|
||||
const VkAllocationCallbacks*,
|
||||
VkInstance* instance) {
|
||||
if (!TestVulkanStubEnabled()) {
|
||||
if (instance != nullptr) {
|
||||
@@ -50,12 +50,12 @@ VkResult vkCreateInstance(const void* /*create_info*/,
|
||||
return kVkSuccess;
|
||||
}
|
||||
|
||||
void vkDestroyInstance(VkInstance /*instance*/,
|
||||
const VkAllocationCallbacks* /*allocator*/) {}
|
||||
void vkDestroyInstance(VkInstance,
|
||||
const VkAllocationCallbacks*) {}
|
||||
|
||||
VkResult vkCreateAndroidSurfaceKHR(VkInstance /*instance*/,
|
||||
const void* /*create_info*/,
|
||||
const VkAllocationCallbacks* /*allocator*/,
|
||||
VkResult vkCreateAndroidSurfaceKHR(VkInstance,
|
||||
const void*,
|
||||
const VkAllocationCallbacks*,
|
||||
VkSurfaceKHR* surface) {
|
||||
if (!TestVulkanStubEnabled()) {
|
||||
if (surface != nullptr) {
|
||||
@@ -69,12 +69,12 @@ VkResult vkCreateAndroidSurfaceKHR(VkInstance /*instance*/,
|
||||
return kVkSuccess;
|
||||
}
|
||||
|
||||
void vkDestroySurfaceKHR(VkInstance /*instance*/, VkSurfaceKHR /*surface*/,
|
||||
const VkAllocationCallbacks* /*allocator*/) {}
|
||||
void vkDestroySurfaceKHR(VkInstance, VkSurfaceKHR,
|
||||
const VkAllocationCallbacks*) {}
|
||||
|
||||
VkResult vkEnumerateInstanceExtensionProperties(const char* /*layer_name*/,
|
||||
VkResult vkEnumerateInstanceExtensionProperties(const char*,
|
||||
uint32_t* property_count,
|
||||
void* /*properties*/) {
|
||||
void*) {
|
||||
if (property_count != nullptr) {
|
||||
*property_count = 0;
|
||||
}
|
||||
@@ -82,19 +82,19 @@ VkResult vkEnumerateInstanceExtensionProperties(const char* /*layer_name*/,
|
||||
}
|
||||
|
||||
VkResult vkEnumerateInstanceLayerProperties(uint32_t* property_count,
|
||||
void* /*properties*/) {
|
||||
void*) {
|
||||
if (property_count != nullptr) {
|
||||
*property_count = 0;
|
||||
}
|
||||
return kVkSuccess;
|
||||
}
|
||||
|
||||
PFN_vkVoidFunction vkGetDeviceProcAddr(VkDevice /*device*/,
|
||||
const char* /*name*/) {
|
||||
PFN_vkVoidFunction vkGetDeviceProcAddr(VkDevice,
|
||||
const char*) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
PFN_vkVoidFunction vkGetInstanceProcAddr(VkInstance /*instance*/,
|
||||
PFN_vkVoidFunction vkGetInstanceProcAddr(VkInstance,
|
||||
const char* name) {
|
||||
if (name == nullptr) {
|
||||
return nullptr;
|
||||
|
||||
@@ -25,6 +25,12 @@ target_compile_definitions(runtime_config_file_test PRIVATE
|
||||
MOCKTAIL_TEST_SOURCE_DIR="${PROJECT_SOURCE_DIR}"
|
||||
)
|
||||
|
||||
add_executable(discord_rpc_test discord_rpc_test.cc)
|
||||
target_link_libraries(discord_rpc_test PRIVATE
|
||||
Mocktail::Runtime
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
add_executable(native_update_helper_fixture native_update_helper_fixture.cc)
|
||||
add_executable(native_progress_helper_fixture native_progress_helper_fixture.cc)
|
||||
mocktail_apply_compile_options(native_update_helper_fixture)
|
||||
@@ -98,6 +104,30 @@ target_link_libraries(game_mode_test PRIVATE
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
add_library(game_mode_namespace_fixture SHARED
|
||||
game_mode_namespace_fixture.cc
|
||||
)
|
||||
set_target_properties(game_mode_namespace_fixture PROPERTIES
|
||||
OUTPUT_NAME gamemode
|
||||
VERSION 0.0.0
|
||||
SOVERSION 0
|
||||
LIBRARY_OUTPUT_DIRECTORY
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/game_mode_namespace_fixture"
|
||||
)
|
||||
target_link_libraries(game_mode_namespace_fixture PRIVATE ${CMAKE_DL_LIBS})
|
||||
|
||||
add_executable(game_mode_link_namespace_test
|
||||
game_mode_link_namespace_test.cc
|
||||
)
|
||||
target_link_libraries(game_mode_link_namespace_test PRIVATE
|
||||
Mocktail::Runtime
|
||||
GTest::gtest_main
|
||||
)
|
||||
target_link_options(game_mode_link_namespace_test PRIVATE
|
||||
-Wl,--export-dynamic
|
||||
)
|
||||
add_dependencies(game_mode_link_namespace_test game_mode_namespace_fixture)
|
||||
|
||||
add_executable(memory_limit_test memory_limit_test.cc)
|
||||
target_link_libraries(memory_limit_test PRIVATE
|
||||
Mocktail::Runtime
|
||||
@@ -780,6 +810,10 @@ gtest_discover_tests(http_client_policy_test)
|
||||
gtest_discover_tests(http_client_spin_guard_test)
|
||||
gtest_discover_tests(crash_report_policy_test)
|
||||
gtest_discover_tests(game_mode_test)
|
||||
gtest_discover_tests(game_mode_link_namespace_test PROPERTIES
|
||||
ENVIRONMENT
|
||||
"LD_LIBRARY_PATH=${CMAKE_CURRENT_BINARY_DIR}/game_mode_namespace_fixture"
|
||||
)
|
||||
gtest_discover_tests(memory_limit_test)
|
||||
gtest_discover_tests(runtime_paths_test)
|
||||
gtest_discover_tests(single_instance_lock_test)
|
||||
@@ -849,6 +883,7 @@ if(MOCKTAIL_ENABLE_UPSTREAM_JNIVM)
|
||||
gtest_discover_tests(upstream_jnivm_integration_test)
|
||||
endif()
|
||||
gtest_discover_tests(libc_shim_test)
|
||||
gtest_discover_tests(discord_rpc_test)
|
||||
gtest_discover_tests(asset_manager_test)
|
||||
gtest_discover_tests(android_native_window_test)
|
||||
gtest_discover_tests(headless_graphics_test)
|
||||
|
||||
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user