Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
169e65bf60 | ||
|
|
69476f0f47 | ||
|
|
79d0cdfb40 | ||
|
|
273209b9ea | ||
|
|
bbfd603e17 | ||
|
|
1ba2941e7f | ||
|
|
68c90ce428 | ||
|
|
068f07e125 | ||
|
|
a469689bbb | ||
|
|
51836a980d | ||
|
|
ad546a7781 | ||
|
|
3c45450dfc | ||
|
|
185c11b57f | ||
|
|
2c3d7b485e | ||
|
|
6f8344a28b | ||
|
|
b60c43e8f0 | ||
|
|
b5e4273277 | ||
|
|
d2858fedc8 | ||
|
|
326a0659ea | ||
|
|
dd45467056 | ||
|
|
94b1036296 | ||
|
|
4c61daad0c | ||
|
|
f5f224875a | ||
|
|
90d4891a4e | ||
|
|
46dd99266c | ||
|
|
77fce18a95 | ||
|
|
d6f0f2c755 | ||
|
|
c6afc5af24 | ||
|
|
74e7455ee1 | ||
|
|
000d021452 | ||
|
|
2eef2c3821 | ||
|
|
4928443f90 | ||
|
|
b37497db7b | ||
|
|
6bbdc39d7e | ||
|
|
1c11336ba1 | ||
|
|
b19bd8406f | ||
|
|
cd46538eb8 | ||
|
|
eca5f395eb | ||
|
|
26767e925c | ||
|
|
01a0e40463 | ||
|
|
a11e833e06 | ||
|
|
b9bce36824 | ||
|
|
14c8a55211 | ||
|
|
5c5422fa02 | ||
|
|
9b041ec2ac | ||
|
|
5f83c4a64f | ||
|
|
a9d8e3185e | ||
|
|
67a9f4db97 | ||
|
|
f07daccc82 | ||
|
|
2eef4f966f | ||
|
|
41003efb09 | ||
|
|
29a0f03a90 | ||
|
|
5a27aa0366 | ||
|
|
c99a55dc54 | ||
|
|
35359f3575 | ||
|
|
ad6a3c3147 | ||
|
|
d982cd9506 | ||
|
|
65adb3bddc | ||
|
|
47e23df2e0 | ||
|
|
c0ff1842ee | ||
|
|
e073947b91 | ||
|
|
66de662e5b | ||
|
|
6c3631058c | ||
|
|
6829fd00ed | ||
|
|
8be3618cd5 | ||
|
|
265b476440 | ||
|
|
ff905fe5b7 | ||
|
|
02108a9bb1 | ||
|
|
7dd32335ca | ||
|
|
5ed6ed5e84 | ||
|
|
cef1b3ec4a | ||
|
|
8bde1a1c0d | ||
|
|
60e9c5c3ef | ||
|
|
d76e1b6f54 | ||
|
|
33c06c063e | ||
|
|
9e8afb233a | ||
|
|
7fda1eb239 | ||
|
|
f128e28b83 | ||
|
|
718e67f9e1 | ||
|
|
c6c354fdb1 | ||
|
|
25930d14ff | ||
|
|
65030d8df1 | ||
|
|
0be5e808c5 | ||
|
|
1b030a57eb | ||
|
|
2f5527b8b5 | ||
|
|
579cedbf1d | ||
|
|
a7b90e5919 | ||
|
|
3bba795ea3 | ||
|
|
5ce4487d4f | ||
|
|
c544389dd0 | ||
|
|
bdb84142d7 | ||
|
|
40698efb7d | ||
|
|
3d2a2b3d98 | ||
|
|
78a314b761 | ||
|
|
2d1dd3788b | ||
|
|
56d3160641 | ||
|
|
5db99041bc | ||
|
|
9ec56338c3 | ||
|
|
894ebf8eb7 | ||
|
|
6bec8a6c5c | ||
|
|
b9e7bc761b | ||
|
|
f87aaca2fb | ||
|
|
901ba615ab | ||
|
|
f098bc1b5d | ||
|
|
e1f7c8c06c | ||
|
|
a9987546d5 | ||
|
|
6a39887e8f | ||
|
|
349e314850 | ||
|
|
b1112d8cdc | ||
|
|
27c383a5d9 | ||
|
|
7cf24feef6 | ||
|
|
4443766d39 | ||
|
|
ea2166b09a | ||
|
|
9e4e69be1c | ||
|
|
dd0655bfde | ||
|
|
d2d275706b | ||
|
|
b7b63dd30f | ||
|
|
aacab4036c | ||
|
|
e104b1d936 | ||
|
|
bf6a6b417d | ||
|
|
dfd35b8cc8 | ||
|
|
1632681e6c | ||
|
|
88d10d53f2 | ||
|
|
eae9fa7030 | ||
|
|
ad76f68224 | ||
|
|
d783793b4e |
No files matched your search
@@ -1,5 +1,6 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
name: Bug report
|
||||
description: Report a reproducible problem in Mocktail
|
||||
description: Report a reproducible problem in Nightcap
|
||||
title: "[Bug]: "
|
||||
labels:
|
||||
- bug
|
||||
@@ -10,14 +11,14 @@ body:
|
||||
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.
|
||||
Use any language you are comfortable with.
|
||||
|
||||
Please write issues and pull requests yourself. Do not use AI-generated text.
|
||||
Please write issues and pull requests yourself.
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: Mocktail version
|
||||
label: Nightcap version
|
||||
description: Provide the release version, package version, or commit hash.
|
||||
placeholder: "For example: v0.4.0 or 1a2b3c4"
|
||||
validations:
|
||||
@@ -36,7 +37,7 @@ body:
|
||||
id: installation
|
||||
attributes:
|
||||
label: Installation method
|
||||
description: Tell us how Mocktail was installed or built.
|
||||
description: Tell us how Nightcap was installed or built.
|
||||
placeholder: "For example: AppImage, Flatpak, AUR, or built from source"
|
||||
validations:
|
||||
required: true
|
||||
@@ -55,7 +56,7 @@ body:
|
||||
label: Steps to reproduce
|
||||
description: List the smallest reliable sequence of actions that triggers the problem.
|
||||
placeholder: |
|
||||
1. Start Mocktail with ...
|
||||
1. Start Nightcap with ...
|
||||
2. Open ...
|
||||
3. Select ...
|
||||
4. Observe ...
|
||||
@@ -75,7 +76,7 @@ body:
|
||||
value: |
|
||||
Latest log:
|
||||
|
||||
- Flatpak: `~/.var/app/space.bigrat.mocktail/.local/state/mocktail/logs/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`.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
name: Feature request
|
||||
description: Suggest an improvement or a new capability for Mocktail
|
||||
description: Suggest an improvement or a new capability for Nightcap
|
||||
title: "[Feature]: "
|
||||
labels:
|
||||
- enhancement
|
||||
@@ -10,9 +11,9 @@ body:
|
||||
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.
|
||||
Use any language you are comfortable with.
|
||||
|
||||
Please write issues and pull requests yourself. Do not use AI-generated text.
|
||||
Please write issues and pull requests yourself.
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
@@ -26,7 +27,7 @@ body:
|
||||
id: proposal
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: Describe how you would like Mocktail to behave.
|
||||
description: Describe how you would like Nightcap to behave.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<!-- 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". -->
|
||||
<!-- Modified by vii from komaruworld/mocktail. See README "About this fork". -->
|
||||
<!-- Briefly describe what this pull request adds, changes, or removes. Link the issue with "Closes #123" or "Fixes #123". -->
|
||||
|
||||
Closes #
|
||||
@@ -1,8 +1,6 @@
|
||||
name: Publish AUR packages
|
||||
name: Check AUR packages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -13,8 +11,8 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
name: Publish AUR packages
|
||||
metadata:
|
||||
name: Refresh AUR metadata
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
@@ -27,10 +25,9 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
package_dir="$GITHUB_WORKSPACE/packaging/aur/mocktail-git"
|
||||
version="$(sed -n \
|
||||
's/^[[:space:]]*VERSION[[:space:]]\+\([0-9][0-9.]*\).*/\1/p' \
|
||||
CMakeLists.txt | head -n 1)"
|
||||
package_dir="$GITHUB_WORKSPACE/packaging/aur/nightcap-git"
|
||||
version="$(git describe --tags --abbrev=0 --match='v[0-9]*')"
|
||||
version="${version#v}"
|
||||
[[ "$version" =~ ^[0-9]+(\.[0-9]+)*$ ]]
|
||||
pkgver="${version}.r$(git rev-list --count HEAD).g$(git rev-parse --short=7 HEAD)"
|
||||
sed -i "s/^pkgver=.*/pkgver=${pkgver}/" "$package_dir/PKGBUILD"
|
||||
@@ -39,76 +36,21 @@ jobs:
|
||||
--volume "$package_dir:/pkg" \
|
||||
archlinux:base-devel bash -Eeuo pipefail -c '
|
||||
useradd --create-home builder
|
||||
install -d -o builder -g builder /tmp/mocktail-git
|
||||
install -d -o builder -g builder /tmp/nightcap-git
|
||||
install -o builder -g builder -m 0644 \
|
||||
/pkg/PKGBUILD /tmp/mocktail-git/PKGBUILD
|
||||
/pkg/PKGBUILD /tmp/nightcap-git/PKGBUILD
|
||||
runuser -u builder -- bash -c \
|
||||
"cd /tmp/mocktail-git && makepkg --printsrcinfo" \
|
||||
"cd /tmp/nightcap-git && makepkg --printsrcinfo" \
|
||||
> /pkg/.SRCINFO
|
||||
'
|
||||
|
||||
grep -Fqx "pkgver=${pkgver}" "$package_dir/PKGBUILD"
|
||||
grep -Fqx $'\tpkgver = '"${pkgver}" "$package_dir/.SRCINFO"
|
||||
printf 'AUR_PKGVER=%s\n' "$pkgver" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Publish to the AUR
|
||||
shell: bash
|
||||
env:
|
||||
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||
AUR_SSH_KEY_PASSPHRASE: ${{ secrets.AUR_SSH_KEY_PASSPHRASE }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
[[ -n "$AUR_SSH_PRIVATE_KEY" ]]
|
||||
[[ -n "$AUR_SSH_KEY_PASSPHRASE" ]]
|
||||
|
||||
private_key="$RUNNER_TEMP/aur_ed25519"
|
||||
askpass="$RUNNER_TEMP/aur_askpass"
|
||||
known_hosts="$RUNNER_TEMP/aur_known_hosts"
|
||||
printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > "$private_key"
|
||||
unset AUR_SSH_PRIVATE_KEY
|
||||
chmod 0600 "$private_key"
|
||||
printf '%s\n' \
|
||||
'#!/usr/bin/env bash' \
|
||||
'printf '\''%s\n'\'' "$AUR_SSH_KEY_PASSPHRASE"' \
|
||||
> "$askpass"
|
||||
chmod 0700 "$askpass"
|
||||
eval "$(ssh-agent -s)"
|
||||
trap 'ssh-agent -k >/dev/null; rm -f -- "$private_key" "$askpass"' EXIT
|
||||
DISPLAY=:0 SSH_ASKPASS="$askpass" SSH_ASKPASS_REQUIRE=force \
|
||||
ssh-add "$private_key" </dev/null
|
||||
unset AUR_SSH_KEY_PASSPHRASE
|
||||
|
||||
ssh-keyscan -T 10 -t ed25519 aur.archlinux.org > "$known_hosts"
|
||||
fingerprint="$(ssh-keygen -lf "$known_hosts" -E sha256 | awk '{print $2}')"
|
||||
[[ "$fingerprint" == \
|
||||
'SHA256:RFzBCUItH9LZS0cKB5UE6ceAYhBD5C8GeOBip8Z11+4' ]]
|
||||
|
||||
export GIT_SSH_COMMAND="ssh -F /dev/null -i $private_key \
|
||||
-o BatchMode=yes -o IdentitiesOnly=yes \
|
||||
-o StrictHostKeyChecking=yes \
|
||||
-o UserKnownHostsFile=$known_hosts"
|
||||
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 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'
|
||||
- name: Upload AUR metadata
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Nightcap-AUR
|
||||
path: packaging/aur/nightcap-git/.SRCINFO
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
@@ -104,14 +104,25 @@ jobs:
|
||||
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 \
|
||||
libplacebo-dev libpng-dev zlib1g-dev
|
||||
libplacebo-dev libpng-dev zlib1g-dev \
|
||||
xvfb xauth libgl1-mesa-dri jq patchelf
|
||||
|
||||
- name: Build and check native executables
|
||||
shell: bash
|
||||
run: |
|
||||
test "$(uname -m)" = "${{ matrix.arch }}"
|
||||
cmake -S . -B build-ci -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF
|
||||
-DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=ON
|
||||
cmake --build build-ci --parallel 4
|
||||
./build-ci/mocktail --help
|
||||
./build-ci/mocktail_updater status
|
||||
|
||||
- name: Run C++ and integration tests
|
||||
shell: bash
|
||||
env:
|
||||
SDL_AUDIODRIVER: dummy
|
||||
SDL_VIDEODRIVER: x11
|
||||
LIBGL_ALWAYS_SOFTWARE: '1'
|
||||
run: |
|
||||
xvfb-run -a ctest --test-dir build-ci --output-on-failure \
|
||||
--parallel 4 --timeout 120
|
||||
@@ -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,41 +27,26 @@ 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 (${{ matrix.arch }})
|
||||
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: ${{ matrix.runner }}
|
||||
name: Flatpak build
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
runner: ubuntu-24.04
|
||||
- arch: aarch64
|
||||
runner: ubuntu-24.04-arm
|
||||
env:
|
||||
FLATPAK_ARCH: ${{ matrix.arch }}
|
||||
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: Install Flatpak builder
|
||||
@@ -74,32 +57,33 @@ jobs:
|
||||
flatpak remote-add --user --if-not-exists flathub \
|
||||
https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
|
||||
- name: Build Flatpak bundle
|
||||
- name: Build Flatpak repository and bundle
|
||||
shell: bash
|
||||
env:
|
||||
FLATPAK_GPG_PRIVATE_KEY: ${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}
|
||||
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
git update-ref refs/heads/main "$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="$FLATPAK_ARCH" \
|
||||
--arch=x86_64 \
|
||||
--jobs=4 \
|
||||
--user \
|
||||
--install-deps-from=flathub \
|
||||
@@ -109,135 +93,50 @@ 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/nightcap-gnupg" \
|
||||
repo
|
||||
fi
|
||||
flatpak build-bundle \
|
||||
--arch="$FLATPAK_ARCH" \
|
||||
--arch=x86_64 \
|
||||
"${bundle_keys[@]}" \
|
||||
repo \
|
||||
"Mocktail-${FLATPAK_ARCH}.flatpak" \
|
||||
space.bigrat.mocktail \
|
||||
Nightcap-x86_64.flatpak \
|
||||
io.github.CoderDayton.nightcap \
|
||||
stable
|
||||
if [[ "$MOCKTAIL_PUBLISH" == true ]]; then
|
||||
./scripts/assemble_flatpak_pages.sh \
|
||||
repo Nightcap-x86_64.flatpak nightcap-flatpak.gpg public
|
||||
fi
|
||||
|
||||
- name: Upload Flatpak
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-${{ matrix.arch }}-flatpak
|
||||
path: Mocktail-${{ matrix.arch }}.flatpak
|
||||
name: Nightcap-x86_64-flatpak
|
||||
path: Nightcap-x86_64.flatpak
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
publish:
|
||||
name: Assemble multi-architecture 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')
|
||||
needs: flatpak
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||
|
||||
- name: Download both Flatpak bundles
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: Mocktail-*-flatpak
|
||||
path: flatpak-bundles
|
||||
merge-multiple: true
|
||||
|
||||
- name: Download package repositories
|
||||
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 repository tools
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes --no-install-recommends flatpak gnupg ostree
|
||||
|
||||
- name: Sign and publish both architectures
|
||||
shell: bash
|
||||
env:
|
||||
FLATPAK_GPG_PRIVATE_KEY: ${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}
|
||||
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
[[ -n "$FLATPAK_GPG_PRIVATE_KEY" && -n "$FLATPAK_GPG_KEY_ID" ]]
|
||||
[[ -s flatpak-bundles/Mocktail-x86_64.flatpak && \
|
||||
-s flatpak-bundles/Mocktail-aarch64.flatpak ]]
|
||||
install -d -m 0700 "$RUNNER_TEMP/mocktail-gnupg"
|
||||
printf '%s' "$FLATPAK_GPG_PRIVATE_KEY" | \
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" --import
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-gnupg" \
|
||||
--export "$FLATPAK_GPG_KEY_ID" >mocktail-flatpak.gpg
|
||||
ostree --repo=repo init --mode=archive
|
||||
for arch in x86_64 aarch64; do
|
||||
flatpak build-import-bundle \
|
||||
--gpg-sign="$FLATPAK_GPG_KEY_ID" \
|
||||
--gpg-homedir="$RUNNER_TEMP/mocktail-gnupg" \
|
||||
--no-update-summary \
|
||||
repo "flatpak-bundles/Mocktail-${arch}.flatpak"
|
||||
ostree --repo=repo refs | \
|
||||
grep -Fxq "app/space.bigrat.mocktail/${arch}/stable"
|
||||
done
|
||||
flatpak build-update-repo \
|
||||
--generate-static-deltas \
|
||||
--static-delta-jobs=2 \
|
||||
--prune \
|
||||
--gpg-sign="$FLATPAK_GPG_KEY_ID" \
|
||||
--gpg-homedir="$RUNNER_TEMP/mocktail-gnupg" \
|
||||
repo
|
||||
./scripts/assemble_flatpak_pages.sh \
|
||||
repo flatpak-bundles/Mocktail-x86_64.flatpak \
|
||||
flatpak-bundles/Mocktail-aarch64.flatpak \
|
||||
mocktail-flatpak.gpg public native-repositories
|
||||
|
||||
- name: Configure GitHub Pages
|
||||
if: env.MOCKTAIL_PUBLISH == 'true'
|
||||
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
||||
|
||||
- name: Upload GitHub Pages repository
|
||||
if: env.MOCKTAIL_PUBLISH == 'true'
|
||||
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
||||
with:
|
||||
path: public
|
||||
|
||||
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')
|
||||
needs: publish
|
||||
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
|
||||
permissions:
|
||||
@@ -251,3 +150,43 @@ 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: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- 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)
|
||||
./scripts/release_notes.sh "$tag" > release/notes.md
|
||||
# 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" --notes-file release/notes.md ||
|
||||
gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null
|
||||
fi
|
||||
gh release edit "$tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--title "Nightcap $tag" --notes-file release/notes.md
|
||||
gh release upload "$tag" --repo "$GITHUB_REPOSITORY" --clobber \
|
||||
release/Nightcap-x86_64.flatpak \
|
||||
release/Nightcap-x86_64.flatpak.sha256
|
||||
@@ -1,39 +1,47 @@
|
||||
name: Native packages
|
||||
name: Nightly AppImage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths-ignore:
|
||||
- '.github/workflows/ci.yml'
|
||||
- '.gitignore'
|
||||
- 'README.md'
|
||||
- 'assets/screenshots/**'
|
||||
- 'docs/**'
|
||||
- 'packaging/aur/**'
|
||||
- 'scripts/build_flatpak.sh'
|
||||
- 'tests/**'
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- '.github/workflows/ci.yml'
|
||||
- '.gitignore'
|
||||
- 'README.md'
|
||||
- 'assets/screenshots/**'
|
||||
- 'docs/**'
|
||||
- 'packaging/aur/**'
|
||||
- 'scripts/build_flatpak.sh'
|
||||
- 'tests/**'
|
||||
schedule:
|
||||
# 05:00 UTC daily. A run whose commit already carries the continuous tag
|
||||
# exits without rebuilding, so a quiet day costs one probe job.
|
||||
- cron: '0 5 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: packages-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
group: nightly-appimage
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
probe:
|
||||
name: Check for new commits
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
build: ${{ steps.decide.outputs.build }}
|
||||
steps:
|
||||
- name: Compare HEAD against the published continuous tag
|
||||
id: decide
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
published="$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/continuous" \
|
||||
--jq .object.sha 2>/dev/null || true)"
|
||||
if [[ "$GITHUB_EVENT_NAME" == "schedule" && "$published" == "$GITHUB_SHA" ]]; then
|
||||
echo 'build=false' >>"$GITHUB_OUTPUT"
|
||||
echo "continuous already points at $GITHUB_SHA; nothing to build"
|
||||
else
|
||||
echo 'build=true' >>"$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
appimage:
|
||||
name: AppImage (AnyLinux)
|
||||
needs: probe
|
||||
if: needs.probe.outputs.build == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
@@ -57,7 +65,7 @@ jobs:
|
||||
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 libpng bubblewrap xdg-dbus-proxy \
|
||||
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.
|
||||
@@ -138,266 +146,17 @@ jobs:
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-AppImage
|
||||
name: Nightcap-AppImage
|
||||
path: dist/Mocktail-x86_64.AppImage
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
appimage-aarch64:
|
||||
name: AppImage (AnyLinux aarch64)
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build aarch64 AppImage
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
docker run --rm --platform linux/arm64 \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
--env GH_TOKEN \
|
||||
ghcr.io/pkgforge-dev/archlinux:latest@sha256:b2507f1964270cab3cc190aa8df858521f6a7e45e969146a012877891d5dfc9b bash -Eeuo pipefail -c '
|
||||
test "$(uname -m)" = aarch64
|
||||
pacman-key --init
|
||||
pacman -Syy --noconfirm archlinux-keyring
|
||||
pacman -Syu --noconfirm --needed \
|
||||
base-devel 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 libpng 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-aarch64
|
||||
echo "3b64db0727a9d5c8212c4b2342770583ce552deac0644d2d6033e9ec280f7635 /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-aarch64-linux
|
||||
echo "298f5293c41f9b2b46290b246e6419783940824ce20b98775a5a153b18fe8422 /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-aarch64
|
||||
echo "5686f126b22d944b2b9c862ee004b4c590b5a5e5215d8790cc0420f1fbbea942 /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-aarch64
|
||||
echo "2d513c43ad652163e5e31c1a96f3c736e7e188e661ff62fc9143ea7147bd936b /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-aarch64 JOBS=4 \
|
||||
APPIMAGE=dist/Mocktail-aarch64.AppImage \
|
||||
PORTABLE_MODE=standalone APPIMAGE_FORMAT=anylinux \
|
||||
ANYLINUX_PACKAGER=/tmp/quick-sharun \
|
||||
ANYLINUX_APPIMAGETOOL=/tmp/appimagetool
|
||||
test -x dist/Mocktail-aarch64.AppImage
|
||||
'
|
||||
|
||||
- name: Smoke-test aarch64 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-aarch64.AppImage --appimage-extract-and-run --help
|
||||
timeout 60s dist/Mocktail-aarch64.AppImage --appimage-extract-and-run mocktail_updater status
|
||||
|
||||
- name: Upload aarch64 AppImage
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-AppImage-aarch64
|
||||
path: dist/Mocktail-aarch64.AppImage
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
deb:
|
||||
name: DEB (Ubuntu 26.04)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build DEB
|
||||
shell: bash
|
||||
run: |
|
||||
nightly_suffix=".nightly.$(git show -s --format=%cd --date=format:%Y%m%d%H%M%S "$GITHUB_SHA").g${GITHUB_SHA:0:8}"
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
--env MOCKTAIL_PACKAGE_VERSION_SUFFIX="$nightly_suffix" \
|
||||
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 fakeroot \
|
||||
dpkg-dev file 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 libplacebo-dev libpng-dev zlib1g-dev
|
||||
cmake -S . -B build-deb -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DMOCKTAIL_PACKAGE_NAME=mocktail-nightly \
|
||||
-DMOCKTAIL_PACKAGE_VERSION_SUFFIX="$MOCKTAIL_PACKAGE_VERSION_SUFFIX" \
|
||||
-DBUILD_TESTING=OFF
|
||||
cmake --build build-deb --parallel 4
|
||||
cpack --config build-deb/CPackConfig.cmake \
|
||||
-G DEB -B dist/deb
|
||||
dpkg-deb --info dist/deb/*.deb >/dev/null
|
||||
'
|
||||
|
||||
- name: Upload DEB
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-DEB
|
||||
path: dist/deb/*.deb
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
rpm:
|
||||
name: RPM (Fedora 44)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build RPM
|
||||
shell: bash
|
||||
run: |
|
||||
nightly_suffix=".nightly.$(git show -s --format=%cd --date=format:%Y%m%d%H%M%S "$GITHUB_SHA").g${GITHUB_SHA:0:8}"
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
--env MOCKTAIL_PACKAGE_VERSION_SUFFIX="$nightly_suffix" \
|
||||
fedora:44 bash -Eeuo pipefail -c '
|
||||
dnf install --assumeyes \
|
||||
gcc-c++ cmake git ninja-build pkgconf-pkg-config lld rpm-build \
|
||||
SDL3-devel SDL3_ttf-devel libcurl-devel openssl-devel \
|
||||
nlohmann-json-devel libyaml-devel elfutils-libelf-devel \
|
||||
minizip-ng-compat-devel capstone-devel gtk4-devel libadwaita-devel \
|
||||
webkitgtk6.0-devel utf8proc-devel fontconfig-devel \
|
||||
libglvnd-devel vulkan-headers vulkan-loader-devel \
|
||||
libplacebo-devel libpng-devel zlib-ng-compat-devel
|
||||
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
|
||||
cmake --build build-rpm --parallel 4
|
||||
cpack --config build-rpm/CPackConfig.cmake \
|
||||
-G RPM -B dist/rpm
|
||||
rpm --query --package --info dist/rpm/*.rpm >/dev/null
|
||||
'
|
||||
|
||||
- name: Upload RPM
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-RPM
|
||||
path: dist/rpm/*.rpm
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
arch:
|
||||
name: Pacman package (Arch Linux)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build pacman package
|
||||
shell: bash
|
||||
run: |
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
archlinux:base-devel bash -Eeuo pipefail -c '
|
||||
pacman -Syu --needed --noconfirm \
|
||||
cmake git ninja pkgconf lld sdl3 sdl3_ttf curl openssl \
|
||||
nlohmann-json libyaml libelf minizip capstone gtk4 \
|
||||
libadwaita webkitgtk-6.0 libutf8proc fontconfig \
|
||||
libplacebo libpng vulkan-headers vulkan-icd-loader libglvnd zlib
|
||||
useradd --create-home builder
|
||||
chown -R builder:builder /workspace
|
||||
runuser -u builder -- env MOCKTAIL_SOURCE_DIR=/workspace \
|
||||
makepkg --dir /workspace/packaging/arch \
|
||||
--noconfirm --cleanbuild --force
|
||||
install -d dist/arch
|
||||
cp packaging/arch/*.pkg.tar.zst dist/arch/
|
||||
pacman -Qp dist/arch/*.pkg.tar.zst >/dev/null
|
||||
'
|
||||
|
||||
- name: Upload pacman package
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-Arch
|
||||
path: dist/arch/*.pkg.tar.zst
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
publish:
|
||||
name: Publish continuous packages
|
||||
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
needs: [appimage, appimage-aarch64, deb, rpm, arch]
|
||||
name: Publish continuous release
|
||||
needs: [probe, appimage]
|
||||
# A manual run from another branch still builds and smoke-tests, but the
|
||||
# continuous tag and release only ever follow main.
|
||||
if: needs.probe.outputs.build == 'true' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
@@ -408,85 +167,49 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download packages
|
||||
- name: Download AppImage
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: Mocktail-*
|
||||
name: Nightcap-AppImage
|
||||
path: release
|
||||
merge-multiple: true
|
||||
|
||||
- name: Prepare continuous packages
|
||||
- name: Name and checksum the nightly build
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
mv -- release/Mocktail-x86_64.AppImage release/mocktail-nightly.AppImage
|
||||
mv -- release/Mocktail-aarch64.AppImage release/mocktail-nightly-aarch64.AppImage
|
||||
mv -- release/*.deb release/mocktail-nightly.deb
|
||||
mv -- release/*.rpm release/mocktail-nightly.rpm
|
||||
mv -- release/*.pkg.tar.zst release/mocktail-nightly.pkg.tar.zst
|
||||
# The Makefile builds under upstream's artifact name; the published
|
||||
# asset carries this fork's.
|
||||
mv -- release/Mocktail-x86_64.AppImage release/Nightcap-nightly-x86_64.AppImage
|
||||
(cd release && sha256sum Nightcap-nightly-x86_64.AppImage \
|
||||
> Nightcap-nightly-x86_64.AppImage.sha256)
|
||||
find release -maxdepth 1 -type f -print
|
||||
|
||||
- name: Build package repositories
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
FLATPAK_GPG_PRIVATE_KEY: ${{ secrets.FLATPAK_GPG_PRIVATE_KEY }}
|
||||
FLATPAK_GPG_KEY_ID: ${{ secrets.FLATPAK_GPG_KEY_ID }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
[[ -n "$FLATPAK_GPG_PRIVATE_KEY" && -n "$FLATPAK_GPG_KEY_ID" ]]
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes --no-install-recommends \
|
||||
apt-utils createrepo-c gnupg rpm
|
||||
install -d stable/deb stable/rpm
|
||||
stable_tag="$(gh api "repos/$GITHUB_REPOSITORY/releases/latest" --jq .tag_name)"
|
||||
gh release download "$stable_tag" --pattern '*.deb' --dir stable/deb
|
||||
gh release download "$stable_tag" --pattern '*.rpm' --dir stable/rpm
|
||||
mapfile -t stable_debs < <(find stable/deb -maxdepth 1 -type f -name '*.deb' -print)
|
||||
mapfile -t stable_rpms < <(find stable/rpm -maxdepth 1 -type f -name '*.rpm' -print)
|
||||
[[ "${#stable_debs[@]}" -eq 1 && "${#stable_rpms[@]}" -eq 1 ]]
|
||||
install -d -m 0700 "$RUNNER_TEMP/mocktail-native-gnupg"
|
||||
printf '%s' "$FLATPAK_GPG_PRIVATE_KEY" | \
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-native-gnupg" --import
|
||||
gpg --batch --homedir "$RUNNER_TEMP/mocktail-native-gnupg" \
|
||||
--export "$FLATPAK_GPG_KEY_ID" >mocktail-packages.gpg
|
||||
./scripts/assemble_native_repositories.sh \
|
||||
"${stable_debs[0]}" release/mocktail-nightly.deb \
|
||||
"${stable_rpms[0]}" release/mocktail-nightly.rpm \
|
||||
mocktail-packages.gpg "$FLATPAK_GPG_KEY_ID" \
|
||||
"$RUNNER_TEMP/mocktail-native-gnupg" native-repositories
|
||||
|
||||
- name: Update continuous release
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
git config user.name komaruworld
|
||||
git config user.email rtysus228@gmail.com
|
||||
git config user.name 'github-actions[bot]'
|
||||
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
||||
git tag --force continuous "$GITHUB_SHA"
|
||||
git push origin refs/tags/continuous --force
|
||||
notes="Automated build of main at ${GITHUB_SHA:0:8}. Untested and unreleased; use a tagged release for normal play."
|
||||
if ! gh release view continuous >/dev/null 2>&1; then
|
||||
gh release create continuous \
|
||||
--prerelease \
|
||||
--title "Mocktail continuous" \
|
||||
--notes "Automated development packages from main."
|
||||
--title "Nightcap continuous" \
|
||||
--notes "$notes"
|
||||
fi
|
||||
gh release edit continuous --title "Nightcap continuous" --notes "$notes"
|
||||
gh release upload continuous release/* --clobber
|
||||
# Drop assets left by earlier runs under other names.
|
||||
while IFS= read -r asset; do
|
||||
case "$asset" in
|
||||
mocktail-nightly.AppImage|mocktail-nightly-aarch64.AppImage|mocktail-nightly.deb|mocktail-nightly.rpm|mocktail-nightly.pkg.tar.zst)
|
||||
Nightcap-nightly-x86_64.AppImage|Nightcap-nightly-x86_64.AppImage.sha256)
|
||||
;;
|
||||
Mocktail-*.AppImage|mocktail_*.deb|mocktail-*.deb|mocktail-*.rpm|mocktail-*.pkg.tar.zst)
|
||||
*)
|
||||
gh release delete-asset continuous "$asset" --yes
|
||||
;;
|
||||
esac
|
||||
done < <(gh release view continuous --json assets --jq '.assets[].name')
|
||||
|
||||
- name: Upload package repositories
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Mocktail-native-repositories
|
||||
path: native-repositories
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
@@ -0,0 +1,338 @@
|
||||
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: Check release notes
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
shell: bash
|
||||
run: ./scripts/release_notes.sh "$GITHUB_REF_NAME" >/dev/null
|
||||
|
||||
- name: Build AppImage
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
--env GH_TOKEN \
|
||||
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
|
||||
|
||||
deb:
|
||||
name: DEB (Ubuntu 26.04)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build DEB
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
--env MOCKTAIL_PACKAGE_VERSION="$(./scripts/package_version.sh)" \
|
||||
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 fakeroot \
|
||||
dpkg-dev file 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 libplacebo-dev zlib1g-dev libpng-dev
|
||||
cmake -S . -B build-deb -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DMOCKTAIL_PACKAGE_NAME=nightcap \
|
||||
-DMOCKTAIL_PACKAGE_VERSION="$MOCKTAIL_PACKAGE_VERSION" \
|
||||
-DBUILD_TESTING=OFF
|
||||
cmake --build build-deb --parallel 4
|
||||
cpack --config build-deb/CPackConfig.cmake \
|
||||
-G DEB -B dist/deb
|
||||
dpkg-deb --info dist/deb/*.deb >/dev/null
|
||||
'
|
||||
|
||||
- name: Upload DEB
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Nightcap-DEB
|
||||
path: dist/deb/*.deb
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
rpm:
|
||||
name: RPM (Fedora 44)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build RPM
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
--env MOCKTAIL_PACKAGE_VERSION="$(./scripts/package_version.sh)" \
|
||||
fedora:44 bash -Eeuo pipefail -c '
|
||||
dnf install --assumeyes \
|
||||
gcc-c++ cmake git ninja-build pkgconf-pkg-config lld rpm-build \
|
||||
SDL3-devel SDL3_ttf-devel libcurl-devel openssl-devel \
|
||||
nlohmann-json-devel libyaml-devel elfutils-libelf-devel \
|
||||
minizip-ng-compat-devel capstone-devel gtk4-devel libadwaita-devel \
|
||||
webkitgtk6.0-devel utf8proc-devel fontconfig-devel \
|
||||
libglvnd-devel vulkan-headers vulkan-loader-devel \
|
||||
libplacebo-devel zlib-ng-compat-devel libpng-devel
|
||||
cmake -S . -B build-rpm -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||
-DMOCKTAIL_PACKAGE_NAME=nightcap \
|
||||
-DMOCKTAIL_PACKAGE_VERSION="$MOCKTAIL_PACKAGE_VERSION" \
|
||||
-DBUILD_TESTING=OFF
|
||||
cmake --build build-rpm --parallel 4
|
||||
cpack --config build-rpm/CPackConfig.cmake \
|
||||
-G RPM -B dist/rpm
|
||||
rpm --query --package --info dist/rpm/*.rpm >/dev/null
|
||||
'
|
||||
|
||||
- name: Upload RPM
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Nightcap-RPM
|
||||
path: dist/rpm/*.rpm
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
arch:
|
||||
name: Pacman package (Arch Linux)
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 45
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Build pacman package
|
||||
shell: bash
|
||||
run: |
|
||||
set -Eeuo pipefail
|
||||
version="$(./scripts/package_version.sh)"
|
||||
sed -i "s/^pkgver=.*/pkgver=${version}/" packaging/arch/PKGBUILD
|
||||
docker run --rm \
|
||||
--volume "$GITHUB_WORKSPACE:/workspace" \
|
||||
--workdir /workspace \
|
||||
ghcr.io/archlinux/archlinux:base-devel@sha256:c3953446016873a756fb7814f10087cac1634c127f7302e80c81c512da796d05 bash -Eeuo pipefail -c '
|
||||
pacman -Syu --needed --noconfirm \
|
||||
cmake git ninja pkgconf lld sdl3 sdl3_ttf curl openssl \
|
||||
nlohmann-json libyaml libelf minizip capstone gtk4 \
|
||||
libadwaita webkitgtk-6.0 libutf8proc fontconfig libpng \
|
||||
libplacebo vulkan-headers vulkan-icd-loader libglvnd zlib
|
||||
useradd --create-home builder
|
||||
chown -R builder:builder /workspace
|
||||
runuser -u builder -- env MOCKTAIL_SOURCE_DIR=/workspace \
|
||||
makepkg --dir /workspace/packaging/arch \
|
||||
--noconfirm --cleanbuild --force
|
||||
install -d dist/arch
|
||||
cp packaging/arch/*.pkg.tar.zst dist/arch/
|
||||
pacman -Qp dist/arch/*.pkg.tar.zst >/dev/null
|
||||
'
|
||||
|
||||
- name: Upload pacman package
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: Nightcap-Arch
|
||||
path: dist/arch/*.pkg.tar.zst
|
||||
if-no-files-found: error
|
||||
retention-days: 14
|
||||
|
||||
publish:
|
||||
name: Publish GitHub release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: [appimage, deb, rpm, arch]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 15
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Check out source
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Download packages
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: 'Nightcap-*'
|
||||
merge-multiple: true
|
||||
path: release
|
||||
|
||||
- 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
|
||||
# An unmatched glob would otherwise reach sha256sum and gh as its own
|
||||
# literal text, so every package pattern has to match before upload.
|
||||
shopt -s nullglob
|
||||
assets=(Nightcap-x86_64.AppImage)
|
||||
for glob in '*.deb' '*.rpm' '*.pkg.tar.zst'; do
|
||||
matched=(release/$glob)
|
||||
if [[ ${#matched[@]} -eq 0 ]]; then
|
||||
echo "::error::no ${glob} package among the release artifacts"
|
||||
exit 1
|
||||
fi
|
||||
assets+=("${matched[@]#release/}")
|
||||
done
|
||||
uploads=()
|
||||
for asset in "${assets[@]}"; do
|
||||
uploads+=("release/$asset" "release/$asset.sha256")
|
||||
done
|
||||
(
|
||||
cd release
|
||||
for asset in "${assets[@]}"; do
|
||||
sha256sum "$asset" > "$asset.sha256"
|
||||
done
|
||||
)
|
||||
./scripts/release_notes.sh "$tag" > release/notes.md
|
||||
# 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" --notes-file release/notes.md ||
|
||||
gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null
|
||||
fi
|
||||
gh release edit "$tag" --repo "$GITHUB_REPOSITORY" \
|
||||
--title "Nightcap $tag" --notes-file release/notes.md
|
||||
gh release upload "$tag" --repo "$GITHUB_REPOSITORY" --clobber \
|
||||
"${uploads[@]}"
|
||||
@@ -1,5 +1,6 @@
|
||||
# Build
|
||||
/build/
|
||||
/build-test/
|
||||
/build_*/
|
||||
/build-flatpak/
|
||||
/build-anylinux*/
|
||||
@@ -176,3 +177,6 @@ lsan.log*
|
||||
# Test fixtures
|
||||
!/third_party/mcpelauncher-linker/core/libziparchive/**/*.zip
|
||||
!/third_party/mcpelauncher-linker/core/libziparchive/**/*.apk
|
||||
|
||||
# Working plans, not part of the shipped documentation
|
||||
/docs/plans/
|
||||
@@ -0,0 +1,289 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to Nightcap. The format follows
|
||||
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Release notes on
|
||||
GitHub are taken from the matching section here.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.3.3] - 2026-10-04
|
||||
|
||||
### Added
|
||||
|
||||
- Joining a friend from a Roblox "follow user" link now works. Nightcap looks
|
||||
up which game they are in and joins it.
|
||||
- Nix users get a desktop entry for the app.
|
||||
|
||||
### Changed
|
||||
|
||||
- Roblox textures now decode about 3 times faster, which shortens the stalls
|
||||
when a place loads and when new parts of the map stream in. Thanks to
|
||||
@glook9001.
|
||||
|
||||
### Fixed
|
||||
|
||||
- `make update-roblox` no longer deletes your previous Roblox install when an
|
||||
import fails partway through. If a step fails, the old files are put back,
|
||||
or kept in a folder whose path is printed. Thanks to @kimlherme.
|
||||
- A failed Roblox update now restores your previous version's records instead
|
||||
of leaving them half switched. A damaged version file is rejected instead of
|
||||
being half read, and a temporary failure to start the startup check no longer
|
||||
marks a working Roblox build as broken for good. Thanks to @kimlherme.
|
||||
- Fixed a possible crash when Roblox clears its audio queue while sound is
|
||||
still playing, and a memory leak in lists of objects passed to Roblox's Java
|
||||
code. Thanks to @kimlherme.
|
||||
- Fixed a possible crash when the text overlay shuts down, a freeze when Roblox
|
||||
releases the mouse lock while another release is already in progress, and
|
||||
network lookups that misread Roblox's options. Thanks to @kimlherme.
|
||||
- Fixed the sky sometimes rendering with the wrong textures in the Vulkan
|
||||
renderer.
|
||||
- Clicking while a Roblox text box has focus no longer closes it. Only Roblox
|
||||
decides when a click ends typing.
|
||||
- Going to another page in the web view, such as Friends from a profile, no
|
||||
longer closes the web view. It crashed on every page change on Wayland
|
||||
systems whose GL setup GTK could not use.
|
||||
- Fixed a crash when Roblox's saved dark or light theme setting was missing or
|
||||
damaged.
|
||||
- The Flatpak build now detects an NVIDIA driver from inside its sandbox, so it
|
||||
uses X11 instead of native Wayland where both are available. This avoids
|
||||
Wayland hangs and display disconnects.
|
||||
|
||||
## [0.3.2] - 2026-09-29
|
||||
|
||||
### Added
|
||||
|
||||
- Performance recordings made with `--profile` now show how busy the CPU was
|
||||
during each frame, which makes it easier to tell what caused a stutter.
|
||||
- Experimental support for ARM64 (`aarch64`) Linux, running Roblox's Android
|
||||
`arm64-v8a` client.
|
||||
|
||||
### Fixed
|
||||
|
||||
- When Roblox asks for a Google Play device check during login, Nightcap now
|
||||
answers right away that it isn't supported. Before, the "Verifying you're
|
||||
not a bot" screen waited about a minute for an answer and then sent you
|
||||
back to the login screen.
|
||||
- Solving a login challenge now closes its window. Before, Roblox's
|
||||
"challenge completed" signal was ignored and the window stayed open.
|
||||
Thanks to @glook9001.
|
||||
- Login no longer stalls after you pass a verification check. Before, Roblox
|
||||
only saw the page's "challenge completed" signal after a one-minute
|
||||
timeout, so it gave up and the login failed. On a Roblox build Nightcap
|
||||
can't handle this way, the log says "WebViewProtocol getter not
|
||||
recognized" and web pages behave as before.
|
||||
- Login check windows and other web pages get 5 seconds to start instead of
|
||||
3, so a slow start no longer closes them. The log says how long each one
|
||||
took to open.
|
||||
- Less stutter while joining a game and while textures load in. In Blade
|
||||
Ball, loading hitches dropped by about half, and the worst one went from
|
||||
0.8 seconds to 0.55 seconds.
|
||||
- Clicking Play on a private server now joins right away. Before, nothing
|
||||
happened until you clicked a sidebar tab like Home.
|
||||
- Middle click now reaches Roblox as the middle mouse button.
|
||||
- Scrolling follows your system's natural scrolling setting.
|
||||
- Fixed a crash on some x86_64 hosts when Roblox asked about a thread other
|
||||
than the one making the call.
|
||||
- Roblox updates no longer mix up the ARM64 and x86_64 compatibility profiles
|
||||
for the same version.
|
||||
- A partial or out-of-range user id in `MOCKTAIL_ROBLOX_USER_ID` or
|
||||
`MOCKTAIL_LOCAL_STORAGE_CURRENT_USER_ID` is now ignored instead of being
|
||||
partly read.
|
||||
|
||||
### Changed
|
||||
|
||||
- `make flatpak` and `scripts/build_flatpak.sh` now build with 2 jobs by
|
||||
default instead of 4, so a local Flatpak build is less likely to run out
|
||||
of memory. Set `FLATPAK_JOBS` or `MOCKTAIL_FLATPAK_JOBS` to use more.
|
||||
Thanks to @glook9001.
|
||||
|
||||
## [0.3.1] - 2026-09-20
|
||||
|
||||
### Added
|
||||
|
||||
- Each release carries a DEB, an RPM, and a pacman package beside the
|
||||
AppImage, with a `.sha256` for every file. They install `/usr/bin/mocktail`
|
||||
and conflict with each other and with upstream's `mocktail` package.
|
||||
Nightcap is in no distribution repository or the AUR; the recipes under
|
||||
`packaging/aur/nightcap{,-bin,-git}` build from this repository locally.
|
||||
- The AppStream metadata lists the Nightcap releases, so software centres
|
||||
show the release history instead of upstream's.
|
||||
|
||||
### Fixed
|
||||
|
||||
- ETC2 texture decoding no longer blocks the thread that submits the frame.
|
||||
Uploads decode on a background dispatcher and the submit carries a timeline
|
||||
semaphore wait, so the GPU waits for the texture instead of the render
|
||||
thread. Reading the application's compressed bytes moves to that dispatcher
|
||||
too whenever they are already mapped. In Blade Ball the worst
|
||||
`vkQueueSubmit` fell from 140ms to 1ms, its p99 from 4.9ms to 0.05ms, and
|
||||
the total time spent in submits from 2534ms to 309ms. Frame interval is
|
||||
unchanged: the remaining tail is the engine's own main-thread step. A device
|
||||
without the `timelineSemaphore` feature decodes inline as before, as does a
|
||||
submit whose `pNext` already sizes arrays by its semaphore counts.
|
||||
- The mouse pointer jumped when a right-click camera drag ended, and drifted
|
||||
to the viewport edge during one. The input router inferred pointer capture
|
||||
from SDL reporting zero absolute coordinates, which holds on X11 but not on
|
||||
Wayland, where the reported position accumulates the relative deltas and
|
||||
leaves the window. Motion, button, and wheel events now carry the window's
|
||||
relative mouse mode. The right-click camera fallback also only captures
|
||||
inside an experience, so the cursor stays free over the app surface.
|
||||
- Shutting the window down while an experience was starting or leaving could
|
||||
free the pointer capture owner underneath the thread using it. Callers off
|
||||
the SDL thread now hold a reference for the length of their call.
|
||||
- The TextBox caret did not move when a trailing space was typed in a
|
||||
single-line field. The final cluster rect SDL_ttf reports stops at the last
|
||||
inked glyph, so the laid-out width is used there instead. Wrapped layouts
|
||||
keep the clamp, because their width is the widest line rather than the last.
|
||||
- Every TextBox keystroke re-ran `TTF_Init`, a fontconfig sort, six
|
||||
`TTF_OpenFont` calls, and `TTF_Quit` on the render thread. The overlay holds
|
||||
its opened fonts and reopens them only when the Roblox font or the point
|
||||
size changes, keeping the current set when a reopen fails. Measured over 100
|
||||
rasters, 1.59ms to 0.12ms per keystroke.
|
||||
- Staged ETC2 copies now carry an explicit host-write barrier. A queue
|
||||
submission only makes host writes from before it visible to the device, and
|
||||
an asynchronous decode writes after the submit, so without the barrier those
|
||||
texels were not guaranteed to reach the copy that reads them.
|
||||
|
||||
### Changed
|
||||
|
||||
- `docs/PERFORMANCE.md` documents the GameMode settings to run with. A
|
||||
`desiredgov=powersave` in `~/.config/gamemode.ini` overrides the packaged
|
||||
`performance` default and clocks the CPU down for the whole session while
|
||||
still reporting an active performance request.
|
||||
|
||||
## [0.3.0] - 2026-09-18
|
||||
|
||||
### Added
|
||||
|
||||
- Custom Discord presence. `integrations.discord_rpc.text.title` replaces the
|
||||
name in "Playing Nightcap", and `integrations.discord_rpc.images` sets the
|
||||
large and small icons and their hover text. Text and image fields accept
|
||||
`{place_name}`, `{place_icon}`, and `{creator_name}`. A field that expands
|
||||
an empty placeholder is left out, and `text.state: ""` hides the state line.
|
||||
- The default Discord card now matches Sober: the title is "Roblox", the
|
||||
details line is "Playing <experience>", the state is "by <creator>", and
|
||||
the Roblox badge sits inside the experience icon. The creator comes from the
|
||||
same Roblox games request that already fetches the experience name. With
|
||||
`show_place_name: false` the creator is hidden too, so the card shows only
|
||||
the title and timer.
|
||||
- `--profile <file>` writes a Chrome-format trace of the Vulkan adapter's
|
||||
work: presents, GPU waits, submits, ETC2 texture decode, and pipeline and
|
||||
shader creation. Open it in ui.perfetto.dev. `docs/BENCHMARKING.md`
|
||||
describes the benchmark runs, and `scripts/summarize_profile_trace.py`
|
||||
turns traces into a before and after table.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Texture uploads stalled the frame they were submitted on. ETC2 batches
|
||||
decoded on worker threads but were then resampled one upload at a time on
|
||||
the submitting thread, which at the default 4x upscale is sixteen times the
|
||||
decoded texels. Resampling now runs on the same worker pool, in two passes so
|
||||
mip level 0 still resolves before the mips built from it. In a 70k-part
|
||||
place the worst `vkQueueSubmit` fell from 167ms to 92ms.
|
||||
- Most ETC2 batches decoded on a single thread. Workers were created and
|
||||
joined per batch, so the parallel floor had to cover that cost and sat at a
|
||||
1024x1024 image; batches under it ran on the caller, which was 54% of all
|
||||
decode time in a 70k-part place. Decode workers are now a persistent pool,
|
||||
and the floor drops to a 128x128 image. Mean decode time in that place fell
|
||||
from 8.2ms to 4.7ms, and 11.8ms before both texture fixes.
|
||||
- On CPUs with a wide core-speed spread, Feral GameMode pinned the process to
|
||||
the few fastest cores, concentrating Roblox's worker pool onto them instead
|
||||
of spreading it. With `performance.gamemode: auto`, a request that narrows
|
||||
the CPU affinity is now released again, which releases the pin. Set
|
||||
`pin_cores=no` in `~/.config/gamemode.ini` to keep GameMode without the
|
||||
pinning, or `performance.gamemode: on` to accept it. See
|
||||
`docs/PERFORMANCE.md`.
|
||||
- With `show_place_name: false`, Discord no longer shows the experience
|
||||
thumbnail or its name on hover.
|
||||
- The host main thread no longer spins on the engine's message pump, which
|
||||
cost about half a core and 146,000 empty calls a second. It now rests 100 µs
|
||||
between polls: sleeping when the governor is `performance`, parking the
|
||||
core with TPAUSE on CPUs that have it, spinning otherwise. With GameMode
|
||||
setting the governor, process CPU in a 60k-part place fell from 216% to
|
||||
107% at the same 120 fps. `MOCKTAIL_ENGINE_PUMP_REST` forces a mode; see
|
||||
`docs/PERFORMANCE.md`.
|
||||
- ETC2 uploads with a `bufferRowLength` or `bufferImageHeight` larger than
|
||||
the image read the padded rows as packed data and decoded garbage.
|
||||
- ETC2 resampling created and joined worker threads on every batch. It now
|
||||
runs on the persistent decode pool.
|
||||
- A failed trace write left the profile writer thread waking every 250 ms
|
||||
for the rest of the session. It now exits.
|
||||
- The `--profile` trace gains a `pump` slice for the engine's main-thread
|
||||
step.
|
||||
|
||||
## [0.2.0] - 2026-09-16
|
||||
|
||||
### Added
|
||||
|
||||
- FAQ covering Discord RPC, log locations, and VR, adapted from upstream.
|
||||
- `input.raw_mouse` in `config.yaml` sends mouse look deltas in the host's
|
||||
own units. Turn it on when first-person aiming feels too slow or too fast
|
||||
under fractional display scaling. Off by default.
|
||||
- Nightly AppImage builds, published as the `continuous` prerelease. They are
|
||||
untested; use a tagged release for normal play.
|
||||
|
||||
### Changed
|
||||
|
||||
- Values in `fflags.json` now win over the performance preset instead of
|
||||
stopping the launch. Startup prints each key it kept.
|
||||
- Discord Rich Presence uses Nightcap's own Discord application, so it shows
|
||||
as "Playing Nightcap". Terms and privacy pages for it live on the
|
||||
project site.
|
||||
- Discord Rich Presence is on by default. Set
|
||||
`integrations.discord_rpc.enabled: false` to turn it off.
|
||||
- The AppImage build verifies its reduced dependency packages against digests
|
||||
taken at build time, rather than a digest list kept in the repository.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Mouse look keeps responding after a long turn. The tracked pointer position
|
||||
could drift past the edge of the view, which swallowed every reversal until
|
||||
the mouse travelled all the way back.
|
||||
- Nightcap no longer refuses to start with `cannot lock appStorage`. It
|
||||
created its own credential directory using the process umask, then rejected
|
||||
that directory for being group writable. Systems with a permissive umask,
|
||||
such as `002`, failed on first run.
|
||||
|
||||
## [0.1.0] - 2026-09-15
|
||||
|
||||
First Nightcap release, forked from komaruworld/mocktail.
|
||||
|
||||
### Added
|
||||
|
||||
- ETC2 and EAC texture emulation on Vulkan, so games that use compressed
|
||||
mobile textures render correctly on desktop GPUs. Decoding runs on worker
|
||||
threads to avoid stutter.
|
||||
- PNG texture overrides in `~/.config/mocktail/textures`, keyed by upload
|
||||
hash, with a dump mode to find the texture you want to replace.
|
||||
- Small textures are upscaled 4x by default. Set
|
||||
`MOCKTAIL_SMALL_TEXTURE_UPSCALE=1` to turn it off.
|
||||
- AppImage and signed Flatpak builds on every release tag. The Flatpak repo
|
||||
is published at https://coderdayton.github.io/nightcap/.
|
||||
- Roadmap in `docs/ROADMAP.md`.
|
||||
|
||||
### Changed
|
||||
|
||||
- Frames are presented as soon as they are ready instead of waiting in a
|
||||
queue, which lowers input latency.
|
||||
- Roblox now sees the real host RAM and display size instead of a low-end
|
||||
phone profile.
|
||||
- App ID is `io.github.CoderDayton.nightcap`, so Nightcap installs next to
|
||||
upstream Mocktail. The binary and config paths keep the `mocktail` name.
|
||||
- New name, icon, and colors.
|
||||
- The desktop entry launches on Wayland and points at the installed binary.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Servers no longer kick with error 319 shortly after joining.
|
||||
- The FMOD output-device bridge is optional, so the updater no longer fails
|
||||
without it.
|
||||
- The AppImage build no longer fails on stale checksums for the slimmed
|
||||
Arch packages.
|
||||
|
||||
[Unreleased]: https://github.com/CoderDayton/nightcap/compare/v0.3.3...HEAD
|
||||
[0.3.3]: https://github.com/CoderDayton/nightcap/releases/tag/v0.3.3
|
||||
[0.3.2]: https://github.com/CoderDayton/nightcap/releases/tag/v0.3.2
|
||||
[0.3.1]: https://github.com/CoderDayton/nightcap/releases/tag/v0.3.1
|
||||
[0.3.0]: https://github.com/CoderDayton/nightcap/releases/tag/v0.3.0
|
||||
[0.2.0]: https://github.com/CoderDayton/nightcap/releases/tag/v0.2.0
|
||||
[0.1.0]: https://github.com/CoderDayton/nightcap/releases/tag/v0.1.0
|
||||
@@ -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");
|
||||
@@ -60,7 +61,7 @@ set(MOCKTAIL_DEFAULT_SIGNING_TRUST_MANIFEST
|
||||
CACHE FILEPATH
|
||||
"Fallback signing trust manifest used outside an installed runtime"
|
||||
)
|
||||
set(MOCKTAIL_DISCORD_APPLICATION_ID "1537088975720812655" CACHE STRING
|
||||
set(MOCKTAIL_DISCORD_APPLICATION_ID "1549428231927500843" CACHE STRING
|
||||
"Discord application ID baked into Mocktail Rich Presence releases"
|
||||
)
|
||||
string(LENGTH "${MOCKTAIL_DISCORD_APPLICATION_ID}"
|
||||
@@ -248,6 +249,9 @@ mocktail_apply_compile_options(mocktail_updater)
|
||||
|
||||
add_library(mocktail_legacy_jnivm STATIC
|
||||
src/jnivm/jnivm.cc
|
||||
src/jnivm/jni_references.cc
|
||||
src/jnivm/jni_fields.cc
|
||||
src/jnivm/jni_strings.cc
|
||||
)
|
||||
add_library(Mocktail::LegacyJni ALIAS mocktail_legacy_jnivm)
|
||||
target_include_directories(mocktail_legacy_jnivm PUBLIC include)
|
||||
@@ -264,8 +268,8 @@ add_library(mocktail_runtime STATIC
|
||||
src/runtime/command_line.cc
|
||||
src/runtime/crash_report_policy.cc
|
||||
src/runtime/discord_rpc.cc
|
||||
src/runtime/device_memory_profile.cc
|
||||
src/runtime/device_profile.cc
|
||||
src/runtime/engine_pump_rest.cc
|
||||
src/runtime/environment.cc
|
||||
src/runtime/external_launch_broker.cc
|
||||
src/runtime/frame_rate_policy.cc
|
||||
@@ -284,9 +288,13 @@ add_library(mocktail_runtime STATIC
|
||||
src/runtime/roblox_capability_resolver.cc
|
||||
src/runtime/roblox_experience_launch_bridge.cc
|
||||
src/runtime/roblox_launch_uri.cc
|
||||
src/runtime/roblox_presence_resolver.cc
|
||||
src/runtime/roblox_game_session_native_adapter.cc
|
||||
src/runtime/roblox_web_view_bridge.cc
|
||||
src/runtime/roblox_permissions_bridge.cc
|
||||
src/runtime/roblox_account_bridge.cc
|
||||
src/runtime/roblox_message_bus_request_bridge.cc
|
||||
src/runtime/roblox_call_protocol_bridge.cc
|
||||
src/runtime/payload_update_preflight.cc
|
||||
src/runtime/process_launch_policy.cc
|
||||
src/runtime/process_diagnostics.cc
|
||||
@@ -301,6 +309,7 @@ add_library(mocktail_runtime STATIC
|
||||
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)
|
||||
@@ -383,7 +392,8 @@ add_library(mocktail_webview_helper_policy STATIC
|
||||
src/webview/webview_helper_policy.cc
|
||||
)
|
||||
add_library(Mocktail::WebViewHelperPolicy ALIAS mocktail_webview_helper_policy)
|
||||
target_include_directories(mocktail_webview_helper_policy PUBLIC src)
|
||||
target_include_directories(mocktail_webview_helper_policy PUBLIC src
|
||||
PRIVATE include)
|
||||
target_link_libraries(mocktail_webview_helper_policy PUBLIC
|
||||
PkgConfig::WEBKITGTK6
|
||||
nlohmann_json::nlohmann_json
|
||||
@@ -470,7 +480,7 @@ target_link_libraries(mocktail_window PUBLIC
|
||||
Mocktail::SdlVulkanWsi
|
||||
SDL3::SDL3
|
||||
${CMAKE_DL_LIBS}
|
||||
PRIVATE Mocktail::Runtime Mocktail::GlesTextOverlay
|
||||
PRIVATE Mocktail::Runtime Mocktail::GlesTextOverlay Mocktail::GraphicsFoundation
|
||||
)
|
||||
|
||||
add_library(mocktail_roblox_output_device_runtime STATIC
|
||||
@@ -730,13 +740,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
|
||||
@@ -745,7 +766,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"
|
||||
)
|
||||
@@ -778,23 +799,29 @@ install(CODE "
|
||||
endif()
|
||||
")
|
||||
|
||||
set(MOCKTAIL_PACKAGE_NAMES mocktail mocktail-nightly nightcap nightcap-nightly)
|
||||
set(MOCKTAIL_PACKAGE_NAME "mocktail" CACHE STRING
|
||||
"Native package name"
|
||||
)
|
||||
set(MOCKTAIL_PACKAGE_VERSION "${PROJECT_VERSION}" CACHE STRING
|
||||
"Native package version"
|
||||
)
|
||||
set(MOCKTAIL_PACKAGE_VERSION_SUFFIX "" CACHE STRING
|
||||
"Native package version suffix"
|
||||
)
|
||||
if(NOT MOCKTAIL_PACKAGE_NAME STREQUAL "mocktail" AND
|
||||
NOT MOCKTAIL_PACKAGE_NAME STREQUAL "mocktail-nightly")
|
||||
if(NOT MOCKTAIL_PACKAGE_NAME IN_LIST MOCKTAIL_PACKAGE_NAMES)
|
||||
string(JOIN ", " MOCKTAIL_PACKAGE_NAMES_TEXT ${MOCKTAIL_PACKAGE_NAMES})
|
||||
message(FATAL_ERROR
|
||||
"MOCKTAIL_PACKAGE_NAME must be mocktail or mocktail-nightly")
|
||||
"MOCKTAIL_PACKAGE_NAME must be one of ${MOCKTAIL_PACKAGE_NAMES_TEXT}")
|
||||
endif()
|
||||
|
||||
set(CPACK_PACKAGE_NAME "${MOCKTAIL_PACKAGE_NAME}")
|
||||
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}${MOCKTAIL_PACKAGE_VERSION_SUFFIX}")
|
||||
set(CPACK_PACKAGE_VENDOR "Mocktail Project")
|
||||
set(CPACK_PACKAGE_CONTACT "komaruworld <rtysus228@gmail.com>")
|
||||
set(CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/komaruworld/mocktail")
|
||||
set(CPACK_PACKAGE_VERSION
|
||||
"${MOCKTAIL_PACKAGE_VERSION}${MOCKTAIL_PACKAGE_VERSION_SUFFIX}"
|
||||
)
|
||||
set(CPACK_PACKAGE_VENDOR "Nightcap")
|
||||
set(CPACK_PACKAGE_CONTACT "vii <coderdayton14@gmail.com>")
|
||||
set(CPACK_PACKAGE_HOMEPAGE_URL "https://github.com/CoderDayton/nightcap")
|
||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "${PROJECT_DESCRIPTION}")
|
||||
set(CPACK_PACKAGE_DESCRIPTION
|
||||
"Runs the Android Roblox client on Linux."
|
||||
@@ -804,7 +831,7 @@ set(CPACK_PACKAGING_INSTALL_PREFIX "/usr")
|
||||
set(CPACK_STRIP_FILES TRUE)
|
||||
|
||||
set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)
|
||||
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "komaruworld <rtysus228@gmail.com>")
|
||||
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "vii <coderdayton14@gmail.com>")
|
||||
set(CPACK_DEBIAN_PACKAGE_SECTION "games")
|
||||
set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional")
|
||||
set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS ON)
|
||||
@@ -814,15 +841,14 @@ set(CPACK_RPM_PACKAGE_LICENSE "Apache-2.0")
|
||||
set(CPACK_RPM_PACKAGE_GROUP "Amusements/Games")
|
||||
set(CPACK_RPM_PACKAGE_AUTOREQPROV ON)
|
||||
|
||||
if(MOCKTAIL_PACKAGE_NAME STREQUAL "mocktail-nightly")
|
||||
set(CPACK_DEBIAN_PACKAGE_CONFLICTS "mocktail")
|
||||
set(CPACK_DEBIAN_PACKAGE_REPLACES "mocktail")
|
||||
set(CPACK_RPM_PACKAGE_CONFLICTS "mocktail")
|
||||
else()
|
||||
set(CPACK_DEBIAN_PACKAGE_CONFLICTS "mocktail-nightly")
|
||||
set(CPACK_DEBIAN_PACKAGE_REPLACES "mocktail-nightly")
|
||||
set(CPACK_RPM_PACKAGE_CONFLICTS "mocktail-nightly")
|
||||
endif()
|
||||
# Every variant installs the same /usr/bin/mocktail and /usr/share/mocktail,
|
||||
# so only one of them can be present at a time.
|
||||
set(MOCKTAIL_PACKAGE_SIBLINGS ${MOCKTAIL_PACKAGE_NAMES})
|
||||
list(REMOVE_ITEM MOCKTAIL_PACKAGE_SIBLINGS "${MOCKTAIL_PACKAGE_NAME}")
|
||||
string(JOIN ", " MOCKTAIL_PACKAGE_CONFLICTS ${MOCKTAIL_PACKAGE_SIBLINGS})
|
||||
set(CPACK_DEBIAN_PACKAGE_CONFLICTS "${MOCKTAIL_PACKAGE_CONFLICTS}")
|
||||
set(CPACK_DEBIAN_PACKAGE_REPLACES "${MOCKTAIL_PACKAGE_CONFLICTS}")
|
||||
set(CPACK_RPM_PACKAGE_CONFLICTS "${MOCKTAIL_PACKAGE_CONFLICTS}")
|
||||
include(CPack)
|
||||
|
||||
add_dependencies(mocktail all_stubs)
|
||||
|
||||
@@ -1,40 +1,50 @@
|
||||
<!-- Modified by vii from komaruworld/mocktail. See README "About this fork". -->
|
||||
# FAQ
|
||||
|
||||
## How do I enable Discord RPC?
|
||||
## How do I turn Discord RPC off?
|
||||
|
||||
RPC is disabled by default. Launch Mocktail once to create the config, then
|
||||
close it. Open `config.yaml`:
|
||||
RPC is on by default. It never signs in to Discord and never reads an
|
||||
account token. To turn it off, launch Nightcap once to create the config,
|
||||
then close it. Open `config.yaml`:
|
||||
|
||||
- Native / AppImage: `~/.config/mocktail/config.yaml`
|
||||
- Flatpak: `~/.var/app/space.bigrat.mocktail/config/mocktail/config.yaml`
|
||||
- Flatpak: `~/.var/app/io.github.CoderDayton.nightcap/config/mocktail/config.yaml`
|
||||
|
||||
For native installs, a custom `$XDG_CONFIG_HOME` replaces `~/.config`.
|
||||
|
||||
Set `enabled` to `true` under `integrations.discord_rpc`. If the block is
|
||||
Set `enabled` to `false` under `integrations.discord_rpc`. If the block is
|
||||
missing, add it under the existing `integrations` section:
|
||||
|
||||
```yaml
|
||||
integrations:
|
||||
discord_rpc:
|
||||
enabled: true
|
||||
enabled: false
|
||||
```
|
||||
|
||||
Save the file, open Discord Desktop, and restart Mocktail. Set `enabled`
|
||||
back to `false` to disable RPC.
|
||||
Save the file and restart Nightcap. Set `enabled` back to `true` to turn
|
||||
RPC on again.
|
||||
|
||||
## Can Discord count Nightcap as Roblox play time or quests?
|
||||
|
||||
No. Discord only credits play time and quests to games its own scanner
|
||||
detects, and Rich Presence never counts. On Linux the scanner only matches
|
||||
games with a Linux entry in Discord's list. Roblox has none, so no process
|
||||
name Nightcap could use would be detected. Only Roblox or Discord can change
|
||||
that.
|
||||
|
||||
## Where can I find logs for a bug report?
|
||||
|
||||
Mocktail saves logs automatically. Reproduce the issue, close Mocktail, and
|
||||
Nightcap saves logs automatically. Reproduce the issue, close Nightcap, and
|
||||
attach `latest.log`:
|
||||
|
||||
- Native / AppImage: `~/.local/state/mocktail/logs/latest.log`
|
||||
- Flatpak: `~/.var/app/space.bigrat.mocktail/.local/state/mocktail/logs/latest.log`
|
||||
- Flatpak: `~/.var/app/io.github.CoderDayton.nightcap/.local/state/mocktail/logs/latest.log`
|
||||
|
||||
For native installs, a custom `$XDG_STATE_HOME` replaces `~/.local/state`.
|
||||
|
||||
## Can I run multiple Roblox instances?
|
||||
|
||||
No. Mocktail won't add multi-instance launching. It would mean working around
|
||||
No. Nightcap won't add multi-instance launching. It would mean working around
|
||||
Roblox's restrictions on running several clients. The [Roblox Terms of Use](https://en.help.roblox.com/hc/en-us/articles/115004647846-Roblox-Terms-of-Use)
|
||||
prohibit bypassing technical protections, and violating the terms can get your
|
||||
account suspended.
|
||||
@@ -46,12 +56,15 @@ after Roblox added measures against running multiple clients.
|
||||
|
||||
## Can I play in VR?
|
||||
|
||||
VR is experimental. Install the [build dependencies](README.md#building),
|
||||
then switch to the `vr` branch and build:
|
||||
VR is experimental and lives on upstream's `vr` branch, which does not
|
||||
include the Nightcap changes. Install the
|
||||
[build dependencies](README.md#building), then switch to that branch and
|
||||
build:
|
||||
|
||||
```bash
|
||||
git fetch origin
|
||||
git switch vr
|
||||
git remote add upstream https://github.com/komaruworld/mocktail.git
|
||||
git fetch upstream
|
||||
git switch -c vr upstream/vr
|
||||
git submodule update --init --recursive
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DMOCKTAIL_ENABLE_VR=ON
|
||||
cmake --build build -j4
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,8 +20,8 @@ ANYLINUX_PACKAGER ?= quick-sharun
|
||||
ANYLINUX_APPIMAGETOOL ?= appimagetool
|
||||
PREFIX ?= /usr
|
||||
FLATPAK_BUILD_DIR ?= build-flatpak
|
||||
FLATPAK_MANIFEST ?= packaging/flatpak/space.bigrat.mocktail.json
|
||||
FLATPAK_JOBS ?= 4
|
||||
FLATPAK_MANIFEST ?= packaging/flatpak/io.github.CoderDayton.nightcap.json
|
||||
FLATPAK_JOBS ?= 2
|
||||
|
||||
define build_native_runtime
|
||||
@git submodule update --init --recursive
|
||||
@@ -55,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)
|
||||
@@ -91,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,20 +1,90 @@
|
||||
# Mocktail
|
||||
<!-- Modified by vii from komaruworld/mocktail. See "About this fork" below. -->
|
||||
<div align="center">
|
||||
<img src="https://raw.githubusercontent.com/CoderDayton/nightcap/main/packaging/io.github.CoderDayton.nightcap.svg" alt="Nightcap logo" width="128">
|
||||
<h1 style="margin-top: 5px;">
|
||||
Nightcap
|
||||
<br>
|
||||
<a href="https://github.com/CoderDayton/nightcap/actions/workflows/ci.yml"><img src="https://github.com/CoderDayton/nightcap/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
||||
<a href="https://github.com/CoderDayton/nightcap/stargazers"><img src="https://img.shields.io/github/stars/CoderDayton/nightcap?style=flat&logo=github" alt="Stars"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue.svg" alt="License"></a>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
[](https://github.com/komaruworld/mocktail/actions/workflows/ci.yml)
|
||||
[](https://github.com/komaruworld/mocktail/stargazers)
|
||||
[](https://github.com/komaruworld/mocktail/releases/latest)
|
||||
[](LICENSE)
|
||||
[](https://discord.gg/rhgpfcmFSD)
|
||||
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 runs the Android `x86_64` Roblox client on Linux and has experimental
|
||||
`aarch64` builds for the Android `arm64-v8a` client. It also runs in a Linux
|
||||
userspace hosted by FreeBSD's Linuxulator. Mocktail 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`.
|
||||
A ready-made crosshair is in [`textures/`](textures/README.md). 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, the
|
||||
[changelog](CHANGELOG.md) for what each release changed,
|
||||
[performance](docs/PERFORMANCE.md) for CPU load, heat and the knobs that change
|
||||
them, and the [FAQ](FAQ.md) for Discord RPC, logs, and VR.
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## Distribution packages
|
||||
|
||||
Each release also carries a DEB, an RPM, and a pacman package, with a
|
||||
`.sha256` beside every file on the
|
||||
[releases page](https://github.com/CoderDayton/nightcap/releases/latest):
|
||||
|
||||
```bash
|
||||
sudo apt install ./nightcap_<version>_amd64.deb
|
||||
sudo dnf install ./nightcap-<version>-1.x86_64.rpm
|
||||
sudo pacman -U nightcap-<version>-1-x86_64.pkg.tar.zst
|
||||
```
|
||||
|
||||
They install `/usr/bin/mocktail`, so they conflict with upstream's `mocktail`
|
||||
package. Nightcap is not in any distribution repository or the AUR; the
|
||||
recipes under `packaging/aur` are kept for local builds only.
|
||||
|
||||
Or build and install from source using the steps under
|
||||
[Building](#building).
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
@@ -22,91 +92,30 @@ 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).
|
||||
A leftover `.previous_good-recovery.json` in the payload store marks pending
|
||||
recovery after a publication or restoration failure and blocks further payload
|
||||
publication and garbage collection. Check `current.json` and `previous_good.json`
|
||||
against the saved recovery state and fix the underlying problem before manually
|
||||
removing the marker.
|
||||
|
||||
<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
|
||||
@@ -118,21 +127,23 @@ caused by `Color3` errors in the newer SDUI page. An explicit
|
||||
"DFIntExample": "120"
|
||||
}
|
||||
```
|
||||
## FAQ
|
||||
## Settings
|
||||
|
||||
To see the most Frequently Asked Question go check [FAQ.md](FAQ.md)
|
||||
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. Linux `aarch64` builds are available, with Roblox
|
||||
runtime validation still experimental. FreeBSD 15.1 Linuxulator support is
|
||||
experimental and has been tested with an `x86_64` Fedora 44 userspace. On
|
||||
FreeBSD, Mocktail 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,
|
||||
Capstone 5, utf8proc, nlohmann/json, GTK4, libadwaita 1.6+, and WebKitGTK 6.0.
|
||||
Linux `x86_64` is supported. Experimental FreeBSD 15.1 Linuxulator support has
|
||||
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, libpng,
|
||||
minizip, Capstone 5, utf8proc, nlohmann/json, GTK4, libadwaita 1.6+, and
|
||||
WebKitGTK 6.0.
|
||||
|
||||
See the [FreeBSD Guide](packaging/freebsd/README.md) for setup and launch instructions.
|
||||
|
||||
@@ -143,10 +154,10 @@ See the [FreeBSD Guide](packaging/freebsd/README.md) for setup and launch instru
|
||||
sudo apt update
|
||||
sudo apt install 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 \
|
||||
nlohmann-json3-dev libyaml-dev libpng-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 \
|
||||
libplacebo-dev libpng-dev zlib1g-dev
|
||||
libplacebo-dev zlib1g-dev
|
||||
```
|
||||
</details>
|
||||
|
||||
@@ -155,7 +166,7 @@ sudo apt install build-essential cmake git ninja-build pkg-config lld \
|
||||
|
||||
```bash
|
||||
sudo pacman -S --needed base-devel cmake git ninja pkgconf lld sdl3 sdl3_ttf \
|
||||
curl openssl nlohmann-json libyaml libelf minizip capstone gtk4 \
|
||||
curl openssl nlohmann-json libyaml libpng libelf minizip capstone gtk4 \
|
||||
libadwaita webkitgtk-6.0 libutf8proc fontconfig libglvnd \
|
||||
libplacebo vulkan-headers vulkan-icd-loader zlib
|
||||
```
|
||||
@@ -167,7 +178,8 @@ sudo pacman -S --needed base-devel cmake git ninja pkgconf lld sdl3 sdl3_ttf \
|
||||
```bash
|
||||
sudo dnf install gcc-c++ cmake git ninja-build pkgconf-pkg-config lld \
|
||||
SDL3-devel SDL3_ttf-devel libcurl-devel openssl-devel \
|
||||
nlohmann-json-devel libyaml-devel elfutils-libelf-devel minizip-ng-compat-devel \
|
||||
nlohmann-json-devel libyaml-devel libpng-devel elfutils-libelf-devel \
|
||||
minizip-ng-compat-devel \
|
||||
capstone-devel gtk4-devel libadwaita-devel webkitgtk6.0-devel \
|
||||
utf8proc-devel fontconfig-devel libglvnd-devel vulkan-headers \
|
||||
vulkan-loader-devel libplacebo-devel zlib-ng-compat-devel
|
||||
@@ -175,17 +187,39 @@ 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 build and install the Flatpak from source, install `flatpak-builder` and run
|
||||
`make flatpak`. It builds with 2 parallel jobs to keep memory use low. Pass
|
||||
`FLATPAK_JOBS=N` to `make flatpak`, or set `MOCKTAIL_FLATPAK_JOBS=N` for
|
||||
`scripts/build_flatpak.sh`, to change that.
|
||||
|
||||
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
|
||||
|
||||
Mocktail does not currently work with `hardened_malloc`. Using
|
||||
`hardened_malloc` may cause Mocktail to fail to start or crash during runtime.
|
||||
Run Mocktail without `hardened_malloc` enabled.
|
||||
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
|
||||
|
||||
@@ -193,7 +227,12 @@ Run Mocktail without `hardened_malloc` enabled.
|
||||
|
||||
## 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`
|
||||
|
||||
Support the upstream project, which does most of the heavy lifting:
|
||||
|
||||
- USDT (TON): `UQCi6Yzcc9cOctoij6n_r1K90-OdVxAT0D_xo2UzGKkQaJDY`
|
||||
- USDT (TRC20): `TNPMG9Vig2xiuo2r1QqnXRChPH7Vu28Jmx`
|
||||
@@ -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.
|
||||
|
||||
@@ -12,7 +13,7 @@ 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}"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -46,7 +47,9 @@ graphics:
|
||||
# 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.
|
||||
# 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:
|
||||
@@ -74,6 +77,14 @@ audio:
|
||||
# between boots; prefer the exact device name when it is unique.
|
||||
input_device: default
|
||||
|
||||
input:
|
||||
# Boolean (default: false): send mouse look deltas in the host's own units.
|
||||
# They are otherwise scaled by the window's pixel density divided by the
|
||||
# desktop display scale, which cancels to 1.0 unless fractional scaling makes
|
||||
# the two disagree. Turn this on when first-person aiming feels too slow or
|
||||
# too fast under fractional scaling. The pointer position is unaffected.
|
||||
raw_mouse: false
|
||||
|
||||
integrations:
|
||||
fleasion:
|
||||
# Boolean (default: false): trust Fleasion's CA without editing Roblox files.
|
||||
@@ -86,9 +97,9 @@ integrations:
|
||||
# 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.
|
||||
# Boolean (default: true): publish Nightcap activity to Discord Desktop.
|
||||
# This never signs in to Discord and never reads an account token.
|
||||
enabled: false
|
||||
enabled: true
|
||||
# Boolean (default: true): show the current Roblox experience name.
|
||||
show_place_name: true
|
||||
# Boolean (default: true): show how long the current session has run.
|
||||
@@ -101,12 +112,24 @@ integrations:
|
||||
public_servers_only: true
|
||||
# button_label: Join Server
|
||||
# English defaults. Uncomment only the lines you want to customize.
|
||||
# Text and image fields accept {place_name}, {place_icon}, and
|
||||
# {creator_name}. A field that expands an empty placeholder is left out,
|
||||
# and a "" title shows the app name.
|
||||
# title, state, and the images fields may be "" to hide them.
|
||||
# text:
|
||||
# browsing: Browsing experiences
|
||||
# joining: Joining an experience
|
||||
# playing: "{place_name}"
|
||||
# state: Playing Roblox
|
||||
# playing: "Playing {place_name}"
|
||||
# state: "by {creator_name}"
|
||||
# unknown_place: Unknown experience
|
||||
# title: Roblox
|
||||
# Images take an https:// URL, or an asset key uploaded to application_id.
|
||||
# large: "" hides both images; the small image needs a large one.
|
||||
# images:
|
||||
# large: "{place_icon}"
|
||||
# large_text: "{place_name}"
|
||||
# small: roblox_small
|
||||
# small_text: Roblox
|
||||
# Advanced: overrides the application bundled by the package maintainer.
|
||||
# application_id: 123456789012345678
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# Benchmarking
|
||||
|
||||
A pull request that claims a speedup shows before and after numbers taken
|
||||
the way this guide describes. Numbers from different machines are not
|
||||
compared with each other. Each run is compared with a run on the same
|
||||
machine.
|
||||
|
||||
## Two layers, two tools
|
||||
|
||||
Roblox measures its own work. Nightcap measures the layer between Roblox and
|
||||
your driver.
|
||||
|
||||
| Layer | What it covers | Tool |
|
||||
| --- | --- | --- |
|
||||
| Roblox | Scripts, physics, rendering jobs, network | Roblox's stats overlays, Shift+F4 and Shift+F5 in game |
|
||||
| Nightcap | Vulkan adapter: present, swapchain and GPU waits, submits, ETC2 texture decode, pipeline and shader creation | `--profile` |
|
||||
|
||||
A Nightcap change should move the `--profile` numbers. If it only moves the
|
||||
Roblox overlay numbers, say so in the pull request, since the cause is then
|
||||
less direct.
|
||||
|
||||
## Recording a trace
|
||||
|
||||
```sh
|
||||
mocktail --profile ~/nightcap-before.json
|
||||
```
|
||||
|
||||
The Vulkan adapter writes a Chrome-format trace to that file, a few times a
|
||||
second. The file is valid JSON after every write, so a session that crashes
|
||||
or is killed still leaves a readable trace. Exit from the Roblox menu when you are done. Open the file
|
||||
in [ui.perfetto.dev](https://ui.perfetto.dev) or `chrome://tracing`.
|
||||
|
||||
Expect about 130 bytes per event, around 15 MB per minute at 240 FPS.
|
||||
Profiling is off unless `--profile` is given, and `MOCKTAIL_PROFILE_TRACE`
|
||||
does the same thing as the flag.
|
||||
|
||||
### What the trace contains
|
||||
|
||||
Each slice is named after the call it times, on the thread that made it.
|
||||
|
||||
| Category | Slices | Arguments |
|
||||
| --- | --- | --- |
|
||||
| `present` | `vkQueuePresentKHR`: the whole adapter present, including the text overlay | `result` |
|
||||
| `present` | `host present`: the host driver's present, nested inside the slice above | |
|
||||
| `wait` | `vkAcquireNextImageKHR`, `vkAcquireNextImage2KHR`, `vkWaitForFences`, `vkWaitSemaphores`, `vkWaitSemaphoresKHR`, `vkQueueWaitIdle`, `vkDeviceWaitIdle` | |
|
||||
| `submit` | `vkQueueSubmit`, `vkQueueSubmit2`, `vkQueueSubmit2KHR` | `submits` |
|
||||
| `texture` | `etc2 decode`: CPU decode of ETC2 textures. On a device with a timeline semaphore this runs on the decode dispatcher, not the submitting thread | `uploads`, `compressed_bytes`, `decoded_bytes` |
|
||||
| `texture` | `etc2 gather`: the part of an asynchronous upload that stays on the submitting thread. Absent when the decode ran inline | `uploads`, `compressed_bytes`, `decoded_bytes`, `ticket` |
|
||||
| `texture` | `etc2 staging create`: creating a staging block for ETC2 uploads, when no block has room left | `bytes` |
|
||||
| `texture` | `etc2 staging destroy`: destroying empty staging blocks beyond the four kept idle | `buffers` |
|
||||
| `texture` | `etc2 release wait`: a command buffer reset waiting for its uploads' decode. Only waits of 0.1 ms or more are recorded | `ticket` |
|
||||
| `memory` | `vkAllocateMemory` | `bytes`, `type` (memory type index), `result` |
|
||||
| `memory` | `vkFreeMemory`, `vkUnmapMemory`: both include waiting for ETC2 decodes that still read that memory | |
|
||||
| `memory` | `vkMapMemory`, `vkCreateImage`, `vkDestroyImage` | |
|
||||
| `pipeline` | `vkCreateGraphicsPipelines`, `vkCreateComputePipelines` | `count`, `cache` (1 when a pipeline cache was passed), `result` |
|
||||
| `pipeline` | `vkCreateShaderModule` | `bytes`, `result` |
|
||||
| `pipeline` | `vkCreatePipelineCache` | `initial_bytes`, `result` |
|
||||
| `pump` | `nativeCallMessagesFromMainThread`: the engine's main-thread step, on the host main thread. Only calls of 20 µs or more are recorded; the empty polls between them are not. | `cpu_us`: CPU time the main thread used during the call. Far below the slice's duration means the step spent it waiting |
|
||||
|
||||
The `frame interval (ms)` counter is the time between successive adapter
|
||||
presents. `vkQueuePresentKHR` time minus `host present` time is the adapter's
|
||||
own cost per frame.
|
||||
|
||||
Two more counters cover the same interval. `present thread cpu (ms)` is the
|
||||
CPU time the thread that presents used in it, and is absent for a frame
|
||||
presented from a different thread than the one before. `process cpu (ms)` is
|
||||
the CPU time of every thread together, so dividing it by the frame interval
|
||||
gives the number of cores busy. A long frame with little present-thread CPU
|
||||
was spent waiting.
|
||||
|
||||
## The benchmark runs
|
||||
|
||||
Run every scenario before and after the change, on the same build type
|
||||
(`make build`), with the same `config.yaml`. Close other GPU-heavy apps.
|
||||
Record `graphics.frame_rate_limit` and `graphics.vsync` in the pull request,
|
||||
because a frame cap hides frame time gains.
|
||||
|
||||
Record `~/.config/gamemode.ini` too, and do not change it between the before
|
||||
and after runs. `desiredgov` there decides the CPU governor for the whole
|
||||
session and therefore which engine pump mode is chosen, which moves process
|
||||
CPU by more than most changes under test. See
|
||||
[PERFORMANCE.md](PERFORMANCE.md).
|
||||
|
||||
Frame interval p99 varies by 2 ms or more between identical runs, so a
|
||||
difference that size is not a result. Three runs per scenario is the minimum
|
||||
for any frame-time claim; slice totals for a specific subsystem are far less
|
||||
noisy and can be read from fewer.
|
||||
|
||||
| Scenario | How to start | What to do | What it shows |
|
||||
| --- | --- | --- | --- |
|
||||
| Home screen, cold | `mocktail --profile home-cold.json` with an empty shader cache (below) | Wait 30 s on the home screen, exit | First-load stutter: `pipeline` and `texture` slices |
|
||||
| Tower of Hell | `mocktail --profile toh.json --launch-uri "roblox://experiences/start?placeId=1962086868"` | Stand still at spawn for 60 s, exit | Steady-state frame time on a light scene |
|
||||
| Murder Mystery 2 | `mocktail --profile mm2.json --launch-uri "roblox://experiences/start?placeId=142823291"` | Stay in the lobby for 60 s, exit | Frame time with many players and a busier scene |
|
||||
|
||||
For the cold run, point the driver's shader cache at an empty directory, so
|
||||
your normal cache is left alone:
|
||||
|
||||
```sh
|
||||
cache=$(mktemp -d)
|
||||
MESA_SHADER_CACHE_DIR="$cache" __GL_SHADER_DISK_CACHE_PATH="$cache" \
|
||||
mocktail --profile home-cold.json
|
||||
```
|
||||
|
||||
`MESA_SHADER_CACHE_DIR` applies to Mesa drivers (AMD, Intel) and
|
||||
`__GL_SHADER_DISK_CACHE_PATH` to the NVIDIA driver.
|
||||
|
||||
Run each scenario three times and keep the median run, since one run can
|
||||
catch a background stall.
|
||||
|
||||
## Comparing runs
|
||||
|
||||
```sh
|
||||
scripts/summarize_profile_trace.py toh-before.json toh-after.json
|
||||
```
|
||||
|
||||
This prints a Markdown table with one column per trace: frame count, frame
|
||||
interval mean, p50, p95, p99 and max, then count, total, mean, p99 and max
|
||||
for every slice name, largest total first. Paste the table into the pull
|
||||
request and attach both traces.
|
||||
|
||||
For frame time, compare p95 and p99 as well as the mean. Stutter shows up in
|
||||
the tail first. For first-load work, compare the `total (ms)` rows of the
|
||||
`pipeline` and `texture` slices from the cold run.
|
||||
@@ -0,0 +1,193 @@
|
||||
# Performance and CPU load
|
||||
|
||||
What Nightcap does to your CPU and GPU, which knobs change it, and how to
|
||||
measure rather than guess.
|
||||
|
||||
## GameMode core pinning
|
||||
|
||||
Nightcap asks Feral GameMode for a performance session at startup
|
||||
(`performance.gamemode`, default `auto`). GameMode raises the CPU governor and
|
||||
the process priority, which helps.
|
||||
|
||||
On a CPU with a wide spread of per-core maximum frequencies — Intel P-core and
|
||||
E-core parts, AMD parts with preferred cores — GameMode also pins the process
|
||||
to the fastest cores. On a 32-thread host that can mean 4 usable threads out of
|
||||
32. Roblox spreads its work across a worker pool, so the pin concentrates that
|
||||
pool onto a couple of physical cores instead of spreading it. Those cores sit
|
||||
near their boost ceiling and run hot while the rest of the chip idles.
|
||||
|
||||
GameMode re-applies the pin roughly every 5 seconds, so widening the mask from
|
||||
inside the process does not hold. Releasing the request does release the pin.
|
||||
|
||||
Under `auto`, Nightcap therefore checks its CPU affinity immediately after the
|
||||
request. If GameMode narrowed it, Nightcap ends the request and prints:
|
||||
|
||||
```
|
||||
[gamemode] released: it pinned this process to a subset of the CPUs, ...
|
||||
```
|
||||
|
||||
The trade is that GameMode's governor and priority boost go with it. To keep
|
||||
those and lose only the pinning, turn pinning off for every game on the host.
|
||||
|
||||
These are the settings to run with:
|
||||
|
||||
```ini
|
||||
# ~/.config/gamemode.ini
|
||||
[general]
|
||||
desiredgov=performance
|
||||
|
||||
[cpu]
|
||||
pin_cores=no
|
||||
park_cores=no
|
||||
```
|
||||
|
||||
`desiredgov` is the one to check first. The packaged default in
|
||||
`/usr/share/gamemode/gamemode.ini` is already `performance`, but a value in
|
||||
your own file replaces it, and `desiredgov=powersave` there means GameMode
|
||||
clocks the CPU **down** for the whole session while still reporting
|
||||
`[gamemode] performance request active`. The symptoms are a low CPU clock
|
||||
under load, a GPU that never leaves its lower SM clocks, and the engine pump
|
||||
falling back to `tpause` — which costs about 70% of a core instead of 3%,
|
||||
because `sleep` is only chosen when the governor reads `performance`. On one
|
||||
13900K host, correcting `powersave` to `performance` took the process from
|
||||
177% CPU to 42% and the package from 70°C to 53-63°C, at the same frame rate.
|
||||
|
||||
GameMode reads `$XDG_CONFIG_HOME`, `/usr/share/gamemode` and `/etc`, in that
|
||||
order. It does not read anything under Nightcap's own config directory, so this
|
||||
file has to live in one of those three places. The daemon reads it when it
|
||||
starts, so restart it after editing: `systemctl --user restart gamemoded`.
|
||||
|
||||
Restarting the daemon drops the session of any game already running; relaunch
|
||||
the game for new settings to apply to it.
|
||||
|
||||
`gamemoded` is D-Bus activated through
|
||||
`com.feralinteractive.GameMode.service`, so `systemctl --user stop gamemoded`
|
||||
does not hold — the next client request starts it again within seconds. To
|
||||
measure with GameMode genuinely off, mask it:
|
||||
|
||||
```bash
|
||||
systemctl --user mask gamemoded # and --unmask afterwards
|
||||
```
|
||||
|
||||
The governor switch runs through polkit. Debian and Ubuntu only allow it for
|
||||
members of the `gamemode` group; without that, the daemon's journal shows
|
||||
`cpugovctl set performance: Not authorized` and the governor stays put while
|
||||
the session is otherwise active. Add yourself and log in again:
|
||||
|
||||
```bash
|
||||
sudo usermod -aG gamemode "$USER"
|
||||
```
|
||||
|
||||
Fedora and Arch allow any logged-in local user.
|
||||
|
||||
Setting `performance.gamemode: on` accepts the pinning instead: `on` is an
|
||||
explicit request for GameMode, and Nightcap keeps the session whatever it does
|
||||
to the affinity. `off` never contacts the daemon.
|
||||
|
||||
To see which CPUs the process may use:
|
||||
|
||||
```bash
|
||||
pid=$(pgrep -x Main)
|
||||
for t in /proc/$pid/task/*; do taskset -pc "${t##*/}"; done
|
||||
```
|
||||
|
||||
Two traps here. The engine process reports its name as `Main`, not
|
||||
`mocktail`; `pgrep -x mocktail` finds only the launcher, which sits near 0%
|
||||
CPU and tells you nothing. And `taskset -p` reads one thread, not the whole
|
||||
process, so a single call reports the main thread's mask and says nothing
|
||||
about the workers — which is where the game's load actually is.
|
||||
|
||||
For the same reason, `ps -L -o psr=` shows which core each thread is running
|
||||
on right now, not which cores it is allowed on. On a hybrid CPU the scheduler
|
||||
preferring P-cores for runnable work is normal and is not a mask.
|
||||
|
||||
## The main-thread pump
|
||||
|
||||
Roblox posts its per-frame main-thread step at a time of its own choosing,
|
||||
and nothing signals the host when it does. The host main thread therefore
|
||||
polls `nativeCallMessagesFromMainThread`, and a post that waits more than a
|
||||
fraction of a millisecond costs a whole frame. Between polls the thread rests
|
||||
for 100 µs, and how it rests decides the CPU clock:
|
||||
|
||||
| Mode | When | What it costs |
|
||||
| --- | --- | --- |
|
||||
| `sleep` | The governor is `performance`, which GameMode sets | The thread idles at about 3% of a core. The governor holds the clock. |
|
||||
| `tpause` | Otherwise, on a CPU with WAITPKG (Intel 12th gen and newer) | The core stays awake in a light C0 state. Frame rate holds; the OS books the time as busy, so the thread shows around 70% of a core, at lower power than a spin. |
|
||||
| `spin` | Otherwise | The core stays awake at full power. |
|
||||
|
||||
Under a power-saving governor the CPU clocks a bursty core down unless some
|
||||
core stays awake, and Roblox's render thread then runs slower and drops
|
||||
frames. That is why `sleep` is not the default there. Startup prints the
|
||||
chosen mode:
|
||||
|
||||
```
|
||||
[main] engine pump rest: tpause (MOCKTAIL_ENGINE_PUMP_REST=sleep|tpause|spin)
|
||||
```
|
||||
|
||||
`MOCKTAIL_ENGINE_PUMP_REST` forces a mode. `sleep` without the governor's help
|
||||
runs cooler for a few frames per second less; measure before keeping it.
|
||||
|
||||
## Small texture upscaling
|
||||
|
||||
`MOCKTAIL_SMALL_TEXTURE_UPSCALE` (default `4`) redraws ETC2 textures of 64px or
|
||||
smaller at N times their width and height. At the default that is 16 times the
|
||||
pixels per affected texture.
|
||||
|
||||
The cost lands in two places. Decoding and resampling is CPU work on up to 8
|
||||
threads while textures load, which shows up as a burst rather than a steady
|
||||
load. The enlarged textures then stay in GPU memory for the session and cost
|
||||
extra sampling bandwidth on every frame that uses them.
|
||||
|
||||
That decode does not run on the thread that submits the frame. On a device
|
||||
with a timeline semaphore, uploads decode on a background dispatcher and the
|
||||
submit carries a semaphore wait, so the GPU waits for the texture instead of
|
||||
the render thread. In the trace this is the `etc2 decode` slice on its own
|
||||
thread, with only a negligible `etc2 gather` left on the submitting one. A
|
||||
device without that feature decodes inline as before.
|
||||
|
||||
Set it to `1` to turn upscaling off:
|
||||
|
||||
```bash
|
||||
MOCKTAIL_SMALL_TEXTURE_UPSCALE=1 mocktail
|
||||
```
|
||||
|
||||
## Frame rate and present mode
|
||||
|
||||
`graphics.frame_rate_limit` (default `-1`) leaves Roblox's own in-game
|
||||
framerate cap in charge; Nightcap sets no `DFIntTaskSchedulerTargetFps`
|
||||
override. A fixed value forwards that number to the scheduler.
|
||||
|
||||
`graphics.vsync` (default `auto`) selects the presentation mode. `auto` and
|
||||
`on` both pick the lowest-latency synchronized mode the driver offers, in
|
||||
order: `FIFO_LATEST_READY`, `MAILBOX`, `FIFO_RELAXED`, `FIFO`. The first two do
|
||||
not block the render thread on the display refresh, so the frame rate is
|
||||
governed by the in-game cap rather than by presentation. `off` selects
|
||||
`IMMEDIATE` and does not synchronize at all.
|
||||
|
||||
If the frame rate is higher than your refresh rate, the extra frames are work
|
||||
you cannot see. Lower the in-game cap to your refresh rate first.
|
||||
|
||||
## Measuring
|
||||
|
||||
Shift+F4 in game shows Roblox's own scheduler jobs, with a millisecond cost and
|
||||
a percentage for each. It does not show Nightcap's threads — the Vulkan
|
||||
adapter, the ETC2 decode workers, the libc shim — so it is a reading on the
|
||||
game, not on the process.
|
||||
|
||||
For the whole process:
|
||||
|
||||
```bash
|
||||
top -H -p $(pgrep -x mocktail) # per-thread CPU, sorted
|
||||
ps -L -o psr=,pcpu=,comm= -p $(pgrep -x mocktail) # which core each thread is on
|
||||
nvidia-smi --query-gpu=utilization.gpu,temperature.gpu,power.draw --format=csv
|
||||
```
|
||||
|
||||
A thread pinned near 100% while the frame rate is steady is a busy-wait. The
|
||||
one exception is the host main thread in `tpause` mode, which the OS reports
|
||||
as busy while the core is parked (see above). Load spread across many
|
||||
`RBX Worker` threads is the game's own scheduler, and its cost tracks the
|
||||
place you are in — part count, moving parts and joints all show in the
|
||||
Shift+F4 World section.
|
||||
|
||||
`--profile <file>` writes a Chrome trace of the Vulkan adapter's work for
|
||||
ui.perfetto.dev. See [BENCHMARKING.md](BENCHMARKING.md).
|
||||
@@ -0,0 +1,81 @@
|
||||
# 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: the `input.raw_mouse` toggle and controller options.
|
||||
- Advanced tab: FFlag overrides with a plain text editor for `fflags.json`.
|
||||
- Discord tab: every field in the `discord_rpc` block, so a friend can see
|
||||
what you are playing and you can switch it all off in one click.
|
||||
- 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 button glyphs
|
||||
|
||||
Upstream added Xbox and PlayStation gamepad support through SDL. Controllers
|
||||
already hot-plug and release their held inputs when unplugged. One rough edge
|
||||
is left.
|
||||
|
||||
- Correct button glyphs for the connected pad. `SDL_GetGamepadType` is only
|
||||
read for Xbox and PlayStation pads, so a Switch Pro, a GameCube pad, or any
|
||||
generic pad SDL reports as `SDL_GAMEPAD_TYPE_STANDARD` reaches Roblox as
|
||||
type 0 and draws generic glyphs. `SDL_GetRealGamepadType`, and the vendor
|
||||
and product IDs, are available and unused.
|
||||
|
||||
Done when a plugged-in pad shows its own buttons.
|
||||
|
||||
## Shipped
|
||||
|
||||
- Lower input latency: frames are presented as soon as they are ready.
|
||||
- Controllers hot-plug without a restart, and a pad that is unplugged mid-game
|
||||
releases its held buttons and sticks instead of leaving them stuck.
|
||||
- Mouse look keeps responding after a long turn, instead of going dead until
|
||||
the pointer travels back from past the screen edge.
|
||||
- Optional raw mouse input: `input.raw_mouse` in `config.yaml` sends look
|
||||
deltas in host units, so fractional display scaling no longer changes
|
||||
first-person sensitivity.
|
||||
- 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.
|
||||
- Discord Rich Presence on by default, with the experience name, creator,
|
||||
elapsed time, the experience icon with a Roblox badge, and a join button
|
||||
when the server is public. See the [FAQ](../FAQ.md) to switch it off.
|
||||
- Custom Discord presence: title, details, state, and large and small icons
|
||||
from `config.yaml`, with `{place_name}`, `{place_icon}`, and
|
||||
`{creator_name}` placeholders.
|
||||
- `--profile` writes a Chrome-format trace of the Vulkan adapter's work,
|
||||
including shader and pipeline creation, readable in `ui.perfetto.dev`.
|
||||
[BENCHMARKING.md](BENCHMARKING.md) fixes the experiences to measure and
|
||||
`scripts/summarize_profile_trace.py` builds the before and after table.
|
||||
- Texture uploads no longer stall the frame they land on: ETC2 decode and
|
||||
resampling run on a persistent worker pool.
|
||||
- Less stutter while joining an experience and while textures load in. See
|
||||
the [changelog](../CHANGELOG.md) for the numbers.
|
||||
- The host main thread rests between engine polls instead of spinning, and
|
||||
GameMode's core pinning is released, so Roblox's workers spread across
|
||||
every core. See [PERFORMANCE.md](PERFORMANCE.md).
|
||||
- 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.
|
||||
|
||||
@@ -49,6 +49,17 @@
|
||||
gdb
|
||||
];
|
||||
|
||||
desktopItems = [
|
||||
(pkgs.makeDesktopItem {
|
||||
name = "Mocktail";
|
||||
exec = "mocktail";
|
||||
icon = "mocktail";
|
||||
desktopName = "Mocktail";
|
||||
comment = "Experimental Roblox compatibility runtime for Linux and FreeBSD";
|
||||
categories = [ "Game" ];
|
||||
})
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.lib.makeLibraryPath package.buildInputs}"
|
||||
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
#ifndef MOCKTAIL_COMPAT_WEB_VIEW_PROTOCOL_CONTRACT_H_
|
||||
#define MOCKTAIL_COMPAT_WEB_VIEW_PROTOCOL_CONTRACT_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
|
||||
// Roblox's Subsystem<IWebViewProtocol> getter is not exported. The exported
|
||||
// WebViewProtocol.initializeAndroidWebViewProtocol calls it first, into a
|
||||
// stack slot, then reads the protocol from the returned holder:
|
||||
//
|
||||
// x86-64: lea slot(%rbp),%rbx; mov %rbx,%rdi; call getter;
|
||||
// mov (%rbx),%rbx; mov 0x18(%rbx),%rax
|
||||
// AArch64: add x8, sp, #slot; bl getter; ldr xN, [sp, #slot];
|
||||
// ldr x8, [xN, #0x18]
|
||||
//
|
||||
// These functions find that call site in the initializer's first bytes and
|
||||
// check the getter's own code. Only relocated or build-varying fields (stack
|
||||
// slots, frame sizes, PC-relative offsets, the call target) are masked.
|
||||
// Verified on x86_64 2.734.917, 2.736.1408 and 2.738.1397, and on arm64-v8a
|
||||
// 2.738.1397 and 2.740.931.
|
||||
|
||||
namespace mocktail::compat {
|
||||
|
||||
// Covers the call site in every verified build (x86-64 +0x27, AArch64 +0x34).
|
||||
inline constexpr std::size_t kWebViewProtocolInitializerScanBytes = 128;
|
||||
inline constexpr std::size_t kWebViewProtocolGetterBytesX86_64 = 44;
|
||||
inline constexpr std::size_t kWebViewProtocolGetterBytesArm64 = 60;
|
||||
|
||||
namespace web_view_protocol_contract_detail {
|
||||
|
||||
inline bool MatchesMasked(std::string_view code, std::size_t offset,
|
||||
std::string_view pattern,
|
||||
std::string_view wildcard) {
|
||||
if (offset > code.size() || code.size() - offset < pattern.size()) {
|
||||
return false;
|
||||
}
|
||||
for (std::size_t index = 0; index < pattern.size(); ++index) {
|
||||
if (wildcard[index] == 'x') continue;
|
||||
if (code[offset + index] != pattern[index]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// AArch64 instructions are little-endian words.
|
||||
inline std::uint32_t Word(std::string_view code, std::size_t index) {
|
||||
std::uint32_t word = 0;
|
||||
for (std::size_t byte = 0; byte < 4; ++byte) {
|
||||
word |= static_cast<std::uint32_t>(
|
||||
static_cast<unsigned char>(code[index * 4 + byte]))
|
||||
<< (8 * byte);
|
||||
}
|
||||
return word;
|
||||
}
|
||||
|
||||
// One AArch64 instruction word: bits outside `mask` may vary.
|
||||
struct MaskedWord {
|
||||
std::uint32_t mask;
|
||||
std::uint32_t value;
|
||||
};
|
||||
|
||||
inline constexpr std::uint32_t kExact = 0xffffffffU;
|
||||
// adrp Xd, page: immlo and immhi vary.
|
||||
inline constexpr std::uint32_t kAdrpMask = 0x9f00001fU;
|
||||
// add Xd, Xn, #imm12: imm12 varies.
|
||||
inline constexpr std::uint32_t kAddImmediateMask = 0xffc003ffU;
|
||||
|
||||
} // namespace web_view_protocol_contract_detail
|
||||
|
||||
// Returns the getter address called from the x86-64 initializer at
|
||||
// `address`, or nullopt unless the frame setup and exactly one call site
|
||||
// match.
|
||||
inline std::optional<std::uintptr_t> FindWebViewProtocolGetterCallX86_64(
|
||||
std::string_view code, std::uintptr_t address) {
|
||||
using web_view_protocol_contract_detail::MatchesMasked;
|
||||
// push %rbp; mov %rsp,%rbp
|
||||
if (!MatchesMasked(code, 0, "\x55\x48\x89\xe5", "....")) {
|
||||
return std::nullopt;
|
||||
}
|
||||
static constexpr std::string_view kCallSite(
|
||||
"\x48\x8d\x5d\x00\x48\x89\xdf\xe8\x00\x00\x00\x00\x48\x8b\x1b\x48\x8b\x43"
|
||||
"\x18",
|
||||
19);
|
||||
static constexpr std::string_view kCallSiteWildcard("...x....xxxx.......");
|
||||
static constexpr std::size_t kCallDisplacement = 8;
|
||||
const std::string_view window =
|
||||
code.substr(0, kWebViewProtocolInitializerScanBytes);
|
||||
std::optional<std::size_t> call_site;
|
||||
for (std::size_t offset = 0; offset < window.size(); ++offset) {
|
||||
if (MatchesMasked(window, offset, kCallSite, kCallSiteWildcard)) {
|
||||
if (call_site.has_value()) return std::nullopt;
|
||||
call_site = offset;
|
||||
}
|
||||
}
|
||||
if (!call_site.has_value()) return std::nullopt;
|
||||
std::int32_t displacement = 0;
|
||||
std::memcpy(&displacement, window.data() + *call_site + kCallDisplacement,
|
||||
sizeof(displacement));
|
||||
const std::uintptr_t next_instruction =
|
||||
address + *call_site + kCallDisplacement + sizeof(displacement);
|
||||
return next_instruction +
|
||||
static_cast<std::uintptr_t>(static_cast<std::intptr_t>(displacement));
|
||||
}
|
||||
|
||||
// The x86-64 getter stores the holder returned by the subsystem's acquire
|
||||
// into its result slot (%rdi) and returns that slot.
|
||||
inline bool HasWebViewProtocolGetterContractX86_64(std::string_view code) {
|
||||
return web_view_protocol_contract_detail::MatchesMasked(
|
||||
code, 0,
|
||||
std::string_view(
|
||||
"\x55\x48\x89\xe5\x53\x50\x48\x89\xfb\x8a\x05\x00\x00\x00\x00\x84\xc0"
|
||||
"\x74\x19\x48\x8d\x3d\x00\x00\x00\x00\xe8\x59\x00\x00\x00\x48\x89\x03"
|
||||
"\x48\x89\xd8\x48\x83\xc4\x08\x5b\x5d\xc3",
|
||||
kWebViewProtocolGetterBytesX86_64),
|
||||
"...........xxxx.......xxxx..................");
|
||||
}
|
||||
|
||||
// Returns the getter address called from the AArch64 initializer at
|
||||
// `address`, or nullopt unless exactly one call site matches.
|
||||
inline std::optional<std::uintptr_t> FindWebViewProtocolGetterCallArm64(
|
||||
std::string_view code, std::uintptr_t address) {
|
||||
using web_view_protocol_contract_detail::Word;
|
||||
const std::size_t words =
|
||||
code.substr(0, kWebViewProtocolInitializerScanBytes).size() / 4;
|
||||
std::optional<std::uintptr_t> getter;
|
||||
for (std::size_t index = 0; index + 4 <= words; ++index) {
|
||||
const std::uint32_t slot_address = Word(code, index);
|
||||
const std::uint32_t call = Word(code, index + 1);
|
||||
const std::uint32_t handle_load = Word(code, index + 2);
|
||||
const std::uint32_t protocol_load = Word(code, index + 3);
|
||||
// add x8, sp, #slot
|
||||
if ((slot_address & 0xffc003ffU) != 0x910003e8U) continue;
|
||||
// bl getter
|
||||
if ((call & 0xfc000000U) != 0x94000000U) continue;
|
||||
// ldr xN, [sp, #slot]
|
||||
if ((handle_load & 0xffc003e0U) != 0xf94003e0U) continue;
|
||||
const std::uint32_t slot = (slot_address >> 10) & 0xfffU;
|
||||
if (((handle_load >> 10) & 0xfffU) * 8 != slot) continue;
|
||||
// ldr x8, [xN, #0x18]
|
||||
const std::uint32_t handle_register = handle_load & 0x1fU;
|
||||
if (protocol_load != (0xf9400c08U | (handle_register << 5))) continue;
|
||||
if (getter.has_value()) return std::nullopt;
|
||||
// imm26 is a signed word offset from the bl instruction.
|
||||
const std::int64_t imm26 = call & 0x03ffffffU;
|
||||
const std::int64_t words_from_call =
|
||||
(imm26 & 0x02000000) != 0 ? imm26 - 0x04000000 : imm26;
|
||||
getter = address + (index + 1) * 4 +
|
||||
static_cast<std::uintptr_t>(words_from_call * 4);
|
||||
}
|
||||
return getter;
|
||||
}
|
||||
|
||||
// The AArch64 getter saves its result slot (x8) and stores the holder
|
||||
// returned by the subsystem's acquire there.
|
||||
inline bool HasWebViewProtocolGetterContractArm64(std::string_view code) {
|
||||
using namespace web_view_protocol_contract_detail;
|
||||
static constexpr MaskedWord kGetter[] = {
|
||||
{kExact, 0xa9be7bfdU}, // stp x29, x30, [sp, #-0x20]!
|
||||
{kExact, 0xf9000bf3U}, // str x19, [sp, #0x10]
|
||||
{kExact, 0x910003fdU}, // mov x29, sp
|
||||
{kAdrpMask, 0x90000009U}, // adrp x9, guard
|
||||
{kAddImmediateMask, 0x91000129U}, // add x9, x9, :lo12:guard
|
||||
{kExact, 0xaa0803f3U}, // mov x19, x8
|
||||
{kExact, 0x08dffd29U}, // ldarb w9, [x9]
|
||||
{kExact, 0x36000109U}, // tbz w9, #0, initialize
|
||||
{kAdrpMask, 0x90000000U}, // adrp x0, holder
|
||||
{kAddImmediateMask, 0x91000000U}, // add x0, x0, :lo12:holder
|
||||
{kExact, 0x94000017U}, // bl acquire
|
||||
{kExact, 0xf9000260U}, // str x0, [x19]
|
||||
{kExact, 0xf9400bf3U}, // ldr x19, [sp, #0x10]
|
||||
{kExact, 0xa8c27bfdU}, // ldp x29, x30, [sp], #0x20
|
||||
{kExact, 0xd65f03c0U}, // ret
|
||||
};
|
||||
static_assert(sizeof(kGetter) / sizeof(kGetter[0]) * 4 ==
|
||||
kWebViewProtocolGetterBytesArm64);
|
||||
if (code.size() < kWebViewProtocolGetterBytesArm64) return false;
|
||||
for (std::size_t index = 0; index < sizeof(kGetter) / sizeof(kGetter[0]);
|
||||
++index) {
|
||||
if ((Word(code, index) & kGetter[index].mask) != kGetter[index].value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace mocktail::compat
|
||||
|
||||
#endif // MOCKTAIL_COMPAT_WEB_VIEW_PROTOCOL_CONTRACT_H_
|
||||
@@ -16,6 +16,9 @@ struct AngleProbeOptions {
|
||||
bool allow_software_device = false;
|
||||
};
|
||||
|
||||
// Checks ANGLE entry points and client extensions without initializing a display.
|
||||
BackendCapability InspectAngleLibraries(const AngleProbeOptions& options);
|
||||
|
||||
// Loads a pinned ANGLE pair, verifies the required EGL/GLES symbols, and
|
||||
// initializes an EGL_PLATFORM_ANGLE Vulkan display. No context is fabricated.
|
||||
BackendCapability ProbeAngleVulkan(const AngleProbeOptions& options);
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
#ifndef MOCKTAIL_GRAPHICS_CHROME_TRACE_WRITER_H_
|
||||
#define MOCKTAIL_GRAPHICS_CHROME_TRACE_WRITER_H_
|
||||
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <filesystem>
|
||||
#include <initializer_list>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace mocktail {
|
||||
namespace graphics {
|
||||
|
||||
// Environment variable naming the trace file the Vulkan adapter writes.
|
||||
inline constexpr char kProfileTraceEnvironment[] = "MOCKTAIL_PROFILE_TRACE";
|
||||
|
||||
struct TraceArg {
|
||||
const char* key;
|
||||
std::int64_t value;
|
||||
};
|
||||
|
||||
// CLOCK_MONOTONIC in nanoseconds.
|
||||
std::uint64_t TraceClockNanos();
|
||||
// CPU time used so far by the calling thread, in nanoseconds.
|
||||
std::uint64_t TraceThreadCpuNanos();
|
||||
// CPU time used so far by every thread of the process, in nanoseconds.
|
||||
std::uint64_t TraceProcessCpuNanos();
|
||||
|
||||
// CPU time spent between two successive presents.
|
||||
struct FrameCpuSample {
|
||||
double process_ms = 0;
|
||||
// Set only when the previous present came from the same thread; thread CPU
|
||||
// clocks of different threads cannot be subtracted.
|
||||
bool has_thread = false;
|
||||
double thread_ms = 0;
|
||||
};
|
||||
|
||||
// Turns CPU clocks read at each present into per-frame CPU time.
|
||||
// Thread-safe.
|
||||
class FrameCpuSampler final {
|
||||
public:
|
||||
// Returns false for the first present, which ends no frame.
|
||||
bool Sample(std::uint64_t thread_id, std::uint64_t thread_cpu_ns,
|
||||
std::uint64_t process_cpu_ns, FrameCpuSample* sample);
|
||||
|
||||
private:
|
||||
std::mutex mutex_;
|
||||
bool has_previous_ = false;
|
||||
std::uint64_t previous_thread_id_ = 0;
|
||||
std::uint64_t previous_thread_cpu_ns_ = 0;
|
||||
std::uint64_t previous_process_cpu_ns_ = 0;
|
||||
};
|
||||
|
||||
// Writes the Trace Event Format JSON array read by chrome://tracing and
|
||||
// ui.perfetto.dev. Recording threads only format into a shared buffer; a
|
||||
// background thread owns file writes, so disk stalls do not land inside the
|
||||
// measured calls. Events are written in arrival order. Each write ends the
|
||||
// file with the closing bracket, so a killed session leaves a loadable trace
|
||||
// holding everything written before it died.
|
||||
// Names, categories, and keys must outlive the call.
|
||||
class ChromeTraceWriter final {
|
||||
public:
|
||||
static std::unique_ptr<ChromeTraceWriter> Open(
|
||||
const std::filesystem::path& path, std::string* error);
|
||||
|
||||
~ChromeTraceWriter();
|
||||
ChromeTraceWriter(const ChromeTraceWriter&) = delete;
|
||||
ChromeTraceWriter& operator=(const ChromeTraceWriter&) = delete;
|
||||
|
||||
// A complete ("X") slice on the calling thread. Slices on one thread must
|
||||
// nest, which scoped timing gives.
|
||||
void Slice(const char* name, const char* category, std::uint64_t start_ns,
|
||||
std::uint64_t end_ns, std::initializer_list<TraceArg> args = {});
|
||||
void Slice(const char* name, const char* category, std::uint64_t start_ns,
|
||||
std::uint64_t end_ns, const TraceArg* args,
|
||||
std::size_t arg_count);
|
||||
// A counter ("C") sample; each name becomes one counter track.
|
||||
void Counter(const char* name, std::uint64_t timestamp_ns, double value);
|
||||
|
||||
// Returns once every event recorded before the call is on disk.
|
||||
void Flush();
|
||||
// Flushes, closes the file, and drops later events. Idempotent.
|
||||
void Close();
|
||||
|
||||
private:
|
||||
explicit ChromeTraceWriter(std::FILE* file);
|
||||
void Append(const char* event, std::size_t size);
|
||||
void WriterLoop();
|
||||
void WritePending();
|
||||
|
||||
std::FILE* file_;
|
||||
const int owner_pid_;
|
||||
std::mutex file_mutex_; // Guards file_ and spare_.
|
||||
std::string spare_;
|
||||
std::mutex mutex_;
|
||||
std::condition_variable wake_;
|
||||
std::string pending_;
|
||||
bool first_event_ = true;
|
||||
bool closed_ = false;
|
||||
bool stopping_ = false;
|
||||
std::thread writer_;
|
||||
};
|
||||
|
||||
// The process trace named by MOCKTAIL_PROFILE_TRACE, opened on first use.
|
||||
// Null when profiling is off or the file could not be opened. The writer is
|
||||
// never destroyed; it is closed when the process exits normally.
|
||||
ChromeTraceWriter* ActiveProfileTrace();
|
||||
|
||||
// Times its own lifetime as one slice. A null writer records nothing.
|
||||
class TraceScope final {
|
||||
public:
|
||||
TraceScope(ChromeTraceWriter* writer, const char* name,
|
||||
const char* category)
|
||||
: writer_(writer),
|
||||
name_(name),
|
||||
category_(category),
|
||||
start_ns_(writer != nullptr ? TraceClockNanos() : 0) {}
|
||||
~TraceScope() {
|
||||
if (writer_ != nullptr) {
|
||||
writer_->Slice(name_, category_, start_ns_, TraceClockNanos(), args_,
|
||||
arg_count_);
|
||||
}
|
||||
}
|
||||
TraceScope(const TraceScope&) = delete;
|
||||
TraceScope& operator=(const TraceScope&) = delete;
|
||||
|
||||
// Keeps the first kMaxArgs arguments.
|
||||
void Arg(const char* key, std::int64_t value) {
|
||||
if (writer_ != nullptr && arg_count_ < kMaxArgs) {
|
||||
args_[arg_count_++] = {key, value};
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr std::size_t kMaxArgs = 4;
|
||||
ChromeTraceWriter* writer_;
|
||||
const char* name_;
|
||||
const char* category_;
|
||||
std::uint64_t start_ns_;
|
||||
TraceArg args_[kMaxArgs]{};
|
||||
std::size_t arg_count_ = 0;
|
||||
};
|
||||
|
||||
} // namespace graphics
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_CHROME_TRACE_WRITER_H_
|
||||
@@ -6,6 +6,9 @@
|
||||
|
||||
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,
|
||||
@@ -16,15 +19,26 @@ enum class EtcFormat {
|
||||
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,
|
||||
@@ -33,6 +47,7 @@ bool DecodeEtcImageBlockRows(EtcFormat format, const std::uint8_t* source,
|
||||
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;
|
||||
@@ -41,13 +56,24 @@ struct EtcDecodeJob {
|
||||
std::uint32_t height = 0;
|
||||
std::uint8_t* destination = nullptr;
|
||||
std::size_t destination_bytes = 0;
|
||||
// Set to DecodeEtcImage's result.
|
||||
bool ok = false;
|
||||
};
|
||||
|
||||
// Synchronous; job destination buffers must not overlap.
|
||||
// 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);
|
||||
|
||||
// Calls run(context) worker_count times, once on the caller and the rest on
|
||||
// pooled decode workers that run alongside it, and returns when every call
|
||||
// has returned. `run` must be safe to call from several threads at once, so
|
||||
// it should drain a shared queue. When the pool is busy with another batch,
|
||||
// or no worker could be started, run is called on the caller alone.
|
||||
void RunOnDecodeWorkers(void (*run)(void*), void* context,
|
||||
unsigned worker_count);
|
||||
|
||||
} // namespace mocktail::graphics
|
||||
|
||||
#endif // MOCKTAIL_GRAPHICS_ETC2_DECODER_H_
|
||||
@@ -12,32 +12,49 @@
|
||||
|
||||
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);
|
||||
void ResampleRgba(const std::uint8_t* source, std::uint32_t source_width,
|
||||
std::uint32_t source_height, std::size_t source_stride_bytes,
|
||||
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);
|
||||
|
||||
@@ -9,41 +9,84 @@
|
||||
|
||||
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);
|
||||
|
||||
// Staging for decoded uploads is carved out of host-visible blocks this
|
||||
// large; an upload that needs more gets a block of its own.
|
||||
inline constexpr VkDeviceSize kEtc2StagingBlockBytes = 16 * 1024 * 1024;
|
||||
|
||||
// A timeline semaphore and the value an asynchronous decode will signal.
|
||||
// `semaphore` is VK_NULL_HANDLE when the decode already finished on the
|
||||
// calling thread and the submit needs no extra wait.
|
||||
struct Etc2SubmitWait {
|
||||
VkSemaphore semaphore = VK_NULL_HANDLE;
|
||||
std::uint64_t value = 0;
|
||||
};
|
||||
|
||||
// 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 ranges live until their command buffer is begun, reset or freed.
|
||||
// MOCKTAIL_DISABLE_ETC2_EMULATION=1 turns the emulation off.
|
||||
class VulkanEtc2Emulation final {
|
||||
public:
|
||||
VulkanEtc2Emulation();
|
||||
explicit VulkanEtc2Emulation(
|
||||
VkDeviceSize staging_block_bytes = kEtc2StagingBlockBytes);
|
||||
~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);
|
||||
|
||||
// `timeline_semaphore` states whether the device was created with the
|
||||
// timelineSemaphore feature enabled. Without it every decode runs on the
|
||||
// calling thread.
|
||||
void RegisterDevice(VkDevice device, VkPhysicalDevice physical_device,
|
||||
bool emulated,
|
||||
const VkPhysicalDeviceMemoryProperties& memory,
|
||||
PFN_vkGetDeviceProcAddr get_device_proc_addr);
|
||||
PFN_vkGetDeviceProcAddr get_device_proc_addr,
|
||||
bool timeline_semaphore = false);
|
||||
void DestroyDevice(VkDevice device);
|
||||
|
||||
VkResult CreateImage(VkDevice device, const VkImageCreateInfo* create_info,
|
||||
@@ -85,10 +128,28 @@ class VulkanEtc2Emulation final {
|
||||
std::uint32_t count,
|
||||
const VkCommandBuffer* secondaries);
|
||||
|
||||
// Resubmission must reread the source bytes.
|
||||
void PrepareSubmit(const VkCommandBuffer* command_buffers,
|
||||
std::uint32_t count);
|
||||
// Call only after the command buffer is no longer pending.
|
||||
// Decodes every upload recorded into these command buffers and the
|
||||
// secondary command buffers they execute.
|
||||
//
|
||||
// With `allow_async` on a device that has a timeline semaphore, the decode
|
||||
// runs on worker threads, and so does the gather of the compressed bytes
|
||||
// whenever the application already has them mapped. The returned semaphore
|
||||
// and value must then be added to the submit's waits, so the GPU copy
|
||||
// cannot execute before the decode finishes. Values are signalled in the
|
||||
// order they are handed out.
|
||||
//
|
||||
// Those threads read the application's source bytes after this returns,
|
||||
// which Vulkan allows: the application may not write them between the
|
||||
// submit and the copy's completion. Unmapping or freeing that memory waits
|
||||
// for the decodes still reading it.
|
||||
//
|
||||
// Returns `{VK_NULL_HANDLE, 0}` when there was nothing to decode, when
|
||||
// `allow_async` is false, or when the device has no timeline semaphore. In
|
||||
// those cases every upload is already decoded when this returns.
|
||||
Etc2SubmitWait PrepareSubmit(const VkCommandBuffer* command_buffers,
|
||||
std::uint32_t count, bool allow_async = false);
|
||||
// Destroys the staging buffers owned by a command buffer that is no longer
|
||||
// pending, after waiting for any decode still writing into them.
|
||||
void ReleaseCommandBuffer(VkCommandBuffer command_buffer);
|
||||
|
||||
struct State;
|
||||
|
||||
@@ -152,6 +152,10 @@ struct MouseMotionEvent {
|
||||
float delta_x = 0.0f;
|
||||
float delta_y = 0.0f;
|
||||
std::uint32_t buttons = 0;
|
||||
// True while the window holds relative mouse mode. The absolute coordinates
|
||||
// are meaningless then: X11 reports zeroes, Wayland reports a position that
|
||||
// accumulates the deltas and can leave the window entirely.
|
||||
bool relative = false;
|
||||
};
|
||||
|
||||
struct MouseButtonEvent {
|
||||
@@ -160,6 +164,8 @@ struct MouseButtonEvent {
|
||||
std::uint8_t clicks = 0;
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
// See MouseMotionEvent::relative.
|
||||
bool relative = false;
|
||||
};
|
||||
|
||||
struct MouseWheelEvent {
|
||||
@@ -167,6 +173,8 @@ struct MouseWheelEvent {
|
||||
float delta_y = 0.0f;
|
||||
float mouse_x = 0.0f;
|
||||
float mouse_y = 0.0f;
|
||||
// See MouseMotionEvent::relative.
|
||||
bool relative = false;
|
||||
};
|
||||
|
||||
struct TouchEvent {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -33,6 +33,8 @@ struct CommandLineOptions {
|
||||
// Requests one explicit launch of the provider latest without promoting it
|
||||
// into the managed payload state.
|
||||
bool force_run_latest = false;
|
||||
// Chrome trace file the Vulkan adapter writes; empty when not profiling.
|
||||
std::string profile_trace_path;
|
||||
};
|
||||
|
||||
struct CommandLineParseResult {
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
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;
|
||||
@@ -15,6 +17,9 @@ struct DeviceMemoryProfile {
|
||||
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
|
||||
|
||||
@@ -13,11 +13,14 @@ namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
struct DiscordRpcActivity {
|
||||
std::string name;
|
||||
std::string details;
|
||||
std::string state;
|
||||
std::optional<int64_t> start_timestamp;
|
||||
std::string large_image;
|
||||
std::string large_text;
|
||||
std::string small_image;
|
||||
std::string small_text;
|
||||
std::string button_label;
|
||||
std::string button_url;
|
||||
};
|
||||
@@ -27,7 +30,8 @@ std::string BuildDiscordJoinUrl(const RobloxExperienceLaunchRequest& request);
|
||||
DiscordRpcActivity BuildDiscordRpcActivity(
|
||||
const DiscordRpcConfig& config, RobloxExperiencePresencePhase phase,
|
||||
const RobloxExperienceLaunchRequest* request, std::string place_name,
|
||||
int64_t session_started_at, std::string place_icon_url = {});
|
||||
int64_t session_started_at, std::string place_icon_url = {},
|
||||
std::string creator_name = {});
|
||||
|
||||
// Maintains a direct, unauthenticated Discord Desktop RPC connection. The
|
||||
// worker owns all network and IPC work; lifecycle notifications only replace
|
||||
|
||||
@@ -7,17 +7,22 @@
|
||||
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;
|
||||
};
|
||||
|
||||
// Physical pixels; window size is captured at startup.
|
||||
// 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) {
|
||||
@@ -55,6 +60,8 @@ inline DisplaySize ParseDisplaySize(const char* value) {
|
||||
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);
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_ENGINE_PUMP_REST_H_
|
||||
#define MOCKTAIL_RUNTIME_ENGINE_PUMP_REST_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace mocktail::runtime {
|
||||
|
||||
// The engine posts its per-frame main-thread step at a time of its own
|
||||
// choosing, with no signal the host can block on, so the host main thread
|
||||
// polls nativeCallMessagesFromMainThread. An empty poll costs a few
|
||||
// microseconds; polled back to back it burns a core.
|
||||
//
|
||||
// Between polls the thread rests for kEnginePumpRestNs. A post is then picked
|
||||
// up at most kEnginePumpRestNs + timer slack + wake latency late, far under
|
||||
// the engine's sub-millisecond slack.
|
||||
//
|
||||
// How it rests decides the clock. Under a power-saving governor the CPU clocks
|
||||
// a bursty core down unless some core stays awake, and the engine's render
|
||||
// thread then runs slower and drops frames. So:
|
||||
// kSleep nanosleep. The core idles. Correct only when the governor holds
|
||||
// the clock up on its own (performance, which GameMode sets).
|
||||
// kTpause TPAUSE in C0.2. The core stays awake at reduced power, the clock
|
||||
// holds, and the OS books the time as busy. Needs WAITPKG.
|
||||
// kSpin No rest. The core stays awake at full power.
|
||||
inline constexpr uint64_t kEnginePumpRestNs = 100'000;
|
||||
// Timer slack the calling thread is set to; the kernel default of 50 us would
|
||||
// otherwise stretch every sleep.
|
||||
inline constexpr uint64_t kEnginePumpTimerSlackNs = 10'000;
|
||||
// Shallow C-state exit plus scheduler latency on the host, for the bound.
|
||||
inline constexpr uint64_t kShallowWakeNs = 50'000;
|
||||
|
||||
enum class EnginePumpRestMode { kSleep, kTpause, kSpin };
|
||||
|
||||
const char* EnginePumpRestModeName(EnginePumpRestMode mode);
|
||||
|
||||
struct EnginePumpRestInputs {
|
||||
bool governor_performance = false;
|
||||
bool cpu_has_waitpkg = false;
|
||||
// MOCKTAIL_ENGINE_PUMP_REST: "sleep", "tpause" or "spin". Null, empty or
|
||||
// unknown defers to the policy; "tpause" without WAITPKG does too.
|
||||
const char* override = nullptr;
|
||||
};
|
||||
|
||||
EnginePumpRestMode ChooseEnginePumpRestMode(const EnginePumpRestInputs& inputs);
|
||||
|
||||
// True when the file holds "performance". `path` is a cpufreq
|
||||
// scaling_governor file; cpu0's stands for the machine.
|
||||
bool CpuGovernorIsPerformance(const char* path);
|
||||
|
||||
bool CpuHasWaitPkg();
|
||||
|
||||
// The mode chosen from the host on first use.
|
||||
EnginePumpRestMode ActiveEnginePumpRestMode();
|
||||
|
||||
// Rests the calling thread in the given mode and returns the rest length, 0
|
||||
// for kSpin. The first call lowers the thread's timer slack to
|
||||
// kEnginePumpTimerSlackNs.
|
||||
uint64_t RestAfterEnginePump(EnginePumpRestMode mode);
|
||||
inline uint64_t RestAfterEnginePump() {
|
||||
return RestAfterEnginePump(ActiveEnginePumpRestMode());
|
||||
}
|
||||
|
||||
} // namespace mocktail::runtime
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_ENGINE_PUMP_REST_H_
|
||||
@@ -1,6 +1,8 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_GAME_MODE_H_
|
||||
#define MOCKTAIL_RUNTIME_GAME_MODE_H_
|
||||
|
||||
#include <sched.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
@@ -22,6 +24,7 @@ enum class GameModeSessionState {
|
||||
kDisabled,
|
||||
kUnavailable,
|
||||
kRequestFailed,
|
||||
kDeclinedCorePinning,
|
||||
kAlreadyActive,
|
||||
kActive,
|
||||
kStopped,
|
||||
@@ -43,9 +46,37 @@ struct GameModeClientApi {
|
||||
ErrorFn error_string = nullptr;
|
||||
};
|
||||
|
||||
// The CPU affinity calls, injectable so tests exercise the restore without
|
||||
// moving the threads they run on.
|
||||
struct CpuAffinityApi {
|
||||
// Reads the calling thread's permitted CPUs. GameMode pins every thread
|
||||
// alike, so one thread's mask is enough to detect the pin.
|
||||
using GetFn = int (*)(cpu_set_t* mask);
|
||||
// Applies `mask` to every thread of this process, not only the caller.
|
||||
// Returns 0 when each live thread took it.
|
||||
using SetFn = int (*)(const cpu_set_t* mask);
|
||||
|
||||
GetFn get = nullptr;
|
||||
SetFn set = nullptr;
|
||||
};
|
||||
|
||||
CpuAffinityApi HostCpuAffinityApi();
|
||||
|
||||
// True when `after` permits a strict subset of the CPUs `before` permits.
|
||||
// An equal, wider or merely relocated set is not a narrowing.
|
||||
bool CpuAffinityNarrowed(const cpu_set_t& before, const cpu_set_t& after);
|
||||
|
||||
// Owns at most one ref-counted GameMode request. Missing host support and
|
||||
// daemon rejection remain nonfatal because GameMode is an optimization, not a
|
||||
// correctness dependency.
|
||||
//
|
||||
// GameMode pins the calling process to the host's fastest cores when it sees a
|
||||
// wide core-speed spread. Roblox spreads its own work over a worker pool, so
|
||||
// that pin concentrates the load onto a couple of cores instead of helping,
|
||||
// and the daemon re-applies it every few seconds, so correcting the mask in
|
||||
// place does not hold. Under `auto`, a request that narrows the affinity is
|
||||
// released again: ending the request is what releases the pin. `on` is an
|
||||
// explicit choice and keeps the pinned session.
|
||||
class GameModeSession final {
|
||||
public:
|
||||
GameModeSession() = default;
|
||||
@@ -59,6 +90,8 @@ class GameModeSession final {
|
||||
static GameModeSession Start(GameModePolicy policy);
|
||||
static GameModeSession StartWithClientForTesting(
|
||||
GameModePolicy policy, GameModeClientApi api);
|
||||
static GameModeSession StartWithClientForTesting(
|
||||
GameModePolicy policy, GameModeClientApi api, CpuAffinityApi affinity);
|
||||
|
||||
GameModeSessionState state() const { return state_; }
|
||||
const std::string& detail() const { return detail_; }
|
||||
@@ -73,6 +106,7 @@ class GameModeSession final {
|
||||
private:
|
||||
static GameModeSession StartBound(GameModePolicy policy,
|
||||
GameModeClientApi api,
|
||||
CpuAffinityApi affinity,
|
||||
void* library_handle);
|
||||
void CloseLibrary();
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
#ifndef MOCKTAIL_RUNTIME_PERFORMANCE_POLICY_H_
|
||||
#define MOCKTAIL_RUNTIME_PERFORMANCE_POLICY_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "runtime/frame_rate_policy.h"
|
||||
#include "runtime/game_mode.h"
|
||||
@@ -49,18 +51,18 @@ PerformancePolicy ParsePerformancePolicy(
|
||||
// Respects CPU affinity and falls back to the available logical count.
|
||||
int DetectAvailablePhysicalCoreCount();
|
||||
|
||||
// Disabled policy preserves overrides; conflicting values fail.
|
||||
bool MergePerformanceClientSettingsOverrides(const PerformancePolicy& policy,
|
||||
std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
// Disabled policy preserves overrides. A base_json value that differs from
|
||||
// the preset is kept as-is and its key is appended to kept_overrides.
|
||||
bool MergePerformanceClientSettingsOverrides(
|
||||
const PerformancePolicy& policy, std::string_view base_json,
|
||||
std::string* merged_json, std::string* error,
|
||||
std::vector<std::string>* kept_overrides = nullptr);
|
||||
|
||||
// Applies mandatory HTTP and crash policies after caller settings.
|
||||
bool MergeRuntimeClientSettingsOverrides(const FrameRatePolicy& frame_rate,
|
||||
const PerformancePolicy& performance,
|
||||
std::string_view base_json,
|
||||
std::string* merged_json,
|
||||
std::string* error);
|
||||
bool MergeRuntimeClientSettingsOverrides(
|
||||
const FrameRatePolicy& frame_rate, const PerformancePolicy& performance,
|
||||
std::string_view base_json, std::string* merged_json, std::string* error,
|
||||
std::vector<std::string>* kept_overrides = nullptr);
|
||||
|
||||
// Keeps the legacy test bypass disabled. The host PermissionsProtocol now
|
||||
// answers microphone authorization from capture configuration. No permission
|
||||
|
||||
@@ -47,8 +47,8 @@ struct RobloxThemeCacheResult {
|
||||
};
|
||||
|
||||
// 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.
|
||||
// missing or unrecognized value falls back to AuthenticatedTheme, then the
|
||||
// caller's startup default.
|
||||
RobloxThemeCacheResult ReadRobloxThemeCache(
|
||||
const std::filesystem::path& app_storage_file,
|
||||
std::int64_t authenticated_user_id);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_ACCOUNT_BRIDGE_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_ACCOUNT_BRIDGE_H_
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/roblox_game_session_native_adapter.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// Exact JNI entrypoints behind the APK AccountProtocol device-integrity
|
||||
// handlers: JNIAccountProtocol name getters and MessageBus raw handlers.
|
||||
struct RobloxAccountProtocolSymbols {
|
||||
using GetStringFn = jstring (*)(JNIEnv*, jclass);
|
||||
|
||||
GetStringFn get_protocol_name = nullptr;
|
||||
GetStringFn get_device_integrity_available_method_name = nullptr;
|
||||
GetStringFn get_integrity_token_method_name = nullptr;
|
||||
GetStringFn get_support_key = nullptr;
|
||||
GetStringFn get_token_key = nullptr;
|
||||
GetStringFn get_result_key = nullptr;
|
||||
void (*set_request_handler_raw)(JNIEnv*, jobject, jstring, jstring,
|
||||
jobject) = nullptr;
|
||||
void (*clear_request_handler)(JNIEnv*, jobject, jstring, jstring) = nullptr;
|
||||
|
||||
bool complete() const {
|
||||
return get_protocol_name && get_device_integrity_available_method_name &&
|
||||
get_integrity_token_method_name && get_support_key &&
|
||||
get_token_key && get_result_key && set_request_handler_raw &&
|
||||
clear_request_handler;
|
||||
}
|
||||
};
|
||||
|
||||
struct RobloxAccountProtocolObjects {
|
||||
jobject message_bus = nullptr;
|
||||
void* context = nullptr;
|
||||
jobject (*create_request_handler)(void*, std::shared_ptr<void>,
|
||||
std::string (*)(void*, JNIEnv*,
|
||||
jstring)) = nullptr;
|
||||
void (*clear_request_handler)(void*, jobject) = nullptr;
|
||||
|
||||
bool complete() const {
|
||||
return message_bus && context && create_request_handler &&
|
||||
clear_request_handler;
|
||||
}
|
||||
};
|
||||
|
||||
// Answers AccountProtocol device-integrity requests for a host that cannot
|
||||
// produce Play Integrity tokens. No integrity token is ever produced.
|
||||
//
|
||||
// DeviceIntegrityAvailable reports no support. The APK handler always reports
|
||||
// support, so this answer differs from Android: it tells Roblox's challenge
|
||||
// flow up front that no token can come, instead of letting it request one.
|
||||
//
|
||||
// GetIntegrityToken returns at once with an empty token and
|
||||
// TOKEN_PROVIDER_UNINITIALIZED, the APK's answer when its Play token provider
|
||||
// never initialized. The APK answers INVALID_NONCE for a request without a
|
||||
// requestHash; this bridge never inspects the request, so it cannot.
|
||||
// LuaApp bounds each token request with its
|
||||
// DeviceIntegrityNativeTimeoutMilliseconds flag; an immediate answer keeps a
|
||||
// login challenge from sitting out that bound.
|
||||
class RobloxAccountBridge final {
|
||||
public:
|
||||
RobloxAccountBridge(JniEnvironmentProvider environment,
|
||||
RobloxAccountProtocolSymbols symbols,
|
||||
RobloxAccountProtocolObjects objects);
|
||||
~RobloxAccountBridge();
|
||||
RobloxAccountBridge(const RobloxAccountBridge&) = delete;
|
||||
RobloxAccountBridge& operator=(const RobloxAccountBridge&) = delete;
|
||||
|
||||
Status Initialize();
|
||||
Status Shutdown();
|
||||
|
||||
private:
|
||||
struct State;
|
||||
Status ShutdownLocked();
|
||||
|
||||
const JniEnvironmentProvider environment_;
|
||||
const RobloxAccountProtocolSymbols symbols_;
|
||||
const RobloxAccountProtocolObjects objects_;
|
||||
// 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_ACCOUNT_BRIDGE_H_
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_CALL_PROTOCOL_BRIDGE_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_CALL_PROTOCOL_BRIDGE_H_
|
||||
|
||||
#include "runtime/roblox_message_bus_request_bridge.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
// In-experience CoreScripts query the app's cross-experience call state before
|
||||
// initializing voice. Mocktail does not start or accept cross-experience calls,
|
||||
// so that state is Idle. This query neither joins voice nor opens a microphone.
|
||||
class RobloxCallProtocolBridge final {
|
||||
public:
|
||||
RobloxCallProtocolBridge(JniEnvironmentProvider environment,
|
||||
RobloxMessageBusSymbols symbols,
|
||||
RobloxMessageBusObjects objects);
|
||||
Status Initialize();
|
||||
Status Shutdown();
|
||||
|
||||
private:
|
||||
RobloxMessageBusRequestBridge bridge_;
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
#endif
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
@@ -14,9 +15,11 @@
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/owned_pthread.h"
|
||||
#include "runtime/roblox_browser_service_bridge.h"
|
||||
#include "runtime/roblox_call_protocol_bridge.h"
|
||||
#include "runtime/roblox_experience_launch_bridge.h"
|
||||
#include "runtime/roblox_experience_presence.h"
|
||||
#include "runtime/roblox_fresh_game_launch_controller.h"
|
||||
#include "runtime/roblox_account_bridge.h"
|
||||
#include "runtime/roblox_permissions_bridge.h"
|
||||
#include "runtime/roblox_web_view_bridge.h"
|
||||
#include "runtime/webview_roblox_cookie.h"
|
||||
@@ -95,7 +98,9 @@ struct RobloxExperienceSurfaceProvider {
|
||||
void* context = nullptr;
|
||||
GameSurface (*snapshot)(void* context) = nullptr;
|
||||
|
||||
bool valid() const { return snapshot != nullptr; }
|
||||
bool valid() const {
|
||||
return snapshot != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
// Production composition for the APK platform protocols and ExperienceProtocol
|
||||
@@ -120,7 +125,8 @@ class RobloxExperienceComposition final {
|
||||
RobloxExperienceSurfaceProvider surface_provider = {},
|
||||
RobloxExperiencePresenceObserver presence_observer = {},
|
||||
bool clear_persisted_web_view_cookie = false,
|
||||
bool microphone_enabled = false);
|
||||
bool microphone_enabled = false,
|
||||
RobloxAccountProtocolSymbols account_symbols = {});
|
||||
~RobloxExperienceComposition();
|
||||
|
||||
RobloxExperienceComposition(const RobloxExperienceComposition&) = delete;
|
||||
@@ -162,6 +168,7 @@ class RobloxExperienceComposition final {
|
||||
bool visible = true;
|
||||
bool back_navigation_disabled = false;
|
||||
bool show_domain_as_title = false;
|
||||
bool close_when_challenge_solved = false;
|
||||
};
|
||||
|
||||
static Status DispatchLaunch(void* context,
|
||||
@@ -201,8 +208,11 @@ class RobloxExperienceComposition final {
|
||||
const WebSurfacePresentation& presentation);
|
||||
// WebViewProtocol and BrowserService are two logical transports over the
|
||||
// APK's single physical WebView container. Either protocol may close or
|
||||
// mutate the surface that the other one opened.
|
||||
Status CloseWebSurface();
|
||||
// mutate the surface that the other one opened. A nonzero
|
||||
// expected_logical_generation closes only that surface.
|
||||
Status CloseWebSurface(uint64_t expected_logical_generation = 0);
|
||||
// Closes a solved challenge that Roblox left open past its grace period.
|
||||
Status CloseUnclaimedSolvedChallenge();
|
||||
void HandleWebSurfaceExit(uint64_t process_generation);
|
||||
Status Dispatch(const RobloxExperienceLaunchRequest& request);
|
||||
Status PromoteAuthenticatedSession();
|
||||
@@ -229,6 +239,7 @@ class RobloxExperienceComposition final {
|
||||
const RobloxWebViewMessageBusSymbols web_view_symbols_;
|
||||
const RobloxBrowserServiceSymbols browser_service_symbols_;
|
||||
const RobloxPermissionsMessageBusSymbols permissions_symbols_;
|
||||
const RobloxAccountProtocolSymbols account_symbols_;
|
||||
const bool microphone_enabled_;
|
||||
const RobloxGameSessionSymbols game_symbols_;
|
||||
const RobloxExperienceJniFactory jni_factory_;
|
||||
@@ -248,6 +259,9 @@ class RobloxExperienceComposition final {
|
||||
std::unique_ptr<RobloxWebViewBridge> web_view_bridge_;
|
||||
std::unique_ptr<RobloxBrowserServiceBridge> browser_service_bridge_;
|
||||
std::unique_ptr<RobloxPermissionsBridge> permissions_bridge_;
|
||||
std::unique_ptr<RobloxCallProtocolBridge> call_protocol_bridge_;
|
||||
// Optional: a payload without the AccountProtocol entrypoints still starts.
|
||||
std::unique_ptr<RobloxAccountBridge> account_bridge_;
|
||||
std::shared_ptr<WebViewHelperProcess> web_surface_process_;
|
||||
std::shared_ptr<WebSurfaceExitTarget> web_surface_exit_target_;
|
||||
std::shared_ptr<LifecycleTarget> lifecycle_target_;
|
||||
@@ -257,6 +271,13 @@ class RobloxExperienceComposition final {
|
||||
uint64_t web_surface_logical_generation_ = 0;
|
||||
uint64_t next_web_surface_process_generation_ = 1;
|
||||
uint64_t next_web_surface_logical_generation_ = 1;
|
||||
// The current surface closes as soon as its solved challenge is forwarded.
|
||||
bool web_surface_close_when_challenge_solved_ = false;
|
||||
// Set when a solved challenge is forwarded; applies only to that logical
|
||||
// surface generation.
|
||||
std::optional<std::chrono::steady_clock::time_point>
|
||||
solved_challenge_close_deadline_;
|
||||
uint64_t solved_challenge_logical_generation_ = 0;
|
||||
SecureWebViewRobloxCookie web_view_cookie_;
|
||||
std::string web_view_cookie_initialization_error_;
|
||||
bool web_view_cookie_synchronized_ = false;
|
||||
|
||||
@@ -123,6 +123,7 @@ class RobloxInputRuntime final {
|
||||
RobloxInputSnapshot Snapshot() const;
|
||||
Status GetMainWindowIsMouseLockedCenter(bool* locked_center);
|
||||
bool SupportsGamepads() const;
|
||||
void SetRawMouseEnabled(bool enabled);
|
||||
|
||||
static void PlatformEventCallback(void* context,
|
||||
const platform::PlatformEvent& event);
|
||||
|
||||
@@ -40,9 +40,10 @@ struct AndroidKeyMapping {
|
||||
// through with an ABI-incompatible numeric value.
|
||||
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);
|
||||
// Translates SDL button numbers to the nativePassMouseButton argument.
|
||||
// The first three are Roblox's zero-based MouseButton1/2/3 indices, not Android
|
||||
// MotionEvent button bits.
|
||||
int32_t MapSdlMouseButtonToRoblox(uint8_t sdl_button);
|
||||
|
||||
int32_t MapSdlGamepadButtonToAndroid(uint8_t sdl_button);
|
||||
int32_t RobloxGamepadType(platform::GamepadFamily family);
|
||||
@@ -171,6 +172,9 @@ class RobloxInputRouter final {
|
||||
const RobloxTextFocusProperties& properties);
|
||||
RobloxInputDispatchResult HandleEvent(const platform::PlatformEvent& event);
|
||||
RobloxInputSnapshot Snapshot() const;
|
||||
// Raw mouse sends look deltas in host units instead of guest surface units.
|
||||
// The reported pointer position stays in guest units either way.
|
||||
void SetRawMouseEnabled(bool enabled);
|
||||
|
||||
private:
|
||||
struct ActiveTouch {
|
||||
@@ -220,6 +224,10 @@ class RobloxInputRouter final {
|
||||
Status DisconnectGamepadLocked(ActiveGamepad& gamepad);
|
||||
Status DisconnectGamepadsLocked();
|
||||
Status ReleasePressedInputsLocked();
|
||||
// Keeps mouse_x_/mouse_y_ where they are for an event that carries no
|
||||
// usable position, clamped to the current viewport. Before any absolute
|
||||
// position has been seen, the held position is the viewport center.
|
||||
void HoldPointerLocked(const platform::SurfaceCoordinateTransform& transform);
|
||||
int32_t AllocatePointerIdLocked() const;
|
||||
std::vector<ActiveTouch>::iterator FindTouchIteratorLocked(int64_t touch_id,
|
||||
int64_t finger_id);
|
||||
@@ -235,8 +243,14 @@ class RobloxInputRouter final {
|
||||
std::vector<ActiveTouch> active_touches_;
|
||||
std::vector<ActiveKey> active_keys_;
|
||||
std::vector<ActiveGamepad> gamepads_;
|
||||
// Last known pointer position in guest units. Relative-mode motion, and
|
||||
// wheel or button events with zero coordinates, do not move it: the pointer
|
||||
// stays where it was captured while the deltas pass through.
|
||||
float mouse_x_ = 0.0f;
|
||||
float mouse_y_ = 0.0f;
|
||||
// True once an absolute position has been received since Activate.
|
||||
bool pointer_anchored_ = false;
|
||||
bool raw_mouse_ = false;
|
||||
};
|
||||
|
||||
const char* RobloxInputDispatchStateName(RobloxInputDispatchState state);
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_MESSAGE_BUS_REQUEST_BRIDGE_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_MESSAGE_BUS_REQUEST_BRIDGE_H_
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/roblox_game_session_native_adapter.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
struct RobloxMessageBusSymbols {
|
||||
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 RobloxMessageBusObjects {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
struct RobloxMessageBusReply {
|
||||
std::string json;
|
||||
jint code = 0;
|
||||
const char* outcome = "ok";
|
||||
};
|
||||
|
||||
// Owns both legacy subscriptions and correlated async request handlers.
|
||||
// A policy receives only a request's method index and JNI-local payload.
|
||||
class RobloxMessageBusRequestBridge final {
|
||||
public:
|
||||
using Handler =
|
||||
std::function<RobloxMessageBusReply(JNIEnv*, jstring, std::size_t)>;
|
||||
RobloxMessageBusRequestBridge(JniEnvironmentProvider environment,
|
||||
RobloxMessageBusSymbols symbols,
|
||||
RobloxMessageBusObjects objects,
|
||||
std::string protocol,
|
||||
std::vector<std::string> methods,
|
||||
Handler handler);
|
||||
~RobloxMessageBusRequestBridge();
|
||||
Status Initialize();
|
||||
Status Shutdown();
|
||||
|
||||
private:
|
||||
struct State;
|
||||
Status ShutdownLocked();
|
||||
const JniEnvironmentProvider environment_;
|
||||
const RobloxMessageBusSymbols symbols_;
|
||||
const RobloxMessageBusObjects objects_;
|
||||
const std::string protocol_name_;
|
||||
const std::vector<std::string> methods_;
|
||||
const Handler handler_;
|
||||
std::mutex lifecycle_mutex_;
|
||||
std::shared_ptr<State> state_;
|
||||
};
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
#endif
|
||||
@@ -1,58 +1,12 @@
|
||||
#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"
|
||||
#include "runtime/roblox_message_bus_request_bridge.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
using RobloxPermissionsMessageBusSymbols = RobloxMessageBusSymbols;
|
||||
using RobloxPermissionsMessageBusObjects = RobloxMessageBusObjects;
|
||||
|
||||
// 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,
|
||||
@@ -60,26 +14,12 @@ class RobloxPermissionsBridge final {
|
||||
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_;
|
||||
std::unique_ptr<RobloxMessageBusRequestBridge> bridge_;
|
||||
};
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_ROBLOX_PERMISSIONS_BRIDGE_H_
|
||||
#endif
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_PLATFORM_WEB_SYMBOLS_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_PLATFORM_WEB_SYMBOLS_H_
|
||||
|
||||
#include "runtime/roblox_account_bridge.h"
|
||||
#include "runtime/roblox_browser_service_bridge.h"
|
||||
#include "runtime/roblox_permissions_bridge.h"
|
||||
#include "runtime/roblox_web_view_bridge.h"
|
||||
@@ -15,6 +16,8 @@ struct RobloxPlatformWebSymbols {
|
||||
RobloxWebViewMessageBusSymbols web_view;
|
||||
RobloxBrowserServiceSymbols browser_service;
|
||||
RobloxPermissionsMessageBusSymbols permissions;
|
||||
// Optional and excluded from complete(); see RobloxExperienceComposition.
|
||||
RobloxAccountProtocolSymbols account;
|
||||
|
||||
bool complete() const {
|
||||
return web_view.complete() && browser_service.complete() &&
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef MOCKTAIL_RUNTIME_ROBLOX_PRESENCE_RESOLVER_H_
|
||||
#define MOCKTAIL_RUNTIME_ROBLOX_PRESENCE_RESOLVER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
struct ResolvedFollowUserPlace {
|
||||
int64_t place_id = 0;
|
||||
std::string game_instance_id; // Roblox's "gameId" for the running session.
|
||||
};
|
||||
|
||||
// Reads the session cookie mocktail already persists at
|
||||
// ~/.local/share/mocktail/auth/roblox.cookie (or $XDG_DATA_HOME
|
||||
// equivalent). Returns an empty string if no session is stored.
|
||||
std::string ReadStoredRoblosecurityCookie();
|
||||
|
||||
// Calls Roblox's presence API to find what place/instance a user is
|
||||
// currently in. `auth_cookie` is the runtime's already-authenticated
|
||||
// .ROBLOSECURITY (or equivalent bearer token) — this function performs no
|
||||
// authentication of its own and requires the runtime's validated session.
|
||||
//
|
||||
// Returns an error if the user is offline, has presence hidden, or is not
|
||||
// in a joinable experience (e.g. in Studio, or privacy settings block it).
|
||||
Status ResolveFollowUserPlace(int64_t user_id, const std::string& auth_cookie,
|
||||
ResolvedFollowUserPlace* out);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
#endif // MOCKTAIL_RUNTIME_ROBLOX_PRESENCE_RESOLVER_H_
|
||||
@@ -11,6 +11,8 @@
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/roblox_text_display_state.h"
|
||||
|
||||
struct TTF_Font;
|
||||
|
||||
namespace mocktail {
|
||||
namespace runtime {
|
||||
|
||||
@@ -39,6 +41,8 @@ class RobloxTextSurfaceOverlay final {
|
||||
const RobloxTextDisplayUpdate& update);
|
||||
void ApplyUpdate(const RobloxTextDisplayUpdate& update);
|
||||
Status RasterizeLocked();
|
||||
Status OpenFontsLocked(std::int32_t font, float point_size);
|
||||
void CloseFontsLocked();
|
||||
void ClearFrameLocked();
|
||||
void RecordFailureLocked(Status status);
|
||||
|
||||
@@ -46,10 +50,14 @@ class RobloxTextSurfaceOverlay final {
|
||||
RobloxTextDisplayState state_;
|
||||
RobloxTextOverlayViewport viewport_;
|
||||
std::vector<std::uint8_t> rgba_;
|
||||
std::vector<TTF_Font*> fonts_;
|
||||
std::int32_t fonts_font_ = 0;
|
||||
float fonts_point_size_ = 0.0F;
|
||||
std::uint64_t state_revision_ = 0;
|
||||
std::uint64_t raster_revision_ = 0;
|
||||
Status failure_;
|
||||
bool initialized_ = false;
|
||||
bool ttf_initialized_ = false;
|
||||
bool failure_logged_ = false;
|
||||
bool ready_logged_ = false;
|
||||
};
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
#include "mocktail/status.h"
|
||||
#include "runtime/roblox_game_session_native_adapter.h"
|
||||
@@ -42,6 +44,10 @@ struct RobloxWebViewOpenRequest {
|
||||
std::optional<bool> show_domain_as_title;
|
||||
std::optional<bool> back_button_visible;
|
||||
std::optional<bool> hide_header;
|
||||
// Set by the bridge, never parsed. The host closes an OPEN_CAPTCHA_VIEW
|
||||
// window once its solved captcha has been forwarded, as Android's captcha
|
||||
// activity finishes itself on success.
|
||||
bool close_when_challenge_solved = false;
|
||||
};
|
||||
|
||||
// WebViewProtocol.mutateWindow is a partial update. Field absence must remain
|
||||
@@ -61,6 +67,9 @@ Status ParseRobloxWebViewMutationJson(const std::string& json,
|
||||
RobloxWebViewMutationRequest* request);
|
||||
Status ParseRobloxCaptchaOpenJson(const std::string& json,
|
||||
RobloxWebViewOpenRequest* request);
|
||||
// True for a page's Navigation.navigateToFeature command reporting a solved
|
||||
// verification (see webview::IsRobloxChallengeSolvedFeature).
|
||||
bool IsRobloxWebViewChallengeSuccess(std::string_view command);
|
||||
Status ParseRobloxProfileViewUrl(const std::string& url,
|
||||
RobloxWebViewOpenRequest* request);
|
||||
Status ParseRobloxPurchaseRobuxNotification(const std::string& json,
|
||||
@@ -108,6 +117,33 @@ using BroadcastWebViewDataModelFocusFn = void (*)(JNIEnv *, jclass, jstring,
|
||||
using SignalWebViewJavascriptCallbackFn = void (*)(JNIEnv*, jclass, jstring);
|
||||
using UpdateRobloxCookieSetHandlerFn =
|
||||
void (*)(JNIEnv*, jobject, jobject);
|
||||
// Roblox's counted handle to its Subsystem<IWebViewProtocol>. The protocol,
|
||||
// and its handleJavascriptCallback subscriber, exist only while a handle is
|
||||
// held. Roblox's handle type is not trivially destructible, so its getter
|
||||
// returns it through the caller-supplied result slot (rdi on x86-64, x8 on
|
||||
// AArch64). The user-provided destructor gives this type the same calling
|
||||
// convention. It does not release the handle.
|
||||
class RobloxWebViewProtocolHandle final {
|
||||
public:
|
||||
RobloxWebViewProtocolHandle() = default;
|
||||
explicit RobloxWebViewProtocolHandle(void* holder) : holder_(holder) {}
|
||||
RobloxWebViewProtocolHandle(const RobloxWebViewProtocolHandle&) = default;
|
||||
RobloxWebViewProtocolHandle& operator=(const RobloxWebViewProtocolHandle&) =
|
||||
default;
|
||||
~RobloxWebViewProtocolHandle() {}
|
||||
|
||||
void* holder() const { return holder_; }
|
||||
|
||||
private:
|
||||
void* holder_ = nullptr;
|
||||
};
|
||||
static_assert(sizeof(RobloxWebViewProtocolHandle) == sizeof(void*));
|
||||
static_assert(!std::is_trivially_destructible_v<RobloxWebViewProtocolHandle>);
|
||||
|
||||
// Roblox's Subsystem<IWebViewProtocol> getter. It is not exported; the
|
||||
// resolver finds it through the first call in
|
||||
// WebViewProtocol.initializeAndroidWebViewProtocol.
|
||||
using AcquireWebViewProtocolFn = RobloxWebViewProtocolHandle (*)();
|
||||
|
||||
struct RobloxWebViewMessageBusSymbols {
|
||||
GetWebViewOpenWindowIdFn get_open_window_id = nullptr;
|
||||
@@ -129,6 +165,8 @@ struct RobloxWebViewMessageBusSymbols {
|
||||
GetWebViewCloseWindowIdFn get_close_window_id = nullptr;
|
||||
SignalWebViewJavascriptCallbackFn signal_javascript_callback = nullptr;
|
||||
UpdateRobloxCookieSetHandlerFn update_cookie_set_handler = nullptr;
|
||||
// Optional: found by code contract, not by symbol name.
|
||||
AcquireWebViewProtocolFn acquire_web_view_protocol = nullptr;
|
||||
|
||||
bool complete() const {
|
||||
return get_open_window_id != nullptr &&
|
||||
@@ -287,6 +325,10 @@ private:
|
||||
std::array<std::shared_ptr<RawCallbackTarget>, kSubscriptionCount>
|
||||
raw_callback_targets_{};
|
||||
std::shared_ptr<HostWindowCloseTarget> host_window_close_target_;
|
||||
// The reference this counts is kept for the process lifetime, as Roblox's
|
||||
// Android app start keeps its own. Neither Shutdown nor destruction
|
||||
// releases it.
|
||||
RobloxWebViewProtocolHandle web_view_protocol_handle_;
|
||||
std::size_t in_flight_dispatches_ = 0;
|
||||
bool initializing_ = false;
|
||||
bool shutting_down_ = false;
|
||||
|
||||
@@ -36,6 +36,7 @@ class RobloxWindowInputRuntime final {
|
||||
Status UpdateTextFocusProperties(uint64_t generation,
|
||||
const RobloxTextFocusProperties& properties);
|
||||
RobloxInputSnapshot Snapshot() const;
|
||||
void SetRawMouseEnabled(bool enabled);
|
||||
|
||||
private:
|
||||
static void PlatformEventCallback(void* context,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
#ifndef MOCKTAIL_RUNTIME_RUNTIME_CONFIG_H_
|
||||
#define MOCKTAIL_RUNTIME_RUNTIME_CONFIG_H_
|
||||
|
||||
@@ -38,6 +39,10 @@ struct InputCapabilityConfig {
|
||||
bool touch_enabled = false;
|
||||
bool mouse_enabled = true;
|
||||
bool keyboard_enabled = true;
|
||||
// Mouse look deltas keep their host units instead of being scaled into guest
|
||||
// surface units. The two differ only when the desktop display scale and the
|
||||
// window's pixel density disagree, as under fractional scaling.
|
||||
bool raw_mouse = false;
|
||||
};
|
||||
|
||||
struct NetworkProxyConfig {
|
||||
@@ -46,16 +51,29 @@ struct NetworkProxyConfig {
|
||||
int port = 0;
|
||||
};
|
||||
|
||||
// Text and image fields expand {place_name}, {place_icon}, and
|
||||
// {creator_name}. A field that expands an empty placeholder, or renders
|
||||
// empty, is left out of the activity.
|
||||
struct DiscordRpcTextConfig {
|
||||
std::string title = "Roblox";
|
||||
std::string browsing = "Browsing experiences";
|
||||
std::string joining = "Joining an experience";
|
||||
std::string playing = "{place_name}";
|
||||
std::string state = "Playing Roblox";
|
||||
std::string playing = "Playing {place_name}";
|
||||
std::string state = "by {creator_name}";
|
||||
std::string unknown_place = "Unknown experience";
|
||||
};
|
||||
|
||||
// Images are an https URL or a Discord application asset key. Discord shows
|
||||
// the small image only next to a large one.
|
||||
struct DiscordRpcImagesConfig {
|
||||
std::string large = "{place_icon}";
|
||||
std::string large_text = "{place_name}";
|
||||
std::string small = "roblox_small";
|
||||
std::string small_text = "Roblox";
|
||||
};
|
||||
|
||||
struct DiscordRpcConfig {
|
||||
bool enabled = false;
|
||||
bool enabled = true;
|
||||
bool show_place_name = true;
|
||||
bool show_elapsed_time = true;
|
||||
bool join_enabled = true;
|
||||
@@ -63,6 +81,7 @@ struct DiscordRpcConfig {
|
||||
std::string join_button_label = "Join Server";
|
||||
std::string application_id;
|
||||
DiscordRpcTextConfig text;
|
||||
DiscordRpcImagesConfig images;
|
||||
};
|
||||
|
||||
std::optional<NetworkProxyConfig> ParseNetworkProxyConfig(
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -33,11 +34,18 @@ struct PayloadGarbageResult {
|
||||
explicit operator bool() const { return error.empty(); }
|
||||
};
|
||||
|
||||
// Optional per-store fault hook for atomic manifest writes. Returning true
|
||||
// makes the selected operation fail through its normal cleanup path.
|
||||
enum class ManifestWritePhase { kCreate, kWrite, kFileFsync, kRename };
|
||||
using ManifestWriteFault =
|
||||
std::function<bool(const std::filesystem::path&, ManifestWritePhase)>;
|
||||
|
||||
class PayloadStore final {
|
||||
public:
|
||||
PayloadStore(std::filesystem::path root,
|
||||
std::filesystem::path compatibility_manifest,
|
||||
std::filesystem::path runtime_binary = {});
|
||||
std::filesystem::path runtime_binary = {},
|
||||
ManifestWriteFault manifest_write_fault = {});
|
||||
|
||||
PayloadStoreResult Stage(const std::filesystem::path& prepared_payload);
|
||||
PayloadStoreResult Promote(std::string_view payload_id);
|
||||
@@ -60,6 +68,7 @@ class PayloadStore final {
|
||||
std::filesystem::path root_;
|
||||
std::filesystem::path compatibility_manifest_;
|
||||
std::filesystem::path runtime_binary_;
|
||||
ManifestWriteFault manifest_write_fault_;
|
||||
};
|
||||
|
||||
} // namespace mocktail::update
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
#ifndef MOCKTAIL_UPDATE_READINESS_CANARY_H_
|
||||
#define MOCKTAIL_UPDATE_READINESS_CANARY_H_
|
||||
|
||||
#include <spawn.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
@@ -22,6 +25,12 @@ std::string_view CanaryGraphicsBackendName(CanaryGraphicsBackend backend);
|
||||
bool ParseCanaryGraphicsBackend(std::string_view name,
|
||||
CanaryGraphicsBackend* backend);
|
||||
|
||||
// Optional per-request replacement for the POSIX spawn operation.
|
||||
// It follows posix_spawn return/ownership semantics; empty uses posix_spawn.
|
||||
using CanarySpawn =
|
||||
std::function<int(pid_t*, const char*, const posix_spawn_file_actions_t*,
|
||||
const posix_spawnattr_t*, char* const[], char* const[])>;
|
||||
|
||||
struct CanaryOptions {
|
||||
std::filesystem::path runtime_binary;
|
||||
std::filesystem::path payload_directory;
|
||||
@@ -29,9 +38,9 @@ struct CanaryOptions {
|
||||
std::filesystem::path host_abi_profile;
|
||||
std::filesystem::path cache_root;
|
||||
std::filesystem::path state_root;
|
||||
CanaryGraphicsBackend graphics_backend =
|
||||
CanaryGraphicsBackend::kDirectVulkan;
|
||||
CanaryGraphicsBackend graphics_backend = CanaryGraphicsBackend::kDirectVulkan;
|
||||
int timeout_seconds = 150;
|
||||
CanarySpawn spawn;
|
||||
|
||||
bool probation_candidate() const { return !host_abi_profile.empty(); }
|
||||
};
|
||||
@@ -39,6 +48,8 @@ struct CanaryOptions {
|
||||
struct CanaryResult {
|
||||
std::filesystem::path log_path;
|
||||
int exit_code = -1;
|
||||
// Direct posix_spawn return code; zero when spawn was not attempted.
|
||||
int spawn_error = 0;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const {
|
||||
|
||||
@@ -26,6 +26,8 @@ struct UpdatePaths {
|
||||
// build the single file happens to describe.
|
||||
std::filesystem::path host_abi_reference_profile;
|
||||
std::filesystem::path runtime_binary;
|
||||
// Optional packaging wrapper; runtime_binary identifies the actual ELF.
|
||||
std::filesystem::path runtime_launcher;
|
||||
};
|
||||
|
||||
struct UpdateRequest {
|
||||
@@ -38,6 +40,7 @@ struct UpdateRequest {
|
||||
CanaryGraphicsBackend canary_graphics_backend =
|
||||
CanaryGraphicsBackend::kDirectVulkan;
|
||||
int progress_fd = -1;
|
||||
CanarySpawn canary_spawn;
|
||||
};
|
||||
|
||||
struct UpdateResult {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#ifndef MOCKTAIL_WEBVIEW_ROBLOX_CHALLENGE_FEATURES_H_
|
||||
#define MOCKTAIL_WEBVIEW_ROBLOX_CHALLENGE_FEATURES_H_
|
||||
|
||||
#include <string_view>
|
||||
|
||||
namespace mocktail::webview {
|
||||
|
||||
// Navigation.navigateToFeature features a Roblox verification page sends when
|
||||
// the user has solved it. The captcha page sends CaptchaSuccess; the generic
|
||||
// challenge page (challenge/cdn/hybrid) sends challengeCompleted. The WebView
|
||||
// helper and the runtime both classify page events with this list.
|
||||
inline bool IsRobloxChallengeSolvedFeature(std::string_view feature) {
|
||||
return feature == "CaptchaSuccess" || feature == "challengeCompleted" ||
|
||||
feature == "ChallengeCompleted";
|
||||
}
|
||||
|
||||
} // namespace mocktail::webview
|
||||
|
||||
#endif // MOCKTAIL_WEBVIEW_ROBLOX_CHALLENGE_FEATURES_H_
|
||||
@@ -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 .
|
||||
@@ -7,6 +7,7 @@ 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_RUNTIME_BINARY=${SHARUN_DIR}/shared/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
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# Maintainer: komaruworld <rtysus228@gmail.com>
|
||||
# Maintainer: vii <coderdayton14@gmail.com>
|
||||
|
||||
pkgname=mocktail
|
||||
pkgver=1.0.3
|
||||
pkgrel=2
|
||||
pkgname=nightcap
|
||||
pkgver=0.3.1
|
||||
pkgrel=1
|
||||
pkgdesc='Android x86-64 Roblox compatibility runtime for Linux'
|
||||
arch=('x86_64')
|
||||
url='https://github.com/komaruworld/mocktail'
|
||||
url='https://github.com/CoderDayton/nightcap'
|
||||
license=('Apache-2.0')
|
||||
provides=('mocktail')
|
||||
conflicts=('mocktail')
|
||||
depends=(
|
||||
'capstone'
|
||||
'curl'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
pkgbase = mocktail-bin
|
||||
pkgbase = nightcap-bin
|
||||
pkgdesc = Android x86-64 Roblox compatibility runtime for Linux (prebuilt)
|
||||
pkgver = 1.0.4
|
||||
pkgver = 0.3.1
|
||||
pkgrel = 1
|
||||
url = https://github.com/komaruworld/mocktail
|
||||
url = https://github.com/CoderDayton/nightcap
|
||||
arch = x86_64
|
||||
license = Apache-2.0
|
||||
depends = capstone
|
||||
@@ -30,12 +30,14 @@ pkgbase = mocktail-bin
|
||||
depends = zlib
|
||||
optdepends = gamemode: Feral GameMode integration
|
||||
provides = mocktail
|
||||
provides = nightcap
|
||||
conflicts = mocktail
|
||||
conflicts = mocktail-git
|
||||
noextract = mocktail-1.0.4-x86_64.pkg.tar.zst
|
||||
conflicts = nightcap
|
||||
conflicts = nightcap-git
|
||||
noextract = nightcap-0.3.1-1-x86_64.pkg.tar.zst
|
||||
options = !debug
|
||||
options = !strip
|
||||
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
|
||||
source = nightcap-0.3.1-1-x86_64.pkg.tar.zst::https://github.com/CoderDayton/nightcap/releases/download/v0.3.1/nightcap-0.3.1-1-x86_64.pkg.tar.zst
|
||||
sha256sums = SKIP
|
||||
|
||||
pkgname = mocktail-bin
|
||||
pkgname = nightcap-bin
|
||||
@@ -1,11 +1,11 @@
|
||||
# Maintainer: komaruworld <rtysus228 at gmail dot com>
|
||||
# Maintainer: vii <coderdayton14 at gmail dot com>
|
||||
|
||||
pkgname=mocktail-bin
|
||||
pkgver=1.0.4
|
||||
pkgname=nightcap-bin
|
||||
pkgver=0.3.1
|
||||
pkgrel=1
|
||||
pkgdesc='Android x86-64 Roblox compatibility runtime for Linux (prebuilt)'
|
||||
arch=('x86_64')
|
||||
url='https://github.com/komaruworld/mocktail'
|
||||
url='https://github.com/CoderDayton/nightcap'
|
||||
license=('Apache-2.0')
|
||||
depends=(
|
||||
'capstone'
|
||||
@@ -33,17 +33,18 @@ depends=(
|
||||
'zlib'
|
||||
)
|
||||
optdepends=('gamemode: Feral GameMode integration')
|
||||
provides=('mocktail')
|
||||
conflicts=('mocktail' 'mocktail-git')
|
||||
provides=('mocktail' 'nightcap')
|
||||
conflicts=('mocktail' 'nightcap' 'nightcap-git')
|
||||
options=('!debug' '!strip')
|
||||
# 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}")
|
||||
# The release asset carries the pacman package release built by release.yml.
|
||||
_archive="nightcap-${pkgver}-1-${CARCH}.pkg.tar.zst"
|
||||
source=("${_archive}::https://github.com/CoderDayton/nightcap/releases/download/v${pkgver}/${_archive}")
|
||||
noextract=("${_archive}")
|
||||
sha256sums=('d5cc7216e88fd2889369e8eb7fba4276b2746c2b084dce566aaaa7505bc2ffb7')
|
||||
# Replace with the checksum published beside the release asset.
|
||||
sha256sums=('SKIP')
|
||||
|
||||
package() {
|
||||
bsdtar -xf "${srcdir}/${_archive}" -C "${pkgdir}" usr
|
||||
install -Dm644 "${pkgdir}/usr/share/licenses/mocktail/LICENSE" \
|
||||
install -Dm644 "${pkgdir}/usr/share/licenses/nightcap/LICENSE" \
|
||||
"${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
pkgbase = mocktail
|
||||
pkgbase = nightcap-git
|
||||
pkgdesc = Android x86-64 Roblox compatibility runtime for Linux
|
||||
pkgver = 1.0.4
|
||||
pkgver = 0.3.1.r0.g0000000
|
||||
pkgrel = 1
|
||||
url = https://github.com/komaruworld/mocktail
|
||||
url = https://github.com/CoderDayton/nightcap
|
||||
arch = x86_64
|
||||
license = Apache-2.0
|
||||
makedepends = cmake>=3.20
|
||||
@@ -36,11 +36,13 @@ pkgbase = mocktail
|
||||
depends = webkitgtk-6.0
|
||||
depends = zlib
|
||||
optdepends = gamemode: Feral GameMode integration
|
||||
conflicts = mocktail-bin
|
||||
conflicts = mocktail-git
|
||||
provides = mocktail
|
||||
provides = nightcap
|
||||
conflicts = mocktail
|
||||
conflicts = nightcap
|
||||
conflicts = nightcap-bin
|
||||
options = !debug
|
||||
options = !lto
|
||||
source = mocktail::git+https://github.com/komaruworld/mocktail.git#tag=1.0.4
|
||||
source = nightcap::git+https://github.com/CoderDayton/nightcap.git#branch=main
|
||||
sha256sums = SKIP
|
||||
|
||||
pkgname = mocktail
|
||||
pkgname = nightcap-git
|
||||
@@ -1,11 +1,11 @@
|
||||
# Maintainer: komaruworld <rtysus228 at gmail dot com>
|
||||
# Maintainer: vii <coderdayton14 at gmail dot com>
|
||||
|
||||
pkgname=mocktail-git
|
||||
pkgver=1.0.2.r24.g9bdd737
|
||||
pkgname=nightcap-git
|
||||
pkgver=0.3.1.r0.g0000000
|
||||
pkgrel=1
|
||||
pkgdesc='Android x86-64 Roblox compatibility runtime for Linux'
|
||||
arch=('x86_64')
|
||||
url='https://github.com/komaruworld/mocktail'
|
||||
url='https://github.com/CoderDayton/nightcap'
|
||||
license=('Apache-2.0')
|
||||
depends=(
|
||||
'capstone'
|
||||
@@ -42,31 +42,30 @@ makedepends=(
|
||||
'vulkan-headers'
|
||||
)
|
||||
optdepends=('gamemode: Feral GameMode integration')
|
||||
provides=('mocktail')
|
||||
conflicts=('mocktail' 'mocktail-bin')
|
||||
provides=('mocktail' 'nightcap')
|
||||
conflicts=('mocktail' 'nightcap' 'nightcap-bin')
|
||||
options=('!debug')
|
||||
source=(
|
||||
'mocktail::git+https://github.com/komaruworld/mocktail.git#branch=main'
|
||||
'nightcap::git+https://github.com/CoderDayton/nightcap.git#branch=main'
|
||||
)
|
||||
sha256sums=(
|
||||
'SKIP'
|
||||
)
|
||||
|
||||
pkgver() {
|
||||
cd mocktail
|
||||
cd nightcap
|
||||
|
||||
local version
|
||||
version="$(sed -n \
|
||||
's/^[[:space:]]*VERSION[[:space:]]\+\([0-9][0-9.]*\).*/\1/p' \
|
||||
CMakeLists.txt | head -n 1)"
|
||||
version="$(git describe --tags --abbrev=0 --match='v[0-9]*' 2>/dev/null)"
|
||||
version="${version#v}"
|
||||
printf '%s.r%s.g%s' \
|
||||
"${version}" \
|
||||
"${version:-0.0.0}" \
|
||||
"$(git rev-list --count HEAD)" \
|
||||
"$(git rev-parse --short=7 HEAD)"
|
||||
}
|
||||
|
||||
prepare() {
|
||||
cd mocktail
|
||||
cd nightcap
|
||||
|
||||
sed -i \
|
||||
's|add_subdirectory(third_party/Vulkan-Headers EXCLUDE_FROM_ALL)|find_package(VulkanHeaders CONFIG REQUIRED)|' \
|
||||
@@ -74,7 +73,7 @@ prepare() {
|
||||
}
|
||||
|
||||
build() {
|
||||
cmake -S mocktail -B build -G Ninja \
|
||||
cmake -S nightcap -B build -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||
@@ -87,6 +86,6 @@ build() {
|
||||
|
||||
package() {
|
||||
DESTDIR="${pkgdir}" cmake --install build
|
||||
install -Dm644 mocktail/LICENSE \
|
||||
install -Dm644 nightcap/LICENSE \
|
||||
"${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
pkgbase = mocktail-git
|
||||
pkgbase = nightcap
|
||||
pkgdesc = Android x86-64 Roblox compatibility runtime for Linux
|
||||
pkgver = 1.0.2.r24.g9bdd737
|
||||
pkgver = 0.3.1
|
||||
pkgrel = 1
|
||||
url = https://github.com/komaruworld/mocktail
|
||||
url = https://github.com/CoderDayton/nightcap
|
||||
arch = x86_64
|
||||
license = Apache-2.0
|
||||
makedepends = cmake>=3.20
|
||||
@@ -38,9 +38,11 @@ pkgbase = mocktail-git
|
||||
optdepends = gamemode: Feral GameMode integration
|
||||
provides = mocktail
|
||||
conflicts = mocktail
|
||||
conflicts = mocktail-bin
|
||||
conflicts = nightcap-bin
|
||||
conflicts = nightcap-git
|
||||
options = !debug
|
||||
source = mocktail::git+https://github.com/komaruworld/mocktail.git#branch=main
|
||||
options = !lto
|
||||
source = nightcap::git+https://github.com/CoderDayton/nightcap.git#tag=v0.3.1
|
||||
sha256sums = SKIP
|
||||
|
||||
pkgname = mocktail-git
|
||||
pkgname = nightcap
|
||||
@@ -1,11 +1,11 @@
|
||||
# Maintainer: komaruworld <rtysus228 at gmail dot com>
|
||||
# Maintainer: vii <coderdayton14 at gmail dot com>
|
||||
|
||||
pkgname=mocktail
|
||||
pkgver=1.0.4
|
||||
pkgname=nightcap
|
||||
pkgver=0.3.1
|
||||
pkgrel=1
|
||||
pkgdesc='Android x86-64 Roblox compatibility runtime for Linux'
|
||||
arch=('x86_64')
|
||||
url='https://github.com/komaruworld/mocktail'
|
||||
url='https://github.com/CoderDayton/nightcap'
|
||||
license=('Apache-2.0')
|
||||
depends=(
|
||||
'capstone'
|
||||
@@ -42,17 +42,18 @@ makedepends=(
|
||||
'vulkan-headers'
|
||||
)
|
||||
optdepends=('gamemode: Feral GameMode integration')
|
||||
conflicts=('mocktail-bin' 'mocktail-git')
|
||||
provides=('mocktail')
|
||||
conflicts=('mocktail' 'nightcap-bin' 'nightcap-git')
|
||||
options=('!debug' '!lto')
|
||||
source=(
|
||||
"mocktail::git+https://github.com/komaruworld/mocktail.git#tag=${pkgver}"
|
||||
"nightcap::git+https://github.com/CoderDayton/nightcap.git#tag=v${pkgver}"
|
||||
)
|
||||
sha256sums=(
|
||||
'SKIP'
|
||||
)
|
||||
|
||||
prepare() {
|
||||
cd mocktail
|
||||
cd nightcap
|
||||
|
||||
sed -i \
|
||||
's|add_subdirectory(third_party/Vulkan-Headers EXCLUDE_FROM_ALL)|find_package(VulkanHeaders CONFIG REQUIRED)|' \
|
||||
@@ -60,7 +61,7 @@ prepare() {
|
||||
}
|
||||
|
||||
build() {
|
||||
cmake -S mocktail -B build -G Ninja \
|
||||
cmake -S nightcap -B build -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||
-DCMAKE_INSTALL_LIBDIR=lib \
|
||||
@@ -73,6 +74,6 @@ build() {
|
||||
|
||||
package() {
|
||||
DESTDIR="${pkgdir}" cmake --install build
|
||||
install -Dm644 mocktail/LICENSE \
|
||||
install -Dm644 nightcap/LICENSE \
|
||||
"${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
|
||||
}
|
||||
@@ -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",
|
||||
@@ -9,7 +9,7 @@
|
||||
"--share=ipc",
|
||||
"--share=network",
|
||||
"--socket=wayland",
|
||||
"--socket=fallback-x11",
|
||||
"--socket=x11",
|
||||
"--socket=pulseaudio",
|
||||
"--device=dri",
|
||||
"--talk-name=com.feralinteractive.GameMode",
|
||||
@@ -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.
|
||||
@@ -0,0 +1,602 @@
|
||||
// cc -O2 -o mocktail_webview_host mocktail_webview_host.c $(pkg-config --cflags --libs gtk4 webkitgtk-6.0)
|
||||
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <glib-unix.h>
|
||||
#include <gtk/gtk.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <signal.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
#include <webkit/webkit.h>
|
||||
|
||||
#define PORT 2137
|
||||
#define MAX_PACKET (64u * 1024u + 12u)
|
||||
#define VERSION "2.725.1142"
|
||||
|
||||
enum {
|
||||
OP_LOAD_URL = 1, OP_SET_TITLE = 2, OP_SET_VISIBLE = 3, OP_EVAL_JS = 4,
|
||||
OP_CLOSE = 5, OP_SET_BACK_DISABLED = 6, OP_SHOW_DOMAIN_TITLE = 7,
|
||||
OP_SET_COOKIE = 8, OP_CLEAR_COOKIE = 9, OP_RETAIN_COOKIE = 10
|
||||
};
|
||||
|
||||
enum { EV_EXECUTE_ROBLOX = 1, EV_ROBLOX_WK_HYBRID = 2, EV_READY = 3, EV_COOKIE = 4 };
|
||||
|
||||
static const char kBridge[] =
|
||||
"(() => { 'use strict';"
|
||||
" const h = window.webkit && window.webkit.messageHandlers;"
|
||||
" const handler = h && h.executeRoblox; if (!handler) return;"
|
||||
" const bridge = {};"
|
||||
" Object.defineProperty(bridge, 'executeRoblox', {"
|
||||
" value: (q) => handler.postMessage(JSON.parse(q)),"
|
||||
" enumerable: true, writable: false, configurable: false });"
|
||||
" try { Object.defineProperty(window, '__globalRobloxAndroidBridge__', {"
|
||||
" value: bridge, enumerable: true, writable: false, configurable: false });"
|
||||
" } catch (_) {} })();";
|
||||
|
||||
static GtkApplication *app;
|
||||
static GtkWindow *window;
|
||||
static WebKitWebView *view;
|
||||
static WebKitNetworkSession *session;
|
||||
static int listener = -1;
|
||||
static int client = -1;
|
||||
static guint client_read_source;
|
||||
static guint client_write_source;
|
||||
static GByteArray *rx;
|
||||
static GQueue *tx;
|
||||
static size_t tx_offset;
|
||||
static gboolean domain_title;
|
||||
static gboolean login_page;
|
||||
static gboolean back_disabled;
|
||||
static gboolean shutting_down;
|
||||
static char *last_cookie;
|
||||
|
||||
static gboolean flush_tx(gint fd, GIOCondition cond, gpointer data);
|
||||
static gboolean on_client(gint fd, GIOCondition cond, gpointer data);
|
||||
static void apply(uint8_t op, const char *payload, size_t size);
|
||||
static void create_surface(void);
|
||||
static void destroy_surface(void);
|
||||
|
||||
static void drop_client(const char *why) {
|
||||
if (client < 0) return;
|
||||
fprintf(stderr, "client dropped: %s\n", why);
|
||||
if (client_read_source) {
|
||||
g_source_remove(client_read_source);
|
||||
client_read_source = 0;
|
||||
}
|
||||
if (client_write_source) {
|
||||
g_source_remove(client_write_source);
|
||||
client_write_source = 0;
|
||||
}
|
||||
close(client);
|
||||
client = -1;
|
||||
g_byte_array_set_size(rx, 0);
|
||||
while (!g_queue_is_empty(tx)) g_bytes_unref(g_queue_pop_head(tx));
|
||||
tx_offset = 0;
|
||||
g_free(last_cookie);
|
||||
last_cookie = NULL;
|
||||
destroy_surface();
|
||||
}
|
||||
|
||||
static void quit_app(void) {
|
||||
if (shutting_down) return;
|
||||
shutting_down = TRUE;
|
||||
drop_client("shutting down");
|
||||
if (app) g_application_quit(G_APPLICATION(app));
|
||||
}
|
||||
|
||||
static void send_event(uint8_t type, const char *payload, size_t size) {
|
||||
if (client < 0) return;
|
||||
size_t body = 12 + size;
|
||||
uint8_t *buffer = g_malloc0(4 + body);
|
||||
buffer[0] = body >> 24; buffer[1] = body >> 16; buffer[2] = body >> 8; buffer[3] = body;
|
||||
memcpy(buffer + 4, "MWVE", 4);
|
||||
buffer[8] = 1;
|
||||
buffer[9] = type;
|
||||
buffer[12] = size >> 24; buffer[13] = size >> 16; buffer[14] = size >> 8; buffer[15] = size;
|
||||
if (size) memcpy(buffer + 16, payload, size);
|
||||
g_queue_push_tail(tx, g_bytes_new_take(buffer, 4 + body));
|
||||
if (!client_write_source)
|
||||
client_write_source = g_unix_fd_add(client, G_IO_OUT | G_IO_HUP | G_IO_ERR, flush_tx, NULL);
|
||||
}
|
||||
|
||||
static gboolean flush_tx(gint fd, GIOCondition cond, gpointer data) {
|
||||
(void)data;
|
||||
if (cond & (G_IO_HUP | G_IO_ERR)) {
|
||||
client_write_source = 0;
|
||||
drop_client("write side closed");
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
while (!g_queue_is_empty(tx)) {
|
||||
GBytes *head = g_queue_peek_head(tx);
|
||||
gsize length;
|
||||
const uint8_t *bytes = g_bytes_get_data(head, &length);
|
||||
ssize_t count = send(fd, bytes + tx_offset, length - tx_offset, MSG_DONTWAIT | MSG_NOSIGNAL);
|
||||
if (count < 0 && errno == EINTR) continue;
|
||||
if (count < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) return G_SOURCE_CONTINUE;
|
||||
if (count <= 0) {
|
||||
client_write_source = 0;
|
||||
drop_client("write failed");
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
tx_offset += (size_t)count;
|
||||
if (tx_offset == length) {
|
||||
tx_offset = 0;
|
||||
g_bytes_unref(g_queue_pop_head(tx));
|
||||
}
|
||||
}
|
||||
client_write_source = 0;
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
static char *user_agent(void) {
|
||||
const char *v = getenv("MOCKTAIL_ROBLOX_VERSION");
|
||||
if (!v || !*v) v = VERSION;
|
||||
return g_strdup_printf(
|
||||
"Mozilla/5.0 AppleWebKit/605.1.15 (KHTML, like Gecko) ROBLOX Android App "
|
||||
"%s Tablet Hybrid() GooglePlayStore RobloxApp/%s (GlobalDist; GooglePlayStore)", v, v);
|
||||
}
|
||||
|
||||
static void on_cookies(GObject *source, GAsyncResult *result, gpointer data) {
|
||||
(void)data;
|
||||
GError *error = NULL;
|
||||
GList *cookies = webkit_cookie_manager_get_cookies_finish(WEBKIT_COOKIE_MANAGER(source), result, &error);
|
||||
for (GList *i = cookies; i; i = i->next) {
|
||||
SoupCookie *c = i->data;
|
||||
const char *name = soup_cookie_get_name(c);
|
||||
const char *value = soup_cookie_get_value(c);
|
||||
if (name && value && strcmp(name, ".ROBLOSECURITY") == 0 && *value &&
|
||||
(!last_cookie || strcmp(last_cookie, value) != 0)) {
|
||||
g_free(last_cookie);
|
||||
last_cookie = g_strdup(value);
|
||||
send_event(EV_COOKIE, value, strlen(value));
|
||||
break;
|
||||
}
|
||||
}
|
||||
g_list_free_full(cookies, (GDestroyNotify)soup_cookie_free);
|
||||
g_clear_error(&error);
|
||||
}
|
||||
|
||||
static void request_cookie(void) {
|
||||
if (!login_page || !session || client < 0) return;
|
||||
webkit_cookie_manager_get_cookies(webkit_network_session_get_cookie_manager(session),
|
||||
"https://www.roblox.com/", NULL, on_cookies, NULL);
|
||||
}
|
||||
|
||||
static void on_cookie_changed(WebKitCookieManager *manager, gpointer data) {
|
||||
(void)manager; (void)data;
|
||||
request_cookie();
|
||||
}
|
||||
|
||||
static void configure_agent(const char *url) {
|
||||
WebKitSettings *settings = webkit_web_view_get_settings(view);
|
||||
GUri *uri = g_uri_parse(url, G_URI_FLAGS_NONE, NULL);
|
||||
const char *path = uri ? g_uri_get_path(uri) : NULL;
|
||||
login_page = path && (strcmp(path, "/login") == 0 || strcmp(path, "/login/") == 0);
|
||||
if (uri) g_uri_unref(uri);
|
||||
if (login_page) {
|
||||
webkit_settings_set_user_agent(settings, NULL);
|
||||
request_cookie();
|
||||
} else {
|
||||
char *agent = user_agent();
|
||||
webkit_settings_set_user_agent(settings, agent);
|
||||
g_free(agent);
|
||||
}
|
||||
}
|
||||
|
||||
static void update_title(void) {
|
||||
if (!domain_title || !window || !view) return;
|
||||
const char *uri = webkit_web_view_get_uri(view);
|
||||
if (!uri) return;
|
||||
GUri *parsed = g_uri_parse(uri, G_URI_FLAGS_NONE, NULL);
|
||||
if (!parsed) return;
|
||||
const char *host = g_uri_get_host(parsed);
|
||||
if (host) gtk_window_set_title(window, host);
|
||||
g_uri_unref(parsed);
|
||||
}
|
||||
|
||||
static void on_load_changed(WebKitWebView *v, WebKitLoadEvent event, gpointer data) {
|
||||
(void)v; (void)data;
|
||||
if (event == WEBKIT_LOAD_COMMITTED || event == WEBKIT_LOAD_FINISHED) {
|
||||
update_title();
|
||||
request_cookie();
|
||||
}
|
||||
}
|
||||
|
||||
static GtkWidget *on_create(WebKitWebView *v, WebKitNavigationAction *action, gpointer data) {
|
||||
(void)v; (void)data;
|
||||
WebKitURIRequest *request = webkit_navigation_action_get_request(action);
|
||||
const char *uri = request ? webkit_uri_request_get_uri(request) : NULL;
|
||||
if (uri && view) webkit_web_view_load_uri(view, uri);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char *json_string_after(const char *json, const char *key) {
|
||||
char *needle = g_strdup_printf("\"%s\"", key);
|
||||
const char *at = strstr(json, needle);
|
||||
g_free(needle);
|
||||
if (!at) return NULL;
|
||||
at = strchr(at + strlen(key) + 2, ':');
|
||||
if (!at) return NULL;
|
||||
at++;
|
||||
while (*at == ' ') at++;
|
||||
if (*at != '"') return NULL;
|
||||
at++;
|
||||
GString *out = g_string_new(NULL);
|
||||
while (*at && *at != '"') {
|
||||
if (*at == '\\' && at[1]) at++;
|
||||
g_string_append_c(out, *at);
|
||||
at++;
|
||||
}
|
||||
return g_string_free(out, FALSE);
|
||||
}
|
||||
|
||||
static void on_callback_done(GObject *source, GAsyncResult *result, gpointer data) {
|
||||
gboolean close_after = GPOINTER_TO_INT(data);
|
||||
GError *error = NULL;
|
||||
JSCValue *value = webkit_web_view_evaluate_javascript_finish(WEBKIT_WEB_VIEW(source), result, &error);
|
||||
gboolean confirmed = !error && value && jsc_value_is_boolean(value) && jsc_value_to_boolean(value);
|
||||
if (value) g_object_unref(value);
|
||||
g_clear_error(&error);
|
||||
if (confirmed && close_after) destroy_surface();
|
||||
}
|
||||
|
||||
static void acknowledge_page_command(const char *json) {
|
||||
if (!view || !json) return;
|
||||
char *feature = json_string_after(json, "feature");
|
||||
char *callback = json_string_after(json, "callbackID");
|
||||
if (feature && callback && *callback) {
|
||||
gboolean success = strcmp(feature, "CaptchaSuccess") == 0;
|
||||
if (success || strcmp(feature, "CaptchaShown") == 0) {
|
||||
GString *quoted = g_string_new("\"");
|
||||
for (const char *p = callback; *p; p++) {
|
||||
if (*p == '"' || *p == '\\') g_string_append_c(quoted, '\\');
|
||||
g_string_append_c(quoted, *p);
|
||||
}
|
||||
g_string_append_c(quoted, '"');
|
||||
char *script = g_strdup_printf(
|
||||
"(() => { try { if (window.Roblox && window.Roblox.Hybrid && "
|
||||
"window.Roblox.Hybrid.Bridge && typeof window.Roblox.Hybrid.Bridge.nativeCallback === 'function') { "
|
||||
"window.Roblox.Hybrid.Bridge.nativeCallback(%s, true, {}); return true; } } catch (_) {} return false; })();",
|
||||
quoted->str);
|
||||
webkit_web_view_evaluate_javascript(view, script, -1, NULL, NULL, NULL,
|
||||
on_callback_done, GINT_TO_POINTER(success));
|
||||
g_free(script);
|
||||
g_string_free(quoted, TRUE);
|
||||
}
|
||||
}
|
||||
g_free(feature);
|
||||
g_free(callback);
|
||||
}
|
||||
|
||||
static void forward(uint8_t type, JSCValue *value) {
|
||||
char *text = NULL;
|
||||
if (jsc_value_is_string(value)) text = jsc_value_to_string(value);
|
||||
else if (jsc_value_is_object(value)) text = jsc_value_to_json(value, 0);
|
||||
if (!text) return;
|
||||
size_t size = strlen(text);
|
||||
if (size && size <= 64u * 1024u) {
|
||||
send_event(type, text, size);
|
||||
acknowledge_page_command(text);
|
||||
}
|
||||
g_free(text);
|
||||
}
|
||||
|
||||
static void on_execute_roblox(WebKitUserContentManager *m, JSCValue *value, gpointer data) {
|
||||
(void)m; (void)data;
|
||||
forward(EV_EXECUTE_ROBLOX, value);
|
||||
}
|
||||
|
||||
static void on_hybrid(WebKitUserContentManager *m, JSCValue *value, gpointer data) {
|
||||
(void)m; (void)data;
|
||||
if (!jsc_value_is_object(value)) return;
|
||||
JSCValue *command = jsc_value_object_get_property(value, "command");
|
||||
if (command) {
|
||||
forward(EV_ROBLOX_WK_HYBRID, command);
|
||||
g_object_unref(command);
|
||||
}
|
||||
}
|
||||
|
||||
static void on_view_close(WebKitWebView *v, gpointer data) {
|
||||
(void)v; (void)data;
|
||||
destroy_surface();
|
||||
}
|
||||
|
||||
static gboolean on_close_request(GtkWindow *w, gpointer data) {
|
||||
(void)w; (void)data;
|
||||
window = NULL;
|
||||
view = NULL;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
static void on_reload(GtkButton *button, gpointer data) {
|
||||
(void)button; (void)data;
|
||||
if (view) webkit_web_view_reload_bypass_cache(view);
|
||||
}
|
||||
|
||||
static void on_eval(GObject *source, GAsyncResult *result, gpointer data) {
|
||||
(void)data;
|
||||
GError *error = NULL;
|
||||
JSCValue *value = webkit_web_view_evaluate_javascript_finish(WEBKIT_WEB_VIEW(source), result, &error);
|
||||
if (value) g_object_unref(value);
|
||||
g_clear_error(&error);
|
||||
}
|
||||
|
||||
static void on_cookie_done(GObject *source, GAsyncResult *result, gpointer data) {
|
||||
(void)data;
|
||||
GError *error = NULL;
|
||||
webkit_cookie_manager_add_cookie_finish(WEBKIT_COOKIE_MANAGER(source), result, &error);
|
||||
g_clear_error(&error);
|
||||
}
|
||||
|
||||
static void on_cookie_deleted(GObject *source, GAsyncResult *result, gpointer data) {
|
||||
(void)data;
|
||||
GError *error = NULL;
|
||||
webkit_cookie_manager_delete_cookie_finish(WEBKIT_COOKIE_MANAGER(source), result, &error);
|
||||
g_clear_error(&error);
|
||||
}
|
||||
|
||||
static void ensure_session(void) {
|
||||
if (session) return;
|
||||
gchar *data_dir = g_build_filename(g_get_user_data_dir(), "mocktail", "webview", NULL);
|
||||
gchar *cache_dir = g_build_filename(g_get_user_cache_dir(), "mocktail", "webview", NULL);
|
||||
g_mkdir_with_parents(data_dir, 0700);
|
||||
g_mkdir_with_parents(cache_dir, 0700);
|
||||
gchar *cookie_db = g_build_filename(data_dir, "cookies.sqlite", NULL);
|
||||
session = webkit_network_session_new(data_dir, cache_dir);
|
||||
g_free(data_dir);
|
||||
g_free(cache_dir);
|
||||
WebKitCookieManager *cookies = webkit_network_session_get_cookie_manager(session);
|
||||
webkit_cookie_manager_set_persistent_storage(cookies, cookie_db, WEBKIT_COOKIE_PERSISTENT_STORAGE_SQLITE);
|
||||
webkit_cookie_manager_set_accept_policy(cookies, WEBKIT_COOKIE_POLICY_ACCEPT_ALWAYS);
|
||||
g_signal_connect(cookies, "changed", G_CALLBACK(on_cookie_changed), NULL);
|
||||
g_free(cookie_db);
|
||||
webkit_network_session_set_itp_enabled(session, FALSE);
|
||||
webkit_network_session_set_persistent_credential_storage_enabled(session, TRUE);
|
||||
}
|
||||
|
||||
static void create_surface(void) {
|
||||
if (window || view) return;
|
||||
ensure_session();
|
||||
|
||||
WebKitUserContentManager *manager = webkit_user_content_manager_new();
|
||||
WebKitWebView *created = WEBKIT_WEB_VIEW(g_object_new(WEBKIT_TYPE_WEB_VIEW, "network-session", session,
|
||||
"user-content-manager", manager, NULL));
|
||||
g_object_unref(manager);
|
||||
manager = webkit_web_view_get_user_content_manager(created);
|
||||
g_signal_connect(manager, "script-message-received::executeRoblox", G_CALLBACK(on_execute_roblox), NULL);
|
||||
g_signal_connect(manager, "script-message-received::RobloxWKHybrid", G_CALLBACK(on_hybrid), NULL);
|
||||
g_signal_connect(manager, "script-message-received::mocktailRobloxBridge", G_CALLBACK(on_execute_roblox), NULL);
|
||||
webkit_user_content_manager_register_script_message_handler(manager, "executeRoblox", NULL);
|
||||
webkit_user_content_manager_register_script_message_handler(manager, "RobloxWKHybrid", NULL);
|
||||
webkit_user_content_manager_register_script_message_handler(manager, "mocktailRobloxBridge", NULL);
|
||||
WebKitUserScript *script = webkit_user_script_new(kBridge, WEBKIT_USER_CONTENT_INJECT_TOP_FRAME,
|
||||
WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, NULL, NULL);
|
||||
webkit_user_content_manager_add_script(manager, script);
|
||||
webkit_user_script_unref(script);
|
||||
|
||||
WebKitSettings *settings = webkit_web_view_get_settings(created);
|
||||
webkit_settings_set_enable_javascript(settings, TRUE);
|
||||
webkit_settings_set_javascript_can_open_windows_automatically(settings, TRUE);
|
||||
webkit_settings_set_enable_back_forward_navigation_gestures(settings, !back_disabled);
|
||||
char *agent = user_agent();
|
||||
webkit_settings_set_user_agent(settings, agent);
|
||||
g_free(agent);
|
||||
|
||||
g_signal_connect(created, "load-changed", G_CALLBACK(on_load_changed), NULL);
|
||||
g_signal_connect(created, "create", G_CALLBACK(on_create), NULL);
|
||||
g_signal_connect(created, "close", G_CALLBACK(on_view_close), NULL);
|
||||
|
||||
GtkWindow *created_window = GTK_WINDOW(gtk_application_window_new(app));
|
||||
gtk_window_set_title(created_window, "Roblox");
|
||||
gtk_window_set_default_size(created_window, 1100, 760);
|
||||
GtkWidget *header = gtk_header_bar_new();
|
||||
GtkWidget *reload = gtk_button_new_from_icon_name("view-refresh-symbolic");
|
||||
g_signal_connect(reload, "clicked", G_CALLBACK(on_reload), NULL);
|
||||
gtk_header_bar_pack_start(GTK_HEADER_BAR(header), reload);
|
||||
gtk_window_set_titlebar(created_window, header);
|
||||
gtk_widget_set_vexpand(GTK_WIDGET(created), TRUE);
|
||||
gtk_window_set_child(created_window, GTK_WIDGET(created));
|
||||
g_signal_connect(created_window, "close-request", G_CALLBACK(on_close_request), NULL);
|
||||
|
||||
view = created;
|
||||
window = created_window;
|
||||
}
|
||||
|
||||
static void destroy_surface(void) {
|
||||
GtkWindow *w = window;
|
||||
window = NULL;
|
||||
view = NULL;
|
||||
login_page = FALSE;
|
||||
domain_title = FALSE;
|
||||
if (w) gtk_window_destroy(w);
|
||||
}
|
||||
|
||||
static void apply(uint8_t op, const char *payload, size_t size) {
|
||||
if (op == OP_CLOSE) {
|
||||
destroy_surface();
|
||||
return;
|
||||
}
|
||||
if (op == OP_SET_BACK_DISABLED) {
|
||||
back_disabled = size == 1 && payload[0];
|
||||
if (view) webkit_settings_set_enable_back_forward_navigation_gestures(
|
||||
webkit_web_view_get_settings(view), !back_disabled);
|
||||
return;
|
||||
}
|
||||
if (op == OP_SHOW_DOMAIN_TITLE) {
|
||||
domain_title = size == 1 && payload[0];
|
||||
update_title();
|
||||
return;
|
||||
}
|
||||
if (op == OP_SET_COOKIE || op == OP_CLEAR_COOKIE) {
|
||||
ensure_session();
|
||||
WebKitCookieManager *cookies = webkit_network_session_get_cookie_manager(session);
|
||||
if (op == OP_SET_COOKIE) {
|
||||
char *value = g_strndup(payload, size);
|
||||
SoupCookie *cookie = soup_cookie_new(".ROBLOSECURITY", value, ".roblox.com", "/", -1);
|
||||
g_free(value);
|
||||
if (cookie) {
|
||||
soup_cookie_set_secure(cookie, TRUE);
|
||||
soup_cookie_set_http_only(cookie, TRUE);
|
||||
soup_cookie_set_same_site_policy(cookie, SOUP_SAME_SITE_POLICY_LAX);
|
||||
webkit_cookie_manager_add_cookie(cookies, cookie, NULL, on_cookie_done, NULL);
|
||||
soup_cookie_free(cookie);
|
||||
}
|
||||
} else {
|
||||
SoupCookie *cookie = soup_cookie_new(".ROBLOSECURITY", "", ".roblox.com", "/", -1);
|
||||
if (cookie) {
|
||||
webkit_cookie_manager_delete_cookie(cookies, cookie, NULL, on_cookie_deleted, NULL);
|
||||
soup_cookie_free(cookie);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (op == OP_RETAIN_COOKIE) return;
|
||||
if (op == OP_LOAD_URL) create_surface();
|
||||
if (!window || !view) return;
|
||||
switch (op) {
|
||||
case OP_LOAD_URL: {
|
||||
char *url = g_strndup(payload, size);
|
||||
configure_agent(url);
|
||||
webkit_web_view_load_uri(view, url);
|
||||
gtk_window_present(window);
|
||||
g_free(url);
|
||||
break;
|
||||
}
|
||||
case OP_SET_TITLE: {
|
||||
char *title = g_strndup(payload, size);
|
||||
gtk_window_set_title(window, title);
|
||||
g_free(title);
|
||||
break;
|
||||
}
|
||||
case OP_SET_VISIBLE:
|
||||
if (size == 1 && payload[0]) gtk_window_present(window);
|
||||
else gtk_widget_set_visible(GTK_WIDGET(window), FALSE);
|
||||
break;
|
||||
case OP_EVAL_JS:
|
||||
webkit_web_view_evaluate_javascript(view, payload, (gssize)size, NULL, NULL, NULL, on_eval, NULL);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static gboolean process_frames(void) {
|
||||
while (rx->len >= 4) {
|
||||
uint32_t length = ((uint32_t)rx->data[0] << 24) | ((uint32_t)rx->data[1] << 16) |
|
||||
((uint32_t)rx->data[2] << 8) | rx->data[3];
|
||||
if (length > MAX_PACKET) return FALSE;
|
||||
if (rx->len < 4u + length) break;
|
||||
uint8_t *copy = g_memdup2(rx->data + 4, length);
|
||||
g_byte_array_remove_range(rx, 0, 4 + length);
|
||||
if (length >= 12 && memcmp(copy, "MWVC", 4) == 0) {
|
||||
size_t size = ((size_t)copy[8] << 24) | ((size_t)copy[9] << 16) |
|
||||
((size_t)copy[10] << 8) | copy[11];
|
||||
if (size == length - 12) apply(copy[5], (const char *)copy + 12, size);
|
||||
}
|
||||
g_free(copy);
|
||||
if (client < 0) return TRUE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
static gboolean on_client(gint fd, GIOCondition cond, gpointer data) {
|
||||
(void)data;
|
||||
uint8_t buffer[8192];
|
||||
while (TRUE) {
|
||||
ssize_t count = read(fd, buffer, sizeof buffer);
|
||||
if (count < 0 && errno == EINTR) continue;
|
||||
if (count < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) break;
|
||||
if (count <= 0) {
|
||||
client_read_source = 0;
|
||||
drop_client("client closed");
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
g_byte_array_append(rx, buffer, (guint)count);
|
||||
if (!process_frames()) {
|
||||
client_read_source = 0;
|
||||
drop_client("bad frame");
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
if (client < 0) return G_SOURCE_REMOVE;
|
||||
}
|
||||
if (cond & (G_IO_HUP | G_IO_ERR)) {
|
||||
client_read_source = 0;
|
||||
drop_client("client hung up");
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
static gboolean on_listener(gint fd, GIOCondition cond, gpointer data) {
|
||||
(void)cond; (void)data;
|
||||
int accepted = accept(fd, NULL, NULL);
|
||||
if (accepted < 0) return G_SOURCE_CONTINUE;
|
||||
if (client >= 0) drop_client("replaced by a new connection");
|
||||
fcntl(accepted, F_SETFD, FD_CLOEXEC);
|
||||
fcntl(accepted, F_SETFL, fcntl(accepted, F_GETFL) | O_NONBLOCK);
|
||||
int one = 1;
|
||||
setsockopt(accepted, IPPROTO_TCP, TCP_NODELAY, &one, sizeof one);
|
||||
client = accepted;
|
||||
fprintf(stderr, "client connected\n");
|
||||
client_read_source = g_unix_fd_add(client, G_IO_IN | G_IO_HUP | G_IO_ERR, on_client, NULL);
|
||||
send_event(EV_READY, "", 0);
|
||||
return G_SOURCE_CONTINUE;
|
||||
}
|
||||
|
||||
static int listen_socket(void) {
|
||||
int fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
fcntl(fd, F_SETFD, FD_CLOEXEC);
|
||||
int one = 1;
|
||||
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
|
||||
struct sockaddr_in address;
|
||||
memset(&address, 0, sizeof address);
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_port = htons(PORT);
|
||||
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
||||
if (bind(fd, (struct sockaddr *)&address, sizeof address) != 0 || listen(fd, 4) != 0) {
|
||||
close(fd);
|
||||
return -1;
|
||||
}
|
||||
fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_NONBLOCK);
|
||||
return fd;
|
||||
}
|
||||
|
||||
static void activate(GtkApplication *application, gpointer data) {
|
||||
(void)application; (void)data;
|
||||
}
|
||||
|
||||
static gboolean on_sigterm(gpointer data) {
|
||||
(void)data;
|
||||
quit_app();
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
(void)argc;
|
||||
setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1", 0);
|
||||
setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", 0);
|
||||
rx = g_byte_array_new();
|
||||
tx = g_queue_new();
|
||||
listener = listen_socket();
|
||||
if (listener < 0) {
|
||||
fprintf(stderr, "cannot listen on 127.0.0.1:%d: %s\n", PORT, strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
g_unix_fd_add(listener, G_IO_IN, on_listener, NULL);
|
||||
g_unix_signal_add(SIGTERM, on_sigterm, NULL);
|
||||
g_unix_signal_add(SIGINT, on_sigterm, NULL);
|
||||
app = gtk_application_new("org.mocktail.WebViewHost", G_APPLICATION_NON_UNIQUE);
|
||||
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
|
||||
g_application_hold(G_APPLICATION(app));
|
||||
int status = g_application_run(G_APPLICATION(app), 1, argv);
|
||||
g_object_unref(app);
|
||||
return status;
|
||||
}
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 538 B |
|
Before Width: | Height: | Size: 617 B |
|
After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 725 B |
|
Before Width: | Height: | Size: 903 B |
|
After Width: | Height: | Size: 771 B |
|
Before Width: | Height: | Size: 1014 B |
|
After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,13 @@
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Nightcap
|
||||
GenericName=Roblox Player
|
||||
Comment=Play Roblox on Linux
|
||||
Exec=env SDL_VIDEODRIVER=wayland,x11 mocktail %u
|
||||
Icon=io.github.CoderDayton.nightcap
|
||||
StartupWMClass=io.github.CoderDayton.nightcap
|
||||
Categories=Game;
|
||||
Terminal=false
|
||||
MimeType=x-scheme-handler/roblox;x-scheme-handler/roblox-player;
|
||||
X-Mocktail-Managed=true
|
||||
@@ -0,0 +1,131 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Modified by vii from komaruworld/mocktail. See README "About this fork". -->
|
||||
<!-- Copyright 2026 Mocktail Project Authors -->
|
||||
<!-- SPDX-License-Identifier: CC0-1.0 -->
|
||||
<component type="desktop-application">
|
||||
<id>io.github.CoderDayton.nightcap</id>
|
||||
<name>Nightcap</name>
|
||||
<summary>Play Roblox on Linux</summary>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>Apache-2.0</project_license>
|
||||
<developer id="io.github.coderdayton">
|
||||
<name>vii</name>
|
||||
</developer>
|
||||
<branding>
|
||||
<color type="primary" scheme_preference="light">#CEB3EF</color>
|
||||
<color type="primary" scheme_preference="dark">#2F2A6B</color>
|
||||
</branding>
|
||||
<url type="homepage">https://github.com/CoderDayton/nightcap</url>
|
||||
<description>
|
||||
<p>
|
||||
Sign in to Roblox, browse experiences, and play them on Linux.
|
||||
</p>
|
||||
<p>
|
||||
Nightcap is an independent community project, forked from Mocktail. It
|
||||
is not affiliated with Roblox Corporation or VinegarHQ, and the Roblox
|
||||
client is not included.
|
||||
</p>
|
||||
</description>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/88b7382577aa138a2d16619b091f69abcdf7da15/assets/screenshots/flatpak-home.png</image>
|
||||
<caption>Browse Roblox experiences</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/88b7382577aa138a2d16619b091f69abcdf7da15/assets/screenshots/flatpak-gameplay-tower.png</image>
|
||||
<caption>Play Tower of Hell on Linux</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/88b7382577aa138a2d16619b091f69abcdf7da15/assets/screenshots/flatpak-gameplay-lobby.png</image>
|
||||
<caption>Play DOORS on Linux</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<launchable type="desktop-id">io.github.CoderDayton.nightcap.desktop</launchable>
|
||||
<content_rating type="oars-1.1">
|
||||
<content_attribute id="violence-cartoon">moderate</content_attribute>
|
||||
<content_attribute id="violence-fantasy">moderate</content_attribute>
|
||||
<content_attribute id="violence-realistic">moderate</content_attribute>
|
||||
<content_attribute id="social-chat">moderate</content_attribute>
|
||||
<content_attribute id="social-info">moderate</content_attribute>
|
||||
<content_attribute id="social-audio">moderate</content_attribute>
|
||||
<content_attribute id="social-contacts">intense</content_attribute>
|
||||
<content_attribute id="money-purchasing">intense</content_attribute>
|
||||
<content_attribute id="money-advertising">intense</content_attribute>
|
||||
<content_attribute id="money-gambling">moderate</content_attribute>
|
||||
</content_rating>
|
||||
<provides>
|
||||
<binary>mocktail</binary>
|
||||
</provides>
|
||||
<releases>
|
||||
<release version="0.3.3" date="2026-10-04">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Joining a friend from a follow user link now works.</li>
|
||||
<li>Roblox textures decode about 3 times faster, so loading stalls are shorter.</li>
|
||||
<li>A failed Roblox update now restores your previous install instead of leaving it half changed.</li>
|
||||
<li>Fixed crashes and freezes with audio, the text overlay, the mouse lock and saved themes.</li>
|
||||
<li>Going to another page in the web view no longer closes it.</li>
|
||||
<li>Clicking while typing no longer closes the text box.</li>
|
||||
<li>Fixed the sky showing the wrong textures.</li>
|
||||
<li>The Flatpak build on NVIDIA now uses X11 to avoid Wayland hangs.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.3.2" date="2026-09-29">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Added experimental support for ARM64 Linux.</li>
|
||||
<li>Fixed login stalling after passing a verification check.</li>
|
||||
<li>Fixed login check windows staying open after they were solved.</li>
|
||||
<li>Fixed Play on a private server doing nothing until a sidebar tab was clicked.</li>
|
||||
<li>Less stutter while joining a game and while textures load in.</li>
|
||||
<li>Middle click and natural scrolling now work as expected.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.3.1" date="2026-09-20">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Added DEB, RPM, and pacman packages to every release.</li>
|
||||
<li>Smoother frames: textures no longer decode on the thread that submits the frame.</li>
|
||||
<li>Fixed the mouse pointer jumping when a right-click camera drag ended.</li>
|
||||
<li>Fixed the text caret not moving when a trailing space was typed.</li>
|
||||
<li>Faster typing in text boxes.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.3.0" date="2026-09-18">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Added a custom Discord presence: set the title, state, and icons.</li>
|
||||
<li>Added --profile to record a trace of the renderer's work.</li>
|
||||
<li>Faster texture loading: compressed textures decode and resample on worker threads.</li>
|
||||
<li>Lower CPU use while playing.</li>
|
||||
<li>Fixed GameMode pinning the game to a few CPU cores.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.2.0" date="2026-09-16">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Discord Rich Presence is on by default.</li>
|
||||
<li>Added input.raw_mouse for mouse look in the host's own units.</li>
|
||||
<li>Fixed mouse look after a long turn.</li>
|
||||
<li>Fixed a first-run failure with "cannot lock appStorage".</li>
|
||||
<li>Values in fflags.json now win over the performance preset.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.1.0" date="2026-09-15">
|
||||
<description>
|
||||
<ul>
|
||||
<li>First Nightcap release, forked from Mocktail.</li>
|
||||
<li>Added ETC2 and EAC texture emulation on Vulkan.</li>
|
||||
<li>Added PNG texture overrides.</li>
|
||||
<li>Lower input latency.</li>
|
||||
<li>Roblox now sees the real host RAM and display size.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
</releases>
|
||||
</component>
|
||||
@@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Modified by vii from komaruworld/mocktail. See README "About this fork". -->
|
||||
<!-- Copyright 2026 Mocktail Project Authors -->
|
||||
<!-- SPDX-License-Identifier: Apache-2.0 -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -6,7 +7,7 @@
|
||||
height="512"
|
||||
viewBox="96 96 320 320"
|
||||
fill="none">
|
||||
<title>Mocktail</title>
|
||||
<title>Nightcap</title>
|
||||
<mask id="mocktail-mark"
|
||||
mask-type="alpha"
|
||||
maskUnits="userSpaceOnUse"
|
||||
@@ -19,12 +20,12 @@
|
||||
</mask>
|
||||
<g mask="url(#mocktail-mark)">
|
||||
<path d="M461.343 267.246L461.165 265.65L444.708 108.145C444.345 105.047 443.009 102.143 440.894 99.8515C438.769 97.5865 436.001 96.028 432.962 95.3864C429.924 94.7448 426.761 95.051 423.903 96.2637C421.056 97.5364 418.67 99.6554 417.07 102.332L356.5 146L206 108.145L193.521 42.4325C193.5 39.3045 192.493 36.2626 190.644 33.7398C188.774 31.2601 186.189 29.4138 183.237 28.4502C180.285 27.4866 177.108 27.4521 174.135 28.3513C171.161 29.2864 168.555 31.1314 166.685 33.6262L73.5931 161.71L72.6616 163.009C59.2667 181.485 52.2413 203.814 52.6444 226.631C53.0476 249.448 60.8576 271.515 74.8967 289.505L75.0459 289.702L75.453 290.192L142.096 374.97L174.978 417.241L195.02 442.943C197.361 445.962 200.625 448.131 204.315 449.12C208.005 450.109 211.918 449.862 215.455 448.418L245.662 436.181L295.274 416.014L395.981 375.674L396.235 375.562C417.35 366.989 435.112 351.794 446.852 332.262C458.593 312.73 463.678 289.915 461.343 267.246Z"
|
||||
fill="#E24329"/>
|
||||
fill="#2F2A6B"/>
|
||||
<path d="M461.403 267.484L461.225 265.887C433.09 264.216 404.933 268.779 378.766 279.25L234.209 337.935C266.844 379.87 295.27 416.324 295.27 416.324L403.5 418L396.231 375.873C417.371 367.306 435.156 352.105 446.91 332.557C458.664 313.009 463.75 290.172 461.403 267.484Z"
|
||||
fill="#FC6D26"/>
|
||||
fill="#6C4FD1"/>
|
||||
<path d="M142.135 375.279L175.016 417.549L195.058 443.252C197.399 446.271 200.664 448.44 204.354 449.429C208.044 450.418 211.956 450.171 215.493 448.727L245.701 436.489L295.313 416.323C295.313 416.323 266.867 379.774 234.21 337.922C185.002 357.838 142.135 375.279 142.135 375.279Z"
|
||||
fill="#FCA326"/>
|
||||
fill="#A78BFA"/>
|
||||
<path d="M138.341 214.829C120.941 192.647 98.8514 174.589 73.6535 161.948L72.722 163.247C59.3272 181.723 52.3017 204.052 52.7049 226.869C53.1081 249.685 60.918 271.752 74.9572 289.743L75.1064 289.94L41.5 337.851L142.156 375.208C142.156 375.208 184.959 357.839 234.232 337.851L138.341 214.829Z"
|
||||
fill="#FC6D26"/>
|
||||
fill="#6C4FD1"/>
|
||||
</g>
|
||||
</svg>
|
||||
@@ -1,12 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Mocktail
|
||||
GenericName=Roblox Player
|
||||
Comment=Play Roblox on Linux
|
||||
Exec=mocktail %u
|
||||
Icon=space.bigrat.mocktail
|
||||
StartupWMClass=space.bigrat.mocktail
|
||||
Categories=Game;
|
||||
Terminal=false
|
||||
MimeType=x-scheme-handler/roblox;x-scheme-handler/roblox-player;
|
||||
X-Mocktail-Managed=true
|
||||
@@ -1,99 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Copyright 2026 Mocktail Project Authors -->
|
||||
<!-- SPDX-License-Identifier: CC0-1.0 -->
|
||||
<component type="desktop-application">
|
||||
<id>space.bigrat.mocktail</id>
|
||||
<name>Mocktail</name>
|
||||
<summary>Play Roblox on Linux</summary>
|
||||
<metadata_license>CC0-1.0</metadata_license>
|
||||
<project_license>Apache-2.0</project_license>
|
||||
<developer id="space.bigrat">
|
||||
<name>Mocktail Project Authors</name>
|
||||
</developer>
|
||||
<branding>
|
||||
<color type="primary" scheme_preference="light">#CEB3EF</color>
|
||||
<color type="primary" scheme_preference="dark">#2F2A6B</color>
|
||||
</branding>
|
||||
<url type="homepage">https://github.com/komaruworld/mocktail</url>
|
||||
<description>
|
||||
<p>
|
||||
Sign in to Roblox, browse experiences, and play them on Linux.
|
||||
</p>
|
||||
<p>
|
||||
Mocktail is an independent community project. It is not affiliated with
|
||||
Roblox Corporation or VinegarHQ, and the Roblox client is not included.
|
||||
</p>
|
||||
</description>
|
||||
<screenshots>
|
||||
<screenshot type="default">
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/88b7382577aa138a2d16619b091f69abcdf7da15/assets/screenshots/flatpak-home.png</image>
|
||||
<caption>Browse Roblox experiences</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/88b7382577aa138a2d16619b091f69abcdf7da15/assets/screenshots/flatpak-gameplay-tower.png</image>
|
||||
<caption>Play Tower of Hell on Linux</caption>
|
||||
</screenshot>
|
||||
<screenshot>
|
||||
<image>https://raw.githubusercontent.com/komaruworld/mocktail/88b7382577aa138a2d16619b091f69abcdf7da15/assets/screenshots/flatpak-gameplay-lobby.png</image>
|
||||
<caption>Play DOORS on Linux</caption>
|
||||
</screenshot>
|
||||
</screenshots>
|
||||
<launchable type="desktop-id">space.bigrat.mocktail.desktop</launchable>
|
||||
<content_rating type="oars-1.1">
|
||||
<content_attribute id="violence-cartoon">moderate</content_attribute>
|
||||
<content_attribute id="violence-fantasy">moderate</content_attribute>
|
||||
<content_attribute id="violence-realistic">moderate</content_attribute>
|
||||
<content_attribute id="social-chat">moderate</content_attribute>
|
||||
<content_attribute id="social-info">moderate</content_attribute>
|
||||
<content_attribute id="social-audio">moderate</content_attribute>
|
||||
<content_attribute id="social-contacts">intense</content_attribute>
|
||||
<content_attribute id="money-purchasing">intense</content_attribute>
|
||||
<content_attribute id="money-advertising">intense</content_attribute>
|
||||
<content_attribute id="money-gambling">moderate</content_attribute>
|
||||
</content_rating>
|
||||
<provides>
|
||||
<binary>mocktail</binary>
|
||||
</provides>
|
||||
<releases>
|
||||
<release version="1.0.4" date="2026-09-11">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Fixed login issues</li>
|
||||
<li>Fixed low-resolution textures, skybox rendering and HiDPI scaling.</li>
|
||||
<li>Added vc support.</li>
|
||||
<li>Added Xbox and PlayStation gamepad support.</li>
|
||||
<li>Added Nix support</li>
|
||||
<li>Improved text input, fullscreen mode, window sizing, and mouse controls.</li>
|
||||
<li>Updated support for Roblox 2.736.1408.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.0.3" date="2026-08-16">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Fixed issues with downloading Roblox on first launch</li>
|
||||
<li>Added fflags.json for custom FFlags</li>
|
||||
<li>Added a message if your cookies have expired</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.0.2" date="2026-08-14">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Fixed the “Roblox is out of date” bug</li>
|
||||
<li>Improved logging</li>
|
||||
<li>Fixed issues with Discord RPC where the game you were playing wasn’t displayed</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="1.0.1" date="2026-08-13">
|
||||
<description>
|
||||
<ul>
|
||||
<li>Fix issues with GameMode</li>
|
||||
<li>Add Discord RPC support</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="0.1.0" date="2026-08-12"/>
|
||||
</releases>
|
||||
</component>
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
@@ -6,12 +7,11 @@ set -Eeuo pipefail
|
||||
umask 022
|
||||
|
||||
readonly REPOSITORY="${1:?Flatpak repository path is required}"
|
||||
readonly X86_64_BUNDLE="${2:?x86_64 Flatpak bundle path is required}"
|
||||
readonly AARCH64_BUNDLE="${3:?aarch64 Flatpak bundle path is required}"
|
||||
readonly PUBLIC_KEY="${4:?GPG public key path is required}"
|
||||
readonly OUTPUT="${5:?Pages output path is required}"
|
||||
readonly NATIVE_REPOSITORIES="${6:?Native repositories path is required}"
|
||||
readonly BASE_URL="https://mocktail.bigrat.space"
|
||||
readonly BUNDLE="${2:?Flatpak bundle path is required}"
|
||||
readonly PUBLIC_KEY="${3:?GPG public key path is required}"
|
||||
readonly OUTPUT="${4:?Pages output path is required}"
|
||||
readonly BASE_URL="https://coderdayton.github.io/nightcap"
|
||||
readonly APP_ID="io.github.CoderDayton.nightcap"
|
||||
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
readonly PROJECT_DIR="$(dirname -- "${SCRIPT_DIR}")"
|
||||
|
||||
@@ -19,21 +19,12 @@ readonly PROJECT_DIR="$(dirname -- "${SCRIPT_DIR}")"
|
||||
printf 'Flatpak repository is missing or unsafe: %s\n' "${REPOSITORY}" >&2
|
||||
exit 1
|
||||
}
|
||||
for bundle in "${X86_64_BUNDLE}" "${AARCH64_BUNDLE}"; do
|
||||
[[ -f "${bundle}" && ! -L "${bundle}" ]] || {
|
||||
printf 'Flatpak bundle is missing or unsafe: %s\n' "${bundle}" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
[[ -s "${PUBLIC_KEY}" && ! -L "${PUBLIC_KEY}" ]] || {
|
||||
printf 'Flatpak public key is missing or unsafe: %s\n' "${PUBLIC_KEY}" >&2
|
||||
[[ -f "${BUNDLE}" && ! -L "${BUNDLE}" ]] || {
|
||||
printf 'Flatpak bundle is missing or unsafe: %s\n' "${BUNDLE}" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ -d "${NATIVE_REPOSITORIES}/apt" &&
|
||||
-d "${NATIVE_REPOSITORIES}/rpm" &&
|
||||
-d "${NATIVE_REPOSITORIES}/downloads" &&
|
||||
-s "${NATIVE_REPOSITORIES}/mocktail-packages.gpg" ]] || {
|
||||
printf 'Native repositories are missing: %s\n' "${NATIVE_REPOSITORIES}" >&2
|
||||
[[ -s "${PUBLIC_KEY}" && ! -L "${PUBLIC_KEY}" ]] || {
|
||||
printf 'Flatpak public key is missing or unsafe: %s\n' "${PUBLIC_KEY}" >&2
|
||||
exit 1
|
||||
}
|
||||
[[ ! -e "${OUTPUT}" ]] || {
|
||||
@@ -43,51 +34,41 @@ done
|
||||
|
||||
mkdir -p -- "${OUTPUT}"
|
||||
cp -a -- "${REPOSITORY}" "${OUTPUT}/repo"
|
||||
install -m 0644 -- "${X86_64_BUNDLE}" "${OUTPUT}/Mocktail-x86_64.flatpak"
|
||||
install -m 0644 -- "${AARCH64_BUNDLE}" "${OUTPUT}/Mocktail-aarch64.flatpak"
|
||||
install -m 0644 -- "${PUBLIC_KEY}" "${OUTPUT}/mocktail-flatpak.gpg"
|
||||
install -m 0644 -- "${BUNDLE}" "${OUTPUT}/Nightcap-x86_64.flatpak"
|
||||
install -m 0644 -- "${PUBLIC_KEY}" "${OUTPUT}/nightcap-flatpak.gpg"
|
||||
install -m 0644 -- \
|
||||
"${PROJECT_DIR}/packaging/space.bigrat.mocktail.svg" \
|
||||
"${OUTPUT}/mocktail.svg"
|
||||
install -m 0644 -- \
|
||||
"${PROJECT_DIR}/packaging/discord-join.html" \
|
||||
"${OUTPUT}/join.html"
|
||||
install -m 0644 -- "${PROJECT_DIR}/site/index.html" "${OUTPUT}/index.html"
|
||||
install -m 0644 -- "${PROJECT_DIR}/site/styles.css" "${OUTPUT}/styles.css"
|
||||
install -d -- "${OUTPUT}/packaging"
|
||||
install -m 0644 -- \
|
||||
"${PROJECT_DIR}/packaging/space.bigrat.mocktail.svg" \
|
||||
"${OUTPUT}/packaging/space.bigrat.mocktail.svg"
|
||||
cp -a -- "${NATIVE_REPOSITORIES}/apt" "${OUTPUT}/apt"
|
||||
cp -a -- "${NATIVE_REPOSITORIES}/rpm" "${OUTPUT}/rpm"
|
||||
cp -a -- "${NATIVE_REPOSITORIES}/downloads" "${OUTPUT}/downloads"
|
||||
install -m 0644 -- \
|
||||
"${NATIVE_REPOSITORIES}/mocktail-packages.gpg" \
|
||||
"${OUTPUT}/mocktail-packages.gpg"
|
||||
"${PROJECT_DIR}/packaging/${APP_ID}.svg" \
|
||||
"${OUTPUT}/nightcap.svg"
|
||||
touch -- "${OUTPUT}/.nojekyll"
|
||||
|
||||
readonly GPG_KEY="$(base64 --wrap=0 "${PUBLIC_KEY}")"
|
||||
|
||||
cat >"${OUTPUT}/mocktail.flatpakrepo" <<EOF
|
||||
cat >"${OUTPUT}/nightcap.flatpakrepo" <<EOF
|
||||
[Flatpak Repo]
|
||||
Title=Mocktail Nightly
|
||||
Title=Nightcap
|
||||
Url=${BASE_URL}/repo/
|
||||
Homepage=https://github.com/komaruworld/mocktail
|
||||
Comment=Nightly builds of Mocktail
|
||||
Description=Signed x86_64 and aarch64 nightly builds published from the latest Mocktail main branch
|
||||
Icon=${BASE_URL}/mocktail.svg
|
||||
Homepage=https://github.com/CoderDayton/nightcap
|
||||
Comment=Nightcap releases
|
||||
Description=Signed x86_64 release builds of Nightcap
|
||||
Icon=${BASE_URL}/nightcap.svg
|
||||
GPGKey=${GPG_KEY}
|
||||
EOF
|
||||
|
||||
cat >"${OUTPUT}/mocktail.flatpakref" <<EOF
|
||||
cat >"${OUTPUT}/nightcap.flatpakref" <<EOF
|
||||
[Flatpak Ref]
|
||||
Title=Mocktail Nightly
|
||||
Name=space.bigrat.mocktail
|
||||
Title=Nightcap
|
||||
Name=${APP_ID}
|
||||
Branch=stable
|
||||
Url=${BASE_URL}/repo/
|
||||
RuntimeRepo=https://dl.flathub.org/repo/flathub.flatpakrepo
|
||||
Homepage=https://github.com/komaruworld/mocktail
|
||||
Comment=Nightly build from the latest main branch
|
||||
Icon=${BASE_URL}/mocktail.svg
|
||||
Homepage=https://github.com/CoderDayton/nightcap
|
||||
Comment=Roblox on Linux, tuned for a real PC
|
||||
Icon=${BASE_URL}/nightcap.svg
|
||||
GPGKey=${GPG_KEY}
|
||||
IsRuntime=false
|
||||
EOF
|
||||
|
||||
install -m 0644 -- "${PROJECT_DIR}/site/index.html" "${OUTPUT}/index.html"
|
||||
install -m 0644 -- "${PROJECT_DIR}/site/styles.css" "${OUTPUT}/styles.css"
|
||||
install -m 0644 -- "${PROJECT_DIR}/site/terms.html" "${OUTPUT}/terms.html"
|
||||
install -m 0644 -- "${PROJECT_DIR}/site/privacy.html" "${OUTPUT}/privacy.html"
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
@@ -8,8 +9,8 @@ readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||
readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd -P)"
|
||||
|
||||
BUILD_DIR="${PROJECT_ROOT}/build-flatpak"
|
||||
MANIFEST="${PROJECT_ROOT}/packaging/flatpak/space.bigrat.mocktail.json"
|
||||
JOBS="${MOCKTAIL_FLATPAK_JOBS:-4}"
|
||||
MANIFEST="${PROJECT_ROOT}/packaging/flatpak/io.github.CoderDayton.nightcap.json"
|
||||
JOBS="${MOCKTAIL_FLATPAK_JOBS:-2}"
|
||||
|
||||
Usage() {
|
||||
cat <<'EOF'
|
||||
@@ -20,7 +21,7 @@ Build and install Mocktail as a per-user Flatpak for the host architecture.
|
||||
Options:
|
||||
--build-dir DIR Builder output directory (default: build-flatpak).
|
||||
--manifest FILE Flatpak manifest path.
|
||||
--jobs N Maximum parallel build jobs (default: 4).
|
||||
--jobs N Maximum parallel build jobs (default: 2).
|
||||
-h, --help Show this help.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
readonly project_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
readonly source_icon="${project_root}/packaging/space.bigrat.mocktail.svg"
|
||||
readonly source_icon="${project_root}/packaging/io.github.CoderDayton.nightcap.svg"
|
||||
readonly output_root="${project_root}/packaging/icons/hicolor"
|
||||
readonly icon_name="space.bigrat.mocktail.png"
|
||||
readonly icon_name="io.github.CoderDayton.nightcap.png"
|
||||
readonly icon_sizes=(16 22 24 32 36 48 64 72 96 128 192 256 512)
|
||||
|
||||
if ! command -v magick >/dev/null 2>&1; then
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Modified by vii from komaruworld/mocktail. See README "About this fork".
|
||||
# Copyright 2026 Mocktail Project Authors
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
@@ -152,8 +153,8 @@ AnyLinuxDeploy() {
|
||||
local -a deployment_environment=(
|
||||
"APPDIR=${app_dir}" "MAIN_BIN=mocktail" "LIB_DIR=/usr/lib"
|
||||
"TMPDIR=${ANYLINUX_WORK}/tmp"
|
||||
"DESKTOP=/usr/share/applications/space.bigrat.mocktail.desktop"
|
||||
"ICON=/usr/share/icons/hicolor/scalable/apps/space.bigrat.mocktail.svg"
|
||||
"DESKTOP=/usr/share/applications/io.github.CoderDayton.nightcap.desktop"
|
||||
"ICON=/usr/share/icons/hicolor/scalable/apps/io.github.CoderDayton.nightcap.svg"
|
||||
"DEPLOY_DATADIR=0" "DEPLOY_LOCALE=0" "DEPLOY_VULKAN=0"
|
||||
"STRACE_MODE=1" "STRACE_BINARY=mocktail" "STRACE_FLAGS=--help"
|
||||
"STRACE_TIME=1"
|
||||
|
||||