mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-17 16:01:38 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9987546d5 | ||
|
|
6a39887e8f | ||
|
|
349e314850 | ||
|
|
b1112d8cdc | ||
|
|
27c383a5d9 | ||
|
|
7cf24feef6 | ||
|
|
4443766d39 | ||
|
|
ea2166b09a | ||
|
|
9e4e69be1c | ||
|
|
dd0655bfde | ||
|
|
d2d275706b | ||
|
|
d76d75b7f8 | ||
|
|
b7b63dd30f | ||
|
|
aacab4036c | ||
|
|
e104b1d936 | ||
|
|
537828acc2 | ||
|
|
5be2f68a55 | ||
|
|
f7e519bae1 | ||
|
|
ad8fbfcf65 | ||
|
|
96e1966d16 | ||
|
|
becaee71c0 | ||
|
|
bf6a6b417d | ||
|
|
dfd35b8cc8 | ||
|
|
1632681e6c | ||
|
|
ab4944a907 | ||
|
|
88d10d53f2 | ||
|
|
eae9fa7030 | ||
|
|
ad76f68224 | ||
|
|
d783793b4e | ||
|
|
017f79398d | ||
|
|
fc120e8285 | ||
|
|
117b9b2016 | ||
|
|
1376ab2474 | ||
|
|
fdce53421f | ||
|
|
770ed086d5 | ||
|
|
c48db09f13 | ||
|
|
0548ebd253 | ||
|
|
c523048518 | ||
|
|
ffe3fba8af | ||
|
|
5d475e03d8 | ||
|
|
240dc1ee63 | ||
|
|
569980ecf2 | ||
|
|
2f80333c5d | ||
|
|
be7d542d49 | ||
|
|
266f917f49 | ||
|
|
859f8f2b8f | ||
|
|
8447bf3f1a | ||
|
|
b71873f2a5 | ||
|
|
92c7d7b12b | ||
|
|
cc3893d422 | ||
|
|
a6fdb5b0cd | ||
|
|
3890efbaaa | ||
|
|
9f7b09e197 | ||
|
|
3284cf9fc1 | ||
|
|
035c7fe1d4 | ||
|
|
d5f9c91358 | ||
|
|
0cc9ab2e32 | ||
|
|
7127ad1bf7 | ||
|
|
54a302dc27 | ||
|
|
23a5c96e3d | ||
|
|
ae417490f1 | ||
|
|
35298050b8 | ||
|
|
703af11d17 | ||
|
|
3a26ecc83e | ||
|
|
278d15e41a | ||
|
|
968df46605 | ||
|
|
409d8a6319 | ||
|
|
9812fbceab | ||
|
|
61a467cadc | ||
|
|
497481e8fa | ||
|
|
5f2be59192 | ||
|
|
9bd5f97063 | ||
|
|
8f63b9deba | ||
|
|
272f61688b | ||
|
|
f797ec7e40 | ||
|
|
02c8c6ca83 | ||
|
|
5af66cf8fc | ||
|
|
b236c59b57 | ||
|
|
386476eda6 | ||
|
|
b6407e7d3a | ||
|
|
21efe88b22 | ||
|
|
0f635abc25 | ||
|
|
2f179068b3 | ||
|
|
81b490e196 | ||
|
|
f2168e2f98 | ||
|
|
64a564f292 | ||
|
|
3728afa0d7 | ||
|
|
931543e976 | ||
|
|
a2a210da87 | ||
|
|
b289e7933e | ||
|
|
497cf9ea6c | ||
|
|
0b7326d8d1 | ||
|
|
e312ab6667 | ||
|
|
b8e535f6f9 | ||
|
|
c0f6787cc6 | ||
|
|
f0e0229ec5 | ||
|
|
56749bbdc8 | ||
|
|
9da2c02836 | ||
|
|
b5eac34e0f | ||
|
|
e4c64c2fec | ||
|
|
ba6287250f | ||
|
|
4b437f623f | ||
|
|
e161fecfc5 | ||
|
|
dcf22a144b | ||
|
|
0bc8823644 | ||
|
|
f9a48cfe83 | ||
|
|
52721bf06c | ||
|
|
9bbd2a1ebf |
No files matched your search
@@ -0,0 +1,103 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
name: Bug report
|
||||
description: Report a reproducible problem in Nightcap
|
||||
title: "[Bug]: "
|
||||
labels:
|
||||
- bug
|
||||
assignees: []
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Please include enough detail for someone else to reproduce the bug.
|
||||
|
||||
Use any language you are comfortable with. The maintainer can read Russian and English directly, so either is preferred. If you are more comfortable in another language, use it.
|
||||
|
||||
Please write issues and pull requests yourself. Do not use AI-generated text.
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Mocktail version
|
||||
description: Provide the release version, package version, or commit hash.
|
||||
placeholder: "For example: v0.4.0 or 1a2b3c4"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: system
|
||||
attributes:
|
||||
label: System information
|
||||
description: Include your distribution, desktop session, GPU, and graphics driver when relevant.
|
||||
placeholder: "For example: Arch Linux, KDE Plasma 6 on Wayland, AMD RX 6600, Mesa 26.1"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: installation
|
||||
attributes:
|
||||
label: Installation method
|
||||
description: Tell us how Mocktail was installed or built.
|
||||
placeholder: "For example: AppImage, Flatpak, AUR, or built from source"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: Describe the problem and its impact.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: reproduction
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: List the smallest reliable sequence of actions that triggers the problem.
|
||||
placeholder: |
|
||||
1. Start Mocktail with ...
|
||||
2. Open ...
|
||||
3. Select ...
|
||||
4. Observe ...
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: Explain what you expected to happen instead.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Latest log:
|
||||
|
||||
- Flatpak: `~/.var/app/io.github.CoderDayton.nightcap/.local/state/mocktail/logs/latest.log`
|
||||
- Other installs: `~/.local/state/mocktail/logs/latest.log`
|
||||
|
||||
Older logs are in the `sessions` directory next to `latest.log`.
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Logs
|
||||
description: Paste the relevant part of latest.log or the terminal output.
|
||||
render: shell
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add screenshots, videos, workarounds, or any other useful details.
|
||||
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Before submitting
|
||||
options:
|
||||
- label: I searched the existing issues and did not find a duplicate.
|
||||
required: true
|
||||
@@ -0,0 +1,2 @@
|
||||
blank_issues_enabled: true
|
||||
contact_links: []
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Feature request
|
||||
description: Suggest an improvement or a new capability for Mocktail
|
||||
title: "[Feature]: "
|
||||
labels:
|
||||
- enhancement
|
||||
assignees: []
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Describe what you want to change and why it would be useful.
|
||||
|
||||
Use any language you are comfortable with. The maintainer can read Russian and English directly, so either is preferred. If you are more comfortable in another language, use it.
|
||||
|
||||
Please write issues and pull requests yourself. Do not use AI-generated text.
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: Problem or use case
|
||||
description: What are you trying to do, and what makes it difficult today?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: proposal
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: Describe how you would like Mocktail to behave.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: alternatives
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
description: Describe any workarounds or alternative designs you have considered.
|
||||
|
||||
- type: textarea
|
||||
id: additional-context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add mockups, screenshots, links, or implementation notes if useful.
|
||||
|
||||
- type: checkboxes
|
||||
id: checks
|
||||
attributes:
|
||||
label: Before submitting
|
||||
options:
|
||||
- label: I searched the existing issues and did not find a duplicate.
|
||||
required: true
|
||||
@@ -0,0 +1,3 @@
|
||||
<!-- Briefly describe what this pull request adds, changes, or removes. Write it yourself and do not use AI-generated text. Link the issue with "Closes #123" or "Fixes #123". -->
|
||||
|
||||
Closes #
|
||||
+27
-17
@@ -1,4 +1,4 @@
|
||||
name: Publish AUR development package
|
||||
name: Publish AUR packages
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -9,12 +9,12 @@ permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: aur-mocktail-git
|
||||
group: aur-packages
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish mocktail-git
|
||||
name: Publish AUR packages
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
@@ -87,18 +87,28 @@ jobs:
|
||||
-o BatchMode=yes -o IdentitiesOnly=yes \
|
||||
-o StrictHostKeyChecking=yes \
|
||||
-o UserKnownHostsFile=$known_hosts"
|
||||
aur_dir="$RUNNER_TEMP/mocktail-git-aur"
|
||||
git clone --branch master --single-branch \
|
||||
ssh://aur@aur.archlinux.org/mocktail-git.git "$aur_dir"
|
||||
install -m 0644 packaging/aur/mocktail-git/PKGBUILD "$aur_dir/PKGBUILD"
|
||||
install -m 0644 packaging/aur/mocktail-git/.SRCINFO "$aur_dir/.SRCINFO"
|
||||
publish_aur_package() {
|
||||
local package_name="$1"
|
||||
local commit_message="$2"
|
||||
local source_dir="packaging/aur/$package_name"
|
||||
local aur_dir="$RUNNER_TEMP/${package_name}-aur"
|
||||
|
||||
git -C "$aur_dir" config user.name komaruworld
|
||||
git -C "$aur_dir" config user.email rtysus228@gmail.com
|
||||
git -C "$aur_dir" add PKGBUILD .SRCINFO
|
||||
if git -C "$aur_dir" diff --cached --quiet; then
|
||||
printf 'mocktail-git %s is already published\n' "$AUR_PKGVER"
|
||||
exit 0
|
||||
fi
|
||||
git -C "$aur_dir" commit -m "Update to ${AUR_PKGVER}"
|
||||
git -C "$aur_dir" push origin HEAD:master
|
||||
git clone --branch master --single-branch \
|
||||
"ssh://aur@aur.archlinux.org/${package_name}.git" "$aur_dir"
|
||||
install -m 0644 "$source_dir/PKGBUILD" "$aur_dir/PKGBUILD"
|
||||
install -m 0644 "$source_dir/.SRCINFO" "$aur_dir/.SRCINFO"
|
||||
|
||||
git -C "$aur_dir" config user.name komaruworld
|
||||
git -C "$aur_dir" config user.email rtysus228@gmail.com
|
||||
git -C "$aur_dir" add PKGBUILD .SRCINFO
|
||||
if git -C "$aur_dir" diff --cached --quiet; then
|
||||
printf '%s is already published\n' "$package_name"
|
||||
return
|
||||
fi
|
||||
git -C "$aur_dir" commit -m "$commit_message"
|
||||
git -C "$aur_dir" push origin HEAD:master
|
||||
}
|
||||
|
||||
publish_aur_package mocktail-git "Update to ${AUR_PKGVER}"
|
||||
publish_aur_package mocktail 'make builds stable'
|
||||
publish_aur_package mocktail-bin 'make builds stable'
|
||||
@@ -52,6 +52,12 @@ jobs:
|
||||
find config packaging/flatpak -type f -name '*.json' -print0 \
|
||||
| xargs -0 -n1 python3 -m json.tool >/dev/null
|
||||
|
||||
- name: Run script unit tests
|
||||
shell: bash
|
||||
run: |
|
||||
python3 -m pip install --disable-pip-version-check requests
|
||||
python3 -m unittest discover -s tests -p '*_test.py' -v
|
||||
|
||||
- name: Verify build and packaging contracts
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
name: Flatpak
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [Native packages]
|
||||
types: [completed]
|
||||
push:
|
||||
tags: ['v*']
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/flatpak.yml'
|
||||
@@ -15,13 +15,11 @@ on:
|
||||
- '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'
|
||||
- 'packaging/io.github.CoderDayton.nightcap.desktop'
|
||||
- 'packaging/io.github.CoderDayton.nightcap.metainfo.xml'
|
||||
- 'packaging/io.github.CoderDayton.nightcap.svg'
|
||||
- 'scripts/assemble_flatpak_pages.sh'
|
||||
- 'scripts/assemble_native_repositories.sh'
|
||||
- 'site/**'
|
||||
- 'src/**'
|
||||
- 'stubs/**'
|
||||
@@ -29,61 +27,28 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: flatpak-${{ github.event.workflow_run.head_sha || github.ref }}
|
||||
cancel-in-progress: true
|
||||
group: flatpak-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
flatpak:
|
||||
name: Flatpak build
|
||||
if: >-
|
||||
github.event_name != 'workflow_run' ||
|
||||
(github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event != 'pull_request' &&
|
||||
github.event.workflow_run.head_branch == 'main')
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
env:
|
||||
MOCKTAIL_PUBLISH: ${{ (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'pull_request' && github.event.workflow_run.head_branch == 'main') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }}
|
||||
MOCKTAIL_SOURCE_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
# Publish on release tags and on manual runs from main. Pull requests
|
||||
# only verify that the manifest still builds.
|
||||
MOCKTAIL_PUBLISH: ${{ startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }}
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
submodules: recursive
|
||||
|
||||
- name: Download package repositories
|
||||
if: env.MOCKTAIL_PUBLISH == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
run_id="${{ github.event.workflow_run.id }}"
|
||||
if [[ -z "$run_id" ]]; then
|
||||
run_id="$(gh run list \
|
||||
--workflow packages.yml \
|
||||
--branch main \
|
||||
--event push \
|
||||
--status success \
|
||||
--limit 1 \
|
||||
--json databaseId \
|
||||
--jq '.[0].databaseId')"
|
||||
fi
|
||||
[[ -n "$run_id" ]]
|
||||
gh run download "$run_id" \
|
||||
--name Mocktail-native-repositories \
|
||||
--dir native-repositories
|
||||
[[ -d native-repositories/apt && \
|
||||
-d native-repositories/rpm && \
|
||||
-d native-repositories/downloads && \
|
||||
-s native-repositories/mocktail-packages.gpg ]]
|
||||
|
||||
- name: Install Flatpak builder
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -99,22 +64,23 @@ jobs:
|
||||
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
git update-ref refs/heads/main "$MOCKTAIL_SOURCE_SHA"
|
||||
# The manifest builds the local checkout from refs/heads/main.
|
||||
git update-ref refs/heads/main "$GITHUB_SHA"
|
||||
builder_signing=()
|
||||
bundle_keys=()
|
||||
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
|
||||
[[ -n "$FLATPAK_GPG_PRIVATE_KEY" && \
|
||||
-n "$FLATPAK_GPG_KEY_ID" ]]
|
||||
install -d -m 0700 "$RUNNER_TEMP/mocktail-gnupg"
|
||||
install -d -m 0700 "$RUNNER_TEMP/nightcap-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
|
||||
gpg --batch --homedir "$RUNNER_TEMP/nightcap-gnupg" --import
|
||||
gpg --batch --homedir "$RUNNER_TEMP/nightcap-gnupg" \
|
||||
--export "$FLATPAK_GPG_KEY_ID" >nightcap-flatpak.gpg
|
||||
builder_signing=(
|
||||
"--gpg-sign=$FLATPAK_GPG_KEY_ID"
|
||||
"--gpg-homedir=$RUNNER_TEMP/mocktail-gnupg"
|
||||
"--gpg-homedir=$RUNNER_TEMP/nightcap-gnupg"
|
||||
)
|
||||
bundle_keys=(--gpg-keys=mocktail-flatpak.gpg)
|
||||
bundle_keys=(--gpg-keys=nightcap-flatpak.gpg)
|
||||
fi
|
||||
flatpak-builder \
|
||||
--arch=x86_64 \
|
||||
@@ -127,34 +93,33 @@ jobs:
|
||||
--repo=repo \
|
||||
"${builder_signing[@]}" \
|
||||
build-flatpak-ci \
|
||||
packaging/flatpak/space.bigrat.mocktail.json
|
||||
packaging/flatpak/io.github.CoderDayton.nightcap.json
|
||||
if [[ "$MOCKTAIL_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" \
|
||||
--gpg-homedir="$RUNNER_TEMP/nightcap-gnupg" \
|
||||
repo
|
||||
fi
|
||||
flatpak build-bundle \
|
||||
--arch=x86_64 \
|
||||
"${bundle_keys[@]}" \
|
||||
repo \
|
||||
Mocktail-x86_64.flatpak \
|
||||
space.bigrat.mocktail \
|
||||
Nightcap-x86_64.flatpak \
|
||||
io.github.CoderDayton.nightcap \
|
||||
stable
|
||||
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
|
||||
./scripts/assemble_flatpak_pages.sh \
|
||||
repo Mocktail-x86_64.flatpak mocktail-flatpak.gpg \
|
||||
public native-repositories
|
||||
repo Nightcap-x86_64.flatpak nightcap-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
|
||||
name: Nightcap-x86_64-flatpak
|
||||
path: Nightcap-x86_64.flatpak
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
@@ -170,13 +135,7 @@ jobs:
|
||||
|
||||
deploy-pages:
|
||||
name: Publish Flatpak repository
|
||||
if: >-
|
||||
(github.event_name == 'workflow_run' &&
|
||||
github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.event != 'pull_request' &&
|
||||
github.event.workflow_run.head_branch == 'main') ||
|
||||
(github.event_name == 'workflow_dispatch' &&
|
||||
github.ref == 'refs/heads/main')
|
||||
if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main')
|
||||
needs: flatpak
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
@@ -191,3 +150,37 @@ jobs:
|
||||
- name: Deploy GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
|
||||
|
||||
release:
|
||||
name: Attach bundle to GitHub release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: flatpak
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download Flatpak
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: Nightcap-x86_64-flatpak
|
||||
path: release
|
||||
|
||||
- name: Upload to release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
(cd release && sha256sum Nightcap-x86_64.flatpak > Nightcap-x86_64.flatpak.sha256)
|
||||
# The Release workflow creates the same release; whichever job
|
||||
# arrives second sees "already exists" and continues.
|
||||
if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
gh release create "$tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--title "Nightcap $tag" --generate-notes ||
|
||||
gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null
|
||||
fi
|
||||
gh release upload "$tag" --repo "$GITHUB_REPOSITORY" --clobber \
|
||||
release/Nightcap-x86_64.flatpak \
|
||||
release/Nightcap-x86_64.flatpak.sha256
|
||||
@@ -33,7 +33,7 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
appimage:
|
||||
name: AppImage (Ubuntu 26.04)
|
||||
name: AppImage (AnyLinux)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
@@ -49,50 +49,89 @@ jobs:
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
ubuntu:26.04 bash -Eeuo pipefail -c '
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install --yes --no-install-recommends \
|
||||
build-essential cmake git ninja-build pkg-config lld \
|
||||
libsdl3-dev libsdl3-ttf-dev libcurl4-openssl-dev \
|
||||
libssl-dev nlohmann-json3-dev libyaml-dev libelf-dev \
|
||||
libminizip-dev libcapstone-dev libgtk-4-dev \
|
||||
libadwaita-1-dev libwebkitgtk-6.0-dev libutf8proc-dev \
|
||||
libfontconfig1-dev libegl-dev libvulkan-dev zlib1g-dev \
|
||||
libplacebo-dev \
|
||||
bubblewrap xdg-dbus-proxy patchelf jq unzip file curl \
|
||||
ca-certificates default-jdk-headless libgif7 python3 \
|
||||
python3-capstone python3-yaml python3-requests \
|
||||
python3-urllib3 python3-certifi python3-charset-normalizer \
|
||||
python3-idna squashfs-tools
|
||||
ghcr.io/archlinux/archlinux:base-devel@sha256:c3953446016873a756fb7814f10087cac1634c127f7302e80c81c512da796d05 bash -Eeuo pipefail -c '
|
||||
pacman -Syu --noconfirm --needed \
|
||||
cmake git ninja pkgconf lld sdl3 sdl3_ttf curl openssl \
|
||||
nlohmann-json libyaml libelf minizip minizip-ng capstone gtk4 \
|
||||
libadwaita webkitgtk-6.0 libutf8proc fontconfig libglvnd \
|
||||
vulkan-headers zlib libplacebo bubblewrap xdg-dbus-proxy \
|
||||
patchelf jq unzip file ca-certificates dbus zsync
|
||||
|
||||
# Reduced ABI-compatible packages belong only in this builder.
|
||||
# The native updater does not require a JRE or Android SDK.
|
||||
./scripts/install_anylinux_dependencies.sh
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/quick-sharun \
|
||||
https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/2a025cef14483c5bc2a75b479071f4d5c58a906f/useful-tools/quick-sharun.sh
|
||||
echo "fa66d0189e1e87df781c049dfb23cbb1e753786056f693a4483d6d3af61e7ee1 /tmp/quick-sharun" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/quick-sharun
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/sharun \
|
||||
https://github.com/pkgforge-dev/Anylinux-sharun/releases/download/2.2.6/sharun-x86_64
|
||||
echo "a4d51752f9861b4870ca529fea04087310b28f8943cc55b99a95b777d4db92ec /tmp/sharun" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/sharun
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/anylinux.c \
|
||||
https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/2a025cef14483c5bc2a75b479071f4d5c58a906f/useful-tools/lib/anylinux.c
|
||||
echo "f50650ad96d177559bdd0737df509fa20bd4af08887631c738fc1f31bfac185f /tmp/anylinux.c" | \
|
||||
sha256sum --check --strict
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/appimagetool \
|
||||
https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage
|
||||
echo "ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0 /tmp/appimagetool" | \
|
||||
https://github.com/pkgforge-dev/appimagetool/releases/download/0.3.3/appimagetool-x86_64-linux
|
||||
echo "0d01a4e2628efc897be9663f0faeed6a171e1851270b4e1ce4c22434a36ecebd /tmp/appimagetool" | \
|
||||
sha256sum --check --strict
|
||||
install -m 0755 /tmp/appimagetool /usr/local/bin/appimagetool
|
||||
chmod 0755 /tmp/appimagetool
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/android-build-tools.zip \
|
||||
https://dl.google.com/android/repository/build-tools_r35_linux.zip
|
||||
echo "bd3a4966912eb8b30ed0d00b0cda6b6543b949d5ffe00bea54c04c81e1561d88 /tmp/android-build-tools.zip" | \
|
||||
--output /tmp/uruntime \
|
||||
https://github.com/VHSgunzo/uruntime/releases/download/v0.5.9/uruntime-appimage-dwarfs-lite-x86_64
|
||||
echo "cef962c299f2fa19b2b3cdf2fa1565ee8541796cc89b9a97a591f94041e8b083 /tmp/uruntime" | \
|
||||
sha256sum --check --strict
|
||||
unzip -q /tmp/android-build-tools.zip -d /tmp/android-build-tools
|
||||
android_tools_root="$(dirname -- "$(find /tmp/android-build-tools -type f -name apksigner -print -quit)")"
|
||||
[[ -n "$android_tools_root" ]]
|
||||
chmod 0755 /tmp/uruntime
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/mkdwarfs \
|
||||
https://github.com/mhx/dwarfs/releases/download/v0.15.6/dwarfs-universal-0.15.6-Linux-x86_64
|
||||
echo "50891c38ba359db8271819a6cbf6aaa8068681523f0c4f2b8242007a45edaa28 /tmp/mkdwarfs" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/mkdwarfs
|
||||
|
||||
git config --global --add safe.directory /workspace
|
||||
git config --global --add safe.directory /workspace/third_party/Vulkan-Headers
|
||||
git config --global --add safe.directory /workspace/third_party/libjnivm
|
||||
export APPIMAGE_EXTRACT_AND_RUN=1
|
||||
export MOCKTAIL_ANDROID_BUILD_TOOLS_ROOT="$android_tools_root"
|
||||
# Install into the disposable container /usr before deployment.
|
||||
export MOCKTAIL_ANYLINUX_SYSTEM_INSTALL=1
|
||||
export MOCKTAIL_ANYLINUX_SHARUN=/tmp/sharun
|
||||
export MOCKTAIL_ANYLINUX_C_SOURCE=/tmp/anylinux.c
|
||||
export RUNTIME=/tmp/uruntime
|
||||
export DWARFS_CMD=/tmp/mkdwarfs
|
||||
make appimage BUILD_DIR=build-appimage JOBS=4 \
|
||||
APPIMAGE=dist/Mocktail-x86_64.AppImage \
|
||||
PORTABLE_MODE=standalone
|
||||
PORTABLE_MODE=standalone APPIMAGE_FORMAT=anylinux \
|
||||
ANYLINUX_PACKAGER=/tmp/quick-sharun \
|
||||
ANYLINUX_APPIMAGETOOL=/tmp/appimagetool
|
||||
test -x dist/Mocktail-x86_64.AppImage
|
||||
'
|
||||
|
||||
- name: Smoke-test AppImage without FUSE
|
||||
shell: bash
|
||||
run: |
|
||||
smoke_dir="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$smoke_dir"' EXIT
|
||||
export XDG_DATA_HOME="$smoke_dir/data"
|
||||
export XDG_CONFIG_HOME="$smoke_dir/config"
|
||||
export XDG_CACHE_HOME="$smoke_dir/cache"
|
||||
export XDG_STATE_HOME="$smoke_dir/state"
|
||||
mkdir -p "$XDG_DATA_HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" "$XDG_STATE_HOME"
|
||||
timeout 60s dist/Mocktail-x86_64.AppImage --appimage-extract-and-run --help
|
||||
timeout 60s dist/Mocktail-x86_64.AppImage --appimage-extract-and-run mocktail_updater status
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
@@ -182,6 +221,7 @@ jobs:
|
||||
cmake -S . -B build-rpm -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||
-DMOCKTAIL_PACKAGE_NAME=mocktail-nightly \
|
||||
-DMOCKTAIL_PACKAGE_VERSION_SUFFIX="$MOCKTAIL_PACKAGE_VERSION_SUFFIX" \
|
||||
-DBUILD_TESTING=OFF
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: release-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
appimage:
|
||||
name: AppImage (AnyLinux)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build AppImage
|
||||
shell: bash
|
||||
run: |
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
ghcr.io/archlinux/archlinux:base-devel@sha256:c3953446016873a756fb7814f10087cac1634c127f7302e80c81c512da796d05 bash -Eeuo pipefail -c '
|
||||
pacman -Syu --noconfirm --needed \
|
||||
cmake git ninja pkgconf lld sdl3 sdl3_ttf curl openssl \
|
||||
nlohmann-json libyaml libelf minizip minizip-ng capstone gtk4 \
|
||||
libadwaita webkitgtk-6.0 libutf8proc fontconfig libglvnd \
|
||||
vulkan-headers zlib libplacebo bubblewrap xdg-dbus-proxy \
|
||||
patchelf jq unzip file ca-certificates dbus zsync
|
||||
|
||||
./scripts/install_anylinux_dependencies.sh
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/quick-sharun \
|
||||
https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/2a025cef14483c5bc2a75b479071f4d5c58a906f/useful-tools/quick-sharun.sh
|
||||
echo "fa66d0189e1e87df781c049dfb23cbb1e753786056f693a4483d6d3af61e7ee1 /tmp/quick-sharun" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/quick-sharun
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/sharun \
|
||||
https://github.com/pkgforge-dev/Anylinux-sharun/releases/download/2.2.6/sharun-x86_64
|
||||
echo "a4d51752f9861b4870ca529fea04087310b28f8943cc55b99a95b777d4db92ec /tmp/sharun" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/sharun
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/anylinux.c \
|
||||
https://raw.githubusercontent.com/pkgforge-dev/Anylinux-AppImages/2a025cef14483c5bc2a75b479071f4d5c58a906f/useful-tools/lib/anylinux.c
|
||||
echo "f50650ad96d177559bdd0737df509fa20bd4af08887631c738fc1f31bfac185f /tmp/anylinux.c" | \
|
||||
sha256sum --check --strict
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/appimagetool \
|
||||
https://github.com/pkgforge-dev/appimagetool/releases/download/0.3.3/appimagetool-x86_64-linux
|
||||
echo "0d01a4e2628efc897be9663f0faeed6a171e1851270b4e1ce4c22434a36ecebd /tmp/appimagetool" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/appimagetool
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/uruntime \
|
||||
https://github.com/VHSgunzo/uruntime/releases/download/v0.5.9/uruntime-appimage-dwarfs-lite-x86_64
|
||||
echo "cef962c299f2fa19b2b3cdf2fa1565ee8541796cc89b9a97a591f94041e8b083 /tmp/uruntime" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/uruntime
|
||||
|
||||
curl --fail --location --retry 3 \
|
||||
--output /tmp/mkdwarfs \
|
||||
https://github.com/mhx/dwarfs/releases/download/v0.15.6/dwarfs-universal-0.15.6-Linux-x86_64
|
||||
echo "50891c38ba359db8271819a6cbf6aaa8068681523f0c4f2b8242007a45edaa28 /tmp/mkdwarfs" | \
|
||||
sha256sum --check --strict
|
||||
chmod 0755 /tmp/mkdwarfs
|
||||
|
||||
git config --global --add safe.directory /workspace
|
||||
git config --global --add safe.directory /workspace/third_party/Vulkan-Headers
|
||||
git config --global --add safe.directory /workspace/third_party/libjnivm
|
||||
export MOCKTAIL_ANYLINUX_SYSTEM_INSTALL=1
|
||||
export MOCKTAIL_ANYLINUX_SHARUN=/tmp/sharun
|
||||
export MOCKTAIL_ANYLINUX_C_SOURCE=/tmp/anylinux.c
|
||||
export RUNTIME=/tmp/uruntime
|
||||
export DWARFS_CMD=/tmp/mkdwarfs
|
||||
make appimage BUILD_DIR=build-appimage JOBS=4 \
|
||||
APPIMAGE=dist/Mocktail-x86_64.AppImage \
|
||||
PORTABLE_MODE=standalone APPIMAGE_FORMAT=anylinux \
|
||||
ANYLINUX_PACKAGER=/tmp/quick-sharun \
|
||||
ANYLINUX_APPIMAGETOOL=/tmp/appimagetool
|
||||
test -x dist/Mocktail-x86_64.AppImage
|
||||
'
|
||||
|
||||
- name: Smoke-test AppImage without FUSE
|
||||
shell: bash
|
||||
run: |
|
||||
smoke_dir="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$smoke_dir"' EXIT
|
||||
export XDG_DATA_HOME="$smoke_dir/data"
|
||||
export XDG_CONFIG_HOME="$smoke_dir/config"
|
||||
export XDG_CACHE_HOME="$smoke_dir/cache"
|
||||
export XDG_STATE_HOME="$smoke_dir/state"
|
||||
mkdir -p "$XDG_DATA_HOME" "$XDG_CONFIG_HOME" "$XDG_CACHE_HOME" "$XDG_STATE_HOME"
|
||||
timeout 60s dist/Mocktail-x86_64.AppImage --appimage-extract-and-run --help
|
||||
timeout 60s dist/Mocktail-x86_64.AppImage --appimage-extract-and-run mocktail_updater status
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-AppImage
|
||||
path: dist/Mocktail-x86_64.AppImage
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
publish:
|
||||
name: Publish GitHub release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [appimage]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Download AppImage
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: Mocktail-AppImage
|
||||
path: release
|
||||
|
||||
- name: Create release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
tag="${GITHUB_REF_NAME}"
|
||||
mv release/Mocktail-x86_64.AppImage release/Nightcap-x86_64.AppImage
|
||||
(cd release && sha256sum Nightcap-x86_64.AppImage > Nightcap-x86_64.AppImage.sha256)
|
||||
# The Flatpak workflow creates the same release; whichever job
|
||||
# arrives second sees "already exists" and continues.
|
||||
if ! gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
gh release create "$tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--title "Nightcap $tag" --generate-notes ||
|
||||
gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null
|
||||
fi
|
||||
gh release upload "$tag" --repo "$GITHUB_REPOSITORY" --clobber \
|
||||
release/Nightcap-x86_64.AppImage \
|
||||
release/Nightcap-x86_64.AppImage.sha256
|
||||
+9
-13
@@ -1,7 +1,9 @@
|
||||
# Build
|
||||
/build/
|
||||
/build-test/
|
||||
/build_*/
|
||||
/build-flatpak/
|
||||
/build-anylinux*/
|
||||
/.flatpak-builder/
|
||||
/cmake-build-*/
|
||||
/out/
|
||||
@@ -20,6 +22,12 @@ Testing/
|
||||
CMakeUserPresets.json
|
||||
*.cmake.user
|
||||
|
||||
# local direnv development shell cache
|
||||
.direnv/
|
||||
|
||||
# clangd cache
|
||||
.cache
|
||||
|
||||
*.o
|
||||
*.obj
|
||||
*.a
|
||||
@@ -50,19 +58,7 @@ Makefile.cmake
|
||||
/cmake-build-release/
|
||||
|
||||
# Roblox payload
|
||||
/rbx_bin/libroblox.so
|
||||
/rbx_bin/libroblox.so.*
|
||||
/rbx_bin/client_settings_android.json
|
||||
/rbx_bin/assets/
|
||||
/rbx_bin/android/
|
||||
/rbx_bin/sober_apk/
|
||||
/rbx_bin/versions/
|
||||
/rbx_bin/.payload-update.*/
|
||||
/rbx_bin/*.apk
|
||||
/rbx_bin/*.so
|
||||
/rbx_bin/*.so.*
|
||||
|
||||
!/rbx_bin/.gitkeep
|
||||
/rbx_bin/
|
||||
|
||||
# Runtime state
|
||||
/runtime/
|
||||
|
||||
+49
-17
@@ -1,3 +1,4 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -8,7 +9,7 @@
|
||||
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(Mocktail
|
||||
VERSION 1.0.3
|
||||
VERSION 1.0.4
|
||||
DESCRIPTION "Android x86-64 Roblox compatibility runtime for Linux"
|
||||
LANGUAGES CXX
|
||||
)
|
||||
@@ -138,12 +139,7 @@ if(BUILD_TESTING)
|
||||
FetchContent_MakeAvailable(googletest)
|
||||
endif()
|
||||
|
||||
add_library(mocktail_crypto_foundation STATIC
|
||||
src/foundation/sha256.cc
|
||||
)
|
||||
add_library(Mocktail::CryptoFoundation ALIAS mocktail_crypto_foundation)
|
||||
target_include_directories(mocktail_crypto_foundation PUBLIC include)
|
||||
mocktail_apply_compile_options(mocktail_crypto_foundation)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
include(${CMAKE_SOURCE_DIR}/cmake/MocktailPlatformGraphics.cmake)
|
||||
foreach(foundation_target IN ITEMS
|
||||
@@ -201,12 +197,12 @@ target_include_directories(mocktail_bionic_stdio_runtime PUBLIC include)
|
||||
mocktail_apply_compile_options(mocktail_bionic_stdio_runtime)
|
||||
|
||||
find_package(CURL REQUIRED)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
find_package(nlohmann_json CONFIG REQUIRED)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(LIBYAML REQUIRED IMPORTED_TARGET yaml-0.1)
|
||||
pkg_check_modules(MINIZIP REQUIRED IMPORTED_TARGET minizip)
|
||||
pkg_check_modules(CAPSTONE REQUIRED IMPORTED_TARGET capstone)
|
||||
pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
|
||||
pkg_check_modules(GTK4 REQUIRED IMPORTED_TARGET gtk4)
|
||||
pkg_check_modules(LIBADWAITA REQUIRED IMPORTED_TARGET "libadwaita-1>=1.6")
|
||||
pkg_check_modules(WEBKITGTK6 REQUIRED IMPORTED_TARGET webkitgtk-6.0)
|
||||
@@ -214,6 +210,7 @@ pkg_check_modules(WEBKITGTK6 REQUIRED IMPORTED_TARGET webkitgtk-6.0)
|
||||
add_library(mocktail_update STATIC
|
||||
src/update/android_manifest.cc
|
||||
src/update/apk_bundle.cc
|
||||
src/update/apk_provider.cc
|
||||
src/update/apk_signature.cc
|
||||
src/update/apkpure_provider.cc
|
||||
src/update/candidate_approval.cc
|
||||
@@ -223,6 +220,7 @@ add_library(mocktail_update STATIC
|
||||
src/update/payload_integrity.cc
|
||||
src/update/payload_store.cc
|
||||
src/update/readiness_canary.cc
|
||||
src/update/unsafe_latest_runner.cc
|
||||
src/update/update_config.cc
|
||||
src/update/update_coordinator.cc
|
||||
src/update/zip_archive.cc
|
||||
@@ -231,7 +229,6 @@ add_library(Mocktail::Update ALIAS mocktail_update)
|
||||
target_include_directories(mocktail_update PUBLIC include)
|
||||
target_link_libraries(mocktail_update PUBLIC
|
||||
Mocktail::Compat
|
||||
Mocktail::CryptoFoundation
|
||||
CURL::libcurl
|
||||
OpenSSL::Crypto
|
||||
PkgConfig::LIBYAML
|
||||
@@ -272,6 +269,7 @@ add_library(mocktail_runtime STATIC
|
||||
src/runtime/environment.cc
|
||||
src/runtime/external_launch_broker.cc
|
||||
src/runtime/frame_rate_policy.cc
|
||||
src/runtime/fleasion.cc
|
||||
src/runtime/game_mode.cc
|
||||
src/runtime/game_session_coordinator.cc
|
||||
src/runtime/graphics_launch_policy.cc
|
||||
@@ -288,7 +286,10 @@ add_library(mocktail_runtime STATIC
|
||||
src/runtime/roblox_launch_uri.cc
|
||||
src/runtime/roblox_game_session_native_adapter.cc
|
||||
src/runtime/roblox_web_view_bridge.cc
|
||||
src/runtime/roblox_permissions_bridge.cc
|
||||
src/runtime/payload_update_preflight.cc
|
||||
src/runtime/process_launch_policy.cc
|
||||
src/runtime/process_diagnostics.cc
|
||||
src/runtime/runtime_config.cc
|
||||
src/runtime/runtime_config_bootstrap.cc
|
||||
src/runtime/runtime_config_file.cc
|
||||
@@ -298,6 +299,9 @@ add_library(mocktail_runtime STATIC
|
||||
src/runtime/startup_state_machine.cc
|
||||
src/runtime/supported_launch_policy.cc
|
||||
src/runtime/support_bundle.cc
|
||||
src/runtime/system_proxy.cc
|
||||
src/runtime/texture_memory_policy.cc
|
||||
src/runtime/device_memory_profile.cc
|
||||
)
|
||||
add_library(Mocktail::Runtime ALIAS mocktail_runtime)
|
||||
target_include_directories(mocktail_runtime PUBLIC include)
|
||||
@@ -328,9 +332,10 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND
|
||||
endif()
|
||||
target_link_libraries(mocktail_runtime PUBLIC Threads::Threads)
|
||||
target_link_libraries(mocktail_runtime PRIVATE
|
||||
Mocktail::CryptoFoundation
|
||||
OpenSSL::Crypto
|
||||
CURL::libcurl
|
||||
nlohmann_json::nlohmann_json
|
||||
PkgConfig::GIO
|
||||
PkgConfig::LIBYAML
|
||||
${CMAKE_DL_LIBS}
|
||||
)
|
||||
@@ -421,6 +426,7 @@ target_link_libraries(mocktail_linker PUBLIC
|
||||
linker
|
||||
mocktail_compat
|
||||
mocktail_bionic_stdio_runtime
|
||||
mocktail_bionic_abi_exports
|
||||
)
|
||||
mocktail_apply_compile_options(mocktail_linker)
|
||||
target_compile_definitions(mocktail_linker PUBLIC MOCKTAIL_USE_BIONIC_LINKER=1)
|
||||
@@ -449,6 +455,7 @@ add_library(mocktail_window STATIC
|
||||
src/window/roblox_fullscreen_menu_request_gate.cc
|
||||
src/window/video_driver_policy.cc
|
||||
src/window/window.cc
|
||||
src/window/window_creation_policy.cc
|
||||
src/window/window_fullscreen_request_gate.cc
|
||||
src/window/window_game_surface_bridge.cc
|
||||
src/window/window_pointer_capture_owner.cc
|
||||
@@ -461,9 +468,10 @@ target_include_directories(mocktail_window PUBLIC src)
|
||||
mocktail_apply_compile_options(mocktail_window)
|
||||
target_link_libraries(mocktail_window PUBLIC
|
||||
Mocktail::PlatformSdl
|
||||
Mocktail::SdlVulkanWsi
|
||||
SDL3::SDL3
|
||||
${CMAKE_DL_LIBS}
|
||||
PRIVATE Mocktail::Runtime
|
||||
PRIVATE Mocktail::Runtime Mocktail::GlesTextOverlay
|
||||
)
|
||||
|
||||
add_library(mocktail_roblox_output_device_runtime STATIC
|
||||
@@ -525,6 +533,19 @@ target_include_directories(mocktail_bionic_abi_exports
|
||||
PRIVATE src
|
||||
)
|
||||
mocktail_apply_compile_options(mocktail_bionic_abi_exports)
|
||||
# These exported Bionic FORTIFY symbols intentionally overlap glibc's
|
||||
# __*_chk entry points. When the bridge is visible to whole-program LTO,
|
||||
# GCC can fold a fortified host call back into its own compatibility wrapper
|
||||
# and turn the resulting tail recursion into an infinite loop. Keep this
|
||||
# translation unit opaque to interprocedural optimization and make its calls
|
||||
# to the host C library explicit external calls.
|
||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||
set_property(SOURCE src/compat/bionic_abi_exports.cc APPEND PROPERTY
|
||||
COMPILE_OPTIONS
|
||||
-fno-lto
|
||||
-fno-builtin
|
||||
)
|
||||
endif()
|
||||
target_link_libraries(mocktail_bionic_abi_exports PRIVATE
|
||||
mocktail_bionic_stdio_runtime
|
||||
mocktail_window
|
||||
@@ -705,13 +726,24 @@ install(FILES
|
||||
config/roblox_signing_certificates.json
|
||||
DESTINATION "${MOCKTAIL_INSTALL_DATA_DIR}/metadata"
|
||||
)
|
||||
install(FILES packaging/space.bigrat.mocktail.desktop
|
||||
DESTINATION "${CMAKE_INSTALL_DATADIR}/applications"
|
||||
)
|
||||
install(FILES packaging/space.bigrat.mocktail.metainfo.xml
|
||||
# The installed desktop entry names the binary by its install path, so it
|
||||
# launches from sessions whose PATH lacks the install prefix. Uses the prefix
|
||||
# given at install time, not the configure-time one.
|
||||
install(CODE "
|
||||
file(READ \"${CMAKE_SOURCE_DIR}/packaging/io.github.CoderDayton.nightcap.desktop\"
|
||||
MOCKTAIL_DESKTOP_ENTRY)
|
||||
string(REPLACE \"Exec=env SDL_VIDEODRIVER=wayland,x11 mocktail %u\"
|
||||
\"Exec=env SDL_VIDEODRIVER=wayland,x11 \${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_BINDIR}/mocktail %u\"
|
||||
MOCKTAIL_DESKTOP_ENTRY \"\${MOCKTAIL_DESKTOP_ENTRY}\")
|
||||
file(WRITE \"${CMAKE_BINARY_DIR}/io.github.CoderDayton.nightcap.desktop\"
|
||||
\"\${MOCKTAIL_DESKTOP_ENTRY}\")
|
||||
file(INSTALL \"${CMAKE_BINARY_DIR}/io.github.CoderDayton.nightcap.desktop\"
|
||||
DESTINATION \"\${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/applications\")
|
||||
")
|
||||
install(FILES packaging/io.github.CoderDayton.nightcap.metainfo.xml
|
||||
DESTINATION "${CMAKE_INSTALL_DATADIR}/metainfo"
|
||||
)
|
||||
install(FILES packaging/space.bigrat.mocktail.svg
|
||||
install(FILES packaging/io.github.CoderDayton.nightcap.svg
|
||||
DESTINATION "${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps"
|
||||
)
|
||||
set(MOCKTAIL_HICOLOR_ICON_SIZES
|
||||
@@ -720,7 +752,7 @@ set(MOCKTAIL_HICOLOR_ICON_SIZES
|
||||
# Generated icons are committed, so ImageMagick is not a build dependency.
|
||||
foreach(icon_size IN LISTS MOCKTAIL_HICOLOR_ICON_SIZES)
|
||||
install(FILES
|
||||
"packaging/icons/hicolor/${icon_size}x${icon_size}/apps/space.bigrat.mocktail.png"
|
||||
"packaging/icons/hicolor/${icon_size}x${icon_size}/apps/io.github.CoderDayton.nightcap.png"
|
||||
DESTINATION
|
||||
"${CMAKE_INSTALL_DATADIR}/icons/hicolor/${icon_size}x${icon_size}/apps"
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
.DEFAULT_GOAL := all
|
||||
.PHONY: all build release debug test apk install register-url-handler release-runtime portable portable-test standalone appimage flatpak run-flatpak run run-smoke run-unlimited run-game run-input run-resize run-network auto-run run-gles run-angle update-roblox update-auto update-auto-launch payload-status payload-rollback support-bundle clean submodules help
|
||||
|
||||
@@ -14,9 +15,12 @@ PORTABLE_MODE ?= standalone
|
||||
PORTABLE_CANONICAL_MODE := $(if $(filter dynamic minimal,$(PORTABLE_MODE)),thin,$(if $(filter full static,$(PORTABLE_MODE)),standalone,$(PORTABLE_MODE)))
|
||||
PORTABLE_DIR ?= dist/mocktail-linux-x86_64-glibc-$(PORTABLE_CANONICAL_MODE)
|
||||
APPIMAGE ?= dist/Mocktail-x86_64.AppImage
|
||||
APPIMAGE_FORMAT ?= classic
|
||||
ANYLINUX_PACKAGER ?= quick-sharun
|
||||
ANYLINUX_APPIMAGETOOL ?= appimagetool
|
||||
PREFIX ?= /usr
|
||||
FLATPAK_BUILD_DIR ?= build-flatpak
|
||||
FLATPAK_MANIFEST ?= packaging/flatpak/space.bigrat.mocktail.json
|
||||
FLATPAK_MANIFEST ?= packaging/flatpak/io.github.CoderDayton.nightcap.json
|
||||
FLATPAK_JOBS ?= 4
|
||||
|
||||
define build_native_runtime
|
||||
@@ -52,7 +56,7 @@ install: ## Install an already-built dynamic runtime (use: sudo make install)
|
||||
register-url-handler: build ## Select this build for Roblox website links
|
||||
@./scripts/register_url_handler.sh --set-default \
|
||||
--executable "$(abspath $(BINARY))" \
|
||||
--desktop-file "$(abspath packaging/space.bigrat.mocktail.desktop)"
|
||||
--desktop-file "$(abspath packaging/io.github.CoderDayton.nightcap.desktop)"
|
||||
|
||||
test: ## Build and run all unit tests
|
||||
$(call build_native_runtime,Debug,ON)
|
||||
@@ -73,13 +77,11 @@ portable-test: release-runtime ## Verify portable packaging and relocation
|
||||
|
||||
standalone: appimage ## Build the standalone x86-64 AppImage
|
||||
|
||||
appimage: release-runtime ## Build AppImage (requires appimagetool)
|
||||
@command -v appimagetool >/dev/null 2>&1 || { \
|
||||
echo "appimagetool is required for make appimage" >&2; \
|
||||
echo "Install it, then rerun make appimage." >&2; \
|
||||
exit 1; \
|
||||
}
|
||||
@./scripts/package_portable.sh --build-dir "$(BUILD_DIR)" \
|
||||
appimage: release-runtime ## Build AppImage (APPIMAGE_FORMAT=classic|anylinux)
|
||||
@MOCKTAIL_APPIMAGE_FORMAT="$(APPIMAGE_FORMAT)" \
|
||||
MOCKTAIL_ANYLINUX_PACKAGER="$(ANYLINUX_PACKAGER)" \
|
||||
MOCKTAIL_ANYLINUX_APPIMAGETOOL="$(ANYLINUX_APPIMAGETOOL)" \
|
||||
./scripts/package_portable.sh --build-dir "$(BUILD_DIR)" \
|
||||
--libc glibc --output "$(PORTABLE_DIR)" --mode "$(PORTABLE_MODE)" \
|
||||
--appimage "$(APPIMAGE)"
|
||||
|
||||
@@ -90,7 +92,7 @@ flatpak: ## Build and install the local x86-64 Flatpak
|
||||
--jobs "$(FLATPAK_JOBS)"
|
||||
|
||||
run-flatpak: ## Run the installed Flatpak
|
||||
@flatpak run space.bigrat.mocktail
|
||||
@flatpak run io.github.CoderDayton.nightcap
|
||||
|
||||
run: export MOCKTAIL_AUTO_EXIT_AFTER_PRESENT_MS = 0
|
||||
run: build ## Run the native binary interactively until the window is closed
|
||||
|
||||
@@ -1,18 +1,67 @@
|
||||
# Mocktail
|
||||
<!-- Modified by vii from komaruworld/mocktail. See "About this fork" below. -->
|
||||
# Nightcap
|
||||
|
||||
[](https://github.com/komaruworld/mocktail/actions/workflows/ci.yml)
|
||||
[](https://github.com/komaruworld/mocktail/stargazers)
|
||||
[](https://github.com/komaruworld/mocktail/releases/latest)
|
||||
[](https://github.com/CoderDayton/nightcap/actions/workflows/ci.yml)
|
||||
[](https://github.com/CoderDayton/nightcap/stargazers)
|
||||
[](LICENSE)
|
||||
|
||||
Mocktail runs the Android `x86_64` Roblox client on Linux, including a Linux
|
||||
userspace hosted by FreeBSD's Linuxulator. It provides the Android ABI and JNI
|
||||
pieces the client expects, then connects them to SDL3 and Vulkan or OpenGL on
|
||||
the Linux side.
|
||||
Nightcap is Mocktail, tuned for playing on a real PC. It runs the Android
|
||||
`x86_64` Roblox client on Linux, including a Linux userspace hosted by
|
||||
FreeBSD's Linuxulator. It provides the Android ABI and JNI pieces the client
|
||||
expects, then connects them to SDL3 and Vulkan or OpenGL on the Linux side.
|
||||
|
||||
Mocktail is an independent community project. It is not affiliated with Roblox
|
||||
Nightcap is an independent community project. It is not affiliated with Roblox
|
||||
Corporation or VinegarHQ and does not distribute the Roblox client.
|
||||
|
||||
## About this fork
|
||||
|
||||
Nightcap is a fork of [komaruworld/mocktail](https://github.com/komaruworld/mocktail)
|
||||
with extra fixes for playing on a PC. The program and config paths keep the
|
||||
`mocktail` name so upstream fixes still apply cleanly. The app ID is
|
||||
`io.github.CoderDayton.nightcap`, so Nightcap and upstream can be installed
|
||||
side by side.
|
||||
|
||||
- **Lower input lag.** Frames are shown as soon as they are ready instead of
|
||||
waiting in a queue.
|
||||
- **Sharper textures.** Small textures are upscaled 4x by default, and you
|
||||
can replace any texture with your own PNG in `~/.config/mocktail/textures`.
|
||||
Set `MOCKTAIL_SMALL_TEXTURE_UPSCALE=1` to turn the upscale off.
|
||||
- **ETC2 textures on any GPU.** Games that use compressed mobile textures now
|
||||
render correctly on desktop GPUs, without stutter.
|
||||
- **No more error 319 kicks.** Servers no longer drop you shortly after
|
||||
joining.
|
||||
- **Correct device info.** Roblox sees your real RAM and screen size.
|
||||
- **Better desktop launcher.** Runs on Wayland and always finds the installed
|
||||
binary.
|
||||
|
||||
Everything else works the same as upstream. See the
|
||||
[roadmap](docs/ROADMAP.md) for what is coming next.
|
||||
|
||||
## Install with Flatpak
|
||||
|
||||
```bash
|
||||
flatpak install --user https://coderdayton.github.io/nightcap/nightcap.flatpakref
|
||||
flatpak run io.github.CoderDayton.nightcap
|
||||
```
|
||||
|
||||
Updates arrive through `flatpak update`. Nightcap is not on Flathub yet.
|
||||
|
||||
## AppImage
|
||||
|
||||
Grab the AppImage from the
|
||||
[latest release](https://github.com/CoderDayton/nightcap/releases/latest),
|
||||
make it executable, and run it:
|
||||
|
||||
```bash
|
||||
chmod +x Nightcap-x86_64.AppImage
|
||||
./Nightcap-x86_64.AppImage
|
||||
```
|
||||
|
||||
Or build and install it from source using the steps under
|
||||
[Building](#building). There are no AUR, DEB, or RPM packages of Nightcap.
|
||||
Upstream ships those for unmodified Mocktail, see
|
||||
[komaruworld/mocktail](https://github.com/komaruworld/mocktail#readme).
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
@@ -20,91 +69,28 @@ Roblox APK -> signature and ABI checks -> Bionic + JNI -> SDL3 + Vulkan/OpenGL
|
||||
```
|
||||
|
||||
The APK is checked before any native code is loaded. It is downloaded on first
|
||||
launch and is not bundled with Mocktail. The last working copy is kept in case
|
||||
launch and is not bundled with Nightcap. The last working copy is kept in case
|
||||
an update fails.
|
||||
|
||||
## Install with Flatpak
|
||||
|
||||
Install the stable release from Flathub:
|
||||
|
||||
```bash
|
||||
flatpak install flathub space.bigrat.mocktail
|
||||
flatpak run space.bigrat.mocktail
|
||||
```
|
||||
|
||||
Nightly builds are produced automatically from the latest `main` branch:
|
||||
|
||||
```bash
|
||||
flatpak install --user https://mocktail.bigrat.space/mocktail.flatpakref
|
||||
flatpak run space.bigrat.mocktail
|
||||
```
|
||||
|
||||
## Install from the AUR
|
||||
|
||||
Arch Linux users can install either the pinned source release (`mocktail`) or
|
||||
the current development version (`mocktail-git`) with an AUR helper:
|
||||
|
||||
```bash
|
||||
paru -S mocktail-git
|
||||
# or
|
||||
yay -S mocktail-git
|
||||
```
|
||||
|
||||
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`.
|
||||
|
||||
## Install with DNF
|
||||
|
||||
Fedora 44 users can install either the stable `mocktail` package or the
|
||||
`mocktail-nightly` package built from `main`:
|
||||
|
||||
```bash
|
||||
sudo curl -fsSL https://mocktail.bigrat.space/rpm/mocktail.repo \
|
||||
-o /etc/yum.repos.d/mocktail.repo
|
||||
sudo dnf install mocktail
|
||||
# or
|
||||
sudo dnf install mocktail-nightly
|
||||
```
|
||||
|
||||
## Install with APT
|
||||
|
||||
Ubuntu 26.04 users can install either the stable `mocktail` package or the
|
||||
`mocktail-nightly` package built from `main`:
|
||||
|
||||
```bash
|
||||
sudo install -d -m 0755 /etc/apt/keyrings
|
||||
sudo curl -fsSL https://mocktail.bigrat.space/mocktail-packages.gpg \
|
||||
-o /etc/apt/keyrings/mocktail.gpg
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/mocktail.gpg] https://mocktail.bigrat.space/apt mocktail main" | \
|
||||
sudo tee /etc/apt/sources.list.d/mocktail.list >/dev/null
|
||||
sudo apt update
|
||||
sudo apt install mocktail
|
||||
# or
|
||||
sudo apt install mocktail-nightly
|
||||
```
|
||||
|
||||
## Direct downloads
|
||||
|
||||
Direct AppImage, DEB, and RPM downloads are available from the
|
||||
[Website](https://mocktail.bigrat.space/) and
|
||||
[GitHub Releases](https://github.com/komaruworld/mocktail/releases).
|
||||
|
||||
<details>
|
||||
<summary>Screenshots</summary>
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
</details>
|
||||
|
||||
## FFlag overrides
|
||||
|
||||
Put a JSON object in `$XDG_CONFIG_HOME/mocktail/fflags.json` (usually
|
||||
`~/.config/mocktail/fflags.json`). Mocktail applies it on the next launch.
|
||||
`~/.config/mocktail/fflags.json`). Nightcap applies it on the next launch.
|
||||
|
||||
PC profiles use the legacy Charts page by default to avoid the blank screen
|
||||
caused by `Color3` errors in the newer SDUI page. An explicit
|
||||
`FFlagLuaAppChartsAppPage` override takes precedence over this default.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -112,11 +98,18 @@ Put a JSON object in `$XDG_CONFIG_HOME/mocktail/fflags.json` (usually
|
||||
"DFIntExample": "120"
|
||||
}
|
||||
```
|
||||
## Settings
|
||||
|
||||
Edit `~/.config/mocktail/config.yaml`. Useful options:
|
||||
|
||||
- `graphics.frame_rate_limit: display` to unlock FPS to your monitor's rate.
|
||||
- `graphics.vsync: off` for the lowest lag, at the cost of tearing.
|
||||
- `graphics.backend: opengl` if Vulkan does not work on your GPU.
|
||||
|
||||
## Building
|
||||
|
||||
Linux `x86_64` is supported. Experimental FreeBSD 15.1 Linuxulator support has
|
||||
been tested with an `x86_64` Fedora 44 userspace. On FreeBSD, Mocktail runs
|
||||
been tested with an `x86_64` Fedora 44 userspace. On FreeBSD, Nightcap runs
|
||||
inside Linuxulator; it is not a native FreeBSD binary. Building requires CMake
|
||||
3.20+, Git, pkg-config, LLD, binutils, a C++17 compiler, SDL 3.4+, SDL3_ttf,
|
||||
Vulkan, EGL, libplacebo, fontconfig, libcurl, OpenSSL, libelf, libyaml, minizip,
|
||||
@@ -163,19 +156,48 @@ sudo dnf install gcc-c++ cmake git ninja-build pkgconf-pkg-config lld \
|
||||
</details>
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/komaruworld/mocktail.git
|
||||
cd mocktail
|
||||
git clone --recurse-submodules https://github.com/CoderDayton/nightcap.git
|
||||
cd nightcap
|
||||
make build
|
||||
./build/mocktail
|
||||
```
|
||||
|
||||
To install it for your user, with an app menu entry and Roblox link handling:
|
||||
|
||||
```bash
|
||||
make install PREFIX=~/.local
|
||||
```
|
||||
|
||||
Use `sudo make install` instead to install system-wide under `/usr`. To make
|
||||
Roblox website links open the build tree copy without installing, run
|
||||
`make register-url-handler`.
|
||||
|
||||
To run the same checks as CI before each commit and push, install
|
||||
[lefthook](https://github.com/evilmartians/lefthook) and run once:
|
||||
|
||||
```bash
|
||||
lefthook install
|
||||
```
|
||||
|
||||
## Known limitations
|
||||
|
||||
Nightcap does not currently work with `hardened_malloc`. Using
|
||||
`hardened_malloc` may cause Nightcap to fail to start or crash during runtime.
|
||||
Run Nightcap without `hardened_malloc` enabled.
|
||||
|
||||
## License
|
||||
|
||||
[Apache License 2.0](LICENSE). Third-party components keep their own licenses.
|
||||
|
||||
## Support
|
||||
|
||||
You can support the project by giving it a star or with cryptocurrency:
|
||||
Support this fork by giving it a star or with cryptocurrency:
|
||||
|
||||
- LTC: `LdABR2ELRYrUESWZWrEk1uNx38gHHMuHUU`
|
||||
- SOL: `tjeDnPoWeyW8zfTf8c5CVAm5QTV8vFPaPouaScAdwfT`
|
||||
- BCH: `qpktw5jwut9ae8e6898qt3v7rj3km44utgk2r9mj8n`
|
||||
|
||||
Support the upstream project, which does most of the heavy lifting:
|
||||
|
||||
- USDT (TON): `UQCi6Yzcc9cOctoij6n_r1K90-OdVxAT0D_xo2UzGKkQaJDY`
|
||||
- USDT (TRC20): `TNPMG9Vig2xiuo2r1QqnXRChPH7Vu28Jmx`
|
||||
@@ -24,6 +24,7 @@ set_target_properties(mocktail_audio_core PROPERTIES
|
||||
add_library(Mocktail::AudioCore ALIAS mocktail_audio_core)
|
||||
|
||||
add_library(mocktail_audio_sdl SHARED
|
||||
${MOCKTAIL_AUDIO_ROOT}/src/audio/sdl_audio_capture.cc
|
||||
${MOCKTAIL_AUDIO_ROOT}/src/audio/sdl_audio_sink.cc
|
||||
)
|
||||
target_include_directories(mocktail_audio_sdl PUBLIC
|
||||
@@ -62,6 +63,7 @@ add_library(Mocktail::FmodJavaAudioRuntime ALIAS
|
||||
|
||||
add_library(mocktail_fmod_jni_audio_bridge STATIC
|
||||
${MOCKTAIL_AUDIO_ROOT}/src/audio/fmod_jni_audio_bridge.cc
|
||||
${MOCKTAIL_AUDIO_ROOT}/src/audio/webrtc_jni_audio_bridge.cc
|
||||
)
|
||||
target_include_directories(mocktail_fmod_jni_audio_bridge PUBLIC
|
||||
${MOCKTAIL_AUDIO_ROOT}/include
|
||||
@@ -199,10 +201,4 @@ if(BUILD_TESTING AND TARGET GTest::gtest_main)
|
||||
gtest_discover_tests(fmod_jni_audio_bridge_test
|
||||
PROPERTIES ENVIRONMENT "SDL_AUDIODRIVER=dummy"
|
||||
)
|
||||
|
||||
add_test(NAME audio_readiness_gate_test
|
||||
COMMAND bash
|
||||
${MOCKTAIL_AUDIO_ROOT}/tests/audio_readiness_gate_test.sh
|
||||
${MOCKTAIL_AUDIO_ROOT}/scripts/audio_readiness_gate.sh
|
||||
)
|
||||
endif()
|
||||
@@ -44,7 +44,7 @@ target_link_libraries(mocktail_compat PUBLIC
|
||||
nlohmann_json::nlohmann_json
|
||||
Threads::Threads
|
||||
)
|
||||
target_link_libraries(mocktail_compat PRIVATE Mocktail::CryptoFoundation)
|
||||
target_link_libraries(mocktail_compat PRIVATE OpenSSL::Crypto)
|
||||
target_compile_features(mocktail_compat PUBLIC cxx_std_17)
|
||||
|
||||
if(COMMAND mocktail_apply_compile_options)
|
||||
|
||||
@@ -13,11 +13,13 @@ get_filename_component(MOCKTAIL_INPUT_ROOT "${CMAKE_CURRENT_LIST_DIR}/.."
|
||||
)
|
||||
|
||||
add_library(mocktail_input_runtime STATIC
|
||||
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_gamepad_input.cc
|
||||
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_input_native_adapter.cc
|
||||
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_input_router.cc
|
||||
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_native_text_box_info_reader.cc
|
||||
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_text_display_state.cc
|
||||
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_text_editor.cc
|
||||
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_text_font_resolver.cc
|
||||
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_text_surface_overlay.cc
|
||||
${MOCKTAIL_INPUT_ROOT}/src/runtime/roblox_window_input_runtime.cc
|
||||
)
|
||||
@@ -36,12 +38,14 @@ target_link_libraries(mocktail_input_runtime PUBLIC
|
||||
PRIVATE
|
||||
PkgConfig::FONTCONFIG
|
||||
SDL3_ttf::SDL3_ttf
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
target_compile_features(mocktail_input_runtime PUBLIC cxx_std_17)
|
||||
mocktail_apply_compile_options(mocktail_input_runtime)
|
||||
|
||||
if(BUILD_TESTING AND TARGET GTest::gtest_main)
|
||||
add_executable(roblox_input_router_test
|
||||
${MOCKTAIL_INPUT_ROOT}/tests/roblox_gamepad_input_test.cc
|
||||
${MOCKTAIL_INPUT_ROOT}/tests/roblox_input_router_test.cc
|
||||
)
|
||||
target_link_libraries(roblox_input_router_test PRIVATE
|
||||
@@ -68,6 +72,15 @@ if(BUILD_TESTING AND TARGET GTest::gtest_main)
|
||||
)
|
||||
mocktail_apply_compile_options(roblox_text_display_state_test)
|
||||
|
||||
add_executable(roblox_text_font_resolver_test
|
||||
${MOCKTAIL_INPUT_ROOT}/tests/roblox_text_font_resolver_test.cc
|
||||
)
|
||||
target_link_libraries(roblox_text_font_resolver_test PRIVATE
|
||||
Mocktail::InputRuntime
|
||||
GTest::gtest_main
|
||||
)
|
||||
mocktail_apply_compile_options(roblox_text_font_resolver_test)
|
||||
|
||||
add_executable(roblox_input_native_adapter_test
|
||||
${MOCKTAIL_INPUT_ROOT}/tests/roblox_input_native_adapter_test.cc
|
||||
)
|
||||
@@ -92,9 +105,7 @@ if(BUILD_TESTING AND TARGET GTest::gtest_main)
|
||||
gtest_discover_tests(roblox_input_router_test)
|
||||
gtest_discover_tests(roblox_text_editor_test)
|
||||
gtest_discover_tests(roblox_text_display_state_test)
|
||||
gtest_discover_tests(roblox_text_font_resolver_test)
|
||||
gtest_discover_tests(roblox_input_native_adapter_test)
|
||||
gtest_discover_tests(roblox_native_text_box_info_reader_test)
|
||||
add_test(NAME input_readiness_gate_synthetic
|
||||
COMMAND bash ${MOCKTAIL_INPUT_ROOT}/tests/input_readiness_gate_test.sh
|
||||
)
|
||||
endif()
|
||||
@@ -1,3 +1,4 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# Licensed under the Apache License, Version 2.0.
|
||||
|
||||
@@ -9,9 +10,10 @@ get_filename_component(MOCKTAIL_PLATFORM_GRAPHICS_ROOT
|
||||
|
||||
find_package(SDL3 3.4 REQUIRED CONFIG)
|
||||
find_path(MOCKTAIL_EGL_INCLUDE_DIR EGL/egl.h REQUIRED)
|
||||
find_path(MOCKTAIL_GLES3_INCLUDE_DIR GLES3/gl3.h REQUIRED)
|
||||
|
||||
set(MOCKTAIL_WINDOW_ICON_PNG
|
||||
"${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/packaging/icons/hicolor/48x48/apps/space.bigrat.mocktail.png"
|
||||
"${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/packaging/icons/hicolor/48x48/apps/io.github.CoderDayton.nightcap.png"
|
||||
)
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
|
||||
"${MOCKTAIL_WINDOW_ICON_PNG}"
|
||||
@@ -69,6 +71,7 @@ add_library(mocktail_platform_sdl STATIC
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_application_metadata.cc
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_display_refresh_capabilities.cc
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_event_converter.cc
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_gamepad_manager.cc
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_platform_runtime.cc
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_text_clipboard.cc
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/platform/sdl_window_icon.cc
|
||||
@@ -102,6 +105,18 @@ target_link_libraries(mocktail_graphics_foundation PRIVATE
|
||||
target_compile_features(mocktail_graphics_foundation PUBLIC cxx_std_17)
|
||||
add_library(Mocktail::GraphicsFoundation ALIAS mocktail_graphics_foundation)
|
||||
|
||||
add_library(mocktail_gles_text_overlay STATIC
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/graphics/gles_text_overlay_compositor.cc
|
||||
)
|
||||
target_include_directories(mocktail_gles_text_overlay
|
||||
PUBLIC ${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/include
|
||||
PRIVATE ${MOCKTAIL_GLES3_INCLUDE_DIR}
|
||||
)
|
||||
target_link_libraries(mocktail_gles_text_overlay PUBLIC SDL3::SDL3)
|
||||
target_compile_features(mocktail_gles_text_overlay PUBLIC cxx_std_17)
|
||||
mocktail_apply_compile_options(mocktail_gles_text_overlay)
|
||||
add_library(Mocktail::GlesTextOverlay ALIAS mocktail_gles_text_overlay)
|
||||
|
||||
add_library(mocktail_sdl_vulkan_wsi STATIC
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/graphics/sdl_vulkan_wsi.cc
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/src/graphics/android_vulkan_wsi_adapter.cc
|
||||
@@ -119,6 +134,16 @@ target_compile_features(mocktail_sdl_vulkan_wsi PUBLIC cxx_std_17)
|
||||
add_library(Mocktail::SdlVulkanWsi ALIAS mocktail_sdl_vulkan_wsi)
|
||||
|
||||
if(BUILD_TESTING AND TARGET GTest::gtest_main)
|
||||
add_executable(gles_text_overlay_compositor_test
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/tests/gles_text_overlay_compositor_test.cc
|
||||
)
|
||||
target_include_directories(gles_text_overlay_compositor_test PRIVATE
|
||||
${MOCKTAIL_GLES3_INCLUDE_DIR}
|
||||
)
|
||||
target_link_libraries(gles_text_overlay_compositor_test PRIVATE
|
||||
Mocktail::GlesTextOverlay
|
||||
GTest::gtest_main
|
||||
)
|
||||
add_executable(bionic_egl_bridge_test
|
||||
${MOCKTAIL_PLATFORM_GRAPHICS_ROOT}/tests/bionic_egl_bridge_test.cc
|
||||
)
|
||||
@@ -152,6 +177,7 @@ if(BUILD_TESTING AND TARGET GTest::gtest_main)
|
||||
GTest::gtest_main
|
||||
)
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(gles_text_overlay_compositor_test)
|
||||
gtest_discover_tests(bionic_egl_bridge_test)
|
||||
gtest_discover_tests(platform_graphics_foundation_test)
|
||||
gtest_discover_tests(display_refresh_capabilities_test)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
# Mocktail configuration.
|
||||
# The file is created once with mode 0600. Mocktail never replaces your edits.
|
||||
|
||||
@@ -31,17 +32,25 @@ runtime:
|
||||
# active verified payload.
|
||||
# roblox_library: /absolute/path/to/libroblox.so
|
||||
|
||||
appearance:
|
||||
# String (default: roblox): use Roblox's saved account theme. Supported
|
||||
# overrides: dark, light, or system (follow the desktop color scheme).
|
||||
theme: roblox
|
||||
|
||||
graphics:
|
||||
# String (default: direct-vulkan): graphics backend. Use opengl on hardware
|
||||
# without a working Vulkan driver; OpenGL ES 3.0 or newer is required.
|
||||
# Recommended values: direct-vulkan, opengl, system, angle-vulkan.
|
||||
backend: direct-vulkan
|
||||
# String or integer (default: display): requested frame-rate policy.
|
||||
# Supported values: display, unlimited, 30, 60, 120, 144, 240. Unlimited
|
||||
# selects Roblox's unmodified 240-FPS scheduler maximum.
|
||||
frame_rate_limit: display
|
||||
# String (default: auto): presentation synchronization: auto, on, or off.
|
||||
vsync: auto
|
||||
# Optional string or integer: Mocktail frame-rate override. Leave commented
|
||||
# (or use -1) to let Roblox Basic Settings own the cap. Supported overrides:
|
||||
# display, unlimited, or any positive integer; fixed values are forwarded to
|
||||
# DFIntTaskSchedulerTargetFps without a whitelist.
|
||||
# frame_rate_limit: -1
|
||||
# Optional presentation synchronization override: auto, on, or off. auto
|
||||
# presents through the lowest-latency synchronized mode the driver offers
|
||||
# unless frame_rate_limit is unlimited.
|
||||
# vsync: off
|
||||
|
||||
performance:
|
||||
# Boolean (default: false): size Roblox scheduler/render queues from every
|
||||
@@ -63,8 +72,22 @@ audio:
|
||||
# String (default: default): `default` follows the host system output.
|
||||
# To pin output, copy an exact SDL device name printed during startup.
|
||||
output_device: default
|
||||
# String (default: default): input device name, `id:<number>` printed during
|
||||
# startup, or `disabled`. IDs disambiguate duplicate names but may change
|
||||
# between boots; prefer the exact device name when it is unique.
|
||||
input_device: default
|
||||
|
||||
integrations:
|
||||
fleasion:
|
||||
# Boolean (default: false): trust Fleasion's CA without editing Roblox files.
|
||||
enabled: false
|
||||
# String: match Fleasion's routing mode: env or hosts.
|
||||
proxy_mode: env
|
||||
# Integer: Fleasion's local HTTP proxy port in env mode.
|
||||
proxy_port: 58443
|
||||
# Optional absolute public CA path. Default: $XDG_CONFIG_HOME/Fleasion/proxy_ca/ca.crt
|
||||
# or ~/.config/Fleasion/proxy_ca/ca.crt. Never select ca.key.
|
||||
# ca_certificate: /home/user/.config/Fleasion/proxy_ca/ca.crt
|
||||
discord_rpc:
|
||||
# Boolean (default: false): publish Mocktail activity to Discord Desktop.
|
||||
# This never signs in to Discord and never reads an account token.
|
||||
@@ -91,20 +114,29 @@ integrations:
|
||||
# application_id: 123456789012345678
|
||||
|
||||
window:
|
||||
# Integer (default: 1280): initial window width in pixels.
|
||||
# Integer (default: 1280): initial window width in logical desktop units.
|
||||
width: 1280
|
||||
# Integer (default: 720): initial window height in pixels.
|
||||
# Integer (default: 720): initial window height in logical desktop units.
|
||||
height: 720
|
||||
# String (default: Roblox): window title.
|
||||
title: Roblox
|
||||
# Boolean (default: false): render at physical display-pixel density instead
|
||||
# of the logical desktop resolution. Enable only for sharper high-DPI output.
|
||||
high_dpi: false
|
||||
|
||||
# Optional host HTTP proxy. Uncomment both values together. The host must not
|
||||
# contain a scheme such as http://.
|
||||
# network:
|
||||
# # String (optional, default: disabled): proxy host name or IP address.
|
||||
# proxy_host: 127.0.0.1
|
||||
# # Integer (optional, default: disabled): proxy TCP port from 1 to 65535.
|
||||
# proxy_port: 8080
|
||||
network:
|
||||
# Boolean (default: false): follow the host HTTP/SOCKS5 proxy and
|
||||
# automatically use its current port. Do not combine with a fixed proxy.
|
||||
use_system_proxy: false
|
||||
# Optional fixed HTTP proxy. Uncomment both values together. The host must
|
||||
# not contain a scheme such as http://.
|
||||
# String (optional, default: disabled): proxy host name or IP address.
|
||||
# proxy_host: 127.0.0.1
|
||||
# Integer (optional, default: disabled): proxy TCP port from 1 to 65535.
|
||||
# proxy_port: 8080
|
||||
# Absolute path (optional): custom PEM CA bundle. Mocktail reads this file
|
||||
# directly and does not replace it during Roblox payload updates.
|
||||
# ca_bundle: /home/user/.config/mocktail/cacert.pem
|
||||
|
||||
updates:
|
||||
# Boolean (default: true): check the latest Roblox version, derive an exact
|
||||
@@ -112,7 +144,9 @@ updates:
|
||||
# backend, and promote only on success. An existing current payload is
|
||||
# preserved when probation fails.
|
||||
automatic: true
|
||||
# String (default: apk-pure): direct x86_64 APK provider.
|
||||
# String (default: apk-pure): direct x86_64 APK provider selection. Both
|
||||
# accepted values resolve the provider chain, which currently starts with
|
||||
# APKPure. Supported values: auto, apk-pure.
|
||||
source: apk-pure
|
||||
# Reserved for desktop update integrations. `mocktail_updater` itself never
|
||||
# launches another process after changing the active payload.
|
||||
|
||||
@@ -42,6 +42,50 @@
|
||||
"allow_host_abi_bridges": true,
|
||||
"allow_host_constructor_replay": true,
|
||||
"reason": "Verified 2026-07-16 on the current host: independently signature-matched native allocator and pre-JNI state, 3479 constructors replayed without recovery, JNI_OnLoad and ASMA/V2 startup, 2988 Vulkan shaders, a real vkQueuePresentKHR, and explicit lifecycle teardown to Stopped; legacy executable patches and signal recovery remain disabled."
|
||||
},
|
||||
{
|
||||
"version_name": "2.734.917",
|
||||
"version_code": 2908,
|
||||
"elf_build_id": "63c5109637b7d7b2bdb8ed8f858023ff5ef49326",
|
||||
"status": "supported",
|
||||
"default_allowed": true,
|
||||
"allow_legacy_binary_patches": false,
|
||||
"allow_host_abi_bridges": true,
|
||||
"allow_host_constructor_replay": true,
|
||||
"user_game_settings_fullscreen_setter_rva": "0x44fee64",
|
||||
"fmod_output_device_bridge": {
|
||||
"vtable_rva": "0x6b7eee8",
|
||||
"string_constructor_rva": "0x1cfd5ec",
|
||||
"count_method_rva": "0x32736d2",
|
||||
"info_method_rva": "0x3273772",
|
||||
"current_method_rva": "0x3273722",
|
||||
"select_method_rva": "0x32734a6"
|
||||
},
|
||||
"reason": "Verified 2026-08-28 on the current host: the machine-derived exact-Build-ID profile passed two isolated no-recovery Tier C Vulkan canaries; fullscreen state synchronization and Build-ID-scoped FmodAudioDevice output interposition were re-derived for the current payload; legacy executable patches and signal recovery remain disabled."
|
||||
},
|
||||
{
|
||||
"version_name": "2.736.1408",
|
||||
"version_code": 2998,
|
||||
"elf_build_id": "ade08266c67aee88ec9c1d00902150e1684dad3a",
|
||||
"status": "supported",
|
||||
"default_allowed": true,
|
||||
"allow_legacy_binary_patches": false,
|
||||
"allow_host_abi_bridges": true,
|
||||
"allow_host_constructor_replay": true,
|
||||
"user_game_settings_fullscreen_setter_rva": "0x45ad8aa",
|
||||
"fmod_output_device_bridge": {
|
||||
"vtable_rva": "0x6c58040",
|
||||
"string_constructor_rva": "0x1d32df8",
|
||||
"count_method_rva": "0x32d0ee0",
|
||||
"info_method_rva": "0x32d0f80",
|
||||
"current_method_rva": "0x32d0f30",
|
||||
"select_method_rva": "0x32d0cb4",
|
||||
"input_count_method_rva": "0x32d12dc",
|
||||
"input_info_method_rva": "0x32d12ea",
|
||||
"input_current_method_rva": "0x32d13a6",
|
||||
"input_select_method_rva": "0x32d0a56"
|
||||
},
|
||||
"reason": "Verified 2026-09-06: exact Build-ID profile passed two isolated no-recovery Tier C Vulkan canaries and real game sessions, including fullscreen state synchronization and host FMOD output-device interposition; legacy executable patches and signal recovery remain disabled."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,64 +1,64 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"elf_build_id": "1686400865ae0e408cd7bd67de7a439625c6fd13",
|
||||
"payload_sha256": "3e9c26c81186f93458ff65d8a8bc240c220974db02b8388b91340b77b336997d",
|
||||
"payload_id": "2628-1686400865ae0e408cd7bd67de7a439625c6fd13",
|
||||
"payload_path": "payloads/2628-1686400865ae0e408cd7bd67de7a439625c6fd13",
|
||||
"elf_build_id": "ade08266c67aee88ec9c1d00902150e1684dad3a",
|
||||
"payload_sha256": "4343a6a900d1fca27ff8e10b9d9c86bee40a5d0ba547cb29903b7769f4f42a9d",
|
||||
"payload_id": "2998-ade08266c67aee88ec9c1d00902150e1684dad3a",
|
||||
"payload_path": "payloads/2998-ade08266c67aee88ec9c1d00902150e1684dad3a",
|
||||
"reference": {
|
||||
"elf_build_id": "d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21",
|
||||
"payload_sha256": "92ca0e4d8bc4e1c7a37fc2b52544869299ec8b9a40ea8da96ebac66660e0b1af"
|
||||
"elf_build_id": "63c5109637b7d7b2bdb8ed8f858023ff5ef49326",
|
||||
"payload_sha256": "72c3b1a5c151b5ac0bfc5394ce3a91775884fbd777589ccab8ac1c4f9885f739"
|
||||
},
|
||||
"profile": {
|
||||
"elf_build_id": "1686400865ae0e408cd7bd67de7a439625c6fd13",
|
||||
"elf_build_id": "ade08266c67aee88ec9c1d00902150e1684dad3a",
|
||||
"bridge_entries": [
|
||||
{
|
||||
"rva": "0x1c39ea2",
|
||||
"rva": "0x1d2c1e2",
|
||||
"kind": "allocate",
|
||||
"label": "small-allocate"
|
||||
},
|
||||
{
|
||||
"rva": "0x1fe4c84",
|
||||
"rva": "0x1d2f43a",
|
||||
"kind": "usable_size",
|
||||
"label": "usable-size"
|
||||
},
|
||||
{
|
||||
"rva": "0x203daae",
|
||||
"rva": "0x21e68bf",
|
||||
"kind": "reallocate",
|
||||
"label": "reallocate"
|
||||
},
|
||||
{
|
||||
"rva": "0x1c39c3a",
|
||||
"rva": "0x1d2bf82",
|
||||
"kind": "allocate",
|
||||
"label": "allocate"
|
||||
},
|
||||
{
|
||||
"rva": "0x1c8dbf8",
|
||||
"rva": "0x1d47a63",
|
||||
"kind": "aligned_allocate",
|
||||
"label": "aligned-allocate-direct"
|
||||
},
|
||||
{
|
||||
"rva": "0x1c3eb72",
|
||||
"rva": "0x1d2f9d9",
|
||||
"kind": "free",
|
||||
"label": "free"
|
||||
}
|
||||
],
|
||||
"data_seeds": {
|
||||
"allocator_object_slot": "0x7030ae0",
|
||||
"empty_string_slot": "0x703a7e8",
|
||||
"jni_singleton_slot": "0x72a16d0",
|
||||
"allocator_object_slot": "0x7652e70",
|
||||
"empty_string_slot": "0x775fd28",
|
||||
"jni_singleton_slot": "0x7bb4340",
|
||||
"jni_singleton_bytes": 1024,
|
||||
"arena_initializer": "0x1c39f1d",
|
||||
"allocator_thread_initializer": "0x2040817",
|
||||
"arena_guard_slot": "0x71e92f0",
|
||||
"arena_table_slot": "0x71e9380",
|
||||
"arena_initializer": "0x1d2c5f8",
|
||||
"allocator_thread_initializer": "0x1db7b57",
|
||||
"arena_guard_slot": "0x7af0430",
|
||||
"arena_table_slot": "0x7af04c0",
|
||||
"arena_table_slot_count": 4194304
|
||||
},
|
||||
"native_allocator": {
|
||||
"allocate": "0x1c39c3a",
|
||||
"deallocate": "0x1c3eb72"
|
||||
"allocate": "0x1d2bf82",
|
||||
"deallocate": "0x1d2f9d9"
|
||||
},
|
||||
"init_array_offset": "0x6c2c9b0",
|
||||
"init_array_count": 3481,
|
||||
"init_array_offset": "0x7057818",
|
||||
"init_array_count": 3570,
|
||||
"constructor_run_ranges": [
|
||||
{
|
||||
"begin": 2,
|
||||
@@ -66,32 +66,32 @@
|
||||
},
|
||||
{
|
||||
"begin": 5,
|
||||
"end_exclusive": 3481
|
||||
"end_exclusive": 3570
|
||||
}
|
||||
],
|
||||
"native_mimalloc_constructor_run_ranges": [
|
||||
{
|
||||
"begin": 2,
|
||||
"end_exclusive": 3481
|
||||
"end_exclusive": 3570
|
||||
}
|
||||
],
|
||||
"native_mimalloc_thread_initializer_after_constructor": 2,
|
||||
"native_pre_jni_bootstrap": {
|
||||
"registry_initializer": "0x1fe72e5",
|
||||
"registry_slot": "0x71ed428"
|
||||
"registry_initializer": "0x21e8e09",
|
||||
"registry_slot": "0x7af4598"
|
||||
},
|
||||
"default_allocator_strategy": "native_mimalloc"
|
||||
},
|
||||
"derivation_anchors": {
|
||||
"signature_version": 1,
|
||||
"allocator_object_initializer_rva": "0x1fbf1de",
|
||||
"empty_string_initializer_rva": "0x1fc82b0",
|
||||
"jni_singleton_initializer_rva": "0x1c39a34",
|
||||
"allocator_object_initializer_rva": "0x21bde7d",
|
||||
"empty_string_initializer_rva": "0x21c9a86",
|
||||
"jni_singleton_initializer_rva": "0x1d2aec0",
|
||||
"constructor_rvas": {
|
||||
"2": "0x1c3c764",
|
||||
"3": "0x1c38c00",
|
||||
"4": "0x1c39884",
|
||||
"5": "0x1c39a34"
|
||||
"2": "0x1d2f731",
|
||||
"3": "0x1d2af84",
|
||||
"4": "0x1d2bc08",
|
||||
"5": "0x1d2aec0"
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-11
@@ -1,23 +1,24 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"package": "com.roblox.client",
|
||||
"version_name": "2.725.1142",
|
||||
"version_code": 2546,
|
||||
"version_name": "2.736.1408",
|
||||
"version_code": 2998,
|
||||
"abi": "x86_64",
|
||||
"elf_build_id": "d0cb1fa0deb3d9161b4cd77530cbcd2e50de3a21",
|
||||
"elf_build_id": "ade08266c67aee88ec9c1d00902150e1684dad3a",
|
||||
"sha256": {
|
||||
"libroblox": "92ca0e4d8bc4e1c7a37fc2b52544869299ec8b9a40ea8da96ebac66660e0b1af",
|
||||
"base_apk": "eba476fec3020eaceb28436c1d160eb35d6689d8ffb060e09678535584babc7a",
|
||||
"x86_64_split_apk": "5c4193c9700481079cee958b38ba9cbb97768b0a130b7f704ed084753f01ce2b"
|
||||
"libroblox": "4343a6a900d1fca27ff8e10b9d9c86bee40a5d0ba547cb29903b7769f4f42a9d",
|
||||
"base_apk": "a4a71ec39df6619a56ab7adce0959d1d60354835485b53806f4026914287ea86",
|
||||
"x86_64_split_apk": "a4a71ec39df6619a56ab7adce0959d1d60354835485b53806f4026914287ea86"
|
||||
},
|
||||
"signing_certificates_sha256": [
|
||||
"2bebd189e8d3106401347056c93d045b61e20e22d0c3cbed85474aeb00a3d12a",
|
||||
"44932ea35a17a267372d71b54d1a0cb3da0dca5113e94406ae2fe18090ba1477"
|
||||
],
|
||||
"assets": {
|
||||
"file_count": 1837
|
||||
"file_count": 1836,
|
||||
"sha256_tree": "caee48e11c194bc11852493f53ff67d5e2bb233172f12b87ea5fca97c0c10840"
|
||||
},
|
||||
"source": "sober-update-service",
|
||||
"imported_at": "2026-07-09T19:12:54Z",
|
||||
"compatibility_status": "supported"
|
||||
"source": "apk-pure-native",
|
||||
"imported_at": "2026-08-31T16:30:19Z",
|
||||
"compatibility_status": "supported",
|
||||
"apk_layout": "monolithic"
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
# Nightcap roadmap
|
||||
|
||||
What Nightcap is working toward, in priority order. Items move to
|
||||
[Shipped](#shipped) when they land on `main`. Open an issue to propose a
|
||||
change to this list.
|
||||
|
||||
## 1. Settings UI
|
||||
|
||||
Nightcap is configured by editing `~/.config/mocktail/config.yaml`. Most
|
||||
people never open it, so they never find the FPS unlock or the vsync switch.
|
||||
|
||||
Goal: a small GTK4 settings window, reachable from the app menu and from a
|
||||
`--settings` flag, that reads and writes the same `config.yaml`.
|
||||
|
||||
- Graphics tab: backend, frame rate limit, vsync, small texture upscale.
|
||||
- Input tab: raw mouse and controller options (see item 2).
|
||||
- Advanced tab: FFlag overrides with a plain text editor for `fflags.json`.
|
||||
- Discord tab: the `discord_rpc` block (see item 3).
|
||||
- Changes apply on next launch. A restart button restarts Nightcap.
|
||||
|
||||
Done when every option in `config/mocktail.example.yaml` that a player would
|
||||
touch can be set without a text editor.
|
||||
|
||||
## 2. Controller and raw mouse input
|
||||
|
||||
Upstream added Xbox and PlayStation gamepad support through SDL. Desktop play
|
||||
still has rough edges.
|
||||
|
||||
- Raw mouse input in first-person games, with no acceleration or clamping.
|
||||
- Better controller support: hot-plug without a restart, correct button
|
||||
glyphs for the connected pad, and no stuck inputs after a disconnect.
|
||||
|
||||
Done when a mouse feels like a mouse and a plugged-in controller just works.
|
||||
|
||||
## 3. Discord Rich Presence
|
||||
|
||||
Upstream ships Discord RPC, off by default, behind `discord_rpc.enabled`. It
|
||||
shows the experience name, elapsed time, and a join button when the server is
|
||||
public.
|
||||
|
||||
- Turn it on by default. It never reads a Discord token, so the privacy cost
|
||||
is only the experience name, which the player already shares in Roblox.
|
||||
- Show the experience icon as the large image instead of a static logo.
|
||||
- Show the current server region and player count when Roblox exposes them.
|
||||
- Make the join button work for private servers the player has a link for.
|
||||
- A Discord tab in the settings window to toggle each field.
|
||||
|
||||
Done when a friend can see what you are playing and join in one click, and
|
||||
you can switch it all off in one click.
|
||||
|
||||
## 4. Performance profiling
|
||||
|
||||
Every performance change should come with numbers. Today they come with a
|
||||
feeling.
|
||||
|
||||
- A frame time overlay, toggled by a key and a config flag, showing CPU time,
|
||||
GPU time, and present latency.
|
||||
- Shader compile and pipeline cache tracing, so first-load stutter can be
|
||||
measured and cut.
|
||||
- A `--profile` flag that writes a trace file readable by Perfetto.
|
||||
- A short benchmark guide in `docs/` with a fixed set of experiences, so
|
||||
before and after numbers can be compared across machines.
|
||||
|
||||
Done when a pull request that claims a speedup can show the trace that
|
||||
proves it.
|
||||
|
||||
## Shipped
|
||||
|
||||
- Lower input latency: frames are presented as soon as they are ready.
|
||||
- Small textures upscaled 4x by default, with PNG overrides in
|
||||
`~/.config/mocktail/textures`.
|
||||
- ETC2 textures decoded on desktop GPUs without stutter.
|
||||
- No more error 319 kicks after joining a server.
|
||||
- Roblox sees the real host RAM and screen size.
|
||||
- Desktop launcher that works on Wayland and finds the installed binary.
|
||||
- Own app ID `io.github.CoderDayton.nightcap`, so Nightcap installs next to
|
||||
upstream.
|
||||
- AppImage and signed Flatpak releases on every `v*` tag, with the Flatpak
|
||||
repo on GitHub Pages.
|
||||
|
||||
## Not planned
|
||||
|
||||
- Replacing the `mocktail` binary and config names. Keeping them makes
|
||||
upstream merges clean.
|
||||
- DEB, RPM, and AUR packages. Upstream ships those for unmodified Mocktail.
|
||||
Generated
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1785627969,
|
||||
"narHash": "sha256-4dtXQk/NMePegK/nWp5NSeuZKLATItOq61lpEvmXqGw=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "427bf4bd9435fdf21321c8cc628c24efc14c0f7a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1787135253,
|
||||
"narHash": "sha256-RD2kNWCG+Bjo6h+JVjWVNntZs2GtRoeY2xHjts/FNkA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "ffb3c9b700e759be2ef13237c9d8f953b32a1e46",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1785031560,
|
||||
"narHash": "sha256-OmshNvn2vupOFpYinLUu+1Dnpu4n7Q5N3ggGVNHpkUI=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "0e79af5e3d4dcfcd676ab5ba3f95d2e3352e078c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-parts": "flake-parts",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
description = "Experimental Roblox compatibility runtime for Linux and FreeBSD";
|
||||
|
||||
inputs = {
|
||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs =
|
||||
inputs@{ flake-parts, ... }:
|
||||
flake-parts.lib.mkFlake { inherit inputs; } {
|
||||
imports = [
|
||||
|
||||
];
|
||||
systems = [
|
||||
"x86_64-linux"
|
||||
# "aarch64-linux"
|
||||
# "aarch64-darwin"
|
||||
# "x86_64-darwin"
|
||||
];
|
||||
perSystem =
|
||||
{
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
package = pkgs.callPackage ./packaging/nix/package.nix { };
|
||||
in
|
||||
{
|
||||
packages.default = package;
|
||||
devShells.default = pkgs.mkShell {
|
||||
inputsFrom = [ package ];
|
||||
|
||||
# nixpkgs installs the header we need under include/libutf8proc
|
||||
NIX_CFLAGS_COMPILE = "-I${pkgs.libutf8proc}/include/libutf8proc";
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
cmake
|
||||
git
|
||||
lld
|
||||
ninja
|
||||
pkg-config
|
||||
vulkan-headers
|
||||
|
||||
llvmPackages_22.clang-tools
|
||||
llvmPackages_22.libclang
|
||||
|
||||
# debugging
|
||||
gdb
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.lib.makeLibraryPath package.buildInputs}"
|
||||
|
||||
# nix packages graphics drivers differently, and mocktail will still work if it cannot guarantee symbols
|
||||
export MOCKTAIL_REQUIRE_REAL_GRAPHICS=0
|
||||
'';
|
||||
};
|
||||
};
|
||||
flake = { };
|
||||
};
|
||||
}
|
||||
@@ -71,6 +71,18 @@ int mocktail_pthread_mutex_destroy(pthread_mutex_t* mutex);
|
||||
int mocktail_pthread_mutex_lock(pthread_mutex_t* mutex);
|
||||
int mocktail_pthread_mutex_trylock(pthread_mutex_t* mutex);
|
||||
int mocktail_pthread_mutex_unlock(pthread_mutex_t* mutex);
|
||||
int mocktail_pthread_once(pthread_once_t* once_control,
|
||||
void (*init_routine)(void));
|
||||
int mocktail_pthread_spin_init(pthread_spinlock_t* lock, int pshared);
|
||||
int mocktail_pthread_spin_destroy(pthread_spinlock_t* lock);
|
||||
int mocktail_pthread_spin_lock(pthread_spinlock_t* lock);
|
||||
int mocktail_pthread_spin_trylock(pthread_spinlock_t* lock);
|
||||
int mocktail_pthread_spin_unlock(pthread_spinlock_t* lock);
|
||||
int mocktail_pthread_barrier_init(pthread_barrier_t* barrier,
|
||||
const pthread_barrierattr_t* attr,
|
||||
unsigned count);
|
||||
int mocktail_pthread_barrier_destroy(pthread_barrier_t* barrier);
|
||||
int mocktail_pthread_barrier_wait(pthread_barrier_t* barrier);
|
||||
|
||||
extern void* mocktail_gameactivity_on_start_native;
|
||||
extern void* mocktail_gameactivity_on_resume_native;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#define MOCKTAIL_COMPAT_BIONIC_PTHREAD_CREATE_RUNTIME_H_
|
||||
|
||||
#include <pthread.h>
|
||||
#include <sched.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
@@ -29,4 +31,10 @@ extern "C" int mocktail_bionic_pthread_create(pthread_t* thread,
|
||||
void* (*start_routine)(void*),
|
||||
void* argument);
|
||||
|
||||
// Guest real-time scheduling is denied to avoid the host RLIMIT_RTTIME watchdog.
|
||||
extern "C" int mocktail_bionic_pthread_setschedparam(
|
||||
pthread_t thread, int policy, const struct sched_param* parameters);
|
||||
extern "C" int mocktail_bionic_sched_setscheduler(
|
||||
pid_t tid, int policy, const struct sched_param* parameters);
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_BIONIC_PTHREAD_CREATE_RUNTIME_H_
|
||||
@@ -3,8 +3,9 @@
|
||||
|
||||
#include <pthread.h>
|
||||
|
||||
// Bionic and glibc pthread_rwlock_t storage is not ABI-compatible. The guest
|
||||
// address is an opaque key; the host object lives entirely in this runtime.
|
||||
// Bionic pthread_rwlock_t is a state word in guest storage. rdlock/wrlock/
|
||||
// unlock use __atomic_* and futex on that word. Zero-initialized storage
|
||||
// still locks.
|
||||
extern "C" {
|
||||
|
||||
int mocktail_bionic_pthread_rwlock_init(pthread_rwlock_t* rwlock,
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
|
||||
#include <semaphore.h>
|
||||
|
||||
// Bionic and glibc use different sem_t representations. These exports keep
|
||||
// host semaphore objects out of guest storage and key them by guest address.
|
||||
// Bionic sem_t is a count word in guest storage (value << 1, shared bit 0).
|
||||
// Wait/post use __atomic_* and futex on that word so inlined guest futex
|
||||
// waiters wake. Process-shared semaphores are rejected.
|
||||
extern "C" {
|
||||
|
||||
int mocktail_bionic_sem_init(sem_t* semaphore, int process_shared,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <cstdarg>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cwchar>
|
||||
|
||||
@@ -17,12 +18,6 @@ namespace mocktail::compat {
|
||||
constexpr size_t kBionicFileSize = 152;
|
||||
constexpr size_t kBionicStandardStreamCount = 3;
|
||||
|
||||
FILE* TranslateBionicFile(FILE* stream) noexcept;
|
||||
void* BionicFileArraySymbolAddress() noexcept;
|
||||
void* BionicStdinSymbolAddress() noexcept;
|
||||
void* BionicStdoutSymbolAddress() noexcept;
|
||||
void* BionicStderrSymbolAddress() noexcept;
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
extern "C" {
|
||||
@@ -31,8 +26,31 @@ alignas(sizeof(void*)) extern unsigned char
|
||||
__sF[mocktail::compat::kBionicStandardStreamCount]
|
||||
[mocktail::compat::kBionicFileSize];
|
||||
|
||||
} // extern "C"
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
inline FILE* TranslateBionicFile(FILE* stream) noexcept {
|
||||
const uintptr_t address = reinterpret_cast<uintptr_t>(stream);
|
||||
if (__builtin_expect(address == reinterpret_cast<uintptr_t>(&__sF[0]), 0)) {
|
||||
return stdin;
|
||||
}
|
||||
if (__builtin_expect(address == reinterpret_cast<uintptr_t>(&__sF[1]), 0)) {
|
||||
return stdout;
|
||||
}
|
||||
if (__builtin_expect(address == reinterpret_cast<uintptr_t>(&__sF[2]), 0)) {
|
||||
return stderr;
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
void* BionicFileArraySymbolAddress() noexcept;
|
||||
void* BionicStdinSymbolAddress() noexcept;
|
||||
void* BionicStdoutSymbolAddress() noexcept;
|
||||
void* BionicStderrSymbolAddress() noexcept;
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
// FILE values returned by host fopen/fdopen pass through unchanged. Pointers
|
||||
// into Bionic's ABI-sized __sF array are translated to the corresponding host
|
||||
// standard stream at this single boundary.
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#ifndef MOCKTAIL_COMPAT_BUILD_PROFILE_H_
|
||||
#define MOCKTAIL_COMPAT_BUILD_PROFILE_H_
|
||||
|
||||
#include <array>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
@@ -22,6 +25,27 @@ struct FmodOutputDeviceBridgeProfile {
|
||||
std::uintptr_t info_method_rva = 0;
|
||||
std::uintptr_t current_method_rva = 0;
|
||||
std::uintptr_t select_method_rva = 0;
|
||||
// Roblox 2.738 inserted device-list methods before the current/select slots.
|
||||
int vtable_layout_version = 1;
|
||||
|
||||
// Optional exact-build input count/info/current/select methods.
|
||||
std::array<std::uintptr_t, 4> input_method_rvas{};
|
||||
bool has_input_devices() const { return input_method_rvas[0] != 0; }
|
||||
std::array<std::size_t, 4> input_vtable_indexes() const {
|
||||
return vtable_layout_version == 2
|
||||
? std::array<std::size_t, 4>{9, 10, 12, 18}
|
||||
: std::array<std::size_t, 4>{8, 9, 10, 16};
|
||||
}
|
||||
|
||||
bool valid_vtable_layout() const {
|
||||
return vtable_layout_version == 1 || vtable_layout_version == 2;
|
||||
}
|
||||
std::size_t current_vtable_index() const {
|
||||
return vtable_layout_version == 2 ? 8 : 7;
|
||||
}
|
||||
std::size_t select_vtable_index() const {
|
||||
return vtable_layout_version == 2 ? 19 : 17;
|
||||
}
|
||||
};
|
||||
|
||||
struct BuildProfile {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef MOCKTAIL_COMPAT_FMOD_OUTPUT_DEVICE_CONTRACT_H_
|
||||
#define MOCKTAIL_COMPAT_FMOD_OUTPUT_DEVICE_CONTRACT_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <initializer_list>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
// The 2.738 integer-index selector adds a feature-flag dispatch before the
|
||||
// count/clamp/setDriver path. Keep the calling convention, count slot (5),
|
||||
// object offset (0x28), and branch into that path exact. Only relocated
|
||||
// addresses and non-local branch/call targets vary between builds.
|
||||
inline constexpr char kFmodDeviceListsSelectContract[] =
|
||||
"\x55\x48\x89\xe5\x41\x57\x41\x56\x41\x54\x53\x48\x83\xec\x50\x41"
|
||||
"\x89\xf7\x49\x89\xfe\x4c\x8b\x25\x00\x00\x00\x00\x49\x8b\x04\x24"
|
||||
"\x48\x89\x45\xd8\x80\x3d\x00\x00\x00\x00\x00\x74\x25\x49\x8b\x04"
|
||||
"\x24\x48\x3b\x45\xd8\x0f\x85\x00\x00\x00\x00\x4c\x89\xf7\x44\x89"
|
||||
"\xfe\x48\x83\xc4\x50\x5b\x41\x5c\x41\x5e\x41\x5f\x5d\xe9\x00\x00"
|
||||
"\x00\x00\x49\x8b\x06\x4c\x89\xf7\xff\x50\x28\x44\x39\xf8\x0f\x8e"
|
||||
"\x00\x00\x00\x00\x31\xdb\x45\x85\xff\x41\x0f\x4f\xdf\x49\x8b\x7e"
|
||||
"\x28\x89\xde\xe8\x00\x00\x00\x00";
|
||||
inline constexpr std::size_t kFmodDeviceListsSelectContractSize =
|
||||
sizeof(kFmodDeviceListsSelectContract) - 1;
|
||||
|
||||
inline bool HasFmodDeviceListsSelectContract(std::string_view code) {
|
||||
if (code.size() < kFmodDeviceListsSelectContractSize) return false;
|
||||
for (std::size_t index = 0; index < kFmodDeviceListsSelectContractSize;
|
||||
++index) {
|
||||
bool relocated = false;
|
||||
for (const std::size_t start : {24, 38, 55, 78, 96, 116}) {
|
||||
if (index >= start && index < start + 4) relocated = true;
|
||||
}
|
||||
if (!relocated && code[index] != kFmodDeviceListsSelectContract[index])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_FMOD_OUTPUT_DEVICE_CONTRACT_H_
|
||||
@@ -54,6 +54,8 @@ ExternalHostAbiProfileResult InitializeHostAbiProfileFromEnvironment(
|
||||
const HostAbiProfile* FindLoadedExternalHostAbiProfile(
|
||||
std::string_view build_id) noexcept;
|
||||
|
||||
void SetHostAbiProfileHashingFaultForTesting(bool enabled) noexcept;
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_HOST_ABI_PROFILE_LOADER_H_
|
||||
+175
-1
@@ -59,6 +59,8 @@ struct RobloxCredentialView {
|
||||
};
|
||||
|
||||
using RobloxCredentialProvider = RobloxCredentialView (*)(const void *context);
|
||||
using RobloxCookieGetter = jstring (*)(JNIEnv* env, jclass clazz,
|
||||
jstring domain);
|
||||
|
||||
jobject CreateAndroidConfiguration(JNIEnv *env);
|
||||
|
||||
@@ -80,6 +82,67 @@ struct FmodAudioDeviceCallbacks {
|
||||
void (*shutdown)(void *context) = nullptr;
|
||||
};
|
||||
|
||||
// Parameters passed by WebRtcAudioManager's constructor to the guest's
|
||||
// nativeCacheAudioParameters(IIIZZZZZZZIIJ)V. Buffer sizes are PCM frames,
|
||||
// not bytes. Querying them must not open a microphone.
|
||||
struct WebRtcAudioManagerParameters {
|
||||
int sample_rate_hz = 0;
|
||||
int output_channels = 0;
|
||||
int input_channels = 0;
|
||||
bool hardware_aec = false;
|
||||
bool hardware_agc = false;
|
||||
bool hardware_ns = false;
|
||||
bool low_latency_output = false;
|
||||
bool low_latency_input = false;
|
||||
bool pro_audio = false;
|
||||
bool aaudio = false;
|
||||
int output_buffer_size_frames = 0;
|
||||
int input_buffer_size_frames = 0;
|
||||
};
|
||||
|
||||
struct WebRtcAudioManagerCallbacks {
|
||||
bool (*get_parameters)(void* context,
|
||||
WebRtcAudioManagerParameters* parameters) = nullptr;
|
||||
bool (*init)(void* context, const void* identity) = nullptr;
|
||||
void (*dispose)(void* context, const void* identity) = nullptr;
|
||||
void (*set_microphone_mute)(void* context, bool muted) = nullptr;
|
||||
};
|
||||
|
||||
using WebRtcAudioRecordDataCallback = void (*)(void *context,
|
||||
const void *identity,
|
||||
std::size_t size_bytes);
|
||||
|
||||
// Host implementation of org/webrtc/voiceengine/WebRtcAudioRecord. Init
|
||||
// returns the number of PCM frames in the stable direct buffer, or -1.
|
||||
struct WebRtcAudioRecordCallbacks {
|
||||
int (*init)(void *context, const void *identity, int sample_rate_hz,
|
||||
int channels, WebRtcAudioRecordDataCallback data_callback,
|
||||
void *data_context, void **direct_buffer,
|
||||
std::size_t *direct_buffer_capacity) = nullptr;
|
||||
bool (*start)(void *context, const void *identity) = nullptr;
|
||||
bool (*stop)(void *context, const void *identity) = nullptr;
|
||||
void (*close)(void *context, const void *identity) = nullptr;
|
||||
void (*shutdown)(void *context) = nullptr;
|
||||
};
|
||||
|
||||
using WebRtcAudioTrackDataCallback = void (*)(void *context,
|
||||
const void *identity,
|
||||
std::size_t size_bytes);
|
||||
|
||||
// Host implementation of org/webrtc/voiceengine/WebRtcAudioTrack. Init
|
||||
// returns the emulated Android AudioTrack buffer size in bytes, or -1.
|
||||
struct WebRtcAudioTrackCallbacks {
|
||||
int (*init)(void *context, const void *identity, int sample_rate_hz,
|
||||
int channels, double buffer_size_factor,
|
||||
WebRtcAudioTrackDataCallback data_callback, void *data_context,
|
||||
void **direct_buffer,
|
||||
std::size_t *direct_buffer_capacity) = nullptr;
|
||||
int (*buffer_size_frames)(void *context, const void *identity) = nullptr;
|
||||
bool (*start)(void *context, const void *identity) = nullptr;
|
||||
bool (*stop)(void *context, const void *identity) = nullptr;
|
||||
void (*close)(void *context, const void *identity) = nullptr;
|
||||
};
|
||||
|
||||
// Guest JNI threads only publish Android window flags. The host callback must
|
||||
// marshal SDL work onto the main thread.
|
||||
struct AndroidWindowCallbacks {
|
||||
@@ -130,6 +193,11 @@ struct MessageBusRequestHandlerCallbacks {
|
||||
std::string (*run)(void *context, JNIEnv *env, jstring message) = nullptr;
|
||||
};
|
||||
|
||||
struct MessageBusAsyncRequestHandlerCallbacks {
|
||||
void (*run)(void* context, JNIEnv* env, jstring message,
|
||||
jstring response_id) = nullptr;
|
||||
};
|
||||
|
||||
// Dispatch accepts Callback.onItemSet(String) only on a receiver created by
|
||||
// this VM. The context stays alive while the callback runs.
|
||||
struct MemStorageCallbackCallbacks {
|
||||
@@ -194,10 +262,12 @@ public:
|
||||
|
||||
JavaVM *GetJavaVM() { return java_vm_; }
|
||||
|
||||
// Resolves the process pseudo-VM that owns an exact JavaVM pointer. Returns
|
||||
// Resolves the live 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);
|
||||
|
||||
// Binds this thread's JNI environment to this VM. Switching owners must
|
||||
// not reuse the previous VM's native function table.
|
||||
JNIEnv *GetJNIEnv();
|
||||
|
||||
std::size_t GetClassCount() const { return class_registry_.size(); }
|
||||
@@ -228,6 +298,9 @@ public:
|
||||
// retained privately and returned instead until the provider is cleared.
|
||||
bool CopyRobloxCredentialFromProvider(std::string *credential) const;
|
||||
|
||||
void SetRobloxCookieGetter(RobloxCookieGetter getter);
|
||||
bool RefreshRobloxCredentialFromEngine(JNIEnv* env);
|
||||
|
||||
void SetRobloxCredentialSink(std::shared_ptr<void> context,
|
||||
const RobloxCredentialSinkCallbacks &callbacks);
|
||||
void ClearRobloxCredentialSink();
|
||||
@@ -250,6 +323,55 @@ public:
|
||||
const std::uint8_t *data, std::size_t size);
|
||||
bool DispatchFmodAudioDeviceClose(const void *identity);
|
||||
|
||||
void SetWebRtcAudioManagerCallbacks(
|
||||
std::shared_ptr<void> context,
|
||||
const WebRtcAudioManagerCallbacks& callbacks);
|
||||
void ClearWebRtcAudioManagerCallbacks();
|
||||
void RegisterWebRtcAudioManagerNative(const char* name, const char* signature,
|
||||
void* function);
|
||||
bool DispatchWebRtcAudioManagerConstruct(jobject manager,
|
||||
jlong native_audio_manager);
|
||||
bool DispatchWebRtcAudioManagerInit(jobject manager);
|
||||
void DispatchWebRtcAudioManagerDispose(jobject manager);
|
||||
void DispatchWebRtcAudioManagerMicrophoneMute(jobject manager, bool muted);
|
||||
|
||||
void
|
||||
SetWebRtcAudioRecordCallbacks(std::shared_ptr<void> context,
|
||||
const WebRtcAudioRecordCallbacks &callbacks);
|
||||
void ClearWebRtcAudioRecordCallbacks();
|
||||
int DispatchWebRtcAudioRecordInit(const void *identity, int sample_rate_hz,
|
||||
int channels, void **direct_buffer,
|
||||
std::size_t *direct_buffer_capacity);
|
||||
bool DispatchWebRtcAudioRecordStart(const void *identity);
|
||||
bool DispatchWebRtcAudioRecordStop(const void *identity);
|
||||
void DispatchWebRtcAudioRecordClose(const void *identity);
|
||||
void DispatchWebRtcAudioRecordData(const void *identity,
|
||||
std::size_t size_bytes);
|
||||
|
||||
// RegisterNatives stores these exact WebRTC callbacks so the host capture
|
||||
// thread can emulate the Java AudioRecord thread.
|
||||
void RegisterWebRtcAudioRecordNative(const char *name, const char *signature,
|
||||
void *function);
|
||||
|
||||
void SetWebRtcAudioTrackCallbacks(std::shared_ptr<void> context,
|
||||
const WebRtcAudioTrackCallbacks &callbacks);
|
||||
void ClearWebRtcAudioTrackCallbacks();
|
||||
int DispatchWebRtcAudioTrackInit(const void *identity, int sample_rate_hz,
|
||||
int channels, double buffer_size_factor,
|
||||
void **direct_buffer,
|
||||
std::size_t *direct_buffer_capacity);
|
||||
int DispatchWebRtcAudioTrackBufferSizeFrames(const void *identity);
|
||||
bool DispatchWebRtcAudioTrackStart(const void *identity);
|
||||
bool DispatchWebRtcAudioTrackStop(const void *identity);
|
||||
void DispatchWebRtcAudioTrackClose(const void *identity);
|
||||
void DispatchWebRtcAudioTrackData(const void *identity,
|
||||
std::size_t size_bytes);
|
||||
|
||||
// RegisterNatives stores the exact WebRTC playout callbacks so the host
|
||||
// thread can emulate AudioTrackJavaThread.
|
||||
void RegisterWebRtcAudioTrackNative(const char *name, const char *signature,
|
||||
void *function);
|
||||
|
||||
void SetAndroidWindowCallbacks(std::shared_ptr<void> context,
|
||||
const AndroidWindowCallbacks &callbacks);
|
||||
void ClearAndroidWindowCallbacks();
|
||||
@@ -278,6 +400,16 @@ public:
|
||||
jstring DispatchMessageBusRequestHandler(jobject handler, JNIEnv *env,
|
||||
jstring message);
|
||||
|
||||
// Exact RequestHandlerAsyncRaw.run(String, String) receiver. Clearing stops
|
||||
// new calls; an in-flight call retains the host context until it returns.
|
||||
jobject CreateMessageBusAsyncRequestHandler(
|
||||
std::shared_ptr<void> context,
|
||||
const MessageBusAsyncRequestHandlerCallbacks& callbacks);
|
||||
void ClearMessageBusAsyncRequestHandler(jobject handler);
|
||||
bool DispatchMessageBusAsyncRequestHandler(jobject handler, JNIEnv* env,
|
||||
jstring message,
|
||||
jstring response_id);
|
||||
|
||||
// Creates the exact com.roblox.engine.jni.memstorage.Callback receiver used
|
||||
// by MemStorage.bind. Clear prevents future dispatch while an in-flight call
|
||||
// retains its shared context until the callback returns.
|
||||
@@ -338,6 +470,9 @@ private:
|
||||
RobloxCredentialProvider roblox_credential_provider_ = nullptr;
|
||||
std::string roblox_credential_override_;
|
||||
|
||||
mutable std::mutex roblox_cookie_getter_mutex_;
|
||||
RobloxCookieGetter roblox_cookie_getter_ = nullptr;
|
||||
|
||||
struct RobloxCredentialSinkBinding {
|
||||
std::shared_ptr<void> context;
|
||||
RobloxCredentialSinkCallbacks callbacks;
|
||||
@@ -352,6 +487,36 @@ private:
|
||||
mutable std::mutex fmod_audio_device_mutex_;
|
||||
FmodAudioDeviceBinding fmod_audio_device_binding_;
|
||||
|
||||
struct WebRtcAudioManagerBinding {
|
||||
std::shared_ptr<void> context;
|
||||
WebRtcAudioManagerCallbacks callbacks;
|
||||
};
|
||||
mutable std::mutex webrtc_audio_manager_mutex_;
|
||||
WebRtcAudioManagerBinding webrtc_audio_manager_binding_;
|
||||
void* webrtc_cache_audio_parameters_native_ = nullptr;
|
||||
|
||||
struct WebRtcAudioRecordBinding {
|
||||
std::shared_ptr<void> context;
|
||||
WebRtcAudioRecordCallbacks callbacks;
|
||||
};
|
||||
mutable std::mutex webrtc_audio_record_mutex_;
|
||||
WebRtcAudioRecordBinding webrtc_audio_record_binding_;
|
||||
|
||||
mutable std::mutex webrtc_audio_record_native_mutex_;
|
||||
void *webrtc_cache_direct_buffer_native_ = nullptr;
|
||||
void *webrtc_data_is_recorded_native_ = nullptr;
|
||||
|
||||
struct WebRtcAudioTrackBinding {
|
||||
std::shared_ptr<void> context;
|
||||
WebRtcAudioTrackCallbacks callbacks;
|
||||
};
|
||||
mutable std::mutex webrtc_audio_track_mutex_;
|
||||
WebRtcAudioTrackBinding webrtc_audio_track_binding_;
|
||||
|
||||
mutable std::mutex webrtc_audio_track_native_mutex_;
|
||||
void *webrtc_track_cache_direct_buffer_native_ = nullptr;
|
||||
void *webrtc_get_playout_data_native_ = nullptr;
|
||||
|
||||
struct AndroidWindowBinding {
|
||||
std::shared_ptr<void> context;
|
||||
AndroidWindowCallbacks callbacks;
|
||||
@@ -379,6 +544,15 @@ private:
|
||||
std::unordered_map<jobject, std::shared_ptr<MessageBusRequestHandlerBinding>>
|
||||
message_bus_request_handler_bindings_;
|
||||
|
||||
struct MessageBusAsyncRequestHandlerBinding {
|
||||
std::shared_ptr<void> context;
|
||||
MessageBusAsyncRequestHandlerCallbacks callbacks;
|
||||
};
|
||||
mutable std::mutex message_bus_async_request_handler_mutex_;
|
||||
std::unordered_map<jobject,
|
||||
std::shared_ptr<MessageBusAsyncRequestHandlerBinding>>
|
||||
message_bus_async_request_handler_bindings_;
|
||||
|
||||
struct MemStorageCallbackBinding {
|
||||
std::shared_ptr<void> context;
|
||||
MemStorageCallbackCallbacks callbacks;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifndef MOCKTAIL_LIBC_SHIM_VULKAN_ETC1_SKY_TRANSCODER_H_
|
||||
#define MOCKTAIL_LIBC_SHIM_VULKAN_ETC1_SKY_TRANSCODER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace libc_shim {
|
||||
|
||||
// Converts one opaque ETC1 RGB block to an opaque BC1 RGB block.
|
||||
void ConvertEtc1RgbBlockToBc1(const uint8_t *etc1, uint8_t *bc1);
|
||||
|
||||
bool TranscodeEtc1SkyTextureForVulkan(const char *path,
|
||||
std::vector<unsigned char> *data);
|
||||
|
||||
} // namespace libc_shim
|
||||
|
||||
#endif // MOCKTAIL_LIBC_SHIM_VULKAN_ETC1_SKY_TRANSCODER_H_
|
||||
@@ -0,0 +1,64 @@
|
||||
#ifndef MOCKTAIL_AUDIO_FMOD_THREAD_FLOATING_POINT_H_
|
||||
#define MOCKTAIL_AUDIO_FMOD_THREAD_FLOATING_POINT_H_
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#if defined(__x86_64__)
|
||||
#include <xmmintrin.h>
|
||||
#endif
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
// FMOD's Android threads identify themselves through JavaVMAttachArgs before
|
||||
// entering their mix loop. Voice DSP filters can accumulate subnormal values
|
||||
// and spend an entire x86 core on floating-point assists (issue #128).
|
||||
// Limit FTZ/DAZ to these audio threads; retain the caller's other FP controls.
|
||||
class FmodThreadFloatingPointMode final {
|
||||
public:
|
||||
FmodThreadFloatingPointMode() = default;
|
||||
~FmodThreadFloatingPointMode() { Restore(); }
|
||||
FmodThreadFloatingPointMode(const FmodThreadFloatingPointMode&) = delete;
|
||||
FmodThreadFloatingPointMode& operator=(const FmodThreadFloatingPointMode&) =
|
||||
delete;
|
||||
|
||||
bool Enable(const char* thread_name) noexcept {
|
||||
#if defined(__x86_64__)
|
||||
if (active_ || thread_name == nullptr) return false;
|
||||
const std::string_view name(thread_name);
|
||||
if (name != "FMOD mixer thread" && name != "FMOD feeder thread" &&
|
||||
name != "FMOD Convolution thread" && name != "FMOD Worker Thread") {
|
||||
return false;
|
||||
}
|
||||
const unsigned int control = _mm_getcsr();
|
||||
original_mode_ = control & kModeMask;
|
||||
_mm_setcsr(control | kModeMask);
|
||||
active_ = true;
|
||||
return true;
|
||||
#else
|
||||
(void)thread_name;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void Restore() noexcept {
|
||||
#if defined(__x86_64__)
|
||||
if (active_) {
|
||||
// Restore only our two bits, preserving rounding, exception masks and
|
||||
// status flags set by the guest while the thread was attached.
|
||||
_mm_setcsr((_mm_getcsr() & ~kModeMask) | original_mode_);
|
||||
active_ = false;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private:
|
||||
#if defined(__x86_64__)
|
||||
static constexpr unsigned int kModeMask = (1U << 15) | (1U << 6);
|
||||
unsigned int original_mode_ = 0;
|
||||
bool active_ = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace mocktail::audio
|
||||
|
||||
#endif // MOCKTAIL_AUDIO_FMOD_THREAD_FLOATING_POINT_H_
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
@@ -13,13 +14,15 @@
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
// Replaces only the output-device query/select slots of the exact
|
||||
class NativeInputCapture;
|
||||
|
||||
// Replaces the profiled input/output-device query/select slots of the exact
|
||||
// Build-ID-scoped FmodAudioDevice vtable. Roblox keeps owning its FMOD engine,
|
||||
// while the existing settings UI sees and selects the SDL host routes that
|
||||
// consume Android AudioTrack PCM.
|
||||
class RobloxOutputDeviceBridge final {
|
||||
public:
|
||||
RobloxOutputDeviceBridge() = default;
|
||||
RobloxOutputDeviceBridge();
|
||||
~RobloxOutputDeviceBridge();
|
||||
|
||||
RobloxOutputDeviceBridge(const RobloxOutputDeviceBridge&) = delete;
|
||||
@@ -48,16 +51,25 @@ class RobloxOutputDeviceBridge final {
|
||||
Status Activate(std::uintptr_t image_base);
|
||||
Status PatchVtableLocked();
|
||||
bool RestoreVtableLocked();
|
||||
int DeviceCount();
|
||||
void* DeviceInfo(void* result, int index);
|
||||
int CurrentDevice();
|
||||
void SelectDevice(int index);
|
||||
static int GetInputDeviceCount(void *self);
|
||||
static void *GetInputDeviceInfo(void *result, void *self, int index);
|
||||
static int GetCurrentInputDevice(void *self);
|
||||
static void SetCurrentInputDevice(void *self, int index);
|
||||
int DeviceCount(bool input = false);
|
||||
void *DeviceInfo(void *result, int index, bool input = false);
|
||||
int CurrentDevice(bool input = false);
|
||||
void SelectDevice(int index, bool input = false);
|
||||
void ConstructGuestString(void* destination, std::string_view value) const;
|
||||
|
||||
std::unique_ptr<NativeInputCapture> capture_;
|
||||
bool capture_profile_supported_ = false;
|
||||
std::array<std::uintptr_t, 6> original_capture_methods_{};
|
||||
mutable std::mutex mutex_;
|
||||
compat::FmodOutputDeviceBridgeProfile profile_;
|
||||
std::vector<MenuDevice> devices_;
|
||||
std::array<std::uintptr_t, 4> original_methods_{};
|
||||
std::vector<MenuDevice> input_devices_;
|
||||
int selected_input_index_ = 0;
|
||||
std::array<std::uintptr_t, 8> original_methods_{};
|
||||
std::uintptr_t library_base_ = 0;
|
||||
std::uintptr_t* vtable_ = nullptr;
|
||||
void* string_constructor_ = nullptr;
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#ifndef MOCKTAIL_AUDIO_SDL_AUDIO_CAPTURE_H_
|
||||
#define MOCKTAIL_AUDIO_SDL_AUDIO_CAPTURE_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "mocktail/audio/audio_sink.h"
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
struct SdlRecordingDevice {
|
||||
std::uint32_t id = 0;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
// The virtual `default` target is not returned. Device IDs are process-local;
|
||||
// names are the stable configuration choice, while `id:<number>` disambiguates
|
||||
// duplicate names for the current host session.
|
||||
Status ListSdlRecordingDevices(std::vector<SdlRecordingDevice>* devices);
|
||||
|
||||
Status ResolveSdlRecordingDevice(
|
||||
std::string_view requested,
|
||||
const std::vector<SdlRecordingDevice>& available_devices,
|
||||
std::uint32_t* recording_device_id, std::string* resolved_name);
|
||||
|
||||
// Must run after SDL audio initialization and before the first capture opens.
|
||||
Status ConfigureSdlRecordingDevice(
|
||||
std::string_view requested,
|
||||
std::vector<SdlRecordingDevice>* available_devices,
|
||||
std::string* resolved_name);
|
||||
|
||||
Status GetConfiguredSdlRecordingDevice(std::uint32_t* recording_device_id,
|
||||
std::string* resolved_name);
|
||||
|
||||
// Migrates live captures without changing their format or callback buffers.
|
||||
Status SwitchSdlRecordingDevice(std::uint32_t recording_device_id,
|
||||
std::string *resolved_name);
|
||||
|
||||
using AudioCaptureDataCallback = void (*)(void* context,
|
||||
std::size_t size_bytes);
|
||||
|
||||
class AudioCapture {
|
||||
public:
|
||||
virtual ~AudioCapture() = default;
|
||||
|
||||
virtual const PcmSpec& output_spec() const = 0;
|
||||
virtual void* buffer_data() = 0;
|
||||
virtual std::size_t buffer_size_bytes() const = 0;
|
||||
virtual std::size_t frames_per_buffer() const = 0;
|
||||
virtual Status Start() = 0;
|
||||
virtual Status Stop() = 0;
|
||||
virtual void Shutdown() = 0;
|
||||
};
|
||||
|
||||
struct SdlAudioCaptureOptions {
|
||||
PcmSpec output_spec;
|
||||
|
||||
// Zero selects the configured process target.
|
||||
std::uint32_t recording_device_id = 0;
|
||||
std::uint32_t frame_duration_ms = 10;
|
||||
AudioCaptureDataCallback data_callback = nullptr;
|
||||
void* data_context = nullptr;
|
||||
};
|
||||
|
||||
// SDL converts the host input to output_spec and delivers fixed-duration
|
||||
// frames through one stable buffer. The callback runs synchronously on SDL's
|
||||
// recording thread after that buffer has been filled.
|
||||
Status CreateSdlAudioCapture(const SdlAudioCaptureOptions& options,
|
||||
std::unique_ptr<AudioCapture>* capture);
|
||||
|
||||
// Called by the SDL subsystem owner before SDL_QuitSubSystem.
|
||||
Status PrepareSdlAudioCaptureSubsystemShutdown();
|
||||
|
||||
} // namespace mocktail::audio
|
||||
|
||||
#endif // MOCKTAIL_AUDIO_SDL_AUDIO_CAPTURE_H_
|
||||
@@ -48,6 +48,13 @@ struct SdlAudioSinkOptions {
|
||||
// Zero selects the configured process target.
|
||||
std::uint32_t playback_device_id = 0;
|
||||
bool start_paused = true;
|
||||
|
||||
// Optional pull provider for the host device. Each call fills one source
|
||||
// buffer whose address remains valid until the next provider call. SDL
|
||||
// copies as many buffers as the current device request needs.
|
||||
bool (*data_needed_callback)(void *context, const void **data,
|
||||
std::size_t *size_bytes) = nullptr;
|
||||
void *data_needed_context = nullptr;
|
||||
};
|
||||
|
||||
// SDL owns conversion, resampling, and borrowed-buffer consumption.
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#ifndef MOCKTAIL_AUDIO_WEBRTC_JNI_AUDIO_BRIDGE_H_
|
||||
#define MOCKTAIL_AUDIO_WEBRTC_JNI_AUDIO_BRIDGE_H_
|
||||
|
||||
#include "mocktail/status.h"
|
||||
|
||||
namespace jnivm {
|
||||
class VM;
|
||||
}
|
||||
|
||||
namespace mocktail::audio {
|
||||
|
||||
// Installs WebRtcAudioManager, WebRtcAudioRecord and WebRtcAudioTrack. The
|
||||
// manager only advertises the host PCM contract; capture opens on recording
|
||||
// init, never on manager construction. SDL's audio subsystem must remain alive
|
||||
// until ShutdownWebRtcJniAudioBridge.
|
||||
Status InstallWebRtcJniAudioBridge(jnivm::VM* vm);
|
||||
Status ShutdownWebRtcJniAudioBridge(jnivm::VM* vm);
|
||||
|
||||
} // namespace mocktail::audio
|
||||
|
||||
#endif // MOCKTAIL_AUDIO_WEBRTC_JNI_AUDIO_BRIDGE_H_
|
||||
@@ -19,30 +19,14 @@ inline constexpr AndroidVulkanResult kAndroidVulkanSuccess =
|
||||
static_cast<AndroidVulkanResult>(0);
|
||||
inline constexpr AndroidVulkanResult kAndroidVulkanErrorInitializationFailed =
|
||||
static_cast<AndroidVulkanResult>(-3);
|
||||
inline constexpr std::uint64_t kHostInfiniteWaitDiagnosticSliceNs =
|
||||
1000000000ULL;
|
||||
inline constexpr std::uint64_t kHostImageAcquireWatchdogTimeoutNs =
|
||||
kHostInfiniteWaitDiagnosticSliceNs;
|
||||
|
||||
inline constexpr char kAndroidSurfaceExtension[] =
|
||||
"VK_KHR_android_surface";
|
||||
|
||||
// Roblox treats VK_SUBOPTIMAL_KHR as a hard error, so expose it as success.
|
||||
// VK_SUBOPTIMAL_KHR still returns a usable image. Roblox handles it as a fatal
|
||||
// error, so accept it until the host reports a real out-of-date surface.
|
||||
VkResult NormalizeAndroidSwapchainResult(VkResult result);
|
||||
|
||||
// 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 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);
|
||||
|
||||
// Returned storage owns the translated extension names.
|
||||
Status TranslateAndroidVulkanInstanceExtensions(
|
||||
const std::vector<std::string>& android_extensions,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#ifndef MOCKTAIL_GRAPHICS_ETC2_DECODER_H_
|
||||
#define MOCKTAIL_GRAPHICS_ETC2_DECODER_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace mocktail::graphics {
|
||||
|
||||
// ETC2 and EAC block encodings from the OpenGL ES 3.0 / Vulkan core formats.
|
||||
// sRGB variants share the unorm encoding; the colour space belongs to the
|
||||
// destination format.
|
||||
enum class EtcFormat {
|
||||
kEtc2Rgb8,
|
||||
kEtc2Rgb8A1,
|
||||
kEtc2Rgba8,
|
||||
kEacR11,
|
||||
kEacR11Signed,
|
||||
kEacRg11,
|
||||
kEacRg11Signed,
|
||||
};
|
||||
|
||||
// Compressed bytes per 4x4 block.
|
||||
std::size_t EtcBlockBytes(EtcFormat format);
|
||||
|
||||
// Decoded bytes per texel: 4 (RGBA8) for ETC2 formats, 2 per channel
|
||||
// (16-bit, little-endian) for EAC formats.
|
||||
std::size_t EtcDecodedTexelBytes(EtcFormat format);
|
||||
|
||||
// Decodes a width x height image stored as ceil(width/4) x ceil(height/4)
|
||||
// blocks into row-major texels. Returns false, writing nothing, when a buffer
|
||||
// is smaller than the image requires.
|
||||
bool DecodeEtcImage(EtcFormat format, const std::uint8_t* source,
|
||||
std::size_t source_bytes, std::uint32_t width,
|
||||
std::uint32_t height, std::uint8_t* destination,
|
||||
std::size_t destination_bytes);
|
||||
|
||||
// Decodes block rows [first_block_row, first_block_row + block_row_count) of
|
||||
// the image DecodeEtcImage describes, writing only the texel rows those blocks
|
||||
// cover. Bands that together cover every block row produce DecodeEtcImage's
|
||||
// output. Returns false, writing nothing, when a buffer is too small or the
|
||||
// rows lie outside the image.
|
||||
bool DecodeEtcImageBlockRows(EtcFormat format, const std::uint8_t* source,
|
||||
std::size_t source_bytes, std::uint32_t width,
|
||||
std::uint32_t height,
|
||||
std::uint32_t first_block_row,
|
||||
std::uint32_t block_row_count,
|
||||
std::uint8_t* destination,
|
||||
std::size_t destination_bytes);
|
||||
|
||||
// One DecodeEtcImage call. Jobs must not share destination bytes.
|
||||
struct EtcDecodeJob {
|
||||
EtcFormat format = EtcFormat::kEtc2Rgb8;
|
||||
const std::uint8_t* source = nullptr;
|
||||
std::size_t source_bytes = 0;
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::uint8_t* destination = nullptr;
|
||||
std::size_t destination_bytes = 0;
|
||||
// Set to DecodeEtcImage's result.
|
||||
bool ok = false;
|
||||
};
|
||||
|
||||
// Decodes every job before returning. Large batches are split into block-row
|
||||
// bands decoded on up to worker_count threads, the caller included; small
|
||||
// batches decode on the caller only.
|
||||
void DecodeEtcJobs(EtcDecodeJob* jobs, std::size_t count,
|
||||
unsigned worker_count);
|
||||
|
||||
} // namespace mocktail::graphics
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_ETC2_DECODER_H_
|
||||
@@ -0,0 +1,36 @@
|
||||
#ifndef MOCKTAIL_GRAPHICS_GLES_TEXT_OVERLAY_COMPOSITOR_H_
|
||||
#define MOCKTAIL_GRAPHICS_GLES_TEXT_OVERLAY_COMPOSITOR_H_
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "mocktail/graphics/text_overlay_frame.h"
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
namespace mocktail::graphics {
|
||||
|
||||
struct GlesTextOverlaySource {
|
||||
bool (*may_present)() = nullptr;
|
||||
bool (*query)(MocktailTextOverlayFrameInfo *) = nullptr;
|
||||
bool (*copy)(std::uint64_t, void *, std::size_t) = nullptr;
|
||||
};
|
||||
|
||||
// Composites the host text editor before SDL swaps the game's GLES backbuffer.
|
||||
// Draw and destruction use the owning GL context when it is current. If that
|
||||
// context is no longer current at destruction, its destruction frees the GL
|
||||
// resources. Create a new compositor when creating a new context.
|
||||
class GlesTextOverlayCompositor {
|
||||
public:
|
||||
explicit GlesTextOverlayCompositor(GlesTextOverlaySource source);
|
||||
~GlesTextOverlayCompositor();
|
||||
|
||||
bool Draw(SDL_Window *window);
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace mocktail::graphics
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_GLES_TEXT_OVERLAY_COMPOSITOR_H_
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef MOCKTAIL_GRAPHICS_PRESENT_MODE_POLICY_H_
|
||||
#define MOCKTAIL_GRAPHICS_PRESENT_MODE_POLICY_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
@@ -17,10 +18,22 @@ enum class PresentModePolicy {
|
||||
|
||||
PresentModePolicy ResolvePresentModePolicy(std::string_view vsync_value,
|
||||
std::string_view frame_rate_value);
|
||||
|
||||
// Reads MOCKTAIL_VSYNC / MOCKTAIL_FRAME_RATE_LIMIT once. Window pacing and
|
||||
// the Vulkan adapter must use this instead of getenv'ing the same keys.
|
||||
PresentModePolicy CachedPresentModePolicy();
|
||||
|
||||
std::vector<VkPresentModeKHR>
|
||||
FilterPresentModes(PresentModePolicy policy,
|
||||
const std::vector<VkPresentModeKHR> &host_modes);
|
||||
const char *PresentModePolicyName(PresentModePolicy policy);
|
||||
const char *PresentModeKhrName(VkPresentModeKHR mode);
|
||||
|
||||
// Extra images so acquire does not wait on the compositor-held buffer.
|
||||
std::uint32_t PreferSwapchainMinImageCount(PresentModePolicy policy,
|
||||
std::uint32_t requested,
|
||||
std::uint32_t min_images,
|
||||
std::uint32_t max_images);
|
||||
|
||||
} // namespace graphics
|
||||
} // namespace mocktail
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
// Process-local ABI with libvulkan.so. Pixels are RGBA8; consumers must copy a
|
||||
// Process-local ABI for host compositors. Pixels are RGBA8; consumers must copy a
|
||||
// matching revision before the producer clears its storage.
|
||||
struct MocktailTextOverlayFrameInfo {
|
||||
static constexpr std::uint32_t kAbiVersion = 2;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#ifndef MOCKTAIL_GRAPHICS_TEXTURE_OVERRIDE_H_
|
||||
#define MOCKTAIL_GRAPHICS_TEXTURE_OVERRIDE_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail::graphics {
|
||||
|
||||
// Tightly packed RGBA8 pixels, row-major, top row first.
|
||||
struct RgbaImage {
|
||||
std::uint32_t width = 0;
|
||||
std::uint32_t height = 0;
|
||||
std::vector<std::uint8_t> pixels;
|
||||
};
|
||||
|
||||
// FNV-1a over the bytes; the texture identity used for dump and override
|
||||
// file names.
|
||||
std::uint64_t HashBytes(const std::uint8_t* data, std::size_t size);
|
||||
// 16 lowercase hex digits.
|
||||
std::string HashName(std::uint64_t hash);
|
||||
|
||||
bool ReadPngRgba(const std::string& path, RgbaImage* image);
|
||||
bool WritePngRgba(const std::string& path, const RgbaImage& image);
|
||||
|
||||
// Writes width x height RGBA8 pixels to destination, each destination pixel
|
||||
// the average of the source box it covers. Growing repeats source pixels.
|
||||
void ResampleRgba(const RgbaImage& source, std::uint32_t width,
|
||||
std::uint32_t height, std::uint8_t* destination);
|
||||
void ResampleRgba(const std::uint8_t* source, std::uint32_t source_width,
|
||||
std::uint32_t source_height, std::uint32_t width,
|
||||
std::uint32_t height, std::uint8_t* destination);
|
||||
|
||||
// Replacement textures keyed by the hash of a texture's level-0 compressed
|
||||
// bytes. An override lives at <override_dir>/<hash>.png. With a dump
|
||||
// directory, every level-0 texture seen is written once as
|
||||
// <dump_dir>/<hash>_<width>x<height>.png so the hash can be found.
|
||||
// Thread-safe.
|
||||
class TextureOverrides {
|
||||
public:
|
||||
TextureOverrides(std::string override_dir, std::string dump_dir);
|
||||
// MOCKTAIL_TEXTURE_OVERRIDE_DIR and MOCKTAIL_TEXTURE_DUMP_DIR.
|
||||
static TextureOverrides FromEnvironment();
|
||||
|
||||
bool enabled() const { return !override_dir_.empty() || !dump_dir_.empty(); }
|
||||
bool dumping() const { return !dump_dir_.empty(); }
|
||||
|
||||
// Null when no override file exists; lookups are cached either way.
|
||||
std::shared_ptr<const RgbaImage> Lookup(std::uint64_t hash);
|
||||
void Dump(std::uint64_t hash, std::uint32_t width, std::uint32_t height,
|
||||
const std::uint8_t* rgba);
|
||||
|
||||
private:
|
||||
std::string override_dir_;
|
||||
std::string dump_dir_;
|
||||
std::mutex mutex_;
|
||||
std::unordered_map<std::uint64_t, std::shared_ptr<const RgbaImage>> cache_;
|
||||
std::unordered_set<std::uint64_t> dumped_;
|
||||
};
|
||||
|
||||
} // namespace mocktail::graphics
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_TEXTURE_OVERRIDE_H_
|
||||
@@ -0,0 +1,131 @@
|
||||
#ifndef MOCKTAIL_GRAPHICS_VULKAN_ETC2_EMULATION_H_
|
||||
#define MOCKTAIL_GRAPHICS_VULKAN_ETC2_EMULATION_H_
|
||||
|
||||
#include <vulkan/vulkan.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "mocktail/graphics/etc2_decoder.h"
|
||||
|
||||
namespace mocktail::graphics {
|
||||
|
||||
// Host format that stores a decoded ETC2/EAC image, with its block encoding.
|
||||
struct Etc2EmulatedFormat {
|
||||
VkFormat host_format = VK_FORMAT_UNDEFINED;
|
||||
EtcFormat etc_format = EtcFormat::kEtc2Rgb8;
|
||||
};
|
||||
|
||||
// Maps an ETC2/EAC Vulkan format to its decoded host format. Returns false for
|
||||
// every other format.
|
||||
bool LookupEmulatedEtc2Format(VkFormat format, Etc2EmulatedFormat* out);
|
||||
|
||||
// Format features reported for an emulated ETC2/EAC format: the host format's
|
||||
// sampling and transfer features only, as for a native compressed format.
|
||||
VkFormatFeatureFlags EmulatedEtc2FormatFeatures(
|
||||
VkFormatFeatureFlags host_features);
|
||||
|
||||
// Staging bytes consumed and decoded bytes produced by one 2D upload region.
|
||||
// Returns false for an empty extent, a depth other than 1, or no layers.
|
||||
bool Etc2UploadByteCounts(EtcFormat format, const VkExtent3D& extent,
|
||||
std::uint32_t layer_count, VkDeviceSize* compressed,
|
||||
VkDeviceSize* decoded);
|
||||
|
||||
// Whether emulated ETC2 is reported to the application as a supported
|
||||
// feature, from the MOCKTAIL_ADVERTISE_ETC2 value: "0" hides it, anything
|
||||
// else (or unset) reports it. Hidden ETC2 still decodes ETC2 images the
|
||||
// application creates; it only steers the application toward other formats.
|
||||
bool Etc2SupportAdvertised(const char* value);
|
||||
bool Etc2SupportAdvertised();
|
||||
|
||||
// Factor applied to emulated ETC2 colour images no larger than
|
||||
// kSmallTextureMaxExtent, from the MOCKTAIL_SMALL_TEXTURE_UPSCALE value:
|
||||
// 1 leaves them alone; 2..8 create them that many times larger and resample
|
||||
// every upload, so a texture override can carry more detail than the
|
||||
// original. Unset or non-numeric values use kDefaultSmallTextureUpscale;
|
||||
// numeric values out of range are clamped.
|
||||
inline constexpr std::uint32_t kSmallTextureMaxExtent = 64;
|
||||
inline constexpr std::uint32_t kDefaultSmallTextureUpscale = 4;
|
||||
std::uint32_t SmallTextureUpscale(const char* value);
|
||||
|
||||
// Reports ETC2/EAC support on hosts without native ETC2 (desktop GPUs) and
|
||||
// stores those textures decoded. Images are created with the host format;
|
||||
// each staged upload into one is redirected to a host-visible staging buffer
|
||||
// that is filled by decoding the application's staging bytes when the
|
||||
// command buffer is submitted. Every submit decodes again, as a native copy
|
||||
// re-reads its source on each execution.
|
||||
// Staging buffers live until their command buffer is begun, reset or freed.
|
||||
// MOCKTAIL_DISABLE_ETC2_EMULATION=1 turns the emulation off.
|
||||
class VulkanEtc2Emulation final {
|
||||
public:
|
||||
VulkanEtc2Emulation();
|
||||
~VulkanEtc2Emulation();
|
||||
VulkanEtc2Emulation(const VulkanEtc2Emulation&) = delete;
|
||||
VulkanEtc2Emulation& operator=(const VulkanEtc2Emulation&) = delete;
|
||||
|
||||
// True when this physical device lacks native ETC2 and emulation is on.
|
||||
bool PhysicalDeviceNeedsEmulation(VkPhysicalDevice physical_device,
|
||||
PFN_vkGetPhysicalDeviceFeatures host);
|
||||
|
||||
void RegisterDevice(VkDevice device, VkPhysicalDevice physical_device,
|
||||
bool emulated,
|
||||
const VkPhysicalDeviceMemoryProperties& memory,
|
||||
PFN_vkGetDeviceProcAddr get_device_proc_addr);
|
||||
void DestroyDevice(VkDevice device);
|
||||
|
||||
VkResult CreateImage(VkDevice device, const VkImageCreateInfo* create_info,
|
||||
const VkAllocationCallbacks* allocator, VkImage* image);
|
||||
void DestroyImage(VkDevice device, VkImage image,
|
||||
const VkAllocationCallbacks* allocator);
|
||||
VkResult CreateImageView(VkDevice device,
|
||||
const VkImageViewCreateInfo* create_info,
|
||||
const VkAllocationCallbacks* allocator,
|
||||
VkImageView* view);
|
||||
VkResult BindBufferMemory(VkDevice device, VkBuffer buffer,
|
||||
VkDeviceMemory memory, VkDeviceSize offset);
|
||||
VkResult BindBufferMemory2(VkDevice device, std::uint32_t count,
|
||||
const VkBindBufferMemoryInfo* infos);
|
||||
VkResult MapMemory(VkDevice device, VkDeviceMemory memory,
|
||||
VkDeviceSize offset, VkDeviceSize size,
|
||||
VkMemoryMapFlags flags, void** data);
|
||||
VkResult MapMemory2(VkDevice device, const VkMemoryMapInfo* info,
|
||||
void** data);
|
||||
void UnmapMemory(VkDevice device, VkDeviceMemory memory);
|
||||
VkResult UnmapMemory2(VkDevice device, const VkMemoryUnmapInfo* info);
|
||||
void FreeMemory(VkDevice device, VkDeviceMemory memory,
|
||||
const VkAllocationCallbacks* allocator);
|
||||
void DestroyBuffer(VkDevice device, VkBuffer buffer,
|
||||
const VkAllocationCallbacks* allocator);
|
||||
void CmdCopyBufferToImage(VkDevice device, VkCommandBuffer command_buffer,
|
||||
VkBuffer source, VkImage destination,
|
||||
VkImageLayout layout, std::uint32_t region_count,
|
||||
const VkBufferImageCopy* regions);
|
||||
void CmdCopyBufferToImage2(VkDevice device, VkCommandBuffer command_buffer,
|
||||
const VkCopyBufferToImageInfo2* info);
|
||||
void CmdCopyImage(VkDevice device, VkCommandBuffer command_buffer,
|
||||
VkImage source, VkImageLayout source_layout,
|
||||
VkImage destination, VkImageLayout destination_layout,
|
||||
std::uint32_t region_count, const VkImageCopy* regions);
|
||||
void CmdCopyImage2(VkDevice device, VkCommandBuffer command_buffer,
|
||||
const VkCopyImageInfo2* info);
|
||||
void CmdExecuteCommands(VkDevice device, VkCommandBuffer command_buffer,
|
||||
std::uint32_t count,
|
||||
const VkCommandBuffer* secondaries);
|
||||
|
||||
// Decodes every upload recorded into these command buffers and the
|
||||
// secondary command buffers they execute. Large batches decode on worker
|
||||
// threads; this returns once every upload is decoded.
|
||||
void PrepareSubmit(const VkCommandBuffer* command_buffers,
|
||||
std::uint32_t count);
|
||||
// Destroys the staging buffers owned by a command buffer that is no longer
|
||||
// pending.
|
||||
void ReleaseCommandBuffer(VkCommandBuffer command_buffer);
|
||||
|
||||
struct State;
|
||||
|
||||
private:
|
||||
State* state_;
|
||||
};
|
||||
|
||||
} // namespace mocktail::graphics
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_VULKAN_ETC2_EMULATION_H_
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef MOCKTAIL_PLATFORM_PLATFORM_RUNTIME_H_
|
||||
#define MOCKTAIL_PLATFORM_PLATFORM_RUNTIME_H_
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
@@ -26,7 +27,7 @@ struct WindowOptions {
|
||||
int height = 720;
|
||||
std::string title = "Roblox";
|
||||
WindowSurfaceApi surface_api = WindowSurfaceApi::kAngleEgl;
|
||||
bool high_pixel_density = true;
|
||||
bool high_pixel_density = false;
|
||||
bool initially_hidden = true;
|
||||
|
||||
// Production callers should keep this true. Tests using SDL's dummy driver
|
||||
@@ -58,11 +59,69 @@ struct NativeWindowDescriptor {
|
||||
|
||||
struct QuitEvent {};
|
||||
|
||||
// Android exposes pointer and TextBox geometry in density-independent surface
|
||||
// coordinates. SDL exposes pointer positions in logical window coordinates,
|
||||
// while Vulkan presents to the pixel-sized drawable. Keep all three spaces in
|
||||
// one value so input, IME placement and same-surface overlays use exact inverse
|
||||
// transforms.
|
||||
struct SurfaceCoordinateTransform {
|
||||
int32_t logical_width = 0;
|
||||
int32_t logical_height = 0;
|
||||
int32_t pixel_width = 0;
|
||||
int32_t pixel_height = 0;
|
||||
float density_scale = 1.0F;
|
||||
|
||||
bool valid() const {
|
||||
return logical_width > 0 && logical_height > 0 && pixel_width > 0 &&
|
||||
pixel_height > 0 && std::isfinite(density_scale) &&
|
||||
density_scale > 0.0F;
|
||||
}
|
||||
|
||||
float guest_width() const {
|
||||
return valid() ? static_cast<float>(pixel_width) / density_scale : 0.0F;
|
||||
}
|
||||
float guest_height() const {
|
||||
return valid() ? static_cast<float>(pixel_height) / density_scale : 0.0F;
|
||||
}
|
||||
int32_t rounded_guest_width() const {
|
||||
return valid() ? static_cast<int32_t>(std::lround(guest_width())) : 0;
|
||||
}
|
||||
int32_t rounded_guest_height() const {
|
||||
return valid() ? static_cast<int32_t>(std::lround(guest_height())) : 0;
|
||||
}
|
||||
|
||||
float HostLogicalToGuestX(float coordinate) const {
|
||||
return valid() ? coordinate * static_cast<float>(pixel_width) /
|
||||
static_cast<float>(logical_width) / density_scale
|
||||
: coordinate;
|
||||
}
|
||||
float HostLogicalToGuestY(float coordinate) const {
|
||||
return valid() ? coordinate * static_cast<float>(pixel_height) /
|
||||
static_cast<float>(logical_height) / density_scale
|
||||
: coordinate;
|
||||
}
|
||||
float GuestToHostLogicalX(float coordinate) const {
|
||||
return valid() ? coordinate * density_scale *
|
||||
static_cast<float>(logical_width) /
|
||||
static_cast<float>(pixel_width)
|
||||
: coordinate;
|
||||
}
|
||||
float GuestToHostLogicalY(float coordinate) const {
|
||||
return valid() ? coordinate * density_scale *
|
||||
static_cast<float>(logical_height) /
|
||||
static_cast<float>(pixel_height)
|
||||
: coordinate;
|
||||
}
|
||||
};
|
||||
|
||||
struct WindowResizedEvent {
|
||||
int logical_width = 0;
|
||||
int logical_height = 0;
|
||||
int pixel_width = 0;
|
||||
int pixel_height = 0;
|
||||
// Android DisplayMetrics.density equivalent. This is independent of the
|
||||
// logical-to-pixel drawable ratio on X11/XWayland.
|
||||
float density_scale = 1.0F;
|
||||
};
|
||||
|
||||
struct WindowFocusEvent {
|
||||
@@ -128,9 +187,27 @@ struct TouchEvent {
|
||||
float pressure = 0.0f;
|
||||
};
|
||||
|
||||
enum class GamepadFamily {
|
||||
kUnknown,
|
||||
kXbox,
|
||||
kPlayStation4,
|
||||
kPlayStation5,
|
||||
};
|
||||
|
||||
struct GamepadDescriptor {
|
||||
GamepadFamily family = GamepadFamily::kUnknown;
|
||||
std::string name;
|
||||
// Bit positions are SDL's standardized gamepad buttons and axes, not raw
|
||||
// joystick indices or Android key codes.
|
||||
std::uint32_t buttons = 0;
|
||||
std::uint32_t axes = 0;
|
||||
};
|
||||
|
||||
struct GamepadConnectionEvent {
|
||||
std::int64_t instance_id = 0;
|
||||
bool connected = false;
|
||||
GamepadDescriptor descriptor{};
|
||||
bool remapped = false;
|
||||
};
|
||||
|
||||
struct GamepadAxisEvent {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
#ifndef MOCKTAIL_PLATFORM_SDL_APPLICATION_METADATA_H_
|
||||
#define MOCKTAIL_PLATFORM_SDL_APPLICATION_METADATA_H_
|
||||
|
||||
@@ -6,9 +7,9 @@
|
||||
namespace mocktail {
|
||||
namespace platform {
|
||||
|
||||
inline constexpr char kMocktailApplicationName[] = "Mocktail";
|
||||
inline constexpr char kMocktailApplicationName[] = "Nightcap";
|
||||
inline constexpr char kMocktailApplicationIdentifier[] =
|
||||
"space.bigrat.mocktail";
|
||||
"io.github.CoderDayton.nightcap";
|
||||
|
||||
// Configures the stable compositor identity before SDL initializes. The
|
||||
// identifier matches the installed desktop file and icon name so Wayland and
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#ifndef MOCKTAIL_PLATFORM_SDL_GAMEPAD_MANAGER_H_
|
||||
#define MOCKTAIL_PLATFORM_SDL_GAMEPAD_MANAGER_H_
|
||||
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
|
||||
#include <map>
|
||||
|
||||
#include "mocktail/platform/platform_runtime.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace platform {
|
||||
|
||||
// Main-thread owner. Consumes events from the existing SDL pump; it never
|
||||
// creates another event loop or consumes keyboard/window events.
|
||||
class SdlGamepadManager final {
|
||||
public:
|
||||
using EventFn = void (*)(void*, const PlatformEvent&);
|
||||
|
||||
~SdlGamepadManager();
|
||||
SdlGamepadManager() = default;
|
||||
SdlGamepadManager(const SdlGamepadManager&) = delete;
|
||||
SdlGamepadManager& operator=(const SdlGamepadManager&) = delete;
|
||||
|
||||
Status Initialize(void* context, EventFn emit);
|
||||
void Shutdown();
|
||||
bool HandleEvent(const PlatformEvent& event);
|
||||
void ResendState();
|
||||
|
||||
private:
|
||||
SDL_Gamepad* Open(SDL_JoystickID id);
|
||||
void Remove(SDL_JoystickID id);
|
||||
void Describe(SDL_JoystickID id, SDL_Gamepad* gamepad, bool remapped);
|
||||
void EmitState(SDL_JoystickID id, SDL_Gamepad* gamepad);
|
||||
void Emit(PlatformEventPayload payload);
|
||||
|
||||
std::map<SDL_JoystickID, SDL_Gamepad*> gamepads_;
|
||||
void* context_ = nullptr;
|
||||
EventFn emit_ = nullptr;
|
||||
bool initialized_ = false;
|
||||
};
|
||||
|
||||
} // namespace platform
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_PLATFORM_SDL_GAMEPAD_MANAGER_H_
|
||||
@@ -1,45 +0,0 @@
|
||||
#ifndef MOCKTAIL_MOCKTAIL_SHA256_H_
|
||||
#define MOCKTAIL_MOCKTAIL_SHA256_H_
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail {
|
||||
namespace foundation {
|
||||
|
||||
class Sha256 final {
|
||||
public:
|
||||
Sha256() = default;
|
||||
~Sha256();
|
||||
|
||||
Sha256(const Sha256&) = delete;
|
||||
Sha256& operator=(const Sha256&) = delete;
|
||||
|
||||
void Update(const unsigned char* bytes, std::size_t size);
|
||||
void Update(std::string_view bytes);
|
||||
std::string FinalHex();
|
||||
|
||||
private:
|
||||
void Transform(const unsigned char* block);
|
||||
|
||||
std::array<std::uint32_t, 8> state_ = {
|
||||
0x6a09e667U, 0xbb67ae85U, 0x3c6ef372U, 0xa54ff53aU,
|
||||
0x510e527fU, 0x9b05688cU, 0x1f83d9abU, 0x5be0cd19U,
|
||||
};
|
||||
std::array<unsigned char, 64> block_{};
|
||||
std::size_t block_bytes_ = 0;
|
||||
std::uint64_t total_bytes_ = 0;
|
||||
bool finalized_ = false;
|
||||
};
|
||||
|
||||
// Returns the lowercase SHA-256 digest of the exact byte sequence, including
|
||||
// embedded NUL bytes.
|
||||
std::string ComputeSha256Hex(std::string_view bytes);
|
||||
|
||||
} // namespace foundation
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_MOCKTAIL_SHA256_H_
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_AUTH_RUNTIME_COMPOSITION_H_
|
||||
#define MOCKTAIL_RUNTIME_AUTH_RUNTIME_COMPOSITION_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -98,6 +99,10 @@ AuthRuntimeComposition ComposeAuthRuntime(
|
||||
services::AuthService& auth_service,
|
||||
std::shared_ptr<services::HttpClient> live_auth_http_client);
|
||||
|
||||
// Persists a raw or prefixed Roblox credential to the given cookie file atomically.
|
||||
bool PersistRobloxCookie(const std::filesystem::path& path,
|
||||
std::string_view cookie_value);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ struct CommandLineOptions {
|
||||
std::string launch_request_json;
|
||||
int launch_argument_index = -1;
|
||||
bool allow_unverified_build = false;
|
||||
// Requests one explicit launch of the provider latest without promoting it
|
||||
// into the managed payload state.
|
||||
bool force_run_latest = false;
|
||||
};
|
||||
|
||||
struct CommandLineParseResult {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_DEVICE_MEMORY_PROFILE_H_
|
||||
#define MOCKTAIL_RUNTIME_DEVICE_MEMORY_PROFILE_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Android ActivityManager memory values published through DeviceParams.
|
||||
// Roblox derives its device RAM tier, and tiered render budgets, from them.
|
||||
struct DeviceMemoryProfile {
|
||||
std::int32_t total_memory_mb = 0;
|
||||
std::int32_t memory_class_mb = 0;
|
||||
std::int32_t large_memory_class_mb = 0;
|
||||
std::int64_t low_memory_killer_background_threshold = 0;
|
||||
std::int64_t low_memory_killer_foreground_threshold = 0;
|
||||
bool low_ram_device = false;
|
||||
};
|
||||
|
||||
// Hosts with at least 4 GiB report their real total memory as a normal-RAM
|
||||
// device. Smaller hosts, or an unreadable total (0), keep the 2 GiB low-RAM
|
||||
// profile.
|
||||
DeviceMemoryProfile BuildDeviceMemoryProfile(std::uint64_t host_memory_bytes);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_DEVICE_MEMORY_PROFILE_H_
|
||||
@@ -0,0 +1,73 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_DISPLAY_SIZE_H_
|
||||
#define MOCKTAIL_RUNTIME_DISPLAY_SIZE_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Host display or window size in physical pixels, published to the Android
|
||||
// guest. Header-only so the JNI VM can use it without link dependencies.
|
||||
struct DisplaySize {
|
||||
std::int32_t width = 1920;
|
||||
std::int32_t height = 1080;
|
||||
};
|
||||
|
||||
// Environment handoffs written after the host window exists, as
|
||||
// "WIDTHxHEIGHT": the window's display mode and the window's initial pixels.
|
||||
inline constexpr const char* kDisplaySizeEnvironment =
|
||||
"MOCKTAIL_DISPLAY_SIZE_INTERNAL";
|
||||
inline constexpr const char* kWindowSizeEnvironment =
|
||||
"MOCKTAIL_WINDOW_SIZE_INTERNAL";
|
||||
|
||||
// Parses "WIDTHxHEIGHT". A null, malformed or non-positive value, or one that
|
||||
// does not fit a Java int, yields the 1920x1080 fallback.
|
||||
inline DisplaySize ParseDisplaySize(const char* value) {
|
||||
const DisplaySize fallback;
|
||||
if (value == nullptr) {
|
||||
return fallback;
|
||||
}
|
||||
std::int64_t parts[2] = {0, 0};
|
||||
const char* cursor = value;
|
||||
for (int index = 0; index < 2; ++index) {
|
||||
if (*cursor < '0' || *cursor > '9') {
|
||||
return fallback;
|
||||
}
|
||||
std::int64_t parsed = 0;
|
||||
while (*cursor >= '0' && *cursor <= '9') {
|
||||
parsed = parsed * 10 + (*cursor - '0');
|
||||
if (parsed > std::numeric_limits<std::int32_t>::max()) {
|
||||
return fallback;
|
||||
}
|
||||
++cursor;
|
||||
}
|
||||
if (parsed == 0) {
|
||||
return fallback;
|
||||
}
|
||||
parts[index] = parsed;
|
||||
if (index == 0) {
|
||||
if (*cursor != 'x') {
|
||||
return fallback;
|
||||
}
|
||||
++cursor;
|
||||
}
|
||||
}
|
||||
if (*cursor != '\0') {
|
||||
return fallback;
|
||||
}
|
||||
return {static_cast<std::int32_t>(parts[0]),
|
||||
static_cast<std::int32_t>(parts[1])};
|
||||
}
|
||||
|
||||
// Physical length of a pixel span at the guest's fixed 160 dpi, rounded down
|
||||
// to whole millimetres.
|
||||
inline std::int32_t PixelsToMillimetersAt160Dpi(std::int32_t pixels) {
|
||||
return static_cast<std::int32_t>(static_cast<std::int64_t>(pixels) * 254 /
|
||||
1600);
|
||||
}
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_DISPLAY_SIZE_H_
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_FLEASION_H_
|
||||
#define MOCKTAIL_RUNTIME_FLEASION_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
#include "runtime/runtime_config.h"
|
||||
#include "runtime/runtime_paths.h"
|
||||
|
||||
namespace mocktail::runtime {
|
||||
|
||||
struct FleasionPreparation {
|
||||
std::filesystem::path certificate;
|
||||
std::filesystem::path base_bundle;
|
||||
std::filesystem::path bundle;
|
||||
std::string error;
|
||||
explicit operator bool() const { return error.empty(); }
|
||||
};
|
||||
|
||||
// Generates a private, replaceable trust bundle outside the managed payload.
|
||||
// Reads certificates only; never reads Fleasion's private key or changes it.
|
||||
FleasionPreparation PrepareFleasion(const RuntimeConfig& config,
|
||||
const Environment& environment,
|
||||
const RuntimePaths& paths);
|
||||
|
||||
} // namespace mocktail::runtime
|
||||
|
||||
#endif
|
||||
@@ -10,6 +10,7 @@ namespace runtime {
|
||||
inline constexpr int kMaximumSupportedRobloxSchedulerFps = 240;
|
||||
|
||||
enum class FrameRateLimitMode {
|
||||
kUnmanaged,
|
||||
kDisplay,
|
||||
kFixed,
|
||||
kUnlimited,
|
||||
@@ -17,7 +18,7 @@ enum class FrameRateLimitMode {
|
||||
};
|
||||
|
||||
struct FrameRatePolicy {
|
||||
FrameRateLimitMode mode = FrameRateLimitMode::kDisplay;
|
||||
FrameRateLimitMode mode = FrameRateLimitMode::kUnmanaged;
|
||||
int fixed_fps = 0;
|
||||
|
||||
bool valid() const { return mode != FrameRateLimitMode::kInvalid; }
|
||||
@@ -25,7 +26,11 @@ struct FrameRatePolicy {
|
||||
|
||||
FrameRatePolicy ParseFrameRatePolicy(std::string_view value);
|
||||
|
||||
// Merges the scheduler policy into Roblox's supported client-settings ingress.
|
||||
// Enables Roblox's Basic Settings frame-rate control and, when explicitly
|
||||
// requested, merges a Mocktail scheduler policy into client settings.
|
||||
// Unmanaged (-1) does not add a scheduler target, so Roblox owns the cap.
|
||||
// Every positive fixed value is forwarded verbatim to
|
||||
// DFIntTaskSchedulerTargetFps; it is not restricted to common refresh rates.
|
||||
// Unlimited selects the unmodified payload's maximum scheduler target (240).
|
||||
// With VSync auto/off, graphics policy separately requests an unthrottled
|
||||
// Vulkan present mode when the host exposes one.
|
||||
|
||||
@@ -41,6 +41,9 @@ struct GameSurface {
|
||||
uintptr_t native_window = 0;
|
||||
uint32_t width = 0;
|
||||
uint32_t height = 0;
|
||||
// Host content scale for Android PlatformParams.dpiScale. Zero means the
|
||||
// producer has no scale information and the JNI configuration is used.
|
||||
float dpi_scale = 0.0f;
|
||||
};
|
||||
|
||||
enum class GamePresentBackend {
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_GRAPHICS_LAUNCH_POLICY_H_
|
||||
#define MOCKTAIL_RUNTIME_GRAPHICS_LAUNCH_POLICY_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "runtime/runtime_config.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Readable ICD manifest for `vendor` that this build can actually load, or an
|
||||
// empty path. Directories are searched in order; a manifest built for another
|
||||
// architecture is never selected.
|
||||
std::string SelectVulkanIcdManifest(
|
||||
const std::vector<std::filesystem::path>& directories,
|
||||
std::string_view vendor);
|
||||
|
||||
// Publishes the resolved graphics backend before the managed payload updater
|
||||
// starts. OpenGL is a strict system EGL/GLES path; it never silently retries
|
||||
// through ANGLE/Vulkan or accepts a window without a real graphics context.
|
||||
|
||||
@@ -12,12 +12,16 @@ namespace runtime {
|
||||
struct PayloadUpdatePreflightResult {
|
||||
bool attempted = false;
|
||||
std::string error;
|
||||
// Last failure the updater reported on its own stderr. Empty when the
|
||||
// updater never ran or never explained itself.
|
||||
std::string details;
|
||||
|
||||
explicit operator bool() const { return error.empty(); }
|
||||
};
|
||||
|
||||
PayloadUpdatePreflightResult RunPayloadUpdatePreflight(
|
||||
const Environment& environment, const RuntimePaths& paths);
|
||||
const Environment& environment, const RuntimePaths& paths,
|
||||
bool force_run_latest = false);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
@@ -62,6 +62,18 @@ bool MergeRuntimeClientSettingsOverrides(const FrameRatePolicy& frame_rate,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
|
||||
// Keeps the legacy test bypass disabled. The host PermissionsProtocol now
|
||||
// answers microphone authorization from capture configuration. No permission
|
||||
// query opens a recording device.
|
||||
bool MergeAudioDeviceMenuClientSettingsOverrides(std::string_view base_json,
|
||||
std::string *merged_json,
|
||||
std::string *error);
|
||||
|
||||
bool MergeAudioCaptureClientSettingsOverrides(bool microphone_enabled,
|
||||
std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_PLATFORM_CACHE_MIGRATION_H_
|
||||
#define MOCKTAIL_RUNTIME_PLATFORM_CACHE_MIGRATION_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
@@ -37,6 +39,25 @@ PlatformCacheMigrationResult MigratePlatformProfileCaches(
|
||||
const Environment& environment, const RuntimePaths& paths,
|
||||
std::string_view desired_revision);
|
||||
|
||||
struct RobloxThemeCacheResult {
|
||||
std::optional<bool> dark_theme;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const { return error.empty(); }
|
||||
};
|
||||
|
||||
// Reads the current account's Roblox theme without changing appStorage. A
|
||||
// missing value leaves dark_theme empty so the caller can use Roblox's own
|
||||
// startup default.
|
||||
RobloxThemeCacheResult ReadRobloxThemeCache(
|
||||
const std::filesystem::path& app_storage_file,
|
||||
std::int64_t authenticated_user_id);
|
||||
|
||||
// Synchronizes only Roblox's local theme keys with the resolved host theme.
|
||||
bool ApplyRobloxThemeCacheOverride(
|
||||
const std::filesystem::path& app_storage_file,
|
||||
std::int64_t authenticated_user_id, bool dark_theme, std::string* error);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_PROCESS_DIAGNOSTICS_H_
|
||||
#define MOCKTAIL_RUNTIME_PROCESS_DIAGNOSTICS_H_
|
||||
|
||||
namespace mocktail::runtime {
|
||||
|
||||
enum class ProcessDiagnosticStage { kStartup, kNativeRuntime, kShutdown };
|
||||
|
||||
// Installs a diagnostic handler only if SIGXCPU still has its default action.
|
||||
// The handler logs to stderr and re-delivers SIGXCPU with the default action.
|
||||
void InstallCpuLimitDiagnostics();
|
||||
void LogProcessDiagnostics(ProcessDiagnosticStage stage);
|
||||
|
||||
} // namespace mocktail::runtime
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_PROCESS_DIAGNOSTICS_H_
|
||||
@@ -0,0 +1,16 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_PROCESS_LAUNCH_POLICY_H_
|
||||
#define MOCKTAIL_RUNTIME_PROCESS_LAUNCH_POLICY_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace mocktail::runtime {
|
||||
|
||||
// Call only for interactive launches, before creating any threads. Removes an
|
||||
// inherited soft CPU limit when the hard limit is unlimited and demotes the
|
||||
// calling thread from FIFO/RR scheduling. Returns diagnostics for the session
|
||||
// log, including failures; never changes hard limits or signal dispositions.
|
||||
std::string ApplyInteractiveProcessLaunchPolicy();
|
||||
|
||||
} // namespace mocktail::runtime
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_PROCESS_LAUNCH_POLICY_H_
|
||||
@@ -20,14 +20,15 @@ struct DesktopAppPolicyResult {
|
||||
explicit operator bool() const { return error.empty(); }
|
||||
};
|
||||
|
||||
// Normalizes only Roblox UniversalApp layout coordinates for a PC profile.
|
||||
// Account eligibility and feature-entitlement fields remain server-owned.
|
||||
// Normalizes Roblox UniversalApp layout coordinates for a PC profile and
|
||||
// applies a fixed theme only when configured. Account eligibility and
|
||||
// feature-entitlement fields remain server-owned.
|
||||
// The guest and authenticated cache keys are both populated so user-switch
|
||||
// startup cannot fall back to the Android Unknown/mobile layout.
|
||||
DesktopAppPolicyResult ApplyDesktopAppPolicy(
|
||||
const std::filesystem::path& app_storage_file,
|
||||
const std::filesystem::path& default_policy_file,
|
||||
std::int64_t authenticated_user_id);
|
||||
std::int64_t authenticated_user_id, std::string_view theme_mode);
|
||||
|
||||
// Routes the normalized policy through Roblox's supported runtime override so
|
||||
// a later GUAC response cannot replace the in-memory desktop layout with the
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/owned_pthread.h"
|
||||
@@ -16,6 +17,7 @@
|
||||
#include "runtime/roblox_experience_launch_bridge.h"
|
||||
#include "runtime/roblox_experience_presence.h"
|
||||
#include "runtime/roblox_fresh_game_launch_controller.h"
|
||||
#include "runtime/roblox_permissions_bridge.h"
|
||||
#include "runtime/roblox_web_view_bridge.h"
|
||||
#include "runtime/webview_roblox_cookie.h"
|
||||
|
||||
@@ -55,6 +57,10 @@ struct RobloxExperienceJniFactory {
|
||||
void (*on_sync_cookies)(void*, JNIEnv*, jstring),
|
||||
void (*on_set_cookie)(void*, JNIEnv*, jstring, jstring)) = nullptr;
|
||||
void (*clear_platform_web_callbacks)(void* context) = nullptr;
|
||||
jobject (*create_async_request_handler)(
|
||||
void* context, std::shared_ptr<void> callback_context,
|
||||
void (*run)(void*, JNIEnv*, jstring, jstring)) = nullptr;
|
||||
void (*clear_async_request_handler)(void* context, jobject handler) = nullptr;
|
||||
|
||||
bool complete() const {
|
||||
return context != nullptr && create_raw_callback != nullptr &&
|
||||
@@ -63,7 +69,9 @@ struct RobloxExperienceJniFactory {
|
||||
create_mem_storage_callback != nullptr &&
|
||||
clear_mem_storage_callback != nullptr &&
|
||||
set_platform_web_callbacks != nullptr &&
|
||||
clear_platform_web_callbacks != nullptr;
|
||||
clear_platform_web_callbacks != nullptr &&
|
||||
create_async_request_handler != nullptr &&
|
||||
clear_async_request_handler != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -103,6 +111,7 @@ class RobloxExperienceComposition final {
|
||||
RobloxExperienceMessageBusSymbols message_bus_symbols,
|
||||
RobloxWebViewMessageBusSymbols web_view_symbols,
|
||||
RobloxBrowserServiceSymbols browser_service_symbols,
|
||||
RobloxPermissionsMessageBusSymbols permissions_symbols,
|
||||
RobloxGameSessionSymbols game_symbols,
|
||||
RobloxExperienceJniFactory jni_factory,
|
||||
RobloxFreshLaunchPresentBoundary present_boundary,
|
||||
@@ -110,7 +119,8 @@ class RobloxExperienceComposition final {
|
||||
const SecureRobloxCredential* initial_web_view_credential = nullptr,
|
||||
RobloxExperienceSurfaceProvider surface_provider = {},
|
||||
RobloxExperiencePresenceObserver presence_observer = {},
|
||||
bool clear_persisted_web_view_cookie = false);
|
||||
bool clear_persisted_web_view_cookie = false,
|
||||
bool microphone_enabled = false);
|
||||
~RobloxExperienceComposition();
|
||||
|
||||
RobloxExperienceComposition(const RobloxExperienceComposition&) = delete;
|
||||
@@ -184,6 +194,7 @@ class RobloxExperienceComposition final {
|
||||
const std::shared_ptr<WebViewHelperProcess>& source_process,
|
||||
uint64_t process_generation, uint64_t logical_generation,
|
||||
const WebViewHelperEvent& event);
|
||||
Status AcceptWebViewRobloxCookie(std::string_view value);
|
||||
Status OpenWebSurface(const std::string& url, const char* transport,
|
||||
WebSurfaceRoute route,
|
||||
WebViewHelperExitObserver exit_observer,
|
||||
@@ -217,6 +228,8 @@ class RobloxExperienceComposition final {
|
||||
const RobloxExperienceMessageBusSymbols message_bus_symbols_;
|
||||
const RobloxWebViewMessageBusSymbols web_view_symbols_;
|
||||
const RobloxBrowserServiceSymbols browser_service_symbols_;
|
||||
const RobloxPermissionsMessageBusSymbols permissions_symbols_;
|
||||
const bool microphone_enabled_;
|
||||
const RobloxGameSessionSymbols game_symbols_;
|
||||
const RobloxExperienceJniFactory jni_factory_;
|
||||
const RobloxFreshLaunchPresentBoundary present_boundary_;
|
||||
@@ -234,6 +247,7 @@ class RobloxExperienceComposition final {
|
||||
std::unique_ptr<RobloxExperienceLaunchBridge> bridge_;
|
||||
std::unique_ptr<RobloxWebViewBridge> web_view_bridge_;
|
||||
std::unique_ptr<RobloxBrowserServiceBridge> browser_service_bridge_;
|
||||
std::unique_ptr<RobloxPermissionsBridge> permissions_bridge_;
|
||||
std::shared_ptr<WebViewHelperProcess> web_surface_process_;
|
||||
std::shared_ptr<WebSurfaceExitTarget> web_surface_exit_target_;
|
||||
std::shared_ptr<LifecycleTarget> lifecycle_target_;
|
||||
|
||||
@@ -192,7 +192,8 @@ class RobloxGameSessionNativeAdapter final {
|
||||
Status RebindAndInvoke(const GameSurface& surface, bool resume);
|
||||
Status CreateSurfaceObjectLocked(JNIEnv* env, const GameSurface& surface,
|
||||
jobject* result) const;
|
||||
Status CreatePlatformParamsLocked(JNIEnv* env, jobject* result) const;
|
||||
Status CreatePlatformParamsLocked(JNIEnv* env, const GameSurface& surface,
|
||||
jobject* result) const;
|
||||
Status CheckJniException(JNIEnv* env, const char* operation) const;
|
||||
void ReleaseGlobalReferenceLocked(JNIEnv* env, jobject reference);
|
||||
void ReleaseGlobalReferencesLocked(JNIEnv* env);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
@@ -32,6 +33,7 @@ class RobloxInputNativeAdapter final {
|
||||
Status Initialize();
|
||||
Status Release();
|
||||
bool initialized() const;
|
||||
bool SupportsGamepads() const;
|
||||
Status GetMainWindowIsMouseLockedCenter(bool* locked_center);
|
||||
Status QueryCurrentTextBoxInfo(RobloxNativeTextBoxInfoQueryResult* result);
|
||||
RobloxInputSink Sink();
|
||||
@@ -48,6 +50,19 @@ class RobloxInputNativeAdapter final {
|
||||
int32_t height);
|
||||
static Status KeyCallback(void* context, bool pressed, int32_t scan_code,
|
||||
int32_t key_code, bool repeat);
|
||||
static Status GamepadSupportedKeyCallback(void* context, int32_t device_id,
|
||||
int32_t key, bool supported,
|
||||
int32_t type);
|
||||
static Status GamepadSupportedMotionCallback(void* context, int32_t device_id,
|
||||
int32_t axis, int32_t direction,
|
||||
bool supported, int32_t type);
|
||||
static Status GamepadConnectCallback(void* context, int32_t device_id,
|
||||
int32_t type);
|
||||
static Status GamepadDisconnectCallback(void* context, int32_t device_id);
|
||||
static Status GamepadButtonCallback(void* context, int32_t device_id,
|
||||
int32_t key, bool pressed);
|
||||
static Status GamepadAxisCallback(void* context, int32_t device_id,
|
||||
int32_t axis, float x, float y, float z);
|
||||
static Status SyncTextCallback(void* context, const char* utf8,
|
||||
std::size_t size, int32_t cursor_utf16);
|
||||
static Status PassTextCallback(void* context, int64_t textbox_handle,
|
||||
@@ -69,10 +84,12 @@ class RobloxInputNativeAdapter final {
|
||||
Status ReleaseFocus(int64_t textbox_handle);
|
||||
Status CheckJniException(JNIEnv* env, const char* operation) const;
|
||||
Status RequireReadyLocked(JNIEnv** env) const;
|
||||
Status AcquireReadyEnv(JNIEnv** env) const;
|
||||
|
||||
const JniEnvironmentProvider environment_;
|
||||
const RobloxInputSymbols symbols_;
|
||||
mutable std::mutex mutex_;
|
||||
std::atomic<bool> ready_{false};
|
||||
jclass native_input_class_ = nullptr;
|
||||
jclass native_gl_class_ = nullptr;
|
||||
bool initialized_ = false;
|
||||
@@ -105,6 +122,7 @@ class RobloxInputRuntime final {
|
||||
RobloxInputDispatchResult HandleEvent(const platform::PlatformEvent& event);
|
||||
RobloxInputSnapshot Snapshot() const;
|
||||
Status GetMainWindowIsMouseLockedCenter(bool* locked_center);
|
||||
bool SupportsGamepads() const;
|
||||
|
||||
static void PlatformEventCallback(void* context,
|
||||
const platform::PlatformEvent& event);
|
||||
@@ -121,6 +139,7 @@ class RobloxInputRuntime final {
|
||||
bool logged_text_ = false;
|
||||
bool logged_text_unsupported_ = false;
|
||||
bool logged_native_error_ = false;
|
||||
bool logged_gamepad_ = false;
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_INPUT_ROUTER_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_INPUT_ROUTER_H_
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
@@ -17,10 +19,12 @@ struct RobloxInputViewport {
|
||||
int32_t logical_height = 0;
|
||||
int32_t pixel_width = 0;
|
||||
int32_t pixel_height = 0;
|
||||
float density_scale = 1.0F;
|
||||
|
||||
bool valid() const {
|
||||
return logical_width > 0 && logical_height > 0 && pixel_width > 0 &&
|
||||
pixel_height > 0;
|
||||
pixel_height > 0 && std::isfinite(density_scale) &&
|
||||
density_scale > 0.0F;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -34,12 +38,39 @@ struct AndroidKeyMapping {
|
||||
// Maps SDL's USB scancode to the Linux evdev scan code and Android KeyEvent
|
||||
// keycode observed by the APK. Unmapped keys are rejected rather than passed
|
||||
// through with an ABI-incompatible numeric value.
|
||||
AndroidKeyMapping MapSdlKeyToAndroid(uint32_t sdl_scancode);
|
||||
AndroidKeyMapping MapSdlKeyToAndroid(uint32_t sdl_scancode, uint32_t sdl_key = 0);
|
||||
|
||||
// Mirrors the APK's MotionEvent.getActionButton() - 1 conversion. SDL button
|
||||
// numbers are not numerically compatible with Android button bit values.
|
||||
int32_t MapSdlMouseButtonToAndroid(uint8_t sdl_button);
|
||||
|
||||
int32_t MapSdlGamepadButtonToAndroid(uint8_t sdl_button);
|
||||
int32_t RobloxGamepadType(platform::GamepadFamily family);
|
||||
|
||||
struct RobloxGamepadSink {
|
||||
using SupportedKeyFn = Status (*)(void*, int32_t, int32_t, bool, int32_t);
|
||||
using SupportedMotionFn = Status (*)(void*, int32_t, int32_t, int32_t, bool,
|
||||
int32_t);
|
||||
using ConnectFn = Status (*)(void*, int32_t, int32_t);
|
||||
using DisconnectFn = Status (*)(void*, int32_t);
|
||||
using ButtonFn = Status (*)(void*, int32_t, int32_t, bool);
|
||||
using AxisFn = Status (*)(void*, int32_t, int32_t, float, float, float);
|
||||
|
||||
void* context = nullptr;
|
||||
SupportedKeyFn supported_key = nullptr;
|
||||
SupportedMotionFn supported_motion = nullptr;
|
||||
ConnectFn connect = nullptr;
|
||||
DisconnectFn disconnect = nullptr;
|
||||
ButtonFn button = nullptr;
|
||||
AxisFn axis = nullptr;
|
||||
|
||||
bool valid() const {
|
||||
return supported_key != nullptr && supported_motion != nullptr &&
|
||||
connect != nullptr && disconnect != nullptr && button != nullptr &&
|
||||
axis != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
struct RobloxInputSink {
|
||||
using MouseMoveFn = Status (*)(void* context, float x, float y, float delta_x,
|
||||
float delta_y);
|
||||
@@ -60,6 +91,7 @@ struct RobloxInputSink {
|
||||
TouchFn touch = nullptr;
|
||||
KeyFn key = nullptr;
|
||||
RobloxTextSink text;
|
||||
RobloxGamepadSink gamepad{};
|
||||
};
|
||||
|
||||
enum class RobloxInputEventKind {
|
||||
@@ -72,6 +104,9 @@ enum class RobloxInputEventKind {
|
||||
kText,
|
||||
kFocus,
|
||||
kViewport,
|
||||
kGamepadConnection,
|
||||
kGamepadButton,
|
||||
kGamepadAxis,
|
||||
};
|
||||
|
||||
enum class RobloxInputDispatchState {
|
||||
@@ -103,10 +138,15 @@ struct RobloxInputSnapshot {
|
||||
uint64_t keyboard_events = 0;
|
||||
uint64_t ignored_text_events = 0;
|
||||
uint64_t text_events = 0;
|
||||
// Zero after the host editor finishes, even if Roblox sends no hide callback.
|
||||
uint64_t text_focus_generation = 0;
|
||||
uint64_t native_errors = 0;
|
||||
uint32_t active_mouse_buttons = 0;
|
||||
uint32_t active_touches = 0;
|
||||
uint32_t active_keys = 0;
|
||||
uint64_t gamepad_events = 0;
|
||||
uint32_t detected_gamepads = 0;
|
||||
uint32_t connected_gamepads = 0;
|
||||
};
|
||||
|
||||
// Stateful translation boundary from SDL platform events to the exact
|
||||
@@ -146,6 +186,16 @@ class RobloxInputRouter final {
|
||||
AndroidKeyMapping android;
|
||||
};
|
||||
|
||||
struct ActiveGamepad {
|
||||
int64_t instance_id = 0;
|
||||
int32_t device_id = 0;
|
||||
platform::GamepadDescriptor descriptor;
|
||||
bool connected = false;
|
||||
uint32_t pressed_buttons = 0;
|
||||
std::array<int16_t, 6> raw_axes{};
|
||||
std::array<float, 6> sent_axes{};
|
||||
};
|
||||
|
||||
RobloxInputDispatchResult HandleMouseMotionLocked(
|
||||
const platform::MouseMotionEvent& event);
|
||||
RobloxInputDispatchResult HandleMouseButtonLocked(
|
||||
@@ -155,6 +205,20 @@ class RobloxInputRouter final {
|
||||
RobloxInputDispatchResult HandleTouchLocked(
|
||||
const platform::TouchEvent& event);
|
||||
RobloxInputDispatchResult HandleKeyLocked(const platform::KeyEvent& event);
|
||||
RobloxInputDispatchResult HandleGamepadConnectionLocked(
|
||||
const platform::GamepadConnectionEvent& event);
|
||||
RobloxInputDispatchResult HandleGamepadButtonLocked(
|
||||
const platform::GamepadButtonEvent& event);
|
||||
RobloxInputDispatchResult HandleGamepadAxisLocked(
|
||||
const platform::GamepadAxisEvent& event);
|
||||
std::vector<ActiveGamepad>::iterator FindGamepadLocked(int64_t instance_id);
|
||||
Status ConfigureGamepadLocked(const ActiveGamepad& gamepad, bool available);
|
||||
Status ConnectGamepadLocked(ActiveGamepad& gamepad);
|
||||
Status DispatchGamepadAxesLocked(ActiveGamepad& gamepad, uint8_t axis,
|
||||
bool* dispatched);
|
||||
Status ReleaseGamepadInputsLocked(ActiveGamepad& gamepad);
|
||||
Status DisconnectGamepadLocked(ActiveGamepad& gamepad);
|
||||
Status DisconnectGamepadsLocked();
|
||||
Status ReleasePressedInputsLocked();
|
||||
int32_t AllocatePointerIdLocked() const;
|
||||
std::vector<ActiveTouch>::iterator FindTouchIteratorLocked(int64_t touch_id,
|
||||
@@ -170,6 +234,7 @@ class RobloxInputRouter final {
|
||||
std::vector<int32_t> active_mouse_buttons_;
|
||||
std::vector<ActiveTouch> active_touches_;
|
||||
std::vector<ActiveKey> active_keys_;
|
||||
std::vector<ActiveGamepad> gamepads_;
|
||||
float mouse_x_ = 0.0f;
|
||||
float mouse_y_ = 0.0f;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_PERMISSIONS_BRIDGE_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_PERMISSIONS_BRIDGE_H_
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/roblox_game_session_native_adapter.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Exact MessageBus JNI entrypoints used by the APK PermissionsProtocol.
|
||||
struct RobloxPermissionsMessageBusSymbols {
|
||||
jobject (*subscribe_request)(JNIEnv*, jobject, jstring, jstring, jobject,
|
||||
jboolean) = nullptr;
|
||||
void (*delete_connection)(JNIEnv*, jobject, jlong) = nullptr;
|
||||
void (*publish_response)(JNIEnv*, jobject, jstring, jstring, jstring, jint,
|
||||
jstring) = nullptr;
|
||||
void (*set_async_handler)(JNIEnv*, jobject, jstring, jstring,
|
||||
jobject) = nullptr;
|
||||
void (*clear_handler)(JNIEnv*, jobject, jstring, jstring) = nullptr;
|
||||
void (*call_response_handler)(JNIEnv*, jobject, jstring, jstring) = nullptr;
|
||||
|
||||
bool complete() const {
|
||||
return subscribe_request && delete_connection && publish_response &&
|
||||
set_async_handler && clear_handler && call_response_handler;
|
||||
}
|
||||
};
|
||||
|
||||
struct RobloxPermissionsMessageBusObjects {
|
||||
jobject message_bus = nullptr;
|
||||
void* context = nullptr;
|
||||
jobject (*create_raw_callback)(void*, std::shared_ptr<void>,
|
||||
void (*)(void*, JNIEnv*, jstring)) = nullptr;
|
||||
void (*clear_raw_callback)(void*, jobject) = nullptr;
|
||||
jobject (*create_async_handler)(void*, std::shared_ptr<void>,
|
||||
void (*)(void*, JNIEnv*, jstring,
|
||||
jstring)) = nullptr;
|
||||
void (*clear_async_handler)(void*, jobject) = nullptr;
|
||||
|
||||
bool complete() const {
|
||||
return message_bus && context && create_raw_callback &&
|
||||
clear_raw_callback && create_async_handler && clear_async_handler;
|
||||
}
|
||||
};
|
||||
|
||||
// Installs both legacy protocol subscriptions and correlated async handlers
|
||||
// before native bootstrap. MICROPHONE_ACCESS follows host capture
|
||||
// configuration; LOCAL_NETWORK needs no extra runtime permission, matching the
|
||||
// Android APK. No query/request opens an audio device, enables a disabled
|
||||
// microphone, or changes Roblox account eligibility. Other capabilities remain
|
||||
// unsupported.
|
||||
class RobloxPermissionsBridge final {
|
||||
public:
|
||||
RobloxPermissionsBridge(JniEnvironmentProvider environment,
|
||||
RobloxPermissionsMessageBusSymbols symbols,
|
||||
RobloxPermissionsMessageBusObjects objects,
|
||||
bool microphone_enabled);
|
||||
~RobloxPermissionsBridge();
|
||||
RobloxPermissionsBridge(const RobloxPermissionsBridge&) = delete;
|
||||
RobloxPermissionsBridge& operator=(const RobloxPermissionsBridge&) = delete;
|
||||
|
||||
Status Initialize();
|
||||
Status Shutdown();
|
||||
|
||||
private:
|
||||
struct State;
|
||||
Status ShutdownLocked();
|
||||
|
||||
const JniEnvironmentProvider environment_;
|
||||
const RobloxPermissionsMessageBusSymbols symbols_;
|
||||
const RobloxPermissionsMessageBusObjects objects_;
|
||||
const bool microphone_enabled_;
|
||||
// Serializes lifecycle only. Native dispatch never holds this mutex.
|
||||
std::mutex lifecycle_mutex_;
|
||||
std::shared_ptr<State> state_;
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_ROBLOX_PERMISSIONS_BRIDGE_H_
|
||||
@@ -2,6 +2,7 @@
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_PLATFORM_WEB_SYMBOLS_H_
|
||||
|
||||
#include "runtime/roblox_browser_service_bridge.h"
|
||||
#include "runtime/roblox_permissions_bridge.h"
|
||||
#include "runtime/roblox_web_view_bridge.h"
|
||||
|
||||
namespace mocktail {
|
||||
@@ -13,9 +14,11 @@ namespace runtime {
|
||||
struct RobloxPlatformWebSymbols {
|
||||
RobloxWebViewMessageBusSymbols web_view;
|
||||
RobloxBrowserServiceSymbols browser_service;
|
||||
RobloxPermissionsMessageBusSymbols permissions;
|
||||
|
||||
bool complete() const {
|
||||
return web_view.complete() && browser_service.complete();
|
||||
return web_view.complete() && browser_service.complete() &&
|
||||
permissions.complete();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_TEXT_DISPLAY_STATE_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_TEXT_DISPLAY_STATE_H_
|
||||
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
@@ -14,8 +15,14 @@ namespace runtime {
|
||||
struct RobloxTextOverlayViewport {
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
float density_scale = 1.0F;
|
||||
|
||||
bool valid() const { return width > 0 && height > 0; }
|
||||
bool valid() const {
|
||||
return width > 0 && height > 0 && std::isfinite(density_scale) &&
|
||||
density_scale > 0.0F;
|
||||
}
|
||||
int coordinate_width() const;
|
||||
int coordinate_height() const;
|
||||
};
|
||||
|
||||
struct RobloxTextOverlayGeometry {
|
||||
@@ -57,10 +64,13 @@ struct RobloxTextOverlayPresentation {
|
||||
uint64_t generation = 0;
|
||||
RobloxTextOverlayGeometry geometry;
|
||||
float font_size = 0.0F;
|
||||
bool multiline = false;
|
||||
int32_t x_alignment = 0;
|
||||
int32_t y_alignment = 1;
|
||||
int32_t text_color = 0;
|
||||
int32_t font = 0;
|
||||
int32_t text_input_type = 0;
|
||||
bool text_wrapped = false;
|
||||
std::string display_utf8;
|
||||
std::size_t caret_utf8_byte = 0;
|
||||
std::size_t selection_begin_utf8_byte = 0;
|
||||
|
||||
@@ -67,10 +67,13 @@ struct RobloxTextDisplayUpdate {
|
||||
int32_t area_width = 0;
|
||||
int32_t area_height = 0;
|
||||
float font_size = 0.0F;
|
||||
bool multiline = false;
|
||||
int32_t x_alignment = 0;
|
||||
int32_t y_alignment = 1;
|
||||
int32_t text_color = 0;
|
||||
int32_t font = 0;
|
||||
int32_t text_input_type = 0;
|
||||
bool text_wrapped = false;
|
||||
const char* utf8 = nullptr;
|
||||
std::size_t utf8_size = 0;
|
||||
int32_t cursor_utf16 = 0;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_TEXT_FONT_RESOLVER_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_TEXT_FONT_RESOLVER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Android's RbxKeyboard resolves the Roblox Font enum through
|
||||
// assets/android/fonts/font-mappings.json. The selected file lives below
|
||||
// assets/content/fonts while legacy SourceSansPro faces live in assets/fonts.
|
||||
struct RobloxTextFontSelection {
|
||||
std::string primary_file;
|
||||
float size_scale = 0.795F;
|
||||
bool exact_mapping = false;
|
||||
};
|
||||
|
||||
RobloxTextFontSelection ResolveRobloxTextFont(
|
||||
int32_t font, const std::string& asset_content_root);
|
||||
RobloxTextFontSelection ResolveRobloxTextFont(int32_t font);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_ROBLOX_TEXT_FONT_RESOLVER_H_
|
||||
@@ -27,6 +27,7 @@ class RobloxTextSurfaceOverlay final {
|
||||
RobloxTextSurfaceOverlay& operator=(const RobloxTextSurfaceOverlay&) = delete;
|
||||
|
||||
Status Initialize(RobloxTextOverlayViewport viewport);
|
||||
Status UpdateViewport(RobloxTextOverlayViewport viewport);
|
||||
Status Shutdown();
|
||||
RobloxTextDisplaySink sink();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "mocktail/platform/sdl_gamepad_manager.h"
|
||||
#include "mocktail/platform/text_clipboard.h"
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/roblox_input_native_adapter.h"
|
||||
@@ -37,11 +38,16 @@ class RobloxWindowInputRuntime final {
|
||||
RobloxInputSnapshot Snapshot() const;
|
||||
|
||||
private:
|
||||
static void PlatformEventCallback(void* context,
|
||||
const platform::PlatformEvent& event);
|
||||
static bool MouseLockQueryCallback(void* context, bool* locked_center);
|
||||
static void GamepadEventCallback(void* context,
|
||||
const platform::PlatformEvent& event);
|
||||
|
||||
RobloxTextSurfaceOverlay text_surface_overlay_;
|
||||
std::unique_ptr<platform::TextClipboard> text_clipboard_;
|
||||
RobloxInputRuntime runtime_;
|
||||
platform::SdlGamepadManager gamepads_;
|
||||
bool observer_registered_ = false;
|
||||
bool mouse_lock_query_registered_ = false;
|
||||
bool initialized_ = false;
|
||||
|
||||
@@ -28,6 +28,10 @@ struct WindowConfig {
|
||||
int width = 1280;
|
||||
int height = 720;
|
||||
std::string title = "Roblox";
|
||||
// GNOME desktop dimensions are logical units; keep the render surface at
|
||||
// that size unless the user explicitly requests high-density rendering.
|
||||
bool high_dpi = false;
|
||||
bool high_dpi_valid = true;
|
||||
};
|
||||
|
||||
struct InputCapabilityConfig {
|
||||
@@ -37,6 +41,7 @@ struct InputCapabilityConfig {
|
||||
};
|
||||
|
||||
struct NetworkProxyConfig {
|
||||
std::string scheme = "http";
|
||||
std::string host;
|
||||
int port = 0;
|
||||
};
|
||||
@@ -61,7 +66,8 @@ struct DiscordRpcConfig {
|
||||
};
|
||||
|
||||
std::optional<NetworkProxyConfig> ParseNetworkProxyConfig(
|
||||
std::string_view host, std::string_view port);
|
||||
std::string_view host, std::string_view port,
|
||||
std::string_view scheme = "http");
|
||||
std::string BuildNetworkProxyUrl(const NetworkProxyConfig& proxy);
|
||||
|
||||
// Immutable, supported runtime options. Advanced MOCKTAIL_PATCH_* controls
|
||||
@@ -79,6 +85,11 @@ class RuntimeConfig {
|
||||
return graphics_backend_name_;
|
||||
}
|
||||
const WindowConfig& window() const { return window_; }
|
||||
const std::string& theme_mode() const { return theme_mode_; }
|
||||
bool theme_mode_valid() const {
|
||||
return theme_mode_ == "roblox" || theme_mode_ == "system" ||
|
||||
theme_mode_ == "light" || theme_mode_ == "dark";
|
||||
}
|
||||
const InputCapabilityConfig& input_capabilities() const {
|
||||
return input_capabilities_;
|
||||
}
|
||||
@@ -97,9 +108,24 @@ class RuntimeConfig {
|
||||
return audio_output_device_;
|
||||
}
|
||||
bool audio_output_device_valid() const { return audio_output_device_valid_; }
|
||||
const std::string& audio_input_device() const { return audio_input_device_; }
|
||||
bool audio_input_device_valid() const { return audio_input_device_valid_; }
|
||||
bool microphone_enabled() const { return audio_input_device_ != "disabled"; }
|
||||
const std::optional<NetworkProxyConfig>& network_proxy() const {
|
||||
return network_proxy_;
|
||||
}
|
||||
const std::optional<std::filesystem::path>& ca_bundle() const {
|
||||
return ca_bundle_;
|
||||
}
|
||||
bool ca_bundle_valid() const { return ca_bundle_valid_; }
|
||||
bool use_system_proxy() const { return use_system_proxy_; }
|
||||
bool fleasion_enabled() const { return fleasion_enabled_; }
|
||||
bool fleasion_valid() const { return fleasion_valid_; }
|
||||
const std::string& fleasion_proxy_mode() const { return fleasion_proxy_mode_; }
|
||||
int fleasion_proxy_port() const { return fleasion_proxy_port_; }
|
||||
const std::optional<std::filesystem::path>& fleasion_ca_certificate() const {
|
||||
return fleasion_ca_certificate_;
|
||||
}
|
||||
const DiscordRpcConfig& discord_rpc() const { return discord_rpc_; }
|
||||
bool discord_rpc_valid() const { return discord_rpc_valid_; }
|
||||
|
||||
@@ -120,6 +146,7 @@ class RuntimeConfig {
|
||||
GraphicsBackend graphics_backend_ = GraphicsBackend::kVulkan;
|
||||
std::string graphics_backend_name_ = "direct-vulkan";
|
||||
WindowConfig window_;
|
||||
std::string theme_mode_ = "roblox";
|
||||
InputCapabilityConfig input_capabilities_;
|
||||
DeviceProfile device_profile_ = *FindDeviceProfile(kDefaultDeviceProfileName);
|
||||
bool device_profile_valid_ = true;
|
||||
@@ -129,7 +156,17 @@ class RuntimeConfig {
|
||||
PerformancePolicy performance_;
|
||||
std::string audio_output_device_ = "default";
|
||||
bool audio_output_device_valid_ = true;
|
||||
std::string audio_input_device_ = "default";
|
||||
bool audio_input_device_valid_ = true;
|
||||
bool use_system_proxy_ = false;
|
||||
bool fleasion_enabled_ = false;
|
||||
bool fleasion_valid_ = true;
|
||||
std::string fleasion_proxy_mode_ = "env";
|
||||
int fleasion_proxy_port_ = 58443;
|
||||
std::optional<std::filesystem::path> fleasion_ca_certificate_;
|
||||
std::optional<NetworkProxyConfig> network_proxy_;
|
||||
std::optional<std::filesystem::path> ca_bundle_;
|
||||
bool ca_bundle_valid_ = true;
|
||||
DiscordRpcConfig discord_rpc_;
|
||||
bool discord_rpc_valid_ = true;
|
||||
std::vector<std::string> unsafe_detached_thread_overrides_;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_SYSTEM_PROXY_H_
|
||||
#define MOCKTAIL_RUNTIME_SYSTEM_PROXY_H_
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "runtime/runtime_config.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
struct SystemProxyResult {
|
||||
std::optional<NetworkProxyConfig> proxy;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const { return error.empty(); }
|
||||
};
|
||||
|
||||
// Selects the first proxy returned by the host resolver. An empty `proxy`
|
||||
// means that the resolver selected a direct connection.
|
||||
SystemProxyResult SelectSystemProxy(
|
||||
const std::vector<std::string>& candidates);
|
||||
|
||||
// Resolves the host's proxy policy for Roblox HTTPS traffic. GIO delegates to
|
||||
// the desktop/environment proxy resolver, including PAC when it is available.
|
||||
// HTTP/HTTPS resolver results and SOCKS5 proxy endpoints are supported.
|
||||
SystemProxyResult ResolveSystemProxy();
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_SYSTEM_PROXY_H_
|
||||
@@ -0,0 +1,32 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_TEXTURE_MEMORY_POLICY_H_
|
||||
#define MOCKTAIL_RUNTIME_TEXTURE_MEMORY_POLICY_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Roblox sizes its texture budget for the Android guest, so a PC host still
|
||||
// gets caps.videoMemory = 64 MiB. These publish a host-sized budget through
|
||||
// Roblox's own FIntRenderForceVideoMemorySize override.
|
||||
|
||||
// Total usable host memory in bytes, or 0 when it cannot be read.
|
||||
std::uint64_t DetectHostMemoryBytes();
|
||||
|
||||
// 0 leaves Roblox's budget alone; otherwise an eighth of host memory, clamped
|
||||
// to a PC-class range.
|
||||
std::uint64_t CalculateTextureMemoryBudgetBytes(std::uint64_t host_memory_bytes);
|
||||
|
||||
// A 0 budget, or a key already present, is preserved: an explicit fflags.json
|
||||
// or environment override wins.
|
||||
bool MergeTextureMemoryClientSettingsOverrides(std::uint64_t budget_bytes,
|
||||
std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_TEXTURE_MEMORY_POLICY_H_
|
||||
@@ -53,6 +53,7 @@ enum class WebViewHelperEventType : unsigned char {
|
||||
kExecuteRoblox = 1,
|
||||
kRobloxWkHybrid = 2,
|
||||
kReady = 3,
|
||||
kRobloxCookie = 4,
|
||||
};
|
||||
|
||||
struct WebViewHelperEvent {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
#ifndef MOCKTAIL_UPDATE_APK_PROVIDER_H_
|
||||
#define MOCKTAIL_UPDATE_APK_PROVIDER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail::update {
|
||||
|
||||
struct ProviderVersion {
|
||||
std::string version_name;
|
||||
std::uint64_t version_code = 0;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const { return error.empty(); }
|
||||
};
|
||||
|
||||
struct ProviderDownloadResult {
|
||||
std::vector<std::filesystem::path> archives;
|
||||
// Provider that produced the archives, so a stored payload can be traced
|
||||
// back to where it came from.
|
||||
std::string source;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const { return error.empty(); }
|
||||
};
|
||||
|
||||
// A source of Roblox APK archives. Providers are interchangeable: whichever
|
||||
// one answers first wins, and every archive still passes the same signature,
|
||||
// Build-ID, and ABI verification afterwards.
|
||||
class ApkProvider {
|
||||
public:
|
||||
virtual ~ApkProvider() = default;
|
||||
|
||||
// Short stable identifier, recorded in payload receipts and error messages.
|
||||
virtual std::string_view name() const = 0;
|
||||
|
||||
// Latest published version. A provider that only serves pinned versions
|
||||
// reports an error here and stays usable through DownloadExact.
|
||||
virtual ProviderVersion CheckLatest() const = 0;
|
||||
|
||||
virtual ProviderDownloadResult DownloadExact(
|
||||
std::string_view version, const std::filesystem::path& output_directory,
|
||||
int progress_fd) const = 0;
|
||||
};
|
||||
|
||||
// Tries every provider in order. One unreachable provider is the most common
|
||||
// first-run failure, and on its own it must not fail the whole update.
|
||||
class ProviderChain final : public ApkProvider {
|
||||
public:
|
||||
void Add(std::unique_ptr<ApkProvider> provider);
|
||||
bool empty() const { return providers_.empty(); }
|
||||
|
||||
std::string_view name() const override { return "provider-chain"; }
|
||||
ProviderVersion CheckLatest() const override;
|
||||
ProviderDownloadResult DownloadExact(
|
||||
std::string_view version, const std::filesystem::path& output_directory,
|
||||
int progress_fd) const override;
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<ApkProvider>> providers_;
|
||||
};
|
||||
|
||||
} // namespace mocktail::update
|
||||
|
||||
#endif // MOCKTAIL_UPDATE_APK_PROVIDER_H_
|
||||
@@ -1,42 +1,30 @@
|
||||
#ifndef MOCKTAIL_UPDATE_APKPURE_PROVIDER_H_
|
||||
#define MOCKTAIL_UPDATE_APKPURE_PROVIDER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "update/apk_provider.h"
|
||||
|
||||
namespace mocktail::update {
|
||||
|
||||
struct ProviderVersion {
|
||||
std::string version_name;
|
||||
std::uint64_t version_code = 0;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const { return error.empty(); }
|
||||
};
|
||||
|
||||
struct ProviderDownloadResult {
|
||||
std::vector<std::filesystem::path> archives;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const { return error.empty(); }
|
||||
};
|
||||
|
||||
ProviderVersion ParseApkPureLatestMetadata(std::string_view metadata);
|
||||
|
||||
std::vector<std::string> ParseApkPureExactDownloadUrls(
|
||||
std::string_view metadata, std::string_view version,
|
||||
std::string* error = nullptr);
|
||||
|
||||
class ApkPureProvider final {
|
||||
class ApkPureProvider final : public ApkProvider {
|
||||
public:
|
||||
ProviderVersion CheckLatest() const;
|
||||
std::string_view name() const override { return "apk-pure"; }
|
||||
|
||||
ProviderVersion CheckLatest() const override;
|
||||
|
||||
ProviderDownloadResult DownloadExact(
|
||||
std::string_view version, const std::filesystem::path& output_directory,
|
||||
int progress_fd = -1) const;
|
||||
int progress_fd = -1) const override;
|
||||
};
|
||||
|
||||
} // namespace mocktail::update
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail::update {
|
||||
|
||||
struct HostAbiDerivationOptions {
|
||||
std::filesystem::path reference_library;
|
||||
std::filesystem::path reference_profile;
|
||||
std::vector<std::filesystem::path> reference_compatibility_manifests;
|
||||
std::filesystem::path candidate_payload_directory;
|
||||
std::filesystem::path output_directory;
|
||||
};
|
||||
@@ -23,6 +25,22 @@ struct HostAbiDerivationResult {
|
||||
}
|
||||
};
|
||||
|
||||
// Identity an exact HostAbi sidecar was derived for. It only validates
|
||||
// against the one library whose bytes it describes, so the reference payload
|
||||
// has to be selected by this Build ID rather than by catalog order.
|
||||
struct HostAbiSidecarIdentity {
|
||||
std::string elf_build_id;
|
||||
std::string payload_id;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const {
|
||||
return error.empty() && !elf_build_id.empty() && !payload_id.empty();
|
||||
}
|
||||
};
|
||||
|
||||
HostAbiSidecarIdentity ReadHostAbiSidecarIdentity(
|
||||
const std::filesystem::path& sidecar);
|
||||
|
||||
// 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
|
||||
|
||||
@@ -15,7 +15,18 @@ struct HttpTransferRequest {
|
||||
std::vector<std::string> allowed_hosts;
|
||||
std::size_t maximum_bytes = 0;
|
||||
long connect_timeout_ms = 10000;
|
||||
// Hard ceiling for one attempt; zero means none. A several hundred MiB
|
||||
// archive over a slow link is not a failed transfer, so the ceiling is the
|
||||
// wrong tool for it - a transfer that actually stops is caught by the
|
||||
// low-speed guard below, in a minute rather than in a quarter of an hour.
|
||||
long transfer_timeout_ms = 120000;
|
||||
long low_speed_bytes_per_second = 1024;
|
||||
long low_speed_seconds = 60;
|
||||
// Transient failures - a provider 503, a dropped connection, a stall - are
|
||||
// retried. A file transfer resumes from the bytes already on disk instead
|
||||
// of pulling the whole archive again.
|
||||
int maximum_attempts = 3;
|
||||
long retry_delay_ms = 2000;
|
||||
int maximum_redirects = 5;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,9 +5,32 @@
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
struct evp_md_ctx_st;
|
||||
|
||||
namespace mocktail::update {
|
||||
|
||||
class FileDigest final {
|
||||
public:
|
||||
FileDigest();
|
||||
~FileDigest();
|
||||
FileDigest(const FileDigest&) = delete;
|
||||
FileDigest& operator=(const FileDigest&) = delete;
|
||||
|
||||
bool valid() const noexcept { return context_ != nullptr; }
|
||||
bool Update(const void* data, std::size_t size);
|
||||
bool Update(std::string_view bytes);
|
||||
std::string FinalHex(std::size_t max_chars = 0);
|
||||
|
||||
private:
|
||||
struct evp_md_ctx_st* context_ = nullptr;
|
||||
};
|
||||
|
||||
std::string HashText(std::string_view text, std::string* error = nullptr);
|
||||
std::string HashText(std::string_view text, std::size_t max_hex_chars,
|
||||
std::string* error = nullptr);
|
||||
|
||||
struct PayloadMetadata {
|
||||
std::string package_name;
|
||||
std::string version_name;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail::update {
|
||||
|
||||
@@ -24,6 +25,14 @@ struct PayloadStoreResult {
|
||||
}
|
||||
};
|
||||
|
||||
struct PayloadGarbageResult {
|
||||
std::vector<std::string> removed;
|
||||
std::uintmax_t freed_bytes = 0;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const { return error.empty(); }
|
||||
};
|
||||
|
||||
class PayloadStore final {
|
||||
public:
|
||||
PayloadStore(std::filesystem::path root,
|
||||
@@ -41,6 +50,10 @@ class PayloadStore final {
|
||||
PayloadStoreResult InspectCurrent() const;
|
||||
PayloadStoreResult VerifyCurrent();
|
||||
PayloadStoreResult Rollback();
|
||||
// Deletes payload trees nothing points at any more. The active payload, the
|
||||
// rollback target, and every ID in `keep` survive; a payload tree is around
|
||||
// 600 MiB, so without this the store grows by one release forever.
|
||||
PayloadGarbageResult CollectGarbage(const std::vector<std::string>& keep);
|
||||
std::string StatusJson(std::string* error = nullptr) const;
|
||||
|
||||
private:
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#ifndef MOCKTAIL_UPDATE_UNSAFE_LATEST_RUNNER_H_
|
||||
#define MOCKTAIL_UPDATE_UNSAFE_LATEST_RUNNER_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail::update {
|
||||
|
||||
// Launches a downloaded latest payload once without promoting it to the active
|
||||
// store. A non-empty HostAbi profile puts the child in explicit candidate mode;
|
||||
// normal startup approval is intentionally not created or modified.
|
||||
struct UnsafeLatestRunOptions {
|
||||
std::filesystem::path runtime_binary;
|
||||
std::filesystem::path payload_directory;
|
||||
std::filesystem::path compatibility_manifest;
|
||||
std::filesystem::path host_abi_profile;
|
||||
|
||||
// Tests may supply a deterministic environment. Production callers leave
|
||||
// this empty to inherit the updater process environment.
|
||||
std::vector<std::string> inherited_environment;
|
||||
};
|
||||
|
||||
struct UnsafeLatestRunResult {
|
||||
int exit_code = -1;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const { return error.empty() && exit_code == 0; }
|
||||
};
|
||||
|
||||
UnsafeLatestRunResult RunUnsafeLatestCandidate(
|
||||
const UnsafeLatestRunOptions& options);
|
||||
|
||||
} // namespace mocktail::update
|
||||
|
||||
#endif // MOCKTAIL_UPDATE_UNSAFE_LATEST_RUNNER_H_
|
||||
@@ -2,9 +2,14 @@
|
||||
#define MOCKTAIL_UPDATE_UPDATE_COORDINATOR_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "update/apk_bundle.h"
|
||||
#include "update/apk_provider.h"
|
||||
#include "update/compatibility_catalog.h"
|
||||
#include "update/payload_store.h"
|
||||
#include "update/readiness_canary.h"
|
||||
|
||||
namespace mocktail::update {
|
||||
@@ -16,6 +21,9 @@ struct UpdatePaths {
|
||||
std::filesystem::path state_root;
|
||||
std::filesystem::path compatibility_manifest;
|
||||
std::filesystem::path signing_trust_manifest;
|
||||
// One exact HostAbi sidecar, or a directory of them. A directory lets a new
|
||||
// supported profile ship its own reference instead of inheriting whichever
|
||||
// build the single file happens to describe.
|
||||
std::filesystem::path host_abi_reference_profile;
|
||||
std::filesystem::path runtime_binary;
|
||||
};
|
||||
@@ -24,6 +32,9 @@ struct UpdateRequest {
|
||||
bool startup_preflight = false;
|
||||
bool check_latest = true;
|
||||
bool run_canary = true;
|
||||
// Explicit manual escape hatch. It launches the provider latest once with a
|
||||
// derived candidate profile and never promotes it into current.json.
|
||||
bool force_run_latest = false;
|
||||
CanaryGraphicsBackend canary_graphics_backend =
|
||||
CanaryGraphicsBackend::kDirectVulkan;
|
||||
int progress_fd = -1;
|
||||
@@ -31,6 +42,10 @@ struct UpdateRequest {
|
||||
|
||||
struct UpdateResult {
|
||||
bool changed = false;
|
||||
// The provider publishes a newer Roblox than the payload that stayed
|
||||
// active. Launching still works, but the Roblox servers reject an outdated
|
||||
// client, so this is a user-visible outcome rather than a quiet success.
|
||||
bool stale = false;
|
||||
std::string payload_id;
|
||||
std::string message;
|
||||
std::vector<std::string> warnings;
|
||||
@@ -39,6 +54,35 @@ struct UpdateResult {
|
||||
explicit operator bool() const { return error.empty(); }
|
||||
};
|
||||
|
||||
// One payload worth trying, best first. The installed payload is deliberately
|
||||
// not a plan entry: it is what remains when every plan fails, so a rejected
|
||||
// candidate cannot pin the user to a version the catalog has moved past.
|
||||
struct UpdateCandidatePlan {
|
||||
ExpectedPayloadIdentity identity;
|
||||
bool exact_supported = false;
|
||||
// The store already holds an approved profile for this exact version, so it
|
||||
// only needs its canaries re-run rather than another few hundred MiB.
|
||||
bool reuse_installed = false;
|
||||
std::string origin;
|
||||
};
|
||||
|
||||
// Newest supported profile whose own bytes an available sidecar describes,
|
||||
// with `sidecar` set to that file. `location` is one sidecar or a directory
|
||||
// of them. A library picked any other way cannot pass the derivation's own
|
||||
// reference check.
|
||||
std::optional<SupportedPayloadProfile> ResolveReferenceProfile(
|
||||
const std::filesystem::path& location,
|
||||
const std::vector<SupportedPayloadProfile>& profiles,
|
||||
std::filesystem::path* sidecar);
|
||||
|
||||
// `latest` is null when the provider could not be consulted. `current` is the
|
||||
// runnable payload (empty when there is none) and `installed` is whatever the
|
||||
// activation manifest points at, runnable or not.
|
||||
std::vector<UpdateCandidatePlan> PlanUpdateCandidates(
|
||||
const std::vector<SupportedPayloadProfile>& profiles,
|
||||
const SupportedPayloadProfile& preferred, const ProviderVersion* latest,
|
||||
const PayloadStoreResult& current, const PayloadStoreResult& installed);
|
||||
|
||||
UpdateResult RunUpdate(const UpdatePaths& paths, const UpdateRequest& request);
|
||||
|
||||
} // namespace mocktail::update
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Local git hooks. Install once with: lefthook install
|
||||
# Mirrors the "Repository contracts" job in .github/workflows/ci.yml.
|
||||
|
||||
pre-commit:
|
||||
parallel: true
|
||||
commands:
|
||||
shell-syntax:
|
||||
glob: "{scripts,tests,packaging}/**/*.sh"
|
||||
run: for f in {staged_files}; do bash -n "$f" || exit 1; done
|
||||
json:
|
||||
glob: "{config,packaging}/**/*.json"
|
||||
run: for f in {staged_files}; do jq empty "$f" || exit 1; done
|
||||
desktop-entry:
|
||||
glob: "*.desktop"
|
||||
run: desktop-file-validate {staged_files}
|
||||
|
||||
pre-push:
|
||||
commands:
|
||||
packaging-contracts:
|
||||
run: >-
|
||||
bash tests/managed_payload_build_contract_test.sh . &&
|
||||
bash tests/flatpak_packaging_test.sh . &&
|
||||
bash tests/native_packaging_test.sh .
|
||||
+24
-3
@@ -1,8 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/sh
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -euo pipefail
|
||||
set -eu
|
||||
|
||||
APPDIR="${APPDIR:-$(CDPATH= cd -P "$(dirname -- "$0")" && pwd)}"
|
||||
|
||||
InitializeAnyLinuxPaths() {
|
||||
# quick-sharun preserves our custom AppRun, so its generated AppRun never
|
||||
# runs these relocation hooks. WebKit and p11-kit use the patched paths
|
||||
# even when WEBKIT_EXEC_PATH and SSL_CERT_FILE are set.
|
||||
for anylinux_hook in 01-check-ca-certs.hook 01-path-mapping-hardcoded.hook; do
|
||||
if [ -f "${APPDIR}/bin/${anylinux_hook}" ]; then
|
||||
. "${APPDIR}/bin/${anylinux_hook}"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
if [ -n "${SHARUN_DIR:-}" ] && [ -x "${APPDIR}/bin/bash" ]; then
|
||||
export PATH="${APPDIR}/bin:${PATH}"
|
||||
export MOCKTAIL_ANYLINUX_BIN_DIR="${APPDIR}/bin"
|
||||
export MOCKTAIL_BUNDLE_ROOT="${APPDIR}/usr/share/mocktail-bundle"
|
||||
InitializeAnyLinuxPaths
|
||||
exec "${APPDIR}/bin/bash" \
|
||||
"${MOCKTAIL_BUNDLE_ROOT}/mocktail/scripts/portable_launcher.sh" "$@"
|
||||
fi
|
||||
|
||||
APPDIR="${APPDIR:-$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)}"
|
||||
exec "${APPDIR}/usr/share/mocktail-bundle/run.sh" "$@"
|
||||
@@ -0,0 +1,12 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
# pkgforge-dev/archlinux-pkgs-debloated, continuous release, 2026-09-15.
|
||||
# These assets are rolling. Refresh names/hashes together; do not bypass a
|
||||
# checksum mismatch. Only the disposable AppImage build container installs them.
|
||||
eb87771dabc819b318e0ddf94e7ea175a662c1b6fbe748661126c520a0fd4627 ffmpeg-mini-x86_64.pkg.tar.zst
|
||||
e62d30173cc87d3a717a218610c9291b9f647b29b99ee86a0a591226c39a0728 gdk-pixbuf2-mini-x86_64.pkg.tar.zst
|
||||
ca18bc00a343650cde3d674135fa482b54faa639017f82792906db8c68ca9fb6 gtk4-mini-x86_64.pkg.tar.zst
|
||||
8500f31bb0f793d159d58f2e428594baba63f0c1a0276394c5a3f1ac1da422ef icu-mini-x86_64.pkg.tar.zst
|
||||
3d51ee49d0e49a065971703b3f61b610bd1c307cd5165ec16846d75f3efe4a08 librsvg-mini-x86_64.pkg.tar.zst
|
||||
fd52222372202ac7d37ff3f6ef0a47b0decee224aab37ac2b24086fb623b8b86 libxml2-mini-x86_64.pkg.tar.zst
|
||||
da5598b3f8e3a33ba0811c225752466a1abd3821053a19b480410937454feb04 mesa-mini-x86_64.pkg.tar.zst
|
||||
ca23eed050345d8c3f45a21610f91aecc273569b09d8d6eed574014e2e560d89 opus-mini-x86_64.pkg.tar.zst
|
||||
@@ -0,0 +1,21 @@
|
||||
# Paths are expanded by sharun after the AppImage is mounted. Keep the
|
||||
# Android ABI adapters separate from the host-ABI toolkit libraries.
|
||||
USE_HOST_XDG_CACHE_HOME=1
|
||||
GSETTINGS_BACKEND=keyfile
|
||||
SHARUN_ALLOW_SYS_VKICD=1
|
||||
MOCKTAIL_PORTABLE_MODE=standalone
|
||||
MOCKTAIL_RUNTIME_LIBRARY_DIR=${SHARUN_DIR}/lib/mocktail
|
||||
MOCKTAIL_BIN=${SHARUN_DIR}/bin/mocktail
|
||||
MOCKTAIL_UPDATE_CANARY_BIN=${SHARUN_DIR}/bin/mocktail
|
||||
MOCKTAIL_UPDATE_HELPER=${SHARUN_DIR}/bin/mocktail_updater
|
||||
MOCKTAIL_WEBVIEW_HELPER=${SHARUN_DIR}/bin/mocktail_webview_helper
|
||||
MOCKTAIL_FAILURE_DIALOG_HELPER=${SHARUN_DIR}/bin/mocktail_failure_dialog
|
||||
MOCKTAIL_UPDATE_PROGRESS_HELPER=${SHARUN_DIR}/bin/mocktail_failure_dialog
|
||||
MOCKTAIL_FREEBSD_SOCKET_HELPER=${SHARUN_DIR}/share/mocktail/helpers/mocktail_freebsd_socket_helper
|
||||
# sharun reloads .env for every child. Publish a default separately so it
|
||||
# cannot overwrite a candidate-specific manifest supplied by the updater.
|
||||
MOCKTAIL_PACKAGED_COMPATIBILITY_MANIFEST=${SHARUN_DIR}/share/mocktail/metadata/roblox_compatibility.json
|
||||
MOCKTAIL_UPDATE_COMPATIBILITY_PATH=${SHARUN_DIR}/share/mocktail/metadata/roblox_compatibility.json
|
||||
MOCKTAIL_UPDATE_SIGNING_TRUST_PATH=${SHARUN_DIR}/share/mocktail/metadata/roblox_signing_certificates.json
|
||||
MOCKTAIL_UPDATE_HOST_ABI_REFERENCE=${SHARUN_DIR}/share/mocktail/metadata/roblox_host_abi_reference.json
|
||||
MOCKTAIL_BOOTSTRAP_SOURCES_PATH=${SHARUN_DIR}/share/mocktail/metadata/roblox_bootstrap_sources.json
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
pkgname=mocktail
|
||||
pkgver=1.0.3
|
||||
pkgrel=1
|
||||
pkgrel=2
|
||||
pkgdesc='Android x86-64 Roblox compatibility runtime for Linux'
|
||||
arch=('x86_64')
|
||||
url='https://github.com/komaruworld/mocktail'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pkgbase = mocktail-bin
|
||||
pkgdesc = Android x86-64 Roblox compatibility runtime for Linux (prebuilt)
|
||||
pkgver = 1.0.2
|
||||
pkgver = 1.0.4
|
||||
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-1.0.2-1-x86_64.pkg.tar.zst
|
||||
noextract = mocktail-1.0.4-x86_64.pkg.tar.zst
|
||||
options = !debug
|
||||
options = !strip
|
||||
source = mocktail-1.0.2-1-x86_64.pkg.tar.zst::https://github.com/komaruworld/mocktail/releases/download/1.0.2/mocktail-1.0.2-1-x86_64.pkg.tar.zst
|
||||
sha256sums = d84673606728150aec96c950051efa87598314d59f800e2a7d8731d74cd617d9
|
||||
source = mocktail-1.0.4-x86_64.pkg.tar.zst::https://github.com/komaruworld/mocktail/releases/download/1.0.4/mocktail-1.0.4-x86_64.pkg.tar.zst
|
||||
sha256sums = d5cc7216e88fd2889369e8eb7fba4276b2746c2b084dce566aaaa7505bc2ffb7
|
||||
|
||||
pkgname = mocktail-bin
|
||||
@@ -1,7 +1,7 @@
|
||||
# Maintainer: komaruworld <rtysus228 at gmail dot com>
|
||||
|
||||
pkgname=mocktail-bin
|
||||
pkgver=1.0.2
|
||||
pkgver=1.0.4
|
||||
pkgrel=1
|
||||
pkgdesc='Android x86-64 Roblox compatibility runtime for Linux (prebuilt)'
|
||||
arch=('x86_64')
|
||||
@@ -35,10 +35,11 @@ optdepends=('gamemode: Feral GameMode integration')
|
||||
provides=('mocktail')
|
||||
conflicts=('mocktail' 'mocktail-git')
|
||||
options=('!debug' '!strip')
|
||||
_archive="mocktail-${pkgver}-${pkgrel}-${CARCH}.pkg.tar.zst"
|
||||
# Release asset names do not include the AUR package release.
|
||||
_archive="mocktail-${pkgver}-${CARCH}.pkg.tar.zst"
|
||||
source=("${_archive}::https://github.com/komaruworld/mocktail/releases/download/${pkgver}/${_archive}")
|
||||
noextract=("${_archive}")
|
||||
sha256sums=('d84673606728150aec96c950051efa87598314d59f800e2a7d8731d74cd617d9')
|
||||
sha256sums=('d5cc7216e88fd2889369e8eb7fba4276b2746c2b084dce566aaaa7505bc2ffb7')
|
||||
|
||||
package() {
|
||||
bsdtar -xf "${srcdir}/${_archive}" -C "${pkgdir}" usr
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pkgbase = mocktail
|
||||
pkgdesc = Android x86-64 Roblox compatibility runtime for Linux
|
||||
pkgver = 1.0.3
|
||||
pkgver = 1.0.4
|
||||
pkgrel = 1
|
||||
url = https://github.com/komaruworld/mocktail
|
||||
arch = x86_64
|
||||
@@ -38,7 +38,8 @@ pkgbase = mocktail
|
||||
conflicts = mocktail-bin
|
||||
conflicts = mocktail-git
|
||||
options = !debug
|
||||
source = mocktail::git+https://github.com/komaruworld/mocktail.git#tag=1.0.3
|
||||
options = !lto
|
||||
source = mocktail::git+https://github.com/komaruworld/mocktail.git#tag=1.0.4
|
||||
sha256sums = SKIP
|
||||
|
||||
pkgname = mocktail
|
||||
@@ -1,7 +1,7 @@
|
||||
# Maintainer: komaruworld <rtysus228 at gmail dot com>
|
||||
|
||||
pkgname=mocktail
|
||||
pkgver=1.0.3
|
||||
pkgver=1.0.4
|
||||
pkgrel=1
|
||||
pkgdesc='Android x86-64 Roblox compatibility runtime for Linux'
|
||||
arch=('x86_64')
|
||||
@@ -42,7 +42,7 @@ makedepends=(
|
||||
)
|
||||
optdepends=('gamemode: Feral GameMode integration')
|
||||
conflicts=('mocktail-bin' 'mocktail-git')
|
||||
options=('!debug')
|
||||
options=('!debug' '!lto')
|
||||
source=(
|
||||
"mocktail::git+https://github.com/komaruworld/mocktail.git#tag=${pkgver}"
|
||||
)
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"app-id": "space.bigrat.mocktail",
|
||||
"app-id": "io.github.CoderDayton.nightcap",
|
||||
"runtime": "org.gnome.Platform",
|
||||
"runtime-version": "50",
|
||||
"sdk": "org.gnome.Sdk",
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- Modified by vii from komaruworld/mocktail. See the top-level README "About this fork". -->
|
||||
# FreeBSD Linuxulator
|
||||
|
||||
FreeBSD Linuxulator support is experimental. I tested it on FreeBSD
|
||||
@@ -15,7 +16,7 @@ tested source revision is `aadd58dddcbc78f4d5594827b46b5633552b15ce`. Run
|
||||
the following commands as root.
|
||||
|
||||
```sh
|
||||
fetch -o /root/linuxulator.patch https://raw.githubusercontent.com/komaruworld/mocktail/main/packaging/freebsd/linuxulator.patch
|
||||
fetch -o /root/linuxulator.patch https://raw.githubusercontent.com/CoderDayton/nightcap/main/packaging/freebsd/linuxulator.patch
|
||||
cd /usr/src
|
||||
git apply /root/linuxulator.patch
|
||||
make -j2 kernel-toolchain
|
||||
@@ -40,11 +41,10 @@ After reboot, verify the result from the Fedora userspace. It must print
|
||||
chroot /compat/linux stat -f -c %t /proc
|
||||
```
|
||||
|
||||
## Run Mocktail
|
||||
## Run Nightcap
|
||||
|
||||
You can simply run the Mocktail AppImage from the
|
||||
[latest release](https://github.com/komaruworld/mocktail/releases/latest) or
|
||||
[nightly build](https://github.com/komaruworld/mocktail/releases/tag/continuous)
|
||||
You can simply run the Nightcap AppImage from the
|
||||
[latest release](https://github.com/CoderDayton/nightcap/releases/latest)
|
||||
inside the Fedora userspace.
|
||||
|
||||
## Audio
|
||||
@@ -72,11 +72,11 @@ ctl.!default {
|
||||
EOF
|
||||
```
|
||||
|
||||
Launch Mocktail with the ALSA audio driver.
|
||||
Launch Nightcap with the ALSA audio driver.
|
||||
|
||||
```sh
|
||||
SDL_AUDIO_DRIVER=alsa ./Mocktail-x86_64.AppImage
|
||||
SDL_AUDIO_DRIVER=alsa ./Nightcap-x86_64.AppImage
|
||||
```
|
||||
|
||||
Mocktail cannot override this check because Roblox reads `/proc` directly. The
|
||||
Nightcap cannot override this check because Roblox reads `/proc` directly. The
|
||||
Linuxulator patch fixes the value before Roblox sees it.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
Loaded 100 of 346 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user